diff --git a/js/.eslintrc b/js/.eslintrc index 97167e2cb06..59ba10717d1 100644 --- a/js/.eslintrc +++ b/js/.eslintrc @@ -16,7 +16,6 @@ "consistent-return": 0, "func-style": 0, "guard-for-in": 0, /*@todo: Make the each function handle objects, then run a guarded for-in there.*/ - "indent": ["error", "tab"], "lines-around-comment": 0, "max-len": [ "error", { @@ -33,7 +32,6 @@ "no-multi-spaces": 0, /*Should be fixed*/ "no-nested-ternary": 0, "no-shadow": 0, /*Same variable names in nested scopes. @todo: Fix this, it is useful*/ - "no-trailing-spaces": 0, "no-undefined": 0, "no-underscore-dangle": 0, /*@todo: Check this*/ "object-curly-spacing": [2, "always"], diff --git a/js/indicators/accumulation-distribution.src.js b/js/indicators/accumulation-distribution.src.js index fd1e8e55b50..119890f16ce 100755 --- a/js/indicators/accumulation-distribution.src.js +++ b/js/indicators/accumulation-distribution.src.js @@ -6,18 +6,18 @@ var seriesType = H.seriesType; // Utils: function populateAverage(xVal, yVal, yValVolume, i) { - var high = yVal[i][1], - low = yVal[i][2], - close = yVal[i][3], - volume = yValVolume[i], - adY = close === high && close === low || high === low ? - 0 : - ((2 * close - low - high) / (high - low)) * volume, - adX = xVal[i]; - - return [adX, adY]; + var high = yVal[i][1], + low = yVal[i][2], + close = yVal[i][3], + volume = yValVolume[i], + adY = close === high && close === low || high === low ? + 0 : + ((2 * close - low - high) / (high - low)) * volume, + adX = xVal[i]; + + return [adX, adY]; } - + /** * The AD series type. * @@ -25,89 +25,89 @@ function populateAverage(xVal, yVal, yValVolume, i) { * @augments seriesTypes.sma */ seriesType('ad', 'sma', - /** - * Accumulation Distribution (AD). This series requires `linkedTo` option to - * be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/accumulation-distribution - * Accumulation/Distribution indicator - * @since 6.0.0 - * @optionparent plotOptions.ad - */ - { - params: { - /** - * The id of volume series which is mandatory. - * For example using OHLC data, volumeSeriesID='volume' means - * the indicator will be calculated using OHLC and volume values. - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - volumeSeriesID: 'volume' - } - }, { - nameComponents: false, - nameBase: 'Accumulation/Distribution', - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - volumeSeriesID = params.volumeSeriesID, - volumeSeries = series.chart.get(volumeSeriesID), - yValVolume = volumeSeries && volumeSeries.yData, - yValLen = yVal ? yVal.length : 0, - AD = [], - xData = [], - yData = [], - len, i, ADPoint; + /** + * Accumulation Distribution (AD). This series requires `linkedTo` option to + * be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/accumulation-distribution + * Accumulation/Distribution indicator + * @since 6.0.0 + * @optionparent plotOptions.ad + */ + { + params: { + /** + * The id of volume series which is mandatory. + * For example using OHLC data, volumeSeriesID='volume' means + * the indicator will be calculated using OHLC and volume values. + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + volumeSeriesID: 'volume' + } + }, { + nameComponents: false, + nameBase: 'Accumulation/Distribution', + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + volumeSeriesID = params.volumeSeriesID, + volumeSeries = series.chart.get(volumeSeriesID), + yValVolume = volumeSeries && volumeSeries.yData, + yValLen = yVal ? yVal.length : 0, + AD = [], + xData = [], + yData = [], + len, i, ADPoint; + + if (xVal.length <= period && yValLen && yVal[0].length !== 4) { + return false; + } + + if (!volumeSeries) { + return H.error( + 'Series ' + + volumeSeriesID + + ' not found! Check `volumeSeriesID`.', + true + ); + } - if (xVal.length <= period && yValLen && yVal[0].length !== 4) { - return false; - } + // i = period <-- skip first N-points + // Calculate value one-by-one for each period in visible data + for (i = period; i < yValLen; i++) { - if (!volumeSeries) { - return H.error( - 'Series ' + - volumeSeriesID + - ' not found! Check `volumeSeriesID`.', - true - ); - } - - // i = period <-- skip first N-points - // Calculate value one-by-one for each period in visible data - for (i = period; i < yValLen; i++) { - - len = AD.length; - ADPoint = populateAverage(xVal, yVal, yValVolume, i, period); - - if (len > 0) { - ADPoint[1] += AD[len - 1][1]; - ADPoint[1] = ADPoint[1]; - } - - AD.push(ADPoint); - - xData.push(ADPoint[0]); - yData.push(ADPoint[1]); - } + len = AD.length; + ADPoint = populateAverage(xVal, yVal, yValVolume, i, period); - return { - values: AD, - xData: xData, - yData: yData - }; - } - }); + if (len > 0) { + ADPoint[1] += AD[len - 1][1]; + ADPoint[1] = ADPoint[1]; + } + + AD.push(ADPoint); + + xData.push(ADPoint[0]); + yData.push(ADPoint[1]); + } + + return { + values: AD, + xData: xData, + yData: yData + }; + } + }); /** * A `AD` series. If the [type](#series.ad.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.ad diff --git a/js/indicators/atr.src.js b/js/indicators/atr.src.js index 822077bf38d..54b57bf4130 100755 --- a/js/indicators/atr.src.js +++ b/js/indicators/atr.src.js @@ -3,36 +3,36 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var isArray = H.isArray, - seriesType = H.seriesType, - UNDEFINED; + seriesType = H.seriesType, + UNDEFINED; // Utils: function accumulateAverage(points, xVal, yVal, i) { - var xValue = xVal[i], - yValue = yVal[i]; - - points.push([xValue, yValue]); + var xValue = xVal[i], + yValue = yVal[i]; + + points.push([xValue, yValue]); } function getTR(currentPoint, prevPoint) { - var pointY = currentPoint, - prevY = prevPoint, - HL = pointY[1] - pointY[2], - HCp = prevY === UNDEFINED ? 0 : Math.abs(pointY[1] - prevY[3]), - LCp = prevY === UNDEFINED ? 0 : Math.abs(pointY[2] - prevY[3]), - TR = Math.max(HL, HCp, LCp); - - return TR; + var pointY = currentPoint, + prevY = prevPoint, + HL = pointY[1] - pointY[2], + HCp = prevY === UNDEFINED ? 0 : Math.abs(pointY[1] - prevY[3]), + LCp = prevY === UNDEFINED ? 0 : Math.abs(pointY[2] - prevY[3]), + TR = Math.max(HL, HCp, LCp); + + return TR; } function populateAverage(points, xVal, yVal, i, period, prevATR) { - var x = xVal[i - 1], - TR = getTR(yVal[i - 1], yVal[i - 2]), - y; + var x = xVal[i - 1], + TR = getTR(yVal[i - 1], yVal[i - 2]), + y; + + y = (((prevATR * (period - 1)) + TR) / period); - y = (((prevATR * (period - 1)) + TR) / period); - - return [x, y]; + return [x, y]; } /** * The ATR series type. @@ -40,89 +40,89 @@ function populateAverage(points, xVal, yVal, i, period, prevATR) { * @constructor seriesTypes.atr * @augments seriesTypes.sma */ -seriesType('atr', 'sma', - /** - * Average true range indicator (ATR). This series requires `linkedTo` - * option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/atr ATR indicator - * @since 6.0.0 - * @optionparent plotOptions.atr - */ - { - params: { - period: 14 - } - }, { - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - xValue = xVal[0], - yValue = yVal[0], - range = 1, - prevATR = 0, - TR = 0, - ATR = [], - xData = [], - yData = [], - point, i, points; - - points = [[xValue, yValue]]; - - if ( - (xVal.length <= period) || !isArray(yVal[0]) || - yVal[0].length !== 4 - ) { - return false; - } - - for (i = 1; i <= yValLen; i++) { - - accumulateAverage(points, xVal, yVal, i); - - if (period < range) { - point = populateAverage( - points, - xVal, - yVal, - i, - period, - prevATR - ); - prevATR = point[1]; - ATR.push(point); - xData.push(point[0]); - yData.push(point[1]); - - } else if (period === range) { - prevATR = TR / (i - 1); - ATR.push([xVal[i - 1], prevATR]); - xData.push(xVal[i - 1]); - yData.push(prevATR); - range++; - } else { - TR += getTR(yVal[i - 1], yVal[i - 2]); - range++; - } - } - - return { - values: ATR, - xData: xData, - yData: yData - }; - } - - }); +seriesType('atr', 'sma', + /** + * Average true range indicator (ATR). This series requires `linkedTo` + * option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/atr ATR indicator + * @since 6.0.0 + * @optionparent plotOptions.atr + */ + { + params: { + period: 14 + } + }, { + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + xValue = xVal[0], + yValue = yVal[0], + range = 1, + prevATR = 0, + TR = 0, + ATR = [], + xData = [], + yData = [], + point, i, points; + + points = [[xValue, yValue]]; + + if ( + (xVal.length <= period) || !isArray(yVal[0]) || + yVal[0].length !== 4 + ) { + return false; + } + + for (i = 1; i <= yValLen; i++) { + + accumulateAverage(points, xVal, yVal, i); + + if (period < range) { + point = populateAverage( + points, + xVal, + yVal, + i, + period, + prevATR + ); + prevATR = point[1]; + ATR.push(point); + xData.push(point[0]); + yData.push(point[1]); + + } else if (period === range) { + prevATR = TR / (i - 1); + ATR.push([xVal[i - 1], prevATR]); + xData.push(xVal[i - 1]); + yData.push(prevATR); + range++; + } else { + TR += getTR(yVal[i - 1], yVal[i - 2]); + range++; + } + } + + return { + values: ATR, + xData: xData, + yData: yData + }; + } + + }); /** * A `ATR` series. If the [type](#series.atr.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.atr diff --git a/js/indicators/bollinger-bands.src.js b/js/indicators/bollinger-bands.src.js index 2c899b34bee..913fe9a498c 100644 --- a/js/indicators/bollinger-bands.src.js +++ b/js/indicators/bollinger-bands.src.js @@ -4,262 +4,262 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var each = H.each, - merge = H.merge, - isArray = H.isArray, - SMA = H.seriesTypes.sma; + merge = H.merge, + isArray = H.isArray, + SMA = H.seriesTypes.sma; // Utils: function getStandardDeviation(arr, index, isOHLC, mean) { - var variance = 0, - arrLen = arr.length, - std = 0, - i = 0, - value; + var variance = 0, + arrLen = arr.length, + std = 0, + i = 0, + value; - for (; i < arrLen; i++) { - value = (isOHLC ? arr[i][index] : arr[i]) - mean; - variance += value * value; - } - variance = variance / (arrLen - 1); + for (; i < arrLen; i++) { + value = (isOHLC ? arr[i][index] : arr[i]) - mean; + variance += value * value; + } + variance = variance / (arrLen - 1); - std = Math.sqrt(variance); - return std; + std = Math.sqrt(variance); + return std; } H.seriesType('bb', 'sma', - /** - * Bollinger bands (BB). This series requires the `linkedTo` option to be - * set and should be loaded after the `stock/indicators/indicators.js` file. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/bollinger-bands - * Bollinger bands - * @since 6.0.0 - * @optionparent plotOptions.bb - */ - { - name: 'BB (20, 2)', - params: { - period: 20, - /** - * Standard deviation for top and bottom bands. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - standardDeviation: 2, - index: 3 - }, - /** - * Bottom line options. - * - * @since 6.0.0 - * @product highstock - */ - bottomLine: { - /** - * Styles for a bottom line. - * - * @since 6.0.0 - * @product highstock - */ - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. If not set, it's inherited from - * [plotOptions.bb.color](#plotOptions.bb.color). - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * Top line options. - * - * @extends {plotOptions.bb.bottomLine} - * @since 6.0.0 - * @product highstock - */ - topLine: { - styles: { - lineWidth: 1, - lineColor: undefined - } - }, - tooltip: { - pointFormat: '\u25CF {series.name}
Top: {point.top}
Middle: {point.middle}
Bottom: {point.bottom}
' - }, - marker: { - enabled: false - }, - dataGrouping: { - approximation: 'averages' - } - }, /** @lends Highcharts.Series.prototype */ { - pointArrayMap: ['top', 'middle', 'bottom'], - pointValKey: 'middle', - nameComponents: ['period', 'standardDeviation'], - init: function () { - SMA.prototype.init.apply(this, arguments); + /** + * Bollinger bands (BB). This series requires the `linkedTo` option to be + * set and should be loaded after the `stock/indicators/indicators.js` file. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/bollinger-bands + * Bollinger bands + * @since 6.0.0 + * @optionparent plotOptions.bb + */ + { + name: 'BB (20, 2)', + params: { + period: 20, + /** + * Standard deviation for top and bottom bands. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + standardDeviation: 2, + index: 3 + }, + /** + * Bottom line options. + * + * @since 6.0.0 + * @product highstock + */ + bottomLine: { + /** + * Styles for a bottom line. + * + * @since 6.0.0 + * @product highstock + */ + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. If not set, it's inherited from + * [plotOptions.bb.color](#plotOptions.bb.color). + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * Top line options. + * + * @extends {plotOptions.bb.bottomLine} + * @since 6.0.0 + * @product highstock + */ + topLine: { + styles: { + lineWidth: 1, + lineColor: undefined + } + }, + tooltip: { + pointFormat: '\u25CF {series.name}
Top: {point.top}
Middle: {point.middle}
Bottom: {point.bottom}
' + }, + marker: { + enabled: false + }, + dataGrouping: { + approximation: 'averages' + } + }, /** @lends Highcharts.Series.prototype */ { + pointArrayMap: ['top', 'middle', 'bottom'], + pointValKey: 'middle', + nameComponents: ['period', 'standardDeviation'], + init: function () { + SMA.prototype.init.apply(this, arguments); - // Set default color for lines: - this.options = merge({ - topLine: { - styles: { - lineColor: this.color - } - }, - bottomLine: { - styles: { - lineColor: this.color - } - } - }, this.options); - }, - toYData: function (point) { - return [point.top, point.middle, point.bottom]; - }, - translate: function () { - var indicator = this, - translatedBB = ['plotTop', 'plotMiddle', 'plotBottom']; + // Set default color for lines: + this.options = merge({ + topLine: { + styles: { + lineColor: this.color + } + }, + bottomLine: { + styles: { + lineColor: this.color + } + } + }, this.options); + }, + toYData: function (point) { + return [point.top, point.middle, point.bottom]; + }, + translate: function () { + var indicator = this, + translatedBB = ['plotTop', 'plotMiddle', 'plotBottom']; - SMA.prototype.translate.apply(indicator, arguments); + SMA.prototype.translate.apply(indicator, arguments); - each(indicator.points, function (point) { - each( - [point.top, point.middle, point.bottom], - function (value, i) { - if (value !== null) { - point[translatedBB[i]] = indicator.yAxis.toPixels( - value, - true - ); - } - } - ); - }); - }, - drawGraph: function () { - var indicator = this, - middleLinePoints = indicator.points, - pointsLength = middleLinePoints.length, - middleLineOptions = indicator.options, - middleLinePath = indicator.graph, - gappedExtend = { - options: { - gapSize: middleLineOptions.gapSize - } - }, - deviations = [[], []], // top and bottom point place holders - point; + each(indicator.points, function (point) { + each( + [point.top, point.middle, point.bottom], + function (value, i) { + if (value !== null) { + point[translatedBB[i]] = indicator.yAxis.toPixels( + value, + true + ); + } + } + ); + }); + }, + drawGraph: function () { + var indicator = this, + middleLinePoints = indicator.points, + pointsLength = middleLinePoints.length, + middleLineOptions = indicator.options, + middleLinePath = indicator.graph, + gappedExtend = { + options: { + gapSize: middleLineOptions.gapSize + } + }, + deviations = [[], []], // top and bottom point place holders + point; - // Generate points for top and bottom lines: - while (pointsLength--) { - point = middleLinePoints[pointsLength]; - deviations[0].push({ - plotX: point.plotX, - plotY: point.plotTop, - isNull: point.isNull - }); - deviations[1].push({ - plotX: point.plotX, - plotY: point.plotBottom, - isNull: point.isNull - }); - } + // Generate points for top and bottom lines: + while (pointsLength--) { + point = middleLinePoints[pointsLength]; + deviations[0].push({ + plotX: point.plotX, + plotY: point.plotTop, + isNull: point.isNull + }); + deviations[1].push({ + plotX: point.plotX, + plotY: point.plotBottom, + isNull: point.isNull + }); + } - // Modify options and generate lines: - each(['topLine', 'bottomLine'], function (lineName, i) { - indicator.points = deviations[i]; - indicator.options = merge( - middleLineOptions[lineName].styles, - gappedExtend - ); - indicator.graph = indicator['graph' + lineName]; - SMA.prototype.drawGraph.call(indicator); + // Modify options and generate lines: + each(['topLine', 'bottomLine'], function (lineName, i) { + indicator.points = deviations[i]; + indicator.options = merge( + middleLineOptions[lineName].styles, + gappedExtend + ); + indicator.graph = indicator['graph' + lineName]; + SMA.prototype.drawGraph.call(indicator); - // Now save lines: - indicator['graph' + lineName] = indicator.graph; - }); + // Now save lines: + indicator['graph' + lineName] = indicator.graph; + }); - // Restore options and draw a middle line: - indicator.points = middleLinePoints; - indicator.options = middleLineOptions; - indicator.graph = middleLinePath; - SMA.prototype.drawGraph.call(indicator); - }, - getValues: function (series, params) { - var period = params.period, - standardDeviation = params.standardDeviation, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - BB = [], // 0- date, 1-middle line, 2-top line, 3-bottom line - ML, TL, BL, // middle line, top line and bottom line - date, - xData = [], - yData = [], - slicedX, - slicedY, - stdDev, - isOHLC, - point, - i; + // Restore options and draw a middle line: + indicator.points = middleLinePoints; + indicator.options = middleLineOptions; + indicator.graph = middleLinePath; + SMA.prototype.drawGraph.call(indicator); + }, + getValues: function (series, params) { + var period = params.period, + standardDeviation = params.standardDeviation, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + BB = [], // 0- date, 1-middle line, 2-top line, 3-bottom line + ML, TL, BL, // middle line, top line and bottom line + date, + xData = [], + yData = [], + slicedX, + slicedY, + stdDev, + isOHLC, + point, + i; - if (xVal.length < period) { - return false; - } + if (xVal.length < period) { + return false; + } - isOHLC = isArray(yVal[0]); + isOHLC = isArray(yVal[0]); - for (i = period; i <= yValLen; i++) { - slicedX = xVal.slice(i - period, i); - slicedY = yVal.slice(i - period, i); + for (i = period; i <= yValLen; i++) { + slicedX = xVal.slice(i - period, i); + slicedY = yVal.slice(i - period, i); - point = SMA.prototype.getValues.call( - this, - { - xData: slicedX, - yData: slicedY - }, - params - ); + point = SMA.prototype.getValues.call( + this, + { + xData: slicedX, + yData: slicedY + }, + params + ); - date = point.xData[0]; - ML = point.yData[0]; - stdDev = getStandardDeviation( - slicedY, - params.index, - isOHLC, - ML - ); - TL = ML + standardDeviation * stdDev; - BL = ML - standardDeviation * stdDev; + date = point.xData[0]; + ML = point.yData[0]; + stdDev = getStandardDeviation( + slicedY, + params.index, + isOHLC, + ML + ); + TL = ML + standardDeviation * stdDev; + BL = ML - standardDeviation * stdDev; - BB.push([date, TL, ML, BL]); - xData.push(date); - yData.push([TL, ML, BL]); - } + BB.push([date, TL, ML, BL]); + xData.push(date); + yData.push([TL, ML, BL]); + } - return { - values: BB, - xData: xData, - yData: yData - }; - } - } + return { + values: BB, + xData: xData, + yData: yData + }; + } + } ); /** diff --git a/js/indicators/cci.src.js b/js/indicators/cci.src.js index a3326835a64..de286aba239 100644 --- a/js/indicators/cci.src.js +++ b/js/indicators/cci.src.js @@ -3,25 +3,25 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var isArray = H.isArray, - seriesType = H.seriesType; + seriesType = H.seriesType; // Utils: function sumArray(array) { - return H.reduce(array, function (prev, cur) { - return prev + cur; - }, 0); + return H.reduce(array, function (prev, cur) { + return prev + cur; + }, 0); } function meanDeviation(arr, sma) { - var len = arr.length, - sum = 0, - i; + var len = arr.length, + sum = 0, + i; - for (i = 0; i < len; i++) { - sum += Math.abs(sma - (arr[i])); - } + for (i = 0; i < len; i++) { + sum += Math.abs(sma - (arr[i])); + } - return sum; + return sum; } /** @@ -30,80 +30,80 @@ function meanDeviation(arr, sma) { * @constructor seriesTypes.cci * @augments seriesTypes.sma */ -seriesType('cci', 'sma', - /** - * Commodity Channel Index (CCI). This series requires `linkedTo` option to - * be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/cci CCI indicator - * @since 6.0.0 - * @optionparent plotOptions.cci - */ - { - params: { - period: 14 - } - }, { - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - TP = [], - periodTP = [], - range = 1, - CCI = [], - xData = [], - yData = [], - CCIPoint, p, len, smaTP, TPtemp, meanDev, i; - - // CCI requires close value - if ( - xVal.length <= period || - !isArray(yVal[0]) || - yVal[0].length !== 4 - ) { - return false; - } - - // accumulate first N-points - while (range < period) { - p = yVal[range - 1]; - TP.push((p[1] + p[2] + p[3]) / 3); - range++; - } - - for (i = period; i <= yValLen; i++) { - - p = yVal[i - 1]; - TPtemp = (p[1] + p[2] + p[3]) / 3; - len = TP.push(TPtemp); - periodTP = TP.slice(len - period); - - smaTP = sumArray(periodTP) / period; - meanDev = meanDeviation(periodTP, smaTP) / period; - - CCIPoint = ((TPtemp - smaTP) / (0.015 * meanDev)); - - CCI.push([xVal[i - 1], CCIPoint]); - xData.push(xVal[i - 1]); - yData.push(CCIPoint); - } - - return { - values: CCI, - xData: xData, - yData: yData - }; - } - }); +seriesType('cci', 'sma', + /** + * Commodity Channel Index (CCI). This series requires `linkedTo` option to + * be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/cci CCI indicator + * @since 6.0.0 + * @optionparent plotOptions.cci + */ + { + params: { + period: 14 + } + }, { + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + TP = [], + periodTP = [], + range = 1, + CCI = [], + xData = [], + yData = [], + CCIPoint, p, len, smaTP, TPtemp, meanDev, i; + + // CCI requires close value + if ( + xVal.length <= period || + !isArray(yVal[0]) || + yVal[0].length !== 4 + ) { + return false; + } + + // accumulate first N-points + while (range < period) { + p = yVal[range - 1]; + TP.push((p[1] + p[2] + p[3]) / 3); + range++; + } + + for (i = period; i <= yValLen; i++) { + + p = yVal[i - 1]; + TPtemp = (p[1] + p[2] + p[3]) / 3; + len = TP.push(TPtemp); + periodTP = TP.slice(len - period); + + smaTP = sumArray(periodTP) / period; + meanDev = meanDeviation(periodTP, smaTP) / period; + + CCIPoint = ((TPtemp - smaTP) / (0.015 * meanDev)); + + CCI.push([xVal[i - 1], CCIPoint]); + xData.push(xVal[i - 1]); + yData.push(CCIPoint); + } + + return { + values: CCI, + xData: xData, + yData: yData + }; + } + }); /** * A `CCI` series. If the [type](#series.cci.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.cci diff --git a/js/indicators/cmf.src.js b/js/indicators/cmf.src.js index 2f1d22d6f4b..dbcb611f0f3 100644 --- a/js/indicators/cmf.src.js +++ b/js/indicators/cmf.src.js @@ -10,7 +10,7 @@ 'use strict'; import H from '../parts/Globals.js'; -H.seriesType('cmf', 'sma', +H.seriesType('cmf', 'sma', /** * Chaikin Money Flow indicator (cmf). * @@ -23,196 +23,196 @@ H.seriesType('cmf', 'sma', * @excluding animationLimit * @optionparent plotOptions.cmf */ - { - params: { - period: 14, + { + params: { + period: 14, /** * The id of another series to use its data as volume data for the * indiator calculation. */ - volumeSeriesID: 'volume' - } - }, { - nameBase: 'Chaikin Money Flow', - /** - * Checks if the series and volumeSeries are accessible, number of - * points.x is longer than period, is series has OHLC data - * @returns {Boolean} - * true if series is valid and can be computed, otherwise false - **/ - isValid: function () { - var chart = this.chart, - options = this.options, - series = this.linkedParent, - volumeSeries = ( - this.volumeSeries || - ( - this.volumeSeries = - chart.get(options.params.volumeSeriesID) - ) - ), - isSeriesOHLC = ( - series && - series.yData && - series.yData[0].length === 4 - ); - - function isLengthValid(serie) { - return serie.xData && - serie.xData.length >= options.params.period; - } - - return !!( - series && - volumeSeries && - isLengthValid(series) && - isLengthValid(volumeSeries) && isSeriesOHLC - ); - }, - - /** - * @typedef {Object} Values - * @property {Number[][]} values - * Combined xData and yData values into a tuple - * @property {Number[]} xData - * Values represent x timestamp values - * @property {Number[]} yData - * Values represent y values - **/ - - /** - * Returns indicator's data - * @returns {False | Values} - * Returns false if the indicator is not valid, otherwise - * returns Values object - **/ - getValues: function (series, params) { - if (!this.isValid()) { - return false; - } - - return this.getMoneyFlow( - series.xData, - series.yData, - this.volumeSeries.yData, - params.period - ); - }, - - /** - * @static - * @param {Number[]} xData x timestamp values - * @param {Number[]} seriesYData yData of basic series - * @param {Number[]} volumeSeriesYData yData of volume series - * @param {Number} period indicator's param - * @returns {Values} object containing computed money flow data - **/ - getMoneyFlow: function (xData, seriesYData, volumeSeriesYData, period) { - var len = seriesYData.length, - moneyFlowVolume = [], - sumVolume = 0, - sumMoneyFlowVolume = 0, - moneyFlowXData = [], - moneyFlowYData = [], - values = [], - i, - point, - nullIndex = -1; - - /** - * Calculates money flow volume, changes i, nullIndex vars from upper - * scope! - * @private - * @param {Number[]} ohlc OHLC point - * @param {Number} volume Volume point's y value - * @returns {Number} volume * moneyFlowMultiplier - **/ - function getMoneyFlowVolume(ohlc, volume) { - var high = ohlc[1], - low = ohlc[2], - close = ohlc[3], - - isValid = - volume !== null && - high !== null && - low !== null && - close !== null && - high !== low; - - - /** - * @private - * @param {Number} h High value - * @param {Number} l Low value - * @param {Number} c Close value - * @returns {Number} calculated multiplier for the point - **/ - function getMoneyFlowMultiplier(h, l, c) { - return ((c - l) - (h - c)) / (h - l); - } - - return isValid ? - getMoneyFlowMultiplier(high, low, close) * volume : - ((nullIndex = i), null); - } - - - if (period > 0 && period <= len) { - for (i = 0; i < period; i++) { - moneyFlowVolume[i] = getMoneyFlowVolume( - seriesYData[i], - volumeSeriesYData[i] - ); - sumVolume += volumeSeriesYData[i]; - sumMoneyFlowVolume += moneyFlowVolume[i]; - } - - moneyFlowXData.push(xData[i - 1]); - moneyFlowYData.push( - i - nullIndex >= period && sumVolume !== 0 ? - sumMoneyFlowVolume / sumVolume : - null - ); - values.push([moneyFlowXData[0], moneyFlowYData[0]]); - - for (; i < len; i++) { - moneyFlowVolume[i] = getMoneyFlowVolume( - seriesYData[i], - volumeSeriesYData[i] - ); - - sumVolume -= volumeSeriesYData[i - period]; - sumVolume += volumeSeriesYData[i]; - - sumMoneyFlowVolume -= moneyFlowVolume[i - period]; - sumMoneyFlowVolume += moneyFlowVolume[i]; - - point = [ - xData[i], - i - nullIndex >= period ? - sumMoneyFlowVolume / sumVolume : - null - ]; - - moneyFlowXData.push(point[0]); - moneyFlowYData.push(point[1]); - values.push([point[0], point[1]]); - } - } - - return { - values: values, - xData: moneyFlowXData, - yData: moneyFlowYData - }; - } - }); + volumeSeriesID: 'volume' + } + }, { + nameBase: 'Chaikin Money Flow', + /** + * Checks if the series and volumeSeries are accessible, number of + * points.x is longer than period, is series has OHLC data + * @returns {Boolean} + * true if series is valid and can be computed, otherwise false + **/ + isValid: function () { + var chart = this.chart, + options = this.options, + series = this.linkedParent, + volumeSeries = ( + this.volumeSeries || + ( + this.volumeSeries = + chart.get(options.params.volumeSeriesID) + ) + ), + isSeriesOHLC = ( + series && + series.yData && + series.yData[0].length === 4 + ); + + function isLengthValid(serie) { + return serie.xData && + serie.xData.length >= options.params.period; + } + + return !!( + series && + volumeSeries && + isLengthValid(series) && + isLengthValid(volumeSeries) && isSeriesOHLC + ); + }, + + /** + * @typedef {Object} Values + * @property {Number[][]} values + * Combined xData and yData values into a tuple + * @property {Number[]} xData + * Values represent x timestamp values + * @property {Number[]} yData + * Values represent y values + **/ + + /** + * Returns indicator's data + * @returns {False | Values} + * Returns false if the indicator is not valid, otherwise + * returns Values object + **/ + getValues: function (series, params) { + if (!this.isValid()) { + return false; + } + + return this.getMoneyFlow( + series.xData, + series.yData, + this.volumeSeries.yData, + params.period + ); + }, + + /** + * @static + * @param {Number[]} xData x timestamp values + * @param {Number[]} seriesYData yData of basic series + * @param {Number[]} volumeSeriesYData yData of volume series + * @param {Number} period indicator's param + * @returns {Values} object containing computed money flow data + **/ + getMoneyFlow: function (xData, seriesYData, volumeSeriesYData, period) { + var len = seriesYData.length, + moneyFlowVolume = [], + sumVolume = 0, + sumMoneyFlowVolume = 0, + moneyFlowXData = [], + moneyFlowYData = [], + values = [], + i, + point, + nullIndex = -1; + + /** + * Calculates money flow volume, changes i, nullIndex vars from + * upper scope! + * @private + * @param {Number[]} ohlc OHLC point + * @param {Number} volume Volume point's y value + * @returns {Number} volume * moneyFlowMultiplier + **/ + function getMoneyFlowVolume(ohlc, volume) { + var high = ohlc[1], + low = ohlc[2], + close = ohlc[3], + + isValid = + volume !== null && + high !== null && + low !== null && + close !== null && + high !== low; + + + /** + * @private + * @param {Number} h High value + * @param {Number} l Low value + * @param {Number} c Close value + * @returns {Number} calculated multiplier for the point + **/ + function getMoneyFlowMultiplier(h, l, c) { + return ((c - l) - (h - c)) / (h - l); + } + + return isValid ? + getMoneyFlowMultiplier(high, low, close) * volume : + ((nullIndex = i), null); + } + + + if (period > 0 && period <= len) { + for (i = 0; i < period; i++) { + moneyFlowVolume[i] = getMoneyFlowVolume( + seriesYData[i], + volumeSeriesYData[i] + ); + sumVolume += volumeSeriesYData[i]; + sumMoneyFlowVolume += moneyFlowVolume[i]; + } + + moneyFlowXData.push(xData[i - 1]); + moneyFlowYData.push( + i - nullIndex >= period && sumVolume !== 0 ? + sumMoneyFlowVolume / sumVolume : + null + ); + values.push([moneyFlowXData[0], moneyFlowYData[0]]); + + for (; i < len; i++) { + moneyFlowVolume[i] = getMoneyFlowVolume( + seriesYData[i], + volumeSeriesYData[i] + ); + + sumVolume -= volumeSeriesYData[i - period]; + sumVolume += volumeSeriesYData[i]; + + sumMoneyFlowVolume -= moneyFlowVolume[i - period]; + sumMoneyFlowVolume += moneyFlowVolume[i]; + + point = [ + xData[i], + i - nullIndex >= period ? + sumMoneyFlowVolume / sumVolume : + null + ]; + + moneyFlowXData.push(point[0]); + moneyFlowYData.push(point[1]); + values.push([point[0], point[1]]); + } + } + + return { + values: values, + xData: moneyFlowXData, + yData: moneyFlowYData + }; + } + }); /** * A `CMF` series. If the [type](#series.cmf.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.cmf @@ -224,7 +224,7 @@ H.seriesType('cmf', 'sma', /** * An array of data points for the series. For the `CMF` series type, * points are calculated dynamically. - * + * * @type {Array} * @since 6.0.0 * @extends series.line.data diff --git a/js/indicators/ema.src.js b/js/indicators/ema.src.js index d27f3951c8e..3ff455baa48 100755 --- a/js/indicators/ema.src.js +++ b/js/indicators/ema.src.js @@ -3,35 +3,35 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var isArray = H.isArray, - seriesType = H.seriesType; + seriesType = H.seriesType; // Utils: function accumulateAverage(points, xVal, yVal, i, index) { - var xValue = xVal[i], - yValue = index < 0 ? yVal[i] : yVal[i][index]; - - points.push([xValue, yValue]); + var xValue = xVal[i], + yValue = index < 0 ? yVal[i] : yVal[i][index]; + + points.push([xValue, yValue]); } function populateAverage( - points, - xVal, - yVal, - i, - EMApercent, - calEMA, - index, - SMA + points, + xVal, + yVal, + i, + EMApercent, + calEMA, + index, + SMA ) { - var x = xVal[i - 1], - yValue = index < 0 ? yVal[i - 1] : yVal[i - 1][index], - y; + var x = xVal[i - 1], + yValue = index < 0 ? yVal[i - 1] : yVal[i - 1][index], + y; - y = calEMA === undefined ? - SMA : - ((yValue * EMApercent) + (calEMA * (1 - EMApercent))); + y = calEMA === undefined ? + SMA : + ((yValue * EMApercent) + (calEMA * (1 - EMApercent))); - return [x, y]; + return [x, y]; } /** * The EMA series type. @@ -39,107 +39,107 @@ function populateAverage( * @constructor seriesTypes.ema * @augments seriesTypes.sma */ -seriesType('ema', 'sma', - /** - * Exponential moving average indicator (EMA). This series requires the - * `linkedTo` option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/ema - * Exponential moving average indicator - * @since 6.0.0 - * @optionparent plotOptions.ema - */ - { - params: { - index: 0, - period: 14 - } - }, { - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - EMApercent = (2 / (period + 1)), - range = 0, - sum = 0, - EMA = [], - xData = [], - yData = [], - index = -1, - points = [], - SMA = 0, - calEMA, - EMAPoint, - i; - - // Check period, if bigger than points length, skip - if (xVal.length < period) { - return false; - } - - // Switch index for OHLC / Candlestick / Arearange - if (isArray(yVal[0])) { - index = params.index ? params.index : 0; - } - - // Accumulate first N-points - while (range < period) { - accumulateAverage(points, xVal, yVal, range, index); - sum += index < 0 ? yVal[range] : yVal[range][index]; - range++; - } - - // first point - SMA = sum / period; - - // Calculate value one-by-one for each period in visible data - for (i = range; i < yValLen; i++) { - EMAPoint = populateAverage( - points, - xVal, - yVal, - i, - EMApercent, - calEMA, - index, - SMA - ); - EMA.push(EMAPoint); - xData.push(EMAPoint[0]); - yData.push(EMAPoint[1]); - calEMA = EMAPoint[1]; - - accumulateAverage(points, xVal, yVal, i, index); - } - - EMAPoint = populateAverage( - points, - xVal, - yVal, - i, - EMApercent, - calEMA, - index - ); - EMA.push(EMAPoint); - xData.push(EMAPoint[0]); - yData.push(EMAPoint[1]); - - return { - values: EMA, - xData: xData, - yData: yData - }; - } - }); +seriesType('ema', 'sma', + /** + * Exponential moving average indicator (EMA). This series requires the + * `linkedTo` option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/ema + * Exponential moving average indicator + * @since 6.0.0 + * @optionparent plotOptions.ema + */ + { + params: { + index: 0, + period: 14 + } + }, { + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + EMApercent = (2 / (period + 1)), + range = 0, + sum = 0, + EMA = [], + xData = [], + yData = [], + index = -1, + points = [], + SMA = 0, + calEMA, + EMAPoint, + i; + + // Check period, if bigger than points length, skip + if (xVal.length < period) { + return false; + } + + // Switch index for OHLC / Candlestick / Arearange + if (isArray(yVal[0])) { + index = params.index ? params.index : 0; + } + + // Accumulate first N-points + while (range < period) { + accumulateAverage(points, xVal, yVal, range, index); + sum += index < 0 ? yVal[range] : yVal[range][index]; + range++; + } + + // first point + SMA = sum / period; + + // Calculate value one-by-one for each period in visible data + for (i = range; i < yValLen; i++) { + EMAPoint = populateAverage( + points, + xVal, + yVal, + i, + EMApercent, + calEMA, + index, + SMA + ); + EMA.push(EMAPoint); + xData.push(EMAPoint[0]); + yData.push(EMAPoint[1]); + calEMA = EMAPoint[1]; + + accumulateAverage(points, xVal, yVal, i, index); + } + + EMAPoint = populateAverage( + points, + xVal, + yVal, + i, + EMApercent, + calEMA, + index + ); + EMA.push(EMAPoint); + xData.push(EMAPoint[0]); + yData.push(EMAPoint[1]); + + return { + values: EMA, + xData: xData, + yData: yData + }; + } + }); /** * A `EMA` series. If the [type](#series.ema.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.ema diff --git a/js/indicators/ichimoku-kinko-hyo.src.js b/js/indicators/ichimoku-kinko-hyo.src.js index 52ce31749e5..ff97b85f720 100644 --- a/js/indicators/ichimoku-kinko-hyo.src.js +++ b/js/indicators/ichimoku-kinko-hyo.src.js @@ -3,77 +3,77 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var UNDEFINED, - seriesType = H.seriesType, - each = H.each, - merge = H.merge, - color = H.color, - isArray = H.isArray, - defined = H.defined, - SMA = H.seriesTypes.sma; + seriesType = H.seriesType, + each = H.each, + merge = H.merge, + color = H.color, + isArray = H.isArray, + defined = H.defined, + SMA = H.seriesTypes.sma; // Utils: function maxHigh(arr) { - return arr.reduce(function (max, res) { - return Math.max(max, res[1]); - }, -Infinity); + return arr.reduce(function (max, res) { + return Math.max(max, res[1]); + }, -Infinity); } function minLow(arr) { - return arr.reduce(function (min, res) { - return Math.min(min, res[2]); - }, Infinity); + return arr.reduce(function (min, res) { + return Math.min(min, res[2]); + }, Infinity); } function highlowLevel(arr) { - return { - high: maxHigh(arr), - low: minLow(arr) - }; + return { + high: maxHigh(arr), + low: minLow(arr) + }; } function getClosestPointRange(axis) { - var closestDataRange, - loopLength, - distance, - xData, - i; - - each(axis.series, function (series) { - - if (series.xData) { - xData = series.xData; - loopLength = series.xIncrement ? 1 : xData.length - 1; - - for (i = loopLength; i > 0; i--) { - distance = xData[i] - xData[i - 1]; - if ( - closestDataRange === UNDEFINED || - distance < closestDataRange - ) { - closestDataRange = distance; - } - } - } - }); - - return closestDataRange; + var closestDataRange, + loopLength, + distance, + xData, + i; + + each(axis.series, function (series) { + + if (series.xData) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if ( + closestDataRange === UNDEFINED || + distance < closestDataRange + ) { + closestDataRange = distance; + } + } + } + }); + + return closestDataRange; } // Data integrity in Ichimoku is different than default "averages": // Point: [undefined, value, value, ...] is correct // Point: [undefined, undefined, undefined, ...] is incorrect H.approximations['ichimoku-averages'] = function () { - var ret = [], - isEmptyRange; + var ret = [], + isEmptyRange; - each(arguments, function (arr, i) { - ret.push(H.approximations.average(arr)); - isEmptyRange = !isEmptyRange && ret[i] === undefined; - }); + each(arguments, function (arr, i) { + ret.push(H.approximations.average(arr)); + isEmptyRange = !isEmptyRange && ret[i] === undefined; + }); - // Return undefined when first elem. is undefined and let - // sum method handle null (#7377) - return isEmptyRange ? undefined : ret; + // Return undefined when first elem. is undefined and let + // sum method handle null (#7377) + return isEmptyRange ? undefined : ret; }; /** @@ -82,548 +82,548 @@ H.approximations['ichimoku-averages'] = function () { * @constructor seriesTypes.ikh * @augments seriesTypes.sma */ -seriesType('ikh', 'sma', - /** - * Ichimoku Kinko Hyo (IKH). This series requires `linkedTo` option to be - * set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/ichimoku-kinko-hyo - * Ichimoku Kinko Hyo indicator - * @since 6.0.0 - * @excluding - * allAreas,colorAxis,compare,compareBase,joinBy,keys,stacking, - * showInNavigator,navigatorOptions,pointInterval, - * pointIntervalUnit,pointPlacement,pointRange,pointStart - * @optionparent plotOptions.ikh - */ - { - params: { - period: 26, - /** - * The base period for Tenkan calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - periodTenkan: 9, - /** - * The base period for Senkou Span B calculations - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - periodSenkouSpanB: 52 - }, - marker: { - enabled: false - }, - tooltip: { - pointFormat: '\u25CF {series.name}
' + - 'TENKAN SEN: {point.tenkanSen:.3f}
' + - 'KIJUN SEN: {point.kijunSen:.3f}
' + - 'CHIKOU SPAN: {point.chikouSpan:.3f}
' + - 'SENKOU SPAN A: {point.senkouSpanA:.3f}
' + - 'SENKOU SPAN B: {point.senkouSpanB:.3f}
' - }, - /** - * The styles for Tenkan line - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - tenkanLine: { - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * The styles for Kijun line - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - kijunLine: { - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * The styles for Chikou line - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - chikouLine: { - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * The styles for Senkou Span A line - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - senkouSpanA: { - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * The styles for Senkou Span B line - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - senkouSpanB: { - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * The styles for fill between Senkou Span A and B - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - senkouSpan: { - styles: { - /** - * Color of the area between Senkou Span A and B. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - fill: 'rgba(255, 0, 0, 0.5)' - } - }, - dataGrouping: { - approximation: 'ichimoku-averages' - } - }, { - pointArrayMap: [ - 'tenkanSen', - 'kijunSen', - 'chikouSpan', - 'senkouSpanA', - 'senkouSpanB' - ], - pointValKey: 'tenkanSen', - nameComponents: ['periodSenkouSpanB', 'period', 'periodTenkan'], - init: function () { - SMA.prototype.init.apply(this, arguments); - - // Set default color for lines: - this.options = merge({ - tenkanLine: { - styles: { - lineColor: this.color - } - }, - kijunLine: { - styles: { - lineColor: this.color - } - }, - chikouLine: { - styles: { - lineColor: this.color - } - }, - senkouSpanA: { - styles: { - lineColor: this.color, - fill: color(this.color).setOpacity(0.5).get() - } - }, - senkouSpanB: { - styles: { - lineColor: this.color, - fill: color(this.color).setOpacity(0.5).get() - } - }, - senkouSpan: { - styles: { - fill: color(this.color).setOpacity(0.2).get() - } - } - }, this.options); - }, - toYData: function (point) { - return [ - point.tenkanSen, - point.kijunSen, - point.chikouSpan, - point.senkouSpanA, - point.senkouSpanB - ]; - }, - translate: function () { - var indicator = this; - - SMA.prototype.translate.apply(indicator); - - each(indicator.points, function (point) { - each(indicator.pointArrayMap, function (value) { - if (defined(point[value])) { - point['plot' + value] = indicator.yAxis.toPixels( - point[value], - true - ); - - // add extra parameters for support tooltip in moved - // lines - point.plotY = point['plot' + value]; - point.tooltipPos = [point.plotX, point['plot' + value]]; - point.isNull = false; - } - }); - }); - }, - // One does not simply - // Render five lines - // And an arearange - // In just one series.. - drawGraph: function () { - var indicator = this, - mainLinePoints = indicator.points, - pointsLength = mainLinePoints.length, - mainLineOptions = indicator.options, - mainLinePath = indicator.graph, - mainColor = indicator.color, - gappedExtend = { - options: { - gapSize: mainLineOptions.gapSize - } - }, - pointArrayMapLength = indicator.pointArrayMap.length, - allIchimokuPoints = [[], [], [], [], [], []], - position, - point, - i; - - // Generate points for all lines and spans lines: - while (pointsLength--) { - point = mainLinePoints[pointsLength]; - for (i = 0; i < pointArrayMapLength; i++) { - position = indicator.pointArrayMap[i]; - - if (defined(point[position])) { - allIchimokuPoints[i].push({ - plotX: point.plotX, - plotY: point['plot' + position], - isNull: false - }); - } - } - } - - // Modify options and generate lines: - each([ - 'tenkanLine', - 'kijunLine', - 'chikouLine', - 'senkouSpanA', - 'senkouSpanB', - 'senkouSpan' - ], function (lineName, i) { - // First line is rendered by default option - indicator.points = allIchimokuPoints[i]; - indicator.options = merge( - mainLineOptions[lineName].styles, - gappedExtend - ); - indicator.graph = indicator['graph' + lineName]; - - // For span, we need an access to the next points, used in - // getGraphPath() - indicator.nextPoints = allIchimokuPoints[i - 1]; - if (i === 5) { - - indicator.points = allIchimokuPoints[i - 1]; - indicator.options = merge( - mainLineOptions[lineName].styles, - gappedExtend - ); - indicator.graph = indicator['graph' + lineName]; - indicator.nextPoints = allIchimokuPoints[i - 2]; - - indicator.fillGraph = true; - indicator.color = indicator.options.fill; - SMA.prototype.drawGraph.call(indicator); - } else { - indicator.fillGraph = false; - indicator.color = mainColor; - SMA.prototype.drawGraph.call(indicator); - } - - // Now save lines: - indicator['graph' + lineName] = indicator.graph; - }); - // Clean temporary properties: - delete indicator.nextPoints; - delete indicator.fillGraph; - - // Restore options and draw the Tenkan line: - indicator.points = mainLinePoints; - indicator.options = mainLineOptions; - indicator.graph = mainLinePath; - }, - getGraphPath: function (points) { - var indicator = this, - path = [], - spanA, - fillArray = [], - spanAarr = []; - - points = points || this.points; - - - // Render Senkou Span - if (indicator.fillGraph && indicator.nextPoints) { - - spanA = SMA.prototype.getGraphPath.call( - indicator, - // Reverse points, so Senkou Span A will start from the end: - indicator.nextPoints - ); - - spanA[0] = 'L'; - - path = SMA.prototype.getGraphPath.call( - indicator, - points - ); - - spanAarr = spanA.slice(0, path.length); - - for (var i = (spanAarr.length - 1); i > 0; i -= 3) { - fillArray.push( - spanAarr[i - 2], - spanAarr[i - 1], - spanAarr[i] - ); - } - path = path.concat(fillArray); - - } else { - path = SMA.prototype.getGraphPath.apply(indicator, arguments); - } - - return path; - }, - getValues: function (series, params) { - - var period = params.period, - periodTenkan = params.periodTenkan, - periodSenkouSpanB = params.periodSenkouSpanB, - xVal = series.xData, - yVal = series.yData, - xAxis = series.xAxis, - yValLen = (yVal && yVal.length) || 0, - closestPointRange = getClosestPointRange(xAxis), - IKH = [], - xData = [], - dateStart, - date, - slicedTSY, - slicedKSY, - slicedSSBY, - pointTS, - pointKS, - pointSSB, - i, - TS, - KS, - CS, - SSA, - SSB; - - // ikh requires close value - if ( - xVal.length <= period || - !isArray(yVal[0]) || - yVal[0].length !== 4 - ) { - return false; - } - - - // add timestamps at the beginning - dateStart = xVal[0] - (period * closestPointRange); - - for (i = 0; i < period; i++) { - xData.push(dateStart + i * closestPointRange); - } - - for (i = 0; i < yValLen; i++) { - - // Tenkan Sen - if (i >= periodTenkan) { - - slicedTSY = yVal.slice(i - periodTenkan, i); - - pointTS = highlowLevel(slicedTSY); - - TS = (pointTS.high + pointTS.low) / 2; - } - - if (i >= period) { - - slicedKSY = yVal.slice(i - period, i); +seriesType('ikh', 'sma', + /** + * Ichimoku Kinko Hyo (IKH). This series requires `linkedTo` option to be + * set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/ichimoku-kinko-hyo + * Ichimoku Kinko Hyo indicator + * @since 6.0.0 + * @excluding + * allAreas,colorAxis,compare,compareBase,joinBy,keys,stacking, + * showInNavigator,navigatorOptions,pointInterval, + * pointIntervalUnit,pointPlacement,pointRange,pointStart + * @optionparent plotOptions.ikh + */ + { + params: { + period: 26, + /** + * The base period for Tenkan calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + periodTenkan: 9, + /** + * The base period for Senkou Span B calculations + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + periodSenkouSpanB: 52 + }, + marker: { + enabled: false + }, + tooltip: { + pointFormat: '\u25CF {series.name}
' + + 'TENKAN SEN: {point.tenkanSen:.3f}
' + + 'KIJUN SEN: {point.kijunSen:.3f}
' + + 'CHIKOU SPAN: {point.chikouSpan:.3f}
' + + 'SENKOU SPAN A: {point.senkouSpanA:.3f}
' + + 'SENKOU SPAN B: {point.senkouSpanB:.3f}
' + }, + /** + * The styles for Tenkan line + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + tenkanLine: { + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * The styles for Kijun line + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + kijunLine: { + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * The styles for Chikou line + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + chikouLine: { + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * The styles for Senkou Span A line + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + senkouSpanA: { + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * The styles for Senkou Span B line + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + senkouSpanB: { + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * The styles for fill between Senkou Span A and B + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + senkouSpan: { + styles: { + /** + * Color of the area between Senkou Span A and B. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + fill: 'rgba(255, 0, 0, 0.5)' + } + }, + dataGrouping: { + approximation: 'ichimoku-averages' + } + }, { + pointArrayMap: [ + 'tenkanSen', + 'kijunSen', + 'chikouSpan', + 'senkouSpanA', + 'senkouSpanB' + ], + pointValKey: 'tenkanSen', + nameComponents: ['periodSenkouSpanB', 'period', 'periodTenkan'], + init: function () { + SMA.prototype.init.apply(this, arguments); + + // Set default color for lines: + this.options = merge({ + tenkanLine: { + styles: { + lineColor: this.color + } + }, + kijunLine: { + styles: { + lineColor: this.color + } + }, + chikouLine: { + styles: { + lineColor: this.color + } + }, + senkouSpanA: { + styles: { + lineColor: this.color, + fill: color(this.color).setOpacity(0.5).get() + } + }, + senkouSpanB: { + styles: { + lineColor: this.color, + fill: color(this.color).setOpacity(0.5).get() + } + }, + senkouSpan: { + styles: { + fill: color(this.color).setOpacity(0.2).get() + } + } + }, this.options); + }, + toYData: function (point) { + return [ + point.tenkanSen, + point.kijunSen, + point.chikouSpan, + point.senkouSpanA, + point.senkouSpanB + ]; + }, + translate: function () { + var indicator = this; + + SMA.prototype.translate.apply(indicator); + + each(indicator.points, function (point) { + each(indicator.pointArrayMap, function (value) { + if (defined(point[value])) { + point['plot' + value] = indicator.yAxis.toPixels( + point[value], + true + ); + + // add extra parameters for support tooltip in moved + // lines + point.plotY = point['plot' + value]; + point.tooltipPos = [point.plotX, point['plot' + value]]; + point.isNull = false; + } + }); + }); + }, + // One does not simply + // Render five lines + // And an arearange + // In just one series.. + drawGraph: function () { + var indicator = this, + mainLinePoints = indicator.points, + pointsLength = mainLinePoints.length, + mainLineOptions = indicator.options, + mainLinePath = indicator.graph, + mainColor = indicator.color, + gappedExtend = { + options: { + gapSize: mainLineOptions.gapSize + } + }, + pointArrayMapLength = indicator.pointArrayMap.length, + allIchimokuPoints = [[], [], [], [], [], []], + position, + point, + i; + + // Generate points for all lines and spans lines: + while (pointsLength--) { + point = mainLinePoints[pointsLength]; + for (i = 0; i < pointArrayMapLength; i++) { + position = indicator.pointArrayMap[i]; + + if (defined(point[position])) { + allIchimokuPoints[i].push({ + plotX: point.plotX, + plotY: point['plot' + position], + isNull: false + }); + } + } + } + + // Modify options and generate lines: + each([ + 'tenkanLine', + 'kijunLine', + 'chikouLine', + 'senkouSpanA', + 'senkouSpanB', + 'senkouSpan' + ], function (lineName, i) { + // First line is rendered by default option + indicator.points = allIchimokuPoints[i]; + indicator.options = merge( + mainLineOptions[lineName].styles, + gappedExtend + ); + indicator.graph = indicator['graph' + lineName]; + + // For span, we need an access to the next points, used in + // getGraphPath() + indicator.nextPoints = allIchimokuPoints[i - 1]; + if (i === 5) { + + indicator.points = allIchimokuPoints[i - 1]; + indicator.options = merge( + mainLineOptions[lineName].styles, + gappedExtend + ); + indicator.graph = indicator['graph' + lineName]; + indicator.nextPoints = allIchimokuPoints[i - 2]; + + indicator.fillGraph = true; + indicator.color = indicator.options.fill; + SMA.prototype.drawGraph.call(indicator); + } else { + indicator.fillGraph = false; + indicator.color = mainColor; + SMA.prototype.drawGraph.call(indicator); + } + + // Now save lines: + indicator['graph' + lineName] = indicator.graph; + }); + // Clean temporary properties: + delete indicator.nextPoints; + delete indicator.fillGraph; + + // Restore options and draw the Tenkan line: + indicator.points = mainLinePoints; + indicator.options = mainLineOptions; + indicator.graph = mainLinePath; + }, + getGraphPath: function (points) { + var indicator = this, + path = [], + spanA, + fillArray = [], + spanAarr = []; + + points = points || this.points; + + + // Render Senkou Span + if (indicator.fillGraph && indicator.nextPoints) { + + spanA = SMA.prototype.getGraphPath.call( + indicator, + // Reverse points, so Senkou Span A will start from the end: + indicator.nextPoints + ); + + spanA[0] = 'L'; + + path = SMA.prototype.getGraphPath.call( + indicator, + points + ); + + spanAarr = spanA.slice(0, path.length); + + for (var i = (spanAarr.length - 1); i > 0; i -= 3) { + fillArray.push( + spanAarr[i - 2], + spanAarr[i - 1], + spanAarr[i] + ); + } + path = path.concat(fillArray); + + } else { + path = SMA.prototype.getGraphPath.apply(indicator, arguments); + } + + return path; + }, + getValues: function (series, params) { + + var period = params.period, + periodTenkan = params.periodTenkan, + periodSenkouSpanB = params.periodSenkouSpanB, + xVal = series.xData, + yVal = series.yData, + xAxis = series.xAxis, + yValLen = (yVal && yVal.length) || 0, + closestPointRange = getClosestPointRange(xAxis), + IKH = [], + xData = [], + dateStart, + date, + slicedTSY, + slicedKSY, + slicedSSBY, + pointTS, + pointKS, + pointSSB, + i, + TS, + KS, + CS, + SSA, + SSB; + + // ikh requires close value + if ( + xVal.length <= period || + !isArray(yVal[0]) || + yVal[0].length !== 4 + ) { + return false; + } + + + // add timestamps at the beginning + dateStart = xVal[0] - (period * closestPointRange); + + for (i = 0; i < period; i++) { + xData.push(dateStart + i * closestPointRange); + } + + for (i = 0; i < yValLen; i++) { + + // Tenkan Sen + if (i >= periodTenkan) { + + slicedTSY = yVal.slice(i - periodTenkan, i); + + pointTS = highlowLevel(slicedTSY); + + TS = (pointTS.high + pointTS.low) / 2; + } - pointKS = highlowLevel(slicedKSY); + if (i >= period) { - KS = (pointKS.high + pointKS.low) / 2; + slicedKSY = yVal.slice(i - period, i); - SSA = (TS + KS) / 2; - } - - if (i >= periodSenkouSpanB) { - - slicedSSBY = yVal.slice(i - periodSenkouSpanB, i); - - pointSSB = highlowLevel(slicedSSBY); - - SSB = (pointSSB.high + pointSSB.low) / 2; - } + pointKS = highlowLevel(slicedKSY); - CS = yVal[i][0]; - - date = xVal[i]; - - if (IKH[i] === UNDEFINED) { - IKH[i] = []; - } - - if (IKH[i + period] === UNDEFINED) { - IKH[i + period] = []; - } - - IKH[i + period][0] = TS; - IKH[i + period][1] = KS; - IKH[i + period][2] = UNDEFINED; + KS = (pointKS.high + pointKS.low) / 2; - if (i >= period) { - IKH[i - period][2] = CS; - } else { - IKH[i + period][3] = UNDEFINED; - IKH[i + period][4] = UNDEFINED; - } - - if (IKH[i + 2 * period] === UNDEFINED) { - IKH[i + 2 * period] = []; - } - - IKH[i + 2 * period][3] = SSA; - IKH[i + 2 * period][4] = SSB; + SSA = (TS + KS) / 2; + } - xData.push(date); + if (i >= periodSenkouSpanB) { - } + slicedSSBY = yVal.slice(i - periodSenkouSpanB, i); + + pointSSB = highlowLevel(slicedSSBY); - // add timestamps for further points - for (i = 1; i <= period; i++) { - xData.push(date + i * closestPointRange); - } + SSB = (pointSSB.high + pointSSB.low) / 2; + } - return { - values: IKH, - xData: xData, - yData: IKH - }; - } - }); + CS = yVal[i][0]; + + date = xVal[i]; + + if (IKH[i] === UNDEFINED) { + IKH[i] = []; + } + + if (IKH[i + period] === UNDEFINED) { + IKH[i + period] = []; + } + + IKH[i + period][0] = TS; + IKH[i + period][1] = KS; + IKH[i + period][2] = UNDEFINED; + + if (i >= period) { + IKH[i - period][2] = CS; + } else { + IKH[i + period][3] = UNDEFINED; + IKH[i + period][4] = UNDEFINED; + } + + if (IKH[i + 2 * period] === UNDEFINED) { + IKH[i + 2 * period] = []; + } + + IKH[i + 2 * period][3] = SSA; + IKH[i + 2 * period][4] = SSB; + + xData.push(date); + + } + + // add timestamps for further points + for (i = 1; i <= period; i++) { + xData.push(date + i * closestPointRange); + } + + return { + values: IKH, + xData: xData, + yData: IKH + }; + } + }); /** * A `IKH` series. If the [type](#series.ikh.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.ikh diff --git a/js/indicators/indicators.src.js b/js/indicators/indicators.src.js index c52e5dfe507..08c31ed75c1 100644 --- a/js/indicators/indicators.src.js +++ b/js/indicators/indicators.src.js @@ -3,12 +3,12 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var pick = H.pick, - each = H.each, - error = H.error, - Series = H.Series, - isArray = H.isArray, - addEvent = H.addEvent, - seriesType = H.seriesType; + each = H.each, + error = H.error, + Series = H.Series, + isArray = H.isArray, + addEvent = H.addEvent, + seriesType = H.seriesType; /** * The SMA series type. @@ -17,233 +17,233 @@ var pick = H.pick, * @augments seriesTypes.line */ seriesType('sma', 'line', - /** - * Simple moving average indicator (SMA). This series requires `linkedTo` - * option to be set. - * - * @extends {plotOptions.line} - * @product highstock - * @sample {highstock} stock/indicators/sma Simple moving average indicator - * @since 6.0.0 - * @excluding - * allAreas,colorAxis,compare,compareBase,joinBy,keys,stacking, - * showInNavigator,navigatorOptions,pointInterval, - * pointIntervalUnit,pointPlacement,pointRange,pointStart,joinBy - * @optionparent plotOptions.sma - */ - { - /** - * The name of the series as shown in the legend, tooltip etc. If not - * set, it will be based on a technical indicator type and default - * params. - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - name: undefined, - tooltip: { - /** - * Number of decimals in indicator series. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - valueDecimals: 4 - }, - /** - * The main series ID that indicator will be based on. Required for this - * indicator. - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - linkedTo: undefined, - params: { - /** - * The point index which indicator calculations will base. For - * example using OHLC data, index=2 means the indicator will be - * calculated using Low values. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - index: 0, - /** - * The base period for indicator calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - period: 14 - } - }, /** @lends Highcharts.Series.prototype */ { - bindTo: { - series: true, - eventName: 'updatedData' - }, - useCommonDataGrouping: true, - nameComponents: ['period'], - nameSuffixes: [], // e.g. Zig Zag uses extra '%'' in the legend name - calculateOn: 'init', - init: function (chart, options) { - var indicator = this; + /** + * Simple moving average indicator (SMA). This series requires `linkedTo` + * option to be set. + * + * @extends {plotOptions.line} + * @product highstock + * @sample {highstock} stock/indicators/sma Simple moving average indicator + * @since 6.0.0 + * @excluding + * allAreas,colorAxis,compare,compareBase,joinBy,keys,stacking, + * showInNavigator,navigatorOptions,pointInterval, + * pointIntervalUnit,pointPlacement,pointRange,pointStart,joinBy + * @optionparent plotOptions.sma + */ + { + /** + * The name of the series as shown in the legend, tooltip etc. If not + * set, it will be based on a technical indicator type and default + * params. + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + name: undefined, + tooltip: { + /** + * Number of decimals in indicator series. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + valueDecimals: 4 + }, + /** + * The main series ID that indicator will be based on. Required for this + * indicator. + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + linkedTo: undefined, + params: { + /** + * The point index which indicator calculations will base. For + * example using OHLC data, index=2 means the indicator will be + * calculated using Low values. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + index: 0, + /** + * The base period for indicator calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + period: 14 + } + }, /** @lends Highcharts.Series.prototype */ { + bindTo: { + series: true, + eventName: 'updatedData' + }, + useCommonDataGrouping: true, + nameComponents: ['period'], + nameSuffixes: [], // e.g. Zig Zag uses extra '%'' in the legend name + calculateOn: 'init', + init: function (chart, options) { + var indicator = this; - Series.prototype.init.call( - indicator, - chart, - options - ); + Series.prototype.init.call( + indicator, + chart, + options + ); - // Make sure we find series which is a base for an indicator - chart.linkSeries(); + // Make sure we find series which is a base for an indicator + chart.linkSeries(); - indicator.dataEventsToUnbind = []; + indicator.dataEventsToUnbind = []; - function recalculateValues() { - var processedData = indicator.getValues( - indicator.linkedParent, - indicator.options.params - ) || { - values: [], - xData: [], - yData: [] - }; + function recalculateValues() { + var processedData = indicator.getValues( + indicator.linkedParent, + indicator.options.params + ) || { + values: [], + xData: [], + yData: [] + }; - indicator.xData = processedData.xData; - indicator.yData = processedData.yData; - indicator.options.data = processedData.values; + indicator.xData = processedData.xData; + indicator.yData = processedData.yData; + indicator.options.data = processedData.values; - // Removal of processedXData property is required because on - // first translate processedXData array is empty - if (indicator.bindTo.series === false) { - delete indicator.processedXData; + // Removal of processedXData property is required because on + // first translate processedXData array is empty + if (indicator.bindTo.series === false) { + delete indicator.processedXData; - indicator.isDirty = true; - indicator.redraw(); - } - indicator.isDirtyData = false; - } + indicator.isDirty = true; + indicator.redraw(); + } + indicator.isDirtyData = false; + } - if (!indicator.linkedParent) { - return error( - 'Series ' + - indicator.options.linkedTo + - ' not found! Check `linkedTo`.' - ); - } + if (!indicator.linkedParent) { + return error( + 'Series ' + + indicator.options.linkedTo + + ' not found! Check `linkedTo`.' + ); + } - indicator.dataEventsToUnbind.push( - addEvent( - indicator.bindTo.series ? - indicator.linkedParent : indicator.linkedParent.xAxis, - indicator.bindTo.eventName, - recalculateValues - ) - ); + indicator.dataEventsToUnbind.push( + addEvent( + indicator.bindTo.series ? + indicator.linkedParent : indicator.linkedParent.xAxis, + indicator.bindTo.eventName, + recalculateValues + ) + ); - if (indicator.calculateOn === 'init') { - recalculateValues(); - } else { - var unbinder = addEvent( - indicator.chart, - indicator.calculateOn, - function () { - recalculateValues(); - // Call this just once, on init - unbinder(); - } - ); - } + if (indicator.calculateOn === 'init') { + recalculateValues(); + } else { + var unbinder = addEvent( + indicator.chart, + indicator.calculateOn, + function () { + recalculateValues(); + // Call this just once, on init + unbinder(); + } + ); + } - return indicator; - }, - getName: function () { - var name = this.name, - params = []; + return indicator; + }, + getName: function () { + var name = this.name, + params = []; - if (!name) { + if (!name) { - each( - this.nameComponents, - function (component, index) { - params.push( - this.options.params[component] + - pick(this.nameSuffixes[index], '') - ); - }, - this - ); + each( + this.nameComponents, + function (component, index) { + params.push( + this.options.params[component] + + pick(this.nameSuffixes[index], '') + ); + }, + this + ); - name = (this.nameBase || this.type.toUpperCase()) + - (this.nameComponents ? ' (' + params.join(', ') + ')' : ''); - } + name = (this.nameBase || this.type.toUpperCase()) + + (this.nameComponents ? ' (' + params.join(', ') + ')' : ''); + } - return name; - }, - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal.length, - range = 0, - sum = 0, - SMA = [], - xData = [], - yData = [], - index = -1, - i, - SMAPoint; + return name; + }, + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal.length, + range = 0, + sum = 0, + SMA = [], + xData = [], + yData = [], + index = -1, + i, + SMAPoint; - if (xVal.length < period) { - return false; - } - - // Switch index for OHLC / Candlestick / Arearange - if (isArray(yVal[0])) { - index = params.index ? params.index : 0; - } + if (xVal.length < period) { + return false; + } - // Accumulate first N-points - while (range < period - 1) { - sum += index < 0 ? yVal[range] : yVal[range][index]; - range++; - } + // Switch index for OHLC / Candlestick / Arearange + if (isArray(yVal[0])) { + index = params.index ? params.index : 0; + } - // Calculate value one-by-one for each period in visible data - for (i = range; i < yValLen; i++) { - sum += index < 0 ? yVal[i] : yVal[i][index]; + // Accumulate first N-points + while (range < period - 1) { + sum += index < 0 ? yVal[range] : yVal[range][index]; + range++; + } - SMAPoint = [xVal[i], sum / period]; - SMA.push(SMAPoint); - xData.push(SMAPoint[0]); - yData.push(SMAPoint[1]); + // Calculate value one-by-one for each period in visible data + for (i = range; i < yValLen; i++) { + sum += index < 0 ? yVal[i] : yVal[i][index]; - sum -= index < 0 ? yVal[i - range] : yVal[i - range][index]; - } + SMAPoint = [xVal[i], sum / period]; + SMA.push(SMAPoint); + xData.push(SMAPoint[0]); + yData.push(SMAPoint[1]); - return { - values: SMA, - xData: xData, - yData: yData - }; - }, - destroy: function () { - each(this.dataEventsToUnbind, function (unbinder) { - unbinder(); - }); - Series.prototype.destroy.call(this); - } - }); + sum -= index < 0 ? yVal[i - range] : yVal[i - range][index]; + } + + return { + values: SMA, + xData: xData, + yData: yData + }; + }, + destroy: function () { + each(this.dataEventsToUnbind, function (unbinder) { + unbinder(); + }); + Series.prototype.destroy.call(this); + } + }); /** * A `SMA` series. If the [type](#series.sma.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.sma @@ -256,7 +256,7 @@ seriesType('sma', 'line', /** * An array of data points for the series. For the `SMA` series type, * points are calculated dynamically. - * + * * @type {Array} * @since 6.0.0 * @extends series.line.data diff --git a/js/indicators/macd.src.js b/js/indicators/macd.src.js index d062dca68e7..ccc256483d9 100644 --- a/js/indicators/macd.src.js +++ b/js/indicators/macd.src.js @@ -4,12 +4,12 @@ import '../parts/Utilities.js'; import './ema.src.js'; var seriesType = H.seriesType, - each = H.each, - noop = H.noop, - merge = H.merge, - defined = H.defined, - SMA = H.seriesTypes.sma, - EMA = H.seriesTypes.ema; + each = H.each, + noop = H.noop, + merge = H.merge, + defined = H.defined, + SMA = H.seriesTypes.sma, + EMA = H.seriesTypes.ema; /** * The MACD series type. @@ -17,387 +17,387 @@ var seriesType = H.seriesType, * @constructor seriesTypes.macd * @augments seriesTypes.sma */ -seriesType('macd', 'sma', - /** - * Moving Average Convergence Divergence (MACD). This series requires - * `linkedTo` option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/macd MACD indicator - * @since 6.0.0 - * @optionparent plotOptions.macd - */ - { - params: { - /** - * The short period for indicator calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - shortPeriod: 12, - /** - * The long period for indicator calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - longPeriod: 26, - /** - * The base period for signal calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - signalPeriod: 9, - period: 26 - }, - /** - * The styles for signal line - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - signalLine: { - /** - * @extends plotOptions.macd.zones - * @sample stock/indicators/macd-zones Zones in MACD - */ - zones: [], - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * The styles for macd line - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - macdLine: { - /** - * @extends plotOptions.macd.zones - * @sample stock/indicators/macd-zones Zones in MACD - */ - zones: [], - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - threshold: 0, - groupPadding: 0.1, - pointPadding: 0.1, - states: { - hover: { - halo: { - size: 0 - } - } - }, - tooltip: { - pointFormat: '\u25CF {series.name}
' + - 'Value: {point.MACD}
' + - 'Signal: {point.signal}
' + - 'Histogram: {point.y}
' - }, - dataGrouping: { - approximation: 'averages' - }, - minPointLength: 0 - }, { - nameComponents: ['longPeriod', 'shortPeriod', 'signalPeriod'], - // "y" value is treated as Histogram data - pointArrayMap: ['y', 'signal', 'MACD'], - parallelArrays: ['x', 'y', 'signal', 'MACD'], - pointValKey: 'y', - // Columns support: - markerAttribs: noop, - getColumnMetrics: H.seriesTypes.column.prototype.getColumnMetrics, - crispCol: H.seriesTypes.column.prototype.crispCol, - // Colors and lines: - init: function () { - SMA.prototype.init.apply(this, arguments); - - // Set default color for a signal line and the histogram: - this.options = merge({ - signalLine: { - styles: { - lineColor: this.color - } - }, - macdLine: { - styles: { - color: this.color - } - } - }, this.options); - - // Zones have indexes automatically calculated, we need to - // translate them to support multiple lines within one indicator - this.macdZones = { - zones: this.options.macdLine.zones, - startIndex: 0 - }; - this.signalZones = { - zones: this.macdZones.zones.concat( - this.options.signalLine.zones - ), - startIndex: this.macdZones.zones.length - }; - this.resetZones = true; - }, - toYData: function (point) { - return [point.y, point.signal, point.MACD]; - }, - translate: function () { - var indicator = this, - plotNames = ['plotSignal', 'plotMACD']; - - H.seriesTypes.column.prototype.translate.apply(indicator); - - each(indicator.points, function (point) { - each([point.signal, point.MACD], function (value, i) { - if (value !== null) { - point[plotNames[i]] = indicator.yAxis.toPixels( - value, - true - ); - } - }); - }); - }, - destroy: function () { - // this.graph is null due to removing two times the same SVG element - this.graph = null; - this.graphmacd = this.graphmacd.destroy(); - this.graphsignal = this.graphsignal.destroy(); - - SMA.prototype.destroy.apply(this, arguments); - }, - drawPoints: H.seriesTypes.column.prototype.drawPoints, - drawGraph: function () { - var indicator = this, - mainLinePoints = indicator.points, - pointsLength = mainLinePoints.length, - mainLineOptions = indicator.options, - histogramZones = indicator.zones, - gappedExtend = { - options: { - gapSize: mainLineOptions.gapSize - } - }, - otherSignals = [[], []], - point; - - // Generate points for top and bottom lines: - while (pointsLength--) { - point = mainLinePoints[pointsLength]; - if (defined(point.plotMACD)) { - otherSignals[0].push({ - plotX: point.plotX, - plotY: point.plotMACD, - isNull: !defined(point.plotMACD) - }); - } - if (defined(point.plotSignal)) { - otherSignals[1].push({ - plotX: point.plotX, - plotY: point.plotSignal, - isNull: !defined(point.plotMACD) - }); - } - } - - // Modify options and generate smoothing line: - each(['macd', 'signal'], function (lineName, i) { - indicator.points = otherSignals[i]; - indicator.options = merge( - mainLineOptions[lineName + 'Line'].styles, - gappedExtend - ); - indicator.graph = indicator['graph' + lineName]; - - // Zones extension: - indicator.currentLineZone = lineName + 'Zones'; - indicator.zones = indicator[indicator.currentLineZone].zones; - - SMA.prototype.drawGraph.call(indicator); - indicator['graph' + lineName] = indicator.graph; - }); - - // Restore options: - indicator.points = mainLinePoints; - indicator.options = mainLineOptions; - indicator.zones = histogramZones; - indicator.currentLineZone = null; - // indicator.graph = null; - }, - getZonesGraphs: function (props) { - var allZones = SMA.prototype.getZonesGraphs.call(this, props), - currentZones = allZones; - - if (this.currentLineZone) { - currentZones = allZones.splice( - this[this.currentLineZone].startIndex + 1 - ); - - if (!currentZones.length) { - // Line has no zones, return basic graph "zone" - currentZones = [props[0]]; - } else { - // Add back basic prop: - currentZones.splice(0, 0, props[0]); - } - } - - return currentZones; - }, - applyZones: function () { - // Histogram zones are handled by drawPoints method - // Here we need to apply zones for all lines - var histogramZones = this.zones; - - // signalZones.zones contains all zones: - this.zones = this.signalZones.zones; - SMA.prototype.applyZones.call(this); - - // applyZones hides only main series.graph, hide macd line manually - if (this.options.macdLine.zones.length) { - this.graphmacd.hide(); - } - - this.zones = histogramZones; - }, - getValues: function (series, params) { - var j = 0, - MACD = [], - xMACD = [], - yMACD = [], - signalLine = [], - shortEMA, - longEMA, - i; - - // Calculating the short and long EMA used when calculating the MACD - shortEMA = EMA.prototype.getValues(series, - { - period: params.shortPeriod - } - ); - - longEMA = EMA.prototype.getValues(series, - { - period: params.longPeriod - } - ); - - shortEMA = shortEMA.values; - longEMA = longEMA.values; - - - // Subtract each Y value from the EMA's and create the new dataset - // (MACD) - for (i = 1; i <= shortEMA.length; i++) { - if (defined(longEMA[i - 1]) && defined(longEMA[i - 1][1])) { - MACD.push([ - shortEMA[i + params.shortPeriod + 1][0], - 0, - null, - shortEMA[i + params.shortPeriod + 1][1] - - longEMA[i - 1][1] - ]); - } - } - - // Set the Y and X data of the MACD. This is used in calculating the - // signal line. - for (i = 0; i < MACD.length; i++) { - xMACD.push(MACD[i][0]); - yMACD.push([0, null, MACD[i][3]]); - } - - // Setting the signalline (Signal Line: X-day EMA of MACD line). - signalLine = EMA.prototype.getValues( - { - xData: xMACD, - yData: yMACD - }, - { - period: params.signalPeriod, - index: 2 - } - ); - - signalLine = signalLine.values; - - // Setting the MACD Histogram. In comparison to the loop with pure - // MACD this loop uses MACD x value not xData. - for (i = 0; i < MACD.length; i++) { - if (MACD[i][0] >= signalLine[0][0]) { // detect the first point - - MACD[i][2] = signalLine[j][1]; - yMACD[i] = [0, signalLine[j][1], MACD[i][3]]; - - if (MACD[i][3] === null) { - MACD[i][1] = 0; - yMACD[i][0] = 0; - } else { - MACD[i][1] = (MACD[i][3] - signalLine[j][1]); - yMACD[i][0] = (MACD[i][3] - signalLine[j][1]); - } - - j++; - } - } - - return { - values: MACD, - xData: xMACD, - yData: yMACD - }; - } - }); +seriesType('macd', 'sma', + /** + * Moving Average Convergence Divergence (MACD). This series requires + * `linkedTo` option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/macd MACD indicator + * @since 6.0.0 + * @optionparent plotOptions.macd + */ + { + params: { + /** + * The short period for indicator calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + shortPeriod: 12, + /** + * The long period for indicator calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + longPeriod: 26, + /** + * The base period for signal calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + signalPeriod: 9, + period: 26 + }, + /** + * The styles for signal line + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + signalLine: { + /** + * @extends plotOptions.macd.zones + * @sample stock/indicators/macd-zones Zones in MACD + */ + zones: [], + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * The styles for macd line + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + macdLine: { + /** + * @extends plotOptions.macd.zones + * @sample stock/indicators/macd-zones Zones in MACD + */ + zones: [], + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + threshold: 0, + groupPadding: 0.1, + pointPadding: 0.1, + states: { + hover: { + halo: { + size: 0 + } + } + }, + tooltip: { + pointFormat: '\u25CF {series.name}
' + + 'Value: {point.MACD}
' + + 'Signal: {point.signal}
' + + 'Histogram: {point.y}
' + }, + dataGrouping: { + approximation: 'averages' + }, + minPointLength: 0 + }, { + nameComponents: ['longPeriod', 'shortPeriod', 'signalPeriod'], + // "y" value is treated as Histogram data + pointArrayMap: ['y', 'signal', 'MACD'], + parallelArrays: ['x', 'y', 'signal', 'MACD'], + pointValKey: 'y', + // Columns support: + markerAttribs: noop, + getColumnMetrics: H.seriesTypes.column.prototype.getColumnMetrics, + crispCol: H.seriesTypes.column.prototype.crispCol, + // Colors and lines: + init: function () { + SMA.prototype.init.apply(this, arguments); + + // Set default color for a signal line and the histogram: + this.options = merge({ + signalLine: { + styles: { + lineColor: this.color + } + }, + macdLine: { + styles: { + color: this.color + } + } + }, this.options); + + // Zones have indexes automatically calculated, we need to + // translate them to support multiple lines within one indicator + this.macdZones = { + zones: this.options.macdLine.zones, + startIndex: 0 + }; + this.signalZones = { + zones: this.macdZones.zones.concat( + this.options.signalLine.zones + ), + startIndex: this.macdZones.zones.length + }; + this.resetZones = true; + }, + toYData: function (point) { + return [point.y, point.signal, point.MACD]; + }, + translate: function () { + var indicator = this, + plotNames = ['plotSignal', 'plotMACD']; + + H.seriesTypes.column.prototype.translate.apply(indicator); + + each(indicator.points, function (point) { + each([point.signal, point.MACD], function (value, i) { + if (value !== null) { + point[plotNames[i]] = indicator.yAxis.toPixels( + value, + true + ); + } + }); + }); + }, + destroy: function () { + // this.graph is null due to removing two times the same SVG element + this.graph = null; + this.graphmacd = this.graphmacd.destroy(); + this.graphsignal = this.graphsignal.destroy(); + + SMA.prototype.destroy.apply(this, arguments); + }, + drawPoints: H.seriesTypes.column.prototype.drawPoints, + drawGraph: function () { + var indicator = this, + mainLinePoints = indicator.points, + pointsLength = mainLinePoints.length, + mainLineOptions = indicator.options, + histogramZones = indicator.zones, + gappedExtend = { + options: { + gapSize: mainLineOptions.gapSize + } + }, + otherSignals = [[], []], + point; + + // Generate points for top and bottom lines: + while (pointsLength--) { + point = mainLinePoints[pointsLength]; + if (defined(point.plotMACD)) { + otherSignals[0].push({ + plotX: point.plotX, + plotY: point.plotMACD, + isNull: !defined(point.plotMACD) + }); + } + if (defined(point.plotSignal)) { + otherSignals[1].push({ + plotX: point.plotX, + plotY: point.plotSignal, + isNull: !defined(point.plotMACD) + }); + } + } + + // Modify options and generate smoothing line: + each(['macd', 'signal'], function (lineName, i) { + indicator.points = otherSignals[i]; + indicator.options = merge( + mainLineOptions[lineName + 'Line'].styles, + gappedExtend + ); + indicator.graph = indicator['graph' + lineName]; + + // Zones extension: + indicator.currentLineZone = lineName + 'Zones'; + indicator.zones = indicator[indicator.currentLineZone].zones; + + SMA.prototype.drawGraph.call(indicator); + indicator['graph' + lineName] = indicator.graph; + }); + + // Restore options: + indicator.points = mainLinePoints; + indicator.options = mainLineOptions; + indicator.zones = histogramZones; + indicator.currentLineZone = null; + // indicator.graph = null; + }, + getZonesGraphs: function (props) { + var allZones = SMA.prototype.getZonesGraphs.call(this, props), + currentZones = allZones; + + if (this.currentLineZone) { + currentZones = allZones.splice( + this[this.currentLineZone].startIndex + 1 + ); + + if (!currentZones.length) { + // Line has no zones, return basic graph "zone" + currentZones = [props[0]]; + } else { + // Add back basic prop: + currentZones.splice(0, 0, props[0]); + } + } + + return currentZones; + }, + applyZones: function () { + // Histogram zones are handled by drawPoints method + // Here we need to apply zones for all lines + var histogramZones = this.zones; + + // signalZones.zones contains all zones: + this.zones = this.signalZones.zones; + SMA.prototype.applyZones.call(this); + + // applyZones hides only main series.graph, hide macd line manually + if (this.options.macdLine.zones.length) { + this.graphmacd.hide(); + } + + this.zones = histogramZones; + }, + getValues: function (series, params) { + var j = 0, + MACD = [], + xMACD = [], + yMACD = [], + signalLine = [], + shortEMA, + longEMA, + i; + + // Calculating the short and long EMA used when calculating the MACD + shortEMA = EMA.prototype.getValues(series, + { + period: params.shortPeriod + } + ); + + longEMA = EMA.prototype.getValues(series, + { + period: params.longPeriod + } + ); + + shortEMA = shortEMA.values; + longEMA = longEMA.values; + + + // Subtract each Y value from the EMA's and create the new dataset + // (MACD) + for (i = 1; i <= shortEMA.length; i++) { + if (defined(longEMA[i - 1]) && defined(longEMA[i - 1][1])) { + MACD.push([ + shortEMA[i + params.shortPeriod + 1][0], + 0, + null, + shortEMA[i + params.shortPeriod + 1][1] - + longEMA[i - 1][1] + ]); + } + } + + // Set the Y and X data of the MACD. This is used in calculating the + // signal line. + for (i = 0; i < MACD.length; i++) { + xMACD.push(MACD[i][0]); + yMACD.push([0, null, MACD[i][3]]); + } + + // Setting the signalline (Signal Line: X-day EMA of MACD line). + signalLine = EMA.prototype.getValues( + { + xData: xMACD, + yData: yMACD + }, + { + period: params.signalPeriod, + index: 2 + } + ); + + signalLine = signalLine.values; + + // Setting the MACD Histogram. In comparison to the loop with pure + // MACD this loop uses MACD x value not xData. + for (i = 0; i < MACD.length; i++) { + if (MACD[i][0] >= signalLine[0][0]) { // detect the first point + + MACD[i][2] = signalLine[j][1]; + yMACD[i] = [0, signalLine[j][1], MACD[i][3]]; + + if (MACD[i][3] === null) { + MACD[i][1] = 0; + yMACD[i][0] = 0; + } else { + MACD[i][1] = (MACD[i][3] - signalLine[j][1]); + yMACD[i][0] = (MACD[i][3] - signalLine[j][1]); + } + + j++; + } + } + + return { + values: MACD, + xData: xMACD, + yData: yMACD + }; + } + }); /** * A `MACD` series. If the [type](#series.macd.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.macd diff --git a/js/indicators/mfi.src.js b/js/indicators/mfi.src.js index 46af58a5fcf..3f5d24c8c90 100644 --- a/js/indicators/mfi.src.js +++ b/js/indicators/mfi.src.js @@ -15,24 +15,24 @@ import '../parts/Utilities.js'; var isArray = H.isArray; - // Utils: + // Utils: function sumArray(array) { - return array.reduce(function (prev, cur) { - return prev + cur; - }); + return array.reduce(function (prev, cur) { + return prev + cur; + }); } function toFixed(a, n) { - return parseFloat(a.toFixed(n)); + return parseFloat(a.toFixed(n)); } function calculateTypicalPrice(point) { - return (point[1] + point[2] + point[3]) / 3; + return (point[1] + point[2] + point[3]) / 3; } function calculateRawMoneyFlow(typicalPrice, volume) { - return typicalPrice * volume; + return typicalPrice * volume; } /** * The MFI series type. @@ -40,148 +40,148 @@ function calculateRawMoneyFlow(typicalPrice, volume) { * @constructor seriesTypes.mfi * @augments seriesTypes.sma */ -H.seriesType('mfi', 'sma', - - /** - * Money Flow Index. This series requires `linkedTo` option to be set and - * should be loaded after the `stock/indicators/indicators.js` file. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/mfi - * Money Flow Index Indicator - * @since 6.0.0 - * @optionparent plotOptions.mfi - */ - - { - /** - * @excluding index - */ - params: { - period: 14, - /** - * The id of volume series which is mandatory. - * For example using OHLC data, volumeSeriesID='volume' means - * the indicator will be calculated using OHLC and volume values. - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - volumeSeriesID: 'volume', - /** - * Number of maximum decimals that are used in MFI calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - decimals: 4 - - } - }, { - nameBase: 'Money Flow Index', - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - decimals = params.decimals, - // MFI starts calculations from the second point - // Cause we need to calculate change between two points - range = 1, - volumeSeries = series.chart.get(params.volumeSeriesID), - yValVolume = volumeSeries && volumeSeries.yData, - MFI = [], - isUp = false, - xData = [], - yData = [], - positiveMoneyFlow = [], - negativeMoneyFlow = [], - newTypicalPrice, - oldTypicalPrice, - rawMoneyFlow, - negativeMoneyFlowSum, - positiveMoneyFlowSum, - moneyFlowRatio, - MFIPoint, i; - - if (!volumeSeries) { - return H.error( - 'Series ' + - params.volumeSeriesID + - ' not found! Check `volumeSeriesID`.', - true - ); - } - - // MFI requires high low and close values - if ( - (xVal.length <= period) || !isArray(yVal[0]) || - yVal[0].length !== 4 || - !yValVolume - ) { - return false; - } - // Calculate first typical price - newTypicalPrice = calculateTypicalPrice(yVal[range]); - // Accumulate first N-points - while (range < period + 1) { - // Calculate if up or down - oldTypicalPrice = newTypicalPrice; - newTypicalPrice = calculateTypicalPrice(yVal[range]); - isUp = newTypicalPrice >= oldTypicalPrice ? true : false; - // Calculate raw money flow - rawMoneyFlow = calculateRawMoneyFlow( - newTypicalPrice, - yValVolume[range] - ); - // Add to array - positiveMoneyFlow.push(isUp ? rawMoneyFlow : 0); - negativeMoneyFlow.push(isUp ? 0 : rawMoneyFlow); - range++; - } - for (i = range - 1; i < yValLen; i++) { - if (i > range - 1) { - // Remove first point from array - positiveMoneyFlow.shift(); - negativeMoneyFlow.shift(); - // Calculate if up or down - oldTypicalPrice = newTypicalPrice; - newTypicalPrice = calculateTypicalPrice(yVal[i]); - isUp = newTypicalPrice > oldTypicalPrice ? true : false; - // Calculate raw money flow - rawMoneyFlow = calculateRawMoneyFlow( - newTypicalPrice, - yValVolume[i] - ); - // Add to array - positiveMoneyFlow.push(isUp ? rawMoneyFlow : 0); - negativeMoneyFlow.push(isUp ? 0 : rawMoneyFlow); - } - - // Calculate sum of negative and positive money flow: - negativeMoneyFlowSum = sumArray(negativeMoneyFlow); - positiveMoneyFlowSum = sumArray(positiveMoneyFlow); - - moneyFlowRatio = positiveMoneyFlowSum / negativeMoneyFlowSum; - MFIPoint = toFixed( - 100 - (100 / (1 + moneyFlowRatio)), - decimals - ); - MFI.push([xVal[i], MFIPoint]); - xData.push(xVal[i]); - yData.push(MFIPoint); - } - - return { - values: MFI, - xData: xData, - yData: yData - }; - } - } +H.seriesType('mfi', 'sma', + + /** + * Money Flow Index. This series requires `linkedTo` option to be set and + * should be loaded after the `stock/indicators/indicators.js` file. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/mfi + * Money Flow Index Indicator + * @since 6.0.0 + * @optionparent plotOptions.mfi + */ + + { + /** + * @excluding index + */ + params: { + period: 14, + /** + * The id of volume series which is mandatory. + * For example using OHLC data, volumeSeriesID='volume' means + * the indicator will be calculated using OHLC and volume values. + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + volumeSeriesID: 'volume', + /** + * Number of maximum decimals that are used in MFI calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + decimals: 4 + + } + }, { + nameBase: 'Money Flow Index', + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + decimals = params.decimals, + // MFI starts calculations from the second point + // Cause we need to calculate change between two points + range = 1, + volumeSeries = series.chart.get(params.volumeSeriesID), + yValVolume = volumeSeries && volumeSeries.yData, + MFI = [], + isUp = false, + xData = [], + yData = [], + positiveMoneyFlow = [], + negativeMoneyFlow = [], + newTypicalPrice, + oldTypicalPrice, + rawMoneyFlow, + negativeMoneyFlowSum, + positiveMoneyFlowSum, + moneyFlowRatio, + MFIPoint, i; + + if (!volumeSeries) { + return H.error( + 'Series ' + + params.volumeSeriesID + + ' not found! Check `volumeSeriesID`.', + true + ); + } + + // MFI requires high low and close values + if ( + (xVal.length <= period) || !isArray(yVal[0]) || + yVal[0].length !== 4 || + !yValVolume + ) { + return false; + } + // Calculate first typical price + newTypicalPrice = calculateTypicalPrice(yVal[range]); + // Accumulate first N-points + while (range < period + 1) { + // Calculate if up or down + oldTypicalPrice = newTypicalPrice; + newTypicalPrice = calculateTypicalPrice(yVal[range]); + isUp = newTypicalPrice >= oldTypicalPrice ? true : false; + // Calculate raw money flow + rawMoneyFlow = calculateRawMoneyFlow( + newTypicalPrice, + yValVolume[range] + ); + // Add to array + positiveMoneyFlow.push(isUp ? rawMoneyFlow : 0); + negativeMoneyFlow.push(isUp ? 0 : rawMoneyFlow); + range++; + } + for (i = range - 1; i < yValLen; i++) { + if (i > range - 1) { + // Remove first point from array + positiveMoneyFlow.shift(); + negativeMoneyFlow.shift(); + // Calculate if up or down + oldTypicalPrice = newTypicalPrice; + newTypicalPrice = calculateTypicalPrice(yVal[i]); + isUp = newTypicalPrice > oldTypicalPrice ? true : false; + // Calculate raw money flow + rawMoneyFlow = calculateRawMoneyFlow( + newTypicalPrice, + yValVolume[i] + ); + // Add to array + positiveMoneyFlow.push(isUp ? rawMoneyFlow : 0); + negativeMoneyFlow.push(isUp ? 0 : rawMoneyFlow); + } + + // Calculate sum of negative and positive money flow: + negativeMoneyFlowSum = sumArray(negativeMoneyFlow); + positiveMoneyFlowSum = sumArray(positiveMoneyFlow); + + moneyFlowRatio = positiveMoneyFlowSum / negativeMoneyFlowSum; + MFIPoint = toFixed( + 100 - (100 / (1 + moneyFlowRatio)), + decimals + ); + MFI.push([xVal[i], MFIPoint]); + xData.push(xVal[i]); + yData.push(MFIPoint); + } + + return { + values: MFI, + xData: xData, + yData: yData + }; + } + } ); /** diff --git a/js/indicators/momentum.src.js b/js/indicators/momentum.src.js index d0f3106641a..f6f5332ac98 100644 --- a/js/indicators/momentum.src.js +++ b/js/indicators/momentum.src.js @@ -3,15 +3,15 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var isArray = H.isArray, - seriesType = H.seriesType; + seriesType = H.seriesType; function populateAverage(points, xVal, yVal, i, period) { - var mmY = yVal[i - 1][3] - yVal[i - period - 1][3], - mmX = xVal[i - 1]; - - points.shift(); // remove point until range < period + var mmY = yVal[i - 1][3] - yVal[i - period - 1][3], + mmX = xVal[i - 1]; - return [mmX, mmY]; + points.shift(); // remove point until range < period + + return [mmX, mmY]; } /** @@ -20,78 +20,78 @@ function populateAverage(points, xVal, yVal, i, period) { * @constructor seriesTypes.momentum * @augments seriesTypes.sma */ -seriesType('momentum', 'sma', - /** - * Momentum. This series requires `linkedTo` option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/momentum Momentum indicator - * @since 6.0.0 - * @optionparent plotOptions.momentum - */ - { - params: { - period: 14 - } - }, { - nameBase: 'Momentum', - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - xValue = xVal[0], - yValue = yVal[0], - MM = [], - xData = [], - yData = [], - index, - i, - points, - MMPoint; +seriesType('momentum', 'sma', + /** + * Momentum. This series requires `linkedTo` option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/momentum Momentum indicator + * @since 6.0.0 + * @optionparent plotOptions.momentum + */ + { + params: { + period: 14 + } + }, { + nameBase: 'Momentum', + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + xValue = xVal[0], + yValue = yVal[0], + MM = [], + xData = [], + yData = [], + index, + i, + points, + MMPoint; + + if (xVal.length <= period) { + return false; + } - if (xVal.length <= period) { - return false; - } + // Switch index for OHLC / Candlestick / Arearange + if (isArray(yVal[0])) { + yValue = yVal[0][3]; + } else { + return false; + } + // Starting point + points = [ + [xValue, yValue] + ]; - // Switch index for OHLC / Candlestick / Arearange - if (isArray(yVal[0])) { - yValue = yVal[0][3]; - } else { - return false; - } - // Starting point - points = [ - [xValue, yValue] - ]; + // Calculate value one-by-one for each perdio in visible data + for (i = (period + 1); i < yValLen; i++) { + MMPoint = populateAverage(points, xVal, yVal, i, period, index); + MM.push(MMPoint); + xData.push(MMPoint[0]); + yData.push(MMPoint[1]); + } - // Calculate value one-by-one for each perdio in visible data - for (i = (period + 1); i < yValLen; i++) { - MMPoint = populateAverage(points, xVal, yVal, i, period, index); - MM.push(MMPoint); - xData.push(MMPoint[0]); - yData.push(MMPoint[1]); - } - - MMPoint = populateAverage(points, xVal, yVal, i, period, index); - MM.push(MMPoint); - xData.push(MMPoint[0]); - yData.push(MMPoint[1]); + MMPoint = populateAverage(points, xVal, yVal, i, period, index); + MM.push(MMPoint); + xData.push(MMPoint[0]); + yData.push(MMPoint[1]); - return { - values: MM, - xData: xData, - yData: yData - }; - } - }); + return { + values: MM, + xData: xData, + yData: yData + }; + } + }); /** * A `Momentum` series. If the [type](#series.momentum.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.momentum diff --git a/js/indicators/pivot-points.src.js b/js/indicators/pivot-points.src.js index 312efa49796..0eab5028ddf 100644 --- a/js/indicators/pivot-points.src.js +++ b/js/indicators/pivot-points.src.js @@ -4,321 +4,321 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var each = H.each, - defined = H.defined, - isArray = H.isArray, - SMA = H.seriesTypes.sma; + defined = H.defined, + isArray = H.isArray, + SMA = H.seriesTypes.sma; function destroyExtraLabels(point, functionName) { - var props = point.series.pointArrayMap, - prop, - i = props.length; - - SMA.prototype.pointClass.prototype[functionName].call(point); - - while (i--) { - prop = 'dataLabel' + props[i]; - // S4 dataLabel could be removed by parent method: - if (point[prop] && point[prop].element) { - point[prop].destroy(); - } - point[prop] = null; - } + var props = point.series.pointArrayMap, + prop, + i = props.length; + + SMA.prototype.pointClass.prototype[functionName].call(point); + + while (i--) { + prop = 'dataLabel' + props[i]; + // S4 dataLabel could be removed by parent method: + if (point[prop] && point[prop].element) { + point[prop].destroy(); + } + point[prop] = null; + } } H.seriesType('pivotpoints', 'sma', - /** - * Pivot points indicator. This series requires the `linkedTo` option to be - * set and should be loaded after `stock/indicators/indicators.js` file. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/pivot-points - * Pivot points - * @since 6.0.0 - * @optionparent plotOptions.pivotpoints - */ - { - /** - * @excluding index - */ - params: { - period: 28, - /** - * Algorithm used to calculate ressistance and support lines based - * on pivot points. Implemented algorithms: `'standard'`, - * `'fibonacci'` and `'camarilla'` - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - algorithm: 'standard' - }, - marker: { - enabled: false - }, - enableMouseTracking: false, - dataLabels: { - enabled: true, - format: '{point.pivotLine}' - }, - dataGrouping: { - approximation: 'averages' - } - }, { - nameBase: 'Pivot Points', - pointArrayMap: ['R4', 'R3', 'R2', 'R1', 'P', 'S1', 'S2', 'S3', 'S4'], - pointValKey: 'P', - toYData: function (point) { - return [point.P]; // The rest should not affect extremes - }, - translate: function () { - var indicator = this; - - SMA.prototype.translate.apply(indicator); - - each(indicator.points, function (point) { - each(indicator.pointArrayMap, function (value) { - if (defined(point[value])) { - point['plot' + value] = indicator.yAxis.toPixels( - point[value], - true - ); - } - }); - }); - - // Pivot points are rendered as horizontal lines - // And last point start not from the next one (as it's the last one) - // But from the approximated last position in a given range - indicator.plotEndPoint = indicator.xAxis.toPixels( - indicator.endPoint, - true - ); - }, - getGraphPath: function (points) { - var indicator = this, - pointsLength = points.length, - allPivotPoints = [[], [], [], [], [], [], [], [], []], - path = [], - endPoint = indicator.plotEndPoint, - pointArrayMapLength = indicator.pointArrayMap.length, - position, - point, - i; - - while (pointsLength--) { - point = points[pointsLength]; - for (i = 0; i < pointArrayMapLength; i++) { - position = indicator.pointArrayMap[i]; - - if (defined(point[position])) { - allPivotPoints[i].push({ - // Start left: - plotX: point.plotX, - plotY: point['plot' + position], - isNull: false - }, { - // Go to right: - plotX: endPoint, - plotY: point['plot' + position], - isNull: false - }, { - // And add null points in path to generate breaks: - plotX: endPoint, - plotY: null, - isNull: true - }); - } - } - endPoint = point.plotX; - } - - each(allPivotPoints, function (pivotPoints) { - path = path.concat( - SMA.prototype.getGraphPath.call(indicator, pivotPoints) - ); - }); - - return path; - }, - drawDataLabels: function () { - var indicator = this, - pointMapping = indicator.pointArrayMap, - currentLabel, - pointsLength, - point, - i; - - if (indicator.options.dataLabels.enabled) { - pointsLength = indicator.points.length; - - // For every Ressitance/Support group we need to render labels. - // Add one more item, which will just store dataLabels from - // previous iteration - each(pointMapping.concat([false]), function (position, k) { - i = pointsLength; - while (i--) { - point = indicator.points[i]; - - if (!position) { - // Store S4 dataLabel too: - point['dataLabel' + pointMapping[k - 1]] = - point.dataLabel; - } else { - point.y = point[position]; - point.pivotLine = position; - point.plotY = point['plot' + position]; - currentLabel = point['dataLabel' + position]; - - // Store previous label - if (k) { - point['dataLabel' + pointMapping[k - 1]] = - point.dataLabel; - } - - point.dataLabel = currentLabel = - currentLabel && currentLabel.element ? - currentLabel : - null; - } - } - SMA.prototype.drawDataLabels.apply(indicator, arguments); - }); - } - }, - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - placement = this[params.algorithm + 'Placement'], - PP = [], // 0- from, 1- to, 2- R1, 3- R2, 4- pivot, 5- S1 etc. - endTimestamp, - xData = [], - yData = [], - slicedXLen, - slicedX, - slicedY, - lastPP, - pivot, - avg, - i; - - // Pivot Points requires high, low and close values - if ( - xVal.length < period || - !isArray(yVal[0]) || - yVal[0].length !== 4 - ) { - return false; - } - - for (i = period + 1; i <= yValLen + period; i += period) { - slicedX = xVal.slice(i - period - 1, i); - slicedY = yVal.slice(i - period - 1, i); - - slicedXLen = slicedX.length; - - endTimestamp = slicedX[slicedXLen - 1]; - - pivot = this.getPivotAndHLC(slicedY); - avg = placement(pivot); - - lastPP = PP.push( - [endTimestamp] - .concat(avg) - ); - - xData.push(endTimestamp); - yData.push(PP[lastPP - 1].slice(1)); - } - - // We don't know exact position in ordinal axis - // So we use simple logic: - // Get first point in last range, calculate visible average range - // and multiply by period - this.endPoint = slicedX[0] + - ((endTimestamp - slicedX[0]) / slicedXLen) * period; - - return { - values: PP, - xData: xData, - yData: yData - }; - }, - getPivotAndHLC: function (values) { - var high = -Infinity, - low = Infinity, - close = values[values.length - 1][3], - pivot; - each(values, function (p) { - high = Math.max(high, p[1]); - low = Math.min(low, p[2]); - }); - pivot = (high + low + close) / 3; - - return [pivot, high, low, close]; - }, - standardPlacement: function (values) { - var diff = values[1] - values[2], - avg = [ - null, - null, - values[0] + diff, - values[0] * 2 - values[2], - values[0], - values[0] * 2 - values[1], - values[0] - diff, - null, - null - ]; - - return avg; - }, - camarillaPlacement: function (values) { - var diff = values[1] - values[2], - avg = [ - values[3] + diff * 1.5, - values[3] + diff * 1.25, - values[3] + diff * 1.1666, - values[3] + diff * 1.0833, - values[0], - values[3] - diff * 1.0833, - values[3] - diff * 1.1666, - values[3] - diff * 1.25, - values[3] - diff * 1.5 - ]; - - return avg; - }, - fibonacciPlacement: function (values) { - var diff = values[1] - values[2], - avg = [ - null, - values[0] + diff, - values[0] + diff * 0.618, - values[0] + diff * 0.382, - values[0], - values[0] - diff * 0.382, - values[0] - diff * 0.618, - values[0] - diff, - null - ]; - - return avg; - } - }, { - // Destroy labels: - // This method is called when cropping data: - destroyElements: function () { - destroyExtraLabels(this, 'destroyElements'); - }, - // This method is called when removing points, e.g. series.update() - destroy: function () { - destroyExtraLabels(this, 'destroyElements'); - } - } + /** + * Pivot points indicator. This series requires the `linkedTo` option to be + * set and should be loaded after `stock/indicators/indicators.js` file. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/pivot-points + * Pivot points + * @since 6.0.0 + * @optionparent plotOptions.pivotpoints + */ + { + /** + * @excluding index + */ + params: { + period: 28, + /** + * Algorithm used to calculate ressistance and support lines based + * on pivot points. Implemented algorithms: `'standard'`, + * `'fibonacci'` and `'camarilla'` + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + algorithm: 'standard' + }, + marker: { + enabled: false + }, + enableMouseTracking: false, + dataLabels: { + enabled: true, + format: '{point.pivotLine}' + }, + dataGrouping: { + approximation: 'averages' + } + }, { + nameBase: 'Pivot Points', + pointArrayMap: ['R4', 'R3', 'R2', 'R1', 'P', 'S1', 'S2', 'S3', 'S4'], + pointValKey: 'P', + toYData: function (point) { + return [point.P]; // The rest should not affect extremes + }, + translate: function () { + var indicator = this; + + SMA.prototype.translate.apply(indicator); + + each(indicator.points, function (point) { + each(indicator.pointArrayMap, function (value) { + if (defined(point[value])) { + point['plot' + value] = indicator.yAxis.toPixels( + point[value], + true + ); + } + }); + }); + + // Pivot points are rendered as horizontal lines + // And last point start not from the next one (as it's the last one) + // But from the approximated last position in a given range + indicator.plotEndPoint = indicator.xAxis.toPixels( + indicator.endPoint, + true + ); + }, + getGraphPath: function (points) { + var indicator = this, + pointsLength = points.length, + allPivotPoints = [[], [], [], [], [], [], [], [], []], + path = [], + endPoint = indicator.plotEndPoint, + pointArrayMapLength = indicator.pointArrayMap.length, + position, + point, + i; + + while (pointsLength--) { + point = points[pointsLength]; + for (i = 0; i < pointArrayMapLength; i++) { + position = indicator.pointArrayMap[i]; + + if (defined(point[position])) { + allPivotPoints[i].push({ + // Start left: + plotX: point.plotX, + plotY: point['plot' + position], + isNull: false + }, { + // Go to right: + plotX: endPoint, + plotY: point['plot' + position], + isNull: false + }, { + // And add null points in path to generate breaks: + plotX: endPoint, + plotY: null, + isNull: true + }); + } + } + endPoint = point.plotX; + } + + each(allPivotPoints, function (pivotPoints) { + path = path.concat( + SMA.prototype.getGraphPath.call(indicator, pivotPoints) + ); + }); + + return path; + }, + drawDataLabels: function () { + var indicator = this, + pointMapping = indicator.pointArrayMap, + currentLabel, + pointsLength, + point, + i; + + if (indicator.options.dataLabels.enabled) { + pointsLength = indicator.points.length; + + // For every Ressitance/Support group we need to render labels. + // Add one more item, which will just store dataLabels from + // previous iteration + each(pointMapping.concat([false]), function (position, k) { + i = pointsLength; + while (i--) { + point = indicator.points[i]; + + if (!position) { + // Store S4 dataLabel too: + point['dataLabel' + pointMapping[k - 1]] = + point.dataLabel; + } else { + point.y = point[position]; + point.pivotLine = position; + point.plotY = point['plot' + position]; + currentLabel = point['dataLabel' + position]; + + // Store previous label + if (k) { + point['dataLabel' + pointMapping[k - 1]] = + point.dataLabel; + } + + point.dataLabel = currentLabel = + currentLabel && currentLabel.element ? + currentLabel : + null; + } + } + SMA.prototype.drawDataLabels.apply(indicator, arguments); + }); + } + }, + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + placement = this[params.algorithm + 'Placement'], + PP = [], // 0- from, 1- to, 2- R1, 3- R2, 4- pivot, 5- S1 etc. + endTimestamp, + xData = [], + yData = [], + slicedXLen, + slicedX, + slicedY, + lastPP, + pivot, + avg, + i; + + // Pivot Points requires high, low and close values + if ( + xVal.length < period || + !isArray(yVal[0]) || + yVal[0].length !== 4 + ) { + return false; + } + + for (i = period + 1; i <= yValLen + period; i += period) { + slicedX = xVal.slice(i - period - 1, i); + slicedY = yVal.slice(i - period - 1, i); + + slicedXLen = slicedX.length; + + endTimestamp = slicedX[slicedXLen - 1]; + + pivot = this.getPivotAndHLC(slicedY); + avg = placement(pivot); + + lastPP = PP.push( + [endTimestamp] + .concat(avg) + ); + + xData.push(endTimestamp); + yData.push(PP[lastPP - 1].slice(1)); + } + + // We don't know exact position in ordinal axis + // So we use simple logic: + // Get first point in last range, calculate visible average range + // and multiply by period + this.endPoint = slicedX[0] + + ((endTimestamp - slicedX[0]) / slicedXLen) * period; + + return { + values: PP, + xData: xData, + yData: yData + }; + }, + getPivotAndHLC: function (values) { + var high = -Infinity, + low = Infinity, + close = values[values.length - 1][3], + pivot; + each(values, function (p) { + high = Math.max(high, p[1]); + low = Math.min(low, p[2]); + }); + pivot = (high + low + close) / 3; + + return [pivot, high, low, close]; + }, + standardPlacement: function (values) { + var diff = values[1] - values[2], + avg = [ + null, + null, + values[0] + diff, + values[0] * 2 - values[2], + values[0], + values[0] * 2 - values[1], + values[0] - diff, + null, + null + ]; + + return avg; + }, + camarillaPlacement: function (values) { + var diff = values[1] - values[2], + avg = [ + values[3] + diff * 1.5, + values[3] + diff * 1.25, + values[3] + diff * 1.1666, + values[3] + diff * 1.0833, + values[0], + values[3] - diff * 1.0833, + values[3] - diff * 1.1666, + values[3] - diff * 1.25, + values[3] - diff * 1.5 + ]; + + return avg; + }, + fibonacciPlacement: function (values) { + var diff = values[1] - values[2], + avg = [ + null, + values[0] + diff, + values[0] + diff * 0.618, + values[0] + diff * 0.382, + values[0], + values[0] - diff * 0.382, + values[0] - diff * 0.618, + values[0] - diff, + null + ]; + + return avg; + } + }, { + // Destroy labels: + // This method is called when cropping data: + destroyElements: function () { + destroyExtraLabels(this, 'destroyElements'); + }, + // This method is called when removing points, e.g. series.update() + destroy: function () { + destroyExtraLabels(this, 'destroyElements'); + } + } ); /** diff --git a/js/indicators/price-envelopes.src.js b/js/indicators/price-envelopes.src.js index 4c9764c4597..f455ff9da85 100644 --- a/js/indicators/price-envelopes.src.js +++ b/js/indicators/price-envelopes.src.js @@ -4,238 +4,238 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var each = H.each, - merge = H.merge, - isArray = H.isArray, - SMA = H.seriesTypes.sma; + merge = H.merge, + isArray = H.isArray, + SMA = H.seriesTypes.sma; H.seriesType('priceenvelopes', 'sma', - /** - * Price envelopes indicator based on [SMA](#plotOptions.sma) calculations. - * This series requires the `linkedTo` option to be set and should be loaded - * after the `stock/indicators/indicators.js` file. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/price-envelopes - * Price envelopes - * @since 6.0.0 - * @optionparent plotOptions.priceenvelopes - */ - { - marker: { - enabled: false - }, - tooltip: { - pointFormat: '\u25CF {series.name}
Top: {point.top}
Middle: {point.middle}
Bottom: {point.bottom}
' - }, - params: { - period: 20, - /** - * Percentage above the moving average that should be displayed. - * 0.1 means 110%. Relative to the calculated value. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - topBand: 0.1, - /** - * Percentage below the moving average that should be displayed. - * 0.1 means 90%. Relative to the calculated value. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - bottomBand: 0.1 - }, - /** - * Bottom line options. - * - * @since 6.0.0 - * @product highstock - */ - bottomLine: { - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. If not set, it's inherited from - * [plotOptions.priceenvelopes.color]( - * #plotOptions.priceenvelopes.color). - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - /** - * Top line options. - * - * @extends {plotOptions.priceenvelopes.bottomLine} - * @since 6.0.0 - * @product highstock - */ - topLine: { - styles: { - lineWidth: 1 - } - }, - dataGrouping: { - approximation: 'averages' - } - }, /** @lends Highcharts.Series.prototype */ { - nameComponents: ['period', 'topBand', 'bottomBand'], - nameBase: 'Price envelopes', - pointArrayMap: ['top', 'middle', 'bottom'], - parallelArrays: ['x', 'y', 'top', 'bottom'], - pointValKey: 'middle', - init: function () { - SMA.prototype.init.apply(this, arguments); + /** + * Price envelopes indicator based on [SMA](#plotOptions.sma) calculations. + * This series requires the `linkedTo` option to be set and should be loaded + * after the `stock/indicators/indicators.js` file. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/price-envelopes + * Price envelopes + * @since 6.0.0 + * @optionparent plotOptions.priceenvelopes + */ + { + marker: { + enabled: false + }, + tooltip: { + pointFormat: '\u25CF {series.name}
Top: {point.top}
Middle: {point.middle}
Bottom: {point.bottom}
' + }, + params: { + period: 20, + /** + * Percentage above the moving average that should be displayed. + * 0.1 means 110%. Relative to the calculated value. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + topBand: 0.1, + /** + * Percentage below the moving average that should be displayed. + * 0.1 means 90%. Relative to the calculated value. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + bottomBand: 0.1 + }, + /** + * Bottom line options. + * + * @since 6.0.0 + * @product highstock + */ + bottomLine: { + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. If not set, it's inherited from + * [plotOptions.priceenvelopes.color]( + * #plotOptions.priceenvelopes.color). + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + /** + * Top line options. + * + * @extends {plotOptions.priceenvelopes.bottomLine} + * @since 6.0.0 + * @product highstock + */ + topLine: { + styles: { + lineWidth: 1 + } + }, + dataGrouping: { + approximation: 'averages' + } + }, /** @lends Highcharts.Series.prototype */ { + nameComponents: ['period', 'topBand', 'bottomBand'], + nameBase: 'Price envelopes', + pointArrayMap: ['top', 'middle', 'bottom'], + parallelArrays: ['x', 'y', 'top', 'bottom'], + pointValKey: 'middle', + init: function () { + SMA.prototype.init.apply(this, arguments); - // Set default color for lines: - this.options = merge({ - topLine: { - styles: { - lineColor: this.color - } - }, - bottomLine: { - styles: { - lineColor: this.color - } - } - }, this.options); - }, - toYData: function (point) { - return [point.top, point.middle, point.bottom]; - }, - translate: function () { - var indicator = this, - translatedEnvelopes = ['plotTop', 'plotMiddle', 'plotBottom']; + // Set default color for lines: + this.options = merge({ + topLine: { + styles: { + lineColor: this.color + } + }, + bottomLine: { + styles: { + lineColor: this.color + } + } + }, this.options); + }, + toYData: function (point) { + return [point.top, point.middle, point.bottom]; + }, + translate: function () { + var indicator = this, + translatedEnvelopes = ['plotTop', 'plotMiddle', 'plotBottom']; - SMA.prototype.translate.apply(indicator); + SMA.prototype.translate.apply(indicator); - each(indicator.points, function (point) { - each( - [point.top, point.middle, point.bottom], - function (value, i) { - if (value !== null) { - point[translatedEnvelopes[i]] = - indicator.yAxis.toPixels(value, true); - } - } - ); - }); - }, - drawGraph: function () { - var indicator = this, - middleLinePoints = indicator.points, - pointsLength = middleLinePoints.length, - middleLineOptions = indicator.options, - middleLinePath = indicator.graph, - gappedExtend = { - options: { - gapSize: middleLineOptions.gapSize - } - }, - deviations = [[], []], // top and bottom point place holders - point; + each(indicator.points, function (point) { + each( + [point.top, point.middle, point.bottom], + function (value, i) { + if (value !== null) { + point[translatedEnvelopes[i]] = + indicator.yAxis.toPixels(value, true); + } + } + ); + }); + }, + drawGraph: function () { + var indicator = this, + middleLinePoints = indicator.points, + pointsLength = middleLinePoints.length, + middleLineOptions = indicator.options, + middleLinePath = indicator.graph, + gappedExtend = { + options: { + gapSize: middleLineOptions.gapSize + } + }, + deviations = [[], []], // top and bottom point place holders + point; - // Generate points for top and bottom lines: - while (pointsLength--) { - point = middleLinePoints[pointsLength]; - deviations[0].push({ - plotX: point.plotX, - plotY: point.plotTop, - isNull: point.isNull - }); - deviations[1].push({ - plotX: point.plotX, - plotY: point.plotBottom, - isNull: point.isNull - }); - } + // Generate points for top and bottom lines: + while (pointsLength--) { + point = middleLinePoints[pointsLength]; + deviations[0].push({ + plotX: point.plotX, + plotY: point.plotTop, + isNull: point.isNull + }); + deviations[1].push({ + plotX: point.plotX, + plotY: point.plotBottom, + isNull: point.isNull + }); + } - // Modify options and generate lines: - each(['topLine', 'bottomLine'], function (lineName, i) { - indicator.points = deviations[i]; - indicator.options = merge( - middleLineOptions[lineName].styles, - gappedExtend - ); - indicator.graph = indicator['graph' + lineName]; - SMA.prototype.drawGraph.call(indicator); + // Modify options and generate lines: + each(['topLine', 'bottomLine'], function (lineName, i) { + indicator.points = deviations[i]; + indicator.options = merge( + middleLineOptions[lineName].styles, + gappedExtend + ); + indicator.graph = indicator['graph' + lineName]; + SMA.prototype.drawGraph.call(indicator); - // Now save lines: - indicator['graph' + lineName] = indicator.graph; - }); + // Now save lines: + indicator['graph' + lineName] = indicator.graph; + }); - // Restore options and draw a middle line: - indicator.points = middleLinePoints; - indicator.options = middleLineOptions; - indicator.graph = middleLinePath; - SMA.prototype.drawGraph.call(indicator); - }, - getValues: function (series, params) { - var period = params.period, - topPercent = params.topBand, - botPercent = params.bottomBand, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - PE = [], // 0- date, 1-top line, 2-middle line, 3-bottom line - ML, TL, BL, // middle line, top line and bottom line - date, - xData = [], - yData = [], - slicedX, - slicedY, - point, - i; + // Restore options and draw a middle line: + indicator.points = middleLinePoints; + indicator.options = middleLineOptions; + indicator.graph = middleLinePath; + SMA.prototype.drawGraph.call(indicator); + }, + getValues: function (series, params) { + var period = params.period, + topPercent = params.topBand, + botPercent = params.bottomBand, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + PE = [], // 0- date, 1-top line, 2-middle line, 3-bottom line + ML, TL, BL, // middle line, top line and bottom line + date, + xData = [], + yData = [], + slicedX, + slicedY, + point, + i; - // Price envelopes requires close value - if ( - xVal.length < period || - !isArray(yVal[0]) || - yVal[0].length !== 4 - ) { - return false; - } + // Price envelopes requires close value + if ( + xVal.length < period || + !isArray(yVal[0]) || + yVal[0].length !== 4 + ) { + return false; + } - for (i = period; i <= yValLen; i++) { - slicedX = xVal.slice(i - period, i); - slicedY = yVal.slice(i - period, i); + for (i = period; i <= yValLen; i++) { + slicedX = xVal.slice(i - period, i); + slicedY = yVal.slice(i - period, i); - point = SMA.prototype.getValues.call(this, { - xData: slicedX, - yData: slicedY - }, params); + point = SMA.prototype.getValues.call(this, { + xData: slicedX, + yData: slicedY + }, params); - date = point.xData[0]; - ML = point.yData[0]; - TL = ML * (1 + topPercent); - BL = ML * (1 - botPercent); - PE.push([date, TL, ML, BL]); - xData.push(date); - yData.push([TL, ML, BL]); - } + date = point.xData[0]; + ML = point.yData[0]; + TL = ML * (1 + topPercent); + BL = ML * (1 - botPercent); + PE.push([date, TL, ML, BL]); + xData.push(date); + yData.push([TL, ML, BL]); + } - return { - values: PE, - xData: xData, - yData: yData - }; - } - } + return { + values: PE, + xData: xData, + yData: yData + }; + } + } ); /** diff --git a/js/indicators/psar.src.js b/js/indicators/psar.src.js index b8dd1172c40..7c192d05655 100755 --- a/js/indicators/psar.src.js +++ b/js/indicators/psar.src.js @@ -16,20 +16,20 @@ import '../parts/Utilities.js'; // Utils: function toFixed(a, n) { - return parseFloat(a.toFixed(n)); + return parseFloat(a.toFixed(n)); } function calculateDirection(previousDirection, low, high, PSAR) { - if ( - (previousDirection === 1 && low > PSAR) || - (previousDirection === -1 && high > PSAR) - ) { - return 1; - } - return -1; + if ( + (previousDirection === 1 && low > PSAR) || + (previousDirection === -1 && high > PSAR) + ) { + return 1; + } + return -1; } -/* +/* * Method for calculating acceleration factor * dir - direction * pDir - previous Direction @@ -40,33 +40,33 @@ function calculateDirection(previousDirection, low, high, PSAR) { * initAcc - initial acceleration factor */ function getAccelerationFactor(dir, pDir, eP, pEP, pAcc, inc, maxAcc, initAcc) { - if (dir === pDir) { - if (dir === 1 && (eP > pEP)) { - return (pAcc === maxAcc) ? maxAcc : toFixed(pAcc + inc, 2); - } else if (dir === -1 && (eP < pEP)) { - return (pAcc === maxAcc) ? maxAcc : toFixed(pAcc + inc, 2); - } - return pAcc; - } - return initAcc; + if (dir === pDir) { + if (dir === 1 && (eP > pEP)) { + return (pAcc === maxAcc) ? maxAcc : toFixed(pAcc + inc, 2); + } else if (dir === -1 && (eP < pEP)) { + return (pAcc === maxAcc) ? maxAcc : toFixed(pAcc + inc, 2); + } + return pAcc; + } + return initAcc; } function getExtremePoint(high, low, previousDirection, previousExtremePoint) { - if (previousDirection === 1) { - return (high > previousExtremePoint) ? high : previousExtremePoint; - } - return (low < previousExtremePoint) ? low : previousExtremePoint; + if (previousDirection === 1) { + return (high > previousExtremePoint) ? high : previousExtremePoint; + } + return (low < previousExtremePoint) ? low : previousExtremePoint; } function getEPMinusPSAR(EP, PSAR) { - return EP - PSAR; + return EP - PSAR; } function getAccelerationFactorMultiply(accelerationFactor, EPMinusSAR) { - return accelerationFactor * EPMinusSAR; + return accelerationFactor * EPMinusSAR; } -/* +/* * Method for calculating PSAR * pdir - previous direction * sDir - second previous Direction @@ -79,17 +79,17 @@ function getAccelerationFactorMultiply(accelerationFactor, EPMinusSAR) { * pEP - previous extreme point */ function getPSAR(pdir, sDir, PSAR, pACCMulti, sLow, pLow, pHigh, sHigh, pEP) { - if (pdir === sDir) { - if (pdir === 1) { - return (PSAR + pACCMulti < Math.min(sLow, pLow)) ? - PSAR + pACCMulti : - Math.min(sLow, pLow); - } - return (PSAR + pACCMulti > Math.max(sHigh, pHigh)) ? - PSAR + pACCMulti : - Math.max(sHigh, pHigh); - } - return pEP; + if (pdir === sDir) { + if (pdir === 1) { + return (PSAR + pACCMulti < Math.min(sLow, pLow)) ? + PSAR + pACCMulti : + Math.min(sLow, pLow); + } + return (PSAR + pACCMulti > Math.max(sHigh, pHigh)) ? + PSAR + pACCMulti : + Math.max(sHigh, pHigh); + } + return pEP; } @@ -100,207 +100,207 @@ function getPSAR(pdir, sDir, PSAR, pACCMulti, sLow, pLow, pHigh, sHigh, pEP) { * @constructor seriesTypes.psar * @augments seriesTypes.sma */ -H.seriesType('psar', 'sma', +H.seriesType('psar', 'sma', - /** - * Parabolic SAR. This series requires `linkedTo` - * option to be set and should be loaded - * after `stock/indicators/indicators.js` file. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/psar - * Parabolic SAR Indicator - * @since 6.0.0 - * @optionparent plotOptions.psar - */ + /** + * Parabolic SAR. This series requires `linkedTo` + * option to be set and should be loaded + * after `stock/indicators/indicators.js` file. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/psar + * Parabolic SAR Indicator + * @since 6.0.0 + * @optionparent plotOptions.psar + */ - { - lineWidth: 0, - marker: { - enabled: true - }, - states: { - hover: { - lineWidthPlus: 0 - } - }, - /** - * @excluding index - * @excluding period - */ - params: { - /** - * The initial value for acceleration factor. - * Acceleration factor is starting with this value - * and increases by specified increment each time - * the extreme point makes a new high. - * AF can reach a maximum of maxAccelerationFactor, - * no matter how long the uptrend extends. - * - * @type {Number} - * @since 6.0.0 - * @excluding period - * @product highstock - */ - initialAccelerationFactor: 0.02, - /** - * The Maximum value for acceleration factor. - * AF can reach a maximum of maxAccelerationFactor, - * no matter how long the uptrend extends. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - maxAccelerationFactor: 0.2, - /** - * Acceleration factor increases by increment each time - * the extreme point makes a new high. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - increment: 0.02, - /** - * Index from which PSAR is starting calculation - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - index: 2, - /** - * Number of maximum decimals that are used in PSAR calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - decimals: 4 - } - }, { - nameComponents: false, - getValues: function (series, params) { - var xVal = series.xData, - yVal = series.yData, - // Extreme point is the lowest low for falling and highest high - // for rising psar - and we are starting with falling - extremePoint = yVal[0][1], - accelerationFactor = params.initialAccelerationFactor, - maxAccelerationFactor = params.maxAccelerationFactor, - increment = params.increment, - // Set initial acc factor (for every new trend!) - initialAccelerationFactor = params.initialAccelerationFactor, - PSAR = yVal[0][2], - decimals = params.decimals, - index = params.index, - PSARArr = [], - xData = [], - yData = [], - previousDirection = 1, - direction, EPMinusPSAR, accelerationFactorMultiply, - newDirection, - prevLow, - prevPrevLow, - prevHigh, - prevPrevHigh, - newExtremePoint, - high, low, ind; + { + lineWidth: 0, + marker: { + enabled: true + }, + states: { + hover: { + lineWidthPlus: 0 + } + }, + /** + * @excluding index + * @excluding period + */ + params: { + /** + * The initial value for acceleration factor. + * Acceleration factor is starting with this value + * and increases by specified increment each time + * the extreme point makes a new high. + * AF can reach a maximum of maxAccelerationFactor, + * no matter how long the uptrend extends. + * + * @type {Number} + * @since 6.0.0 + * @excluding period + * @product highstock + */ + initialAccelerationFactor: 0.02, + /** + * The Maximum value for acceleration factor. + * AF can reach a maximum of maxAccelerationFactor, + * no matter how long the uptrend extends. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + maxAccelerationFactor: 0.2, + /** + * Acceleration factor increases by increment each time + * the extreme point makes a new high. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + increment: 0.02, + /** + * Index from which PSAR is starting calculation + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + index: 2, + /** + * Number of maximum decimals that are used in PSAR calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + decimals: 4 + } + }, { + nameComponents: false, + getValues: function (series, params) { + var xVal = series.xData, + yVal = series.yData, + // Extreme point is the lowest low for falling and highest high + // for rising psar - and we are starting with falling + extremePoint = yVal[0][1], + accelerationFactor = params.initialAccelerationFactor, + maxAccelerationFactor = params.maxAccelerationFactor, + increment = params.increment, + // Set initial acc factor (for every new trend!) + initialAccelerationFactor = params.initialAccelerationFactor, + PSAR = yVal[0][2], + decimals = params.decimals, + index = params.index, + PSARArr = [], + xData = [], + yData = [], + previousDirection = 1, + direction, EPMinusPSAR, accelerationFactorMultiply, + newDirection, + prevLow, + prevPrevLow, + prevHigh, + prevPrevHigh, + newExtremePoint, + high, low, ind; - for (ind = 0; ind < index; ind++) { - extremePoint = Math.max(yVal[ind][1], extremePoint); - PSAR = Math.min(yVal[ind][2], toFixed(PSAR, decimals)); - } + for (ind = 0; ind < index; ind++) { + extremePoint = Math.max(yVal[ind][1], extremePoint); + PSAR = Math.min(yVal[ind][2], toFixed(PSAR, decimals)); + } - direction = (yVal[ind][1] > PSAR) ? 1 : -1; - EPMinusPSAR = getEPMinusPSAR(extremePoint, PSAR); - accelerationFactor = params.initialAccelerationFactor; - accelerationFactorMultiply = getAccelerationFactorMultiply( - accelerationFactor, - EPMinusPSAR - ); + direction = (yVal[ind][1] > PSAR) ? 1 : -1; + EPMinusPSAR = getEPMinusPSAR(extremePoint, PSAR); + accelerationFactor = params.initialAccelerationFactor; + accelerationFactorMultiply = getAccelerationFactorMultiply( + accelerationFactor, + EPMinusPSAR + ); - PSARArr.push([xVal[index], PSAR]); - xData.push(xVal[index]); - yData.push(toFixed(PSAR, decimals)); + PSARArr.push([xVal[index], PSAR]); + xData.push(xVal[index]); + yData.push(toFixed(PSAR, decimals)); - for (ind = index + 1; ind < yVal.length; ind++) { + for (ind = index + 1; ind < yVal.length; ind++) { - prevLow = yVal[ind - 1][2]; - prevPrevLow = yVal[ind - 2][2]; - prevHigh = yVal[ind - 1][1]; - prevPrevHigh = yVal[ind - 2][1]; - high = yVal[ind][1]; - low = yVal[ind][2]; + prevLow = yVal[ind - 1][2]; + prevPrevLow = yVal[ind - 2][2]; + prevHigh = yVal[ind - 1][1]; + prevPrevHigh = yVal[ind - 2][1]; + high = yVal[ind][1]; + low = yVal[ind][2]; - // Null points break PSAR - if ( - prevPrevLow !== null && - prevPrevHigh !== null && - prevLow !== null && - prevHigh !== null && - high !== null && - low !== null - ) { - PSAR = getPSAR( - direction, - previousDirection, - PSAR, - accelerationFactorMultiply, - prevPrevLow, - prevLow, - prevHigh, - prevPrevHigh, - extremePoint - ); + // Null points break PSAR + if ( + prevPrevLow !== null && + prevPrevHigh !== null && + prevLow !== null && + prevHigh !== null && + high !== null && + low !== null + ) { + PSAR = getPSAR( + direction, + previousDirection, + PSAR, + accelerationFactorMultiply, + prevPrevLow, + prevLow, + prevHigh, + prevPrevHigh, + extremePoint + ); - newExtremePoint = getExtremePoint( - high, - low, - direction, - extremePoint - ); - newDirection = calculateDirection( - previousDirection, - low, - high, - PSAR - ); - accelerationFactor = getAccelerationFactor( - newDirection, - direction, - newExtremePoint, - extremePoint, - accelerationFactor, - increment, - maxAccelerationFactor, - initialAccelerationFactor - ); + newExtremePoint = getExtremePoint( + high, + low, + direction, + extremePoint + ); + newDirection = calculateDirection( + previousDirection, + low, + high, + PSAR + ); + accelerationFactor = getAccelerationFactor( + newDirection, + direction, + newExtremePoint, + extremePoint, + accelerationFactor, + increment, + maxAccelerationFactor, + initialAccelerationFactor + ); - EPMinusPSAR = getEPMinusPSAR(newExtremePoint, PSAR); - accelerationFactorMultiply = getAccelerationFactorMultiply( - accelerationFactor, - EPMinusPSAR - ); - PSARArr.push([xVal[ind], toFixed(PSAR, decimals)]); - xData.push(xVal[ind]); - yData.push(toFixed(PSAR, decimals)); + EPMinusPSAR = getEPMinusPSAR(newExtremePoint, PSAR); + accelerationFactorMultiply = getAccelerationFactorMultiply( + accelerationFactor, + EPMinusPSAR + ); + PSARArr.push([xVal[ind], toFixed(PSAR, decimals)]); + xData.push(xVal[ind]); + yData.push(toFixed(PSAR, decimals)); - previousDirection = direction; - direction = newDirection; - extremePoint = newExtremePoint; - } - } - return { - values: PSARArr, - xData: xData, - yData: yData - }; - } - } + previousDirection = direction; + direction = newDirection; + extremePoint = newExtremePoint; + } + } + return { + values: PSARArr, + xData: xData, + yData: yData + }; + } + } ); /** diff --git a/js/indicators/roc.src.js b/js/indicators/roc.src.js index 0f9d44b740a..0b8b715c417 100644 --- a/js/indicators/roc.src.js +++ b/js/indicators/roc.src.js @@ -9,35 +9,35 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var seriesType = H.seriesType, - isArray = H.isArray; + isArray = H.isArray; // Utils: function populateAverage(xVal, yVal, i, period, index) { - /** - * Calculated as: - * (Closing Price [today] - Closing Price [n days ago]) / - * Closing Price [n days ago] * 100 - * - * Return y as null when avoiding division by zero - */ - var nDaysAgoY, - rocY; + /** + * Calculated as: + * (Closing Price [today] - Closing Price [n days ago]) / + * Closing Price [n days ago] * 100 + * + * Return y as null when avoiding division by zero + */ + var nDaysAgoY, + rocY; - if (index < 0) { - // y data given as an array of values - nDaysAgoY = yVal[i - period]; - rocY = nDaysAgoY ? - (yVal[i] - nDaysAgoY) / nDaysAgoY * 100 : - null; - } else { - // y data given as an array of arrays and the index should be used - nDaysAgoY = yVal[i - period][index]; - rocY = nDaysAgoY ? - (yVal[i][index] - nDaysAgoY) / nDaysAgoY * 100 : - null; - } - - return [xVal[i], rocY]; + if (index < 0) { + // y data given as an array of values + nDaysAgoY = yVal[i - period]; + rocY = nDaysAgoY ? + (yVal[i] - nDaysAgoY) / nDaysAgoY * 100 : + null; + } else { + // y data given as an array of arrays and the index should be used + nDaysAgoY = yVal[i - period][index]; + rocY = nDaysAgoY ? + (yVal[i][index] - nDaysAgoY) / nDaysAgoY * 100 : + null; + } + + return [xVal[i], rocY]; } /** @@ -47,72 +47,72 @@ function populateAverage(xVal, yVal, i, period, index) { * @augments seriesTypes.sma */ seriesType('roc', 'sma', - /** - * Rate of change indicator (ROC). The indicator value for each point - * is defined as: - * - * `(C - Cn) / Cn * 100` - * - * where: `C` is the close value of the point of the same x in the - * linked series and `Cn` is the close value of the point `n` periods - * ago. `n` is set through [period](#plotOptions.roc.params.period). - * - * This series requires `linkedTo` option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/roc - * Rate of change indicator - * @since 6.0.0 - * @optionparent plotOptions.roc - */ - { - name: 'Rate of Change (9)', - params: { - index: 3, - period: 9 - } - }, { - nameBase: 'Rate of Change', - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - ROC = [], - xData = [], - yData = [], - i, - index = -1, - ROCPoint; - - // Period is used as a number of time periods ago, so we need more - // (at least 1 more) data than the period value - if (xVal.length <= period) { - return false; - } + /** + * Rate of change indicator (ROC). The indicator value for each point + * is defined as: + * + * `(C - Cn) / Cn * 100` + * + * where: `C` is the close value of the point of the same x in the + * linked series and `Cn` is the close value of the point `n` periods + * ago. `n` is set through [period](#plotOptions.roc.params.period). + * + * This series requires `linkedTo` option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/roc + * Rate of change indicator + * @since 6.0.0 + * @optionparent plotOptions.roc + */ + { + name: 'Rate of Change (9)', + params: { + index: 3, + period: 9 + } + }, { + nameBase: 'Rate of Change', + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + ROC = [], + xData = [], + yData = [], + i, + index = -1, + ROCPoint; + + // Period is used as a number of time periods ago, so we need more + // (at least 1 more) data than the period value + if (xVal.length <= period) { + return false; + } + + // Switch index for OHLC / Candlestick / Arearange + if (isArray(yVal[0])) { + index = params.index; + } - // Switch index for OHLC / Candlestick / Arearange - if (isArray(yVal[0])) { - index = params.index; - } - - // i = period <-- skip first N-points - // Calculate value one-by-one for each period in visible data - for (i = period; i < yValLen; i++) { - ROCPoint = populateAverage(xVal, yVal, i, period, index); - ROC.push(ROCPoint); - xData.push(ROCPoint[0]); - yData.push(ROCPoint[1]); - } - - return { - values: ROC, - xData: xData, - yData: yData - }; - } - }); + // i = period <-- skip first N-points + // Calculate value one-by-one for each period in visible data + for (i = period; i < yValLen; i++) { + ROCPoint = populateAverage(xVal, yVal, i, period, index); + ROC.push(ROCPoint); + xData.push(ROCPoint[0]); + yData.push(ROCPoint[1]); + } + + return { + values: ROC, + xData: xData, + yData: yData + }; + } + }); /** * A `ROC` series. If the [type](#series.wma.type) option is not @@ -128,7 +128,7 @@ seriesType('roc', 'sma', * ago. `n` is set through [period](#series.roc.params.period). * * This series requires `linkedTo` option to be set. - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.roc diff --git a/js/indicators/rsi.src.js b/js/indicators/rsi.src.js index 775c8dd7389..4699e28716f 100755 --- a/js/indicators/rsi.src.js +++ b/js/indicators/rsi.src.js @@ -7,130 +7,130 @@ var isArray = H.isArray; // Utils: function toFixed(a, n) { - return parseFloat(a.toFixed(n)); + return parseFloat(a.toFixed(n)); } H.seriesType('rsi', 'sma', - /** - * Relative strength index (RSI) technical indicator. This series - * requires the `linkedTo` option to be set and should be loaded after - * the `stock/indicators/indicators.js` file. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/rsi - * RSI indicator - * @since 6.0.0 - * @optionparent plotOptions.rsi - */ - { - /** - * @excluding index - */ - params: { - period: 14, - /** - * Number of maximum decimals that are used in RSI calculations. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - decimals: 4 - } - }, { - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - decimals = params.decimals, - // RSI starts calculations from the second point - // Cause we need to calculate change between two points - range = 1, - RSI = [], - xData = [], - yData = [], - index = 3, - gain = 0, - loss = 0, - RSIPoint, change, avgGain, avgLoss, i; - - // RSI requires close value - if ( - (xVal.length < period) || !isArray(yVal[0]) || - yVal[0].length !== 4 - ) { - return false; - } - - // Calculate changes for first N points - while (range < period) { - change = toFixed( - yVal[range][index] - yVal[range - 1][index], - decimals - ); - - if (change > 0) { - gain += change; - } else { - loss += Math.abs(change); - } - - range++; - } - - // Average for first n-1 points: - avgGain = toFixed(gain / (period - 1), decimals); - avgLoss = toFixed(loss / (period - 1), decimals); - - for (i = range; i < yValLen; i++) { - change = toFixed(yVal[i][index] - yVal[i - 1][index], decimals); - - if (change > 0) { - gain = change; - loss = 0; - } else { - gain = 0; - loss = Math.abs(change); - } - - // Calculate smoothed averages, RS, RSI values: - avgGain = toFixed( - (avgGain * (period - 1) + gain) / period, - decimals - ); - avgLoss = toFixed( - (avgLoss * (period - 1) + loss) / period, - decimals - ); - // If average-loss is equal zero, then by definition RSI is set - // to 100: - if (avgLoss === 0) { - RSIPoint = 100; - // If average-gain is equal zero, then by definition RSI is set - // to 0: - } else if (avgGain === 0) { - RSIPoint = 0; - } else { - RSIPoint = toFixed( - 100 - (100 / (1 + (avgGain / avgLoss))), - decimals - ); - } - - RSI.push([xVal[i], RSIPoint]); - xData.push(xVal[i]); - yData.push(RSIPoint); - } - - return { - values: RSI, - xData: xData, - yData: yData - }; - } - } + /** + * Relative strength index (RSI) technical indicator. This series + * requires the `linkedTo` option to be set and should be loaded after + * the `stock/indicators/indicators.js` file. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/rsi + * RSI indicator + * @since 6.0.0 + * @optionparent plotOptions.rsi + */ + { + /** + * @excluding index + */ + params: { + period: 14, + /** + * Number of maximum decimals that are used in RSI calculations. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + decimals: 4 + } + }, { + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + decimals = params.decimals, + // RSI starts calculations from the second point + // Cause we need to calculate change between two points + range = 1, + RSI = [], + xData = [], + yData = [], + index = 3, + gain = 0, + loss = 0, + RSIPoint, change, avgGain, avgLoss, i; + + // RSI requires close value + if ( + (xVal.length < period) || !isArray(yVal[0]) || + yVal[0].length !== 4 + ) { + return false; + } + + // Calculate changes for first N points + while (range < period) { + change = toFixed( + yVal[range][index] - yVal[range - 1][index], + decimals + ); + + if (change > 0) { + gain += change; + } else { + loss += Math.abs(change); + } + + range++; + } + + // Average for first n-1 points: + avgGain = toFixed(gain / (period - 1), decimals); + avgLoss = toFixed(loss / (period - 1), decimals); + + for (i = range; i < yValLen; i++) { + change = toFixed(yVal[i][index] - yVal[i - 1][index], decimals); + + if (change > 0) { + gain = change; + loss = 0; + } else { + gain = 0; + loss = Math.abs(change); + } + + // Calculate smoothed averages, RS, RSI values: + avgGain = toFixed( + (avgGain * (period - 1) + gain) / period, + decimals + ); + avgLoss = toFixed( + (avgLoss * (period - 1) + loss) / period, + decimals + ); + // If average-loss is equal zero, then by definition RSI is set + // to 100: + if (avgLoss === 0) { + RSIPoint = 100; + // If average-gain is equal zero, then by definition RSI is set + // to 0: + } else if (avgGain === 0) { + RSIPoint = 0; + } else { + RSIPoint = toFixed( + 100 - (100 / (1 + (avgGain / avgLoss))), + decimals + ); + } + + RSI.push([xVal[i], RSIPoint]); + xData.push(xVal[i]); + yData.push(RSIPoint); + } + + return { + values: RSI, + xData: xData, + yData: yData + }; + } + } ); /** diff --git a/js/indicators/stochastic.src.js b/js/indicators/stochastic.src.js index 75c1a4c6618..9c043d78594 100644 --- a/js/indicators/stochastic.src.js +++ b/js/indicators/stochastic.src.js @@ -4,233 +4,233 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var each = H.each, - merge = H.merge, - isArray = H.isArray, - defined = H.defined, - SMA = H.seriesTypes.sma; + merge = H.merge, + isArray = H.isArray, + defined = H.defined, + SMA = H.seriesTypes.sma; // Utils: function minInArray(arr, index) { - return H.reduce(arr, function (min, target) { - return Math.min(min, target[index]); - }, Infinity); + return H.reduce(arr, function (min, target) { + return Math.min(min, target[index]); + }, Infinity); } function maxInArray(arr, index) { - return H.reduce(arr, function (min, target) { - return Math.max(min, target[index]); - }, 0); + return H.reduce(arr, function (min, target) { + return Math.max(min, target[index]); + }, 0); } H.seriesType('stochastic', 'sma', - /** - * Stochastic oscillator. This series requires the `linkedTo` option to be - * set and should be loaded after the `stock/indicators/indicators.js` file. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/stochastic - * Stochastic oscillator - * @since 6.0.0 - * @optionparent plotOptions.stochastic - */ - { - name: 'Stochastic (14, 3)', - /** - * @excluding index,period - */ - params: { - /** - * Periods for Stochastic oscillator: [%K, %D]. - * - * @default [14, 3] - * @type {Array} - * @since 6.0.0 - * @product highstock - */ - periods: [14, 3] - }, - marker: { - enabled: false - }, - tooltip: { - pointFormat: '\u25CF {series.name}
%K: {point.y}
%D: {point.smoothed}
' - }, - /** - * Smoothed line options. - * - * @since 6.0.0 - * @product highstock - */ - smoothedLine: { - /** - * Styles for a smoothed line. - * - * @since 6.0.0 - * @product highstock - */ - styles: { - /** - * Pixel width of the line. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1, - /** - * Color of the line. If not set, it's inherited from - * [plotOptions.stochastic.color]( - * #plotOptions.stochastic.color). - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - lineColor: undefined - } - }, - dataGrouping: { - approximation: 'averages' - } - }, /** @lends Highcharts.Series.prototype */ { - nameComponents: ['periods'], - nameBase: 'Stochastic', - pointArrayMap: ['y', 'smoothed'], - parallelArrays: ['x', 'y', 'smoothed'], - pointValKey: 'y', - init: function () { - SMA.prototype.init.apply(this, arguments); + /** + * Stochastic oscillator. This series requires the `linkedTo` option to be + * set and should be loaded after the `stock/indicators/indicators.js` file. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/stochastic + * Stochastic oscillator + * @since 6.0.0 + * @optionparent plotOptions.stochastic + */ + { + name: 'Stochastic (14, 3)', + /** + * @excluding index,period + */ + params: { + /** + * Periods for Stochastic oscillator: [%K, %D]. + * + * @default [14, 3] + * @type {Array} + * @since 6.0.0 + * @product highstock + */ + periods: [14, 3] + }, + marker: { + enabled: false + }, + tooltip: { + pointFormat: '\u25CF {series.name}
%K: {point.y}
%D: {point.smoothed}
' + }, + /** + * Smoothed line options. + * + * @since 6.0.0 + * @product highstock + */ + smoothedLine: { + /** + * Styles for a smoothed line. + * + * @since 6.0.0 + * @product highstock + */ + styles: { + /** + * Pixel width of the line. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1, + /** + * Color of the line. If not set, it's inherited from + * [plotOptions.stochastic.color]( + * #plotOptions.stochastic.color). + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + lineColor: undefined + } + }, + dataGrouping: { + approximation: 'averages' + } + }, /** @lends Highcharts.Series.prototype */ { + nameComponents: ['periods'], + nameBase: 'Stochastic', + pointArrayMap: ['y', 'smoothed'], + parallelArrays: ['x', 'y', 'smoothed'], + pointValKey: 'y', + init: function () { + SMA.prototype.init.apply(this, arguments); - // Set default color for lines: - this.options = merge({ - smoothedLine: { - styles: { - lineColor: this.color - } - } - }, this.options); - }, - toYData: function (point) { - return [point.y, point.smoothed]; - }, - translate: function () { - var indicator = this; + // Set default color for lines: + this.options = merge({ + smoothedLine: { + styles: { + lineColor: this.color + } + } + }, this.options); + }, + toYData: function (point) { + return [point.y, point.smoothed]; + }, + translate: function () { + var indicator = this; - SMA.prototype.translate.apply(indicator); + SMA.prototype.translate.apply(indicator); - each(indicator.points, function (point) { - if (point.smoothed !== null) { - point.plotSmoothed = indicator.yAxis.toPixels( - point.smoothed, - true - ); - } - }); - }, - drawGraph: function () { - var indicator = this, - mainLinePoints = indicator.points, - pointsLength = mainLinePoints.length, - mainLineOptions = indicator.options, - mainLinePath = indicator.graph, - gappedExtend = { - options: { - gapSize: mainLineOptions.gapSize - } - }, - smoothing = [], - point; + each(indicator.points, function (point) { + if (point.smoothed !== null) { + point.plotSmoothed = indicator.yAxis.toPixels( + point.smoothed, + true + ); + } + }); + }, + drawGraph: function () { + var indicator = this, + mainLinePoints = indicator.points, + pointsLength = mainLinePoints.length, + mainLineOptions = indicator.options, + mainLinePath = indicator.graph, + gappedExtend = { + options: { + gapSize: mainLineOptions.gapSize + } + }, + smoothing = [], + point; - // Generate points for %K and %D lines: - while (pointsLength--) { - point = mainLinePoints[pointsLength]; - smoothing.push({ - plotX: point.plotX, - plotY: point.plotSmoothed, - isNull: !defined(point.plotSmoothed) - }); - } + // Generate points for %K and %D lines: + while (pointsLength--) { + point = mainLinePoints[pointsLength]; + smoothing.push({ + plotX: point.plotX, + plotY: point.plotSmoothed, + isNull: !defined(point.plotSmoothed) + }); + } - // Modify options and generate smoothing line: - indicator.points = smoothing; - indicator.options = merge( - mainLineOptions.smoothedLine.styles, - gappedExtend - ); - indicator.graph = indicator.graphSmoothed; - SMA.prototype.drawGraph.call(indicator); - indicator.graphSmoothed = indicator.graph; + // Modify options and generate smoothing line: + indicator.points = smoothing; + indicator.options = merge( + mainLineOptions.smoothedLine.styles, + gappedExtend + ); + indicator.graph = indicator.graphSmoothed; + SMA.prototype.drawGraph.call(indicator); + indicator.graphSmoothed = indicator.graph; - // Restore options and draw a main line: - indicator.points = mainLinePoints; - indicator.options = mainLineOptions; - indicator.graph = mainLinePath; - SMA.prototype.drawGraph.call(indicator); - }, - getValues: function (series, params) { - var periodK = params.periods[0], - periodD = params.periods[1], - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - SO = [], // 0- date, 1-%K, 2-%D - xData = [], - yData = [], - slicedY, - close = 3, - low = 2, - high = 1, - CL, HL, LL, K, - D = null, - points, - i; + // Restore options and draw a main line: + indicator.points = mainLinePoints; + indicator.options = mainLineOptions; + indicator.graph = mainLinePath; + SMA.prototype.drawGraph.call(indicator); + }, + getValues: function (series, params) { + var periodK = params.periods[0], + periodD = params.periods[1], + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + SO = [], // 0- date, 1-%K, 2-%D + xData = [], + yData = [], + slicedY, + close = 3, + low = 2, + high = 1, + CL, HL, LL, K, + D = null, + points, + i; - // Stochastic requires close value - if ( - xVal.length < periodK || - !isArray(yVal[0]) || - yVal[0].length !== 4 - ) { - return false; - } + // Stochastic requires close value + if ( + xVal.length < periodK || + !isArray(yVal[0]) || + yVal[0].length !== 4 + ) { + return false; + } - // For a N-period, we start from N-1 point, to calculate Nth point - // That is why we later need to comprehend slice() elements list - // with (+1) - for (i = periodK - 1; i < yValLen; i++) { - slicedY = yVal.slice(i - periodK + 1, i + 1); - - // Calculate %K - LL = minInArray(slicedY, low); // Lowest low in %K periods - CL = yVal[i][close] - LL; - HL = maxInArray(slicedY, high) - LL; - K = CL / HL * 100; - - // Calculate smoothed %D, which is SMA of %K - if (i >= periodK + periodD) { - points = SMA.prototype.getValues.call(this, { - xData: xData.slice(i - periodD - periodK, i - periodD), - yData: yData.slice(i - periodD - periodK, i - periodD) - }, { - period: periodD - }); - D = points.yData[0]; - } - - SO.push([xVal[i], K, D]); - xData.push(xVal[i]); - yData.push([K, D]); - } + // For a N-period, we start from N-1 point, to calculate Nth point + // That is why we later need to comprehend slice() elements list + // with (+1) + for (i = periodK - 1; i < yValLen; i++) { + slicedY = yVal.slice(i - periodK + 1, i + 1); - return { - values: SO, - xData: xData, - yData: yData - }; - } - } + // Calculate %K + LL = minInArray(slicedY, low); // Lowest low in %K periods + CL = yVal[i][close] - LL; + HL = maxInArray(slicedY, high) - LL; + K = CL / HL * 100; + + // Calculate smoothed %D, which is SMA of %K + if (i >= periodK + periodD) { + points = SMA.prototype.getValues.call(this, { + xData: xData.slice(i - periodD - periodK, i - periodD), + yData: yData.slice(i - periodD - periodK, i - periodD) + }, { + period: periodD + }); + D = points.yData[0]; + } + + SO.push([xVal[i], K, D]); + xData.push(xVal[i]); + yData.push([K, D]); + } + + return { + values: SO, + xData: xData, + yData: yData + }; + } + } ); /** diff --git a/js/indicators/volume-by-price.src.js b/js/indicators/volume-by-price.src.js index 557f0ee13bd..4e9068e2871 100644 --- a/js/indicators/volume-by-price.src.js +++ b/js/indicators/volume-by-price.src.js @@ -11,36 +11,36 @@ import '../parts/Utilities.js'; // Utils function arrayExtremesOHLC(data) { - var dataLength = data.length, - min = data[0][3], - max = min, - i = 1, - currentPoint; - - for (; i < dataLength; i++) { - currentPoint = data[i][3]; - if (currentPoint < min) { - min = currentPoint; - } - - if (currentPoint > max) { - max = currentPoint; - } - } - - return { - min: min, - max: max - }; + var dataLength = data.length, + min = data[0][3], + max = min, + i = 1, + currentPoint; + + for (; i < dataLength; i++) { + currentPoint = data[i][3]; + if (currentPoint < min) { + min = currentPoint; + } + + if (currentPoint > max) { + max = currentPoint; + } + } + + return { + min: min, + max: max + }; } var abs = Math.abs, - each = H.each, - noop = H.noop, - addEvent = H.addEvent, - correctFloat = H.correctFloat, - seriesType = H.seriesType, - columnPrototype = H.seriesTypes.column.prototype; + each = H.each, + noop = H.noop, + addEvent = H.addEvent, + correctFloat = H.correctFloat, + seriesType = H.seriesType, + columnPrototype = H.seriesTypes.column.prototype; /** * The Volume By Price (VBP) series type. @@ -48,641 +48,641 @@ var abs = Math.abs, * @constructor seriesTypes.vbp * @augments seriesTypes.vbp */ -seriesType('vbp', 'sma', - /** - * Volume By Price indicator. - * - * This series requires `linkedTo` option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/volume-by-price - * Volume By Price indicator - * @since 6.0.0 - * @optionparent plotOptions.vbp - */ - { - /** - * @excluding index,period - */ - params: { - /** - * The number of price zones. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - ranges: 12, - /** - * The id of volume series which is mandatory. For example using - * OHLC data, volumeSeriesID='volume' means the indicator will be - * calculated using OHLC and volume values. - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - volumeSeriesID: 'volume' - }, - /** - * The styles for lines which determine price zones. - * - * @type {Object} - * @since 6.0.0 - * @product highstock - */ - zoneLines: { - /** - * Enable/disable zone lines. - * - * @type {Boolean} - * @since 6.0.0 - * @default true - * @product highstock - */ - enabled: true, - styles: { - /** - * Color of zone lines. - * - * @type {Color} - * @since 6.0.0 - * @product highstock - */ - color: '#0A9AC9', - /** - * The dash style of zone lines. - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - dashStyle: 'LongDash', - /** - * Pixel width of zone lines. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lineWidth: 1 - } - }, - /** - * The styles for bars when volume is divided into positive/negative. - * - * @type {Object} - * @since 6.0.0 - * @product highstock - */ - volumeDivision: { - /** - * Option to control if volume is divided. - * - * @type {Boolean} - * @since 6.0.0 - * @product highstock - */ - enabled: true, - styles: { - /** - * Color of positive volume bars. - * - * @type {Color} - * @since 6.0.0 - * @product highstock - */ - positiveColor: 'rgba(144, 237, 125, 0.8)', - /** - * Color of negative volume bars. - * - * @type {Color} - * @since 6.0.0 - * @product highstock - */ - negativeColor: 'rgba(244, 91, 91, 0.8)' - } - }, - // To enable series animation; must be animationLimit > pointCount - animationLimit: 1000, - enableMouseTracking: false, - pointPadding: 0, - zIndex: -1, - crisp: true, - dataGrouping: { - enabled: false - }, - dataLabels: { - enabled: true, - allowOverlap: true, - verticalAlign: 'top', - format: 'P: {point.volumePos:.2f} | N: {point.volumeNeg:.2f}', - padding: 0, - style: { - fontSize: '7px' - } - } - }, { - nameBase: 'Volume by Price', - bindTo: { - series: false, - eventName: 'afterSetExtremes' - }, - calculateOn: 'render', - markerAttribs: noop, - drawGraph: noop, - getColumnMetrics: columnPrototype.getColumnMetrics, - crispCol: columnPrototype.crispCol, - init: function (chart) { - var indicator = this, - params, - baseSeries, - volumeSeries; - - H.seriesTypes.sma.prototype.init.apply(indicator, arguments); - - params = indicator.options.params; - baseSeries = indicator.linkedParent; - volumeSeries = chart.get(params.volumeSeriesID); - - indicator.addCustomEvents(baseSeries, volumeSeries); - - return indicator; - }, - // Adds events related with removing series - addCustomEvents: function (baseSeries, volumeSeries) { - var indicator = this; - - function toEmptyIndicator() { - indicator.chart.redraw(); - - indicator.setData([]); - indicator.zoneStarts = []; - - if (indicator.zoneLinesSVG) { - indicator.zoneLinesSVG.destroy(); - delete indicator.zoneLinesSVG; - } - } - - // If base series is deleted, indicator series data is filled with - // an empty array - indicator.dataEventsToUnbind.push( - addEvent(baseSeries, 'remove', function () { - toEmptyIndicator(); - }) - ); - - // If volume series is deleted, indicator series data is filled with - // an empty array - if (volumeSeries) { - indicator.dataEventsToUnbind.push( - addEvent(volumeSeries, 'remove', function () { - toEmptyIndicator(); - }) - ); - } - - return indicator; - }, - // Initial animation - animate: function (init) { - var series = this, - attr = {}; - - if (H.svg && !init) { - attr.translateX = series.yAxis.pos; - series.group.animate( - attr, - H.extend(H.animObject(series.options.animation), { - step: function (val, fx) { - series.group.attr({ - scaleX: Math.max(0.001, fx.pos) - }); - } - }) - ); - - // Delete this function to allow it only once - series.animate = null; - } - }, - drawPoints: function () { - var indicator = this; - - if (indicator.options.volumeDivision.enabled) { - indicator.posNegVolume(true, true); - columnPrototype.drawPoints.apply(indicator, arguments); - indicator.posNegVolume(false, false); - } - - columnPrototype.drawPoints.apply(indicator, arguments); - }, - // Function responsible for dividing volume into positive and negative - posNegVolume: function (initVol, pos) { - var indicator = this, - signOrder = pos ? - ['positive', 'negative'] : - ['negative', 'positive'], - volumeDivision = indicator.options.volumeDivision, - pointLength = indicator.points.length, - posWidths = [], - negWidths = [], - i = 0, - pointWidth, - priceZone, - wholeVol, - point; - - if (initVol) { - indicator.posWidths = posWidths; - indicator.negWidths = negWidths; - } else { - posWidths = indicator.posWidths; - negWidths = indicator.negWidths; - } - - for (; i < pointLength; i++) { - point = indicator.points[i]; - point[signOrder[0] + 'Graphic'] = point.graphic; - point.graphic = point[signOrder[1] + 'Graphic']; - - if (initVol) { - pointWidth = point.shapeArgs.width; - priceZone = indicator.priceZones[i]; - wholeVol = priceZone.wholeVolumeData; - - if (wholeVol) { - posWidths.push( - pointWidth / wholeVol * priceZone.positiveVolumeData - ); - negWidths.push( - pointWidth / wholeVol * priceZone.negativeVolumeData - ); - } else { - posWidths.push(0); - negWidths.push(0); - } - } - - point.color = pos ? - volumeDivision.styles.positiveColor : - volumeDivision.styles.negativeColor; - point.shapeArgs.width = pos ? - indicator.posWidths[i] : - indicator.negWidths[i]; - point.shapeArgs.x = pos ? - point.shapeArgs.x : - indicator.posWidths[i]; - } - }, - translate: function () { - var indicator = this, - options = indicator.options, - chart = indicator.chart, - yAxis = indicator.yAxis, - yAxisMin = yAxis.min, - zoneLinesOptions = indicator.options.zoneLines, - priceZones = indicator.priceZones, - yBarOffset = 0, - indicatorPoints, - volumeDataArray, - maxVolume, - primalBarWidth, - barHeight, - barHeightP, - oldBarHeight, - barWidth, - pointPadding, - chartPlotTop, - barX, - barY; - - columnPrototype.translate.apply(indicator); - indicatorPoints = indicator.points; - - // Do translate operation when points exist - if (indicatorPoints.length) { - pointPadding = options.pointPadding < 0.5 ? - options.pointPadding : - 0.1; - volumeDataArray = indicator.volumeDataArray; - maxVolume = H.arrayMax(volumeDataArray); - primalBarWidth = chart.plotWidth / 2; - chartPlotTop = chart.plotTop; - barHeight = abs(yAxis.toPixels(yAxisMin) - - yAxis.toPixels(yAxisMin + indicator.rangeStep)); - oldBarHeight = abs(yAxis.toPixels(yAxisMin) - - yAxis.toPixels(yAxisMin + indicator.rangeStep)); - - if (pointPadding) { - barHeightP = abs(barHeight * (1 - 2 * pointPadding)); - yBarOffset = abs((barHeight - barHeightP) / 2); - barHeight = abs(barHeightP); - } - - each(indicatorPoints, function (point, index) { - barX = point.barX = point.plotX = 0; - barY = point.plotY = ( - yAxis.toPixels(priceZones[index].start) - - chartPlotTop - - ( - yAxis.reversed ? - (barHeight - oldBarHeight) : - barHeight - ) - - yBarOffset - ); - barWidth = correctFloat( - primalBarWidth * - priceZones[index].wholeVolumeData / maxVolume - ); - point.pointWidth = barWidth; - - point.shapeArgs = indicator.crispCol.apply( - indicator, - [barX, barY, barWidth, barHeight] - ); - - point.volumeNeg = priceZones[index].negativeVolumeData; - point.volumePos = priceZones[index].positiveVolumeData; - point.volumeAll = priceZones[index].wholeVolumeData; - }); - - if (zoneLinesOptions.enabled) { - indicator.drawZones( - chart, - yAxis, - indicator.zoneStarts, - zoneLinesOptions.styles - ); - } - } - }, - getValues: function (series, params) { - var indicator = this, - xValues = series.processedXData, - yValues = series.processedYData, - chart = series.chart, - ranges = params.ranges, - VBP = [], - xData = [], - yData = [], - isOHLC, - volumeSeries, - priceZones; - - // Checks if base series exists - if (!chart) { - return H.error( - 'Base series not found! In case it has been removed, add ' + - 'a new one.', - true - ); - } - - // Checks if volume series exists - if (!(volumeSeries = chart.get(params.volumeSeriesID))) { - return H.error( - 'Series ' + - params.volumeSeriesID + - ' not found! Check `volumeSeriesID`.', - true - ); - } - - // Checks if series data fits the OHLC format - isOHLC = H.isArray(yValues[0]); - - if (isOHLC && yValues[0].length !== 4) { - return H.error( - 'Type of ' + - series.name + - ' series is different than line, OHLC or candlestick.', - true - ); - } - - // Price zones contains all the information about the zones (index, - // start, end, volumes, etc.) - priceZones = indicator.priceZones = indicator.specifyZones( - isOHLC, - xValues, - yValues, - ranges, - volumeSeries - ); - - each(priceZones, function (zone, index) { - VBP.push([zone.x, zone.end]); - xData.push(VBP[index][0]); - yData.push(VBP[index][1]); - }); - - return { - values: VBP, - xData: xData, - yData: yData - }; - }, - // Specifing where each zone should start ans end - specifyZones: function ( - isOHLC, - xValues, - yValues, - ranges, - volumeSeries - ) { - var indicator = this, - rangeExtremes = isOHLC ? arrayExtremesOHLC(yValues) : false, - lowRange = rangeExtremes ? - rangeExtremes.min : - H.arrayMin(yValues), - highRange = rangeExtremes ? - rangeExtremes.max : - H.arrayMax(yValues), - zoneStarts = indicator.zoneStarts = [], - priceZones = [], - i = 0, - j = 1, - rangeStep, - zoneStartsLength; - - if (!lowRange || !highRange) { - if (this.points.length) { - this.setData([]); - this.zoneStarts = []; - this.zoneLinesSVG.destroy(); - } - return []; - } - - rangeStep = indicator.rangeStep = - correctFloat(highRange - lowRange) / ranges; - zoneStarts.push(lowRange); - - for (; i < ranges - 1; i++) { - zoneStarts.push(correctFloat(zoneStarts[i] + rangeStep)); - } - - zoneStarts.push(highRange); - zoneStartsLength = zoneStarts.length; - - // Creating zones - for (; j < zoneStartsLength; j++) { - priceZones.push({ - index: j - 1, - x: xValues[0], - start: zoneStarts[j - 1], - end: zoneStarts[j] - }); - } - - return indicator.volumePerZone( - isOHLC, - priceZones, - volumeSeries, - xValues, - yValues - ); - }, - // Calculating sum of volume values for a specific zone - volumePerZone: function ( - isOHLC, - priceZones, - volumeSeries, - xValues, - yValues - ) { - var indicator = this, - volumeXData = volumeSeries.processedXData, - volumeYData = volumeSeries.processedYData, - lastZoneIndex = priceZones.length - 1, - baseSeriesLength = yValues.length, - volumeSeriesLength = volumeYData.length, - previousValue, - startFlag, - endFlag, - value, - i; - - // Checks if each point has a corresponding volume value - if (abs(baseSeriesLength - volumeSeriesLength)) { - // If the first point don't have volume, add 0 value at the - // beggining of the volume array - if (xValues[0] !== volumeXData[0]) { - volumeYData.unshift(0); - } - - // If the last point don't have volume, add 0 value at the end - // of the volume array - if ( - xValues[baseSeriesLength - 1] !== - volumeXData[volumeSeriesLength - 1] - ) { - volumeYData.push(0); - } - } - - indicator.volumeDataArray = []; - - each(priceZones, function (zone) { - zone.wholeVolumeData = 0; - zone.positiveVolumeData = 0; - zone.negativeVolumeData = 0; - - for (i = 0; i < baseSeriesLength; i++) { - startFlag = false; - endFlag = false; - value = isOHLC ? yValues[i][3] : yValues[i]; - previousValue = i ? - (isOHLC ? yValues[i - 1][3] : yValues[i - 1]) : - value; - - // Checks if this is the point with the lowest close value - // and if so, adds it calculations - if (value <= zone.start && zone.index === 0) { - startFlag = true; - } - - // Checks if this is the point with the highest close value - // and if so, adds it calculations - if (value >= zone.end && zone.index === lastZoneIndex) { - endFlag = true; - } - - if ( - (value > zone.start || startFlag) && - (value < zone.end || endFlag) - ) { - zone.wholeVolumeData += volumeYData[i]; - - if (previousValue > value) { - zone.negativeVolumeData += volumeYData[i]; - } else { - zone.positiveVolumeData += volumeYData[i]; - } - } - } - indicator.volumeDataArray.push(zone.wholeVolumeData); - }); - - return priceZones; - }, - // Function responsoble for drawing additional lines indicating zones - drawZones: function (chart, yAxis, zonesValues, zonesStyles) { - var indicator = this, - renderer = chart.renderer, - zoneLinesSVG = indicator.zoneLinesSVG, - zoneLinesPath = [], - leftLinePos = 0, - rightLinePos = chart.plotWidth, - verticalOffset = chart.plotTop, - verticalLinePos; - - each(zonesValues, function (value) { - verticalLinePos = yAxis.toPixels(value) - verticalOffset; - zoneLinesPath = zoneLinesPath.concat(chart.renderer.crispLine([ - 'M', - leftLinePos, - verticalLinePos, - 'L', - rightLinePos, - verticalLinePos - ], zonesStyles.lineWidth)); - }); - - // Create zone lines one path or update it while animating - if (zoneLinesSVG) { - zoneLinesSVG.animate({ - d: zoneLinesPath - }); - } else { - zoneLinesSVG = indicator.zoneLinesSVG = - renderer.path(zoneLinesPath).attr({ - 'stroke-width': zonesStyles.lineWidth, - 'stroke': zonesStyles.color, - 'dashstyle': zonesStyles.dashStyle, - 'zIndex': indicator.group.zIndex + 0.1 - }) - .add(indicator.group); - } - } - }, { - // Required for destroying negative part of volume - destroy: function () { - if (this.negativeGraphic) { - this.negativeGraphic = this.negativeGraphic.destroy(); - } - return H.Point.prototype.destroy.apply(this, arguments); - } - }); +seriesType('vbp', 'sma', + /** + * Volume By Price indicator. + * + * This series requires `linkedTo` option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/volume-by-price + * Volume By Price indicator + * @since 6.0.0 + * @optionparent plotOptions.vbp + */ + { + /** + * @excluding index,period + */ + params: { + /** + * The number of price zones. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + ranges: 12, + /** + * The id of volume series which is mandatory. For example using + * OHLC data, volumeSeriesID='volume' means the indicator will be + * calculated using OHLC and volume values. + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + volumeSeriesID: 'volume' + }, + /** + * The styles for lines which determine price zones. + * + * @type {Object} + * @since 6.0.0 + * @product highstock + */ + zoneLines: { + /** + * Enable/disable zone lines. + * + * @type {Boolean} + * @since 6.0.0 + * @default true + * @product highstock + */ + enabled: true, + styles: { + /** + * Color of zone lines. + * + * @type {Color} + * @since 6.0.0 + * @product highstock + */ + color: '#0A9AC9', + /** + * The dash style of zone lines. + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + dashStyle: 'LongDash', + /** + * Pixel width of zone lines. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lineWidth: 1 + } + }, + /** + * The styles for bars when volume is divided into positive/negative. + * + * @type {Object} + * @since 6.0.0 + * @product highstock + */ + volumeDivision: { + /** + * Option to control if volume is divided. + * + * @type {Boolean} + * @since 6.0.0 + * @product highstock + */ + enabled: true, + styles: { + /** + * Color of positive volume bars. + * + * @type {Color} + * @since 6.0.0 + * @product highstock + */ + positiveColor: 'rgba(144, 237, 125, 0.8)', + /** + * Color of negative volume bars. + * + * @type {Color} + * @since 6.0.0 + * @product highstock + */ + negativeColor: 'rgba(244, 91, 91, 0.8)' + } + }, + // To enable series animation; must be animationLimit > pointCount + animationLimit: 1000, + enableMouseTracking: false, + pointPadding: 0, + zIndex: -1, + crisp: true, + dataGrouping: { + enabled: false + }, + dataLabels: { + enabled: true, + allowOverlap: true, + verticalAlign: 'top', + format: 'P: {point.volumePos:.2f} | N: {point.volumeNeg:.2f}', + padding: 0, + style: { + fontSize: '7px' + } + } + }, { + nameBase: 'Volume by Price', + bindTo: { + series: false, + eventName: 'afterSetExtremes' + }, + calculateOn: 'render', + markerAttribs: noop, + drawGraph: noop, + getColumnMetrics: columnPrototype.getColumnMetrics, + crispCol: columnPrototype.crispCol, + init: function (chart) { + var indicator = this, + params, + baseSeries, + volumeSeries; + + H.seriesTypes.sma.prototype.init.apply(indicator, arguments); + + params = indicator.options.params; + baseSeries = indicator.linkedParent; + volumeSeries = chart.get(params.volumeSeriesID); + + indicator.addCustomEvents(baseSeries, volumeSeries); + + return indicator; + }, + // Adds events related with removing series + addCustomEvents: function (baseSeries, volumeSeries) { + var indicator = this; + + function toEmptyIndicator() { + indicator.chart.redraw(); + + indicator.setData([]); + indicator.zoneStarts = []; + + if (indicator.zoneLinesSVG) { + indicator.zoneLinesSVG.destroy(); + delete indicator.zoneLinesSVG; + } + } + + // If base series is deleted, indicator series data is filled with + // an empty array + indicator.dataEventsToUnbind.push( + addEvent(baseSeries, 'remove', function () { + toEmptyIndicator(); + }) + ); + + // If volume series is deleted, indicator series data is filled with + // an empty array + if (volumeSeries) { + indicator.dataEventsToUnbind.push( + addEvent(volumeSeries, 'remove', function () { + toEmptyIndicator(); + }) + ); + } + + return indicator; + }, + // Initial animation + animate: function (init) { + var series = this, + attr = {}; + + if (H.svg && !init) { + attr.translateX = series.yAxis.pos; + series.group.animate( + attr, + H.extend(H.animObject(series.options.animation), { + step: function (val, fx) { + series.group.attr({ + scaleX: Math.max(0.001, fx.pos) + }); + } + }) + ); + + // Delete this function to allow it only once + series.animate = null; + } + }, + drawPoints: function () { + var indicator = this; + + if (indicator.options.volumeDivision.enabled) { + indicator.posNegVolume(true, true); + columnPrototype.drawPoints.apply(indicator, arguments); + indicator.posNegVolume(false, false); + } + + columnPrototype.drawPoints.apply(indicator, arguments); + }, + // Function responsible for dividing volume into positive and negative + posNegVolume: function (initVol, pos) { + var indicator = this, + signOrder = pos ? + ['positive', 'negative'] : + ['negative', 'positive'], + volumeDivision = indicator.options.volumeDivision, + pointLength = indicator.points.length, + posWidths = [], + negWidths = [], + i = 0, + pointWidth, + priceZone, + wholeVol, + point; + + if (initVol) { + indicator.posWidths = posWidths; + indicator.negWidths = negWidths; + } else { + posWidths = indicator.posWidths; + negWidths = indicator.negWidths; + } + + for (; i < pointLength; i++) { + point = indicator.points[i]; + point[signOrder[0] + 'Graphic'] = point.graphic; + point.graphic = point[signOrder[1] + 'Graphic']; + + if (initVol) { + pointWidth = point.shapeArgs.width; + priceZone = indicator.priceZones[i]; + wholeVol = priceZone.wholeVolumeData; + + if (wholeVol) { + posWidths.push( + pointWidth / wholeVol * priceZone.positiveVolumeData + ); + negWidths.push( + pointWidth / wholeVol * priceZone.negativeVolumeData + ); + } else { + posWidths.push(0); + negWidths.push(0); + } + } + + point.color = pos ? + volumeDivision.styles.positiveColor : + volumeDivision.styles.negativeColor; + point.shapeArgs.width = pos ? + indicator.posWidths[i] : + indicator.negWidths[i]; + point.shapeArgs.x = pos ? + point.shapeArgs.x : + indicator.posWidths[i]; + } + }, + translate: function () { + var indicator = this, + options = indicator.options, + chart = indicator.chart, + yAxis = indicator.yAxis, + yAxisMin = yAxis.min, + zoneLinesOptions = indicator.options.zoneLines, + priceZones = indicator.priceZones, + yBarOffset = 0, + indicatorPoints, + volumeDataArray, + maxVolume, + primalBarWidth, + barHeight, + barHeightP, + oldBarHeight, + barWidth, + pointPadding, + chartPlotTop, + barX, + barY; + + columnPrototype.translate.apply(indicator); + indicatorPoints = indicator.points; + + // Do translate operation when points exist + if (indicatorPoints.length) { + pointPadding = options.pointPadding < 0.5 ? + options.pointPadding : + 0.1; + volumeDataArray = indicator.volumeDataArray; + maxVolume = H.arrayMax(volumeDataArray); + primalBarWidth = chart.plotWidth / 2; + chartPlotTop = chart.plotTop; + barHeight = abs(yAxis.toPixels(yAxisMin) - + yAxis.toPixels(yAxisMin + indicator.rangeStep)); + oldBarHeight = abs(yAxis.toPixels(yAxisMin) - + yAxis.toPixels(yAxisMin + indicator.rangeStep)); + + if (pointPadding) { + barHeightP = abs(barHeight * (1 - 2 * pointPadding)); + yBarOffset = abs((barHeight - barHeightP) / 2); + barHeight = abs(barHeightP); + } + + each(indicatorPoints, function (point, index) { + barX = point.barX = point.plotX = 0; + barY = point.plotY = ( + yAxis.toPixels(priceZones[index].start) - + chartPlotTop - + ( + yAxis.reversed ? + (barHeight - oldBarHeight) : + barHeight + ) - + yBarOffset + ); + barWidth = correctFloat( + primalBarWidth * + priceZones[index].wholeVolumeData / maxVolume + ); + point.pointWidth = barWidth; + + point.shapeArgs = indicator.crispCol.apply( + indicator, + [barX, barY, barWidth, barHeight] + ); + + point.volumeNeg = priceZones[index].negativeVolumeData; + point.volumePos = priceZones[index].positiveVolumeData; + point.volumeAll = priceZones[index].wholeVolumeData; + }); + + if (zoneLinesOptions.enabled) { + indicator.drawZones( + chart, + yAxis, + indicator.zoneStarts, + zoneLinesOptions.styles + ); + } + } + }, + getValues: function (series, params) { + var indicator = this, + xValues = series.processedXData, + yValues = series.processedYData, + chart = series.chart, + ranges = params.ranges, + VBP = [], + xData = [], + yData = [], + isOHLC, + volumeSeries, + priceZones; + + // Checks if base series exists + if (!chart) { + return H.error( + 'Base series not found! In case it has been removed, add ' + + 'a new one.', + true + ); + } + + // Checks if volume series exists + if (!(volumeSeries = chart.get(params.volumeSeriesID))) { + return H.error( + 'Series ' + + params.volumeSeriesID + + ' not found! Check `volumeSeriesID`.', + true + ); + } + + // Checks if series data fits the OHLC format + isOHLC = H.isArray(yValues[0]); + + if (isOHLC && yValues[0].length !== 4) { + return H.error( + 'Type of ' + + series.name + + ' series is different than line, OHLC or candlestick.', + true + ); + } + + // Price zones contains all the information about the zones (index, + // start, end, volumes, etc.) + priceZones = indicator.priceZones = indicator.specifyZones( + isOHLC, + xValues, + yValues, + ranges, + volumeSeries + ); + + each(priceZones, function (zone, index) { + VBP.push([zone.x, zone.end]); + xData.push(VBP[index][0]); + yData.push(VBP[index][1]); + }); + + return { + values: VBP, + xData: xData, + yData: yData + }; + }, + // Specifing where each zone should start ans end + specifyZones: function ( + isOHLC, + xValues, + yValues, + ranges, + volumeSeries + ) { + var indicator = this, + rangeExtremes = isOHLC ? arrayExtremesOHLC(yValues) : false, + lowRange = rangeExtremes ? + rangeExtremes.min : + H.arrayMin(yValues), + highRange = rangeExtremes ? + rangeExtremes.max : + H.arrayMax(yValues), + zoneStarts = indicator.zoneStarts = [], + priceZones = [], + i = 0, + j = 1, + rangeStep, + zoneStartsLength; + + if (!lowRange || !highRange) { + if (this.points.length) { + this.setData([]); + this.zoneStarts = []; + this.zoneLinesSVG.destroy(); + } + return []; + } + + rangeStep = indicator.rangeStep = + correctFloat(highRange - lowRange) / ranges; + zoneStarts.push(lowRange); + + for (; i < ranges - 1; i++) { + zoneStarts.push(correctFloat(zoneStarts[i] + rangeStep)); + } + + zoneStarts.push(highRange); + zoneStartsLength = zoneStarts.length; + + // Creating zones + for (; j < zoneStartsLength; j++) { + priceZones.push({ + index: j - 1, + x: xValues[0], + start: zoneStarts[j - 1], + end: zoneStarts[j] + }); + } + + return indicator.volumePerZone( + isOHLC, + priceZones, + volumeSeries, + xValues, + yValues + ); + }, + // Calculating sum of volume values for a specific zone + volumePerZone: function ( + isOHLC, + priceZones, + volumeSeries, + xValues, + yValues + ) { + var indicator = this, + volumeXData = volumeSeries.processedXData, + volumeYData = volumeSeries.processedYData, + lastZoneIndex = priceZones.length - 1, + baseSeriesLength = yValues.length, + volumeSeriesLength = volumeYData.length, + previousValue, + startFlag, + endFlag, + value, + i; + + // Checks if each point has a corresponding volume value + if (abs(baseSeriesLength - volumeSeriesLength)) { + // If the first point don't have volume, add 0 value at the + // beggining of the volume array + if (xValues[0] !== volumeXData[0]) { + volumeYData.unshift(0); + } + + // If the last point don't have volume, add 0 value at the end + // of the volume array + if ( + xValues[baseSeriesLength - 1] !== + volumeXData[volumeSeriesLength - 1] + ) { + volumeYData.push(0); + } + } + + indicator.volumeDataArray = []; + + each(priceZones, function (zone) { + zone.wholeVolumeData = 0; + zone.positiveVolumeData = 0; + zone.negativeVolumeData = 0; + + for (i = 0; i < baseSeriesLength; i++) { + startFlag = false; + endFlag = false; + value = isOHLC ? yValues[i][3] : yValues[i]; + previousValue = i ? + (isOHLC ? yValues[i - 1][3] : yValues[i - 1]) : + value; + + // Checks if this is the point with the lowest close value + // and if so, adds it calculations + if (value <= zone.start && zone.index === 0) { + startFlag = true; + } + + // Checks if this is the point with the highest close value + // and if so, adds it calculations + if (value >= zone.end && zone.index === lastZoneIndex) { + endFlag = true; + } + + if ( + (value > zone.start || startFlag) && + (value < zone.end || endFlag) + ) { + zone.wholeVolumeData += volumeYData[i]; + + if (previousValue > value) { + zone.negativeVolumeData += volumeYData[i]; + } else { + zone.positiveVolumeData += volumeYData[i]; + } + } + } + indicator.volumeDataArray.push(zone.wholeVolumeData); + }); + + return priceZones; + }, + // Function responsoble for drawing additional lines indicating zones + drawZones: function (chart, yAxis, zonesValues, zonesStyles) { + var indicator = this, + renderer = chart.renderer, + zoneLinesSVG = indicator.zoneLinesSVG, + zoneLinesPath = [], + leftLinePos = 0, + rightLinePos = chart.plotWidth, + verticalOffset = chart.plotTop, + verticalLinePos; + + each(zonesValues, function (value) { + verticalLinePos = yAxis.toPixels(value) - verticalOffset; + zoneLinesPath = zoneLinesPath.concat(chart.renderer.crispLine([ + 'M', + leftLinePos, + verticalLinePos, + 'L', + rightLinePos, + verticalLinePos + ], zonesStyles.lineWidth)); + }); + + // Create zone lines one path or update it while animating + if (zoneLinesSVG) { + zoneLinesSVG.animate({ + d: zoneLinesPath + }); + } else { + zoneLinesSVG = indicator.zoneLinesSVG = + renderer.path(zoneLinesPath).attr({ + 'stroke-width': zonesStyles.lineWidth, + 'stroke': zonesStyles.color, + 'dashstyle': zonesStyles.dashStyle, + 'zIndex': indicator.group.zIndex + 0.1 + }) + .add(indicator.group); + } + } + }, { + // Required for destroying negative part of volume + destroy: function () { + if (this.negativeGraphic) { + this.negativeGraphic = this.negativeGraphic.destroy(); + } + return H.Point.prototype.destroy.apply(this, arguments); + } + }); /** * A `Volume By Price (VBP)` series. If the [type](#series.vbp.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.vbp diff --git a/js/indicators/vwap.src.js b/js/indicators/vwap.src.js index 57e7fee4274..098680a019d 100644 --- a/js/indicators/vwap.src.js +++ b/js/indicators/vwap.src.js @@ -10,7 +10,7 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var isArray = H.isArray, - seriesType = H.seriesType; + seriesType = H.seriesType; /** * The Volume Weighted Average Price (VWAP) series type. @@ -18,156 +18,156 @@ var isArray = H.isArray, * @constructor seriesTypes.vwap * @augments seriesTypes.sma */ -seriesType('vwap', 'sma', - /** - * Volume Weighted Average Price indicator. - * - * This series requires `linkedTo` option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/vwap - * Volume Weighted Average Price indicator - * @since 6.0.0 - * @optionparent plotOptions.vwap - */ - { - /** - * @excluding index - */ - params: { - period: 30, - /** - * The id of volume series which is mandatory. For example using - * OHLC data, volumeSeriesID='volume' means the indicator will be - * calculated using OHLC and volume values. - * - * @type {String} - * @since 6.0.0 - * @product highstock - */ - volumeSeriesID: 'volume' - } - }, { - /** - * Returns the final values of the indicator ready to be presented on a - * chart - * @returns {Object} Object containing computed VWAP - **/ - getValues: function (series, params) { - var indicator = this, - chart = series.chart, - xValues = series.xData, - yValues = series.yData, - period = params.period, - isOHLC = true, - volumeSeries; +seriesType('vwap', 'sma', + /** + * Volume Weighted Average Price indicator. + * + * This series requires `linkedTo` option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/vwap + * Volume Weighted Average Price indicator + * @since 6.0.0 + * @optionparent plotOptions.vwap + */ + { + /** + * @excluding index + */ + params: { + period: 30, + /** + * The id of volume series which is mandatory. For example using + * OHLC data, volumeSeriesID='volume' means the indicator will be + * calculated using OHLC and volume values. + * + * @type {String} + * @since 6.0.0 + * @product highstock + */ + volumeSeriesID: 'volume' + } + }, { + /** + * Returns the final values of the indicator ready to be presented on a + * chart + * @returns {Object} Object containing computed VWAP + **/ + getValues: function (series, params) { + var indicator = this, + chart = series.chart, + xValues = series.xData, + yValues = series.yData, + period = params.period, + isOHLC = true, + volumeSeries; - // Checks if volume series exists - if (!(volumeSeries = chart.get(params.volumeSeriesID))) { - return H.error( - 'Series ' + - params.volumeSeriesID + - ' not found! Check `volumeSeriesID`.', - true - ); - } - - // Checks if series data fits the OHLC format - if (!(isArray(yValues[0]))) { - isOHLC = false; - } + // Checks if volume series exists + if (!(volumeSeries = chart.get(params.volumeSeriesID))) { + return H.error( + 'Series ' + + params.volumeSeriesID + + ' not found! Check `volumeSeriesID`.', + true + ); + } - return indicator.calculateVWAPValues( - isOHLC, - xValues, - yValues, - volumeSeries, - period - ); - }, - /** - * Main algorithm used to calculate Volume Weighted Average Price (VWAP) - * values - * @param {Boolean} isOHLC says if data has OHLC format - * @param {Array} xValues array of timestamps - * @param {Array} yValues - * array of yValues, can be an array of a four arrays (OHLC) or - * array of values (line) - * @param {Array} volumeSeries volume series - * @param {Number} period number of points to be calculated - * @returns {Object} Object contains computed VWAP - **/ - calculateVWAPValues: function ( - isOHLC, - xValues, - yValues, - volumeSeries, - period - ) { - var volumeValues = volumeSeries.yData, - volumeLength = volumeSeries.xData.length, - pointsLength = xValues.length, - cumulativePrice = [], - cumulativeVolume = [], - xData = [], - yData = [], - VWAP = [], - commonLength, - typicalPrice, - cPrice, - cVolume, - i, - j; + // Checks if series data fits the OHLC format + if (!(isArray(yValues[0]))) { + isOHLC = false; + } - if (pointsLength <= volumeLength) { - commonLength = pointsLength; - } else { - commonLength = volumeLength; - } + return indicator.calculateVWAPValues( + isOHLC, + xValues, + yValues, + volumeSeries, + period + ); + }, + /** + * Main algorithm used to calculate Volume Weighted Average Price (VWAP) + * values + * @param {Boolean} isOHLC says if data has OHLC format + * @param {Array} xValues array of timestamps + * @param {Array} yValues + * array of yValues, can be an array of a four arrays (OHLC) or + * array of values (line) + * @param {Array} volumeSeries volume series + * @param {Number} period number of points to be calculated + * @returns {Object} Object contains computed VWAP + **/ + calculateVWAPValues: function ( + isOHLC, + xValues, + yValues, + volumeSeries, + period + ) { + var volumeValues = volumeSeries.yData, + volumeLength = volumeSeries.xData.length, + pointsLength = xValues.length, + cumulativePrice = [], + cumulativeVolume = [], + xData = [], + yData = [], + VWAP = [], + commonLength, + typicalPrice, + cPrice, + cVolume, + i, + j; - for (i = 0, j = 0; i < commonLength; i++) { - // Depending on whether series is OHLC or line type, price is - // average of the high, low and close or a simple value - typicalPrice = isOHLC ? - ((yValues[i][1] + yValues[i][2] + yValues[i][3]) / 3) : - yValues[i]; - typicalPrice *= volumeValues[i]; + if (pointsLength <= volumeLength) { + commonLength = pointsLength; + } else { + commonLength = volumeLength; + } - cPrice = j ? - (cumulativePrice[i - 1] + typicalPrice) : - typicalPrice; - cVolume = j ? - (cumulativeVolume[i - 1] + volumeValues[i]) : - volumeValues[i]; + for (i = 0, j = 0; i < commonLength; i++) { + // Depending on whether series is OHLC or line type, price is + // average of the high, low and close or a simple value + typicalPrice = isOHLC ? + ((yValues[i][1] + yValues[i][2] + yValues[i][3]) / 3) : + yValues[i]; + typicalPrice *= volumeValues[i]; - cumulativePrice.push(cPrice); - cumulativeVolume.push(cVolume); + cPrice = j ? + (cumulativePrice[i - 1] + typicalPrice) : + typicalPrice; + cVolume = j ? + (cumulativeVolume[i - 1] + volumeValues[i]) : + volumeValues[i]; - VWAP.push([xValues[i], (cPrice / cVolume)]); - xData.push(VWAP[i][0]); - yData.push(VWAP[i][1]); + cumulativePrice.push(cPrice); + cumulativeVolume.push(cVolume); - j++; + VWAP.push([xValues[i], (cPrice / cVolume)]); + xData.push(VWAP[i][0]); + yData.push(VWAP[i][1]); - if (j === period) { - j = 0; - } - } + j++; - return { - values: VWAP, - xData: xData, - yData: yData - }; - } - }); + if (j === period) { + j = 0; + } + } + + return { + values: VWAP, + xData: xData, + yData: yData + }; + } + }); /** * A `Volume Weighted Average Price (VWAP)` series. If the * [type](#series.vwap.type) option is not specified, it is inherited from * [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.vwap diff --git a/js/indicators/wma.src.js b/js/indicators/wma.src.js index e6aacf5a18e..195535413f3 100644 --- a/js/indicators/wma.src.js +++ b/js/indicators/wma.src.js @@ -9,36 +9,36 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var isArray = H.isArray, - seriesType = H.seriesType; + seriesType = H.seriesType; // Utils: function accumulateAverage(points, xVal, yVal, i, index) { - var xValue = xVal[i], - yValue = index < 0 ? yVal[i] : yVal[i][index]; - - points.push([xValue, yValue]); + var xValue = xVal[i], + yValue = index < 0 ? yVal[i] : yVal[i][index]; + + points.push([xValue, yValue]); } function weightedSumArray(array, pLen) { - // The denominator is the sum of the number of days as a triangular number. - // If there are 5 days, the triangular numbers are 5, 4, 3, 2, and 1. - // The sum is 5 + 4 + 3 + 2 + 1 = 15. - var denominator = (pLen + 1) / 2 * pLen; - - // reduce VS loop => reduce - return array.reduce(function (prev, cur, i) { - return [null, prev[1] + cur[1] * (i + 1)]; - })[1] / denominator; + // The denominator is the sum of the number of days as a triangular number. + // If there are 5 days, the triangular numbers are 5, 4, 3, 2, and 1. + // The sum is 5 + 4 + 3 + 2 + 1 = 15. + var denominator = (pLen + 1) / 2 * pLen; + + // reduce VS loop => reduce + return array.reduce(function (prev, cur, i) { + return [null, prev[1] + cur[1] * (i + 1)]; + })[1] / denominator; } function populateAverage(points, xVal, yVal, i) { - var pLen = points.length, - wmaY = weightedSumArray(points, pLen), - wmaX = xVal[i - 1]; - - points.shift(); // remove point until range < period - - return [wmaX, wmaY]; + var pLen = points.length, + wmaY = weightedSumArray(points, pLen), + wmaX = xVal[i - 1]; + + points.shift(); // remove point until range < period + + return [wmaX, wmaY]; } /** @@ -48,82 +48,82 @@ function populateAverage(points, xVal, yVal, i) { * @augments seriesTypes.sma */ seriesType('wma', 'sma', - /** - * Weighted moving average indicator (WMA). This series requires `linkedTo` - * option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/wma - * Weighted moving average indicator - * @since 6.0.0 - * @optionparent plotOptions.wma - */ - { - params: { - index: 3, - period: 9 - } - }, /** @lends Highcharts.Series.prototype */ { - getValues: function (series, params) { - var period = params.period, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - range = 1, - xValue = xVal[0], - yValue = yVal[0], - WMA = [], - xData = [], - yData = [], - index = -1, - i, points, WMAPoint; - - if (xVal.length < period) { - return false; - } - - // Switch index for OHLC / Candlestick - if (isArray(yVal[0])) { - index = params.index; - yValue = yVal[0][index]; - } - // Starting point - points = [[xValue, yValue]]; - - // Accumulate first N-points - while (range !== period) { - accumulateAverage(points, xVal, yVal, range, index); - range++; - } - - // Calculate value one-by-one for each period in visible data - for (i = range; i < yValLen; i++) { - WMAPoint = populateAverage(points, xVal, yVal, i); - WMA.push(WMAPoint); - xData.push(WMAPoint[0]); - yData.push(WMAPoint[1]); - - accumulateAverage(points, xVal, yVal, i, index); - } - - WMAPoint = populateAverage(points, xVal, yVal, i); - WMA.push(WMAPoint); - xData.push(WMAPoint[0]); - yData.push(WMAPoint[1]); - - return { - values: WMA, - xData: xData, - yData: yData - }; - } - }); + /** + * Weighted moving average indicator (WMA). This series requires `linkedTo` + * option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/wma + * Weighted moving average indicator + * @since 6.0.0 + * @optionparent plotOptions.wma + */ + { + params: { + index: 3, + period: 9 + } + }, /** @lends Highcharts.Series.prototype */ { + getValues: function (series, params) { + var period = params.period, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + range = 1, + xValue = xVal[0], + yValue = yVal[0], + WMA = [], + xData = [], + yData = [], + index = -1, + i, points, WMAPoint; + + if (xVal.length < period) { + return false; + } + + // Switch index for OHLC / Candlestick + if (isArray(yVal[0])) { + index = params.index; + yValue = yVal[0][index]; + } + // Starting point + points = [[xValue, yValue]]; + + // Accumulate first N-points + while (range !== period) { + accumulateAverage(points, xVal, yVal, range, index); + range++; + } + + // Calculate value one-by-one for each period in visible data + for (i = range; i < yValLen; i++) { + WMAPoint = populateAverage(points, xVal, yVal, i); + WMA.push(WMAPoint); + xData.push(WMAPoint[0]); + yData.push(WMAPoint[1]); + + accumulateAverage(points, xVal, yVal, i, index); + } + + WMAPoint = populateAverage(points, xVal, yVal, i); + WMA.push(WMAPoint); + xData.push(WMAPoint[0]); + yData.push(WMAPoint[1]); + + return { + values: WMA, + xData: xData, + yData: yData + }; + } + }); /** * A `WMA` series. If the [type](#series.wma.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.wma diff --git a/js/indicators/zigzag.src.js b/js/indicators/zigzag.src.js index c4236e1f6c7..5501bb07b1c 100644 --- a/js/indicators/zigzag.src.js +++ b/js/indicators/zigzag.src.js @@ -9,7 +9,7 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var seriesType = H.seriesType, - UNDEFINED; + UNDEFINED; /** * The Zig Zag series type. @@ -18,199 +18,199 @@ var seriesType = H.seriesType, * @augments seriesTypes.sma */ seriesType('zigzag', 'sma', - /** - * Zig Zag indicator. - * - * This series requires `linkedTo` option to be set. - * - * @extends {plotOptions.sma} - * @product highstock - * @sample {highstock} stock/indicators/zigzag - * Zig Zag indicator - * @since 6.0.0 - * @optionparent plotOptions.zigzag - */ - { - /** - * @excluding index,period - */ - params: { - /** - * The point index which indicator calculations will base - low - * value. - * - * For example using OHLC data, index=2 means the indicator will be - * calculated using Low values. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - lowIndex: 2, - /** - * The point index which indicator calculations will base - high - * value. - * - * For example using OHLC data, index=1 means the indicator will be - * calculated using High values. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - highIndex: 1, - /** - * The threshold for the value change. - * - * For example deviation=1 means the indicator will ignore all price - * movements less than 1%. - * - * @type {Number} - * @since 6.0.0 - * @product highstock - */ - deviation: 1 - } - }, { - nameComponents: ['deviation'], - nameSuffixes: ['%'], - nameBase: 'Zig Zag', - getValues: function (series, params) { - var lowIndex = params.lowIndex, - highIndex = params.highIndex, - deviation = params.deviation / 100, - deviations = { - 'low': 1 + deviation, - 'high': 1 - deviation - }, - xVal = series.xData, - yVal = series.yData, - yValLen = yVal ? yVal.length : 0, - Zigzag = [], - xData = [], - yData = [], - i, j, - ZigzagPoint, - firstZigzagLow, - firstZigzagHigh, - directionUp, - zigZagLen, - exitLoop = false, - yIndex = false; - - // Exit if not enught points or no low or high values - if ( - xVal.length <= 1 || - ( - yValLen && - ( - yVal[0][lowIndex] === UNDEFINED || - yVal[0][highIndex] === UNDEFINED - ) - ) - ) { - return false; - } - - // Set first zigzag point candidate - firstZigzagLow = yVal[0][lowIndex]; - firstZigzagHigh = yVal[0][highIndex]; - - // Search for a second zigzag point candidate, - // this will also set first zigzag point - for (i = 1; i < yValLen; i++) { - // requried change to go down - if (yVal[i][lowIndex] <= firstZigzagHigh * deviations.high) { - Zigzag.push([xVal[0], firstZigzagHigh]); - // second zigzag point candidate - ZigzagPoint = [xVal[i], yVal[i][lowIndex]]; - // next line will be going up - directionUp = true; - exitLoop = true; - - // requried change to go up - } else if ( - yVal[i][highIndex] >= firstZigzagLow * deviations.low - ) { - Zigzag.push([xVal[0], firstZigzagLow]); - // second zigzag point candidate - ZigzagPoint = [xVal[i], yVal[i][highIndex]]; - // next line will be going down - directionUp = false; - exitLoop = true; - - } - if (exitLoop) { - xData.push(Zigzag[0][0]); - yData.push(Zigzag[0][1]); - j = i++; - i = yValLen; - } - } - - // Search for next zigzags - for (i = j; i < yValLen; i++) { - if (directionUp) { // next line up - - // lower when going down -> change zigzag candidate - if (yVal[i][lowIndex] <= ZigzagPoint[1]) { - ZigzagPoint = [xVal[i], yVal[i][lowIndex]]; - } - - // requried change to go down -> new zigzagpoint and - // direction change - if (yVal[i][highIndex] >= ZigzagPoint[1] * deviations.low) { - yIndex = highIndex; - } - - } else { // next line down - - // higher when going up -> change zigzag candidate - if (yVal[i][highIndex] >= ZigzagPoint[1]) { - ZigzagPoint = [xVal[i], yVal[i][highIndex]]; - } - - // requried change to go down -> new zigzagpoint and - // direction change - if (yVal[i][lowIndex] <= ZigzagPoint[1] * deviations.high) { - yIndex = lowIndex; - } - } - if (yIndex !== false) { // new zigzag point and direction change - Zigzag.push(ZigzagPoint); - xData.push(ZigzagPoint[0]); - yData.push(ZigzagPoint[1]); - ZigzagPoint = [xVal[i], yVal[i][yIndex]]; - directionUp = !directionUp; - - yIndex = false; - } - } - - zigZagLen = Zigzag.length; - - // no zigzag for last point - if ( - zigZagLen !== 0 && - Zigzag[zigZagLen - 1][0] < xVal[yValLen - 1] - ) { - // set last point from zigzag candidate - Zigzag.push(ZigzagPoint); - xData.push(ZigzagPoint[0]); - yData.push(ZigzagPoint[1]); - } - return { - values: Zigzag, - xData: xData, - yData: yData - }; - } - }); + /** + * Zig Zag indicator. + * + * This series requires `linkedTo` option to be set. + * + * @extends {plotOptions.sma} + * @product highstock + * @sample {highstock} stock/indicators/zigzag + * Zig Zag indicator + * @since 6.0.0 + * @optionparent plotOptions.zigzag + */ + { + /** + * @excluding index,period + */ + params: { + /** + * The point index which indicator calculations will base - low + * value. + * + * For example using OHLC data, index=2 means the indicator will be + * calculated using Low values. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + lowIndex: 2, + /** + * The point index which indicator calculations will base - high + * value. + * + * For example using OHLC data, index=1 means the indicator will be + * calculated using High values. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + highIndex: 1, + /** + * The threshold for the value change. + * + * For example deviation=1 means the indicator will ignore all price + * movements less than 1%. + * + * @type {Number} + * @since 6.0.0 + * @product highstock + */ + deviation: 1 + } + }, { + nameComponents: ['deviation'], + nameSuffixes: ['%'], + nameBase: 'Zig Zag', + getValues: function (series, params) { + var lowIndex = params.lowIndex, + highIndex = params.highIndex, + deviation = params.deviation / 100, + deviations = { + 'low': 1 + deviation, + 'high': 1 - deviation + }, + xVal = series.xData, + yVal = series.yData, + yValLen = yVal ? yVal.length : 0, + Zigzag = [], + xData = [], + yData = [], + i, j, + ZigzagPoint, + firstZigzagLow, + firstZigzagHigh, + directionUp, + zigZagLen, + exitLoop = false, + yIndex = false; + + // Exit if not enught points or no low or high values + if ( + xVal.length <= 1 || + ( + yValLen && + ( + yVal[0][lowIndex] === UNDEFINED || + yVal[0][highIndex] === UNDEFINED + ) + ) + ) { + return false; + } + + // Set first zigzag point candidate + firstZigzagLow = yVal[0][lowIndex]; + firstZigzagHigh = yVal[0][highIndex]; + + // Search for a second zigzag point candidate, + // this will also set first zigzag point + for (i = 1; i < yValLen; i++) { + // requried change to go down + if (yVal[i][lowIndex] <= firstZigzagHigh * deviations.high) { + Zigzag.push([xVal[0], firstZigzagHigh]); + // second zigzag point candidate + ZigzagPoint = [xVal[i], yVal[i][lowIndex]]; + // next line will be going up + directionUp = true; + exitLoop = true; + + // requried change to go up + } else if ( + yVal[i][highIndex] >= firstZigzagLow * deviations.low + ) { + Zigzag.push([xVal[0], firstZigzagLow]); + // second zigzag point candidate + ZigzagPoint = [xVal[i], yVal[i][highIndex]]; + // next line will be going down + directionUp = false; + exitLoop = true; + + } + if (exitLoop) { + xData.push(Zigzag[0][0]); + yData.push(Zigzag[0][1]); + j = i++; + i = yValLen; + } + } + + // Search for next zigzags + for (i = j; i < yValLen; i++) { + if (directionUp) { // next line up + + // lower when going down -> change zigzag candidate + if (yVal[i][lowIndex] <= ZigzagPoint[1]) { + ZigzagPoint = [xVal[i], yVal[i][lowIndex]]; + } + + // requried change to go down -> new zigzagpoint and + // direction change + if (yVal[i][highIndex] >= ZigzagPoint[1] * deviations.low) { + yIndex = highIndex; + } + + } else { // next line down + + // higher when going up -> change zigzag candidate + if (yVal[i][highIndex] >= ZigzagPoint[1]) { + ZigzagPoint = [xVal[i], yVal[i][highIndex]]; + } + + // requried change to go down -> new zigzagpoint and + // direction change + if (yVal[i][lowIndex] <= ZigzagPoint[1] * deviations.high) { + yIndex = lowIndex; + } + } + if (yIndex !== false) { // new zigzag point and direction change + Zigzag.push(ZigzagPoint); + xData.push(ZigzagPoint[0]); + yData.push(ZigzagPoint[1]); + ZigzagPoint = [xVal[i], yVal[i][yIndex]]; + directionUp = !directionUp; + + yIndex = false; + } + } + + zigZagLen = Zigzag.length; + + // no zigzag for last point + if ( + zigZagLen !== 0 && + Zigzag[zigZagLen - 1][0] < xVal[yValLen - 1] + ) { + // set last point from zigzag candidate + Zigzag.push(ZigzagPoint); + xData.push(ZigzagPoint[0]); + yData.push(ZigzagPoint[1]); + } + return { + values: Zigzag, + xData: xData, + yData: yData + }; + } + }); /** * A `Zig Zag` series. If the [type](#series.zigzag.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.zigzag diff --git a/js/masters/modules/drilldown.src.js b/js/masters/modules/drilldown.src.js index 39917077c54..f896aa3f1a4 100644 --- a/js/masters/modules/drilldown.src.js +++ b/js/masters/modules/drilldown.src.js @@ -1,7 +1,7 @@ /** * @license @product.name@ JS v@product.version@ (@product.date@) * Highcharts Drilldown module - * + * * Author: Torstein Honsi * License: www.highcharts.com/license * diff --git a/js/mixins/centered-series.js b/js/mixins/centered-series.js index 08d70d083b8..83839e32eb0 100644 --- a/js/mixins/centered-series.js +++ b/js/mixins/centered-series.js @@ -7,78 +7,78 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var deg2rad = H.deg2rad, - isNumber = H.isNumber, - pick = H.pick, - relativeLength = H.relativeLength; + isNumber = H.isNumber, + pick = H.pick, + relativeLength = H.relativeLength; H.CenteredSeriesMixin = { - /** - * Get the center of the pie based on the size and center options relative - * to the plot area. Borrowed by the polar and gauge series types. - */ - getCenter: function () { + /** + * Get the center of the pie based on the size and center options relative + * to the plot area. Borrowed by the polar and gauge series types. + */ + getCenter: function () { - var options = this.options, - chart = this.chart, - slicingRoom = 2 * (options.slicedOffset || 0), - handleSlicingRoom, - plotWidth = chart.plotWidth - 2 * slicingRoom, - plotHeight = chart.plotHeight - 2 * slicingRoom, - centerOption = options.center, - positions = [ - pick(centerOption[0], '50%'), - pick(centerOption[1], '50%'), - options.size || '100%', - options.innerSize || 0 - ], - smallestSize = Math.min(plotWidth, plotHeight), - i, - value; + var options = this.options, + chart = this.chart, + slicingRoom = 2 * (options.slicedOffset || 0), + handleSlicingRoom, + plotWidth = chart.plotWidth - 2 * slicingRoom, + plotHeight = chart.plotHeight - 2 * slicingRoom, + centerOption = options.center, + positions = [ + pick(centerOption[0], '50%'), + pick(centerOption[1], '50%'), + options.size || '100%', + options.innerSize || 0 + ], + smallestSize = Math.min(plotWidth, plotHeight), + i, + value; - for (i = 0; i < 4; ++i) { - value = positions[i]; - handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value)); + for (i = 0; i < 4; ++i) { + value = positions[i]; + handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value)); - // i == 0: centerX, relative to width - // i == 1: centerY, relative to height - // i == 2: size, relative to smallestSize - // i == 3: innerSize, relative to size - positions[i] = relativeLength( - value, - [plotWidth, plotHeight, smallestSize, positions[2]][i] - ) + (handleSlicingRoom ? slicingRoom : 0); + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + // i == 3: innerSize, relative to size + positions[i] = relativeLength( + value, + [plotWidth, plotHeight, smallestSize, positions[2]][i] + ) + (handleSlicingRoom ? slicingRoom : 0); - } - // innerSize cannot be larger than size (#3632) - if (positions[3] > positions[2]) { - positions[3] = positions[2]; - } - return positions; - }, - /** - * getStartAndEndRadians - Calculates start and end angles in radians. - * Used in series types such as pie and sunburst. - * - * @param {Number} start Start angle in degrees. - * @param {Number} end Start angle in degrees. - * @return {object} Returns an object containing start and end angles as - * radians. - */ - getStartAndEndRadians: function getStartAndEndRadians(start, end) { - var startAngle = isNumber(start) ? start : 0, // must be a number - endAngle = ( - ( - isNumber(end) && // must be a number - end > startAngle && // must be larger than the start angle - // difference must be less than 360 degrees - (end - startAngle) < 360 - ) ? - end : - startAngle + 360 - ), - correction = -90; - return { - start: deg2rad * (startAngle + correction), - end: deg2rad * (endAngle + correction) - }; - } + } + // innerSize cannot be larger than size (#3632) + if (positions[3] > positions[2]) { + positions[3] = positions[2]; + } + return positions; + }, + /** + * getStartAndEndRadians - Calculates start and end angles in radians. + * Used in series types such as pie and sunburst. + * + * @param {Number} start Start angle in degrees. + * @param {Number} end Start angle in degrees. + * @return {object} Returns an object containing start and end angles as + * radians. + */ + getStartAndEndRadians: function getStartAndEndRadians(start, end) { + var startAngle = isNumber(start) ? start : 0, // must be a number + endAngle = ( + ( + isNumber(end) && // must be a number + end > startAngle && // must be larger than the start angle + // difference must be less than 360 degrees + (end - startAngle) < 360 + ) ? + end : + startAngle + 360 + ), + correction = -90; + return { + start: deg2rad * (startAngle + correction), + end: deg2rad * (endAngle + correction) + }; + } }; diff --git a/js/mixins/derived-series.js b/js/mixins/derived-series.js index 60fe77f7815..e71fb9ad25c 100644 --- a/js/mixins/derived-series.js +++ b/js/mixins/derived-series.js @@ -3,9 +3,9 @@ import H from '../parts/Globals.js'; import '../parts/Series.js'; var each = H.each, - Series = H.Series, - addEvent = H.addEvent, - noop = H.noop; + Series = H.Series, + addEvent = H.addEvent, + noop = H.noop; /* *************************************************************************** @@ -17,7 +17,7 @@ var each = H.each, /** * Provides methods for auto setting/updating series data based on the based * series data. - * + * * @mixin **/ var derivedSeriesMixin = { @@ -26,15 +26,15 @@ var derivedSeriesMixin = { * * returns {undefined} **/ - init: function () { - Series.prototype.init.apply(this, arguments); + init: function () { + Series.prototype.init.apply(this, arguments); - this.initialised = false; - this.baseSeries = null; - this.eventRemovers = []; + this.initialised = false; + this.baseSeries = null; + this.eventRemovers = []; - this.addEvents(); - }, + this.addEvents(); + }, /** * Method to be implemented - inside the method the series has already access @@ -44,50 +44,50 @@ var derivedSeriesMixin = { * * @returns {Array} - an array of data **/ - setDerivedData: noop, + setDerivedData: noop, /** * Sets base series for the series * - * returns {undefined} + * returns {undefined} **/ - setBaseSeries: function () { - var chart = this.chart, - baseSeriesOptions = this.options.baseSeries, - baseSeries = - baseSeriesOptions && + setBaseSeries: function () { + var chart = this.chart, + baseSeriesOptions = this.options.baseSeries, + baseSeries = + baseSeriesOptions && (chart.series[baseSeriesOptions] || chart.get(baseSeriesOptions)); - this.baseSeries = baseSeries || null; - }, - + this.baseSeries = baseSeries || null; + }, + /** * Adds events for the series * * @returns {undefined} **/ - addEvents: function () { - var derivedSeries = this, - chartSeriesLinked; - - chartSeriesLinked = addEvent( - this.chart, - 'afterLinkSeries', - function () { - derivedSeries.setBaseSeries(); - - if (derivedSeries.baseSeries && !derivedSeries.initialised) { - derivedSeries.setDerivedData(); - derivedSeries.addBaseSeriesEvents(); - derivedSeries.initialised = true; - } - } - ); - - this.eventRemovers.push( - chartSeriesLinked - ); - }, + addEvents: function () { + var derivedSeries = this, + chartSeriesLinked; + + chartSeriesLinked = addEvent( + this.chart, + 'afterLinkSeries', + function () { + derivedSeries.setBaseSeries(); + + if (derivedSeries.baseSeries && !derivedSeries.initialised) { + derivedSeries.setDerivedData(); + derivedSeries.addBaseSeriesEvents(); + derivedSeries.initialised = true; + } + } + ); + + this.eventRemovers.push( + chartSeriesLinked + ); + }, /** * Adds events to the base series - it required for recalculating the data in @@ -95,46 +95,46 @@ var derivedSeriesMixin = { * * @returns {undefined} **/ - addBaseSeriesEvents: function () { - var derivedSeries = this, - updatedDataRemover, - destroyRemover; - - updatedDataRemover = addEvent( - derivedSeries.baseSeries, - 'updatedData', - function () { - derivedSeries.setDerivedData(); - } - ); - - destroyRemover = addEvent( - derivedSeries.baseSeries, - 'destroy', - function () { - derivedSeries.baseSeries = null; - derivedSeries.initialised = false; - } - ); - - derivedSeries.eventRemovers.push( + addBaseSeriesEvents: function () { + var derivedSeries = this, + updatedDataRemover, + destroyRemover; + + updatedDataRemover = addEvent( + derivedSeries.baseSeries, + 'updatedData', + function () { + derivedSeries.setDerivedData(); + } + ); + + destroyRemover = addEvent( + derivedSeries.baseSeries, + 'destroy', + function () { + derivedSeries.baseSeries = null; + derivedSeries.initialised = false; + } + ); + + derivedSeries.eventRemovers.push( updatedDataRemover, destroyRemover ); - }, + }, /** * Destroys the series * * @returns {undefined} **/ - destroy: function () { - each(this.eventRemovers, function (remover) { - remover(); - }); + destroy: function () { + each(this.eventRemovers, function (remover) { + remover(); + }); - Series.prototype.destroy.apply(this, arguments); - } + Series.prototype.destroy.apply(this, arguments); + } }; export default derivedSeriesMixin; diff --git a/js/mixins/draw-point.js b/js/mixins/draw-point.js index f1c70086338..3ffcfed0229 100644 --- a/js/mixins/draw-point.js +++ b/js/mixins/draw-point.js @@ -1,5 +1,5 @@ var isFn = function (x) { - return typeof x === 'function'; + return typeof x === 'function'; }; /** @@ -10,33 +10,33 @@ var isFn = function (x) { * @return {undefined} Returns undefined. */ var draw = function draw(params) { - var point = this, - graphic = point.graphic, - animate = params.animate, - attr = params.attr, - onComplete = params.onComplete, - css = params.css, - group = params.group, - renderer = params.renderer, - shape = params.shapeArgs, - type = params.shapeType; + var point = this, + graphic = point.graphic, + animate = params.animate, + attr = params.attr, + onComplete = params.onComplete, + css = params.css, + group = params.group, + renderer = params.renderer, + shape = params.shapeArgs, + type = params.shapeType; - if (point.shouldDraw()) { - if (!graphic) { - point.graphic = graphic = renderer[type](shape).add(group); - } - graphic.css(css).attr(attr).animate(animate, undefined, onComplete); - } else if (graphic) { - graphic.animate(animate, undefined, function () { - point.graphic = graphic = graphic.destroy(); - if (isFn(onComplete)) { - onComplete(); - } - }); - } - if (graphic) { - graphic.addClass(point.getClassName(), true); - } + if (point.shouldDraw()) { + if (!graphic) { + point.graphic = graphic = renderer[type](shape).add(group); + } + graphic.css(css).attr(attr).animate(animate, undefined, onComplete); + } else if (graphic) { + graphic.animate(animate, undefined, function () { + point.graphic = graphic = graphic.destroy(); + if (isFn(onComplete)) { + onComplete(); + } + }); + } + if (graphic) { + graphic.addClass(point.getClassName(), true); + } }; export default draw; diff --git a/js/mixins/on-series.js b/js/mixins/on-series.js index 38e7286f72f..98eafe76adc 100644 --- a/js/mixins/on-series.js +++ b/js/mixins/on-series.js @@ -7,148 +7,148 @@ import H from '../parts/Globals.js'; var each = H.each, - defined = H.defined, - seriesTypes = H.seriesTypes, - stableSort = H.stableSort; + defined = H.defined, + seriesTypes = H.seriesTypes, + stableSort = H.stableSort; var onSeriesMixin = { - /** - * Override getPlotBox. If the onSeries option is valid, return the plot box - * of the onSeries, otherwise proceed as usual. - */ - getPlotBox: function () { - return H.Series.prototype.getPlotBox.call( - ( - this.options.onSeries && - this.chart.get(this.options.onSeries) - ) || this - ); - }, - - /** - * 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, - inverted = chart.inverted, - xAxis = series.xAxis, - yAxis = series.yAxis, - xOffset = 0, - leftPoint, - lastX, - rightPoint, - currentDataGrouping, - distanceRatio; - - // 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]) { - leftPoint = onData[i]; - point = points[cursor]; - point.y = leftPoint.y; - - 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) { - // the distance ratio, between 0 and 1 - distanceRatio = (point.x - leftPoint.x) / - (rightPoint.x - leftPoint.x); - point.plotY += - distanceRatio * - // the plotY distance - (rightPoint[onKey] - leftPoint[onKey]); - point.y += - distanceRatio * - (rightPoint.y - leftPoint.y); - } - } - } - 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; - - point.plotX += xOffset; // #2049 - - // 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). For inverted charts, we need - // to calculate position anyway, because series.invertGroups is not - // defined - if (point.plotY === undefined || inverted) { - if (point.plotX >= 0 && point.plotX <= xAxis.len) { - // We're inside xAxis range - if (inverted) { - point.plotY = xAxis.translate(point.x, 0, 1, 0, 1); - point.plotX = defined(point.y) ? - yAxis.translate(point.y, 0, 0, 0, 1) : 0; - } else { - point.plotY = chart.chartHeight - xAxis.bottom - - (xAxis.opposite ? xAxis.height : 0) + - xAxis.offset - yAxis.top; // #3517 - } - } else { - point.shapeArgs = {}; // 847 - } - } - - // 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 - }); - - this.onSeries = onSeries; - } + /** + * Override getPlotBox. If the onSeries option is valid, return the plot box + * of the onSeries, otherwise proceed as usual. + */ + getPlotBox: function () { + return H.Series.prototype.getPlotBox.call( + ( + this.options.onSeries && + this.chart.get(this.options.onSeries) + ) || this + ); + }, + + /** + * 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, + inverted = chart.inverted, + xAxis = series.xAxis, + yAxis = series.yAxis, + xOffset = 0, + leftPoint, + lastX, + rightPoint, + currentDataGrouping, + distanceRatio; + + // 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]) { + leftPoint = onData[i]; + point = points[cursor]; + point.y = leftPoint.y; + + 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) { + // the distance ratio, between 0 and 1 + distanceRatio = (point.x - leftPoint.x) / + (rightPoint.x - leftPoint.x); + point.plotY += + distanceRatio * + // the plotY distance + (rightPoint[onKey] - leftPoint[onKey]); + point.y += + distanceRatio * + (rightPoint.y - leftPoint.y); + } + } + } + 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; + + point.plotX += xOffset; // #2049 + + // 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). For inverted charts, we need + // to calculate position anyway, because series.invertGroups is not + // defined + if (point.plotY === undefined || inverted) { + if (point.plotX >= 0 && point.plotX <= xAxis.len) { + // We're inside xAxis range + if (inverted) { + point.plotY = xAxis.translate(point.x, 0, 1, 0, 1); + point.plotX = defined(point.y) ? + yAxis.translate(point.y, 0, 0, 0, 1) : 0; + } else { + point.plotY = chart.chartHeight - xAxis.bottom - + (xAxis.opposite ? xAxis.height : 0) + + xAxis.offset - yAxis.top; // #3517 + } + } else { + point.shapeArgs = {}; // 847 + } + } + + // 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 + }); + + this.onSeries = onSeries; + } }; export default onSeriesMixin; diff --git a/js/mixins/tree-series.js b/js/mixins/tree-series.js index 210d0459731..8774e5237db 100644 --- a/js/mixins/tree-series.js +++ b/js/mixins/tree-series.js @@ -1,137 +1,137 @@ import H from '../parts/Globals.js'; var each = H.each, - extend = H.extend, - isArray = H.isArray, - isBoolean = function (x) { - return typeof x === 'boolean'; - }, - isFn = function (x) { - return typeof x === 'function'; - }, - isObject = H.isObject, - isNumber = H.isNumber, - merge = H.merge, - pick = H.pick, - reduce = H.reduce; + extend = H.extend, + isArray = H.isArray, + isBoolean = function (x) { + return typeof x === 'boolean'; + }, + isFn = function (x) { + return typeof x === 'function'; + }, + isObject = H.isObject, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + reduce = H.reduce; // TODO Combine buildTree and buildNode with setTreeValues // TODO Remove logic from Treemap and make it utilize this mixin. var setTreeValues = function setTreeValues(tree, options) { - var before = options.before, - idRoot = options.idRoot, - mapIdToNode = options.mapIdToNode, - nodeRoot = mapIdToNode[idRoot], - levelIsConstant = ( - isBoolean(options.levelIsConstant) ? - options.levelIsConstant : - true - ), - points = options.points, - point = points[tree.i], - optionsPoint = point && point.options || {}, - childrenTotal = 0, - children = [], - value; - extend(tree, { - levelDynamic: tree.level - (levelIsConstant ? 0 : nodeRoot.level), - name: pick(point && point.name, ''), - visible: ( - idRoot === tree.id || - (isBoolean(options.visible) ? options.visible : false) - ) - }); - if (isFn(before)) { - tree = before(tree, options); - } - // First give the children some values - each(tree.children, function (child, i) { - var newOptions = extend({}, options); - extend(newOptions, { - index: i, - siblings: tree.children.length, - visible: tree.visible - }); - child = setTreeValues(child, newOptions); - children.push(child); - if (child.visible) { - childrenTotal += child.val; - } - }); - tree.visible = childrenTotal > 0 || tree.visible; - // Set the values - value = pick(optionsPoint.value, childrenTotal); - extend(tree, { - children: children, - childrenTotal: childrenTotal, - isLeaf: tree.visible && !childrenTotal, - val: value - }); - return tree; + var before = options.before, + idRoot = options.idRoot, + mapIdToNode = options.mapIdToNode, + nodeRoot = mapIdToNode[idRoot], + levelIsConstant = ( + isBoolean(options.levelIsConstant) ? + options.levelIsConstant : + true + ), + points = options.points, + point = points[tree.i], + optionsPoint = point && point.options || {}, + childrenTotal = 0, + children = [], + value; + extend(tree, { + levelDynamic: tree.level - (levelIsConstant ? 0 : nodeRoot.level), + name: pick(point && point.name, ''), + visible: ( + idRoot === tree.id || + (isBoolean(options.visible) ? options.visible : false) + ) + }); + if (isFn(before)) { + tree = before(tree, options); + } + // First give the children some values + each(tree.children, function (child, i) { + var newOptions = extend({}, options); + extend(newOptions, { + index: i, + siblings: tree.children.length, + visible: tree.visible + }); + child = setTreeValues(child, newOptions); + children.push(child); + if (child.visible) { + childrenTotal += child.val; + } + }); + tree.visible = childrenTotal > 0 || tree.visible; + // Set the values + value = pick(optionsPoint.value, childrenTotal); + extend(tree, { + children: children, + childrenTotal: childrenTotal, + isLeaf: tree.visible && !childrenTotal, + val: value + }); + return tree; }; var getColor = function getColor(node, options) { - var index = options.index, - mapOptionsToLevel = options.mapOptionsToLevel, - parentColor = options.parentColor, - parentColorIndex = options.parentColorIndex, - series = options.series, - colors = options.colors, - siblings = options.siblings, - points = series.points, - getColorByPoint, - point, - level, - colorByPoint, - colorIndexByPoint, - color, - colorIndex; - function variation(color) { - var colorVariation = level && level.colorVariation; - if (colorVariation) { - if (colorVariation.key === 'brightness') { - return H.color(color).brighten( - colorVariation.to * (index / siblings) - ).get(); - } - } + var index = options.index, + mapOptionsToLevel = options.mapOptionsToLevel, + parentColor = options.parentColor, + parentColorIndex = options.parentColorIndex, + series = options.series, + colors = options.colors, + siblings = options.siblings, + points = series.points, + getColorByPoint, + point, + level, + colorByPoint, + colorIndexByPoint, + color, + colorIndex; + function variation(color) { + var colorVariation = level && level.colorVariation; + if (colorVariation) { + if (colorVariation.key === 'brightness') { + return H.color(color).brighten( + colorVariation.to * (index / siblings) + ).get(); + } + } - return color; - } + return color; + } - if (node) { - point = points[node.i]; - level = mapOptionsToLevel[node.level] || {}; - getColorByPoint = point && level.colorByPoint; + if (node) { + point = points[node.i]; + level = mapOptionsToLevel[node.level] || {}; + getColorByPoint = point && level.colorByPoint; - if (getColorByPoint) { - colorIndexByPoint = point.index % (colors ? - colors.length : - series.chart.options.chart.colorCount - ); - colorByPoint = colors && colors[colorIndexByPoint]; - } + if (getColorByPoint) { + colorIndexByPoint = point.index % (colors ? + colors.length : + series.chart.options.chart.colorCount + ); + colorByPoint = colors && colors[colorIndexByPoint]; + } - /*= if (build.classic) { =*/ - // Select either point color, level color or inherited color. - color = pick( - point && point.options.color, - level && level.color, - colorByPoint, - parentColor && variation(parentColor), - series.color - ); - /*= } =*/ - colorIndex = pick( - point && point.options.colorIndex, - level && level.colorIndex, - colorIndexByPoint, - parentColorIndex, - options.colorIndex - ); - } - return { - color: color, - colorIndex: colorIndex - }; + /*= if (build.classic) { =*/ + // Select either point color, level color or inherited color. + color = pick( + point && point.options.color, + level && level.color, + colorByPoint, + parentColor && variation(parentColor), + series.color + ); + /*= } =*/ + colorIndex = pick( + point && point.options.colorIndex, + level && level.colorIndex, + colorIndexByPoint, + parentColorIndex, + options.colorIndex + ); + } + return { + color: color, + colorIndex: colorIndex + }; }; /** @@ -147,55 +147,55 @@ var getColor = function getColor(node, options) { * Returns null if invalid input parameters. */ var getLevelOptions = function getLevelOptions(params) { - var result = null, - defaults, - converted, - i, - from, - to, - levels; - if (isObject(params)) { - result = {}; - from = isNumber(params.from) ? params.from : 1; - levels = params.levels; - converted = {}; - defaults = isObject(params.defaults) ? params.defaults : {}; - if (isArray(levels)) { - converted = reduce(levels, function (obj, item) { - var level, - levelIsConstant, - options; - if (isObject(item) && isNumber(item.level)) { - options = merge({}, item); - levelIsConstant = ( - isBoolean(options.levelIsConstant) ? - options.levelIsConstant : - defaults.levelIsConstant - ); - // Delete redundant properties. - delete options.levelIsConstant; - delete options.level; - // Calculate which level these options apply to. - level = item.level + (levelIsConstant ? 0 : from - 1); - if (isObject(obj[level])) { - extend(obj[level], options); - } else { - obj[level] = options; - } - } - return obj; - }, {}); - } - to = isNumber(params.to) ? params.to : 1; - for (i = 0; i <= to; i++) { - result[i] = merge( - {}, - defaults, - isObject(converted[i]) ? converted[i] : {} - ); - } - } - return result; + var result = null, + defaults, + converted, + i, + from, + to, + levels; + if (isObject(params)) { + result = {}; + from = isNumber(params.from) ? params.from : 1; + levels = params.levels; + converted = {}; + defaults = isObject(params.defaults) ? params.defaults : {}; + if (isArray(levels)) { + converted = reduce(levels, function (obj, item) { + var level, + levelIsConstant, + options; + if (isObject(item) && isNumber(item.level)) { + options = merge({}, item); + levelIsConstant = ( + isBoolean(options.levelIsConstant) ? + options.levelIsConstant : + defaults.levelIsConstant + ); + // Delete redundant properties. + delete options.levelIsConstant; + delete options.level; + // Calculate which level these options apply to. + level = item.level + (levelIsConstant ? 0 : from - 1); + if (isObject(obj[level])) { + extend(obj[level], options); + } else { + obj[level] = options; + } + } + return obj; + }, {}); + } + to = isNumber(params.to) ? params.to : 1; + for (i = 0; i <= to; i++) { + result[i] = merge( + {}, + defaults, + isObject(converted[i]) ? converted[i] : {} + ); + } + } + return result; }; /** @@ -205,29 +205,29 @@ var getLevelOptions = function getLevelOptions(params) { * @returns Returns the resulting rootId after update. */ var updateRootId = function (series) { - var rootId, - options; - if (isObject(series)) { - // Get the series options. - options = isObject(series.options) ? series.options : {}; + var rootId, + options; + if (isObject(series)) { + // Get the series options. + options = isObject(series.options) ? series.options : {}; - // Calculate the rootId. - rootId = pick(series.rootNode, options.rootId, ''); + // Calculate the rootId. + rootId = pick(series.rootNode, options.rootId, ''); - // Set rootId on series.userOptions to pick it up in exporting. - if (isObject(series.userOptions)) { - series.userOptions.rootId = rootId; - } - // Set rootId on series to pick it up on next update. - series.rootNode = rootId; - } - return rootId; + // Set rootId on series.userOptions to pick it up in exporting. + if (isObject(series.userOptions)) { + series.userOptions.rootId = rootId; + } + // Set rootId on series to pick it up on next update. + series.rootNode = rootId; + } + return rootId; }; var result = { - getColor: getColor, - getLevelOptions: getLevelOptions, - setTreeValues: setTreeValues, - updateRootId: updateRootId + getColor: getColor, + getLevelOptions: getLevelOptions, + setTreeValues: setTreeValues, + updateRootId: updateRootId }; export default result; diff --git a/js/modules/a11y-i18n.src.js b/js/modules/a11y-i18n.src.js index 5ce9663f610..6ee593dd22a 100644 --- a/js/modules/a11y-i18n.src.js +++ b/js/modules/a11y-i18n.src.js @@ -12,7 +12,7 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var each = H.each, - pick = H.pick; + pick = H.pick; /** * String trim that works for IE6-8 as well. @@ -20,7 +20,7 @@ var each = H.each, * @return {string} The trimmed string */ function stringTrim(str) { - return str.trim && str.trim() || str.replace(/^\s+|\s+$/g, ''); + return str.trim && str.trim() || str.replace(/^\s+|\s+$/g, ''); } /** @@ -29,85 +29,85 @@ function stringTrim(str) { * statement within brackets. Invalid array statements return an empty string. */ function formatExtendedStatement(statement, ctx) { - var eachStart = statement.indexOf('#each('), - pluralStart = statement.indexOf('#plural('), - indexStart = statement.indexOf('['), - indexEnd = statement.indexOf(']'), - arr, - result; + var eachStart = statement.indexOf('#each('), + pluralStart = statement.indexOf('#plural('), + indexStart = statement.indexOf('['), + indexEnd = statement.indexOf(']'), + arr, + result; - // Dealing with an each-function? - if (eachStart > -1) { - var eachEnd = statement.slice(eachStart).indexOf(')') + eachStart, - preEach = statement.substring(0, eachStart), - postEach = statement.substring(eachEnd + 1), - eachStatement = statement.substring(eachStart + 6, eachEnd), - eachArguments = eachStatement.split(','), - lenArg = Number(eachArguments[1]), - len; - result = ''; - arr = ctx[eachArguments[0]]; - if (arr) { - lenArg = isNaN(lenArg) ? arr.length : lenArg; - len = lenArg < 0 ? - arr.length + lenArg : - Math.min(lenArg, arr.length); // Overshoot - // Run through the array for the specified length - for (var i = 0; i < len; ++i) { - result += preEach + arr[i] + postEach; - } - } - return result.length ? result : ''; - } + // Dealing with an each-function? + if (eachStart > -1) { + var eachEnd = statement.slice(eachStart).indexOf(')') + eachStart, + preEach = statement.substring(0, eachStart), + postEach = statement.substring(eachEnd + 1), + eachStatement = statement.substring(eachStart + 6, eachEnd), + eachArguments = eachStatement.split(','), + lenArg = Number(eachArguments[1]), + len; + result = ''; + arr = ctx[eachArguments[0]]; + if (arr) { + lenArg = isNaN(lenArg) ? arr.length : lenArg; + len = lenArg < 0 ? + arr.length + lenArg : + Math.min(lenArg, arr.length); // Overshoot + // Run through the array for the specified length + for (var i = 0; i < len; ++i) { + result += preEach + arr[i] + postEach; + } + } + return result.length ? result : ''; + } - // Dealing with a plural-function? - if (pluralStart > -1) { - var pluralEnd = statement.slice(pluralStart).indexOf(')') + pluralStart, - pluralStatement = statement.substring(pluralStart + 8, pluralEnd), - pluralArguments = pluralStatement.split(','), - num = Number(ctx[pluralArguments[0]]); - switch (num) { - case 0: - result = pick(pluralArguments[4], pluralArguments[1]); - break; - case 1: - result = pick(pluralArguments[2], pluralArguments[1]); - break; - case 2: - result = pick(pluralArguments[3], pluralArguments[1]); - break; - default: - result = pluralArguments[1]; - } - return result ? stringTrim(result) : ''; - } + // Dealing with a plural-function? + if (pluralStart > -1) { + var pluralEnd = statement.slice(pluralStart).indexOf(')') + pluralStart, + pluralStatement = statement.substring(pluralStart + 8, pluralEnd), + pluralArguments = pluralStatement.split(','), + num = Number(ctx[pluralArguments[0]]); + switch (num) { + case 0: + result = pick(pluralArguments[4], pluralArguments[1]); + break; + case 1: + result = pick(pluralArguments[2], pluralArguments[1]); + break; + case 2: + result = pick(pluralArguments[3], pluralArguments[1]); + break; + default: + result = pluralArguments[1]; + } + return result ? stringTrim(result) : ''; + } - // Array index - if (indexStart > -1) { - var arrayName = statement.substring(0, indexStart), - ix = Number(statement.substring(indexStart + 1, indexEnd)), - val; - arr = ctx[arrayName]; - if (!isNaN(ix) && arr) { - if (ix < 0) { - val = arr[arr.length + ix]; - // Handle negative overshoot - if (val === undefined) { - val = arr[0]; - } - } else { - val = arr[ix]; - // Handle positive overshoot - if (val === undefined) { - val = arr[arr.length - 1]; - } - } - } - return val !== undefined ? val : ''; - } + // Array index + if (indexStart > -1) { + var arrayName = statement.substring(0, indexStart), + ix = Number(statement.substring(indexStart + 1, indexEnd)), + val; + arr = ctx[arrayName]; + if (!isNaN(ix) && arr) { + if (ix < 0) { + val = arr[arr.length + ix]; + // Handle negative overshoot + if (val === undefined) { + val = arr[0]; + } + } else { + val = arr[ix]; + // Handle positive overshoot + if (val === undefined) { + val = arr[arr.length - 1]; + } + } + } + return val !== undefined ? val : ''; + } - // Standard substitution, delegate to H.format or similar - return '{' + statement + '}'; + // Standard substitution, delegate to H.format or similar + return '{' + statement + '}'; } @@ -159,64 +159,64 @@ function formatExtendedStatement(statement, ctx) { * @return {string} The formatted string. */ H.i18nFormat = function (formatString, context, time) { - var getFirstBracketStatement = function (sourceStr, offset) { - var str = sourceStr.slice(offset || 0), - startBracket = str.indexOf('{'), - endBracket = str.indexOf('}'); - if (startBracket > -1 && endBracket > startBracket) { - return { - statement: str.substring(startBracket + 1, endBracket), - begin: offset + startBracket + 1, - end: offset + endBracket - }; - } - }, - tokens = [], - bracketRes, - constRes, - cursor = 0; + var getFirstBracketStatement = function (sourceStr, offset) { + var str = sourceStr.slice(offset || 0), + startBracket = str.indexOf('{'), + endBracket = str.indexOf('}'); + if (startBracket > -1 && endBracket > startBracket) { + return { + statement: str.substring(startBracket + 1, endBracket), + begin: offset + startBracket + 1, + end: offset + endBracket + }; + } + }, + tokens = [], + bracketRes, + constRes, + cursor = 0; - // Tokenize format string into bracket statements and constants - do { - bracketRes = getFirstBracketStatement(formatString, cursor); - constRes = formatString.substring( - cursor, - bracketRes && bracketRes.begin - 1 - ); + // Tokenize format string into bracket statements and constants + do { + bracketRes = getFirstBracketStatement(formatString, cursor); + constRes = formatString.substring( + cursor, + bracketRes && bracketRes.begin - 1 + ); - // If we have constant content before this bracket statement, add it - if (constRes.length) { - tokens.push({ - value: constRes, - type: 'constant' - }); - } + // If we have constant content before this bracket statement, add it + if (constRes.length) { + tokens.push({ + value: constRes, + type: 'constant' + }); + } - // Add the bracket statement - if (bracketRes) { - tokens.push({ - value: bracketRes.statement, - type: 'statement' - }); - } + // Add the bracket statement + if (bracketRes) { + tokens.push({ + value: bracketRes.statement, + type: 'statement' + }); + } - cursor = bracketRes && bracketRes.end + 1; - } while (bracketRes); + cursor = bracketRes && bracketRes.end + 1; + } while (bracketRes); - // Perform the formatting. The formatArrayStatement function returns the - // statement in brackets if it is not an array statement, which means it - // gets picked up by H.format below. - each(tokens, function (token) { - if (token.type === 'statement') { - token.value = formatExtendedStatement(token.value, context); - } - }); + // Perform the formatting. The formatArrayStatement function returns the + // statement in brackets if it is not an array statement, which means it + // gets picked up by H.format below. + each(tokens, function (token) { + if (token.type === 'statement') { + token.value = formatExtendedStatement(token.value, context); + } + }); - // Join string back together and pass to H.format to pick up non-array - // statements. - return H.format(H.reduce(tokens, function (acc, cur) { - return acc + cur.value; - }, ''), context, time); + // Join string back together and pass to H.format to pick up non-array + // statements. + return H.format(H.reduce(tokens, function (acc, cur) { + return acc + cur.value; + }, ''), context, time); }; @@ -227,247 +227,247 @@ H.i18nFormat = function (formatString, context, time) { * @return {string} The formatted string */ H.Chart.prototype.langFormat = function (langKey, context, time) { - var keys = langKey.split('.'), - formatString = this.options.lang, - i = 0; - for (; i < keys.length; ++i) { - formatString = formatString && formatString[keys[i]]; - } - return typeof formatString === 'string' && H.i18nFormat( - formatString, context, time - ); + var keys = langKey.split('.'), + formatString = this.options.lang, + i = 0; + for (; i < keys.length; ++i) { + formatString = formatString && formatString[keys[i]]; + } + return typeof formatString === 'string' && H.i18nFormat( + formatString, context, time + ); }; H.setOptions({ - lang: { - /** - * Configure the accessibility strings in the chart. Requires the - * [accessibility module](//code.highcharts.com/modules/accessibility. - * js) to be loaded. For a description of the module and information - * on its features, see [Highcharts Accessibility](http://www.highcharts. - * com/docs/chart-concepts/accessibility). - * - * For more dynamic control over the accessibility functionality, see - * [accessibility.pointDescriptionFormatter]( - * accessibility.pointDescriptionFormatter), - * [accessibility.seriesDescriptionFormatter]( - * accessibility.seriesDescriptionFormatter), and - * [accessibility.screenReaderSectionFormatter]( - * accessibility.screenReaderSectionFormatter). - * - * @since 6.0.6 - * @type {Object} - * @optionparent lang.accessibility - */ - accessibility: { - /* eslint-disable max-len */ + lang: { + /** + * Configure the accessibility strings in the chart. Requires the + * [accessibility module](//code.highcharts.com/modules/accessibility. + * js) to be loaded. For a description of the module and information + * on its features, see [Highcharts Accessibility](http://www.highcharts. + * com/docs/chart-concepts/accessibility). + * + * For more dynamic control over the accessibility functionality, see + * [accessibility.pointDescriptionFormatter]( + * accessibility.pointDescriptionFormatter), + * [accessibility.seriesDescriptionFormatter]( + * accessibility.seriesDescriptionFormatter), and + * [accessibility.screenReaderSectionFormatter]( + * accessibility.screenReaderSectionFormatter). + * + * @since 6.0.6 + * @type {Object} + * @optionparent lang.accessibility + */ + accessibility: { + /* eslint-disable max-len */ - screenReaderRegionLabel: 'Chart screen reader information.', - navigationHint: 'Use regions/landmarks to skip ahead to chart {#plural(numSeries, and navigate between data series,)}', - defaultChartTitle: 'Chart', - longDescriptionHeading: 'Long description.', - noDescription: 'No description available.', - structureHeading: 'Structure.', - viewAsDataTable: 'View as data table.', - chartHeading: 'Chart graphic.', - chartContainerLabel: 'Interactive chart. {title}. Use up and down arrows to navigate with most screen readers.', - rangeSelectorMinInput: 'Select start date.', - rangeSelectorMaxInput: 'Select end date.', - tableSummary: 'Table representation of chart.', - mapZoomIn: 'Zoom chart', - mapZoomOut: 'Zoom out chart', - rangeSelectorButton: 'Select range {buttonText}', - legendItem: 'Toggle visibility of series {itemName}', + screenReaderRegionLabel: 'Chart screen reader information.', + navigationHint: 'Use regions/landmarks to skip ahead to chart {#plural(numSeries, and navigate between data series,)}', + defaultChartTitle: 'Chart', + longDescriptionHeading: 'Long description.', + noDescription: 'No description available.', + structureHeading: 'Structure.', + viewAsDataTable: 'View as data table.', + chartHeading: 'Chart graphic.', + chartContainerLabel: 'Interactive chart. {title}. Use up and down arrows to navigate with most screen readers.', + rangeSelectorMinInput: 'Select start date.', + rangeSelectorMaxInput: 'Select end date.', + tableSummary: 'Table representation of chart.', + mapZoomIn: 'Zoom chart', + mapZoomOut: 'Zoom out chart', + rangeSelectorButton: 'Select range {buttonText}', + legendItem: 'Toggle visibility of series {itemName}', - /** - * Title element text for the chart SVG element. Leave this - * empty to disable adding the title element. Browsers will display - * this content when hovering over elements in the chart. Assistive - * technology may use this element to label the chart. - * - * @since 6.0.8 - */ - svgContainerTitle: '{chartTitle}', + /** + * Title element text for the chart SVG element. Leave this + * empty to disable adding the title element. Browsers will display + * this content when hovering over elements in the chart. Assistive + * technology may use this element to label the chart. + * + * @since 6.0.8 + */ + svgContainerTitle: '{chartTitle}', - /** - * Descriptions of lesser known series types. The relevant - * description is added to the screen reader information region - * when these series types are used. - * - * @since 6.0.6 - * @type {Object} - * @optionparent lang.accessibility.seriesTypeDescriptions - */ - seriesTypeDescriptions: { - boxplot: 'Box plot charts are typically used to display ' + - 'groups of statistical data. Each data point in the ' + - 'chart can have up to 5 values: minimum, lower quartile, ' + - 'median, upper quartile, and maximum.', - arearange: 'Arearange charts are line charts displaying a ' + - 'range between a lower and higher value for each point.', - areasplinerange: 'These charts are line charts displaying a ' + - 'range between a lower and higher value for each point.', - bubble: 'Bubble charts are scatter charts where each data ' + - 'point also has a size value.', - columnrange: 'Columnrange charts are column charts ' + - 'displaying a range between a lower and higher value for ' + - 'each point.', - errorbar: 'Errorbar series are used to display the ' + - 'variability of the data.', - funnel: 'Funnel charts are used to display reduction of data ' + - 'in stages.', - pyramid: 'Pyramid charts consist of a single pyramid with ' + - 'item heights corresponding to each point value.', - waterfall: 'A waterfall chart is a column chart where each ' + - 'column contributes towards a total end value.' - }, + /** + * Descriptions of lesser known series types. The relevant + * description is added to the screen reader information region + * when these series types are used. + * + * @since 6.0.6 + * @type {Object} + * @optionparent lang.accessibility.seriesTypeDescriptions + */ + seriesTypeDescriptions: { + boxplot: 'Box plot charts are typically used to display ' + + 'groups of statistical data. Each data point in the ' + + 'chart can have up to 5 values: minimum, lower quartile, ' + + 'median, upper quartile, and maximum.', + arearange: 'Arearange charts are line charts displaying a ' + + 'range between a lower and higher value for each point.', + areasplinerange: 'These charts are line charts displaying a ' + + 'range between a lower and higher value for each point.', + bubble: 'Bubble charts are scatter charts where each data ' + + 'point also has a size value.', + columnrange: 'Columnrange charts are column charts ' + + 'displaying a range between a lower and higher value for ' + + 'each point.', + errorbar: 'Errorbar series are used to display the ' + + 'variability of the data.', + funnel: 'Funnel charts are used to display reduction of data ' + + 'in stages.', + pyramid: 'Pyramid charts consist of a single pyramid with ' + + 'item heights corresponding to each point value.', + waterfall: 'A waterfall chart is a column chart where each ' + + 'column contributes towards a total end value.' + }, - /** - * Chart type description strings. This is added to the chart - * information region. - * - * If there is only a single series type used in the chart, we use - * the format string for the series type, or default if missing. - * There is one format string for cases where there is only a single - * series in the chart, and one for multiple series of the same - * type. - * - * @since 6.0.6 - * @type {Object} - * @optionparent lang.accessibility.chartTypes - */ - chartTypes: { - emptyChart: 'Empty chart', - mapTypeDescription: 'Map of {mapTitle} with {numSeries} data series.', - unknownMap: 'Map of unspecified region with {numSeries} data series.', - combinationChart: 'Combination chart with {numSeries} data series.', - defaultSingle: 'Chart with {numPoints} data {#plural(numPoints, points, point)}.', - defaultMultiple: 'Chart with {numSeries} data series.', - splineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.', - splineMultiple: 'Line chart with {numSeries} lines.', - lineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.', - lineMultiple: 'Line chart with {numSeries} lines.', - columnSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.', - columnMultiple: 'Bar chart with {numSeries} data series.', - barSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.', - barMultiple: 'Bar chart with {numSeries} data series.', - pieSingle: 'Pie chart with {numPoints} {#plural(numPoints, slices, slice)}.', - pieMultiple: 'Pie chart with {numSeries} pies.', - scatterSingle: 'Scatter chart with {numPoints} {#plural(numPoints, points, point)}.', - scatterMultiple: 'Scatter chart with {numSeries} data series.', - boxplotSingle: 'Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.', - boxplotMultiple: 'Boxplot with {numSeries} data series.', - bubbleSingle: 'Bubble chart with {numPoints} {#plural(numPoints, bubbles, bubble)}.', - bubbleMultiple: 'Bubble chart with {numSeries} data series.' - }, + /** + * Chart type description strings. This is added to the chart + * information region. + * + * If there is only a single series type used in the chart, we use + * the format string for the series type, or default if missing. + * There is one format string for cases where there is only a single + * series in the chart, and one for multiple series of the same + * type. + * + * @since 6.0.6 + * @type {Object} + * @optionparent lang.accessibility.chartTypes + */ + chartTypes: { + emptyChart: 'Empty chart', + mapTypeDescription: 'Map of {mapTitle} with {numSeries} data series.', + unknownMap: 'Map of unspecified region with {numSeries} data series.', + combinationChart: 'Combination chart with {numSeries} data series.', + defaultSingle: 'Chart with {numPoints} data {#plural(numPoints, points, point)}.', + defaultMultiple: 'Chart with {numSeries} data series.', + splineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.', + splineMultiple: 'Line chart with {numSeries} lines.', + lineSingle: 'Line chart with {numPoints} data {#plural(numPoints, points, point)}.', + lineMultiple: 'Line chart with {numSeries} lines.', + columnSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.', + columnMultiple: 'Bar chart with {numSeries} data series.', + barSingle: 'Bar chart with {numPoints} {#plural(numPoints, bars, bar)}.', + barMultiple: 'Bar chart with {numSeries} data series.', + pieSingle: 'Pie chart with {numPoints} {#plural(numPoints, slices, slice)}.', + pieMultiple: 'Pie chart with {numSeries} pies.', + scatterSingle: 'Scatter chart with {numPoints} {#plural(numPoints, points, point)}.', + scatterMultiple: 'Scatter chart with {numSeries} data series.', + boxplotSingle: 'Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.', + boxplotMultiple: 'Boxplot with {numSeries} data series.', + bubbleSingle: 'Bubble chart with {numPoints} {#plural(numPoints, bubbles, bubble)}.', + bubbleMultiple: 'Bubble chart with {numSeries} data series.' + }, - /** - * Axis description format strings. - * - * @since 6.0.6 - * @type {Object} - * @optionparent lang.accessibility.axis - */ - axis: { - xAxisDescriptionSingular: 'The chart has 1 X axis displaying {names[0]}.', - xAxisDescriptionPlural: 'The chart has {numAxes} X axes displaying {#each(names, -1), }and {names[-1]}', - yAxisDescriptionSingular: 'The chart has 1 Y axis displaying {names[0]}.', - yAxisDescriptionPlural: 'The chart has {numAxes} Y axes displaying {#each(names, -1), }and {names[-1]}' - }, + /** + * Axis description format strings. + * + * @since 6.0.6 + * @type {Object} + * @optionparent lang.accessibility.axis + */ + axis: { + xAxisDescriptionSingular: 'The chart has 1 X axis displaying {names[0]}.', + xAxisDescriptionPlural: 'The chart has {numAxes} X axes displaying {#each(names, -1), }and {names[-1]}', + yAxisDescriptionSingular: 'The chart has 1 Y axis displaying {names[0]}.', + yAxisDescriptionPlural: 'The chart has {numAxes} Y axes displaying {#each(names, -1), }and {names[-1]}' + }, - /** - * Exporting menu format strings for accessibility module. - * - * @since 6.0.6 - * @type {Object} - * @optionparent lang.accessibility.exporting - */ - exporting: { - chartMenuLabel: 'Chart export', - menuButtonLabel: 'View export menu', - exportRegionLabel: 'Chart export menu' - }, + /** + * Exporting menu format strings for accessibility module. + * + * @since 6.0.6 + * @type {Object} + * @optionparent lang.accessibility.exporting + */ + exporting: { + chartMenuLabel: 'Chart export', + menuButtonLabel: 'View export menu', + exportRegionLabel: 'Chart export menu' + }, - /** - * Lang configuration for different series types. For more dynamic - * control over the series element descriptions, see - * [accessibility.seriesDescriptionFormatter]( - * accessibility.seriesDescriptionFormatter). - * - * @since 6.0.6 - * @type {Object} - * @optionparent lang.accessibility.series - */ - series: { - /** - * Lang configuration for the series main summary. Each series - * type has two modes: - * 1. This series type is the only series type used in the - * chart - * 2. This is a combination chart with multiple series types - * - * If a definition does not exist for the specific series type - * and mode, the 'default' lang definitions are used. - * - * @since 6.0.6 - * @type {Object} - * @optionparent lang.accessibility.series.summary - */ - summary: { - default: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', - defaultCombination: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', - line: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', - lineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', - spline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', - splineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', - column: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.', - columnCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.', - bar: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.', - barCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.', - pie: '{name}, pie {ix} of {numSeries} with {numPoints} {#plural(numPoints, slices, slice)}.', - pieCombination: '{name}, series {ix} of {numSeries}. Pie with {numPoints} {#plural(numPoints, slices, slice)}.', - scatter: '{name}, scatter plot {ix} of {numSeries} with {numPoints} {#plural(numPoints, points, point)}.', - scatterCombination: '{name}, series {ix} of {numSeries}, scatter plot with {numPoints} {#plural(numPoints, points, point)}.', - boxplot: '{name}, boxplot {ix} of {numSeries} with {numPoints} {#plural(numPoints, boxes, box)}.', - boxplotCombination: '{name}, series {ix} of {numSeries}. Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.', - bubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.', - bubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.', - map: '{name}, map {ix} of {numSeries} with {numPoints} {#plural(numPoints, areas, area)}.', - mapCombination: '{name}, series {ix} of {numSeries}. Map with {numPoints} {#plural(numPoints, areas, area)}.', - mapline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', - maplineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', - mapbubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.', - mapbubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.' - }, - /* eslint-enable max-len */ + /** + * Lang configuration for different series types. For more dynamic + * control over the series element descriptions, see + * [accessibility.seriesDescriptionFormatter]( + * accessibility.seriesDescriptionFormatter). + * + * @since 6.0.6 + * @type {Object} + * @optionparent lang.accessibility.series + */ + series: { + /** + * Lang configuration for the series main summary. Each series + * type has two modes: + * 1. This series type is the only series type used in the + * chart + * 2. This is a combination chart with multiple series types + * + * If a definition does not exist for the specific series type + * and mode, the 'default' lang definitions are used. + * + * @since 6.0.6 + * @type {Object} + * @optionparent lang.accessibility.series.summary + */ + summary: { + default: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', + defaultCombination: '{name}, series {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', + line: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', + lineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', + spline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', + splineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', + column: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.', + columnCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.', + bar: '{name}, bar series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bars, bar)}.', + barCombination: '{name}, series {ix} of {numSeries}. Bar series with {numPoints} {#plural(numPoints, bars, bar)}.', + pie: '{name}, pie {ix} of {numSeries} with {numPoints} {#plural(numPoints, slices, slice)}.', + pieCombination: '{name}, series {ix} of {numSeries}. Pie with {numPoints} {#plural(numPoints, slices, slice)}.', + scatter: '{name}, scatter plot {ix} of {numSeries} with {numPoints} {#plural(numPoints, points, point)}.', + scatterCombination: '{name}, series {ix} of {numSeries}, scatter plot with {numPoints} {#plural(numPoints, points, point)}.', + boxplot: '{name}, boxplot {ix} of {numSeries} with {numPoints} {#plural(numPoints, boxes, box)}.', + boxplotCombination: '{name}, series {ix} of {numSeries}. Boxplot with {numPoints} {#plural(numPoints, boxes, box)}.', + bubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.', + bubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.', + map: '{name}, map {ix} of {numSeries} with {numPoints} {#plural(numPoints, areas, area)}.', + mapCombination: '{name}, series {ix} of {numSeries}. Map with {numPoints} {#plural(numPoints, areas, area)}.', + mapline: '{name}, line {ix} of {numSeries} with {numPoints} data {#plural(numPoints, points, point)}.', + maplineCombination: '{name}, series {ix} of {numSeries}. Line with {numPoints} data {#plural(numPoints, points, point)}.', + mapbubble: '{name}, bubble series {ix} of {numSeries} with {numPoints} {#plural(numPoints, bubbles, bubble)}.', + mapbubbleCombination: '{name}, series {ix} of {numSeries}. Bubble series with {numPoints} {#plural(numPoints, bubbles, bubble)}.' + }, + /* eslint-enable max-len */ - /** - * User supplied description text. This is added after the main - * summary if present. - * - * @type {String} - * @since 6.0.6 - */ - description: '{description}', + /** + * User supplied description text. This is added after the main + * summary if present. + * + * @type {String} + * @since 6.0.6 + */ + description: '{description}', - /** - * xAxis description for series if there are multiple xAxes in - * the chart. - * - * @type {String} - * @since 6.0.6 - */ - xAxisDescription: 'X axis, {name}', + /** + * xAxis description for series if there are multiple xAxes in + * the chart. + * + * @type {String} + * @since 6.0.6 + */ + xAxisDescription: 'X axis, {name}', - /** - * yAxis description for series if there are multiple yAxes in - * the chart. - * - * @type {String} - * @since 6.0.6 - */ - yAxisDescription: 'Y axis, {name}' - } - } - } + /** + * yAxis description for series if there are multiple yAxes in + * the chart. + * + * @type {String} + * @since 6.0.6 + */ + yAxisDescription: 'Y axis, {name}' + } + } + } }); diff --git a/js/modules/annotations.src.js b/js/modules/annotations.src.js index 3212e8b05d1..e2081442f6b 100644 --- a/js/modules/annotations.src.js +++ b/js/modules/annotations.src.js @@ -10,29 +10,29 @@ import '../parts/Chart.js'; import '../parts/Series.js'; import '../parts/Tooltip.js'; -var merge = H.merge, - addEvent = H.addEvent, - extend = H.extend, - each = H.each, - isString = H.isString, - isNumber = H.isNumber, - defined = H.defined, - isObject = H.isObject, - inArray = H.inArray, - erase = H.erase, - find = H.find, - format = H.format, - pick = H.pick, - objectEach = H.objectEach, - uniqueKey = H.uniqueKey, - doc = H.doc, - splat = H.splat, - destroyObjectProperties = H.destroyObjectProperties, - grep = H.grep, - - tooltipPrototype = H.Tooltip.prototype, - seriesPrototype = H.Series.prototype, - chartPrototype = H.Chart.prototype; +var merge = H.merge, + addEvent = H.addEvent, + extend = H.extend, + each = H.each, + isString = H.isString, + isNumber = H.isNumber, + defined = H.defined, + isObject = H.isObject, + inArray = H.inArray, + erase = H.erase, + find = H.find, + format = H.format, + pick = H.pick, + objectEach = H.objectEach, + uniqueKey = H.uniqueKey, + doc = H.doc, + splat = H.splat, + destroyObjectProperties = H.destroyObjectProperties, + grep = H.grep, + + tooltipPrototype = H.Tooltip.prototype, + seriesPrototype = H.Series.prototype, + chartPrototype = H.Chart.prototype; /* *************************************************************************** @@ -74,108 +74,108 @@ var merge = H.merge, * @apioption defs */ var defaultMarkers = { - arrow: { - tagName: 'marker', - render: false, - id: 'arrow', - refY: 5, - refX: 5, - markerWidth: 10, - markerHeight: 10, - children: [{ - tagName: 'path', - d: 'M 0 0 L 10 5 L 0 10 Z', // triangle (used as an arrow) - /*= if (build.classic) { =*/ - strokeWidth: 0 - /*= } =*/ - }] - } + arrow: { + tagName: 'marker', + render: false, + id: 'arrow', + refY: 5, + refX: 5, + markerWidth: 10, + markerHeight: 10, + children: [{ + tagName: 'path', + d: 'M 0 0 L 10 5 L 0 10 Z', // triangle (used as an arrow) + /*= if (build.classic) { =*/ + strokeWidth: 0 + /*= } =*/ + }] + } }; var MarkerMixin = { - markerSetter: function (markerType) { - return function (value) { - this.attr(markerType, 'url(#' + value + ')'); - }; - } + markerSetter: function (markerType) { + return function (value) { + this.attr(markerType, 'url(#' + value + ')'); + }; + } }; extend(MarkerMixin, { - markerEndSetter: MarkerMixin.markerSetter('marker-end'), - markerStartSetter: MarkerMixin.markerSetter('marker-start') + markerEndSetter: MarkerMixin.markerSetter('marker-end'), + markerStartSetter: MarkerMixin.markerSetter('marker-start') }); /*= if (build.classic) { =*/ // In a styled mode definition is implemented H.SVGRenderer.prototype.definition = function (def) { - var ren = this; + var ren = this; - function recurse(config, parent) { - var ret; - each(splat(config), function (item) { - var node = ren.createElement(item.tagName), - attr = {}; + function recurse(config, parent) { + var ret; + each(splat(config), function (item) { + var node = ren.createElement(item.tagName), + attr = {}; - // Set attributes - objectEach(item, function (val, key) { - if ( + // Set attributes + objectEach(item, function (val, key) { + if ( key !== 'tagName' && key !== 'children' && key !== 'textContent' ) { - attr[key] = val; - } - }); - node.attr(attr); + attr[key] = val; + } + }); + node.attr(attr); - // Add to the tree - node.add(parent || ren.defs); + // Add to the tree + node.add(parent || ren.defs); - // Add text content - if (item.textContent) { - node.element.appendChild( + // Add text content + if (item.textContent) { + node.element.appendChild( doc.createTextNode(item.textContent) ); - } + } - // Recurse - recurse(item.children || [], node); + // Recurse + recurse(item.children || [], node); - ret = node; - }); + ret = node; + }); - // Return last node added (on top level it's the only one) - return ret; - } - return recurse(def); + // Return last node added (on top level it's the only one) + return ret; + } + return recurse(def); }; /*= } =*/ H.SVGRenderer.prototype.addMarker = function (id, markerOptions) { - var options = { id: id }; - - /*= if (build.classic) { =*/ - var attrs = { - stroke: markerOptions.color || 'none', - fill: markerOptions.color || 'rgba(0, 0, 0, 0.75)' - }; - - options.children = H.map(markerOptions.children, function (child) { - return merge(attrs, child); - }); - /*= } =*/ - - var marker = this.definition(merge({ - markerWidth: 20, - markerHeight: 20, - refX: 0, - refY: 0, - orient: 'auto' - }, markerOptions, options)); - - marker.id = id; - - return marker; + var options = { id: id }; + + /*= if (build.classic) { =*/ + var attrs = { + stroke: markerOptions.color || 'none', + fill: markerOptions.color || 'rgba(0, 0, 0, 0.75)' + }; + + options.children = H.map(markerOptions.children, function (child) { + return merge(attrs, child); + }); + /*= } =*/ + + var marker = this.definition(merge({ + markerWidth: 20, + markerHeight: 20, + refX: 0, + refY: 0, + orient: 'auto' + }, markerOptions, options)); + + marker.id = id; + + return marker; }; @@ -200,7 +200,7 @@ H.SVGRenderer.prototype.addMarker = function (id, markerOptions) { * A trimmed point object which imitates {@link Highchart.Point} class. * It is created when there is a need of pointing to some chart's position * using axis values or pixel values - * + * * @class MockPoint * @memberOf Highcharts * @private @@ -209,26 +209,26 @@ H.SVGRenderer.prototype.addMarker = function (id, markerOptions) { * @param {MockPointOptions} - the options object */ var MockPoint = H.MockPoint = function (chart, options) { - this.mock = true; - this.series = { - visible: true, - chart: chart, - getPlotBox: seriesPrototype.getPlotBox - }; + this.mock = true; + this.series = { + visible: true, + chart: chart, + getPlotBox: seriesPrototype.getPlotBox + }; - // this.plotX - // this.plotY + // this.plotX + // this.plotY - /* Those might not exist if a specific axis was not found/defined */ - // this.x? - // this.y? + /* Those might not exist if a specific axis was not found/defined */ + // this.x? + // this.y? - this.init(chart, options); + this.init(chart, options); }; /** * A factory function for creating a mock point object - * + * * @function #mockPoint * @memberOf Highcharts * @@ -236,144 +236,144 @@ var MockPoint = H.MockPoint = function (chart, options) { * @return {MockPoint} a mock point */ var mockPoint = H.mockPoint = function (chart, mockPointOptions) { - return new MockPoint(chart, mockPointOptions); + return new MockPoint(chart, mockPointOptions); }; MockPoint.prototype = { - /** - * Initialisation of the mock point - * - * @function init - * @memberOf Highcharts.MockPoint# - * - * @param {Highcharts.Chart} chart - a chart object to which the mock point - * is attached - * @param {MockPointOptions} options - a config for the mock point - */ - init: function (chart, options) { - var xAxisId = options.xAxis, - xAxis = defined(xAxisId) ? - chart.xAxis[xAxisId] || chart.get(xAxisId) : - null, - - yAxisId = options.yAxis, - yAxis = defined(yAxisId) ? - chart.yAxis[yAxisId] || chart.get(yAxisId) : - null; - - - if (xAxis) { - this.x = options.x; - this.series.xAxis = xAxis; - } else { - this.plotX = options.x; - } - - if (yAxis) { - this.y = options.y; - this.series.yAxis = yAxis; - } else { - this.plotY = options.y; - } - }, - - /** - * Update of the point's coordinates (plotX/plotY) - * - * @function translate - * @memberOf Highcharts.MockPoint# - * - * @return {undefined} - */ - translate: function () { - var series = this.series, - xAxis = series.xAxis, - yAxis = series.yAxis; - - if (xAxis) { - this.plotX = xAxis.toPixels(this.x, true); - } - - if (yAxis) { - this.plotY = yAxis.toPixels(this.y, true); - } - - this.isInside = this.isInsidePane(); - }, - - /** - * Returns a box to which an item can be aligned to - * - * @function #alignToBox - * @memberOf Highcharts.MockPoint# - * - * @param {Boolean} [forceTranslate=false] - whether to update the point's - * coordinates - * @return {Array.} A quadruple of numbers which denotes x, y, - * width and height of the box - **/ - alignToBox: function (forceTranslate) { - if (forceTranslate) { - this.translate(); - } - - var x = this.plotX, - y = this.plotY, - temp; - - - if (this.series.chart.inverted) { - temp = x; - x = y; - y = temp; - } - - return [x, y, 0, 0]; - }, - - /** - * Returns a label config object - - * the same as Highcharts.Point.prototype.getLabelConfig - * - * @function getLabelConfig - * @memberOf Highcharts.MockPoint# - * - * @return {Object} labelConfig - label config object - * @return {Number|undefined} labelConfig.x - * X value translated to x axis scale - * @return {Number|undefined} labelConfig.y - * Y value translated to y axis scale - * @return {MockPoint} labelConfig.point - * The instance of the point - */ - getLabelConfig: function () { - return { - x: this.x, - y: this.y, - point: this - }; - }, - - isInsidePane: function () { - var plotX = this.plotX, - plotY = this.plotY, - xAxis = this.series.xAxis, - yAxis = this.series.yAxis, - isInside = true; - - if (xAxis) { - isInside = defined(plotX) && plotX >= 0 && plotX <= xAxis.len; - } - - if (yAxis) { - isInside = - isInside && - defined(plotY) && - plotY >= 0 && plotY <= yAxis.len; - } - - return isInside; - } + /** + * Initialisation of the mock point + * + * @function init + * @memberOf Highcharts.MockPoint# + * + * @param {Highcharts.Chart} chart - a chart object to which the mock point + * is attached + * @param {MockPointOptions} options - a config for the mock point + */ + init: function (chart, options) { + var xAxisId = options.xAxis, + xAxis = defined(xAxisId) ? + chart.xAxis[xAxisId] || chart.get(xAxisId) : + null, + + yAxisId = options.yAxis, + yAxis = defined(yAxisId) ? + chart.yAxis[yAxisId] || chart.get(yAxisId) : + null; + + + if (xAxis) { + this.x = options.x; + this.series.xAxis = xAxis; + } else { + this.plotX = options.x; + } + + if (yAxis) { + this.y = options.y; + this.series.yAxis = yAxis; + } else { + this.plotY = options.y; + } + }, + + /** + * Update of the point's coordinates (plotX/plotY) + * + * @function translate + * @memberOf Highcharts.MockPoint# + * + * @return {undefined} + */ + translate: function () { + var series = this.series, + xAxis = series.xAxis, + yAxis = series.yAxis; + + if (xAxis) { + this.plotX = xAxis.toPixels(this.x, true); + } + + if (yAxis) { + this.plotY = yAxis.toPixels(this.y, true); + } + + this.isInside = this.isInsidePane(); + }, + + /** + * Returns a box to which an item can be aligned to + * + * @function #alignToBox + * @memberOf Highcharts.MockPoint# + * + * @param {Boolean} [forceTranslate=false] - whether to update the point's + * coordinates + * @return {Array.} A quadruple of numbers which denotes x, y, + * width and height of the box + **/ + alignToBox: function (forceTranslate) { + if (forceTranslate) { + this.translate(); + } + + var x = this.plotX, + y = this.plotY, + temp; + + + if (this.series.chart.inverted) { + temp = x; + x = y; + y = temp; + } + + return [x, y, 0, 0]; + }, + + /** + * Returns a label config object - + * the same as Highcharts.Point.prototype.getLabelConfig + * + * @function getLabelConfig + * @memberOf Highcharts.MockPoint# + * + * @return {Object} labelConfig - label config object + * @return {Number|undefined} labelConfig.x + * X value translated to x axis scale + * @return {Number|undefined} labelConfig.y + * Y value translated to y axis scale + * @return {MockPoint} labelConfig.point + * The instance of the point + */ + getLabelConfig: function () { + return { + x: this.x, + y: this.y, + point: this + }; + }, + + isInsidePane: function () { + var plotX = this.plotX, + plotY = this.plotY, + xAxis = this.series.xAxis, + yAxis = this.series.yAxis, + isInside = true; + + if (xAxis) { + isInside = defined(plotX) && plotX >= 0 && plotX <= xAxis.len; + } + + if (yAxis) { + isInside = + isInside && + defined(plotY) && + plotY >= 0 && plotY <= yAxis.len; + } + + return isInside; + } }; @@ -388,8 +388,8 @@ H.defaultOptions.annotations = []; /** * An annotation class which serves as a container for items like labels or * shapes. Created items are positioned on the chart either by linking them to - * existing points or created mock points - * + * existing points or created mock points + * * @class Annotation * @memberOf Highcharts * @@ -398,1309 +398,1309 @@ H.defaultOptions.annotations = []; */ var Annotation = H.Annotation = function (chart, userOptions) { - /** - * The chart that the annotation belongs to. - * - * @name chart - * @memberOf Highcharts.Annotation# - * @type {Chart} - */ - this.chart = chart; - - /** - * The array of labels which belong to the annotation. - * - * @name labels - * @memberOf Highcharts.Annotation# - * @type {Array} - */ - this.labels = []; - - /** - * The array of shapes which belong to the annotation. - * - * @name shapes - * @memberOf Highcharts.Annotation# - * @type {Array} - */ - this.shapes = []; - - /** - * The options for the annotations. It containers user defined options - * merged with the default options. - * - * @name options - * @memberOf Highcharts.Annotation# - * @type {AnnotationOptions} - */ - this.options = merge(this.defaultOptions, userOptions); - - /** - * The callback that reports to the overlapping-labels module which - * labels it should account for. - * - * @name labelCollector - * @memberOf Highcharts.Annotation# - * @type {Function} - * @private - */ - - /** - * The group element of the annotation. - * - * @name group - * @memberOf Highcharts.Annotation# - * @type {Highcharts.SVGElement} - * @private - */ - - /** - * The group element of the annotation's shapes. - * - * @name shapesGroup - * @memberOf Highcharts.Annotation# - * @type {Highcharts.SVGElement} - * @private - */ - - /** - * The group element of the annotation's labels. - * - * @name labelsGroup - * @memberOf Highcharts.Annotation# - * @type {Highcharts.SVGElement} - * @private - */ - - this.init(chart, userOptions); + /** + * The chart that the annotation belongs to. + * + * @name chart + * @memberOf Highcharts.Annotation# + * @type {Chart} + */ + this.chart = chart; + + /** + * The array of labels which belong to the annotation. + * + * @name labels + * @memberOf Highcharts.Annotation# + * @type {Array} + */ + this.labels = []; + + /** + * The array of shapes which belong to the annotation. + * + * @name shapes + * @memberOf Highcharts.Annotation# + * @type {Array} + */ + this.shapes = []; + + /** + * The options for the annotations. It containers user defined options + * merged with the default options. + * + * @name options + * @memberOf Highcharts.Annotation# + * @type {AnnotationOptions} + */ + this.options = merge(this.defaultOptions, userOptions); + + /** + * The callback that reports to the overlapping-labels module which + * labels it should account for. + * + * @name labelCollector + * @memberOf Highcharts.Annotation# + * @type {Function} + * @private + */ + + /** + * The group element of the annotation. + * + * @name group + * @memberOf Highcharts.Annotation# + * @type {Highcharts.SVGElement} + * @private + */ + + /** + * The group element of the annotation's shapes. + * + * @name shapesGroup + * @memberOf Highcharts.Annotation# + * @type {Highcharts.SVGElement} + * @private + */ + + /** + * The group element of the annotation's labels. + * + * @name labelsGroup + * @memberOf Highcharts.Annotation# + * @type {Highcharts.SVGElement} + * @private + */ + + this.init(chart, userOptions); }; Annotation.prototype = /** @lends Highcharts.Annotation# */ { - /** - * Shapes which do not have background - the object is used for proper - * setting of the contrast color - * - * @type {Array.} - * @private - */ - shapesWithoutBackground: ['connector'], - - /** - * A map object which allows to map options attributes to element - * attributes. - * - * @type {Object} - * @private - */ - attrsMap: { - /*= if (build.classic) { =*/ - backgroundColor: 'fill', - borderColor: 'stroke', - borderWidth: 'stroke-width', - dashStyle: 'dashstyle', - strokeWidth: 'stroke-width', - stroke: 'stroke', - fill: 'fill', - - /*= } =*/ - zIndex: 'zIndex', - width: 'width', - height: 'height', - borderRadius: 'r', - r: 'r', - padding: 'padding' - }, - - /** - * Options for configuring annotations, for example labels, arrows or - * shapes. Annotations can be tied to points, axis coordinates or chart - * pixel coordinates. - * - * @private - * @type {Array} - * @sample highcharts/annotations/basic/ - * Basic annotations - * @sample highcharts/demo/annotations/ - * Advanced annotations - * @sample highcharts/css/annotations - * Styled mode - * @sample {highstock} stock/annotations/fibonacci-retracements - * Custom annotation, Fibonacci retracement - * @since 6.0.0 - * @optionparent annotations - */ - defaultOptions: { - - /** - * Whether the annotation is visible. - * - * @sample highcharts/annotations/visible/ - * Set annotation visibility - */ - visible: true, - - /** - * Options for annotation's labels. Each label inherits options - * from the labelOptions object. An option from the labelOptions can be - * overwritten by config for a specific label. - */ - labelOptions: { - - /** - * The alignment of the annotation's label. If right, - * the right side of the label should be touching the point. - * - * @validvalue ["left", "center", "right"] - * @sample highcharts/annotations/label-position/ - * Set labels position - */ - align: 'center', - - /** - * Whether to allow the annotation's labels to overlap. - * To make the labels less sensitive for overlapping, - * the can be set to 0. - * - * @sample highcharts/annotations/tooltip-like/ - * Hide overlapping labels - */ - allowOverlap: false, - - /** - * The background color or gradient for the annotation's label. - * - * @type {Color} - * @sample highcharts/annotations/label-presentation/ - * Set labels graphic options - */ - backgroundColor: 'rgba(0, 0, 0, 0.75)', - - /** - * The border color for the annotation's label. - * - * @type {Color} - * @sample highcharts/annotations/label-presentation/ - * Set labels graphic options - */ - borderColor: 'black', - - /** - * The border radius in pixels for the annotaiton's label. - * - * @sample highcharts/annotations/label-presentation/ - * Set labels graphic options - */ - borderRadius: 3, - - /** - * The border width in pixels for the annotation's label - * - * @sample highcharts/annotations/label-presentation/ - * Set labels graphic options - */ - borderWidth: 1, - - /** - * A class name for styling by CSS. - * - * @sample highcharts/css/annotations - * Styled mode annotations - * @since 6.0.5 - */ - className: '', - - /** - * Whether to hide the annotation's label that is outside the plot - * area. - * - * @sample highcharts/annotations/label-crop-overflow/ - * Crop or justify labels - */ - crop: false, - - /** - * The label's pixel distance from the point. - * - * @type {Number} - * @sample highcharts/annotations/label-position/ - * Set labels position - * @default undefined - * @apioption annotations.labelOptions.distance - */ - - /** - * A [format](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting) string for the data label. - * - * @type {String} - * @see [plotOptions.series.dataLabels.format]( - * plotOptions.series.dataLabels.format.html) - * @sample highcharts/annotations/label-text/ - * Set labels text - * @default undefined - * @apioption annotations.labelOptions.format - */ - - /** - * Alias for the format option. - * - * @type {String} - * @see [format](annotations.labelOptions.format.html) - * @sample highcharts/annotations/label-text/ - * Set labels text - * @default undefined - * @apioption annotations.labelOptions.text - */ - - /** - * Callback JavaScript function to format the annotation's label. - * Note that if a `format` or `text` are defined, the format or text - * take precedence and the formatter is ignored. `This` refers to a - * point object. - * - * @type {Function} - * @sample highcharts/annotations/label-text/ - * Set labels text - * @default function () { - * return defined(this.y) ? this.y : 'Annotation label'; - * } - */ - formatter: function () { - return defined(this.y) ? this.y : 'Annotation label'; - }, - - /** - * How to handle the annotation's label that flow outside the plot - * area. The justify option aligns the label inside the plot area. - * - * @validvalue ["none", "justify"] - * @sample highcharts/annotations/label-crop-overflow/ - * Crop or justify labels - **/ - overflow: 'justify', - - /** - * When either the borderWidth or the backgroundColor is set, - * this is the padding within the box. - * - * @sample highcharts/annotations/label-presentation/ - * Set labels graphic options - */ - padding: 5, - - /** - * The shadow of the box. The shadow can be an object configuration - * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. - * - * @type {Boolean|Object} - * @sample highcharts/annotations/label-presentation/ - * Set labels graphic options - */ - shadow: false, - - /** - * The name of a symbol to use for the border around the label. - * Symbols are predefined functions on the Renderer object. - * - * @type {String} - * @sample highcharts/annotations/shapes/ - * Available shapes for labels - */ - shape: 'callout', - - /** - * Styles for the annotation's label. - * - * @type {CSSObject} - * @sample highcharts/annotations/label-presentation/ - * Set labels graphic options - * @see [plotOptions.series.dataLabels.style]( - * plotOptions.series.dataLabels.style.html) - */ - style: { - fontSize: '11px', - fontWeight: 'normal', - color: 'contrast' - }, - - /** - * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting#html) - * to render the annotation's label. - * - * @type {Boolean} - * @default false - */ - useHTML: false, - - /** - * The vertical alignment of the annotation's label. - * - * @type {String} - * @validvalue ["top", "middle", "bottom"] - * @sample highcharts/annotations/label-position/ - * Set labels position - */ - verticalAlign: 'bottom', - - /** - * The x position offset of the label relative to the point. - * Note that if a `distance` is defined, the distance takes - * precedence over `x` and `y` options. - * - * @sample highcharts/annotations/label-position/ - * Set labels position - */ - x: 0, - - /** - * The y position offset of the label relative to the point. - * Note that if a `distance` is defined, the distance takes - * precedence over `x` and `y` options. - * - * @sample highcharts/annotations/label-position/ - * Set labels position - */ - y: -16 - }, - - /** - * An array of labels for the annotation. For options that apply to - * multiple labels, they can be added to the - * [labelOptions](annotations.labelOptions.html). - * - * @type {Array} - * @extends annotations.labelOptions - * @apioption annotations.labels - */ - - /** - * This option defines the point to which the label will be connected. - * It can be either the point which exists in the series - it is - * referenced by the point's id - or a new point with defined x, y - * properies and optionally axes. - * - * @type {String|Object} - * @sample highcharts/annotations/mock-point/ - * Attach annotation to a mock point - * @apioption annotations.labels.point - */ - - /** - * The x position of the point. Units can be either in axis - * or chart pixel coordinates. - * - * @type {Number} - * @apioption annotations.labels.point.x - */ - - /** - * The y position of the point. Units can be either in axis - * or chart pixel coordinates. - * - * @type {Number} - * @apioption annotations.labels.point.y - */ - - /** - * This number defines which xAxis the point is connected to. It refers - * to either the axis id or the index of the axis in the xAxis array. - * If the option is not configured or the axis is not found the point's - * x coordinate refers to the chart pixels. - * - * @type {Number|String} - * @apioption annotations.labels.point.xAxis - */ - - /** - * This number defines which yAxis the point is connected to. It refers - * to either the axis id or the index of the axis in the yAxis array. - * If the option is not configured or the axis is not found the point's - * y coordinate refers to the chart pixels. - * - * @type {Number|String} - * @apioption annotations.labels.point.yAxis - */ - - - - /** - * An array of shapes for the annotation. For options that apply to - * multiple shapes, then can be added to the - * [shapeOptions](annotations.shapeOptions.html). - * - * @type {Array} - * @extends annotations.shapeOptions - * @apioption annotations.shapes - */ - - /** - * This option defines the point to which the shape will be connected. - * It can be either the point which exists in the series - it is - * referenced by the point's id - or a new point with defined x, y - * properties and optionally axes. - * - * @type {String|Object} - * @extends annotations.labels.point - * @apioption annotations.shapes.point - */ - - /** - * An array of points for the shape. This option is available for shapes - * which can use multiple points such as path. A point can be either - * a point object or a point's id. - * - * @type {Array} - * @see [annotations.shapes.point](annotations.shapes.point.html) - * @apioption annotations.shapes.points - */ - - /** - * Id of the marker which will be drawn at the final vertex of the path. - * Custom markers can be defined in defs property. - * - * @type {String} - * @see [defs.markers](defs.markers.html) - * @sample highcharts/annotations/custom-markers/ - * Define a custom marker for annotations - * @apioption annotations.shapes.markerEnd - */ - - /** - * Id of the marker which will be drawn at the first vertex of the path. - * Custom markers can be defined in defs property. - * - * @type {String} - * @see [defs.markers](defs.markers.html) - * @sample {highcharts} highcharts/annotations/custom-markers/ - * Define a custom marker for annotations - * @apioption annotations.shapes.markerStart - */ - - - /** - * Options for annotation's shapes. Each shape inherits options - * from the shapeOptions object. An option from the shapeOptions can be - * overwritten by config for a specific shape. - * - * @type {Object} - */ - shapeOptions: { - - /** - * The width of the shape. - * - * @type {Number} - * @sample highcharts/annotations/shape/ - * Basic shape annotation - * @apioption annotations.shapeOptions.width - **/ - - /** - * The height of the shape. - * - * @type {Number} - * @sample highcharts/annotations/shape/ - * Basic shape annotation - * @apioption annotations.shapeOptions.height - */ - - /** - * The color of the shape's stroke. - * - * @type {Color} - * @sample highcharts/annotations/shape/ - * Basic shape annotation - */ - stroke: 'rgba(0, 0, 0, 0.75)', - - /** - * The pixel stroke width of the shape. - * - * @sample highcharts/annotations/shape/ - * Basic shape annotation - */ - strokeWidth: 1, - - /** - * The color of the shape's fill. - * - * @type {Color} - * @sample highcharts/annotations/shape/ - * Basic shape annotation - */ - fill: 'rgba(0, 0, 0, 0.75)', - - /** - * The type of the shape, e.g. circle or rectangle. - * - * @type {String} - * @sample highcharts/annotations/shape/ - * Basic shape annotation - * @default 'rect' - * @apioption annotations.shapeOptions.type - */ - - /** - * The radius of the shape. - * - * @sample highcharts/annotations/shape/ - * Basic shape annotation - */ - r: 0 - }, - - /** - * The Z index of the annotation. - * - * @type {Number} - * @default 6 - */ - zIndex: 6 - }, - - /** - * Initialize the annotation. - * - * @param {Chart} - the chart - * @param {AnnotationOptions} - the user options for the annotation - */ - init: function () { - var anno = this; - each(this.options.labels || [], this.initLabel, this); - each(this.options.shapes || [], this.initShape, this); - - this.labelCollector = function () { - return grep(anno.labels, function (label) { - return !label.options.allowOverlap; - }); - }; - - this.chart.labelCollectors.push(this.labelCollector); - }, - - /** - * Main method for drawing an annotation. - **/ - redraw: function () { - if (!this.group) { - this.render(); - } - - this.redrawItems(this.shapes); - this.redrawItems(this.labels); - }, - - /** - * @private - * @param {Array} items - **/ - redrawItems: function (items) { - var i = items.length; - - // needs a backward loop - // labels/shapes array might be modified due to destruction of the item - while (i--) { - this.redrawItem(items[i]); - } - }, - - /** - * Render the annotation. - **/ - render: function () { - var renderer = this.chart.renderer; - - var group = this.group = renderer.g('annotation') - .attr({ - zIndex: this.options.zIndex, - visibility: this.options.visible ? 'visible' : 'hidden' - }) - .add(); - - this.shapesGroup = renderer.g('annotation-shapes').add(group); - - this.labelsGroup = renderer.g('annotation-labels').attr({ + /** + * Shapes which do not have background - the object is used for proper + * setting of the contrast color + * + * @type {Array.} + * @private + */ + shapesWithoutBackground: ['connector'], + + /** + * A map object which allows to map options attributes to element + * attributes. + * + * @type {Object} + * @private + */ + attrsMap: { + /*= if (build.classic) { =*/ + backgroundColor: 'fill', + borderColor: 'stroke', + borderWidth: 'stroke-width', + dashStyle: 'dashstyle', + strokeWidth: 'stroke-width', + stroke: 'stroke', + fill: 'fill', + + /*= } =*/ + zIndex: 'zIndex', + width: 'width', + height: 'height', + borderRadius: 'r', + r: 'r', + padding: 'padding' + }, + + /** + * Options for configuring annotations, for example labels, arrows or + * shapes. Annotations can be tied to points, axis coordinates or chart + * pixel coordinates. + * + * @private + * @type {Array} + * @sample highcharts/annotations/basic/ + * Basic annotations + * @sample highcharts/demo/annotations/ + * Advanced annotations + * @sample highcharts/css/annotations + * Styled mode + * @sample {highstock} stock/annotations/fibonacci-retracements + * Custom annotation, Fibonacci retracement + * @since 6.0.0 + * @optionparent annotations + */ + defaultOptions: { + + /** + * Whether the annotation is visible. + * + * @sample highcharts/annotations/visible/ + * Set annotation visibility + */ + visible: true, + + /** + * Options for annotation's labels. Each label inherits options + * from the labelOptions object. An option from the labelOptions can be + * overwritten by config for a specific label. + */ + labelOptions: { + + /** + * The alignment of the annotation's label. If right, + * the right side of the label should be touching the point. + * + * @validvalue ["left", "center", "right"] + * @sample highcharts/annotations/label-position/ + * Set labels position + */ + align: 'center', + + /** + * Whether to allow the annotation's labels to overlap. + * To make the labels less sensitive for overlapping, + * the can be set to 0. + * + * @sample highcharts/annotations/tooltip-like/ + * Hide overlapping labels + */ + allowOverlap: false, + + /** + * The background color or gradient for the annotation's label. + * + * @type {Color} + * @sample highcharts/annotations/label-presentation/ + * Set labels graphic options + */ + backgroundColor: 'rgba(0, 0, 0, 0.75)', + + /** + * The border color for the annotation's label. + * + * @type {Color} + * @sample highcharts/annotations/label-presentation/ + * Set labels graphic options + */ + borderColor: 'black', + + /** + * The border radius in pixels for the annotaiton's label. + * + * @sample highcharts/annotations/label-presentation/ + * Set labels graphic options + */ + borderRadius: 3, + + /** + * The border width in pixels for the annotation's label + * + * @sample highcharts/annotations/label-presentation/ + * Set labels graphic options + */ + borderWidth: 1, + + /** + * A class name for styling by CSS. + * + * @sample highcharts/css/annotations + * Styled mode annotations + * @since 6.0.5 + */ + className: '', + + /** + * Whether to hide the annotation's label that is outside the plot + * area. + * + * @sample highcharts/annotations/label-crop-overflow/ + * Crop or justify labels + */ + crop: false, + + /** + * The label's pixel distance from the point. + * + * @type {Number} + * @sample highcharts/annotations/label-position/ + * Set labels position + * @default undefined + * @apioption annotations.labelOptions.distance + */ + + /** + * A [format](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting) string for the data label. + * + * @type {String} + * @see [plotOptions.series.dataLabels.format]( + * plotOptions.series.dataLabels.format.html) + * @sample highcharts/annotations/label-text/ + * Set labels text + * @default undefined + * @apioption annotations.labelOptions.format + */ + + /** + * Alias for the format option. + * + * @type {String} + * @see [format](annotations.labelOptions.format.html) + * @sample highcharts/annotations/label-text/ + * Set labels text + * @default undefined + * @apioption annotations.labelOptions.text + */ + + /** + * Callback JavaScript function to format the annotation's label. + * Note that if a `format` or `text` are defined, the format or text + * take precedence and the formatter is ignored. `This` refers to a + * point object. + * + * @type {Function} + * @sample highcharts/annotations/label-text/ + * Set labels text + * @default function () { + * return defined(this.y) ? this.y : 'Annotation label'; + * } + */ + formatter: function () { + return defined(this.y) ? this.y : 'Annotation label'; + }, + + /** + * How to handle the annotation's label that flow outside the plot + * area. The justify option aligns the label inside the plot area. + * + * @validvalue ["none", "justify"] + * @sample highcharts/annotations/label-crop-overflow/ + * Crop or justify labels + **/ + overflow: 'justify', + + /** + * When either the borderWidth or the backgroundColor is set, + * this is the padding within the box. + * + * @sample highcharts/annotations/label-presentation/ + * Set labels graphic options + */ + padding: 5, + + /** + * The shadow of the box. The shadow can be an object configuration + * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample highcharts/annotations/label-presentation/ + * Set labels graphic options + */ + shadow: false, + + /** + * The name of a symbol to use for the border around the label. + * Symbols are predefined functions on the Renderer object. + * + * @type {String} + * @sample highcharts/annotations/shapes/ + * Available shapes for labels + */ + shape: 'callout', + + /** + * Styles for the annotation's label. + * + * @type {CSSObject} + * @sample highcharts/annotations/label-presentation/ + * Set labels graphic options + * @see [plotOptions.series.dataLabels.style]( + * plotOptions.series.dataLabels.style.html) + */ + style: { + fontSize: '11px', + fontWeight: 'normal', + color: 'contrast' + }, + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting#html) + * to render the annotation's label. + * + * @type {Boolean} + * @default false + */ + useHTML: false, + + /** + * The vertical alignment of the annotation's label. + * + * @type {String} + * @validvalue ["top", "middle", "bottom"] + * @sample highcharts/annotations/label-position/ + * Set labels position + */ + verticalAlign: 'bottom', + + /** + * The x position offset of the label relative to the point. + * Note that if a `distance` is defined, the distance takes + * precedence over `x` and `y` options. + * + * @sample highcharts/annotations/label-position/ + * Set labels position + */ + x: 0, + + /** + * The y position offset of the label relative to the point. + * Note that if a `distance` is defined, the distance takes + * precedence over `x` and `y` options. + * + * @sample highcharts/annotations/label-position/ + * Set labels position + */ + y: -16 + }, + + /** + * An array of labels for the annotation. For options that apply to + * multiple labels, they can be added to the + * [labelOptions](annotations.labelOptions.html). + * + * @type {Array} + * @extends annotations.labelOptions + * @apioption annotations.labels + */ + + /** + * This option defines the point to which the label will be connected. + * It can be either the point which exists in the series - it is + * referenced by the point's id - or a new point with defined x, y + * properies and optionally axes. + * + * @type {String|Object} + * @sample highcharts/annotations/mock-point/ + * Attach annotation to a mock point + * @apioption annotations.labels.point + */ + + /** + * The x position of the point. Units can be either in axis + * or chart pixel coordinates. + * + * @type {Number} + * @apioption annotations.labels.point.x + */ + + /** + * The y position of the point. Units can be either in axis + * or chart pixel coordinates. + * + * @type {Number} + * @apioption annotations.labels.point.y + */ + + /** + * This number defines which xAxis the point is connected to. It refers + * to either the axis id or the index of the axis in the xAxis array. + * If the option is not configured or the axis is not found the point's + * x coordinate refers to the chart pixels. + * + * @type {Number|String} + * @apioption annotations.labels.point.xAxis + */ + + /** + * This number defines which yAxis the point is connected to. It refers + * to either the axis id or the index of the axis in the yAxis array. + * If the option is not configured or the axis is not found the point's + * y coordinate refers to the chart pixels. + * + * @type {Number|String} + * @apioption annotations.labels.point.yAxis + */ + + + + /** + * An array of shapes for the annotation. For options that apply to + * multiple shapes, then can be added to the + * [shapeOptions](annotations.shapeOptions.html). + * + * @type {Array} + * @extends annotations.shapeOptions + * @apioption annotations.shapes + */ + + /** + * This option defines the point to which the shape will be connected. + * It can be either the point which exists in the series - it is + * referenced by the point's id - or a new point with defined x, y + * properties and optionally axes. + * + * @type {String|Object} + * @extends annotations.labels.point + * @apioption annotations.shapes.point + */ + + /** + * An array of points for the shape. This option is available for shapes + * which can use multiple points such as path. A point can be either + * a point object or a point's id. + * + * @type {Array} + * @see [annotations.shapes.point](annotations.shapes.point.html) + * @apioption annotations.shapes.points + */ + + /** + * Id of the marker which will be drawn at the final vertex of the path. + * Custom markers can be defined in defs property. + * + * @type {String} + * @see [defs.markers](defs.markers.html) + * @sample highcharts/annotations/custom-markers/ + * Define a custom marker for annotations + * @apioption annotations.shapes.markerEnd + */ + + /** + * Id of the marker which will be drawn at the first vertex of the path. + * Custom markers can be defined in defs property. + * + * @type {String} + * @see [defs.markers](defs.markers.html) + * @sample {highcharts} highcharts/annotations/custom-markers/ + * Define a custom marker for annotations + * @apioption annotations.shapes.markerStart + */ + + + /** + * Options for annotation's shapes. Each shape inherits options + * from the shapeOptions object. An option from the shapeOptions can be + * overwritten by config for a specific shape. + * + * @type {Object} + */ + shapeOptions: { + + /** + * The width of the shape. + * + * @type {Number} + * @sample highcharts/annotations/shape/ + * Basic shape annotation + * @apioption annotations.shapeOptions.width + **/ + + /** + * The height of the shape. + * + * @type {Number} + * @sample highcharts/annotations/shape/ + * Basic shape annotation + * @apioption annotations.shapeOptions.height + */ + + /** + * The color of the shape's stroke. + * + * @type {Color} + * @sample highcharts/annotations/shape/ + * Basic shape annotation + */ + stroke: 'rgba(0, 0, 0, 0.75)', + + /** + * The pixel stroke width of the shape. + * + * @sample highcharts/annotations/shape/ + * Basic shape annotation + */ + strokeWidth: 1, + + /** + * The color of the shape's fill. + * + * @type {Color} + * @sample highcharts/annotations/shape/ + * Basic shape annotation + */ + fill: 'rgba(0, 0, 0, 0.75)', + + /** + * The type of the shape, e.g. circle or rectangle. + * + * @type {String} + * @sample highcharts/annotations/shape/ + * Basic shape annotation + * @default 'rect' + * @apioption annotations.shapeOptions.type + */ + + /** + * The radius of the shape. + * + * @sample highcharts/annotations/shape/ + * Basic shape annotation + */ + r: 0 + }, + + /** + * The Z index of the annotation. + * + * @type {Number} + * @default 6 + */ + zIndex: 6 + }, + + /** + * Initialize the annotation. + * + * @param {Chart} - the chart + * @param {AnnotationOptions} - the user options for the annotation + */ + init: function () { + var anno = this; + each(this.options.labels || [], this.initLabel, this); + each(this.options.shapes || [], this.initShape, this); + + this.labelCollector = function () { + return grep(anno.labels, function (label) { + return !label.options.allowOverlap; + }); + }; + + this.chart.labelCollectors.push(this.labelCollector); + }, + + /** + * Main method for drawing an annotation. + **/ + redraw: function () { + if (!this.group) { + this.render(); + } + + this.redrawItems(this.shapes); + this.redrawItems(this.labels); + }, + + /** + * @private + * @param {Array} items + **/ + redrawItems: function (items) { + var i = items.length; + + // needs a backward loop + // labels/shapes array might be modified due to destruction of the item + while (i--) { + this.redrawItem(items[i]); + } + }, + + /** + * Render the annotation. + **/ + render: function () { + var renderer = this.chart.renderer; + + var group = this.group = renderer.g('annotation') + .attr({ + zIndex: this.options.zIndex, + visibility: this.options.visible ? 'visible' : 'hidden' + }) + .add(); + + this.shapesGroup = renderer.g('annotation-shapes').add(group); + + this.labelsGroup = renderer.g('annotation-labels').attr({ // hideOverlappingLabels requires translation - translateX: 0, - translateY: 0 - }).add(group); - - this.shapesGroup.clip(this.chart.plotBoxClip); - }, - - /** - * Set the annotation's visibility. - * - * @param {Boolean} [visibility] - Whether to show or hide an annotation. - * If the param is omitted, the annotation's visibility is toggled. - **/ - setVisible: function (visibility) { - var options = this.options, - visible = pick(visibility, !options.visible); - - this.group.attr({ - visibility: visible ? 'visible' : 'hidden' - }); - - options.visible = visible; - }, - - - /** - * Destroy the annotation. This function does not touch the chart - * that the annotation belongs to (all annotations are kept in - * the chart.annotations array) - it is recommended to use - * {@link Highcharts.Chart#removeAnnotation} instead. - **/ - destroy: function () { - var chart = this.chart; - - erase(this.chart.labelCollectors, this.labelCollector); - - each(this.labels, function (label) { - label.destroy(); - }); - - each(this.shapes, function (shape) { - shape.destroy(); - }); - - destroyObjectProperties(this, chart); - }, - - - /* *********************************************************************** - * ITEM SECTION - * Contains methods for handling a single item in an annotation - *********************************************************************** */ - - /** - * Initialisation of a single shape - * - * @private - * @param {Object} shapeOptions - a confg object for a single shape - **/ - initShape: function (shapeOptions) { - var renderer = this.chart.renderer, - options = merge(this.options.shapeOptions, shapeOptions), - attr = this.attrsFromOptions(options), - - type = renderer[options.type] ? options.type : 'rect', - shape = renderer[type](0, -9e9, 0, 0); - - shape.points = []; - shape.type = type; - shape.options = options; - shape.itemType = 'shape'; - - if (type === 'path') { - extend(shape, { - markerStartSetter: MarkerMixin.markerStartSetter, - markerEndSetter: MarkerMixin.markerEndSetter, - markerStart: MarkerMixin.markerStart, - markerEnd: MarkerMixin.markerEnd - }); - } - - shape.attr(attr); - - - if (options.className) { - shape.addClass(options.className); - } - - this.shapes.push(shape); - }, - - /** - * Initialisation of a single label - * - * @private - * @param {Object} labelOptions - **/ - initLabel: function (labelOptions) { - var options = merge(this.options.labelOptions, labelOptions), - attr = this.attrsFromOptions(options), - - label = this.chart.renderer.label( - '', - 0, -9e9, - options.shape, - null, - null, - options.useHTML, - null, - 'annotation-label' - ); - - label.points = []; - label.options = options; - label.itemType = 'label'; - - // Labelrank required for hideOverlappingLabels() - label.labelrank = options.labelrank; - label.annotation = this; - - label.attr(attr); - - /*= if (build.classic) { =*/ - var style = options.style; - if (style.color === 'contrast') { - style.color = this.chart.renderer.getContrast( - inArray(options.shape, this.shapesWithoutBackground) > -1 ? - '#FFFFFF' : - options.backgroundColor - ); - } - label.css(style).shadow(options.shadow); - /*= } =*/ - - if (options.className) { - label.addClass(options.className); - } - - - this.labels.push(label); - }, - - /** - * Redrawing a single item - * - * @private - * @param {SVGElement} item - */ - redrawItem: function (item) { - var points = this.linkPoints(item), - itemOptions = item.options, - text, - time = this.chart.time; - - if (!points.length) { - this.destroyItem(item); - - } else { - if (!item.parentGroup) { - this.renderItem(item); - } - - if (item.itemType === 'label') { - text = itemOptions.format || itemOptions.text; - item.attr({ - text: text ? - format(text, points[0].getLabelConfig(), time) : - itemOptions.formatter.call(points[0]) - }); - } - - - if (item.type === 'path') { - this.redrawPath(item); - - } else { - this.alignItem(item, !item.placed); - } - } - }, - - /** - * Destroing a single item - * - * @private - * @param {SVGElement} item - */ - destroyItem: function (item) { - // erase from shapes or labels array - erase(this[item.itemType + 's'], item); - item.destroy(); - }, - - /** - * Returns a point object - * - * @private - * @param {Object} pointOptions - * @param {Highcharts.MockPoint|Highcharts.Point} point - * @return {Highcharts.MockPoint|Highcharts.Point|null} if the point is - * found/exists returns this point, otherwise null - */ - pointItem: function (pointOptions, point) { - if (!point || point.series === null) { - if (isObject(pointOptions)) { - point = mockPoint(this.chart, pointOptions); - - } else if (isString(pointOptions)) { - point = this.chart.get(pointOptions) || null; - } - } - - return point; - }, - - /** - * Linking item with the point or points and returning an array of linked - * points. - * - * @private - * @param {SVGElement} item - * @return { - * Highcharts.Point| - * Highcharts.MockPoint| - * Array - * } - */ - linkPoints: function (item) { - var pointsOptions = ( - item.options.points || - (item.options.point && H.splat(item.options.point)) - ), - points = item.points, - len = pointsOptions && pointsOptions.length, - i, - point; - - for (i = 0; i < len; i++) { - point = this.pointItem(pointsOptions[i], points[i]); - - if (!point) { - return (item.points = []); - } - - points[i] = point; - } - - return points; - }, - - /** - * Aligning the item and setting its anchor - * - * @private - * @param {SVGElement} item - * @param {Boolean} isNew - * If the label is re-positioned (is not new) it is animated - * @return {undefined} - */ - alignItem: function (item, isNew) { - var anchor = this.itemAnchor(item, item.points[0]), - attrs = this.itemPosition(item, anchor); - - if (attrs) { - item.alignAttr = attrs; - item.placed = true; - - attrs.anchorX = anchor.absolutePosition.x; - attrs.anchorY = anchor.absolutePosition.y; - - item[isNew ? 'attr' : 'animate'](attrs); - - } else { - item.placed = false; - - item.attr({ - x: 0, - y: -9e9 - }); - } - }, - - /** - * @private - */ - redrawPath: function (pathItem, isNew) { - var points = pathItem.points, - strokeWidth = pathItem['stroke-width'] || 1, - d = ['M'], - pointIndex = 0, - dIndex = 0, - len = points && points.length, - crispSegmentIndex, - anchor, - point, - showPath; - - if (len) { - do { - point = points[pointIndex]; - - anchor = this.itemAnchor(pathItem, point).absolutePosition; - d[++dIndex] = anchor.x; - d[++dIndex] = anchor.y; - - // Crisping line, it might be replaced with - // Renderer.prototype.crispLine but it requires creating many - // temporary arrays - crispSegmentIndex = dIndex % 5; - if (crispSegmentIndex === 0) { - if (d[crispSegmentIndex + 1] === d[crispSegmentIndex + 4]) { - d[crispSegmentIndex + 1] = d[crispSegmentIndex + 4] = - Math.round(d[crispSegmentIndex + 1]) - - (strokeWidth % 2 / 2); - } - - if (d[crispSegmentIndex + 2] === d[crispSegmentIndex + 5]) { - d[crispSegmentIndex + 2] = d[crispSegmentIndex + 5] = - Math.round(d[crispSegmentIndex + 2]) + - (strokeWidth % 2 / 2); - } - } - - if (pointIndex < len - 1) { - d[++dIndex] = 'L'; - } - - showPath = point.series.visible; - - } while (++pointIndex < len && showPath); - } - - - if (showPath) { - pathItem[isNew ? 'attr' : 'animate']({ - d: d - }); - - } else { - pathItem.attr({ - d: 'M 0 ' + -9e9 - }); - } - - pathItem.placed = showPath; - }, - - /* - * @private - */ - renderItem: function (item) { - item.add( - item.itemType === 'label' ? - this.labelsGroup : - this.shapesGroup - ); - - this.setItemMarkers(item); - }, - - /* - * @private - */ - setItemMarkers: function (item) { - var itemOptions = item.options, - chart = this.chart, - defs = chart.options.defs, - fill = itemOptions.fill, - color = defined(fill) && fill !== 'none' ? - fill : - itemOptions.stroke, - - - setMarker = function (markerType) { - var markerId = itemOptions[markerType], - def, - predefinedMarker, - key, - marker; - - if (markerId) { - for (key in defs) { - def = defs[key]; - if (markerId === def.id && def.tagName === 'marker') { - predefinedMarker = def; - break; - } - } - - if (predefinedMarker) { - marker = item[markerType] = chart.renderer.addMarker( - (itemOptions.id || uniqueKey()) + '-' + - predefinedMarker.id, - merge(predefinedMarker, { color: color }) - ); - - item.attr(markerType, marker.attr('id')); - } - } - }; - - each(['markerStart', 'markerEnd'], setMarker); - }, - - /** - * An object which denotes an anchor position - * - * @typedef {Object} AnchorPosition - * @property {Number} AnchorPosition.x - * @property {Number} AnchorPosition.y - * @property {Number} AnchorPosition.height - * @property {Number} AnchorPosition.width - */ - - /** - * Returns object which denotes anchor position - relative and absolute - * - * @private - * @param {SVGElement} item - * @param {Highcharts.Point|Highcharts.MockPoint} point - * @return {Object} anchor - * @return {AnchorPosition} anchor.relativePosition - * Relative to the plot area position - * @return {AnchorPosition} anchor.absolutePosition - * Absolute position - */ - itemAnchor: function (item, point) { - var plotBox = point.series.getPlotBox(), - - box = point.mock ? - point.alignToBox(true) : - tooltipPrototype.getAnchor.call({ - chart: this.chart - }, point), - - anchor = { - x: box[0], - y: box[1], - height: box[2] || 0, - width: box[3] || 0 - }; - - return { - relativePosition: anchor, - absolutePosition: merge(anchor, { - x: anchor.x + plotBox.translateX, - y: anchor.y + plotBox.translateY - }) - }; - }, - - /** - * Returns the item position - * - * @private - * @param {SVGElement} item - * @param {AnchorPosition} anchor - * @return {Object|null} position - * @return {Number} position.x - * @return {Number} position.y - */ - itemPosition: function (item, anchor) { - var chart = this.chart, - point = item.points[0], - itemOptions = item.options, - anchorAbsolutePosition = anchor.absolutePosition, - anchorRelativePosition = anchor.relativePosition, - itemPosition, - alignTo, - itemPosRelativeX, - itemPosRelativeY, - - showItem = - point.series.visible && - MockPoint.prototype.isInsidePane.call(point); - - if (showItem) { - - if (defined(itemOptions.distance) || itemOptions.positioner) { - itemPosition = ( - itemOptions.positioner || - tooltipPrototype.getPosition - ).call( - { - chart: chart, - distance: pick(itemOptions.distance, 16) - }, - item.width, - item.height, - { - plotX: anchorRelativePosition.x, - plotY: anchorRelativePosition.y, - negative: point.negative, - ttBelow: point.ttBelow, - h: anchorRelativePosition.height || - anchorRelativePosition.width - } - ); - - } else { - alignTo = { - x: anchorAbsolutePosition.x, - y: anchorAbsolutePosition.y, - width: 0, - height: 0 - }; - - itemPosition = this.alignedPosition( - extend(itemOptions, { - width: item.width, - height: item.height - }), - alignTo - ); - - if (item.options.overflow === 'justify') { - itemPosition = this.alignedPosition( - this.justifiedOptions(item, itemOptions, itemPosition), - alignTo - ); - } - } - - - if (itemOptions.crop) { - itemPosRelativeX = itemPosition.x - chart.plotLeft; - itemPosRelativeY = itemPosition.y - chart.plotTop; - - showItem = - chart.isInsidePlot(itemPosRelativeX, itemPosRelativeY) && - chart.isInsidePlot( - itemPosRelativeX + item.width, - itemPosRelativeY + item.height - ); - } - } - - return showItem ? itemPosition : null; - }, - - /** - * Returns new aligned position based alignment options and box to align to. - * It is almost a one-to-one copy from SVGElement.prototype.align - * except it does not use and mutate an element - * - * @private - * @param {Object} alignOptions - * @param {Object} box - * @return {Object} aligned position - **/ - alignedPosition: function (alignOptions, box) { - var align = alignOptions.align, - vAlign = alignOptions.verticalAlign, - x = (box.x || 0) + (alignOptions.x || 0), - y = (box.y || 0) + (alignOptions.y || 0), - - alignFactor, - vAlignFactor; - - if (align === 'right') { - alignFactor = 1; - } else if (align === 'center') { - alignFactor = 2; - } - if (alignFactor) { - x += (box.width - (alignOptions.width || 0)) / alignFactor; - } - - if (vAlign === 'bottom') { - vAlignFactor = 1; - } else if (vAlign === 'middle') { - vAlignFactor = 2; - } - if (vAlignFactor) { - y += (box.height - (alignOptions.height || 0)) / vAlignFactor; - } - - return { - x: Math.round(x), - y: Math.round(y) - }; - }, - - /** - * Returns new alignment options for a label if the label is outside the - * plot area. It is almost a one-to-one copy from - * Series.prototype.justifyDataLabel except it does not mutate the label and - * it works with absolute instead of relative position. - * - * @private - * @param {Object} label - * @param {Object} alignOptions - * @param {Object} alignAttr - * @return {Object} justified options - **/ - justifiedOptions: function (label, alignOptions, alignAttr) { - var chart = this.chart, - align = alignOptions.align, - verticalAlign = alignOptions.verticalAlign, - padding = label.box ? 0 : (label.padding || 0), - bBox = label.getBBox(), - off, - - options = { - align: align, - verticalAlign: verticalAlign, - x: alignOptions.x, - y: alignOptions.y, - width: label.width, - height: label.height - }, - - x = alignAttr.x - chart.plotLeft, - y = alignAttr.y - chart.plotTop; - - // Off left - off = x + padding; - if (off < 0) { - if (align === 'right') { - options.align = 'left'; - } else { - options.x = -off; - } - } - - // Off right - off = x + bBox.width - padding; - if (off > chart.plotWidth) { - if (align === 'left') { - options.align = 'right'; - } else { - options.x = chart.plotWidth - off; - } - } - - // Off top - off = y + padding; - if (off < 0) { - if (verticalAlign === 'bottom') { - options.verticalAlign = 'top'; - } else { - options.y = -off; - } - } - - // Off bottom - off = y + bBox.height - padding; - if (off > chart.plotHeight) { - if (verticalAlign === 'top') { - options.verticalAlign = 'bottom'; - } else { - options.y = chart.plotHeight - off; - } - } - - return options; - }, - - - /** - * Utility function for mapping item's options to element's attribute - * - * @private - * @param {Object} options - * @return {Object} mapped options - **/ - attrsFromOptions: function (options) { - var map = this.attrsMap, - attrs = {}, - key, - mappedKey; - - for (key in options) { - mappedKey = map[key]; - if (mappedKey) { - attrs[mappedKey] = options[key]; - } - } - - return attrs; - } + translateX: 0, + translateY: 0 + }).add(group); + + this.shapesGroup.clip(this.chart.plotBoxClip); + }, + + /** + * Set the annotation's visibility. + * + * @param {Boolean} [visibility] - Whether to show or hide an annotation. + * If the param is omitted, the annotation's visibility is toggled. + **/ + setVisible: function (visibility) { + var options = this.options, + visible = pick(visibility, !options.visible); + + this.group.attr({ + visibility: visible ? 'visible' : 'hidden' + }); + + options.visible = visible; + }, + + + /** + * Destroy the annotation. This function does not touch the chart + * that the annotation belongs to (all annotations are kept in + * the chart.annotations array) - it is recommended to use + * {@link Highcharts.Chart#removeAnnotation} instead. + **/ + destroy: function () { + var chart = this.chart; + + erase(this.chart.labelCollectors, this.labelCollector); + + each(this.labels, function (label) { + label.destroy(); + }); + + each(this.shapes, function (shape) { + shape.destroy(); + }); + + destroyObjectProperties(this, chart); + }, + + + /* *********************************************************************** + * ITEM SECTION + * Contains methods for handling a single item in an annotation + *********************************************************************** */ + + /** + * Initialisation of a single shape + * + * @private + * @param {Object} shapeOptions - a confg object for a single shape + **/ + initShape: function (shapeOptions) { + var renderer = this.chart.renderer, + options = merge(this.options.shapeOptions, shapeOptions), + attr = this.attrsFromOptions(options), + + type = renderer[options.type] ? options.type : 'rect', + shape = renderer[type](0, -9e9, 0, 0); + + shape.points = []; + shape.type = type; + shape.options = options; + shape.itemType = 'shape'; + + if (type === 'path') { + extend(shape, { + markerStartSetter: MarkerMixin.markerStartSetter, + markerEndSetter: MarkerMixin.markerEndSetter, + markerStart: MarkerMixin.markerStart, + markerEnd: MarkerMixin.markerEnd + }); + } + + shape.attr(attr); + + + if (options.className) { + shape.addClass(options.className); + } + + this.shapes.push(shape); + }, + + /** + * Initialisation of a single label + * + * @private + * @param {Object} labelOptions + **/ + initLabel: function (labelOptions) { + var options = merge(this.options.labelOptions, labelOptions), + attr = this.attrsFromOptions(options), + + label = this.chart.renderer.label( + '', + 0, -9e9, + options.shape, + null, + null, + options.useHTML, + null, + 'annotation-label' + ); + + label.points = []; + label.options = options; + label.itemType = 'label'; + + // Labelrank required for hideOverlappingLabels() + label.labelrank = options.labelrank; + label.annotation = this; + + label.attr(attr); + + /*= if (build.classic) { =*/ + var style = options.style; + if (style.color === 'contrast') { + style.color = this.chart.renderer.getContrast( + inArray(options.shape, this.shapesWithoutBackground) > -1 ? + '#FFFFFF' : + options.backgroundColor + ); + } + label.css(style).shadow(options.shadow); + /*= } =*/ + + if (options.className) { + label.addClass(options.className); + } + + + this.labels.push(label); + }, + + /** + * Redrawing a single item + * + * @private + * @param {SVGElement} item + */ + redrawItem: function (item) { + var points = this.linkPoints(item), + itemOptions = item.options, + text, + time = this.chart.time; + + if (!points.length) { + this.destroyItem(item); + + } else { + if (!item.parentGroup) { + this.renderItem(item); + } + + if (item.itemType === 'label') { + text = itemOptions.format || itemOptions.text; + item.attr({ + text: text ? + format(text, points[0].getLabelConfig(), time) : + itemOptions.formatter.call(points[0]) + }); + } + + + if (item.type === 'path') { + this.redrawPath(item); + + } else { + this.alignItem(item, !item.placed); + } + } + }, + + /** + * Destroing a single item + * + * @private + * @param {SVGElement} item + */ + destroyItem: function (item) { + // erase from shapes or labels array + erase(this[item.itemType + 's'], item); + item.destroy(); + }, + + /** + * Returns a point object + * + * @private + * @param {Object} pointOptions + * @param {Highcharts.MockPoint|Highcharts.Point} point + * @return {Highcharts.MockPoint|Highcharts.Point|null} if the point is + * found/exists returns this point, otherwise null + */ + pointItem: function (pointOptions, point) { + if (!point || point.series === null) { + if (isObject(pointOptions)) { + point = mockPoint(this.chart, pointOptions); + + } else if (isString(pointOptions)) { + point = this.chart.get(pointOptions) || null; + } + } + + return point; + }, + + /** + * Linking item with the point or points and returning an array of linked + * points. + * + * @private + * @param {SVGElement} item + * @return { + * Highcharts.Point| + * Highcharts.MockPoint| + * Array + * } + */ + linkPoints: function (item) { + var pointsOptions = ( + item.options.points || + (item.options.point && H.splat(item.options.point)) + ), + points = item.points, + len = pointsOptions && pointsOptions.length, + i, + point; + + for (i = 0; i < len; i++) { + point = this.pointItem(pointsOptions[i], points[i]); + + if (!point) { + return (item.points = []); + } + + points[i] = point; + } + + return points; + }, + + /** + * Aligning the item and setting its anchor + * + * @private + * @param {SVGElement} item + * @param {Boolean} isNew + * If the label is re-positioned (is not new) it is animated + * @return {undefined} + */ + alignItem: function (item, isNew) { + var anchor = this.itemAnchor(item, item.points[0]), + attrs = this.itemPosition(item, anchor); + + if (attrs) { + item.alignAttr = attrs; + item.placed = true; + + attrs.anchorX = anchor.absolutePosition.x; + attrs.anchorY = anchor.absolutePosition.y; + + item[isNew ? 'attr' : 'animate'](attrs); + + } else { + item.placed = false; + + item.attr({ + x: 0, + y: -9e9 + }); + } + }, + + /** + * @private + */ + redrawPath: function (pathItem, isNew) { + var points = pathItem.points, + strokeWidth = pathItem['stroke-width'] || 1, + d = ['M'], + pointIndex = 0, + dIndex = 0, + len = points && points.length, + crispSegmentIndex, + anchor, + point, + showPath; + + if (len) { + do { + point = points[pointIndex]; + + anchor = this.itemAnchor(pathItem, point).absolutePosition; + d[++dIndex] = anchor.x; + d[++dIndex] = anchor.y; + + // Crisping line, it might be replaced with + // Renderer.prototype.crispLine but it requires creating many + // temporary arrays + crispSegmentIndex = dIndex % 5; + if (crispSegmentIndex === 0) { + if (d[crispSegmentIndex + 1] === d[crispSegmentIndex + 4]) { + d[crispSegmentIndex + 1] = d[crispSegmentIndex + 4] = + Math.round(d[crispSegmentIndex + 1]) - + (strokeWidth % 2 / 2); + } + + if (d[crispSegmentIndex + 2] === d[crispSegmentIndex + 5]) { + d[crispSegmentIndex + 2] = d[crispSegmentIndex + 5] = + Math.round(d[crispSegmentIndex + 2]) + + (strokeWidth % 2 / 2); + } + } + + if (pointIndex < len - 1) { + d[++dIndex] = 'L'; + } + + showPath = point.series.visible; + + } while (++pointIndex < len && showPath); + } + + + if (showPath) { + pathItem[isNew ? 'attr' : 'animate']({ + d: d + }); + + } else { + pathItem.attr({ + d: 'M 0 ' + -9e9 + }); + } + + pathItem.placed = showPath; + }, + + /* + * @private + */ + renderItem: function (item) { + item.add( + item.itemType === 'label' ? + this.labelsGroup : + this.shapesGroup + ); + + this.setItemMarkers(item); + }, + + /* + * @private + */ + setItemMarkers: function (item) { + var itemOptions = item.options, + chart = this.chart, + defs = chart.options.defs, + fill = itemOptions.fill, + color = defined(fill) && fill !== 'none' ? + fill : + itemOptions.stroke, + + + setMarker = function (markerType) { + var markerId = itemOptions[markerType], + def, + predefinedMarker, + key, + marker; + + if (markerId) { + for (key in defs) { + def = defs[key]; + if (markerId === def.id && def.tagName === 'marker') { + predefinedMarker = def; + break; + } + } + + if (predefinedMarker) { + marker = item[markerType] = chart.renderer.addMarker( + (itemOptions.id || uniqueKey()) + '-' + + predefinedMarker.id, + merge(predefinedMarker, { color: color }) + ); + + item.attr(markerType, marker.attr('id')); + } + } + }; + + each(['markerStart', 'markerEnd'], setMarker); + }, + + /** + * An object which denotes an anchor position + * + * @typedef {Object} AnchorPosition + * @property {Number} AnchorPosition.x + * @property {Number} AnchorPosition.y + * @property {Number} AnchorPosition.height + * @property {Number} AnchorPosition.width + */ + + /** + * Returns object which denotes anchor position - relative and absolute + * + * @private + * @param {SVGElement} item + * @param {Highcharts.Point|Highcharts.MockPoint} point + * @return {Object} anchor + * @return {AnchorPosition} anchor.relativePosition + * Relative to the plot area position + * @return {AnchorPosition} anchor.absolutePosition + * Absolute position + */ + itemAnchor: function (item, point) { + var plotBox = point.series.getPlotBox(), + + box = point.mock ? + point.alignToBox(true) : + tooltipPrototype.getAnchor.call({ + chart: this.chart + }, point), + + anchor = { + x: box[0], + y: box[1], + height: box[2] || 0, + width: box[3] || 0 + }; + + return { + relativePosition: anchor, + absolutePosition: merge(anchor, { + x: anchor.x + plotBox.translateX, + y: anchor.y + plotBox.translateY + }) + }; + }, + + /** + * Returns the item position + * + * @private + * @param {SVGElement} item + * @param {AnchorPosition} anchor + * @return {Object|null} position + * @return {Number} position.x + * @return {Number} position.y + */ + itemPosition: function (item, anchor) { + var chart = this.chart, + point = item.points[0], + itemOptions = item.options, + anchorAbsolutePosition = anchor.absolutePosition, + anchorRelativePosition = anchor.relativePosition, + itemPosition, + alignTo, + itemPosRelativeX, + itemPosRelativeY, + + showItem = + point.series.visible && + MockPoint.prototype.isInsidePane.call(point); + + if (showItem) { + + if (defined(itemOptions.distance) || itemOptions.positioner) { + itemPosition = ( + itemOptions.positioner || + tooltipPrototype.getPosition + ).call( + { + chart: chart, + distance: pick(itemOptions.distance, 16) + }, + item.width, + item.height, + { + plotX: anchorRelativePosition.x, + plotY: anchorRelativePosition.y, + negative: point.negative, + ttBelow: point.ttBelow, + h: anchorRelativePosition.height || + anchorRelativePosition.width + } + ); + + } else { + alignTo = { + x: anchorAbsolutePosition.x, + y: anchorAbsolutePosition.y, + width: 0, + height: 0 + }; + + itemPosition = this.alignedPosition( + extend(itemOptions, { + width: item.width, + height: item.height + }), + alignTo + ); + + if (item.options.overflow === 'justify') { + itemPosition = this.alignedPosition( + this.justifiedOptions(item, itemOptions, itemPosition), + alignTo + ); + } + } + + + if (itemOptions.crop) { + itemPosRelativeX = itemPosition.x - chart.plotLeft; + itemPosRelativeY = itemPosition.y - chart.plotTop; + + showItem = + chart.isInsidePlot(itemPosRelativeX, itemPosRelativeY) && + chart.isInsidePlot( + itemPosRelativeX + item.width, + itemPosRelativeY + item.height + ); + } + } + + return showItem ? itemPosition : null; + }, + + /** + * Returns new aligned position based alignment options and box to align to. + * It is almost a one-to-one copy from SVGElement.prototype.align + * except it does not use and mutate an element + * + * @private + * @param {Object} alignOptions + * @param {Object} box + * @return {Object} aligned position + **/ + alignedPosition: function (alignOptions, box) { + var align = alignOptions.align, + vAlign = alignOptions.verticalAlign, + x = (box.x || 0) + (alignOptions.x || 0), + y = (box.y || 0) + (alignOptions.y || 0), + + alignFactor, + vAlignFactor; + + if (align === 'right') { + alignFactor = 1; + } else if (align === 'center') { + alignFactor = 2; + } + if (alignFactor) { + x += (box.width - (alignOptions.width || 0)) / alignFactor; + } + + if (vAlign === 'bottom') { + vAlignFactor = 1; + } else if (vAlign === 'middle') { + vAlignFactor = 2; + } + if (vAlignFactor) { + y += (box.height - (alignOptions.height || 0)) / vAlignFactor; + } + + return { + x: Math.round(x), + y: Math.round(y) + }; + }, + + /** + * Returns new alignment options for a label if the label is outside the + * plot area. It is almost a one-to-one copy from + * Series.prototype.justifyDataLabel except it does not mutate the label and + * it works with absolute instead of relative position. + * + * @private + * @param {Object} label + * @param {Object} alignOptions + * @param {Object} alignAttr + * @return {Object} justified options + **/ + justifiedOptions: function (label, alignOptions, alignAttr) { + var chart = this.chart, + align = alignOptions.align, + verticalAlign = alignOptions.verticalAlign, + padding = label.box ? 0 : (label.padding || 0), + bBox = label.getBBox(), + off, + + options = { + align: align, + verticalAlign: verticalAlign, + x: alignOptions.x, + y: alignOptions.y, + width: label.width, + height: label.height + }, + + x = alignAttr.x - chart.plotLeft, + y = alignAttr.y - chart.plotTop; + + // Off left + off = x + padding; + if (off < 0) { + if (align === 'right') { + options.align = 'left'; + } else { + options.x = -off; + } + } + + // Off right + off = x + bBox.width - padding; + if (off > chart.plotWidth) { + if (align === 'left') { + options.align = 'right'; + } else { + options.x = chart.plotWidth - off; + } + } + + // Off top + off = y + padding; + if (off < 0) { + if (verticalAlign === 'bottom') { + options.verticalAlign = 'top'; + } else { + options.y = -off; + } + } + + // Off bottom + off = y + bBox.height - padding; + if (off > chart.plotHeight) { + if (verticalAlign === 'top') { + options.verticalAlign = 'bottom'; + } else { + options.y = chart.plotHeight - off; + } + } + + return options; + }, + + + /** + * Utility function for mapping item's options to element's attribute + * + * @private + * @param {Object} options + * @return {Object} mapped options + **/ + attrsFromOptions: function (options) { + var map = this.attrsMap, + attrs = {}, + key, + mappedKey; + + for (key in options) { + mappedKey = map[key]; + if (mappedKey) { + attrs[mappedKey] = options[key]; + } + } + + return attrs; + } }; /* *************************************************************************** @@ -1710,94 +1710,94 @@ Annotation.prototype = /** @lends Highcharts.Annotation# */ { **************************************************************************** */ H.extend(chartPrototype, /** @lends Chart# */ { - /** - * Add an annotation to the chart after render time. - * - * @param {AnnotationOptions} options - * The series options for the new, detailed series. - * - * @return {Highcharts.Annotation} - The newly generated annotation. - */ - addAnnotation: function (userOptions, redraw) { - var annotation = new Annotation(this, userOptions); - - this.annotations.push(annotation); - - if (pick(redraw, true)) { - annotation.redraw(); - } - - return annotation; - }, - - /** - * Remove an annotation from the chart. - * - * @param {String} id - The annotation's id. - */ - removeAnnotation: function (id) { - var annotations = this.annotations, - annotation = find(annotations, function (annotation) { - return annotation.options.id === id; - }); - - if (annotation) { - erase(annotations, annotation); - annotation.destroy(); - } - }, - - /** - * @private - * @memberOf Highcharts.Chart# - * @function drawAnnotations - */ - drawAnnotations: function () { - var clip = this.plotBoxClip, - plotBox = this.plotBox; - - if (clip) { - clip.attr(plotBox); - } else { - this.plotBoxClip = this.renderer.clipRect(plotBox); - } - - each(this.annotations, function (annotation) { - annotation.redraw(); - }); - } + /** + * Add an annotation to the chart after render time. + * + * @param {AnnotationOptions} options + * The series options for the new, detailed series. + * + * @return {Highcharts.Annotation} - The newly generated annotation. + */ + addAnnotation: function (userOptions, redraw) { + var annotation = new Annotation(this, userOptions); + + this.annotations.push(annotation); + + if (pick(redraw, true)) { + annotation.redraw(); + } + + return annotation; + }, + + /** + * Remove an annotation from the chart. + * + * @param {String} id - The annotation's id. + */ + removeAnnotation: function (id) { + var annotations = this.annotations, + annotation = find(annotations, function (annotation) { + return annotation.options.id === id; + }); + + if (annotation) { + erase(annotations, annotation); + annotation.destroy(); + } + }, + + /** + * @private + * @memberOf Highcharts.Chart# + * @function drawAnnotations + */ + drawAnnotations: function () { + var clip = this.plotBoxClip, + plotBox = this.plotBox; + + if (clip) { + clip.attr(plotBox); + } else { + this.plotBoxClip = this.renderer.clipRect(plotBox); + } + + each(this.annotations, function (annotation) { + annotation.redraw(); + }); + } }); chartPrototype.callbacks.push(function (chart) { - chart.annotations = []; + chart.annotations = []; - each(chart.options.annotations, function (annotationOptions) { - chart.addAnnotation(annotationOptions, false); - }); + each(chart.options.annotations, function (annotationOptions) { + chart.addAnnotation(annotationOptions, false); + }); - chart.drawAnnotations(); - addEvent(chart, 'redraw', chart.drawAnnotations); - addEvent(chart, 'destroy', function () { - var plotBoxClip = chart.plotBoxClip; + chart.drawAnnotations(); + addEvent(chart, 'redraw', chart.drawAnnotations); + addEvent(chart, 'destroy', function () { + var plotBoxClip = chart.plotBoxClip; - if (plotBoxClip && plotBoxClip.destroy) { - plotBoxClip.destroy(); - } - }); + if (plotBoxClip && plotBoxClip.destroy) { + plotBoxClip.destroy(); + } + }); }); addEvent(H.Chart, 'afterGetContainer', function () { - this.options.defs = merge(defaultMarkers, this.options.defs || {}); - + this.options.defs = merge(defaultMarkers, this.options.defs || {}); + /*= if (build.classic) { =*/ - objectEach(this.options.defs, function (def) { - if (def.tagName === 'marker' && def.render !== false) { - this.renderer.addMarker(def.id, def); - } - }, this); + objectEach(this.options.defs, function (def) { + if (def.tagName === 'marker' && def.render !== false) { + this.renderer.addMarker(def.id, def); + } + }, this); /*= } =*/ }); @@ -1808,41 +1808,41 @@ addEvent(H.Chart, 'afterGetContainer', function () { * General symbol definition for labels with connector */ H.SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) { - var anchorX = options && options.anchorX, - anchorY = options && options.anchorY, - path, - yOffset, - lateral = w / 2; - - if (isNumber(anchorX) && isNumber(anchorY)) { - - path = ['M', anchorX, anchorY]; - - // Prefer 45 deg connectors - yOffset = y - anchorY; - if (yOffset < 0) { - yOffset = -h - yOffset; - } - if (yOffset < w) { - lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset; - } - - // Anchor below label - if (anchorY > y + h) { - path.push('L', x + lateral, y + h); - - // Anchor above label - } else if (anchorY < y) { - path.push('L', x + lateral, y); - - // Anchor left of label - } else if (anchorX < x) { - path.push('L', x, y + h / 2); - - // Anchor right of label - } else if (anchorX > x + w) { - path.push('L', x + w, y + h / 2); - } - } - return path || []; + var anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path, + yOffset, + lateral = w / 2; + + if (isNumber(anchorX) && isNumber(anchorY)) { + + path = ['M', anchorX, anchorY]; + + // Prefer 45 deg connectors + yOffset = y - anchorY; + if (yOffset < 0) { + yOffset = -h - yOffset; + } + if (yOffset < w) { + lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset; + } + + // Anchor below label + if (anchorY > y + h) { + path.push('L', x + lateral, y + h); + + // Anchor above label + } else if (anchorY < y) { + path.push('L', x + lateral, y); + + // Anchor left of label + } else if (anchorX < x) { + path.push('L', x, y + h / 2); + + // Anchor right of label + } else if (anchorX > x + w) { + path.push('L', x + w, y + h / 2); + } + } + return path || []; }; diff --git a/js/modules/bellcurve.src.js b/js/modules/bellcurve.src.js index 1d29006e54b..5f2fce3c4be 100644 --- a/js/modules/bellcurve.src.js +++ b/js/modules/bellcurve.src.js @@ -13,10 +13,10 @@ import '../parts/Utilities.js'; import derivedSeriesMixin from '../mixins/derived-series.js'; var seriesType = H.seriesType, - correctFloat = H.correctFloat, - isNumber = H.isNumber, - merge = H.merge, - reduce = H.reduce; + correctFloat = H.correctFloat, + isNumber = H.isNumber, + merge = H.merge, + reduce = H.reduce; /** **************************************************************************** @@ -26,40 +26,40 @@ var seriesType = H.seriesType, ******************************************************************************/ function mean(data) { - var length = data.length, - sum = reduce(data, function (sum, value) { - return (sum += value); - }, 0); + var length = data.length, + sum = reduce(data, function (sum, value) { + return (sum += value); + }, 0); - return length > 0 && sum / length; + return length > 0 && sum / length; } function standardDeviation(data, average) { - var len = data.length, - sum; + var len = data.length, + sum; - average = isNumber(average) ? average : mean(data); - - sum = reduce(data, function (sum, value) { - var diff = value - average; - return (sum += diff * diff); - }, 0); + average = isNumber(average) ? average : mean(data); - return len > 1 && Math.sqrt(sum / (len - 1)); + sum = reduce(data, function (sum, value) { + var diff = value - average; + return (sum += diff * diff); + }, 0); + + return len > 1 && Math.sqrt(sum / (len - 1)); } function normalDensity(x, mean, standardDeviation) { - var translation = x - mean; - return Math.exp( - -(translation * translation) / - (2 * standardDeviation * standardDeviation) - ) / (standardDeviation * Math.sqrt(2 * Math.PI)); + var translation = x - mean; + return Math.exp( + -(translation * translation) / + (2 * standardDeviation * standardDeviation) + ) / (standardDeviation * Math.sqrt(2 * Math.PI)); } /** * Bell curve class - * + * * @constructor seriesTypes.bellcurve * @augments seriesTypes.areaspline * @mixes DerivedSeriesMixin @@ -87,70 +87,70 @@ seriesType('bellcurve', 'areaspline', { * @sample highcharts/plotoptions/bellcurve-intervals-pointsininterval * Intervals and points in interval */ - intervals: 3, + intervals: 3, /** - * Defines how many points should be plotted within 1 interval. See + * Defines how many points should be plotted within 1 interval. See * `plotOptions.bellcurve.intervals`. * * @sample highcharts/plotoptions/bellcurve-intervals-pointsininterval * Intervals and points in interval */ - pointsInInterval: 3, + pointsInInterval: 3, - marker: { - enabled: false - } + marker: { + enabled: false + } }, merge(derivedSeriesMixin, { - setMean: function () { - this.mean = correctFloat(mean(this.baseSeries.yData)); - }, - - setStandardDeviation: function () { - this.standardDeviation = correctFloat( - standardDeviation(this.baseSeries.yData, this.mean) - ); - }, - - setDerivedData: function () { - if (this.baseSeries.yData.length > 1) { - this.setMean(); - this.setStandardDeviation(); - this.setData( - this.derivedData(this.mean, this.standardDeviation), false - ); - } - }, - - derivedData: function (mean, standardDeviation) { - var intervals = this.options.intervals, - pointsInInterval = this.options.pointsInInterval, - x = mean - intervals * standardDeviation, - stop = intervals * pointsInInterval * 2 + 1, - increment = standardDeviation / pointsInInterval, - data = [], - i; - - for (i = 0; i < stop; i++) { - data.push([x, normalDensity(x, mean, standardDeviation)]); - x += increment; - } - - return data; - } + setMean: function () { + this.mean = correctFloat(mean(this.baseSeries.yData)); + }, + + setStandardDeviation: function () { + this.standardDeviation = correctFloat( + standardDeviation(this.baseSeries.yData, this.mean) + ); + }, + + setDerivedData: function () { + if (this.baseSeries.yData.length > 1) { + this.setMean(); + this.setStandardDeviation(); + this.setData( + this.derivedData(this.mean, this.standardDeviation), false + ); + } + }, + + derivedData: function (mean, standardDeviation) { + var intervals = this.options.intervals, + pointsInInterval = this.options.pointsInInterval, + x = mean - intervals * standardDeviation, + stop = intervals * pointsInInterval * 2 + 1, + increment = standardDeviation / pointsInInterval, + data = [], + i; + + for (i = 0; i < stop; i++) { + data.push([x, normalDensity(x, mean, standardDeviation)]); + x += increment; + } + + return data; + } })); /** * A `bellcurve` series. If the [type](#series.bellcurve.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.bellcurve](#plotOptions.bellcurve). -* +* * @type {Object} * @since 6.0.0 * @extends series,plotOptions.bellcurve @@ -166,12 +166,12 @@ seriesType('bellcurve', 'areaspline', { * @type {Number|String} * @default undefined * @apioption series.bellcurve.baseSeries -*/ +*/ /** * An array of data points for the series. For the `bellcurve` series type, * points are calculated dynamically. -* +* * @type {Array} * @since 6.0.0 * @extends series.areaspline.data diff --git a/js/modules/boost-canvas.src.js b/js/modules/boost-canvas.src.js index 646e3b71144..d2762705a19 100644 --- a/js/modules/boost-canvas.src.js +++ b/js/modules/boost-canvas.src.js @@ -16,677 +16,677 @@ import '../parts/Series.js'; import '../parts/Options.js'; var win = H.win, - doc = win.document, - noop = function () {}, - Color = H.Color, - Series = H.Series, - seriesTypes = H.seriesTypes, - each = H.each, - extend = H.extend, - addEvent = H.addEvent, - fireEvent = H.fireEvent, - isNumber = H.isNumber, - merge = H.merge, - pick = H.pick, - wrap = H.wrap, - CHUNK_SIZE = 50000, - destroyLoadingDiv; + doc = win.document, + noop = function () {}, + Color = H.Color, + Series = H.Series, + seriesTypes = H.seriesTypes, + each = H.each, + extend = H.extend, + addEvent = H.addEvent, + fireEvent = H.fireEvent, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + wrap = H.wrap, + CHUNK_SIZE = 50000, + destroyLoadingDiv; H.initCanvasBoost = function () { - if (H.seriesTypes.heatmap) { - H.wrap(H.seriesTypes.heatmap.prototype, 'drawPoints', function () { - var ctx = this.getContext(); - if (ctx) { - - // draw the columns - each(this.points, function (point) { - var plotY = point.plotY, - shapeArgs, - pointAttr; - - if ( - plotY !== undefined && - !isNaN(plotY) && - point.y !== null - ) { - shapeArgs = point.shapeArgs; - - /*= if (build.classic) { =*/ - pointAttr = point.series.pointAttribs(point); - /*= } else { =*/ - pointAttr = point.series.colorAttribs(point); - /*= } =*/ - - ctx.fillStyle = pointAttr.fill; - ctx.fillRect( - shapeArgs.x, - shapeArgs.y, - shapeArgs.width, - shapeArgs.height - ); - } - }); - - this.canvasToSVG(); - - } else { - this.chart.showLoading( - 'Your browser doesn\'t support HTML5 canvas,
' + - 'please use a modern browser'); - - // Uncomment this to provide low-level (slow) support in oldIE. - // It will cause script errors on charts with more than a few - // thousand points. - // arguments[0].call(this); - } - }); - } - - - H.extend(Series.prototype, { - - /** - * Create a hidden canvas to draw the graph on. The contents is later - * copied over to an SVG image element. - */ - getContext: function () { - var chart = this.chart, - width = chart.chartWidth, - height = chart.chartHeight, - targetGroup = chart.seriesGroup || this.group, - target = this, - ctx, - swapXY = function (proceed, x, y, a, b, c, d) { - proceed.call(this, y, x, a, b, c, d); - }; - - if (chart.isChartSeriesBoosting()) { - target = chart; - targetGroup = chart.seriesGroup; - } - - ctx = target.ctx; - - if (!target.canvas) { - target.canvas = doc.createElement('canvas'); - - target.renderTarget = chart.renderer.image( - '', - 0, - 0, - width, - height - ) - .addClass('highcharts-boost-canvas') - .add(targetGroup); - - target.ctx = ctx = target.canvas.getContext('2d'); - - if (chart.inverted) { - each(['moveTo', 'lineTo', 'rect', 'arc'], function (fn) { - wrap(ctx, fn, swapXY); - }); - } - - target.boostCopy = function () { - target.renderTarget.attr({ - href: target.canvas.toDataURL('image/png') - }); - }; - - target.boostClear = function () { - ctx.clearRect( - 0, - 0, - target.canvas.width, - target.canvas.height - ); - - if (target === this) { - target.renderTarget.attr({ href: '' }); - } - }; - - target.boostClipRect = chart.renderer.clipRect(); - - target.renderTarget.clip(target.boostClipRect); - - } else if (!(target instanceof H.Chart)) { - // ctx.clearRect(0, 0, width, height); - } - - if (target.canvas.width !== width) { - target.canvas.width = width; - } - - if (target.canvas.height !== height) { - target.canvas.height = height; - } - - target.renderTarget.attr({ - x: 0, - y: 0, - width: width, - height: height, - style: 'pointer-events: none', - href: '' - }); - - target.boostClipRect.attr(chart.getBoostClipRect(target)); - - return ctx; - }, - - /** - * Draw the canvas image inside an SVG image - */ - canvasToSVG: function () { - if (!this.chart.isChartSeriesBoosting()) { - if (this.boostCopy || this.chart.boostCopy) { - (this.boostCopy || this.chart.boostCopy)(); - } - } else { - if (this.boostClear) { - this.boostClear(); - } - } - }, - - cvsLineTo: function (ctx, clientX, plotY) { - ctx.lineTo(clientX, plotY); - }, - - renderCanvas: function () { - var series = this, - options = series.options, - chart = series.chart, - xAxis = this.xAxis, - yAxis = this.yAxis, - activeBoostSettings = chart.options.boost || {}, - boostSettings = { - timeRendering: activeBoostSettings.timeRendering || false, - timeSeriesProcessing: - activeBoostSettings.timeSeriesProcessing || false, - timeSetup: activeBoostSettings.timeSetup || false - }, - ctx, - c = 0, - xData = series.processedXData, - yData = series.processedYData, - rawData = options.data, - xExtremes = xAxis.getExtremes(), - xMin = xExtremes.min, - xMax = xExtremes.max, - yExtremes = yAxis.getExtremes(), - yMin = yExtremes.min, - yMax = yExtremes.max, - pointTaken = {}, - lastClientX, - sampling = !!series.sampling, - points, - r = options.marker && options.marker.radius, - cvsDrawPoint = this.cvsDrawPoint, - cvsLineTo = options.lineWidth ? this.cvsLineTo : false, - cvsMarker = r && r <= 1 ? - this.cvsMarkerSquare : - this.cvsMarkerCircle, - strokeBatch = this.cvsStrokeBatch || 1000, - enableMouseTracking = options.enableMouseTracking !== false, - lastPoint, - threshold = options.threshold, - yBottom = yAxis.getThreshold(threshold), - hasThreshold = isNumber(threshold), - translatedThreshold = yBottom, - doFill = this.fill, - isRange = series.pointArrayMap && - series.pointArrayMap.join(',') === 'low,high', - isStacked = !!options.stacking, - cropStart = series.cropStart || 0, - loadingOptions = chart.options.loading, - requireSorting = series.requireSorting, - wasNull, - connectNulls = options.connectNulls, - useRaw = !xData, - minVal, - maxVal, - minI, - maxI, - kdIndex, - sdata = isStacked ? series.data : (xData || rawData), - fillColor = series.fillOpacity ? - new Color(series.color).setOpacity( - pick(options.fillOpacity, 0.75) - ).get() : - series.color, - - stroke = function () { - if (doFill) { - ctx.fillStyle = fillColor; - ctx.fill(); - } else { - ctx.strokeStyle = series.color; - ctx.lineWidth = options.lineWidth; - ctx.stroke(); - } - }, - - drawPoint = function (clientX, plotY, yBottom, i) { - if (c === 0) { - ctx.beginPath(); - - if (cvsLineTo) { - ctx.lineJoin = 'round'; - } - } - - if ( - chart.scroller && - series.options.className === - 'highcharts-navigator-series' - ) { - plotY += chart.scroller.top; - if (yBottom) { - yBottom += chart.scroller.top; - } - } else { - plotY += chart.plotTop; - } - - clientX += chart.plotLeft; - - if (wasNull) { - ctx.moveTo(clientX, plotY); - } else { - if (cvsDrawPoint) { - cvsDrawPoint( - ctx, - clientX, - plotY, - yBottom, - lastPoint - ); - } else if (cvsLineTo) { - cvsLineTo(ctx, clientX, plotY); - } else if (cvsMarker) { - cvsMarker.call(series, ctx, clientX, plotY, r, i); - } - } - - // We need to stroke the line for every 1000 pixels. It will - // crash the browser memory use if we stroke too - // infrequently. - c = c + 1; - if (c === strokeBatch) { - stroke(); - c = 0; - } - - // Area charts need to keep track of the last point - lastPoint = { - clientX: clientX, - plotY: plotY, - yBottom: yBottom - }; - }, - - addKDPoint = function (clientX, plotY, i) { - // Avoid more string concatination than required - kdIndex = clientX + ',' + plotY; - - // The k-d tree requires series points. Reduce the amount of - // points, since the time to build the tree increases - // exponentially. - if (enableMouseTracking && !pointTaken[kdIndex]) { - pointTaken[kdIndex] = true; - - if (chart.inverted) { - clientX = xAxis.len - clientX; - plotY = yAxis.len - plotY; - } - - points.push({ - clientX: clientX, - plotX: clientX, - plotY: plotY, - i: cropStart + i - }); - } - }; - - if (this.renderTarget) { - this.renderTarget.attr({ 'href': '' }); - } - - // If we are zooming out from SVG mode, destroy the graphics - if (this.points || this.graph) { - this.destroyGraphics(); - } - - // The group - series.plotGroup( - 'group', - 'series', - series.visible ? 'visible' : 'hidden', - options.zIndex, - chart.seriesGroup - ); - - series.markerGroup = series.group; - addEvent(series, 'destroy', function () { // Prevent destroy twice - series.markerGroup = null; - }); - - points = this.points = []; - ctx = this.getContext(); - series.buildKDTree = noop; // Do not start building while drawing - - if (this.boostClear) { - this.boostClear(); - } - - // if (this.canvas) { - // ctx.clearRect( - // 0, - // 0, - // this.canvas.width, - // this.canvas.height - // ); - // } - - if (!this.visible) { - return; - } - - // Display a loading indicator - if (rawData.length > 99999) { - chart.options.loading = merge(loadingOptions, { - labelStyle: { - backgroundColor: H.color('${palette.backgroundColor}') - .setOpacity(0.75).get(), - padding: '1em', - borderRadius: '0.5em' - }, - style: { - backgroundColor: 'none', - opacity: 1 - } - }); - H.clearTimeout(destroyLoadingDiv); - chart.showLoading('Drawing...'); - chart.options.loading = loadingOptions; // reset - } - - if (boostSettings.timeRendering) { - console.time('canvas rendering'); // eslint-disable-line no-console - } - - // Loop over the points - H.eachAsync(sdata, function (d, i) { - var x, - y, - clientX, - plotY, - isNull, - low, - isNextInside = false, - isPrevInside = false, - nx = false, - px = false, - chartDestroyed = typeof chart.index === 'undefined', - isYInside = true; - - if (!chartDestroyed) { - if (useRaw) { - x = d[0]; - y = d[1]; - - if (sdata[i + 1]) { - nx = sdata[i + 1][0]; - } - - if (sdata[i - 1]) { - px = sdata[i - 1][0]; - } - } else { - x = d; - y = yData[i]; - - if (sdata[i + 1]) { - nx = sdata[i + 1]; - } - - if (sdata[i - 1]) { - px = sdata[i - 1]; - } - } - - if (nx && nx >= xMin && nx <= xMax) { - isNextInside = true; - } - - if (px && px >= xMin && px <= xMax) { - isPrevInside = true; - } - - // Resolve low and high for range series - if (isRange) { - if (useRaw) { - y = d.slice(1, 3); - } - low = y[0]; - y = y[1]; - } else if (isStacked) { - x = d.x; - y = d.stackY; - low = y - d.y; - } - - isNull = y === null; - - // Optimize for scatter zooming - if (!requireSorting) { - isYInside = y >= yMin && y <= yMax; - } - - if (!isNull && - ( - (x >= xMin && x <= xMax && isYInside) || - (isNextInside || isPrevInside) - )) { - - - clientX = Math.round(xAxis.toPixels(x, true)); - - if (sampling) { - if (minI === undefined || clientX === lastClientX) { - if (!isRange) { - low = y; - } - if (maxI === undefined || y > maxVal) { - maxVal = y; - maxI = i; - } - if (minI === undefined || low < minVal) { - minVal = low; - minI = i; - } - - } - // Add points and reset - if (clientX !== lastClientX) { - if (minI !== undefined) { // maxI also a number - plotY = yAxis.toPixels(maxVal, true); - yBottom = yAxis.toPixels(minVal, true); - drawPoint( - clientX, - hasThreshold ? - Math.min( - plotY, - translatedThreshold - ) : plotY, - hasThreshold ? - Math.max( - yBottom, - translatedThreshold - ) : yBottom, - i - ); - addKDPoint(clientX, plotY, maxI); - if (yBottom !== plotY) { - addKDPoint(clientX, yBottom, minI); - } - } - - minI = maxI = undefined; - lastClientX = clientX; - } - } else { - plotY = Math.round(yAxis.toPixels(y, true)); - drawPoint(clientX, plotY, yBottom, i); - addKDPoint(clientX, plotY, i); - } - } - wasNull = isNull && !connectNulls; - - if (i % CHUNK_SIZE === 0) { - if (series.boostCopy || series.chart.boostCopy) { - (series.boostCopy || series.chart.boostCopy)(); - } - } - } - - return !chartDestroyed; - }, function () { - var loadingDiv = chart.loadingDiv, - loadingShown = chart.loadingShown; - stroke(); - - // if (series.boostCopy || series.chart.boostCopy) { - // (series.boostCopy || series.chart.boostCopy)(); - // } - - series.canvasToSVG(); - - if (boostSettings.timeRendering) { - console.timeEnd('canvas rendering'); // eslint-disable-line no-console - } - - fireEvent(series, 'renderedCanvas'); - - // Do not use chart.hideLoading, as it runs JS animation and - // will be blocked by buildKDTree. CSS animation looks good, but - // then it must be deleted in timeout. If we add the module to - // core, change hideLoading so we can skip this block. - if (loadingShown) { - extend(loadingDiv.style, { - transition: 'opacity 250ms', - opacity: 0 - }); - chart.loadingShown = false; - destroyLoadingDiv = setTimeout(function () { - if (loadingDiv.parentNode) { // In exporting it is falsy - loadingDiv.parentNode.removeChild(loadingDiv); - } - chart.loadingDiv = chart.loadingSpan = null; - }, 250); - } - - // Go back to prototype, ready to build - delete series.buildKDTree; - - series.buildKDTree(); - - // Don't do async on export, the exportChart, getSVGForExport and - // getSVG methods are not chained for it. - }, chart.renderer.forExport ? Number.MAX_VALUE : undefined); - } - }); - - seriesTypes.scatter.prototype.cvsMarkerCircle = function ( - ctx, - clientX, - plotY, - r - ) { - ctx.moveTo(clientX, plotY); - ctx.arc(clientX, plotY, r, 0, 2 * Math.PI, false); - }; - - // Rect is twice as fast as arc, should be used for small markers - seriesTypes.scatter.prototype.cvsMarkerSquare = function ( - ctx, - clientX, - plotY, - r - ) { - ctx.rect(clientX - r, plotY - r, r * 2, r * 2); - }; - seriesTypes.scatter.prototype.fill = true; - - if (seriesTypes.bubble) { - seriesTypes.bubble.prototype.cvsMarkerCircle = function ( - ctx, - clientX, - plotY, - r, - i - ) { - ctx.moveTo(clientX, plotY); - ctx.arc( - clientX, - plotY, - this.radii && this.radii[i], 0, 2 * Math.PI, - false - ); - }; - seriesTypes.bubble.prototype.cvsStrokeBatch = 1; - } - - extend(seriesTypes.area.prototype, { - cvsDrawPoint: function (ctx, clientX, plotY, yBottom, lastPoint) { - if (lastPoint && clientX !== lastPoint.clientX) { - ctx.moveTo(lastPoint.clientX, lastPoint.yBottom); - ctx.lineTo(lastPoint.clientX, lastPoint.plotY); - ctx.lineTo(clientX, plotY); - ctx.lineTo(clientX, yBottom); - } - }, - fill: true, - fillOpacity: true, - sampling: true - }); - - extend(seriesTypes.column.prototype, { - cvsDrawPoint: function (ctx, clientX, plotY, yBottom) { - ctx.rect(clientX - 1, plotY, 1, yBottom - plotY); - }, - fill: true, - sampling: true - }); - - H.Chart.prototype.callbacks.push(function (chart) { - function canvasToSVG() { - if (chart.boostCopy) { - chart.boostCopy(); - } - } - - function clear() { - if (chart.renderTarget) { - chart.renderTarget.attr({ href: '' }); - } - - if (chart.canvas) { - chart.canvas.getContext('2d').clearRect( - 0, - 0, - chart.canvas.width, - chart.canvas.height - ); - } - } - - addEvent(chart, 'predraw', clear); - addEvent(chart, 'render', canvasToSVG); - }); + if (H.seriesTypes.heatmap) { + H.wrap(H.seriesTypes.heatmap.prototype, 'drawPoints', function () { + var ctx = this.getContext(); + if (ctx) { + + // draw the columns + each(this.points, function (point) { + var plotY = point.plotY, + shapeArgs, + pointAttr; + + if ( + plotY !== undefined && + !isNaN(plotY) && + point.y !== null + ) { + shapeArgs = point.shapeArgs; + + /*= if (build.classic) { =*/ + pointAttr = point.series.pointAttribs(point); + /*= } else { =*/ + pointAttr = point.series.colorAttribs(point); + /*= } =*/ + + ctx.fillStyle = pointAttr.fill; + ctx.fillRect( + shapeArgs.x, + shapeArgs.y, + shapeArgs.width, + shapeArgs.height + ); + } + }); + + this.canvasToSVG(); + + } else { + this.chart.showLoading( + 'Your browser doesn\'t support HTML5 canvas,
' + + 'please use a modern browser'); + + // Uncomment this to provide low-level (slow) support in oldIE. + // It will cause script errors on charts with more than a few + // thousand points. + // arguments[0].call(this); + } + }); + } + + + H.extend(Series.prototype, { + + /** + * Create a hidden canvas to draw the graph on. The contents is later + * copied over to an SVG image element. + */ + getContext: function () { + var chart = this.chart, + width = chart.chartWidth, + height = chart.chartHeight, + targetGroup = chart.seriesGroup || this.group, + target = this, + ctx, + swapXY = function (proceed, x, y, a, b, c, d) { + proceed.call(this, y, x, a, b, c, d); + }; + + if (chart.isChartSeriesBoosting()) { + target = chart; + targetGroup = chart.seriesGroup; + } + + ctx = target.ctx; + + if (!target.canvas) { + target.canvas = doc.createElement('canvas'); + + target.renderTarget = chart.renderer.image( + '', + 0, + 0, + width, + height + ) + .addClass('highcharts-boost-canvas') + .add(targetGroup); + + target.ctx = ctx = target.canvas.getContext('2d'); + + if (chart.inverted) { + each(['moveTo', 'lineTo', 'rect', 'arc'], function (fn) { + wrap(ctx, fn, swapXY); + }); + } + + target.boostCopy = function () { + target.renderTarget.attr({ + href: target.canvas.toDataURL('image/png') + }); + }; + + target.boostClear = function () { + ctx.clearRect( + 0, + 0, + target.canvas.width, + target.canvas.height + ); + + if (target === this) { + target.renderTarget.attr({ href: '' }); + } + }; + + target.boostClipRect = chart.renderer.clipRect(); + + target.renderTarget.clip(target.boostClipRect); + + } else if (!(target instanceof H.Chart)) { + // ctx.clearRect(0, 0, width, height); + } + + if (target.canvas.width !== width) { + target.canvas.width = width; + } + + if (target.canvas.height !== height) { + target.canvas.height = height; + } + + target.renderTarget.attr({ + x: 0, + y: 0, + width: width, + height: height, + style: 'pointer-events: none', + href: '' + }); + + target.boostClipRect.attr(chart.getBoostClipRect(target)); + + return ctx; + }, + + /** + * Draw the canvas image inside an SVG image + */ + canvasToSVG: function () { + if (!this.chart.isChartSeriesBoosting()) { + if (this.boostCopy || this.chart.boostCopy) { + (this.boostCopy || this.chart.boostCopy)(); + } + } else { + if (this.boostClear) { + this.boostClear(); + } + } + }, + + cvsLineTo: function (ctx, clientX, plotY) { + ctx.lineTo(clientX, plotY); + }, + + renderCanvas: function () { + var series = this, + options = series.options, + chart = series.chart, + xAxis = this.xAxis, + yAxis = this.yAxis, + activeBoostSettings = chart.options.boost || {}, + boostSettings = { + timeRendering: activeBoostSettings.timeRendering || false, + timeSeriesProcessing: + activeBoostSettings.timeSeriesProcessing || false, + timeSetup: activeBoostSettings.timeSetup || false + }, + ctx, + c = 0, + xData = series.processedXData, + yData = series.processedYData, + rawData = options.data, + xExtremes = xAxis.getExtremes(), + xMin = xExtremes.min, + xMax = xExtremes.max, + yExtremes = yAxis.getExtremes(), + yMin = yExtremes.min, + yMax = yExtremes.max, + pointTaken = {}, + lastClientX, + sampling = !!series.sampling, + points, + r = options.marker && options.marker.radius, + cvsDrawPoint = this.cvsDrawPoint, + cvsLineTo = options.lineWidth ? this.cvsLineTo : false, + cvsMarker = r && r <= 1 ? + this.cvsMarkerSquare : + this.cvsMarkerCircle, + strokeBatch = this.cvsStrokeBatch || 1000, + enableMouseTracking = options.enableMouseTracking !== false, + lastPoint, + threshold = options.threshold, + yBottom = yAxis.getThreshold(threshold), + hasThreshold = isNumber(threshold), + translatedThreshold = yBottom, + doFill = this.fill, + isRange = series.pointArrayMap && + series.pointArrayMap.join(',') === 'low,high', + isStacked = !!options.stacking, + cropStart = series.cropStart || 0, + loadingOptions = chart.options.loading, + requireSorting = series.requireSorting, + wasNull, + connectNulls = options.connectNulls, + useRaw = !xData, + minVal, + maxVal, + minI, + maxI, + kdIndex, + sdata = isStacked ? series.data : (xData || rawData), + fillColor = series.fillOpacity ? + new Color(series.color).setOpacity( + pick(options.fillOpacity, 0.75) + ).get() : + series.color, + + stroke = function () { + if (doFill) { + ctx.fillStyle = fillColor; + ctx.fill(); + } else { + ctx.strokeStyle = series.color; + ctx.lineWidth = options.lineWidth; + ctx.stroke(); + } + }, + + drawPoint = function (clientX, plotY, yBottom, i) { + if (c === 0) { + ctx.beginPath(); + + if (cvsLineTo) { + ctx.lineJoin = 'round'; + } + } + + if ( + chart.scroller && + series.options.className === + 'highcharts-navigator-series' + ) { + plotY += chart.scroller.top; + if (yBottom) { + yBottom += chart.scroller.top; + } + } else { + plotY += chart.plotTop; + } + + clientX += chart.plotLeft; + + if (wasNull) { + ctx.moveTo(clientX, plotY); + } else { + if (cvsDrawPoint) { + cvsDrawPoint( + ctx, + clientX, + plotY, + yBottom, + lastPoint + ); + } else if (cvsLineTo) { + cvsLineTo(ctx, clientX, plotY); + } else if (cvsMarker) { + cvsMarker.call(series, ctx, clientX, plotY, r, i); + } + } + + // We need to stroke the line for every 1000 pixels. It will + // crash the browser memory use if we stroke too + // infrequently. + c = c + 1; + if (c === strokeBatch) { + stroke(); + c = 0; + } + + // Area charts need to keep track of the last point + lastPoint = { + clientX: clientX, + plotY: plotY, + yBottom: yBottom + }; + }, + + addKDPoint = function (clientX, plotY, i) { + // Avoid more string concatination than required + kdIndex = clientX + ',' + plotY; + + // The k-d tree requires series points. Reduce the amount of + // points, since the time to build the tree increases + // exponentially. + if (enableMouseTracking && !pointTaken[kdIndex]) { + pointTaken[kdIndex] = true; + + if (chart.inverted) { + clientX = xAxis.len - clientX; + plotY = yAxis.len - plotY; + } + + points.push({ + clientX: clientX, + plotX: clientX, + plotY: plotY, + i: cropStart + i + }); + } + }; + + if (this.renderTarget) { + this.renderTarget.attr({ 'href': '' }); + } + + // If we are zooming out from SVG mode, destroy the graphics + if (this.points || this.graph) { + this.destroyGraphics(); + } + + // The group + series.plotGroup( + 'group', + 'series', + series.visible ? 'visible' : 'hidden', + options.zIndex, + chart.seriesGroup + ); + + series.markerGroup = series.group; + addEvent(series, 'destroy', function () { // Prevent destroy twice + series.markerGroup = null; + }); + + points = this.points = []; + ctx = this.getContext(); + series.buildKDTree = noop; // Do not start building while drawing + + if (this.boostClear) { + this.boostClear(); + } + + // if (this.canvas) { + // ctx.clearRect( + // 0, + // 0, + // this.canvas.width, + // this.canvas.height + // ); + // } + + if (!this.visible) { + return; + } + + // Display a loading indicator + if (rawData.length > 99999) { + chart.options.loading = merge(loadingOptions, { + labelStyle: { + backgroundColor: H.color('${palette.backgroundColor}') + .setOpacity(0.75).get(), + padding: '1em', + borderRadius: '0.5em' + }, + style: { + backgroundColor: 'none', + opacity: 1 + } + }); + H.clearTimeout(destroyLoadingDiv); + chart.showLoading('Drawing...'); + chart.options.loading = loadingOptions; // reset + } + + if (boostSettings.timeRendering) { + console.time('canvas rendering'); // eslint-disable-line no-console + } + + // Loop over the points + H.eachAsync(sdata, function (d, i) { + var x, + y, + clientX, + plotY, + isNull, + low, + isNextInside = false, + isPrevInside = false, + nx = false, + px = false, + chartDestroyed = typeof chart.index === 'undefined', + isYInside = true; + + if (!chartDestroyed) { + if (useRaw) { + x = d[0]; + y = d[1]; + + if (sdata[i + 1]) { + nx = sdata[i + 1][0]; + } + + if (sdata[i - 1]) { + px = sdata[i - 1][0]; + } + } else { + x = d; + y = yData[i]; + + if (sdata[i + 1]) { + nx = sdata[i + 1]; + } + + if (sdata[i - 1]) { + px = sdata[i - 1]; + } + } + + if (nx && nx >= xMin && nx <= xMax) { + isNextInside = true; + } + + if (px && px >= xMin && px <= xMax) { + isPrevInside = true; + } + + // Resolve low and high for range series + if (isRange) { + if (useRaw) { + y = d.slice(1, 3); + } + low = y[0]; + y = y[1]; + } else if (isStacked) { + x = d.x; + y = d.stackY; + low = y - d.y; + } + + isNull = y === null; + + // Optimize for scatter zooming + if (!requireSorting) { + isYInside = y >= yMin && y <= yMax; + } + + if (!isNull && + ( + (x >= xMin && x <= xMax && isYInside) || + (isNextInside || isPrevInside) + )) { + + + clientX = Math.round(xAxis.toPixels(x, true)); + + if (sampling) { + if (minI === undefined || clientX === lastClientX) { + if (!isRange) { + low = y; + } + if (maxI === undefined || y > maxVal) { + maxVal = y; + maxI = i; + } + if (minI === undefined || low < minVal) { + minVal = low; + minI = i; + } + + } + // Add points and reset + if (clientX !== lastClientX) { + if (minI !== undefined) { // maxI also a number + plotY = yAxis.toPixels(maxVal, true); + yBottom = yAxis.toPixels(minVal, true); + drawPoint( + clientX, + hasThreshold ? + Math.min( + plotY, + translatedThreshold + ) : plotY, + hasThreshold ? + Math.max( + yBottom, + translatedThreshold + ) : yBottom, + i + ); + addKDPoint(clientX, plotY, maxI); + if (yBottom !== plotY) { + addKDPoint(clientX, yBottom, minI); + } + } + + minI = maxI = undefined; + lastClientX = clientX; + } + } else { + plotY = Math.round(yAxis.toPixels(y, true)); + drawPoint(clientX, plotY, yBottom, i); + addKDPoint(clientX, plotY, i); + } + } + wasNull = isNull && !connectNulls; + + if (i % CHUNK_SIZE === 0) { + if (series.boostCopy || series.chart.boostCopy) { + (series.boostCopy || series.chart.boostCopy)(); + } + } + } + + return !chartDestroyed; + }, function () { + var loadingDiv = chart.loadingDiv, + loadingShown = chart.loadingShown; + stroke(); + + // if (series.boostCopy || series.chart.boostCopy) { + // (series.boostCopy || series.chart.boostCopy)(); + // } + + series.canvasToSVG(); + + if (boostSettings.timeRendering) { + console.timeEnd('canvas rendering'); // eslint-disable-line no-console + } + + fireEvent(series, 'renderedCanvas'); + + // Do not use chart.hideLoading, as it runs JS animation and + // will be blocked by buildKDTree. CSS animation looks good, but + // then it must be deleted in timeout. If we add the module to + // core, change hideLoading so we can skip this block. + if (loadingShown) { + extend(loadingDiv.style, { + transition: 'opacity 250ms', + opacity: 0 + }); + chart.loadingShown = false; + destroyLoadingDiv = setTimeout(function () { + if (loadingDiv.parentNode) { // In exporting it is falsy + loadingDiv.parentNode.removeChild(loadingDiv); + } + chart.loadingDiv = chart.loadingSpan = null; + }, 250); + } + + // Go back to prototype, ready to build + delete series.buildKDTree; + + series.buildKDTree(); + + // Don't do async on export, the exportChart, getSVGForExport and + // getSVG methods are not chained for it. + }, chart.renderer.forExport ? Number.MAX_VALUE : undefined); + } + }); + + seriesTypes.scatter.prototype.cvsMarkerCircle = function ( + ctx, + clientX, + plotY, + r + ) { + ctx.moveTo(clientX, plotY); + ctx.arc(clientX, plotY, r, 0, 2 * Math.PI, false); + }; + + // Rect is twice as fast as arc, should be used for small markers + seriesTypes.scatter.prototype.cvsMarkerSquare = function ( + ctx, + clientX, + plotY, + r + ) { + ctx.rect(clientX - r, plotY - r, r * 2, r * 2); + }; + seriesTypes.scatter.prototype.fill = true; + + if (seriesTypes.bubble) { + seriesTypes.bubble.prototype.cvsMarkerCircle = function ( + ctx, + clientX, + plotY, + r, + i + ) { + ctx.moveTo(clientX, plotY); + ctx.arc( + clientX, + plotY, + this.radii && this.radii[i], 0, 2 * Math.PI, + false + ); + }; + seriesTypes.bubble.prototype.cvsStrokeBatch = 1; + } + + extend(seriesTypes.area.prototype, { + cvsDrawPoint: function (ctx, clientX, plotY, yBottom, lastPoint) { + if (lastPoint && clientX !== lastPoint.clientX) { + ctx.moveTo(lastPoint.clientX, lastPoint.yBottom); + ctx.lineTo(lastPoint.clientX, lastPoint.plotY); + ctx.lineTo(clientX, plotY); + ctx.lineTo(clientX, yBottom); + } + }, + fill: true, + fillOpacity: true, + sampling: true + }); + + extend(seriesTypes.column.prototype, { + cvsDrawPoint: function (ctx, clientX, plotY, yBottom) { + ctx.rect(clientX - 1, plotY, 1, yBottom - plotY); + }, + fill: true, + sampling: true + }); + + H.Chart.prototype.callbacks.push(function (chart) { + function canvasToSVG() { + if (chart.boostCopy) { + chart.boostCopy(); + } + } + + function clear() { + if (chart.renderTarget) { + chart.renderTarget.attr({ href: '' }); + } + + if (chart.canvas) { + chart.canvas.getContext('2d').clearRect( + 0, + 0, + chart.canvas.width, + chart.canvas.height + ); + } + } + + addEvent(chart, 'predraw', clear); + addEvent(chart, 'render', canvasToSVG); + }); }; diff --git a/js/modules/boost.src.js b/js/modules/boost.src.js index 2038e703d0e..737a6c84e48 100644 --- a/js/modules/boost.src.js +++ b/js/modules/boost.src.js @@ -23,10 +23,10 @@ * - Marker shapes are not supported: markers will always be circles * * Optimizing tips for users - * - Set extremes (min, max) explicitly on the axes in order for Highcharts to + * - Set extremes (min, max) explicitly on the axes in order for Highcharts to * avoid computing extremes. * - Set enableMouseTracking to false on the series to improve total rendering - * time. + * time. * - The default threshold is set based on one series. If you have multiple, * dense series, the combined number of points drawn gets higher, and you may * want to set the threshold lower in order to use optimizations. @@ -40,9 +40,9 @@ * errors, so your millage may vary. * * Settings - * There are two ways of setting the boost threshold: - * - Per series: boost based on number of points in individual series - * - Per chart: boost based on the number of series + * There are two ways of setting the boost threshold: + * - Per series: boost based on number of points in individual series + * - Per chart: boost based on the number of series * * To set the series boost threshold, set seriesBoostThreshold on the chart * object. @@ -51,16 +51,16 @@ * * In addition, the following can be set in the boost object: * { - * //Wether or not to use alpha blending - * useAlpha: boolean - default: true - * //Set to true to perform translations on the GPU. - * //Much faster, but may cause rendering issues - * //when using values far from 0 due to floating point - * //rounding issues - * useGPUTranslations: boolean - default: false - * //Use pre-allocated buffers, much faster, - * //but may cause rendering issues with some data sets - * usePreallocated: boolean - default: false + * //Wether or not to use alpha blending + * useAlpha: boolean - default: true + * //Set to true to perform translations on the GPU. + * //Much faster, but may cause rendering issues + * //when using values far from 0 due to floating point + * //rounding issues + * useGPUTranslations: boolean - default: false + * //Use pre-allocated buffers, much faster, + * //but may cause rendering issues with some data sets + * usePreallocated: boolean - default: false * } */ @@ -264,188 +264,188 @@ import '../parts/Point.js'; import '../parts/Interaction.js'; var win = H.win, - doc = win.document, - noop = function () {}, - Chart = H.Chart, - Color = H.Color, - Series = H.Series, - seriesTypes = H.seriesTypes, - each = H.each, - extend = H.extend, - addEvent = H.addEvent, - fireEvent = H.fireEvent, - grep = H.grep, - isNumber = H.isNumber, - merge = H.merge, - pick = H.pick, - wrap = H.wrap, - plotOptions = H.getOptions().plotOptions, - CHUNK_SIZE = 30000, - mainCanvas = doc.createElement('canvas'), - index, - boostable = [ - 'area', - 'arearange', - 'column', - 'columnrange', - 'bar', - 'line', - 'scatter', - 'heatmap', - 'bubble', - 'treemap' - ], - boostableMap = {}; + doc = win.document, + noop = function () {}, + Chart = H.Chart, + Color = H.Color, + Series = H.Series, + seriesTypes = H.seriesTypes, + each = H.each, + extend = H.extend, + addEvent = H.addEvent, + fireEvent = H.fireEvent, + grep = H.grep, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + wrap = H.wrap, + plotOptions = H.getOptions().plotOptions, + CHUNK_SIZE = 30000, + mainCanvas = doc.createElement('canvas'), + index, + boostable = [ + 'area', + 'arearange', + 'column', + 'columnrange', + 'bar', + 'line', + 'scatter', + 'heatmap', + 'bubble', + 'treemap' + ], + boostableMap = {}; each(boostable, function (item) { - boostableMap[item] = 1; + boostableMap[item] = 1; }); // Register color names since GL can't render those directly. Color.prototype.names = { - aliceblue: '#f0f8ff', - antiquewhite: '#faebd7', - aqua: '#00ffff', - aquamarine: '#7fffd4', - azure: '#f0ffff', - beige: '#f5f5dc', - bisque: '#ffe4c4', - black: '#000000', - blanchedalmond: '#ffebcd', - blue: '#0000ff', - blueviolet: '#8a2be2', - brown: '#a52a2a', - burlywood: '#deb887', - cadetblue: '#5f9ea0', - chartreuse: '#7fff00', - chocolate: '#d2691e', - coral: '#ff7f50', - cornflowerblue: '#6495ed', - cornsilk: '#fff8dc', - crimson: '#dc143c', - cyan: '#00ffff', - darkblue: '#00008b', - darkcyan: '#008b8b', - darkgoldenrod: '#b8860b', - darkgray: '#a9a9a9', - darkgreen: '#006400', - darkkhaki: '#bdb76b', - darkmagenta: '#8b008b', - darkolivegreen: '#556b2f', - darkorange: '#ff8c00', - darkorchid: '#9932cc', - darkred: '#8b0000', - darksalmon: '#e9967a', - darkseagreen: '#8fbc8f', - darkslateblue: '#483d8b', - darkslategray: '#2f4f4f', - darkturquoise: '#00ced1', - darkviolet: '#9400d3', - deeppink: '#ff1493', - deepskyblue: '#00bfff', - dimgray: '#696969', - dodgerblue: '#1e90ff', - feldspar: '#d19275', - firebrick: '#b22222', - floralwhite: '#fffaf0', - forestgreen: '#228b22', - fuchsia: '#ff00ff', - gainsboro: '#dcdcdc', - ghostwhite: '#f8f8ff', - gold: '#ffd700', - goldenrod: '#daa520', - gray: '#808080', - green: '#008000', - greenyellow: '#adff2f', - honeydew: '#f0fff0', - hotpink: '#ff69b4', - indianred: '#cd5c5c', - indigo: '#4b0082', - ivory: '#fffff0', - khaki: '#f0e68c', - lavender: '#e6e6fa', - lavenderblush: '#fff0f5', - lawngreen: '#7cfc00', - lemonchiffon: '#fffacd', - lightblue: '#add8e6', - lightcoral: '#f08080', - lightcyan: '#e0ffff', - lightgoldenrodyellow: '#fafad2', - lightgrey: '#d3d3d3', - lightgreen: '#90ee90', - lightpink: '#ffb6c1', - lightsalmon: '#ffa07a', - lightseagreen: '#20b2aa', - lightskyblue: '#87cefa', - lightslateblue: '#8470ff', - lightslategray: '#778899', - lightsteelblue: '#b0c4de', - lightyellow: '#ffffe0', - lime: '#00ff00', - limegreen: '#32cd32', - linen: '#faf0e6', - magenta: '#ff00ff', - maroon: '#800000', - mediumaquamarine: '#66cdaa', - mediumblue: '#0000cd', - mediumorchid: '#ba55d3', - mediumpurple: '#9370d8', - mediumseagreen: '#3cb371', - mediumslateblue: '#7b68ee', - mediumspringgreen: '#00fa9a', - mediumturquoise: '#48d1cc', - mediumvioletred: '#c71585', - midnightblue: '#191970', - mintcream: '#f5fffa', - mistyrose: '#ffe4e1', - moccasin: '#ffe4b5', - navajowhite: '#ffdead', - navy: '#000080', - oldlace: '#fdf5e6', - olive: '#808000', - olivedrab: '#6b8e23', - orange: '#ffa500', - orangered: '#ff4500', - orchid: '#da70d6', - palegoldenrod: '#eee8aa', - palegreen: '#98fb98', - paleturquoise: '#afeeee', - palevioletred: '#d87093', - papayawhip: '#ffefd5', - peachpuff: '#ffdab9', - peru: '#cd853f', - pink: '#ffc0cb', - plum: '#dda0dd', - powderblue: '#b0e0e6', - purple: '#800080', - red: '#ff0000', - rosybrown: '#bc8f8f', - royalblue: '#4169e1', - saddlebrown: '#8b4513', - salmon: '#fa8072', - sandybrown: '#f4a460', - seagreen: '#2e8b57', - seashell: '#fff5ee', - sienna: '#a0522d', - silver: '#c0c0c0', - skyblue: '#87ceeb', - slateblue: '#6a5acd', - slategray: '#708090', - snow: '#fffafa', - springgreen: '#00ff7f', - steelblue: '#4682b4', - tan: '#d2b48c', - teal: '#008080', - thistle: '#d8bfd8', - tomato: '#ff6347', - turquoise: '#40e0d0', - violet: '#ee82ee', - violetred: '#d02090', - wheat: '#f5deb3', - white: '#ffffff', - whitesmoke: '#f5f5f5', - yellow: '#ffff00', - yellowgreen: '#9acd32' + aliceblue: '#f0f8ff', + antiquewhite: '#faebd7', + aqua: '#00ffff', + aquamarine: '#7fffd4', + azure: '#f0ffff', + beige: '#f5f5dc', + bisque: '#ffe4c4', + black: '#000000', + blanchedalmond: '#ffebcd', + blue: '#0000ff', + blueviolet: '#8a2be2', + brown: '#a52a2a', + burlywood: '#deb887', + cadetblue: '#5f9ea0', + chartreuse: '#7fff00', + chocolate: '#d2691e', + coral: '#ff7f50', + cornflowerblue: '#6495ed', + cornsilk: '#fff8dc', + crimson: '#dc143c', + cyan: '#00ffff', + darkblue: '#00008b', + darkcyan: '#008b8b', + darkgoldenrod: '#b8860b', + darkgray: '#a9a9a9', + darkgreen: '#006400', + darkkhaki: '#bdb76b', + darkmagenta: '#8b008b', + darkolivegreen: '#556b2f', + darkorange: '#ff8c00', + darkorchid: '#9932cc', + darkred: '#8b0000', + darksalmon: '#e9967a', + darkseagreen: '#8fbc8f', + darkslateblue: '#483d8b', + darkslategray: '#2f4f4f', + darkturquoise: '#00ced1', + darkviolet: '#9400d3', + deeppink: '#ff1493', + deepskyblue: '#00bfff', + dimgray: '#696969', + dodgerblue: '#1e90ff', + feldspar: '#d19275', + firebrick: '#b22222', + floralwhite: '#fffaf0', + forestgreen: '#228b22', + fuchsia: '#ff00ff', + gainsboro: '#dcdcdc', + ghostwhite: '#f8f8ff', + gold: '#ffd700', + goldenrod: '#daa520', + gray: '#808080', + green: '#008000', + greenyellow: '#adff2f', + honeydew: '#f0fff0', + hotpink: '#ff69b4', + indianred: '#cd5c5c', + indigo: '#4b0082', + ivory: '#fffff0', + khaki: '#f0e68c', + lavender: '#e6e6fa', + lavenderblush: '#fff0f5', + lawngreen: '#7cfc00', + lemonchiffon: '#fffacd', + lightblue: '#add8e6', + lightcoral: '#f08080', + lightcyan: '#e0ffff', + lightgoldenrodyellow: '#fafad2', + lightgrey: '#d3d3d3', + lightgreen: '#90ee90', + lightpink: '#ffb6c1', + lightsalmon: '#ffa07a', + lightseagreen: '#20b2aa', + lightskyblue: '#87cefa', + lightslateblue: '#8470ff', + lightslategray: '#778899', + lightsteelblue: '#b0c4de', + lightyellow: '#ffffe0', + lime: '#00ff00', + limegreen: '#32cd32', + linen: '#faf0e6', + magenta: '#ff00ff', + maroon: '#800000', + mediumaquamarine: '#66cdaa', + mediumblue: '#0000cd', + mediumorchid: '#ba55d3', + mediumpurple: '#9370d8', + mediumseagreen: '#3cb371', + mediumslateblue: '#7b68ee', + mediumspringgreen: '#00fa9a', + mediumturquoise: '#48d1cc', + mediumvioletred: '#c71585', + midnightblue: '#191970', + mintcream: '#f5fffa', + mistyrose: '#ffe4e1', + moccasin: '#ffe4b5', + navajowhite: '#ffdead', + navy: '#000080', + oldlace: '#fdf5e6', + olive: '#808000', + olivedrab: '#6b8e23', + orange: '#ffa500', + orangered: '#ff4500', + orchid: '#da70d6', + palegoldenrod: '#eee8aa', + palegreen: '#98fb98', + paleturquoise: '#afeeee', + palevioletred: '#d87093', + papayawhip: '#ffefd5', + peachpuff: '#ffdab9', + peru: '#cd853f', + pink: '#ffc0cb', + plum: '#dda0dd', + powderblue: '#b0e0e6', + purple: '#800080', + red: '#ff0000', + rosybrown: '#bc8f8f', + royalblue: '#4169e1', + saddlebrown: '#8b4513', + salmon: '#fa8072', + sandybrown: '#f4a460', + seagreen: '#2e8b57', + seashell: '#fff5ee', + sienna: '#a0522d', + silver: '#c0c0c0', + skyblue: '#87ceeb', + slateblue: '#6a5acd', + slategray: '#708090', + snow: '#fffafa', + springgreen: '#00ff7f', + steelblue: '#4682b4', + tan: '#d2b48c', + teal: '#008080', + thistle: '#d8bfd8', + tomato: '#ff6347', + turquoise: '#40e0d0', + violet: '#ee82ee', + violetred: '#d02090', + wheat: '#f5deb3', + white: '#ffffff', + whitesmoke: '#f5f5f5', + yellow: '#ffff00', + yellowgreen: '#9acd32' }; /** @@ -453,24 +453,24 @@ Color.prototype.names = { * @return {number} max value */ function patientMax() { - var args = Array.prototype.slice.call(arguments), - r = -Number.MAX_VALUE; - - each(args, function (t) { - if ( - typeof t !== 'undefined' && - t !== null && - typeof t.length !== 'undefined' - ) { - // r = r < t.length ? t.length : r; - if (t.length > 0) { - r = t.length; - return true; - } - } - }); - - return r; + var args = Array.prototype.slice.call(arguments), + r = -Number.MAX_VALUE; + + each(args, function (t) { + if ( + typeof t !== 'undefined' && + t !== null && + typeof t.length !== 'undefined' + ) { + // r = r < t.length ? t.length : r; + if (t.length > 0) { + r = t.length; + return true; + } + } + }); + + return r; } /* @@ -483,49 +483,49 @@ function patientMax() { * */ function shouldForceChartSeriesBoosting(chart) { - // If there are more than five series currently boosting, - // we should boost the whole chart to avoid running out of webgl contexts. - var sboostCount = 0, - canBoostCount = 0, - allowBoostForce = pick( - chart.options.boost && chart.options.boost.allowForce, - true - ), - series; - - if (typeof chart.boostForceChartBoost !== 'undefined') { - return chart.boostForceChartBoost; - } - - if (chart.series.length > 1) { - for (var i = 0; i < chart.series.length; i++) { - - series = chart.series[i]; - - if (boostableMap[series.type]) { - ++canBoostCount; - } - - if (patientMax( - series.processedXData, - series.options.data, - // series.xData, - series.points - ) >= (series.options.boostThreshold || Number.MAX_VALUE)) { - ++sboostCount; - } - } - } - - chart.boostForceChartBoost = - ( - allowBoostForce && - canBoostCount === chart.series.length && - sboostCount > 0 - ) || - sboostCount > 5; - - return chart.boostForceChartBoost; + // If there are more than five series currently boosting, + // we should boost the whole chart to avoid running out of webgl contexts. + var sboostCount = 0, + canBoostCount = 0, + allowBoostForce = pick( + chart.options.boost && chart.options.boost.allowForce, + true + ), + series; + + if (typeof chart.boostForceChartBoost !== 'undefined') { + return chart.boostForceChartBoost; + } + + if (chart.series.length > 1) { + for (var i = 0; i < chart.series.length; i++) { + + series = chart.series[i]; + + if (boostableMap[series.type]) { + ++canBoostCount; + } + + if (patientMax( + series.processedXData, + series.options.data, + // series.xData, + series.points + ) >= (series.options.boostThreshold || Number.MAX_VALUE)) { + ++sboostCount; + } + } + } + + chart.boostForceChartBoost = + ( + allowBoostForce && + canBoostCount === chart.series.length && + sboostCount > 0 + ) || + sboostCount > 5; + + return chart.boostForceChartBoost; } /* @@ -534,16 +534,16 @@ function shouldForceChartSeriesBoosting(chart) { * @returns {Boolean} - true if the chart is in series boost mode */ Chart.prototype.isChartSeriesBoosting = function () { - var isSeriesBoosting, - threshold = pick( - this.options.boost && this.options.boost.seriesThreshold, - 50 - ); + var isSeriesBoosting, + threshold = pick( + this.options.boost && this.options.boost.seriesThreshold, + 50 + ); - isSeriesBoosting = threshold <= this.series.length || - shouldForceChartSeriesBoosting(this); + isSeriesBoosting = threshold <= this.series.length || + shouldForceChartSeriesBoosting(this); - return isSeriesBoosting; + return isSeriesBoosting; }; /* @@ -552,24 +552,24 @@ Chart.prototype.isChartSeriesBoosting = function () { * Highstock panes and navigator. */ Chart.prototype.getBoostClipRect = function (target) { - var clipBox = { - x: this.plotLeft, - y: this.plotTop, - width: this.plotWidth, - height: this.plotHeight - }; - - if (target === this) { - each(this.yAxis, function (yAxis) { - clipBox.y = Math.min(yAxis.pos, clipBox.y); - clipBox.height = Math.max( - yAxis.pos - this.plotTop + yAxis.len, - clipBox.height - ); - }, this); - } - - return clipBox; + var clipBox = { + x: this.plotLeft, + y: this.plotTop, + width: this.plotWidth, + height: this.plotHeight + }; + + if (target === this) { + each(this.yAxis, function (yAxis) { + clipBox.y = Math.min(yAxis.pos, clipBox.y); + clipBox.height = Math.max( + yAxis.pos - this.plotTop + yAxis.len, + clipBox.height + ); + }, this); + } + + return clipBox; }; /* @@ -579,16 +579,16 @@ Chart.prototype.getBoostClipRect = function (target) { */ /* function isSeriesBoosting(series, overrideThreshold) { - return isChartSeriesBoosting(series.chart) || - patientMax( - series.processedXData, - series.options.data, - series.points - ) >= ( - overrideThreshold || - series.options.boostThreshold || - Number.MAX_VALUE - ); + return isChartSeriesBoosting(series.chart) || + patientMax( + series.processedXData, + series.options.data, + series.points + ) >= ( + overrideThreshold || + series.options.boostThreshold || + Number.MAX_VALUE + ); } */ @@ -599,457 +599,457 @@ function isSeriesBoosting(series, overrideThreshold) { * @param gl {WebGLContext} - the context in which the shader is active */ function GLShader(gl) { - var vertShade = [ - /* eslint-disable */ - '#version 100', - 'precision highp float;', - - 'attribute vec4 aVertexPosition;', - 'attribute vec4 aColor;', - - 'varying highp vec2 position;', - 'varying highp vec4 vColor;', - - 'uniform mat4 uPMatrix;', - 'uniform float pSize;', - - 'uniform float translatedThreshold;', - 'uniform bool hasThreshold;', - - 'uniform bool skipTranslation;', - - 'uniform float plotHeight;', - - 'uniform float xAxisTrans;', - 'uniform float xAxisMin;', - 'uniform float xAxisMinPad;', - 'uniform float xAxisPointRange;', - 'uniform float xAxisLen;', - 'uniform bool xAxisPostTranslate;', - 'uniform float xAxisOrdinalSlope;', - 'uniform float xAxisOrdinalOffset;', - 'uniform float xAxisPos;', - 'uniform bool xAxisCVSCoord;', - - 'uniform float yAxisTrans;', - 'uniform float yAxisMin;', - 'uniform float yAxisMinPad;', - 'uniform float yAxisPointRange;', - 'uniform float yAxisLen;', - 'uniform bool yAxisPostTranslate;', - 'uniform float yAxisOrdinalSlope;', - 'uniform float yAxisOrdinalOffset;', - 'uniform float yAxisPos;', - 'uniform bool yAxisCVSCoord;', - - 'uniform bool isBubble;', - 'uniform bool bubbleSizeByArea;', - 'uniform float bubbleZMin;', - 'uniform float bubbleZMax;', - 'uniform float bubbleZThreshold;', - 'uniform float bubbleMinSize;', - 'uniform float bubbleMaxSize;', - 'uniform bool bubbleSizeAbs;', - 'uniform bool isInverted;', - - 'float bubbleRadius(){', - 'float value = aVertexPosition.w;', - 'float zMax = bubbleZMax;', - 'float zMin = bubbleZMin;', - 'float radius = 0.0;', - 'float pos = 0.0;', - 'float zRange = zMax - zMin;', - - 'if (bubbleSizeAbs){', - 'value = value - bubbleZThreshold;', - 'zMax = max(zMax - bubbleZThreshold, zMin - bubbleZThreshold);', - 'zMin = 0.0;', - '}', - - 'if (value < zMin){', - 'radius = bubbleZMin / 2.0 - 1.0;', - '} else {', - 'pos = zRange > 0.0 ? (value - zMin) / zRange : 0.5;', - 'if (bubbleSizeByArea && pos > 0.0){', - 'pos = sqrt(pos);', - '}', - 'radius = ceil(bubbleMinSize + pos * (bubbleMaxSize - bubbleMinSize)) / 2.0;', - '}', - - 'return radius * 2.0;', - '}', - - 'float translate(float val,', - 'float pointPlacement,', - 'float localA,', - 'float localMin,', - 'float minPixelPadding,', - 'float pointRange,', - 'float len,', - 'bool cvsCoord', - '){', - - 'float sign = 1.0;', - 'float cvsOffset = 0.0;', - - 'if (cvsCoord) {', - 'sign *= -1.0;', - 'cvsOffset = len;', - '}', - - 'return sign * (val - localMin) * localA + cvsOffset + ', - '(sign * minPixelPadding);',//' + localA * pointPlacement * pointRange;', - '}', - - 'float xToPixels(float value){', - 'if (skipTranslation){', - 'return value;// + xAxisPos;', - '}', - - 'return translate(value, 0.0, xAxisTrans, xAxisMin, xAxisMinPad, xAxisPointRange, xAxisLen, xAxisCVSCoord);// + xAxisPos;', - '}', - - 'float yToPixels(float value, float checkTreshold){', - 'float v;', - 'if (skipTranslation){', - 'v = value;// + yAxisPos;', - '} else {', - 'v = translate(value, 0.0, yAxisTrans, yAxisMin, yAxisMinPad, yAxisPointRange, yAxisLen, yAxisCVSCoord);// + yAxisPos;', - 'if (v > plotHeight) {', - 'v = plotHeight;', - '}', - '}', - 'if (checkTreshold > 0.0 && hasThreshold) {', - 'v = min(v, translatedThreshold);', - '}', - 'return v;', - '}', - - 'void main(void) {', - 'if (isBubble){', - 'gl_PointSize = bubbleRadius();', - '} else {', - 'gl_PointSize = pSize;', - '}', - //'gl_PointSize = 10.0;', - 'vColor = aColor;', - - 'if (isInverted) {', - 'gl_Position = uPMatrix * vec4(xToPixels(aVertexPosition.y) + yAxisPos, yToPixels(aVertexPosition.x, aVertexPosition.z) + xAxisPos, 0.0, 1.0);', - '} else {', - 'gl_Position = uPMatrix * vec4(xToPixels(aVertexPosition.x) + xAxisPos, yToPixels(aVertexPosition.y, aVertexPosition.z) + yAxisPos, 0.0, 1.0);', - '}', - //'gl_Position = uPMatrix * vec4(aVertexPosition.x, aVertexPosition.y, 0.0, 1.0);', - '}' - /* eslint-enable */ - ].join('\n'), - // Fragment shader source - fragShade = [ - /* eslint-disable */ - 'precision highp float;', - 'uniform vec4 fillColor;', - 'varying highp vec2 position;', - 'varying highp vec4 vColor;', - 'uniform sampler2D uSampler;', - 'uniform bool isCircle;', - 'uniform bool hasColor;', - - // 'vec4 toColor(float value, vec2 point) {', - // 'return vec4(0.0, 0.0, 0.0, 0.0);', - // '}', - - 'void main(void) {', - 'vec4 col = fillColor;', - 'vec4 tcol;', - - 'if (hasColor) {', - 'col = vColor;', - '}', - - 'if (isCircle) {', - 'tcol = texture2D(uSampler, gl_PointCoord.st);', - 'col *= tcol;', - 'if (tcol.r < 0.0) {', - 'discard;', - '} else {', - 'gl_FragColor = col;', - '}', - '} else {', - 'gl_FragColor = col;', - '}', - '}' - /* eslint-enable */ - ].join('\n'), - uLocations = {}, - // The shader program - shaderProgram, - // Uniform handle to the perspective matrix - pUniform, - // Uniform for point size - psUniform, - // Uniform for fill color - fillColorUniform, - // Uniform for isBubble - isBubbleUniform, - // Uniform for bubble abs sizing - bubbleSizeAbsUniform, - bubbleSizeAreaUniform, - // Skip translation uniform - skipTranslationUniform, - // Set to 1 if circle - isCircleUniform, - // Uniform for invertion - isInverted, - plotHeightUniform, - // Texture uniform - uSamplerUniform; - - /* String to shader program - * @param {string} str - the program source - * @param {string} type - the program type: either `vertex` or `fragment` - * @returns {bool|shader} - */ - function stringToProgram(str, type) { - var t = type === 'vertex' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER, - shader = gl.createShader(t) - ; - - gl.shaderSource(shader, str); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - // console.error('shader error:', gl.getShaderInfoLog(shader)); - return false; - } - return shader; - } - - /* - * Create the shader. - * Loads the shader program statically defined above - */ - function createShader() { - var v = stringToProgram(vertShade, 'vertex'), - f = stringToProgram(fragShade, 'fragment') - ; - - if (!v || !f) { - shaderProgram = false; - // console.error('error creating shader program'); - return false; - } - - function uloc(n) { - return gl.getUniformLocation(shaderProgram, n); - } - - shaderProgram = gl.createProgram(); - - gl.attachShader(shaderProgram, v); - gl.attachShader(shaderProgram, f); - gl.linkProgram(shaderProgram); - - gl.useProgram(shaderProgram); - - gl.bindAttribLocation(shaderProgram, 0, 'aVertexPosition'); - - pUniform = uloc('uPMatrix'); - psUniform = uloc('pSize'); - fillColorUniform = uloc('fillColor'); - isBubbleUniform = uloc('isBubble'); - bubbleSizeAbsUniform = uloc('bubbleSizeAbs'); - bubbleSizeAreaUniform = uloc('bubbleSizeByArea'); - uSamplerUniform = uloc('uSampler'); - skipTranslationUniform = uloc('skipTranslation'); - isCircleUniform = uloc('isCircle'); - isInverted = uloc('isInverted'); - plotHeightUniform = uloc('plotHeight'); - return true; - } - - /* - * Destroy the shader - */ - function destroy() { - if (gl) { - if (shaderProgram) { - gl.deleteProgram(shaderProgram); - shaderProgram = false; - } - } - } - - /* - * Bind the shader. - * This makes the shader the active one until another one is bound, - * or until 0 is bound. - */ - function bind() { - gl.useProgram(shaderProgram); - } - - /* - * Set a uniform value. - * This uses a hash map to cache uniform locations. - * @param name {string} - the name of the uniform to set - * @param val {float} - the value to set - */ - function setUniform(name, val) { - var u = uLocations[name] = uLocations[name] || - gl.getUniformLocation(shaderProgram, name); - gl.uniform1f(u, val); - } - - /* - * Set the active texture - * @param texture - the texture - */ - function setTexture() { - gl.uniform1i(uSamplerUniform, 0); - } - - /* - * Set if inversion state - * @flag is the state - */ - function setInverted(flag) { - gl.uniform1i(isInverted, flag); - } - - /* - * Enable/disable circle drawing - */ - function setDrawAsCircle(flag) { - gl.uniform1i(isCircleUniform, flag ? 1 : 0); - } - - function setPlotHeight(n) { - gl.uniform1f(plotHeightUniform, n); - } - - /* - * Flush - */ - function reset() { - gl.uniform1i(isBubbleUniform, 0); - gl.uniform1i(isCircleUniform, 0); - } - - /* - * Set bubble uniforms - * @param series {Highcharts.Series} - the series to use - */ - function setBubbleUniforms(series, zCalcMin, zCalcMax) { - var seriesOptions = series.options, - zMin = Number.MAX_VALUE, - zMax = -Number.MAX_VALUE; - - if (series.type === 'bubble') { - zMin = pick(seriesOptions.zMin, Math.min( - zMin, - Math.max( - zCalcMin, - seriesOptions.displayNegative === false ? - seriesOptions.zThreshold : -Number.MAX_VALUE - ) - )); - - zMax = pick(seriesOptions.zMax, Math.max(zMax, zCalcMax)); - - gl.uniform1i(isBubbleUniform, 1); - gl.uniform1i(isCircleUniform, 1); - gl.uniform1i( - bubbleSizeAreaUniform, - series.options.sizeBy !== 'width' - ); - gl.uniform1i( - bubbleSizeAbsUniform, - series.options.sizeByAbsoluteValue - ); - - setUniform('bubbleZMin', zMin); - setUniform('bubbleZMax', zMax); - setUniform('bubbleZThreshold', series.options.zThreshold); - setUniform('bubbleMinSize', series.minPxSize); - setUniform('bubbleMaxSize', series.maxPxSize); - } - } - - /* - * Set the Color uniform. - * @param color {Array} - an array with RGBA values - */ - function setColor(color) { - gl.uniform4f( - fillColorUniform, - color[0] / 255.0, - color[1] / 255.0, - color[2] / 255.0, - color[3] - ); - } - - /* - * Set skip translation - */ - function setSkipTranslation(flag) { - gl.uniform1i(skipTranslationUniform, flag === true ? 1 : 0); - } - - /* - * Set the perspective matrix - * @param m {Matrix4x4} - the matrix - */ - function setPMatrix(m) { - gl.uniformMatrix4fv(pUniform, false, m); - } - - /* - * Set the point size. - * @param p {float} - point size - */ - function setPointSize(p) { - gl.uniform1f(psUniform, p); - } - - /* - * Get the shader program handle - * @returns {GLInt} - the handle for the program - */ - function getProgram() { - return shaderProgram; - } - - if (gl) { - createShader(); - } - - return { - psUniform: function () { - return psUniform; - }, - pUniform: function () { - return pUniform; - }, - fillColorUniform: function () { - return fillColorUniform; - }, - setPlotHeight: setPlotHeight, - setBubbleUniforms: setBubbleUniforms, - bind: bind, - program: getProgram, - create: createShader, - setUniform: setUniform, - setPMatrix: setPMatrix, - setColor: setColor, - setPointSize: setPointSize, - setSkipTranslation: setSkipTranslation, - setTexture: setTexture, - setDrawAsCircle: setDrawAsCircle, - reset: reset, - setInverted: setInverted, - destroy: destroy - }; + var vertShade = [ + /* eslint-disable */ + '#version 100', + 'precision highp float;', + + 'attribute vec4 aVertexPosition;', + 'attribute vec4 aColor;', + + 'varying highp vec2 position;', + 'varying highp vec4 vColor;', + + 'uniform mat4 uPMatrix;', + 'uniform float pSize;', + + 'uniform float translatedThreshold;', + 'uniform bool hasThreshold;', + + 'uniform bool skipTranslation;', + + 'uniform float plotHeight;', + + 'uniform float xAxisTrans;', + 'uniform float xAxisMin;', + 'uniform float xAxisMinPad;', + 'uniform float xAxisPointRange;', + 'uniform float xAxisLen;', + 'uniform bool xAxisPostTranslate;', + 'uniform float xAxisOrdinalSlope;', + 'uniform float xAxisOrdinalOffset;', + 'uniform float xAxisPos;', + 'uniform bool xAxisCVSCoord;', + + 'uniform float yAxisTrans;', + 'uniform float yAxisMin;', + 'uniform float yAxisMinPad;', + 'uniform float yAxisPointRange;', + 'uniform float yAxisLen;', + 'uniform bool yAxisPostTranslate;', + 'uniform float yAxisOrdinalSlope;', + 'uniform float yAxisOrdinalOffset;', + 'uniform float yAxisPos;', + 'uniform bool yAxisCVSCoord;', + + 'uniform bool isBubble;', + 'uniform bool bubbleSizeByArea;', + 'uniform float bubbleZMin;', + 'uniform float bubbleZMax;', + 'uniform float bubbleZThreshold;', + 'uniform float bubbleMinSize;', + 'uniform float bubbleMaxSize;', + 'uniform bool bubbleSizeAbs;', + 'uniform bool isInverted;', + + 'float bubbleRadius(){', + 'float value = aVertexPosition.w;', + 'float zMax = bubbleZMax;', + 'float zMin = bubbleZMin;', + 'float radius = 0.0;', + 'float pos = 0.0;', + 'float zRange = zMax - zMin;', + + 'if (bubbleSizeAbs){', + 'value = value - bubbleZThreshold;', + 'zMax = max(zMax - bubbleZThreshold, zMin - bubbleZThreshold);', + 'zMin = 0.0;', + '}', + + 'if (value < zMin){', + 'radius = bubbleZMin / 2.0 - 1.0;', + '} else {', + 'pos = zRange > 0.0 ? (value - zMin) / zRange : 0.5;', + 'if (bubbleSizeByArea && pos > 0.0){', + 'pos = sqrt(pos);', + '}', + 'radius = ceil(bubbleMinSize + pos * (bubbleMaxSize - bubbleMinSize)) / 2.0;', + '}', + + 'return radius * 2.0;', + '}', + + 'float translate(float val,', + 'float pointPlacement,', + 'float localA,', + 'float localMin,', + 'float minPixelPadding,', + 'float pointRange,', + 'float len,', + 'bool cvsCoord', + '){', + + 'float sign = 1.0;', + 'float cvsOffset = 0.0;', + + 'if (cvsCoord) {', + 'sign *= -1.0;', + 'cvsOffset = len;', + '}', + + 'return sign * (val - localMin) * localA + cvsOffset + ', + '(sign * minPixelPadding);',//' + localA * pointPlacement * pointRange;', + '}', + + 'float xToPixels(float value){', + 'if (skipTranslation){', + 'return value;// + xAxisPos;', + '}', + + 'return translate(value, 0.0, xAxisTrans, xAxisMin, xAxisMinPad, xAxisPointRange, xAxisLen, xAxisCVSCoord);// + xAxisPos;', + '}', + + 'float yToPixels(float value, float checkTreshold){', + 'float v;', + 'if (skipTranslation){', + 'v = value;// + yAxisPos;', + '} else {', + 'v = translate(value, 0.0, yAxisTrans, yAxisMin, yAxisMinPad, yAxisPointRange, yAxisLen, yAxisCVSCoord);// + yAxisPos;', + 'if (v > plotHeight) {', + 'v = plotHeight;', + '}', + '}', + 'if (checkTreshold > 0.0 && hasThreshold) {', + 'v = min(v, translatedThreshold);', + '}', + 'return v;', + '}', + + 'void main(void) {', + 'if (isBubble){', + 'gl_PointSize = bubbleRadius();', + '} else {', + 'gl_PointSize = pSize;', + '}', + //'gl_PointSize = 10.0;', + 'vColor = aColor;', + + 'if (isInverted) {', + 'gl_Position = uPMatrix * vec4(xToPixels(aVertexPosition.y) + yAxisPos, yToPixels(aVertexPosition.x, aVertexPosition.z) + xAxisPos, 0.0, 1.0);', + '} else {', + 'gl_Position = uPMatrix * vec4(xToPixels(aVertexPosition.x) + xAxisPos, yToPixels(aVertexPosition.y, aVertexPosition.z) + yAxisPos, 0.0, 1.0);', + '}', + //'gl_Position = uPMatrix * vec4(aVertexPosition.x, aVertexPosition.y, 0.0, 1.0);', + '}' + /* eslint-enable */ + ].join('\n'), + // Fragment shader source + fragShade = [ + /* eslint-disable */ + 'precision highp float;', + 'uniform vec4 fillColor;', + 'varying highp vec2 position;', + 'varying highp vec4 vColor;', + 'uniform sampler2D uSampler;', + 'uniform bool isCircle;', + 'uniform bool hasColor;', + + // 'vec4 toColor(float value, vec2 point) {', + // 'return vec4(0.0, 0.0, 0.0, 0.0);', + // '}', + + 'void main(void) {', + 'vec4 col = fillColor;', + 'vec4 tcol;', + + 'if (hasColor) {', + 'col = vColor;', + '}', + + 'if (isCircle) {', + 'tcol = texture2D(uSampler, gl_PointCoord.st);', + 'col *= tcol;', + 'if (tcol.r < 0.0) {', + 'discard;', + '} else {', + 'gl_FragColor = col;', + '}', + '} else {', + 'gl_FragColor = col;', + '}', + '}' + /* eslint-enable */ + ].join('\n'), + uLocations = {}, + // The shader program + shaderProgram, + // Uniform handle to the perspective matrix + pUniform, + // Uniform for point size + psUniform, + // Uniform for fill color + fillColorUniform, + // Uniform for isBubble + isBubbleUniform, + // Uniform for bubble abs sizing + bubbleSizeAbsUniform, + bubbleSizeAreaUniform, + // Skip translation uniform + skipTranslationUniform, + // Set to 1 if circle + isCircleUniform, + // Uniform for invertion + isInverted, + plotHeightUniform, + // Texture uniform + uSamplerUniform; + + /* String to shader program + * @param {string} str - the program source + * @param {string} type - the program type: either `vertex` or `fragment` + * @returns {bool|shader} + */ + function stringToProgram(str, type) { + var t = type === 'vertex' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER, + shader = gl.createShader(t) + ; + + gl.shaderSource(shader, str); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + // console.error('shader error:', gl.getShaderInfoLog(shader)); + return false; + } + return shader; + } + + /* + * Create the shader. + * Loads the shader program statically defined above + */ + function createShader() { + var v = stringToProgram(vertShade, 'vertex'), + f = stringToProgram(fragShade, 'fragment') + ; + + if (!v || !f) { + shaderProgram = false; + // console.error('error creating shader program'); + return false; + } + + function uloc(n) { + return gl.getUniformLocation(shaderProgram, n); + } + + shaderProgram = gl.createProgram(); + + gl.attachShader(shaderProgram, v); + gl.attachShader(shaderProgram, f); + gl.linkProgram(shaderProgram); + + gl.useProgram(shaderProgram); + + gl.bindAttribLocation(shaderProgram, 0, 'aVertexPosition'); + + pUniform = uloc('uPMatrix'); + psUniform = uloc('pSize'); + fillColorUniform = uloc('fillColor'); + isBubbleUniform = uloc('isBubble'); + bubbleSizeAbsUniform = uloc('bubbleSizeAbs'); + bubbleSizeAreaUniform = uloc('bubbleSizeByArea'); + uSamplerUniform = uloc('uSampler'); + skipTranslationUniform = uloc('skipTranslation'); + isCircleUniform = uloc('isCircle'); + isInverted = uloc('isInverted'); + plotHeightUniform = uloc('plotHeight'); + return true; + } + + /* + * Destroy the shader + */ + function destroy() { + if (gl) { + if (shaderProgram) { + gl.deleteProgram(shaderProgram); + shaderProgram = false; + } + } + } + + /* + * Bind the shader. + * This makes the shader the active one until another one is bound, + * or until 0 is bound. + */ + function bind() { + gl.useProgram(shaderProgram); + } + + /* + * Set a uniform value. + * This uses a hash map to cache uniform locations. + * @param name {string} - the name of the uniform to set + * @param val {float} - the value to set + */ + function setUniform(name, val) { + var u = uLocations[name] = uLocations[name] || + gl.getUniformLocation(shaderProgram, name); + gl.uniform1f(u, val); + } + + /* + * Set the active texture + * @param texture - the texture + */ + function setTexture() { + gl.uniform1i(uSamplerUniform, 0); + } + + /* + * Set if inversion state + * @flag is the state + */ + function setInverted(flag) { + gl.uniform1i(isInverted, flag); + } + + /* + * Enable/disable circle drawing + */ + function setDrawAsCircle(flag) { + gl.uniform1i(isCircleUniform, flag ? 1 : 0); + } + + function setPlotHeight(n) { + gl.uniform1f(plotHeightUniform, n); + } + + /* + * Flush + */ + function reset() { + gl.uniform1i(isBubbleUniform, 0); + gl.uniform1i(isCircleUniform, 0); + } + + /* + * Set bubble uniforms + * @param series {Highcharts.Series} - the series to use + */ + function setBubbleUniforms(series, zCalcMin, zCalcMax) { + var seriesOptions = series.options, + zMin = Number.MAX_VALUE, + zMax = -Number.MAX_VALUE; + + if (series.type === 'bubble') { + zMin = pick(seriesOptions.zMin, Math.min( + zMin, + Math.max( + zCalcMin, + seriesOptions.displayNegative === false ? + seriesOptions.zThreshold : -Number.MAX_VALUE + ) + )); + + zMax = pick(seriesOptions.zMax, Math.max(zMax, zCalcMax)); + + gl.uniform1i(isBubbleUniform, 1); + gl.uniform1i(isCircleUniform, 1); + gl.uniform1i( + bubbleSizeAreaUniform, + series.options.sizeBy !== 'width' + ); + gl.uniform1i( + bubbleSizeAbsUniform, + series.options.sizeByAbsoluteValue + ); + + setUniform('bubbleZMin', zMin); + setUniform('bubbleZMax', zMax); + setUniform('bubbleZThreshold', series.options.zThreshold); + setUniform('bubbleMinSize', series.minPxSize); + setUniform('bubbleMaxSize', series.maxPxSize); + } + } + + /* + * Set the Color uniform. + * @param color {Array} - an array with RGBA values + */ + function setColor(color) { + gl.uniform4f( + fillColorUniform, + color[0] / 255.0, + color[1] / 255.0, + color[2] / 255.0, + color[3] + ); + } + + /* + * Set skip translation + */ + function setSkipTranslation(flag) { + gl.uniform1i(skipTranslationUniform, flag === true ? 1 : 0); + } + + /* + * Set the perspective matrix + * @param m {Matrix4x4} - the matrix + */ + function setPMatrix(m) { + gl.uniformMatrix4fv(pUniform, false, m); + } + + /* + * Set the point size. + * @param p {float} - point size + */ + function setPointSize(p) { + gl.uniform1f(psUniform, p); + } + + /* + * Get the shader program handle + * @returns {GLInt} - the handle for the program + */ + function getProgram() { + return shaderProgram; + } + + if (gl) { + createShader(); + } + + return { + psUniform: function () { + return psUniform; + }, + pUniform: function () { + return pUniform; + }, + fillColorUniform: function () { + return fillColorUniform; + }, + setPlotHeight: setPlotHeight, + setBubbleUniforms: setBubbleUniforms, + bind: bind, + program: getProgram, + create: createShader, + setUniform: setUniform, + setPMatrix: setPMatrix, + setColor: setColor, + setPointSize: setPointSize, + setSkipTranslation: setSkipTranslation, + setTexture: setTexture, + setDrawAsCircle: setDrawAsCircle, + reset: reset, + setInverted: setInverted, + destroy: destroy + }; } /* @@ -1060,1400 +1060,1401 @@ function GLShader(gl) { * @param shader {GLShader} - the shader to use */ function GLVertexBuffer(gl, shader, dataComponents /* , type */) { - var buffer = false, - vertAttribute = false, - components = dataComponents || 2, - preAllocated = false, - iterator = 0, - // farray = false, - data; - - // type = type || 'float'; - - function destroy() { - if (buffer) { - gl.deleteBuffer(buffer); - buffer = false; - vertAttribute = false; - } - - iterator = 0; - components = dataComponents || 2; - data = []; - } - - /* - * Build the buffer - * @param dataIn {Array} - a 0 padded array of indices - * @param attrib {String} - the name of the Attribute to bind the buffer to - * @param dataComponents {Integer} - the number of components per. indice - */ - function build(dataIn, attrib, dataComponents) { - var farray; - - data = dataIn || []; - - if ((!data || data.length === 0) && !preAllocated) { - // console.error('trying to render empty vbuffer'); - destroy(); - return false; - } - - components = dataComponents || components; - - if (buffer) { - gl.deleteBuffer(buffer); - } - - if (!preAllocated) { - farray = new Float32Array(data); - } - - buffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.bufferData( - gl.ARRAY_BUFFER, - preAllocated || farray, - gl.STATIC_DRAW - ); - - // gl.bindAttribLocation(shader.program(), 0, 'aVertexPosition'); - vertAttribute = gl.getAttribLocation(shader.program(), attrib); - gl.enableVertexAttribArray(vertAttribute); - - // Trigger cleanup - farray = false; - - return true; - } - - /* - * Bind the buffer - */ - function bind() { - if (!buffer) { - return false; - } - - // gl.bindAttribLocation(shader.program(), 0, 'aVertexPosition'); - // gl.enableVertexAttribArray(vertAttribute); - // gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.vertexAttribPointer( - vertAttribute, components, gl.FLOAT, false, 0, 0 - ); - // gl.enableVertexAttribArray(vertAttribute); - } - - /* - * Render the buffer - * @param from {Integer} - the start indice - * @param to {Integer} - the end indice - * @param drawMode {String} - the draw mode - */ - function render(from, to, drawMode) { - var length = preAllocated ? preAllocated.length : data.length; - - if (!buffer) { - return false; - } - - if (!length) { - return false; - } - - if (!from || from > length || from < 0) { - from = 0; - } - - if (!to || to > length) { - to = length; - } - - drawMode = drawMode || 'points'; - - gl.drawArrays( - gl[drawMode.toUpperCase()], - from / components, - (to - from) / components - ); - - return true; - } - - function push(x, y, a, b) { - if (preAllocated) { // && iterator <= preAllocated.length - 4) { - preAllocated[++iterator] = x; - preAllocated[++iterator] = y; - preAllocated[++iterator] = a; - preAllocated[++iterator] = b; - } - } - - /* - * Note about pre-allocated buffers: - * - This is slower for charts with many series - */ - function allocate(size) { - size *= 4; - iterator = -1; - - preAllocated = new Float32Array(size); - } - - // ///////////////////////////////////////////////////////////////////////// - return { - destroy: destroy, - bind: bind, - data: data, - build: build, - render: render, - allocate: allocate, - push: push - }; + var buffer = false, + vertAttribute = false, + components = dataComponents || 2, + preAllocated = false, + iterator = 0, + // farray = false, + data; + + // type = type || 'float'; + + function destroy() { + if (buffer) { + gl.deleteBuffer(buffer); + buffer = false; + vertAttribute = false; + } + + iterator = 0; + components = dataComponents || 2; + data = []; + } + + /* + * Build the buffer + * @param dataIn {Array} - a 0 padded array of indices + * @param attrib {String} - the name of the Attribute to bind the buffer to + * @param dataComponents {Integer} - the number of components per. indice + */ + function build(dataIn, attrib, dataComponents) { + var farray; + + data = dataIn || []; + + if ((!data || data.length === 0) && !preAllocated) { + // console.error('trying to render empty vbuffer'); + destroy(); + return false; + } + + components = dataComponents || components; + + if (buffer) { + gl.deleteBuffer(buffer); + } + + if (!preAllocated) { + farray = new Float32Array(data); + } + + buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData( + gl.ARRAY_BUFFER, + preAllocated || farray, + gl.STATIC_DRAW + ); + + // gl.bindAttribLocation(shader.program(), 0, 'aVertexPosition'); + vertAttribute = gl.getAttribLocation(shader.program(), attrib); + gl.enableVertexAttribArray(vertAttribute); + + // Trigger cleanup + farray = false; + + return true; + } + + /* + * Bind the buffer + */ + function bind() { + if (!buffer) { + return false; + } + + // gl.bindAttribLocation(shader.program(), 0, 'aVertexPosition'); + // gl.enableVertexAttribArray(vertAttribute); + // gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.vertexAttribPointer( + vertAttribute, components, gl.FLOAT, false, 0, 0 + ); + // gl.enableVertexAttribArray(vertAttribute); + } + + /* + * Render the buffer + * @param from {Integer} - the start indice + * @param to {Integer} - the end indice + * @param drawMode {String} - the draw mode + */ + function render(from, to, drawMode) { + var length = preAllocated ? preAllocated.length : data.length; + + if (!buffer) { + return false; + } + + if (!length) { + return false; + } + + if (!from || from > length || from < 0) { + from = 0; + } + + if (!to || to > length) { + to = length; + } + + drawMode = drawMode || 'points'; + + gl.drawArrays( + gl[drawMode.toUpperCase()], + from / components, + (to - from) / components + ); + + return true; + } + + function push(x, y, a, b) { + if (preAllocated) { // && iterator <= preAllocated.length - 4) { + preAllocated[++iterator] = x; + preAllocated[++iterator] = y; + preAllocated[++iterator] = a; + preAllocated[++iterator] = b; + } + } + + /* + * Note about pre-allocated buffers: + * - This is slower for charts with many series + */ + function allocate(size) { + size *= 4; + iterator = -1; + + preAllocated = new Float32Array(size); + } + + // ///////////////////////////////////////////////////////////////////////// + return { + destroy: destroy, + bind: bind, + data: data, + build: build, + render: render, + allocate: allocate, + push: push + }; } /* Main renderer. Used to render series. - * Notes to self: - * - May be able to build a point map by rendering to a separate canvas - * and encoding values in the color data. - * - Need to figure out a way to transform the data quicker + * Notes to self: + * - May be able to build a point map by rendering to a separate canvas + * and encoding values in the color data. + * - Need to figure out a way to transform the data quicker */ function GLRenderer(postRenderCallback) { - var // Shader - shader = false, - // Vertex buffers - keyed on shader attribute name - vbuffer = false, - // Opengl context - gl = false, - // Width of our viewport in pixels - width = 0, - // Height of our viewport in pixels - height = 0, - // The data to render - array of coordinates - data = false, - // The marker data - markerData = false, - // Is the texture ready? - textureIsReady = false, - // Exports - exports = {}, - // Is it inited? - isInited = false, - // The series stack - series = [], - // Texture for circles - circleTexture = doc.createElement('canvas'), - // Context for circle texture - circleCtx = circleTexture.getContext('2d'), - // Handle for the circle texture - circleTextureHandle, - // Things to draw as "rectangles" (i.e lines) - asBar = { - 'column': true, - 'columnrange': true, - 'bar': true, - 'area': true, - 'arearange': true - }, - asCircle = { - 'scatter': true, - 'bubble': true - }, - // Render settings - settings = { - pointSize: 1, - lineWidth: 1, - fillColor: '#AA00AA', - useAlpha: true, - usePreallocated: false, - useGPUTranslations: false, - debug: { - timeRendering: false, - timeSeriesProcessing: false, - timeSetup: false, - timeBufferCopy: false, - timeKDTree: false, - showSkipSummary: false - } - }; - - // ///////////////////////////////////////////////////////////////////////// - - function setOptions(options) { - merge(true, settings, options); - } - - function seriesPointCount(series) { - var isStacked, - xData, - s; - - if (series.isSeriesBoosting) { - isStacked = !!series.options.stacking; - xData = ( - series.xData || - series.options.xData || - series.processedXData - ); - s = (isStacked ? series.data : (xData || series.options.data)) - .length; - - if (series.type === 'treemap') { - s *= 12; - } else if (series.type === 'heatmap') { - s *= 6; - } else if (asBar[series.type]) { - s *= 2; - } - - return s; - } - - return 0; - } - - /* Allocate a float buffer to fit all series */ - function allocateBuffer(chart) { - var s = 0; - - if (!settings.usePreallocated) { - return; - } - - each(chart.series, function (series) { - if (series.isSeriesBoosting) { - s += seriesPointCount(series); - } - }); - - vbuffer.allocate(s); - } - - function allocateBufferForSingleSeries(series) { - var s = 0; - - if (!settings.usePreallocated) { - return; - } - - if (series.isSeriesBoosting) { - s = seriesPointCount(series); - } - - vbuffer.allocate(s); - } - - /* - * Returns an orthographic perspective matrix - * @param {number} width - the width of the viewport in pixels - * @param {number} height - the height of the viewport in pixels - */ - function orthoMatrix(width, height) { - var near = 0, - far = 1; - - return [ - 2 / width, 0, 0, 0, - 0, -(2 / height), 0, 0, - 0, 0, -2 / (far - near), 0, - -1, 1, -(far + near) / (far - near), 1 - ]; - } - - /* - * Clear the depth and color buffer - */ - function clear() { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - } - - /* - * Get the WebGL context - * @returns {WebGLContext} - the context - */ - function getGL() { - return gl; - } - - /* - * Push data for a single series - * This calculates additional vertices and transforms the data to be - * aligned correctly in memory - */ - function pushSeriesData(series, inst) { - var isRange = series.pointArrayMap && - series.pointArrayMap.join(',') === 'low,high', - chart = series.chart, - options = series.options, - isStacked = !!options.stacking, - rawData = options.data, - xExtremes = series.xAxis.getExtremes(), - xMin = xExtremes.min, - xMax = xExtremes.max, - yExtremes = series.yAxis.getExtremes(), - yMin = yExtremes.min, - yMax = yExtremes.max, - xData = series.xData || options.xData || series.processedXData, - yData = series.yData || options.yData || series.processedYData, - zData = series.zData || options.zData || series.processedZData, - yAxis = series.yAxis, - xAxis = series.xAxis, - plotHeight = series.chart.plotHeight, - plotWidth = series.chart.plotWidth, - useRaw = !xData || xData.length === 0, - // threshold = options.threshold, - // yBottom = chart.yAxis[0].getThreshold(threshold), - // hasThreshold = isNumber(threshold), - // colorByPoint = series.options.colorByPoint, - // This is required for color by point, so make sure this is - // uncommented if enabling that - // colorIndex = 0, - // Required for color axis support - // caxis, - connectNulls = options.connectNulls, - // For some reason eslint doesn't pick up that this is actually used - maxVal, // eslint-disable-line no-unused-vars - points = series.points || false, - lastX = false, - lastY = false, - minVal, - color, - scolor, - sdata = isStacked ? series.data : (xData || rawData), - closestLeft = { x: -Number.MAX_VALUE, y: 0 }, - closestRight = { x: Number.MIN_VALUE, y: 0 }, - - skipped = 0, - - cullXThreshold = 1, - cullYThreshold = 1, - - // The following are used in the builder while loop - x, - y, - d, - z, - i = -1, - px = false, - nx = false, - // This is in fact used. - low, // eslint-disable-line no-unused-vars - chartDestroyed = typeof chart.index === 'undefined', - nextInside = false, - prevInside = false, - pcolor = false, - drawAsBar = asBar[series.type], - isXInside = false, - isYInside = true; - - if (options.boostData && options.boostData.length > 0) { - return; - } - - if (chart.inverted) { - plotHeight = series.chart.plotWidth; - plotWidth = series.chart.plotHeight; - } - - series.closestPointRangePx = Number.MAX_VALUE; - - // Push color to color buffer - need to do this per. vertex - function pushColor(color) { - if (color) { - inst.colorData.push(color[0]); - inst.colorData.push(color[1]); - inst.colorData.push(color[2]); - inst.colorData.push(color[3]); - } - } - - // Push a vertice to the data buffer - function vertice(x, y, checkTreshold, pointSize, color) { - pushColor(color); - if (settings.usePreallocated) { - vbuffer.push(x, y, checkTreshold ? 1 : 0, pointSize || 1); - } else { - data.push(x); - data.push(y); - data.push(checkTreshold ? 1 : 0); - data.push(pointSize || 1); - } - } - - function closeSegment() { - if (inst.segments.length) { - inst.segments[inst.segments.length - 1].to = data.length; - } - } - - // Create a new segment for the current set - function beginSegment() { - // Insert a segment on the series. - // A segment is just a start indice. - // When adding a segment, if one exists from before, it should - // set the previous segment's end - - if (inst.segments.length && - inst.segments[inst.segments.length - 1].from === data.length - ) { - return; - } - - closeSegment(); - - inst.segments.push({ - from: data.length - }); - - } - - // Push a rectangle to the data buffer - function pushRect(x, y, w, h, color) { - pushColor(color); - vertice(x + w, y); - pushColor(color); - vertice(x, y); - pushColor(color); - vertice(x, y + h); - - pushColor(color); - vertice(x, y + h); - pushColor(color); - vertice(x + w, y + h); - pushColor(color); - vertice(x + w, y); - } - - // Create the first segment - beginSegment(); - - // Special case for point shapes - if (points && points.length > 0) { - - // If we're doing points, we assume that the points are already - // translated, so we skip the shader translation. - inst.skipTranslation = true; - // Force triangle draw mode - inst.drawMode = 'triangles'; - - // We don't have a z component in the shader, so we need to sort. - if (points[0].node && points[0].node.levelDynamic) { - points.sort(function (a, b) { - if (a.node) { - if (a.node.levelDynamic > b.node.levelDynamic) { - return 1; - } else if (a.node.levelDynamic < b.node.levelDynamic) { - return -1; - } - } - return 0; - }); - } - - each(points, function (point) { - var plotY = point.plotY, - shapeArgs, - swidth, - pointAttr; - - if ( - typeof plotY !== 'undefined' && - !isNaN(plotY) && - point.y !== null - ) { - shapeArgs = point.shapeArgs; - - /*= if (build.classic) { =*/ - pointAttr = point.series.pointAttribs(point); - /*= } else { =*/ - pointAttr = point.series.colorAttribs(point); - /*= } =*/ - swidth = pointAttr['stroke-width'] || 0; - - // Handle point colors - color = H.color(pointAttr.fill).rgba; - color[0] /= 255.0; - color[1] /= 255.0; - color[2] /= 255.0; - - // So there are two ways of doing this. Either we can - // create a rectangle of two triangles, or we can do a - // point and use point size. Latter is faster, but - // only supports squares. So we're doing triangles. - // We could also use one color per. vertice to get - // better color interpolation. - - // If there's stroking, we do an additional rect - if (series.type === 'treemap') { - swidth = swidth || 1; - scolor = H.color(pointAttr.stroke).rgba; - - scolor[0] /= 255.0; - scolor[1] /= 255.0; - scolor[2] /= 255.0; - - pushRect( - shapeArgs.x, - shapeArgs.y, - shapeArgs.width, - shapeArgs.height, - scolor - ); - - swidth /= 2; - } - // } else { - // swidth = 0; - // } - - // Fixes issues with inverted heatmaps (see #6981) - // The root cause is that the coordinate system is flipped. - // In other words, instead of [0,0] being top-left, it's - // bottom-right. This causes a vertical and horizontal flip - // in the resulting image, making it rotated 180 degrees. - if (series.type === 'heatmap' && chart.inverted) { - shapeArgs.x = xAxis.len - shapeArgs.x; - shapeArgs.y = yAxis.len - shapeArgs.y; - shapeArgs.width = -shapeArgs.width; - shapeArgs.height = -shapeArgs.height; - } - - pushRect( - shapeArgs.x + swidth, - shapeArgs.y + swidth, - shapeArgs.width - (swidth * 2), - shapeArgs.height - (swidth * 2), - color - ); - } - }); - - closeSegment(); - - return; - } - - // Extract color axis - // each(chart.axes || [], function (a) { - // if (H.ColorAxis && a instanceof H.ColorAxis) { - // caxis = a; - // } - // }); - - while (i < sdata.length - 1) { - d = sdata[++i]; - - // px = x = y = z = nx = low = false; - // chartDestroyed = typeof chart.index === 'undefined'; - // nextInside = prevInside = pcolor = isXInside = isYInside = false; - // drawAsBar = asBar[series.type]; - - if (chartDestroyed) { - break; - } - - // Uncomment this to enable color by point. - // This currently left disabled as the charts look really ugly - // when enabled and there's a lot of points. - // Leaving in for the future (tm). - // if (colorByPoint) { - // colorIndex = ++colorIndex % series.chart.options.colors.length; - // pcolor = toRGBAFast(series.chart.options.colors[colorIndex]); - // pcolor[0] /= 255.0; - // pcolor[1] /= 255.0; - // pcolor[2] /= 255.0; - // } - - if (useRaw) { - x = d[0]; - y = d[1]; - - if (sdata[i + 1]) { - nx = sdata[i + 1][0]; - } - - if (sdata[i - 1]) { - px = sdata[i - 1][0]; - } - - if (d.length >= 3) { - z = d[2]; - - if (d[2] > inst.zMax) { - inst.zMax = d[2]; - } - - if (d[2] < inst.zMin) { - inst.zMin = d[2]; - } - } - - } else { - x = d; - y = yData[i]; - - if (sdata[i + 1]) { - nx = sdata[i + 1]; - } - - if (sdata[i - 1]) { - px = sdata[i - 1]; - } - - if (zData && zData.length) { - z = zData[i]; - - if (zData[i] > inst.zMax) { - inst.zMax = zData[i]; - } - - if (zData[i] < inst.zMin) { - inst.zMin = zData[i]; - } - } - } - - if (!connectNulls && (x === null || y === null)) { - beginSegment(); - continue; - } - - if (nx && nx >= xMin && nx <= xMax) { - nextInside = true; - } - - if (px && px >= xMin && px <= xMax) { - prevInside = true; - } - - if (isRange) { - if (useRaw) { - y = d.slice(1, 3); - } - - low = y[0]; - y = y[1]; - - } else if (isStacked) { - x = d.x; - y = d.stackY; - low = y - d.y; - } - - if (yMin !== null && - typeof yMin !== 'undefined' && - yMax !== null && - typeof yMax !== 'undefined' - ) { - isYInside = y >= yMin && y <= yMax; - } - - if (x > xMax && closestRight.x < xMax) { - closestRight.x = x; - closestRight.y = y; - } - - if (x < xMin && closestLeft.x < xMin) { - closestLeft.x = x; - closestLeft.y = y; - } - - if (y === null && connectNulls) { - continue; - } - - // Cull points outside the extremes - if (y === null || !isYInside) { - beginSegment(); - continue; - } - - if (x >= xMin && x <= xMax) { - isXInside = true; - } - - if (!isXInside && !nextInside && !prevInside) { - continue; - } - - // Skip translations - temporary floating point fix - if (!settings.useGPUTranslations) { - inst.skipTranslation = true; - x = xAxis.toPixels(x, true); - y = yAxis.toPixels(y, true); - - // Make sure we're not drawing outside of the chart area. - // See #6594. - if (y > plotHeight) { - y = plotHeight; - } - - if (x > plotWidth) { - x = plotWidth; - } - - } - - if (drawAsBar) { - - maxVal = y; - minVal = low; - - if (low === false || typeof low === 'undefined') { - if (y < 0) { - minVal = y; - } else { - minVal = 0; - } - } - - if (!settings.useGPUTranslations) { - minVal = yAxis.toPixels(minVal, true); - } - - // Need to add an extra point here - vertice(x, minVal, 0, 0, pcolor); - } - - // No markers on out of bounds things. - // Out of bound things are shown if and only if the next - // or previous point is inside the rect. - if (inst.hasMarkers) { // && isXInside) { - // x = H.correctFloat( - // Math.min(Math.max(-1e5, xAxis.translate( - // x, - // 0, - // 0, - // 0, - // 1, - // 0.5, - // false - // )), 1e5) - // ); - - if (lastX !== false) { - series.closestPointRangePx = Math.min( - series.closestPointRangePx, - Math.abs(x - lastX) - ); - } - } - - // If the last _drawn_ point is closer to this point than the - // threshold, skip it. Shaves off 20-100ms in processing. - - if (!settings.useGPUTranslations && - !settings.usePreallocated && - (lastX && x - lastX < cullXThreshold) && - (lastY && Math.abs(y - lastY) < cullYThreshold) - ) { - if (settings.debug.showSkipSummary) { - ++skipped; - } - - continue; - } - - // Do step line if enabled. - // Draws an additional point at the old Y at the new X. - // See #6976. - - if (options.step) { - vertice( - x, - lastY, - 0, - 2, - pcolor - ); - } - - vertice( - x, - y, - 0, - series.type === 'bubble' ? (z || 1) : 2, - pcolor - ); - - // Uncomment this to support color axis. - // if (caxis) { - // color = H.color(caxis.toColor(y)).rgba; - - // inst.colorData.push(color[0] / 255.0); - // inst.colorData.push(color[1] / 255.0); - // inst.colorData.push(color[2] / 255.0); - // inst.colorData.push(color[3]); - // } - - lastX = x; - lastY = y; - } - - if (settings.debug.showSkipSummary) { - console.log('skipped points:', skipped); // eslint-disable-line no-console - } - - function pushSupplementPoint(point) { - if (!settings.useGPUTranslations) { - inst.skipTranslation = true; - point.x = xAxis.toPixels(point.x, true); - point.y = yAxis.toPixels(point.y, true); - } - - // We should only do this for lines, and we should ignore markers - // since there's no point here that would have a marker. - - vertice( - point.x, - point.y, - 0, - 2 - ); - } - - if (!lastX && - connectNulls !== false && - closestLeft > -Number.MAX_VALUE && - closestRight < Number.MAX_VALUE) { - // There are no points within the selected range - pushSupplementPoint(closestLeft); - pushSupplementPoint(closestRight); - } - - closeSegment(); - } - - /* - * Push a series to the renderer - * If we render the series immediatly, we don't have to loop later - * @param s {Highchart.Series} - the series to push - */ - function pushSeries(s) { - if (series.length > 0) { - // series[series.length - 1].to = data.length; - if (series[series.length - 1].hasMarkers) { - series[series.length - 1].markerTo = markerData.length; - } - } - - if (settings.debug.timeSeriesProcessing) { - console.time('building ' + s.type + ' series'); // eslint-disable-line no-console - } - - series.push({ - segments: [], - // from: data.length, - markerFrom: markerData.length, - // Push RGBA values to this array to use per. point coloring. - // It should be 0-padded, so each component should be pushed in - // succession. - colorData: [], - series: s, - zMin: Number.MAX_VALUE, - zMax: -Number.MAX_VALUE, - hasMarkers: s.options.marker ? - s.options.marker.enabled !== false : - false, - showMarksers: true, - drawMode: ({ - 'area': 'lines', - 'arearange': 'lines', - 'areaspline': 'line_strip', - 'column': 'lines', - 'columnrange': 'lines', - 'bar': 'lines', - 'line': 'line_strip', - 'scatter': 'points', - 'heatmap': 'triangles', - 'treemap': 'triangles', - 'bubble': 'points' - })[s.type] || 'line_strip' - }); - - // Add the series data to our buffer(s) - pushSeriesData(s, series[series.length - 1]); - - if (settings.debug.timeSeriesProcessing) { - console.timeEnd('building ' + s.type + ' series'); // eslint-disable-line no-console - } - } - - /* - * Flush the renderer. - * This removes pushed series and vertices. - * Should be called after clearing and before rendering - */ - function flush() { - series = []; - exports.data = data = []; - markerData = []; - - if (vbuffer) { - vbuffer.destroy(); - } - } - - /* - * Pass x-axis to shader - * @param axis {Highcharts.Axis} - the x-axis - */ - function setXAxis(axis) { - if (!shader) { - return; - } - - shader.setUniform('xAxisTrans', axis.transA); - shader.setUniform('xAxisMin', axis.min); - shader.setUniform('xAxisMinPad', axis.minPixelPadding); - shader.setUniform('xAxisPointRange', axis.pointRange); - shader.setUniform('xAxisLen', axis.len); - shader.setUniform('xAxisPos', axis.pos); - shader.setUniform('xAxisCVSCoord', !axis.horiz); - } - - /* - * Pass y-axis to shader - * @param axis {Highcharts.Axis} - the y-axis - */ - function setYAxis(axis) { - if (!shader) { - return; - } - - shader.setUniform('yAxisTrans', axis.transA); - shader.setUniform('yAxisMin', axis.min); - shader.setUniform('yAxisMinPad', axis.minPixelPadding); - shader.setUniform('yAxisPointRange', axis.pointRange); - shader.setUniform('yAxisLen', axis.len); - shader.setUniform('yAxisPos', axis.pos); - shader.setUniform('yAxisCVSCoord', !axis.horiz); - } - - /* - * Set the translation threshold - * @param has {boolean} - has threshold flag - * @param translation {Float} - the threshold - */ - function setThreshold(has, translation) { - shader.setUniform('hasThreshold', has); - shader.setUniform('translatedThreshold', translation); - } - - /* - * Render the data - * This renders all pushed series. - */ - function render(chart) { - - if (chart) { - if (!chart.chartHeight || !chart.chartWidth) { - // chart.setChartSize(); - } - - width = chart.chartWidth || 800; - height = chart.chartHeight || 400; - } else { - return false; - } - - if (!gl || !width || !height) { - return false; - } - - if (settings.debug.timeRendering) { - console.time('gl rendering'); // eslint-disable-line no-console - } - - gl.canvas.width = width; - gl.canvas.height = height; - - shader.bind(); - - - gl.viewport(0, 0, width, height); - shader.setPMatrix(orthoMatrix(width, height)); - shader.setPlotHeight(chart.plotHeight); - - if (settings.lineWidth > 1 && !H.isMS) { - gl.lineWidth(settings.lineWidth); - } - - vbuffer.build(exports.data, 'aVertexPosition', 4); - vbuffer.bind(); - - if (textureIsReady) { - gl.bindTexture(gl.TEXTURE_2D, circleTextureHandle); - shader.setTexture(circleTextureHandle); - } - - shader.setInverted(chart.inverted); - - - // Render the series - each(series, function (s, si) { - var options = s.series.options, - sindex, - lineWidth = typeof options.lineWidth !== 'undefined' ? - options.lineWidth : - 1, - threshold = options.threshold, - hasThreshold = isNumber(threshold), - yBottom = s.series.yAxis.getThreshold(threshold), - translatedThreshold = yBottom, - cbuffer, - showMarkers = pick( - options.marker ? options.marker.enabled : null, - s.series.xAxis.isRadial ? true : null, - s.series.closestPointRangePx > - 2 * (( - options.marker ? - options.marker.radius : - 10 - ) || 10) - ), - fillColor = - (s.series.pointAttribs && s.series.pointAttribs().fill) || - s.series.color, - color; - - if (s.series.fillOpacity && options.fillOpacity) { - fillColor = new Color(fillColor).setOpacity( - pick(options.fillOpacity, 1.0) - ).get(); - } - - if (options.colorByPoint) { - fillColor = s.series.chart.options.colors[si]; - } - - color = H.color(fillColor).rgba; - - if (!settings.useAlpha) { - color[3] = 1.0; - } - - // This is very much temporary - if (s.drawMode === 'lines' && settings.useAlpha && color[3] < 1) { - color[3] /= 10; - } - - // Blending - if (options.boostBlending === 'add') { - gl.blendFunc(gl.SRC_ALPHA, gl.ONE); - gl.blendEquation(gl.FUNC_ADD); - - } else if (options.boostBlending === 'mult') { - gl.blendFunc(gl.DST_COLOR, gl.ZERO); - - } else if (options.boostBlending === 'darken') { - gl.blendFunc(gl.ONE, gl.ONE); - gl.blendEquation(gl.FUNC_MIN); - - } else { - // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - // gl.blendEquation(gl.FUNC_ADD); - gl.blendFuncSeparate( - gl.SRC_ALPHA, - gl.ONE_MINUS_SRC_ALPHA, - gl.ONE, - gl.ONE_MINUS_SRC_ALPHA - ); - } - - shader.reset(); - - // If there are entries in the colorData buffer, build and bind it. - if (s.colorData.length > 0) { - shader.setUniform('hasColor', 1.0); - cbuffer = GLVertexBuffer(gl, shader); // eslint-disable-line new-cap - cbuffer.build(s.colorData, 'aColor', 4); - cbuffer.bind(); - } - - // Set series specific uniforms - shader.setColor(color); - setXAxis(s.series.xAxis); - setYAxis(s.series.yAxis); - setThreshold(hasThreshold, translatedThreshold); - - if (s.drawMode === 'points') { - if (options.marker && options.marker.radius) { - shader.setPointSize(options.marker.radius * 2.0); - } else { - shader.setPointSize(1); - } - } - - // If set to true, the toPixels translations in the shader - // is skipped, i.e it's assumed that the value is a pixel coord. - shader.setSkipTranslation(s.skipTranslation); - - if (s.series.type === 'bubble') { - shader.setBubbleUniforms(s.series, s.zMin, s.zMax); - } - - shader.setDrawAsCircle( - (asCircle[s.series.type] && textureIsReady) || false - ); - - // Do the actual rendering - // If the line width is < 0, skip rendering of the lines. See #7833. - if (lineWidth > 0 || s.drawMode !== 'line_strip') { - for (sindex = 0; sindex < s.segments.length; sindex++) { - // if (s.segments[sindex].from < s.segments[sindex].to) { - vbuffer.render( - s.segments[sindex].from, - s.segments[sindex].to, - s.drawMode - ); - // } - } - } - - if (s.hasMarkers && showMarkers) { - if (options.marker && options.marker.radius) { - shader.setPointSize(options.marker.radius * 2.0); - } else { - shader.setPointSize(10); - } - shader.setDrawAsCircle(true); - for (sindex = 0; sindex < s.segments.length; sindex++) { - // if (s.segments[sindex].from < s.segments[sindex].to) { - vbuffer.render( - s.segments[sindex].from, - s.segments[sindex].to, - 'POINTS' - ); - // } - } - } - }); - - if (settings.debug.timeRendering) { - console.timeEnd('gl rendering'); // eslint-disable-line no-console - } - - if (postRenderCallback) { - postRenderCallback(); - } - - flush(); - } - - /* - * Render the data when ready - */ - function renderWhenReady(chart) { - clear(); - - if (chart.renderer.forExport) { - return render(chart); - } - - if (isInited) { - render(chart); - } else { - setTimeout(function () { - renderWhenReady(chart); - }, 1); - } - } - - /* - * Set the viewport size in pixels - * Creates an orthographic perspective matrix and applies it. - * @param w {Integer} - the width of the viewport - * @param h {Integer} - the height of the viewport - */ - function setSize(w, h) { - // Skip if there's no change - if (width === w && h === h) { - return; - } - - width = w; - height = h; - - shader.bind(); - shader.setPMatrix(orthoMatrix(width, height)); - } - - /* - * Init OpenGL - * @param canvas {HTMLCanvas} - the canvas to render to - */ - function init(canvas, noFlush) { - var i = 0, - contexts = [ - 'webgl', - 'experimental-webgl', - 'moz-webgl', - 'webkit-3d' - ]; - - isInited = false; - - if (!canvas) { - return false; - } - - if (settings.debug.timeSetup) { - console.time('gl setup'); // eslint-disable-line no-console - } - - for (; i < contexts.length; i++) { - gl = canvas.getContext(contexts[i], { - // premultipliedAlpha: false - }); - if (gl) { - break; - } - } - - if (gl) { - if (!noFlush) { - flush(); - } - } else { - return false; - } - - gl.enable(gl.BLEND); - // gl.blendFunc(gl.SRC_ALPHA, gl.ONE); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - gl.disable(gl.DEPTH_TEST); - // gl.depthMask(gl.FALSE); - gl.depthFunc(gl.LESS); - - shader = GLShader(gl); // eslint-disable-line new-cap - vbuffer = GLVertexBuffer(gl, shader); // eslint-disable-line new-cap - - textureIsReady = false; - - // Set up the circle texture used for bubbles - circleTextureHandle = gl.createTexture(); - - // Draw the circle - circleTexture.width = 512; - circleTexture.height = 512; - - circleCtx.mozImageSmoothingEnabled = false; - circleCtx.webkitImageSmoothingEnabled = false; - circleCtx.msImageSmoothingEnabled = false; - circleCtx.imageSmoothingEnabled = false; - - circleCtx.strokeStyle = 'rgba(255, 255, 255, 0)'; - circleCtx.fillStyle = '#FFF'; - - circleCtx.beginPath(); - circleCtx.arc(256, 256, 256, 0, 2 * Math.PI); - circleCtx.stroke(); - circleCtx.fill(); - - try { - - gl.bindTexture(gl.TEXTURE_2D, circleTextureHandle); - // gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - circleTexture - ); - - gl.texParameteri( - gl.TEXTURE_2D, - gl.TEXTURE_WRAP_S, - gl.CLAMP_TO_EDGE - ); - gl.texParameteri( - gl.TEXTURE_2D, - gl.TEXTURE_WRAP_T, - gl.CLAMP_TO_EDGE - ); - gl.texParameteri( - gl.TEXTURE_2D, - gl.TEXTURE_MAG_FILTER, - gl.LINEAR - ); - gl.texParameteri( - gl.TEXTURE_2D, - gl.TEXTURE_MIN_FILTER, - gl.LINEAR - ); - - // gl.generateMipmap(gl.TEXTURE_2D); - - gl.bindTexture(gl.TEXTURE_2D, null); - - textureIsReady = true; - } catch (e) {} - - isInited = true; - - if (settings.debug.timeSetup) { - console.timeEnd('gl setup'); // eslint-disable-line no-console - } - - return true; - } - - /* - * Check if we have a valid OGL context - * @returns {Boolean} - true if the context is valid - */ - function valid() { - return gl !== false; - } - - /* - * Check if the renderer has been initialized - * @returns {Boolean} - true if it has, false if not - */ - function inited() { - return isInited; - } - - function destroy() { - flush(); - vbuffer.destroy(); - shader.destroy(); - if (gl) { - if (circleTextureHandle) { - gl.deleteTexture(circleTextureHandle); - } - gl.canvas.width = 1; - gl.canvas.height = 1; - } - } - - // ///////////////////////////////////////////////////////////////////////// - exports = { - allocateBufferForSingleSeries: allocateBufferForSingleSeries, - pushSeries: pushSeries, - setSize: setSize, - inited: inited, - setThreshold: setThreshold, - init: init, - render: renderWhenReady, - settings: settings, - valid: valid, - clear: clear, - flush: flush, - setXAxis: setXAxis, - setYAxis: setYAxis, - data: data, - gl: getGL, - allocateBuffer: allocateBuffer, - destroy: destroy, - setOptions: setOptions - }; - - return exports; + var // Shader + shader = false, + // Vertex buffers - keyed on shader attribute name + vbuffer = false, + // Opengl context + gl = false, + // Width of our viewport in pixels + width = 0, + // Height of our viewport in pixels + height = 0, + // The data to render - array of coordinates + data = false, + // The marker data + markerData = false, + // Is the texture ready? + textureIsReady = false, + // Exports + exports = {}, + // Is it inited? + isInited = false, + // The series stack + series = [], + // Texture for circles + circleTexture = doc.createElement('canvas'), + // Context for circle texture + circleCtx = circleTexture.getContext('2d'), + // Handle for the circle texture + circleTextureHandle, + // Things to draw as "rectangles" (i.e lines) + asBar = { + 'column': true, + 'columnrange': true, + 'bar': true, + 'area': true, + 'arearange': true + }, + asCircle = { + 'scatter': true, + 'bubble': true + }, + // Render settings + settings = { + pointSize: 1, + lineWidth: 1, + fillColor: '#AA00AA', + useAlpha: true, + usePreallocated: false, + useGPUTranslations: false, + debug: { + timeRendering: false, + timeSeriesProcessing: false, + timeSetup: false, + timeBufferCopy: false, + timeKDTree: false, + showSkipSummary: false + } + }; + + // ///////////////////////////////////////////////////////////////////////// + + function setOptions(options) { + merge(true, settings, options); + } + + function seriesPointCount(series) { + var isStacked, + xData, + s; + + if (series.isSeriesBoosting) { + isStacked = !!series.options.stacking; + xData = ( + series.xData || + series.options.xData || + series.processedXData + ); + s = (isStacked ? series.data : (xData || series.options.data)) + .length; + + if (series.type === 'treemap') { + s *= 12; + } else if (series.type === 'heatmap') { + s *= 6; + } else if (asBar[series.type]) { + s *= 2; + } + + return s; + } + + return 0; + } + + /* Allocate a float buffer to fit all series */ + function allocateBuffer(chart) { + var s = 0; + + if (!settings.usePreallocated) { + return; + } + + each(chart.series, function (series) { + if (series.isSeriesBoosting) { + s += seriesPointCount(series); + } + }); + + vbuffer.allocate(s); + } + + function allocateBufferForSingleSeries(series) { + var s = 0; + + if (!settings.usePreallocated) { + return; + } + + if (series.isSeriesBoosting) { + s = seriesPointCount(series); + } + + vbuffer.allocate(s); + } + + /* + * Returns an orthographic perspective matrix + * @param {number} width - the width of the viewport in pixels + * @param {number} height - the height of the viewport in pixels + */ + function orthoMatrix(width, height) { + var near = 0, + far = 1; + + return [ + 2 / width, 0, 0, 0, + 0, -(2 / height), 0, 0, + 0, 0, -2 / (far - near), 0, + -1, 1, -(far + near) / (far - near), 1 + ]; + } + + /* + * Clear the depth and color buffer + */ + function clear() { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + } + + /* + * Get the WebGL context + * @returns {WebGLContext} - the context + */ + function getGL() { + return gl; + } + + /* + * Push data for a single series + * This calculates additional vertices and transforms the data to be + * aligned correctly in memory + */ + function pushSeriesData(series, inst) { + var isRange = series.pointArrayMap && + series.pointArrayMap.join(',') === 'low,high', + chart = series.chart, + options = series.options, + isStacked = !!options.stacking, + rawData = options.data, + xExtremes = series.xAxis.getExtremes(), + xMin = xExtremes.min, + xMax = xExtremes.max, + yExtremes = series.yAxis.getExtremes(), + yMin = yExtremes.min, + yMax = yExtremes.max, + xData = series.xData || options.xData || series.processedXData, + yData = series.yData || options.yData || series.processedYData, + zData = series.zData || options.zData || series.processedZData, + yAxis = series.yAxis, + xAxis = series.xAxis, + plotHeight = series.chart.plotHeight, + plotWidth = series.chart.plotWidth, + useRaw = !xData || xData.length === 0, + // threshold = options.threshold, + // yBottom = chart.yAxis[0].getThreshold(threshold), + // hasThreshold = isNumber(threshold), + // colorByPoint = series.options.colorByPoint, + // This is required for color by point, so make sure this is + // uncommented if enabling that + // colorIndex = 0, + // Required for color axis support + // caxis, + connectNulls = options.connectNulls, + // For some reason eslint doesn't pick up that this is actually used + maxVal, // eslint-disable-line no-unused-vars + points = series.points || false, + lastX = false, + lastY = false, + minVal, + color, + scolor, + sdata = isStacked ? series.data : (xData || rawData), + closestLeft = { x: -Number.MAX_VALUE, y: 0 }, + closestRight = { x: Number.MIN_VALUE, y: 0 }, + + skipped = 0, + + cullXThreshold = 1, + cullYThreshold = 1, + + // The following are used in the builder while loop + x, + y, + d, + z, + i = -1, + px = false, + nx = false, + // This is in fact used. + low, // eslint-disable-line no-unused-vars + chartDestroyed = typeof chart.index === 'undefined', + nextInside = false, + prevInside = false, + pcolor = false, + drawAsBar = asBar[series.type], + isXInside = false, + isYInside = true; + + if (options.boostData && options.boostData.length > 0) { + return; + } + + if (chart.inverted) { + plotHeight = series.chart.plotWidth; + plotWidth = series.chart.plotHeight; + } + + series.closestPointRangePx = Number.MAX_VALUE; + + // Push color to color buffer - need to do this per. vertex + function pushColor(color) { + if (color) { + inst.colorData.push(color[0]); + inst.colorData.push(color[1]); + inst.colorData.push(color[2]); + inst.colorData.push(color[3]); + } + } + + // Push a vertice to the data buffer + function vertice(x, y, checkTreshold, pointSize, color) { + pushColor(color); + if (settings.usePreallocated) { + vbuffer.push(x, y, checkTreshold ? 1 : 0, pointSize || 1); + } else { + data.push(x); + data.push(y); + data.push(checkTreshold ? 1 : 0); + data.push(pointSize || 1); + } + } + + function closeSegment() { + if (inst.segments.length) { + inst.segments[inst.segments.length - 1].to = data.length; + } + } + + // Create a new segment for the current set + function beginSegment() { + // Insert a segment on the series. + // A segment is just a start indice. + // When adding a segment, if one exists from before, it should + // set the previous segment's end + + if (inst.segments.length && + inst.segments[inst.segments.length - 1].from === data.length + ) { + return; + } + + closeSegment(); + + inst.segments.push({ + from: data.length + }); + + } + + // Push a rectangle to the data buffer + function pushRect(x, y, w, h, color) { + pushColor(color); + vertice(x + w, y); + pushColor(color); + vertice(x, y); + pushColor(color); + vertice(x, y + h); + + pushColor(color); + vertice(x, y + h); + pushColor(color); + vertice(x + w, y + h); + pushColor(color); + vertice(x + w, y); + } + + // Create the first segment + beginSegment(); + + // Special case for point shapes + if (points && points.length > 0) { + + // If we're doing points, we assume that the points are already + // translated, so we skip the shader translation. + inst.skipTranslation = true; + // Force triangle draw mode + inst.drawMode = 'triangles'; + + // We don't have a z component in the shader, so we need to sort. + if (points[0].node && points[0].node.levelDynamic) { + points.sort(function (a, b) { + if (a.node) { + if (a.node.levelDynamic > b.node.levelDynamic) { + return 1; + } else if (a.node.levelDynamic < b.node.levelDynamic) { + return -1; + } + } + return 0; + }); + } + + each(points, function (point) { + var plotY = point.plotY, + shapeArgs, + swidth, + pointAttr; + + if ( + typeof plotY !== 'undefined' && + !isNaN(plotY) && + point.y !== null + ) { + shapeArgs = point.shapeArgs; + + /*= if (build.classic) { =*/ + pointAttr = point.series.pointAttribs(point); + /*= } else { =*/ + pointAttr = point.series.colorAttribs(point); + /*= } =*/ + swidth = pointAttr['stroke-width'] || 0; + + // Handle point colors + color = H.color(pointAttr.fill).rgba; + color[0] /= 255.0; + color[1] /= 255.0; + color[2] /= 255.0; + + // So there are two ways of doing this. Either we can + // create a rectangle of two triangles, or we can do a + // point and use point size. Latter is faster, but + // only supports squares. So we're doing triangles. + // We could also use one color per. vertice to get + // better color interpolation. + + // If there's stroking, we do an additional rect + if (series.type === 'treemap') { + swidth = swidth || 1; + scolor = H.color(pointAttr.stroke).rgba; + + scolor[0] /= 255.0; + scolor[1] /= 255.0; + scolor[2] /= 255.0; + + pushRect( + shapeArgs.x, + shapeArgs.y, + shapeArgs.width, + shapeArgs.height, + scolor + ); + + swidth /= 2; + } + // } else { + // swidth = 0; + // } + + // Fixes issues with inverted heatmaps (see #6981) + // The root cause is that the coordinate system is flipped. + // In other words, instead of [0,0] being top-left, it's + // bottom-right. This causes a vertical and horizontal flip + // in the resulting image, making it rotated 180 degrees. + if (series.type === 'heatmap' && chart.inverted) { + shapeArgs.x = xAxis.len - shapeArgs.x; + shapeArgs.y = yAxis.len - shapeArgs.y; + shapeArgs.width = -shapeArgs.width; + shapeArgs.height = -shapeArgs.height; + } + + pushRect( + shapeArgs.x + swidth, + shapeArgs.y + swidth, + shapeArgs.width - (swidth * 2), + shapeArgs.height - (swidth * 2), + color + ); + } + }); + + closeSegment(); + + return; + } + + // Extract color axis + // each(chart.axes || [], function (a) { + // if (H.ColorAxis && a instanceof H.ColorAxis) { + // caxis = a; + // } + // }); + + while (i < sdata.length - 1) { + d = sdata[++i]; + + // px = x = y = z = nx = low = false; + // chartDestroyed = typeof chart.index === 'undefined'; + // nextInside = prevInside = pcolor = isXInside = isYInside = false; + // drawAsBar = asBar[series.type]; + + if (chartDestroyed) { + break; + } + + // Uncomment this to enable color by point. + // This currently left disabled as the charts look really ugly + // when enabled and there's a lot of points. + // Leaving in for the future (tm). + // if (colorByPoint) { + // colorIndex = ++colorIndex % + // series.chart.options.colors.length; + // pcolor = toRGBAFast(series.chart.options.colors[colorIndex]); + // pcolor[0] /= 255.0; + // pcolor[1] /= 255.0; + // pcolor[2] /= 255.0; + // } + + if (useRaw) { + x = d[0]; + y = d[1]; + + if (sdata[i + 1]) { + nx = sdata[i + 1][0]; + } + + if (sdata[i - 1]) { + px = sdata[i - 1][0]; + } + + if (d.length >= 3) { + z = d[2]; + + if (d[2] > inst.zMax) { + inst.zMax = d[2]; + } + + if (d[2] < inst.zMin) { + inst.zMin = d[2]; + } + } + + } else { + x = d; + y = yData[i]; + + if (sdata[i + 1]) { + nx = sdata[i + 1]; + } + + if (sdata[i - 1]) { + px = sdata[i - 1]; + } + + if (zData && zData.length) { + z = zData[i]; + + if (zData[i] > inst.zMax) { + inst.zMax = zData[i]; + } + + if (zData[i] < inst.zMin) { + inst.zMin = zData[i]; + } + } + } + + if (!connectNulls && (x === null || y === null)) { + beginSegment(); + continue; + } + + if (nx && nx >= xMin && nx <= xMax) { + nextInside = true; + } + + if (px && px >= xMin && px <= xMax) { + prevInside = true; + } + + if (isRange) { + if (useRaw) { + y = d.slice(1, 3); + } + + low = y[0]; + y = y[1]; + + } else if (isStacked) { + x = d.x; + y = d.stackY; + low = y - d.y; + } + + if (yMin !== null && + typeof yMin !== 'undefined' && + yMax !== null && + typeof yMax !== 'undefined' + ) { + isYInside = y >= yMin && y <= yMax; + } + + if (x > xMax && closestRight.x < xMax) { + closestRight.x = x; + closestRight.y = y; + } + + if (x < xMin && closestLeft.x < xMin) { + closestLeft.x = x; + closestLeft.y = y; + } + + if (y === null && connectNulls) { + continue; + } + + // Cull points outside the extremes + if (y === null || !isYInside) { + beginSegment(); + continue; + } + + if (x >= xMin && x <= xMax) { + isXInside = true; + } + + if (!isXInside && !nextInside && !prevInside) { + continue; + } + + // Skip translations - temporary floating point fix + if (!settings.useGPUTranslations) { + inst.skipTranslation = true; + x = xAxis.toPixels(x, true); + y = yAxis.toPixels(y, true); + + // Make sure we're not drawing outside of the chart area. + // See #6594. + if (y > plotHeight) { + y = plotHeight; + } + + if (x > plotWidth) { + x = plotWidth; + } + + } + + if (drawAsBar) { + + maxVal = y; + minVal = low; + + if (low === false || typeof low === 'undefined') { + if (y < 0) { + minVal = y; + } else { + minVal = 0; + } + } + + if (!settings.useGPUTranslations) { + minVal = yAxis.toPixels(minVal, true); + } + + // Need to add an extra point here + vertice(x, minVal, 0, 0, pcolor); + } + + // No markers on out of bounds things. + // Out of bound things are shown if and only if the next + // or previous point is inside the rect. + if (inst.hasMarkers) { // && isXInside) { + // x = H.correctFloat( + // Math.min(Math.max(-1e5, xAxis.translate( + // x, + // 0, + // 0, + // 0, + // 1, + // 0.5, + // false + // )), 1e5) + // ); + + if (lastX !== false) { + series.closestPointRangePx = Math.min( + series.closestPointRangePx, + Math.abs(x - lastX) + ); + } + } + + // If the last _drawn_ point is closer to this point than the + // threshold, skip it. Shaves off 20-100ms in processing. + + if (!settings.useGPUTranslations && + !settings.usePreallocated && + (lastX && x - lastX < cullXThreshold) && + (lastY && Math.abs(y - lastY) < cullYThreshold) + ) { + if (settings.debug.showSkipSummary) { + ++skipped; + } + + continue; + } + + // Do step line if enabled. + // Draws an additional point at the old Y at the new X. + // See #6976. + + if (options.step) { + vertice( + x, + lastY, + 0, + 2, + pcolor + ); + } + + vertice( + x, + y, + 0, + series.type === 'bubble' ? (z || 1) : 2, + pcolor + ); + + // Uncomment this to support color axis. + // if (caxis) { + // color = H.color(caxis.toColor(y)).rgba; + + // inst.colorData.push(color[0] / 255.0); + // inst.colorData.push(color[1] / 255.0); + // inst.colorData.push(color[2] / 255.0); + // inst.colorData.push(color[3]); + // } + + lastX = x; + lastY = y; + } + + if (settings.debug.showSkipSummary) { + console.log('skipped points:', skipped); // eslint-disable-line no-console + } + + function pushSupplementPoint(point) { + if (!settings.useGPUTranslations) { + inst.skipTranslation = true; + point.x = xAxis.toPixels(point.x, true); + point.y = yAxis.toPixels(point.y, true); + } + + // We should only do this for lines, and we should ignore markers + // since there's no point here that would have a marker. + + vertice( + point.x, + point.y, + 0, + 2 + ); + } + + if (!lastX && + connectNulls !== false && + closestLeft > -Number.MAX_VALUE && + closestRight < Number.MAX_VALUE) { + // There are no points within the selected range + pushSupplementPoint(closestLeft); + pushSupplementPoint(closestRight); + } + + closeSegment(); + } + + /* + * Push a series to the renderer + * If we render the series immediatly, we don't have to loop later + * @param s {Highchart.Series} - the series to push + */ + function pushSeries(s) { + if (series.length > 0) { + // series[series.length - 1].to = data.length; + if (series[series.length - 1].hasMarkers) { + series[series.length - 1].markerTo = markerData.length; + } + } + + if (settings.debug.timeSeriesProcessing) { + console.time('building ' + s.type + ' series'); // eslint-disable-line no-console + } + + series.push({ + segments: [], + // from: data.length, + markerFrom: markerData.length, + // Push RGBA values to this array to use per. point coloring. + // It should be 0-padded, so each component should be pushed in + // succession. + colorData: [], + series: s, + zMin: Number.MAX_VALUE, + zMax: -Number.MAX_VALUE, + hasMarkers: s.options.marker ? + s.options.marker.enabled !== false : + false, + showMarksers: true, + drawMode: ({ + 'area': 'lines', + 'arearange': 'lines', + 'areaspline': 'line_strip', + 'column': 'lines', + 'columnrange': 'lines', + 'bar': 'lines', + 'line': 'line_strip', + 'scatter': 'points', + 'heatmap': 'triangles', + 'treemap': 'triangles', + 'bubble': 'points' + })[s.type] || 'line_strip' + }); + + // Add the series data to our buffer(s) + pushSeriesData(s, series[series.length - 1]); + + if (settings.debug.timeSeriesProcessing) { + console.timeEnd('building ' + s.type + ' series'); // eslint-disable-line no-console + } + } + + /* + * Flush the renderer. + * This removes pushed series and vertices. + * Should be called after clearing and before rendering + */ + function flush() { + series = []; + exports.data = data = []; + markerData = []; + + if (vbuffer) { + vbuffer.destroy(); + } + } + + /* + * Pass x-axis to shader + * @param axis {Highcharts.Axis} - the x-axis + */ + function setXAxis(axis) { + if (!shader) { + return; + } + + shader.setUniform('xAxisTrans', axis.transA); + shader.setUniform('xAxisMin', axis.min); + shader.setUniform('xAxisMinPad', axis.minPixelPadding); + shader.setUniform('xAxisPointRange', axis.pointRange); + shader.setUniform('xAxisLen', axis.len); + shader.setUniform('xAxisPos', axis.pos); + shader.setUniform('xAxisCVSCoord', !axis.horiz); + } + + /* + * Pass y-axis to shader + * @param axis {Highcharts.Axis} - the y-axis + */ + function setYAxis(axis) { + if (!shader) { + return; + } + + shader.setUniform('yAxisTrans', axis.transA); + shader.setUniform('yAxisMin', axis.min); + shader.setUniform('yAxisMinPad', axis.minPixelPadding); + shader.setUniform('yAxisPointRange', axis.pointRange); + shader.setUniform('yAxisLen', axis.len); + shader.setUniform('yAxisPos', axis.pos); + shader.setUniform('yAxisCVSCoord', !axis.horiz); + } + + /* + * Set the translation threshold + * @param has {boolean} - has threshold flag + * @param translation {Float} - the threshold + */ + function setThreshold(has, translation) { + shader.setUniform('hasThreshold', has); + shader.setUniform('translatedThreshold', translation); + } + + /* + * Render the data + * This renders all pushed series. + */ + function render(chart) { + + if (chart) { + if (!chart.chartHeight || !chart.chartWidth) { + // chart.setChartSize(); + } + + width = chart.chartWidth || 800; + height = chart.chartHeight || 400; + } else { + return false; + } + + if (!gl || !width || !height) { + return false; + } + + if (settings.debug.timeRendering) { + console.time('gl rendering'); // eslint-disable-line no-console + } + + gl.canvas.width = width; + gl.canvas.height = height; + + shader.bind(); + + + gl.viewport(0, 0, width, height); + shader.setPMatrix(orthoMatrix(width, height)); + shader.setPlotHeight(chart.plotHeight); + + if (settings.lineWidth > 1 && !H.isMS) { + gl.lineWidth(settings.lineWidth); + } + + vbuffer.build(exports.data, 'aVertexPosition', 4); + vbuffer.bind(); + + if (textureIsReady) { + gl.bindTexture(gl.TEXTURE_2D, circleTextureHandle); + shader.setTexture(circleTextureHandle); + } + + shader.setInverted(chart.inverted); + + + // Render the series + each(series, function (s, si) { + var options = s.series.options, + sindex, + lineWidth = typeof options.lineWidth !== 'undefined' ? + options.lineWidth : + 1, + threshold = options.threshold, + hasThreshold = isNumber(threshold), + yBottom = s.series.yAxis.getThreshold(threshold), + translatedThreshold = yBottom, + cbuffer, + showMarkers = pick( + options.marker ? options.marker.enabled : null, + s.series.xAxis.isRadial ? true : null, + s.series.closestPointRangePx > + 2 * (( + options.marker ? + options.marker.radius : + 10 + ) || 10) + ), + fillColor = + (s.series.pointAttribs && s.series.pointAttribs().fill) || + s.series.color, + color; + + if (s.series.fillOpacity && options.fillOpacity) { + fillColor = new Color(fillColor).setOpacity( + pick(options.fillOpacity, 1.0) + ).get(); + } + + if (options.colorByPoint) { + fillColor = s.series.chart.options.colors[si]; + } + + color = H.color(fillColor).rgba; + + if (!settings.useAlpha) { + color[3] = 1.0; + } + + // This is very much temporary + if (s.drawMode === 'lines' && settings.useAlpha && color[3] < 1) { + color[3] /= 10; + } + + // Blending + if (options.boostBlending === 'add') { + gl.blendFunc(gl.SRC_ALPHA, gl.ONE); + gl.blendEquation(gl.FUNC_ADD); + + } else if (options.boostBlending === 'mult') { + gl.blendFunc(gl.DST_COLOR, gl.ZERO); + + } else if (options.boostBlending === 'darken') { + gl.blendFunc(gl.ONE, gl.ONE); + gl.blendEquation(gl.FUNC_MIN); + + } else { + // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + // gl.blendEquation(gl.FUNC_ADD); + gl.blendFuncSeparate( + gl.SRC_ALPHA, + gl.ONE_MINUS_SRC_ALPHA, + gl.ONE, + gl.ONE_MINUS_SRC_ALPHA + ); + } + + shader.reset(); + + // If there are entries in the colorData buffer, build and bind it. + if (s.colorData.length > 0) { + shader.setUniform('hasColor', 1.0); + cbuffer = GLVertexBuffer(gl, shader); // eslint-disable-line new-cap + cbuffer.build(s.colorData, 'aColor', 4); + cbuffer.bind(); + } + + // Set series specific uniforms + shader.setColor(color); + setXAxis(s.series.xAxis); + setYAxis(s.series.yAxis); + setThreshold(hasThreshold, translatedThreshold); + + if (s.drawMode === 'points') { + if (options.marker && options.marker.radius) { + shader.setPointSize(options.marker.radius * 2.0); + } else { + shader.setPointSize(1); + } + } + + // If set to true, the toPixels translations in the shader + // is skipped, i.e it's assumed that the value is a pixel coord. + shader.setSkipTranslation(s.skipTranslation); + + if (s.series.type === 'bubble') { + shader.setBubbleUniforms(s.series, s.zMin, s.zMax); + } + + shader.setDrawAsCircle( + (asCircle[s.series.type] && textureIsReady) || false + ); + + // Do the actual rendering + // If the line width is < 0, skip rendering of the lines. See #7833. + if (lineWidth > 0 || s.drawMode !== 'line_strip') { + for (sindex = 0; sindex < s.segments.length; sindex++) { + // if (s.segments[sindex].from < s.segments[sindex].to) { + vbuffer.render( + s.segments[sindex].from, + s.segments[sindex].to, + s.drawMode + ); + // } + } + } + + if (s.hasMarkers && showMarkers) { + if (options.marker && options.marker.radius) { + shader.setPointSize(options.marker.radius * 2.0); + } else { + shader.setPointSize(10); + } + shader.setDrawAsCircle(true); + for (sindex = 0; sindex < s.segments.length; sindex++) { + // if (s.segments[sindex].from < s.segments[sindex].to) { + vbuffer.render( + s.segments[sindex].from, + s.segments[sindex].to, + 'POINTS' + ); + // } + } + } + }); + + if (settings.debug.timeRendering) { + console.timeEnd('gl rendering'); // eslint-disable-line no-console + } + + if (postRenderCallback) { + postRenderCallback(); + } + + flush(); + } + + /* + * Render the data when ready + */ + function renderWhenReady(chart) { + clear(); + + if (chart.renderer.forExport) { + return render(chart); + } + + if (isInited) { + render(chart); + } else { + setTimeout(function () { + renderWhenReady(chart); + }, 1); + } + } + + /* + * Set the viewport size in pixels + * Creates an orthographic perspective matrix and applies it. + * @param w {Integer} - the width of the viewport + * @param h {Integer} - the height of the viewport + */ + function setSize(w, h) { + // Skip if there's no change + if (width === w && h === h) { + return; + } + + width = w; + height = h; + + shader.bind(); + shader.setPMatrix(orthoMatrix(width, height)); + } + + /* + * Init OpenGL + * @param canvas {HTMLCanvas} - the canvas to render to + */ + function init(canvas, noFlush) { + var i = 0, + contexts = [ + 'webgl', + 'experimental-webgl', + 'moz-webgl', + 'webkit-3d' + ]; + + isInited = false; + + if (!canvas) { + return false; + } + + if (settings.debug.timeSetup) { + console.time('gl setup'); // eslint-disable-line no-console + } + + for (; i < contexts.length; i++) { + gl = canvas.getContext(contexts[i], { + // premultipliedAlpha: false + }); + if (gl) { + break; + } + } + + if (gl) { + if (!noFlush) { + flush(); + } + } else { + return false; + } + + gl.enable(gl.BLEND); + // gl.blendFunc(gl.SRC_ALPHA, gl.ONE); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.disable(gl.DEPTH_TEST); + // gl.depthMask(gl.FALSE); + gl.depthFunc(gl.LESS); + + shader = GLShader(gl); // eslint-disable-line new-cap + vbuffer = GLVertexBuffer(gl, shader); // eslint-disable-line new-cap + + textureIsReady = false; + + // Set up the circle texture used for bubbles + circleTextureHandle = gl.createTexture(); + + // Draw the circle + circleTexture.width = 512; + circleTexture.height = 512; + + circleCtx.mozImageSmoothingEnabled = false; + circleCtx.webkitImageSmoothingEnabled = false; + circleCtx.msImageSmoothingEnabled = false; + circleCtx.imageSmoothingEnabled = false; + + circleCtx.strokeStyle = 'rgba(255, 255, 255, 0)'; + circleCtx.fillStyle = '#FFF'; + + circleCtx.beginPath(); + circleCtx.arc(256, 256, 256, 0, 2 * Math.PI); + circleCtx.stroke(); + circleCtx.fill(); + + try { + + gl.bindTexture(gl.TEXTURE_2D, circleTextureHandle); + // gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + circleTexture + ); + + gl.texParameteri( + gl.TEXTURE_2D, + gl.TEXTURE_WRAP_S, + gl.CLAMP_TO_EDGE + ); + gl.texParameteri( + gl.TEXTURE_2D, + gl.TEXTURE_WRAP_T, + gl.CLAMP_TO_EDGE + ); + gl.texParameteri( + gl.TEXTURE_2D, + gl.TEXTURE_MAG_FILTER, + gl.LINEAR + ); + gl.texParameteri( + gl.TEXTURE_2D, + gl.TEXTURE_MIN_FILTER, + gl.LINEAR + ); + + // gl.generateMipmap(gl.TEXTURE_2D); + + gl.bindTexture(gl.TEXTURE_2D, null); + + textureIsReady = true; + } catch (e) {} + + isInited = true; + + if (settings.debug.timeSetup) { + console.timeEnd('gl setup'); // eslint-disable-line no-console + } + + return true; + } + + /* + * Check if we have a valid OGL context + * @returns {Boolean} - true if the context is valid + */ + function valid() { + return gl !== false; + } + + /* + * Check if the renderer has been initialized + * @returns {Boolean} - true if it has, false if not + */ + function inited() { + return isInited; + } + + function destroy() { + flush(); + vbuffer.destroy(); + shader.destroy(); + if (gl) { + if (circleTextureHandle) { + gl.deleteTexture(circleTextureHandle); + } + gl.canvas.width = 1; + gl.canvas.height = 1; + } + } + + // ///////////////////////////////////////////////////////////////////////// + exports = { + allocateBufferForSingleSeries: allocateBufferForSingleSeries, + pushSeries: pushSeries, + setSize: setSize, + inited: inited, + setThreshold: setThreshold, + init: init, + render: renderWhenReady, + settings: settings, + valid: valid, + clear: clear, + flush: flush, + setXAxis: setXAxis, + setYAxis: setYAxis, + data: data, + gl: getGL, + allocateBuffer: allocateBuffer, + destroy: destroy, + setOptions: setOptions + }; + + return exports; } // END OF WEBGL ABSTRACTIONS @@ -2465,150 +2466,150 @@ function GLRenderer(postRenderCallback) { * @param chart {Highcharts.Chart} - the chart */ function createAndAttachRenderer(chart, series) { - var width = chart.chartWidth, - height = chart.chartHeight, - target = chart, - targetGroup = chart.seriesGroup || series.group, - alpha = 1, - foSupported = doc.implementation.hasFeature( - 'www.http://w3.org/TR/SVG11/feature#Extensibility', - '1.1' - ); - - if (chart.isChartSeriesBoosting()) { - target = chart; - } else { - target = series; - } - - // Support for foreignObject is flimsy as best. - // IE does not support it, and Chrome has a bug which messes up - // the canvas draw order. - // As such, we force the Image fallback for now, but leaving the - // actual Canvas path in-place in case this changes in the future. - foSupported = false; - - if (!target.renderTarget) { - target.canvas = mainCanvas; - - // Fall back to image tag if foreignObject isn't supported, - // or if we're exporting. - if (chart.renderer.forExport || !foSupported) { - target.renderTarget = chart.renderer.image( - '', - 0, - 0, - width, - height - ) - .addClass('highcharts-boost-canvas') - .add(targetGroup); - - target.boostClear = function () { - target.renderTarget.attr({ href: '' }); - }; - - target.boostCopy = function () { - target.boostResizeTarget(); - target.renderTarget.attr({ - href: target.canvas.toDataURL('image/png') - }); - }; - - } else { - target.renderTargetFo = chart.renderer - .createElement('foreignObject') - .add(targetGroup); - - target.renderTarget = doc.createElement('canvas'); - target.renderTargetCtx = target.renderTarget.getContext('2d'); - - target.renderTargetFo.element.appendChild(target.renderTarget); - - target.boostClear = function () { - target.renderTarget.width = target.canvas.width; - target.renderTarget.height = target.canvas.height; - }; - - target.boostCopy = function () { - target.renderTarget.width = target.canvas.width; - target.renderTarget.height = target.canvas.height; - - target.renderTargetCtx.drawImage(target.canvas, 0, 0); - }; - } - - target.boostResizeTarget = function () { - width = chart.chartWidth; - height = chart.chartHeight; - - (target.renderTargetFo || target.renderTarget) - .attr({ - x: 0, - y: 0, - width: width, - height: height - }) - .css({ - pointerEvents: 'none', - mixedBlendMode: 'normal', - opacity: alpha - }); - - if (target instanceof H.Chart) { - target.markerGroup.translate( - chart.plotLeft, - chart.plotTop - ); - } - }; - - target.boostClipRect = chart.renderer.clipRect(); - - (target.renderTargetFo || target.renderTarget) - .clip(target.boostClipRect); - - if (target instanceof H.Chart) { - target.markerGroup = target.renderer.g().add(targetGroup); - - target.markerGroup.translate(series.xAxis.pos, series.yAxis.pos); - } - } - - target.canvas.width = width; - target.canvas.height = height; - - target.boostClipRect.attr(chart.getBoostClipRect(target)); - - target.boostResizeTarget(); - target.boostClear(); - - if (!target.ogl) { - target.ogl = GLRenderer(function () { // eslint-disable-line new-cap - if (target.ogl.settings.debug.timeBufferCopy) { - console.time('buffer copy'); // eslint-disable-line no-console - } - - target.boostCopy(); - - if (target.ogl.settings.debug.timeBufferCopy) { - console.timeEnd('buffer copy'); // eslint-disable-line no-console - } - - }); // eslint-disable-line new-cap - - target.ogl.init(target.canvas); - // target.ogl.clear(); - target.ogl.setOptions(chart.options.boost || {}); - - if (target instanceof H.Chart) { - target.ogl.allocateBuffer(chart); - } - } - - target.ogl.setSize(width, height); + var width = chart.chartWidth, + height = chart.chartHeight, + target = chart, + targetGroup = chart.seriesGroup || series.group, + alpha = 1, + foSupported = doc.implementation.hasFeature( + 'www.http://w3.org/TR/SVG11/feature#Extensibility', + '1.1' + ); + + if (chart.isChartSeriesBoosting()) { + target = chart; + } else { + target = series; + } + + // Support for foreignObject is flimsy as best. + // IE does not support it, and Chrome has a bug which messes up + // the canvas draw order. + // As such, we force the Image fallback for now, but leaving the + // actual Canvas path in-place in case this changes in the future. + foSupported = false; + + if (!target.renderTarget) { + target.canvas = mainCanvas; + + // Fall back to image tag if foreignObject isn't supported, + // or if we're exporting. + if (chart.renderer.forExport || !foSupported) { + target.renderTarget = chart.renderer.image( + '', + 0, + 0, + width, + height + ) + .addClass('highcharts-boost-canvas') + .add(targetGroup); + + target.boostClear = function () { + target.renderTarget.attr({ href: '' }); + }; + + target.boostCopy = function () { + target.boostResizeTarget(); + target.renderTarget.attr({ + href: target.canvas.toDataURL('image/png') + }); + }; + + } else { + target.renderTargetFo = chart.renderer + .createElement('foreignObject') + .add(targetGroup); + + target.renderTarget = doc.createElement('canvas'); + target.renderTargetCtx = target.renderTarget.getContext('2d'); + + target.renderTargetFo.element.appendChild(target.renderTarget); + + target.boostClear = function () { + target.renderTarget.width = target.canvas.width; + target.renderTarget.height = target.canvas.height; + }; + + target.boostCopy = function () { + target.renderTarget.width = target.canvas.width; + target.renderTarget.height = target.canvas.height; + + target.renderTargetCtx.drawImage(target.canvas, 0, 0); + }; + } + + target.boostResizeTarget = function () { + width = chart.chartWidth; + height = chart.chartHeight; + + (target.renderTargetFo || target.renderTarget) + .attr({ + x: 0, + y: 0, + width: width, + height: height + }) + .css({ + pointerEvents: 'none', + mixedBlendMode: 'normal', + opacity: alpha + }); + + if (target instanceof H.Chart) { + target.markerGroup.translate( + chart.plotLeft, + chart.plotTop + ); + } + }; + + target.boostClipRect = chart.renderer.clipRect(); + + (target.renderTargetFo || target.renderTarget) + .clip(target.boostClipRect); + + if (target instanceof H.Chart) { + target.markerGroup = target.renderer.g().add(targetGroup); + + target.markerGroup.translate(series.xAxis.pos, series.yAxis.pos); + } + } + + target.canvas.width = width; + target.canvas.height = height; + + target.boostClipRect.attr(chart.getBoostClipRect(target)); + + target.boostResizeTarget(); + target.boostClear(); + + if (!target.ogl) { + target.ogl = GLRenderer(function () { // eslint-disable-line new-cap + if (target.ogl.settings.debug.timeBufferCopy) { + console.time('buffer copy'); // eslint-disable-line no-console + } + + target.boostCopy(); + + if (target.ogl.settings.debug.timeBufferCopy) { + console.timeEnd('buffer copy'); // eslint-disable-line no-console + } + + }); // eslint-disable-line new-cap + + target.ogl.init(target.canvas); + // target.ogl.clear(); + target.ogl.setOptions(chart.options.boost || {}); + + if (target instanceof H.Chart) { + target.ogl.allocateBuffer(chart); + } + } + + target.ogl.setSize(width, height); - return target.ogl; + return target.ogl; } /* @@ -2618,23 +2619,23 @@ function createAndAttachRenderer(chart, series) { * @param series {Highcharts.Series} - the series */ function renderIfNotSeriesBoosting(renderer, series, chart) { - if (renderer && - series.renderTarget && - series.canvas && - !(chart || series.chart).isChartSeriesBoosting() - ) { - renderer.render(chart || series.chart); - } + if (renderer && + series.renderTarget && + series.canvas && + !(chart || series.chart).isChartSeriesBoosting() + ) { + renderer.render(chart || series.chart); + } } function allocateIfNotSeriesBoosting(renderer, series) { - if (renderer && - series.renderTarget && - series.canvas && - !series.chart.isChartSeriesBoosting() - ) { - renderer.allocateBufferForSingleSeries(series); - } + if (renderer && + series.renderTarget && + series.canvas && + !series.chart.isChartSeriesBoosting() + ) { + renderer.allocateBufferForSingleSeries(series); + } } /* @@ -2649,37 +2650,37 @@ function allocateIfNotSeriesBoosting(renderer, series) { * @param noTimeout {Boolean} - set to true to skip timeouts */ H.eachAsync = function (arr, fn, finalFunc, chunkSize, i, noTimeout) { - i = i || 0; - chunkSize = chunkSize || CHUNK_SIZE; - - var threshold = i + chunkSize, - proceed = true; - - while (proceed && i < threshold && i < arr.length) { - proceed = fn(arr[i], i); - ++i; - } - - if (proceed) { - if (i < arr.length) { - - if (noTimeout) { - H.eachAsync(arr, fn, finalFunc, chunkSize, i, noTimeout); - } else if (win.requestAnimationFrame) { - // If available, do requestAnimationFrame - shaves off a few ms - win.requestAnimationFrame(function () { - H.eachAsync(arr, fn, finalFunc, chunkSize, i); - }); - } else { - setTimeout(function () { - H.eachAsync(arr, fn, finalFunc, chunkSize, i); - }); - } - - } else if (finalFunc) { - finalFunc(); - } - } + i = i || 0; + chunkSize = chunkSize || CHUNK_SIZE; + + var threshold = i + chunkSize, + proceed = true; + + while (proceed && i < threshold && i < arr.length) { + proceed = fn(arr[i], i); + ++i; + } + + if (proceed) { + if (i < arr.length) { + + if (noTimeout) { + H.eachAsync(arr, fn, finalFunc, chunkSize, i, noTimeout); + } else if (win.requestAnimationFrame) { + // If available, do requestAnimationFrame - shaves off a few ms + win.requestAnimationFrame(function () { + H.eachAsync(arr, fn, finalFunc, chunkSize, i); + }); + } else { + setTimeout(function () { + H.eachAsync(arr, fn, finalFunc, chunkSize, i); + }); + } + + } else if (finalFunc) { + finalFunc(); + } + } }; // ///////////////////////////////////////////////////////////////////////////// @@ -2692,36 +2693,36 @@ H.eachAsync = function (arr, fn, finalFunc, chunkSize, i, noTimeout) { * @returns {Object} A Point object as per http://api.highcharts.com/highcharts#Point */ Series.prototype.getPoint = function (boostPoint) { - var point = boostPoint, - xData = this.xData || this.options.xData || this.processedXData || false - ; - - if (boostPoint && !(boostPoint instanceof this.pointClass)) { - point = (new this.pointClass()).init( // eslint-disable-line new-cap - this, - this.options.data[boostPoint.i], - xData ? xData[boostPoint.i] : undefined - ); - - point.category = point.x; - - point.dist = boostPoint.dist; - point.distX = boostPoint.distX; - point.plotX = boostPoint.plotX; - point.plotY = boostPoint.plotY; - point.index = boostPoint.i; - } - - return point; + var point = boostPoint, + xData = this.xData || this.options.xData || this.processedXData || false + ; + + if (boostPoint && !(boostPoint instanceof this.pointClass)) { + point = (new this.pointClass()).init( // eslint-disable-line new-cap + this, + this.options.data[boostPoint.i], + xData ? xData[boostPoint.i] : undefined + ); + + point.category = point.x; + + point.dist = boostPoint.dist; + point.distX = boostPoint.distX; + point.plotX = boostPoint.plotX; + point.plotY = boostPoint.plotY; + point.index = boostPoint.i; + } + + return point; }; /** * Return a point instance from the k-d-tree */ wrap(Series.prototype, 'searchPoint', function (proceed) { - return this.getPoint( - proceed.apply(this, [].slice.call(arguments, 1)) - ); + return this.getPoint( + proceed.apply(this, [].slice.call(arguments, 1)) + ); }); /** @@ -2730,22 +2731,22 @@ wrap(Series.prototype, 'searchPoint', function (proceed) { * but the fake search points are not registered like that. */ addEvent(Series, 'destroy', function () { - var series = this, - chart = series.chart; - - if (chart.markerGroup === series.markerGroup) { - series.markerGroup = null; - } - - if (chart.hoverPoints) { - chart.hoverPoints = grep(chart.hoverPoints, function (point) { - return point.series === series; - }); - } - - if (chart.hoverPoint && chart.hoverPoint.series === series) { - chart.hoverPoint = null; - } + var series = this, + chart = series.chart; + + if (chart.markerGroup === series.markerGroup) { + series.markerGroup = null; + } + + if (chart.hoverPoints) { + chart.hoverPoints = grep(chart.hoverPoints, function (point) { + return point.series === series; + }); + } + + if (chart.hoverPoint && chart.hoverPoint.series === series) { + chart.hoverPoint = null; + } }); /** @@ -2754,21 +2755,21 @@ addEvent(Series, 'destroy', function () { * to hasExtremes to the methods directly. */ wrap(Series.prototype, 'getExtremes', function (proceed) { - if (!this.isSeriesBoosting || (!this.hasExtremes || !this.hasExtremes())) { - return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + if (!this.isSeriesBoosting || (!this.hasExtremes || !this.hasExtremes())) { + return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } }); // Set default options each(boostable, - function (type) { - if (plotOptions[type]) { - plotOptions[type].boostThreshold = 5000; - plotOptions[type].boostData = []; - - seriesTypes[type].prototype.fillOpacity = true; - } - } + function (type) { + if (plotOptions[type]) { + plotOptions[type].boostThreshold = 5000; + plotOptions[type].boostData = []; + + seriesTypes[type].prototype.fillOpacity = true; + } + } ); /** @@ -2779,55 +2780,55 @@ each(boostable, * Note that we're not overriding any of these for heatmaps. */ each([ - 'translate', - 'generatePoints', - 'drawTracker', - 'drawPoints', - 'render' + 'translate', + 'generatePoints', + 'drawTracker', + 'drawPoints', + 'render' ], function (method) { - function branch(proceed) { - var letItPass = this.options.stacking && - (method === 'translate' || method === 'generatePoints'), - enabled = pick( - ( - this.chart && - this.chart.options && - this.chart.options.boost && - this.chart.options.boost.enabled - ), - true - ); - - if ( - !this.isSeriesBoosting || - letItPass || - !enabled || - this.type === 'heatmap' || - this.type === 'treemap' || - !boostableMap[this.type] - ) { - - proceed.call(this); - - // If a canvas version of the method exists, like renderCanvas(), run - } else if (this[method + 'Canvas']) { - this[method + 'Canvas'](); - } - } - - wrap(Series.prototype, method, branch); - - // A special case for some types - their translate method is already wrapped - if (method === 'translate') { - each( - ['column', 'bar', 'arearange', 'columnrange', 'heatmap', 'treemap'], - function (type) { - if (seriesTypes[type]) { - wrap(seriesTypes[type].prototype, method, branch); - } - } - ); - } + function branch(proceed) { + var letItPass = this.options.stacking && + (method === 'translate' || method === 'generatePoints'), + enabled = pick( + ( + this.chart && + this.chart.options && + this.chart.options.boost && + this.chart.options.boost.enabled + ), + true + ); + + if ( + !this.isSeriesBoosting || + letItPass || + !enabled || + this.type === 'heatmap' || + this.type === 'treemap' || + !boostableMap[this.type] + ) { + + proceed.call(this); + + // If a canvas version of the method exists, like renderCanvas(), run + } else if (this[method + 'Canvas']) { + this[method + 'Canvas'](); + } + } + + wrap(Series.prototype, method, branch); + + // A special case for some types - their translate method is already wrapped + if (method === 'translate') { + each( + ['column', 'bar', 'arearange', 'columnrange', 'heatmap', 'treemap'], + function (type) { + if (seriesTypes[type]) { + wrap(seriesTypes[type].prototype, method, branch); + } + } + ); + } }); /** If the series is a heatmap or treemap, or if the series is not boosting @@ -2836,60 +2837,60 @@ each([ */ wrap(Series.prototype, 'processData', function (proceed) { - var series = this, - dataToMeasure = this.options.data; - - // Used twice in this function, first on this.options.data, the second - // time it runs the check again after processedXData is built. - // @todo Check what happens with data grouping - function getSeriesBoosting(data) { - return series.chart.isChartSeriesBoosting() || ( - (data ? data.length : 0) >= - (series.options.boostThreshold || Number.MAX_VALUE) - ); - } - - if (boostableMap[this.type]) { - - // If there are no extremes given in the options, we also need to - // process the data to read the data extremes. If this is a heatmap, do - // default behaviour. - if ( - !getSeriesBoosting(dataToMeasure) || // First pass with options.data - this.type === 'heatmap' || - this.type === 'treemap' || - this.options.stacking || // processedYData for the stack (#7481) - !this.hasExtremes || - !this.hasExtremes(true) - ) { - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - dataToMeasure = this.processedXData; - } - - // Set the isBoosting flag, second pass with processedXData to see if we - // have zoomed. - this.isSeriesBoosting = getSeriesBoosting(dataToMeasure); - - // Enter or exit boost mode - if (this.isSeriesBoosting) { - this.enterBoost(); - } else if (this.exitBoost) { - this.exitBoost(); - } - - // The series type is not boostable - } else { - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + var series = this, + dataToMeasure = this.options.data; + + // Used twice in this function, first on this.options.data, the second + // time it runs the check again after processedXData is built. + // @todo Check what happens with data grouping + function getSeriesBoosting(data) { + return series.chart.isChartSeriesBoosting() || ( + (data ? data.length : 0) >= + (series.options.boostThreshold || Number.MAX_VALUE) + ); + } + + if (boostableMap[this.type]) { + + // If there are no extremes given in the options, we also need to + // process the data to read the data extremes. If this is a heatmap, do + // default behaviour. + if ( + !getSeriesBoosting(dataToMeasure) || // First pass with options.data + this.type === 'heatmap' || + this.type === 'treemap' || + this.options.stacking || // processedYData for the stack (#7481) + !this.hasExtremes || + !this.hasExtremes(true) + ) { + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + dataToMeasure = this.processedXData; + } + + // Set the isBoosting flag, second pass with processedXData to see if we + // have zoomed. + this.isSeriesBoosting = getSeriesBoosting(dataToMeasure); + + // Enter or exit boost mode + if (this.isSeriesBoosting) { + this.enterBoost(); + } else if (this.exitBoost) { + this.exitBoost(); + } + + // The series type is not boostable + } else { + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } }); addEvent(Series, 'hide', function () { - if (this.canvas && this.renderTarget) { - if (this.ogl) { - this.ogl.clear(); - } - this.boostClear(); - } + if (this.canvas && this.renderTarget) { + if (this.ogl) { + this.ogl.clear(); + } + this.boostClear(); + } }); @@ -2898,63 +2899,63 @@ addEvent(Series, 'hide', function () { */ Series.prototype.enterBoost = function () { - this.alteredByBoost = []; - - // Save the original values, including whether it was an own property or - // inherited from the prototype. - each(['allowDG', 'directTouch', 'stickyTracking'], function (prop) { - this.alteredByBoost.push({ - prop: prop, - val: this[prop], - own: this.hasOwnProperty(prop) - }); - }, this); - - this.allowDG = false; - this.directTouch = false; - this.stickyTracking = true; - - // Once we've been in boost mode, we don't want animation when returning to - // vanilla mode. - this.animate = null; - - // Hide series label if any - if (this.labelBySeries) { - this.labelBySeries = this.labelBySeries.destroy(); - } + this.alteredByBoost = []; + + // Save the original values, including whether it was an own property or + // inherited from the prototype. + each(['allowDG', 'directTouch', 'stickyTracking'], function (prop) { + this.alteredByBoost.push({ + prop: prop, + val: this[prop], + own: this.hasOwnProperty(prop) + }); + }, this); + + this.allowDG = false; + this.directTouch = false; + this.stickyTracking = true; + + // Once we've been in boost mode, we don't want animation when returning to + // vanilla mode. + this.animate = null; + + // Hide series label if any + if (this.labelBySeries) { + this.labelBySeries = this.labelBySeries.destroy(); + } }; /** * Exit from boost mode and restore non-boost properties. */ Series.prototype.exitBoost = function () { - // Reset instance properties and/or delete instance properties and go back - // to prototype - each(this.alteredByBoost || [], function (setting) { - if (setting.own) { - this[setting.prop] = setting.val; - } else { - // Revert to prototype - delete this[setting.prop]; - } - }, this); - - // Clear previous run - if (this.boostClear) { - this.boostClear(); - } + // Reset instance properties and/or delete instance properties and go back + // to prototype + each(this.alteredByBoost || [], function (setting) { + if (setting.own) { + this[setting.prop] = setting.val; + } else { + // Revert to prototype + delete this[setting.prop]; + } + }, this); + + // Clear previous run + if (this.boostClear) { + this.boostClear(); + } }; Series.prototype.hasExtremes = function (checkX) { - var options = this.options, - data = options.data, - xAxis = this.xAxis && this.xAxis.options, - yAxis = this.yAxis && this.yAxis.options; - - return data.length > (options.boostThreshold || Number.MAX_VALUE) && - isNumber(yAxis.min) && isNumber(yAxis.max) && - (!checkX || (isNumber(xAxis.min) && isNumber(xAxis.max))); + var options = this.options, + data = options.data, + xAxis = this.xAxis && this.xAxis.options, + yAxis = this.yAxis && this.yAxis.options; + + return data.length > (options.boostThreshold || Number.MAX_VALUE) && + isNumber(yAxis.min) && isNumber(yAxis.max) && + (!checkX || (isNumber(xAxis.min) && isNumber(xAxis.max))); }; /** @@ -2962,25 +2963,25 @@ Series.prototype.hasExtremes = function (checkX) { * shared with other similar methods in Highcharts. */ Series.prototype.destroyGraphics = function () { - var series = this, - points = this.points, - point, - i; - - if (points) { - for (i = 0; i < points.length; i = i + 1) { - point = points[i]; - if (point && point.destroyElements) { - point.destroyElements(); // #7557 - } - } - } - - each(['graph', 'area', 'tracker'], function (prop) { - if (series[prop]) { - series[prop] = series[prop].destroy(); - } - }); + var series = this, + points = this.points, + point, + i; + + if (points) { + for (i = 0; i < points.length; i = i + 1) { + point = points[i]; + if (point && point.destroyElements) { + point.destroyElements(); // #7557 + } + } + } + + each(['graph', 'area', 'tracker'], function (prop) { + if (series[prop]) { + series[prop] = series[prop].destroy(); + } + }); }; @@ -2989,413 +2990,414 @@ Series.prototype.destroyGraphics = function () { * Returns true if the current browser supports webgl */ H.hasWebGLSupport = function () { - var i = 0, - canvas, - contexts = ['webgl', 'experimental-webgl', 'moz-webgl', 'webkit-3d'], - context = false; - - if (typeof win.WebGLRenderingContext !== 'undefined') { - canvas = doc.createElement('canvas'); - - for (; i < contexts.length; i++) { - try { - context = canvas.getContext(contexts[i]); - if (typeof context !== 'undefined' && context !== null) { - return true; - } - } catch (e) { - - } - } - } - - return false; + var i = 0, + canvas, + contexts = ['webgl', 'experimental-webgl', 'moz-webgl', 'webkit-3d'], + context = false; + + if (typeof win.WebGLRenderingContext !== 'undefined') { + canvas = doc.createElement('canvas'); + + for (; i < contexts.length; i++) { + try { + context = canvas.getContext(contexts[i]); + if (typeof context !== 'undefined' && context !== null) { + return true; + } + } catch (e) { + + } + } + } + + return false; }; /* Used for treemap|heatmap.drawPoints */ function pointDrawHandler(proceed) { - var enabled = true, - renderer; + var enabled = true, + renderer; - if (this.chart.options && this.chart.options.boost) { - enabled = typeof this.chart.options.boost.enabled === 'undefined' ? - true : - this.chart.options.boost.enabled; - } + if (this.chart.options && this.chart.options.boost) { + enabled = typeof this.chart.options.boost.enabled === 'undefined' ? + true : + this.chart.options.boost.enabled; + } - if (!enabled || !this.isSeriesBoosting) { - return proceed.call(this); - } + if (!enabled || !this.isSeriesBoosting) { + return proceed.call(this); + } - this.chart.isBoosting = true; + this.chart.isBoosting = true; - // Make sure we have a valid OGL context - renderer = createAndAttachRenderer(this.chart, this); + // Make sure we have a valid OGL context + renderer = createAndAttachRenderer(this.chart, this); - if (renderer) { - allocateIfNotSeriesBoosting(renderer, this); - renderer.pushSeries(this); - } + if (renderer) { + allocateIfNotSeriesBoosting(renderer, this); + renderer.pushSeries(this); + } - renderIfNotSeriesBoosting(renderer, this); + renderIfNotSeriesBoosting(renderer, this); } // ///////////////////////////////////////////////////////////////////////////// // We're wrapped in a closure, so just return if there's no webgl support if (!H.hasWebGLSupport()) { - if (typeof H.initCanvasBoost !== 'undefined') { - // Fallback to canvas boost - H.initCanvasBoost(); - } else { - H.error(26); - } + if (typeof H.initCanvasBoost !== 'undefined') { + // Fallback to canvas boost + H.initCanvasBoost(); + } else { + H.error(26); + } } else { - // ///////////////////////////////////////////////////////////////////////// - // GL-SPECIFIC WRAPPINGS FOLLOWS - - - - H.extend(Series.prototype, { - - renderCanvas: function () { - var series = this, - options = series.options || {}, - renderer = false, - chart = series.chart, - xAxis = this.xAxis, - yAxis = this.yAxis, - xData = options.xData || series.processedXData, - yData = options.yData || series.processedYData, - rawData = options.data, - xExtremes = xAxis.getExtremes(), - xMin = xExtremes.min, - xMax = xExtremes.max, - yExtremes = yAxis.getExtremes(), - yMin = yExtremes.min, - yMax = yExtremes.max, - pointTaken = {}, - lastClientX, - sampling = !!series.sampling, - points, - enableMouseTracking = options.enableMouseTracking !== false, - threshold = options.threshold, - yBottom = yAxis.getThreshold(threshold), - isRange = series.pointArrayMap && - series.pointArrayMap.join(',') === 'low,high', - isStacked = !!options.stacking, - cropStart = series.cropStart || 0, - requireSorting = series.requireSorting, - useRaw = !xData, - minVal, - maxVal, - minI, - maxI, - boostOptions, - - xDataFull = ( - this.xData || - this.options.xData || - this.processedXData || - false - ), - - addKDPoint = function (clientX, plotY, i) { - // Shaves off about 60ms compared to repeated concatination - index = clientX + ',' + plotY; - - // The k-d tree requires series points. - // Reduce the amount of points, since the time to build the - // tree increases exponentially. - if (enableMouseTracking && !pointTaken[index]) { - pointTaken[index] = true; - - if (chart.inverted) { - clientX = xAxis.len - clientX; - plotY = yAxis.len - plotY; - } - - points.push({ - x: xDataFull ? xDataFull[cropStart + i] : false, - clientX: clientX, - plotX: clientX, - plotY: plotY, - i: cropStart + i - }); - } - }; - - // Get or create the renderer - renderer = createAndAttachRenderer(chart, series); - - chart.isBoosting = true; - - boostOptions = renderer.settings; - - if (!this.visible) { - return; - } - - // If we are zooming out from SVG mode, destroy the graphics - if (this.points || this.graph) { - - this.animate = null; - this.destroyGraphics(); - } - - // If we're rendering per. series we should create the marker groups - // as usual. - if (!chart.isChartSeriesBoosting()) { - this.markerGroup = series.plotGroup( - 'markerGroup', - 'markers', - true, - 1, - chart.seriesGroup - ); - } else { - // Use a single group for the markers - this.markerGroup = chart.markerGroup; - - // When switching from chart boosting mode, destroy redundant - // series boosting targets - if (this.renderTarget) { - this.renderTarget = this.renderTarget.destroy(); - } - } - - points = this.points = []; - - // Do not start building while drawing - series.buildKDTree = noop; - - if (renderer) { - allocateIfNotSeriesBoosting(renderer, this); - renderer.pushSeries(series); - // Perform the actual renderer if we're on series level - renderIfNotSeriesBoosting(renderer, this, chart); - // console.log(series, chart); - } - - /* This builds the KD-tree */ - function processPoint(d, i) { - var x, - y, - clientX, - plotY, - isNull, - low = false, - chartDestroyed = typeof chart.index === 'undefined', - isYInside = true; - - if (!chartDestroyed) { - if (useRaw) { - x = d[0]; - y = d[1]; - } else { - x = d; - y = yData[i]; - } - - // Resolve low and high for range series - if (isRange) { - if (useRaw) { - y = d.slice(1, 3); - } - low = y[0]; - y = y[1]; - } else if (isStacked) { - x = d.x; - y = d.stackY; - low = y - d.y; - } - - isNull = y === null; - - // Optimize for scatter zooming - if (!requireSorting) { - isYInside = y >= yMin && y <= yMax; - } - - if (!isNull && x >= xMin && x <= xMax && isYInside) { - - // We use ceil to allow the KD tree to work with sub - // pixels, which can be used in boost to space pixels - clientX = Math.ceil(xAxis.toPixels(x, true)); - - if (sampling) { - if (minI === undefined || clientX === lastClientX) { - if (!isRange) { - low = y; - } - if (maxI === undefined || y > maxVal) { - maxVal = y; - maxI = i; - } - if (minI === undefined || low < minVal) { - minVal = low; - minI = i; - } - - } - // Add points and reset - if (clientX !== lastClientX) { - if (minI !== undefined) { // maxI is number too - plotY = yAxis.toPixels(maxVal, true); - yBottom = yAxis.toPixels(minVal, true); - - addKDPoint(clientX, plotY, maxI); - if (yBottom !== plotY) { - addKDPoint(clientX, yBottom, minI); - } - } - - minI = maxI = undefined; - lastClientX = clientX; - } - } else { - plotY = Math.ceil(yAxis.toPixels(y, true)); - addKDPoint(clientX, plotY, i); - } - } - } - - return !chartDestroyed; - } - - function doneProcessing() { - fireEvent(series, 'renderedCanvas'); - - // Go back to prototype, ready to build - delete series.buildKDTree; - series.buildKDTree(); - - if (boostOptions.debug.timeKDTree) { - console.timeEnd('kd tree building'); // eslint-disable-line no-console - } - } - - // Loop over the points to build the k-d tree - skip this if - // exporting - if (!chart.renderer.forExport) { - if (boostOptions.debug.timeKDTree) { - console.time('kd tree building'); // eslint-disable-line no-console - } - - H.eachAsync( - isStacked ? series.data : (xData || rawData), - processPoint, - doneProcessing - ); - } - } - }); - - /* - * We need to handle heatmaps separatly, since we can't perform the - * size/color calculations in the shader easily. - * - * This likely needs future optimization. - * - */ - each(['heatmap', 'treemap'], - function (t) { - if (seriesTypes[t]) { - wrap(seriesTypes[t].prototype, 'drawPoints', pointDrawHandler); - } - } - ); - - if (seriesTypes.bubble) { - // By default, the bubble series does not use the KD-tree, so force it - // to. - delete seriesTypes.bubble.prototype.buildKDTree; - // seriesTypes.bubble.prototype.directTouch = false; - - // Needed for markers to work correctly - wrap( - seriesTypes.bubble.prototype, - 'markerAttribs', - function (proceed) { - if (this.isSeriesBoosting) { - return false; - } - return proceed.apply(this, [].slice.call(arguments, 1)); - } - ); - } - - seriesTypes.scatter.prototype.fill = true; - - extend(seriesTypes.area.prototype, { - fill: true, - fillOpacity: true, - sampling: true - }); - - - extend(seriesTypes.column.prototype, { - fill: true, - sampling: true - }); - - /** - * Take care of the canvas blitting - */ - H.Chart.prototype.callbacks.push(function (chart) { - - /* Convert chart-level canvas to image */ - function canvasToSVG() { - if (chart.ogl && chart.isChartSeriesBoosting()) { - chart.ogl.render(chart); - } - } - - /* Clear chart-level canvas */ - function preRender() { - // Reset force state - chart.boostForceChartBoost = undefined; - chart.boostForceChartBoost = shouldForceChartSeriesBoosting(chart); - chart.isBoosting = false; - - if (!chart.isChartSeriesBoosting() && chart.didBoost) { - chart.didBoost = false; - } - - // Clear the canvas - if (chart.boostClear) { - chart.boostClear(); - } - - if (chart.canvas && chart.ogl && chart.isChartSeriesBoosting()) { - chart.didBoost = true; - - // Allocate - chart.ogl.allocateBuffer(chart); - } - - // see #6518 + #6739 - if ( - chart.markerGroup && - chart.xAxis && - chart.xAxis.length > 0 && - chart.yAxis && - chart.yAxis.length > 0 - ) { - chart.markerGroup.translate( - chart.xAxis[0].pos, - chart.yAxis[0].pos - ); - } - } - - addEvent(chart, 'predraw', preRender); - addEvent(chart, 'render', canvasToSVG); - - // addEvent(chart, 'zoom', function () { - // chart.boostForceChartBoost = shouldForceChartSeriesBoosting(chart); - // }); - - }); + // ///////////////////////////////////////////////////////////////////////// + // GL-SPECIFIC WRAPPINGS FOLLOWS + + + + H.extend(Series.prototype, { + + renderCanvas: function () { + var series = this, + options = series.options || {}, + renderer = false, + chart = series.chart, + xAxis = this.xAxis, + yAxis = this.yAxis, + xData = options.xData || series.processedXData, + yData = options.yData || series.processedYData, + rawData = options.data, + xExtremes = xAxis.getExtremes(), + xMin = xExtremes.min, + xMax = xExtremes.max, + yExtremes = yAxis.getExtremes(), + yMin = yExtremes.min, + yMax = yExtremes.max, + pointTaken = {}, + lastClientX, + sampling = !!series.sampling, + points, + enableMouseTracking = options.enableMouseTracking !== false, + threshold = options.threshold, + yBottom = yAxis.getThreshold(threshold), + isRange = series.pointArrayMap && + series.pointArrayMap.join(',') === 'low,high', + isStacked = !!options.stacking, + cropStart = series.cropStart || 0, + requireSorting = series.requireSorting, + useRaw = !xData, + minVal, + maxVal, + minI, + maxI, + boostOptions, + + xDataFull = ( + this.xData || + this.options.xData || + this.processedXData || + false + ), + + addKDPoint = function (clientX, plotY, i) { + // Shaves off about 60ms compared to repeated concatination + index = clientX + ',' + plotY; + + // The k-d tree requires series points. + // Reduce the amount of points, since the time to build the + // tree increases exponentially. + if (enableMouseTracking && !pointTaken[index]) { + pointTaken[index] = true; + + if (chart.inverted) { + clientX = xAxis.len - clientX; + plotY = yAxis.len - plotY; + } + + points.push({ + x: xDataFull ? xDataFull[cropStart + i] : false, + clientX: clientX, + plotX: clientX, + plotY: plotY, + i: cropStart + i + }); + } + }; + + // Get or create the renderer + renderer = createAndAttachRenderer(chart, series); + + chart.isBoosting = true; + + boostOptions = renderer.settings; + + if (!this.visible) { + return; + } + + // If we are zooming out from SVG mode, destroy the graphics + if (this.points || this.graph) { + + this.animate = null; + this.destroyGraphics(); + } + + // If we're rendering per. series we should create the marker groups + // as usual. + if (!chart.isChartSeriesBoosting()) { + this.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + true, + 1, + chart.seriesGroup + ); + } else { + // Use a single group for the markers + this.markerGroup = chart.markerGroup; + + // When switching from chart boosting mode, destroy redundant + // series boosting targets + if (this.renderTarget) { + this.renderTarget = this.renderTarget.destroy(); + } + } + + points = this.points = []; + + // Do not start building while drawing + series.buildKDTree = noop; + + if (renderer) { + allocateIfNotSeriesBoosting(renderer, this); + renderer.pushSeries(series); + // Perform the actual renderer if we're on series level + renderIfNotSeriesBoosting(renderer, this, chart); + // console.log(series, chart); + } + + /* This builds the KD-tree */ + function processPoint(d, i) { + var x, + y, + clientX, + plotY, + isNull, + low = false, + chartDestroyed = typeof chart.index === 'undefined', + isYInside = true; + + if (!chartDestroyed) { + if (useRaw) { + x = d[0]; + y = d[1]; + } else { + x = d; + y = yData[i]; + } + + // Resolve low and high for range series + if (isRange) { + if (useRaw) { + y = d.slice(1, 3); + } + low = y[0]; + y = y[1]; + } else if (isStacked) { + x = d.x; + y = d.stackY; + low = y - d.y; + } + + isNull = y === null; + + // Optimize for scatter zooming + if (!requireSorting) { + isYInside = y >= yMin && y <= yMax; + } + + if (!isNull && x >= xMin && x <= xMax && isYInside) { + + // We use ceil to allow the KD tree to work with sub + // pixels, which can be used in boost to space pixels + clientX = Math.ceil(xAxis.toPixels(x, true)); + + if (sampling) { + if (minI === undefined || clientX === lastClientX) { + if (!isRange) { + low = y; + } + if (maxI === undefined || y > maxVal) { + maxVal = y; + maxI = i; + } + if (minI === undefined || low < minVal) { + minVal = low; + minI = i; + } + + } + // Add points and reset + if (clientX !== lastClientX) { + if (minI !== undefined) { // maxI is number too + plotY = yAxis.toPixels(maxVal, true); + yBottom = yAxis.toPixels(minVal, true); + + addKDPoint(clientX, plotY, maxI); + if (yBottom !== plotY) { + addKDPoint(clientX, yBottom, minI); + } + } + + minI = maxI = undefined; + lastClientX = clientX; + } + } else { + plotY = Math.ceil(yAxis.toPixels(y, true)); + addKDPoint(clientX, plotY, i); + } + } + } + + return !chartDestroyed; + } + + function doneProcessing() { + fireEvent(series, 'renderedCanvas'); + + // Go back to prototype, ready to build + delete series.buildKDTree; + series.buildKDTree(); + + if (boostOptions.debug.timeKDTree) { + console.timeEnd('kd tree building'); // eslint-disable-line no-console + } + } + + // Loop over the points to build the k-d tree - skip this if + // exporting + if (!chart.renderer.forExport) { + if (boostOptions.debug.timeKDTree) { + console.time('kd tree building'); // eslint-disable-line no-console + } + + H.eachAsync( + isStacked ? series.data : (xData || rawData), + processPoint, + doneProcessing + ); + } + } + }); + + /* + * We need to handle heatmaps separatly, since we can't perform the + * size/color calculations in the shader easily. + * + * This likely needs future optimization. + * + */ + each(['heatmap', 'treemap'], + function (t) { + if (seriesTypes[t]) { + wrap(seriesTypes[t].prototype, 'drawPoints', pointDrawHandler); + } + } + ); + + if (seriesTypes.bubble) { + // By default, the bubble series does not use the KD-tree, so force it + // to. + delete seriesTypes.bubble.prototype.buildKDTree; + // seriesTypes.bubble.prototype.directTouch = false; + + // Needed for markers to work correctly + wrap( + seriesTypes.bubble.prototype, + 'markerAttribs', + function (proceed) { + if (this.isSeriesBoosting) { + return false; + } + return proceed.apply(this, [].slice.call(arguments, 1)); + } + ); + } + + seriesTypes.scatter.prototype.fill = true; + + extend(seriesTypes.area.prototype, { + fill: true, + fillOpacity: true, + sampling: true + }); + + + extend(seriesTypes.column.prototype, { + fill: true, + sampling: true + }); + + /** + * Take care of the canvas blitting + */ + H.Chart.prototype.callbacks.push(function (chart) { + + /* Convert chart-level canvas to image */ + function canvasToSVG() { + if (chart.ogl && chart.isChartSeriesBoosting()) { + chart.ogl.render(chart); + } + } + + /* Clear chart-level canvas */ + function preRender() { + // Reset force state + chart.boostForceChartBoost = undefined; + chart.boostForceChartBoost = shouldForceChartSeriesBoosting(chart); + chart.isBoosting = false; + + if (!chart.isChartSeriesBoosting() && chart.didBoost) { + chart.didBoost = false; + } + + // Clear the canvas + if (chart.boostClear) { + chart.boostClear(); + } + + if (chart.canvas && chart.ogl && chart.isChartSeriesBoosting()) { + chart.didBoost = true; + + // Allocate + chart.ogl.allocateBuffer(chart); + } + + // see #6518 + #6739 + if ( + chart.markerGroup && + chart.xAxis && + chart.xAxis.length > 0 && + chart.yAxis && + chart.yAxis.length > 0 + ) { + chart.markerGroup.translate( + chart.xAxis[0].pos, + chart.yAxis[0].pos + ); + } + } + + addEvent(chart, 'predraw', preRender); + addEvent(chart, 'render', canvasToSVG); + + // addEvent(chart, 'zoom', function () { + // chart.boostForceChartBoost = + // shouldForceChartSeriesBoosting(chart); + // }); + + }); } // if hasCanvasSupport diff --git a/js/modules/broken-axis.src.js b/js/modules/broken-axis.src.js index fb787a0b54d..997e55dcc54 100644 --- a/js/modules/broken-axis.src.js +++ b/js/modules/broken-axis.src.js @@ -10,358 +10,358 @@ import '../parts/Axis.js'; import '../parts/Series.js'; var addEvent = H.addEvent, - pick = H.pick, - wrap = H.wrap, - each = H.each, - extend = H.extend, - isArray = H.isArray, - fireEvent = H.fireEvent, - Axis = H.Axis, - Series = H.Series; + pick = H.pick, + wrap = H.wrap, + each = H.each, + extend = H.extend, + isArray = H.isArray, + fireEvent = H.fireEvent, + Axis = H.Axis, + Series = H.Series; function stripArguments() { - return Array.prototype.slice.call(arguments, 1); + return Array.prototype.slice.call(arguments, 1); } extend(Axis.prototype, { - isInBreak: function (brk, val) { - var ret, - repeat = brk.repeat || Infinity, - from = brk.from, - length = brk.to - brk.from, - test = ( - val >= from ? - (val - from) % repeat : - repeat - ((from - val) % repeat) - ); - - if (!brk.inclusive) { - ret = test < length && test !== 0; - } else { - ret = test <= length; - } - return ret; - }, - - isInAnyBreak: function (val, testKeep) { - - var breaks = this.options.breaks, - i = breaks && breaks.length, - inbrk, - keep, - ret; - - - if (i) { - - while (i--) { - if (this.isInBreak(breaks[i], val)) { - inbrk = true; - if (!keep) { - keep = pick( - breaks[i].showPoints, - this.isXAxis ? false : true - ); - } - } - } - - if (inbrk && testKeep) { - ret = inbrk && !keep; - } else { - ret = inbrk; - } - } - return ret; - } + isInBreak: function (brk, val) { + var ret, + repeat = brk.repeat || Infinity, + from = brk.from, + length = brk.to - brk.from, + test = ( + val >= from ? + (val - from) % repeat : + repeat - ((from - val) % repeat) + ); + + if (!brk.inclusive) { + ret = test < length && test !== 0; + } else { + ret = test <= length; + } + return ret; + }, + + isInAnyBreak: function (val, testKeep) { + + var breaks = this.options.breaks, + i = breaks && breaks.length, + inbrk, + keep, + ret; + + + if (i) { + + while (i--) { + if (this.isInBreak(breaks[i], val)) { + inbrk = true; + if (!keep) { + keep = pick( + breaks[i].showPoints, + this.isXAxis ? false : true + ); + } + } + } + + if (inbrk && testKeep) { + ret = inbrk && !keep; + } else { + ret = inbrk; + } + } + return ret; + } }); addEvent(Axis, 'afterSetTickPositions', function () { - if (this.options.breaks) { - var axis = this, - tickPositions = this.tickPositions, - info = this.tickPositions.info, - newPositions = [], - i; - - for (i = 0; i < tickPositions.length; i++) { - if (!axis.isInAnyBreak(tickPositions[i])) { - newPositions.push(tickPositions[i]); - } - } - - this.tickPositions = newPositions; - this.tickPositions.info = info; - } + if (this.options.breaks) { + var axis = this, + tickPositions = this.tickPositions, + info = this.tickPositions.info, + newPositions = [], + i; + + for (i = 0; i < tickPositions.length; i++) { + if (!axis.isInAnyBreak(tickPositions[i])) { + newPositions.push(tickPositions[i]); + } + } + + this.tickPositions = newPositions; + this.tickPositions.info = info; + } }); // Force Axis to be not-ordinal when breaks are defined addEvent(Axis, 'afterSetOptions', function () { - if (this.options.breaks && this.options.breaks.length) { - this.options.ordinal = false; - } + if (this.options.breaks && this.options.breaks.length) { + this.options.ordinal = false; + } }); addEvent(Axis, 'afterInit', function () { - var axis = this, - breaks; - - breaks = this.options.breaks; - axis.isBroken = (isArray(breaks) && !!breaks.length); - if (axis.isBroken) { - axis.val2lin = function (val) { - var nval = val, - brk, - i; - - for (i = 0; i < axis.breakArray.length; i++) { - brk = axis.breakArray[i]; - if (brk.to <= val) { - nval -= brk.len; - } else if (brk.from >= val) { - break; - } else if (axis.isInBreak(brk, val)) { - nval -= (val - brk.from); - break; - } - } - - return nval; - }; - - axis.lin2val = function (val) { - var nval = val, - brk, - i; - - for (i = 0; i < axis.breakArray.length; i++) { - brk = axis.breakArray[i]; - if (brk.from >= nval) { - break; - } else if (brk.to < nval) { - nval += brk.len; - } else if (axis.isInBreak(brk, nval)) { - nval += brk.len; - } - } - return nval; - }; - - axis.setExtremes = function ( - newMin, - newMax, - redraw, - animation, - eventArguments - ) { - // If trying to set extremes inside a break, extend it to before and - // after the break ( #3857 ) - while (this.isInAnyBreak(newMin)) { - newMin -= this.closestPointRange; - } - while (this.isInAnyBreak(newMax)) { - newMax -= this.closestPointRange; - } - Axis.prototype.setExtremes.call( - this, - newMin, - newMax, - redraw, - animation, - eventArguments - ); - }; - - axis.setAxisTranslation = function (saveOld) { - Axis.prototype.setAxisTranslation.call(this, saveOld); - - var breaks = axis.options.breaks, - breakArrayT = [], // Temporary one - breakArray = [], - length = 0, - inBrk, - repeat, - min = axis.userMin || axis.min, - max = axis.userMax || axis.max, - pointRangePadding = pick(axis.pointRangePadding, 0), - start, - i; - - // Min & max check (#4247) - each(breaks, function (brk) { - repeat = brk.repeat || Infinity; - if (axis.isInBreak(brk, min)) { - min += (brk.to % repeat) - (min % repeat); - } - if (axis.isInBreak(brk, max)) { - max -= (max % repeat) - (brk.from % repeat); - } - }); - - // Construct an array holding all breaks in the axis - each(breaks, function (brk) { - start = brk.from; - repeat = brk.repeat || Infinity; - - while (start - repeat > min) { - start -= repeat; - } - while (start < min) { - start += repeat; - } - - for (i = start; i < max; i += repeat) { - breakArrayT.push({ - value: i, - move: 'in' - }); - breakArrayT.push({ - value: i + (brk.to - brk.from), - move: 'out', - size: brk.breakSize - }); - } - }); - - breakArrayT.sort(function (a, b) { - var ret; - if (a.value === b.value) { - ret = (a.move === 'in' ? 0 : 1) - (b.move === 'in' ? 0 : 1); - } else { - ret = a.value - b.value; - } - return ret; - }); - - // Simplify the breaks - inBrk = 0; - start = min; - - each(breakArrayT, function (brk) { - inBrk += (brk.move === 'in' ? 1 : -1); - - if (inBrk === 1 && brk.move === 'in') { - start = brk.value; - } - if (inBrk === 0) { - breakArray.push({ - from: start, - to: brk.value, - len: brk.value - start - (brk.size || 0) - }); - length += brk.value - start - (brk.size || 0); - } - }); - - axis.breakArray = breakArray; - - // Used with staticScale, and below, the actual axis length when - // breaks are substracted. - axis.unitLength = max - min - length + pointRangePadding; - - fireEvent(axis, 'afterBreaks'); - - if (axis.options.staticScale) { - axis.transA = axis.options.staticScale; - } else if (axis.unitLength) { - axis.transA *= (max - axis.min + pointRangePadding) / - axis.unitLength; - } - - if (pointRangePadding) { - axis.minPixelPadding = axis.transA * axis.minPointOffset; - } - - axis.min = min; - axis.max = max; - }; - } + var axis = this, + breaks; + + breaks = this.options.breaks; + axis.isBroken = (isArray(breaks) && !!breaks.length); + if (axis.isBroken) { + axis.val2lin = function (val) { + var nval = val, + brk, + i; + + for (i = 0; i < axis.breakArray.length; i++) { + brk = axis.breakArray[i]; + if (brk.to <= val) { + nval -= brk.len; + } else if (brk.from >= val) { + break; + } else if (axis.isInBreak(brk, val)) { + nval -= (val - brk.from); + break; + } + } + + return nval; + }; + + axis.lin2val = function (val) { + var nval = val, + brk, + i; + + for (i = 0; i < axis.breakArray.length; i++) { + brk = axis.breakArray[i]; + if (brk.from >= nval) { + break; + } else if (brk.to < nval) { + nval += brk.len; + } else if (axis.isInBreak(brk, nval)) { + nval += brk.len; + } + } + return nval; + }; + + axis.setExtremes = function ( + newMin, + newMax, + redraw, + animation, + eventArguments + ) { + // If trying to set extremes inside a break, extend it to before and + // after the break ( #3857 ) + while (this.isInAnyBreak(newMin)) { + newMin -= this.closestPointRange; + } + while (this.isInAnyBreak(newMax)) { + newMax -= this.closestPointRange; + } + Axis.prototype.setExtremes.call( + this, + newMin, + newMax, + redraw, + animation, + eventArguments + ); + }; + + axis.setAxisTranslation = function (saveOld) { + Axis.prototype.setAxisTranslation.call(this, saveOld); + + var breaks = axis.options.breaks, + breakArrayT = [], // Temporary one + breakArray = [], + length = 0, + inBrk, + repeat, + min = axis.userMin || axis.min, + max = axis.userMax || axis.max, + pointRangePadding = pick(axis.pointRangePadding, 0), + start, + i; + + // Min & max check (#4247) + each(breaks, function (brk) { + repeat = brk.repeat || Infinity; + if (axis.isInBreak(brk, min)) { + min += (brk.to % repeat) - (min % repeat); + } + if (axis.isInBreak(brk, max)) { + max -= (max % repeat) - (brk.from % repeat); + } + }); + + // Construct an array holding all breaks in the axis + each(breaks, function (brk) { + start = brk.from; + repeat = brk.repeat || Infinity; + + while (start - repeat > min) { + start -= repeat; + } + while (start < min) { + start += repeat; + } + + for (i = start; i < max; i += repeat) { + breakArrayT.push({ + value: i, + move: 'in' + }); + breakArrayT.push({ + value: i + (brk.to - brk.from), + move: 'out', + size: brk.breakSize + }); + } + }); + + breakArrayT.sort(function (a, b) { + var ret; + if (a.value === b.value) { + ret = (a.move === 'in' ? 0 : 1) - (b.move === 'in' ? 0 : 1); + } else { + ret = a.value - b.value; + } + return ret; + }); + + // Simplify the breaks + inBrk = 0; + start = min; + + each(breakArrayT, function (brk) { + inBrk += (brk.move === 'in' ? 1 : -1); + + if (inBrk === 1 && brk.move === 'in') { + start = brk.value; + } + if (inBrk === 0) { + breakArray.push({ + from: start, + to: brk.value, + len: brk.value - start - (brk.size || 0) + }); + length += brk.value - start - (brk.size || 0); + } + }); + + axis.breakArray = breakArray; + + // Used with staticScale, and below, the actual axis length when + // breaks are substracted. + axis.unitLength = max - min - length + pointRangePadding; + + fireEvent(axis, 'afterBreaks'); + + if (axis.options.staticScale) { + axis.transA = axis.options.staticScale; + } else if (axis.unitLength) { + axis.transA *= (max - axis.min + pointRangePadding) / + axis.unitLength; + } + + if (pointRangePadding) { + axis.minPixelPadding = axis.transA * axis.minPointOffset; + } + + axis.min = min; + axis.max = max; + }; + } }); wrap(Series.prototype, 'generatePoints', function (proceed) { - proceed.apply(this, stripArguments(arguments)); - - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis, - points = series.points, - point, - i = points.length, - connectNulls = series.options.connectNulls, - nullGap; - - - if (xAxis && yAxis && (xAxis.options.breaks || yAxis.options.breaks)) { - while (i--) { - point = points[i]; - - // Respect nulls inside the break (#4275) - nullGap = point.y === null && connectNulls === false; - if ( - !nullGap && - ( - xAxis.isInAnyBreak(point.x, true) || - yAxis.isInAnyBreak(point.y, true) - ) - ) { - points.splice(i, 1); - if (this.data[i]) { - // Removes the graphics for this point if they exist - this.data[i].destroyElements(); - } - } - } - } + proceed.apply(this, stripArguments(arguments)); + + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + points = series.points, + point, + i = points.length, + connectNulls = series.options.connectNulls, + nullGap; + + + if (xAxis && yAxis && (xAxis.options.breaks || yAxis.options.breaks)) { + while (i--) { + point = points[i]; + + // Respect nulls inside the break (#4275) + nullGap = point.y === null && connectNulls === false; + if ( + !nullGap && + ( + xAxis.isInAnyBreak(point.x, true) || + yAxis.isInAnyBreak(point.y, true) + ) + ) { + points.splice(i, 1); + if (this.data[i]) { + // Removes the graphics for this point if they exist + this.data[i].destroyElements(); + } + } + } + } }); function drawPointsWrapped(proceed) { - proceed.apply(this); - this.drawBreaks(this.xAxis, ['x']); - this.drawBreaks(this.yAxis, pick(this.pointArrayMap, ['y'])); + proceed.apply(this); + this.drawBreaks(this.xAxis, ['x']); + this.drawBreaks(this.yAxis, pick(this.pointArrayMap, ['y'])); } H.Series.prototype.drawBreaks = function (axis, keys) { - var series = this, - points = series.points, - breaks, - threshold, - eventName, - y; - - if (!axis) { - return; // #5950 - } - - each(keys, function (key) { - breaks = axis.breakArray || []; - threshold = axis.isXAxis ? - axis.min : - pick(series.options.threshold, axis.min); - each(points, function (point) { - y = pick(point['stack' + key.toUpperCase()], point[key]); - each(breaks, function (brk) { - eventName = false; - - if ( - (threshold < brk.from && y > brk.to) || - (threshold > brk.from && y < brk.from) - ) { - eventName = 'pointBreak'; - - } else if ( - (threshold < brk.from && y > brk.from && y < brk.to) || - (threshold > brk.from && y > brk.to && y < brk.from) - ) { - eventName = 'pointInBreak'; - } - if (eventName) { - fireEvent(axis, eventName, { point: point, brk: brk }); - } - }); - }); - }); + var series = this, + points = series.points, + breaks, + threshold, + eventName, + y; + + if (!axis) { + return; // #5950 + } + + each(keys, function (key) { + breaks = axis.breakArray || []; + threshold = axis.isXAxis ? + axis.min : + pick(series.options.threshold, axis.min); + each(points, function (point) { + y = pick(point['stack' + key.toUpperCase()], point[key]); + each(breaks, function (brk) { + eventName = false; + + if ( + (threshold < brk.from && y > brk.to) || + (threshold > brk.from && y < brk.from) + ) { + eventName = 'pointBreak'; + + } else if ( + (threshold < brk.from && y > brk.from && y < brk.to) || + (threshold > brk.from && y > brk.to && y < brk.from) + ) { + eventName = 'pointInBreak'; + } + if (eventName) { + fireEvent(axis, eventName, { point: point, brk: brk }); + } + }); + }); + }); }; @@ -371,106 +371,106 @@ H.Series.prototype.drawBreaks = function (axis, keys) { * module as of #5045. */ H.Series.prototype.gappedPath = function () { - var currentDataGrouping = this.currentDataGrouping, - groupingSize = currentDataGrouping && currentDataGrouping.totalRange, - gapSize = this.options.gapSize, - points = this.points.slice(), - i = points.length - 1, - yAxis = this.yAxis, - xRange, - stack; - - /** - * Defines when to display a gap in the graph, together with the - * [gapUnit](plotOptions.series.gapUnit) option. - * - * In case when `dataGrouping` is enabled, points can be grouped into a - * larger time span. This can make the grouped points to have a greater - * distance than the absolute value of `gapSize` property, which will result - * in disappearing graph completely. To prevent this situation the mentioned - * distance between grouped points is used instead of previously defined - * `gapSize`. - * - * In practice, this option is most often used to visualize gaps in - * time series. In a stock chart, intraday data is available for daytime - * hours, while gaps will appear in nights and weekends. - * - * @type {Number} - * @see [gapUnit](plotOptions.series.gapUnit) and - * [xAxis.breaks](#xAxis.breaks) - * @sample {highstock} stock/plotoptions/series-gapsize/ - * Setting the gap size to 2 introduces gaps for weekends in daily - * datasets. - * @default 0 - * @product highstock - * @apioption plotOptions.series.gapSize - */ - - /** - * Together with [gapSize](plotOptions.series.gapSize), this option defines - * where to draw gaps in the graph. - * - * When the `gapUnit` is `relative` (default), a gap size of 5 means - * that if the distance between two points is greater than five times - * that of the two closest points, the graph will be broken. - * - * When the `gapUnit` is `value`, the gap is based on absolute axis values, - * which on a datetime axis is milliseconds. This also applies to the - * navigator series that inherits gap options from the base series. - * - * @type {String} - * @see [gapSize](plotOptions.series.gapSize) - * @default relative - * @validvalue ["relative", "value"] - * @since 5.0.13 - * @product highstock - * @apioption plotOptions.series.gapUnit - */ - - if (gapSize && i > 0) { // #5008 - - // Gap unit is relative - if (this.options.gapUnit !== 'value') { - gapSize *= this.closestPointRange; - } - - // Setting a new gapSize in case dataGrouping is enabled (#7686) - if (groupingSize && groupingSize > gapSize) { - gapSize = groupingSize; - } - - // extension for ordinal breaks - while (i--) { - if (points[i + 1].x - points[i].x > gapSize) { - xRange = (points[i].x + points[i + 1].x) / 2; - - points.splice( // insert after this one - i + 1, - 0, - { - isNull: true, - x: xRange - } - ); - - // For stacked chart generate empty stack items, #6546 - if (this.options.stacking) { - stack = yAxis.stacks[this.stackKey][xRange] = - new H.StackItem( - yAxis, - yAxis.options.stackLabels, - false, - xRange, - this.stack - ); - stack.total = 0; - } - } - } - } - - // Call base method - return this.getGraphPath(points); + var currentDataGrouping = this.currentDataGrouping, + groupingSize = currentDataGrouping && currentDataGrouping.totalRange, + gapSize = this.options.gapSize, + points = this.points.slice(), + i = points.length - 1, + yAxis = this.yAxis, + xRange, + stack; + + /** + * Defines when to display a gap in the graph, together with the + * [gapUnit](plotOptions.series.gapUnit) option. + * + * In case when `dataGrouping` is enabled, points can be grouped into a + * larger time span. This can make the grouped points to have a greater + * distance than the absolute value of `gapSize` property, which will result + * in disappearing graph completely. To prevent this situation the mentioned + * distance between grouped points is used instead of previously defined + * `gapSize`. + * + * In practice, this option is most often used to visualize gaps in + * time series. In a stock chart, intraday data is available for daytime + * hours, while gaps will appear in nights and weekends. + * + * @type {Number} + * @see [gapUnit](plotOptions.series.gapUnit) and + * [xAxis.breaks](#xAxis.breaks) + * @sample {highstock} stock/plotoptions/series-gapsize/ + * Setting the gap size to 2 introduces gaps for weekends in daily + * datasets. + * @default 0 + * @product highstock + * @apioption plotOptions.series.gapSize + */ + + /** + * Together with [gapSize](plotOptions.series.gapSize), this option defines + * where to draw gaps in the graph. + * + * When the `gapUnit` is `relative` (default), a gap size of 5 means + * that if the distance between two points is greater than five times + * that of the two closest points, the graph will be broken. + * + * When the `gapUnit` is `value`, the gap is based on absolute axis values, + * which on a datetime axis is milliseconds. This also applies to the + * navigator series that inherits gap options from the base series. + * + * @type {String} + * @see [gapSize](plotOptions.series.gapSize) + * @default relative + * @validvalue ["relative", "value"] + * @since 5.0.13 + * @product highstock + * @apioption plotOptions.series.gapUnit + */ + + if (gapSize && i > 0) { // #5008 + + // Gap unit is relative + if (this.options.gapUnit !== 'value') { + gapSize *= this.closestPointRange; + } + + // Setting a new gapSize in case dataGrouping is enabled (#7686) + if (groupingSize && groupingSize > gapSize) { + gapSize = groupingSize; + } + + // extension for ordinal breaks + while (i--) { + if (points[i + 1].x - points[i].x > gapSize) { + xRange = (points[i].x + points[i + 1].x) / 2; + + points.splice( // insert after this one + i + 1, + 0, + { + isNull: true, + x: xRange + } + ); + + // For stacked chart generate empty stack items, #6546 + if (this.options.stacking) { + stack = yAxis.stacks[this.stackKey][xRange] = + new H.StackItem( + yAxis, + yAxis.options.stackLabels, + false, + xRange, + this.stack + ); + stack.total = 0; + } + } + } + } + + // Call base method + return this.getGraphPath(points); }; wrap(H.seriesTypes.column.prototype, 'drawPoints', drawPointsWrapped); diff --git a/js/modules/bullet.src.js b/js/modules/bullet.src.js index 55081d39ac9..87383b26198 100644 --- a/js/modules/bullet.src.js +++ b/js/modules/bullet.src.js @@ -9,11 +9,11 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var each = H.each, - pick = H.pick, - isNumber = H.isNumber, - relativeLength = H.relativeLength, - seriesType = H.seriesType, - columnProto = H.seriesTypes.column.prototype; + pick = H.pick, + isNumber = H.isNumber, + relativeLength = H.relativeLength, + seriesType = H.seriesType, + columnProto = H.seriesTypes.column.prototype; /** * The bullet series type. @@ -22,263 +22,263 @@ var each = H.each, * @augments seriesTypes.column */ seriesType('bullet', 'column', - /** - * A bullet graph is a variation of a bar graph. The bullet graph features - * a single measure, compares it to a target, and displays it in the context - * of qualitative ranges of performance that could be set using - * [plotBands](#yAxis.plotBands) on [yAxis](#yAxis). - * - * @extends {plotOptions.column} - * @product highcharts - * @sample {highcharts} highcharts/demo/bullet-graph/ Bullet graph - * @since 6.0.0 - * @excluding allAreas,boostThreshold,colorAxis,compare,compareBase - * @optionparent plotOptions.bullet - */ - { - /** - * All options related with look and positiong of targets. - * - * @sample {highcharts} highcharts/plotoptions/bullet-targetoptions/ - * Target options - * - * @type {Object} - * @since 6.0.0 - * @product highcharts - */ - targetOptions: { - /** - * The width of the rectangle representing the target. Could be set - * as a pixel value or as a percentage of a column width. - * - * @type {Number|String} - * @since 6.0.0 - * @product highcharts - */ - width: '140%', + /** + * A bullet graph is a variation of a bar graph. The bullet graph features + * a single measure, compares it to a target, and displays it in the context + * of qualitative ranges of performance that could be set using + * [plotBands](#yAxis.plotBands) on [yAxis](#yAxis). + * + * @extends {plotOptions.column} + * @product highcharts + * @sample {highcharts} highcharts/demo/bullet-graph/ Bullet graph + * @since 6.0.0 + * @excluding allAreas,boostThreshold,colorAxis,compare,compareBase + * @optionparent plotOptions.bullet + */ + { + /** + * All options related with look and positiong of targets. + * + * @sample {highcharts} highcharts/plotoptions/bullet-targetoptions/ + * Target options + * + * @type {Object} + * @since 6.0.0 + * @product highcharts + */ + targetOptions: { + /** + * The width of the rectangle representing the target. Could be set + * as a pixel value or as a percentage of a column width. + * + * @type {Number|String} + * @since 6.0.0 + * @product highcharts + */ + width: '140%', - /** - * The height of the rectangle representing the target. - * - * @since 6.0.0 - * @product highcharts - */ - height: 3, + /** + * The height of the rectangle representing the target. + * + * @since 6.0.0 + * @product highcharts + */ + height: 3, - /*= if (build.classic) { =*/ + /*= if (build.classic) { =*/ - /** - * The border color of the rectangle representing the target. When - * not set, the point's border color is used. - * - * In styled mode, use class `highcharts-bullet-target` instead. - * - * @type {Color} - * @since 6.0.0 - * @product highcharts - * @apioption plotOptions.bullet.targetOptions.borderColor - */ + /** + * The border color of the rectangle representing the target. When + * not set, the point's border color is used. + * + * In styled mode, use class `highcharts-bullet-target` instead. + * + * @type {Color} + * @since 6.0.0 + * @product highcharts + * @apioption plotOptions.bullet.targetOptions.borderColor + */ - /** - * The color of the rectangle representing the target. When not set, - * point's color (if set in point's options - - * [`color`](#series.bullet.data.color)) or zone of the target value - * (if [`zones`](#plotOptions.bullet.zones) or - * [`negativeColor`](#plotOptions.bullet.negativeColor) are set) - * or the same color as the point has is used. - * - * In styled mode, use class `highcharts-bullet-target` instead. - * - * @type {Color} - * @since 6.0.0 - * @product highcharts - * @apioption plotOptions.bullet.targetOptions.color - */ - - /** - * The border width of the rectangle representing the target. - * - * In styled mode, use class `highcharts-bullet-target` instead. - * - * @since 6.0.0 - * @product highcharts - */ - borderWidth: 0 - - /*= } =*/ - }, + /** + * The color of the rectangle representing the target. When not set, + * point's color (if set in point's options - + * [`color`](#series.bullet.data.color)) or zone of the target value + * (if [`zones`](#plotOptions.bullet.zones) or + * [`negativeColor`](#plotOptions.bullet.negativeColor) are set) + * or the same color as the point has is used. + * + * In styled mode, use class `highcharts-bullet-target` instead. + * + * @type {Color} + * @since 6.0.0 + * @product highcharts + * @apioption plotOptions.bullet.targetOptions.color + */ - tooltip: { - /*= if (build.classic) { =*/ - pointFormat: '\u25CF' + - ' {series.name}: {point.y}. Target: {point.target}' + - '
', - /*= } else { =*/ + /** + * The border width of the rectangle representing the target. + * + * In styled mode, use class `highcharts-bullet-target` instead. + * + * @since 6.0.0 + * @product highcharts + */ + borderWidth: 0 - pointFormat: '' + // eslint-disable-line no-dupe-keys - '' + - '\u25CF {series.name}: ' + - '{point.y}. ' + - 'Target: ' + - '{point.target}
' - /*= } =*/ - } - }, { - pointArrayMap: ['y', 'target'], - parallelArrays: ['x', 'y', 'target'], + /*= } =*/ + }, - /** - * Draws the targets. For inverted chart, the `series.group` is rotated, - * so the same coordinates apply. This method is based on - * column series drawPoints function. - */ - drawPoints: function () { - var series = this, - chart = series.chart, - options = series.options, - animationLimit = options.animationLimit || 250; + tooltip: { + /*= if (build.classic) { =*/ + pointFormat: '\u25CF' + + ' {series.name}: {point.y}. Target: {point.target}' + + '
', + /*= } else { =*/ - columnProto.drawPoints.apply(this); + pointFormat: '' + // eslint-disable-line no-dupe-keys + '' + + '\u25CF {series.name}: ' + + '{point.y}. ' + + 'Target: ' + + '{point.target}
' + /*= } =*/ + } + }, { + pointArrayMap: ['y', 'target'], + parallelArrays: ['x', 'y', 'target'], - each(series.points, function (point) { - var pointOptions = point.options, - shapeArgs, - targetGraphic = point.targetGraphic, - targetShapeArgs, - targetVal = point.target, - pointVal = point.y, - width, - height, - targetOptions, - y; + /** + * Draws the targets. For inverted chart, the `series.group` is rotated, + * so the same coordinates apply. This method is based on + * column series drawPoints function. + */ + drawPoints: function () { + var series = this, + chart = series.chart, + options = series.options, + animationLimit = options.animationLimit || 250; - if (isNumber(targetVal) && targetVal !== null) { - targetOptions = H.merge( - options.targetOptions, - pointOptions.targetOptions - ); - height = targetOptions.height; + columnProto.drawPoints.apply(this); - shapeArgs = point.shapeArgs; - width = relativeLength( - targetOptions.width, - shapeArgs.width - ); - y = series.yAxis.translate( - targetVal, - false, - true, - false, - true - ) - targetOptions.height / 2 - 0.5; + each(series.points, function (point) { + var pointOptions = point.options, + shapeArgs, + targetGraphic = point.targetGraphic, + targetShapeArgs, + targetVal = point.target, + pointVal = point.y, + width, + height, + targetOptions, + y; - targetShapeArgs = series.crispCol.apply({ - // Use fake series object to set borderWidth of target - chart: chart, - borderWidth: targetOptions.borderWidth, - options: { - crisp: options.crisp - } - }, [ - shapeArgs.x + shapeArgs.width / 2 - width / 2, - y, - width, - height - ]); + if (isNumber(targetVal) && targetVal !== null) { + targetOptions = H.merge( + options.targetOptions, + pointOptions.targetOptions + ); + height = targetOptions.height; - if (targetGraphic) { - // Update - targetGraphic[ - chart.pointCount < animationLimit ? - 'animate' : - 'attr' - ](targetShapeArgs); + shapeArgs = point.shapeArgs; + width = relativeLength( + targetOptions.width, + shapeArgs.width + ); + y = series.yAxis.translate( + targetVal, + false, + true, + false, + true + ) - targetOptions.height / 2 - 0.5; - // Add or remove tooltip reference - if (isNumber(pointVal) && pointVal !== null) { - targetGraphic.element.point = point; - } else { - targetGraphic.element.point = undefined; - } - } else { - point.targetGraphic = targetGraphic = chart.renderer - .rect() - .attr(targetShapeArgs) - .add(series.group); - } - /*= if (build.classic) { =*/ - // Presentational - targetGraphic.attr({ - fill: pick( - targetOptions.color, - pointOptions.color, - (series.zones.length && (point.getZone.call({ - series: series, - x: point.x, - y: targetVal, - options: {} - }).color || series.color)) || undefined, - point.color, - series.color - ), - stroke: pick( - targetOptions.borderColor, - point.borderColor, - series.options.borderColor - ), - 'stroke-width': targetOptions.borderWidth - }); - /*= } =*/ + targetShapeArgs = series.crispCol.apply({ + // Use fake series object to set borderWidth of target + chart: chart, + borderWidth: targetOptions.borderWidth, + options: { + crisp: options.crisp + } + }, [ + shapeArgs.x + shapeArgs.width / 2 - width / 2, + y, + width, + height + ]); - // Add tooltip reference - if (isNumber(pointVal) && pointVal !== null) { - targetGraphic.element.point = point; - } + if (targetGraphic) { + // Update + targetGraphic[ + chart.pointCount < animationLimit ? + 'animate' : + 'attr' + ](targetShapeArgs); - targetGraphic.addClass(point.getClassName() + - ' highcharts-bullet-target', true); - } else if (targetGraphic) { - point.targetGraphic = targetGraphic.destroy(); // #1269 - } - }); - }, + // Add or remove tooltip reference + if (isNumber(pointVal) && pointVal !== null) { + targetGraphic.element.point = point; + } else { + targetGraphic.element.point = undefined; + } + } else { + point.targetGraphic = targetGraphic = chart.renderer + .rect() + .attr(targetShapeArgs) + .add(series.group); + } + /*= if (build.classic) { =*/ + // Presentational + targetGraphic.attr({ + fill: pick( + targetOptions.color, + pointOptions.color, + (series.zones.length && (point.getZone.call({ + series: series, + x: point.x, + y: targetVal, + options: {} + }).color || series.color)) || undefined, + point.color, + series.color + ), + stroke: pick( + targetOptions.borderColor, + point.borderColor, + series.options.borderColor + ), + 'stroke-width': targetOptions.borderWidth + }); + /*= } =*/ - /** - * Includes target values to extend extremes from y values. - */ - getExtremes: function (yData) { - var series = this, - targetData = series.targetData, - yMax, - yMin; + // Add tooltip reference + if (isNumber(pointVal) && pointVal !== null) { + targetGraphic.element.point = point; + } - columnProto.getExtremes.call(this, yData); + targetGraphic.addClass(point.getClassName() + + ' highcharts-bullet-target', true); + } else if (targetGraphic) { + point.targetGraphic = targetGraphic.destroy(); // #1269 + } + }); + }, - if (targetData && targetData.length) { - yMax = series.dataMax; - yMin = series.dataMin; - columnProto.getExtremes.call(this, targetData); - series.dataMax = Math.max(series.dataMax, yMax); - series.dataMin = Math.min(series.dataMin, yMin); - } - } - }, /** @lends seriesTypes.ohlc.prototype.pointClass.prototype */ { - /** - * Destroys target graphic. - */ - destroy: function () { - if (this.targetGraphic) { - this.targetGraphic = this.targetGraphic.destroy(); - } - columnProto.pointClass.prototype.destroy.apply(this, arguments); - } - }); + /** + * Includes target values to extend extremes from y values. + */ + getExtremes: function (yData) { + var series = this, + targetData = series.targetData, + yMax, + yMin; + + columnProto.getExtremes.call(this, yData); + + if (targetData && targetData.length) { + yMax = series.dataMax; + yMin = series.dataMin; + columnProto.getExtremes.call(this, targetData); + series.dataMax = Math.max(series.dataMax, yMax); + series.dataMin = Math.min(series.dataMin, yMin); + } + } + }, /** @lends seriesTypes.ohlc.prototype.pointClass.prototype */ { + /** + * Destroys target graphic. + */ + destroy: function () { + if (this.targetGraphic) { + this.targetGraphic = this.targetGraphic.destroy(); + } + columnProto.pointClass.prototype.destroy.apply(this, arguments); + } + }); /** * A `bullet` series. If the [type](#series.bullet.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.bullet @@ -290,7 +290,7 @@ seriesType('bullet', 'column', /** * An array of data points for the series. For the `bullet` series type, * points can be given in the following ways: - * + * * 1. An array of arrays with 3 or 2 values. In this case, the values * correspond to `x,y,target`. If the first value is a string, * it is applied as the name of the point, and the `x` value is inferred. @@ -298,7 +298,7 @@ seriesType('bullet', 'column', * should be of length 2\. 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, 40, 75], @@ -306,12 +306,12 @@ seriesType('bullet', 'column', * [2, 60, 40] * ] * ``` - * + * * 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.bullet.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 0, @@ -327,7 +327,7 @@ seriesType('bullet', 'column', * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @since 6.0.0 * @extends series.column.data @@ -337,7 +337,7 @@ seriesType('bullet', 'column', /** * The target value of a point. - * + * * @type {Number} * @since 6.0.0 * @product highcharts @@ -346,7 +346,7 @@ seriesType('bullet', 'column', /** * Individual target options for each point. - * + * * @extends plotOptions.bullet.targetOptions * @product highcharts * @apioption series.bullet.data.targetOptions diff --git a/js/modules/data.src.js b/js/modules/data.src.js index b9dfddd8e15..aacab4d9f78 100644 --- a/js/modules/data.src.js +++ b/js/modules/data.src.js @@ -12,19 +12,19 @@ import '../parts/Chart.js'; // Utilities var addEvent = Highcharts.addEvent, - Chart = Highcharts.Chart, - win = Highcharts.win, - doc = win.document, - each = Highcharts.each, - objectEach = Highcharts.objectEach, - pick = Highcharts.pick, - inArray = Highcharts.inArray, - isNumber = Highcharts.isNumber, - merge = Highcharts.merge, - splat = Highcharts.splat, - fireEvent = Highcharts.fireEvent, - some = Highcharts.some, - SeriesBuilder; + Chart = Highcharts.Chart, + win = Highcharts.win, + doc = win.document, + each = Highcharts.each, + objectEach = Highcharts.objectEach, + pick = Highcharts.pick, + inArray = Highcharts.inArray, + isNumber = Highcharts.isNumber, + merge = Highcharts.merge, + splat = Highcharts.splat, + fireEvent = Highcharts.fireEvent, + some = Highcharts.some, + SeriesBuilder; /** * @typedef {Object} AjaxSettings @@ -45,70 +45,70 @@ var addEvent = Highcharts.addEvent, * */ Highcharts.ajax = function (attr) { - var options = merge(true, { - url: false, - type: 'GET', - dataType: 'json', - success: false, - error: false, - data: false, - headers: {} - }, attr), - headers = { - json: 'application/json', - xml: 'application/xml', - text: 'text/plain', - octet: 'application/octet-stream' - }, - r = new XMLHttpRequest(); - - function handleError(xhr, err) { - if (options.error) { - options.error(xhr, err); - } else { - // Maybe emit a highcharts error event here - } - } - - if (!options.url) { - return false; - } - - r.open(options.type.toUpperCase(), options.url, true); - r.setRequestHeader( - 'Content-Type', - headers[options.dataType] || headers.text - ); - - Highcharts.objectEach(options.headers, function (val, key) { - r.setRequestHeader(key, val); - }); - - r.onreadystatechange = function () { - var res; - - if (r.readyState === 4) { - if (r.status === 200) { - res = r.responseText; - if (options.dataType === 'json') { - try { - res = JSON.parse(res); - } catch (e) { - return handleError(r, e); - } - } - return options.success && options.success(res); - } - - handleError(r, r.responseText); - } - }; - - try { - options.data = JSON.stringify(options.data); - } catch (e) {} - - r.send(options.data || true); + var options = merge(true, { + url: false, + type: 'GET', + dataType: 'json', + success: false, + error: false, + data: false, + headers: {} + }, attr), + headers = { + json: 'application/json', + xml: 'application/xml', + text: 'text/plain', + octet: 'application/octet-stream' + }, + r = new XMLHttpRequest(); + + function handleError(xhr, err) { + if (options.error) { + options.error(xhr, err); + } else { + // Maybe emit a highcharts error event here + } + } + + if (!options.url) { + return false; + } + + r.open(options.type.toUpperCase(), options.url, true); + r.setRequestHeader( + 'Content-Type', + headers[options.dataType] || headers.text + ); + + Highcharts.objectEach(options.headers, function (val, key) { + r.setRequestHeader(key, val); + }); + + r.onreadystatechange = function () { + var res; + + if (r.readyState === 4) { + if (r.status === 200) { + res = r.responseText; + if (options.dataType === 'json') { + try { + res = JSON.parse(res); + } catch (e) { + return handleError(r, e); + } + } + return options.success && options.success(res); + } + + handleError(r, r.responseText); + } + }; + + try { + options.data = JSON.stringify(options.data); + } catch (e) {} + + r.send(options.data || true); }; /** @@ -404,7 +404,7 @@ Highcharts.ajax = function (attr) { * * @type {String} * @sample highcharts/data/livedata-columns - * Categorized bar chart with CSV and live polling + * Categorized bar chart with CSV and live polling * @sample highcharts/data/livedata-csv * Time based line chart with CSV and live polling * @apioption data.csvURL @@ -431,12 +431,12 @@ Highcharts.ajax = function (attr) { */ /** - * Sets the refresh rate for data polling when importing remote dataset by - * setting [data.csvURL](data.csvURL), [data.rowsURL](data.rowsURL), - * [data.columnsURL](data.columnsURL), or + * Sets the refresh rate for data polling when importing remote dataset by + * setting [data.csvURL](data.csvURL), [data.rowsURL](data.rowsURL), + * [data.columnsURL](data.columnsURL), or * [data.googleSpreadsheetKey](data.googleSpreadsheetKey). * - * Note that polling must be enabled by setting + * Note that polling must be enabled by setting * [data.enablePolling](data.enablePolling) to true. * * The value is the number of seconds between pollings. @@ -453,14 +453,14 @@ Highcharts.ajax = function (attr) { * Enables automatic refetching of remote datasets every _n_ seconds (defined by * setting [data.dataRefreshRate](data.dataRefreshRate)). * - * Only works when either [data.csvURL](data.csvURL), - * [data.rowsURL](data.rowsURL), [data.columnsURL](data.columnsURL), or + * Only works when either [data.csvURL](data.csvURL), + * [data.rowsURL](data.rowsURL), [data.columnsURL](data.columnsURL), or * [data.googleSpreadsheetKey](data.googleSpreadsheetKey). * * @sample highcharts/demo/live-data * Live data * @sample highcharts/data/livedata-columns - * Categorized bar chart with CSV and live polling + * Categorized bar chart with CSV and live polling * * @type {Bool} * @default false @@ -469,1595 +469,1595 @@ Highcharts.ajax = function (attr) { // The Data constructor var Data = function (dataOptions, chartOptions, chart) { - this.init(dataOptions, chartOptions, chart); + this.init(dataOptions, chartOptions, chart); }; // Set the prototype properties Highcharts.extend(Data.prototype, { - /** - * Initialize the Data object with the given options - */ - init: function (options, chartOptions, chart) { - - var decimalPoint = options.decimalPoint, - hasData; - - if (chartOptions) { - this.chartOptions = chartOptions; - } - if (chart) { - this.chart = chart; - } - - if (decimalPoint !== '.' && decimalPoint !== ',') { - decimalPoint = undefined; - } - - this.options = options; - this.columns = ( - options.columns || - this.rowsToColumns(options.rows) || - [] - ); - - this.firstRowAsNames = pick( - options.firstRowAsNames, - this.firstRowAsNames, - true - ); - - this.decimalRegex = ( - decimalPoint && - new RegExp('^(-?[0-9]+)' + decimalPoint + '([0-9]+)$') // eslint-disable-line security/detect-non-literal-regexp - ); - - // This is a two-dimensional array holding the raw, trimmed string - // values with the same organisation as the columns array. It makes it - // possible for example to revert from interpreted timestamps to - // string-based categories. - this.rawColumns = []; - - // No need to parse or interpret anything - if (this.columns.length) { - this.dataFound(); - hasData = true; - } - - if (!hasData) { - // Fetch live data - hasData = this.fetchLiveData(); - } - - if (!hasData) { - // Parse a CSV string if options.csv is given. The parseCSV function - // returns a columns array, if it has no length, we have no data - hasData = Boolean(this.parseCSV().length); - } - - if (!hasData) { - // Parse a HTML table if options.table is given - hasData = Boolean(this.parseTable().length); - } - - if (!hasData) { - // Parse a Google Spreadsheet - hasData = this.parseGoogleSpreadsheet(); - } - - if (!hasData && options.afterComplete) { - options.afterComplete(); - } - }, - - /** - * Get the column distribution. For example, a line series takes a single - * column for Y values. A range series takes two columns for low and high - * values respectively, and an OHLC series takes four columns. - */ - getColumnDistribution: function () { - var chartOptions = this.chartOptions, - options = this.options, - xColumns = [], - getValueCount = function (type) { - return ( - Highcharts.seriesTypes[type || 'line'].prototype - .pointArrayMap || - [0] - ).length; - }, - getPointArrayMap = function (type) { - return Highcharts.seriesTypes[type || 'line'] - .prototype.pointArrayMap; - }, - globalType = ( - chartOptions && - chartOptions.chart && - chartOptions.chart.type - ), - individualCounts = [], - seriesBuilders = [], - seriesIndex = 0, - i; - - each((chartOptions && chartOptions.series) || [], function (series) { - individualCounts.push(getValueCount(series.type || globalType)); - }); - - // Collect the x-column indexes from seriesMapping - each((options && options.seriesMapping) || [], function (mapping) { - xColumns.push(mapping.x || 0); - }); - - // If there are no defined series with x-columns, use the first column - // as x column - if (xColumns.length === 0) { - xColumns.push(0); - } - - // Loop all seriesMappings and constructs SeriesBuilders from - // the mapping options. - each((options && options.seriesMapping) || [], function (mapping) { - var builder = new SeriesBuilder(), - numberOfValueColumnsNeeded = individualCounts[seriesIndex] || - getValueCount(globalType), - seriesArr = (chartOptions && chartOptions.series) || [], - series = seriesArr[seriesIndex] || {}, - pointArrayMap = getPointArrayMap(series.type || globalType) || - ['y']; - - // Add an x reader from the x property or from an undefined column - // if the property is not set. It will then be auto populated later. - builder.addColumnReader(mapping.x, 'x'); - - // Add all column mappings - objectEach(mapping, function (val, name) { - if (name !== 'x') { - builder.addColumnReader(val, name); - } - }); - - // Add missing columns - for (i = 0; i < numberOfValueColumnsNeeded; i++) { - if (!builder.hasReader(pointArrayMap[i])) { - // Create and add a column reader for the next free column - // index - builder.addColumnReader(undefined, pointArrayMap[i]); - } - } - - seriesBuilders.push(builder); - seriesIndex++; - }); - - var globalPointArrayMap = getPointArrayMap(globalType); - if (globalPointArrayMap === undefined) { - globalPointArrayMap = ['y']; - } - - this.valueCount = { - global: getValueCount(globalType), - xColumns: xColumns, - individual: individualCounts, - seriesBuilders: seriesBuilders, - globalPointArrayMap: globalPointArrayMap - }; - }, - - /** - * When the data is parsed into columns, either by CSV, table, GS or direct - * input, continue with other operations. - */ - dataFound: function () { - - if (this.options.switchRowsAndColumns) { - this.columns = this.rowsToColumns(this.columns); - } - - // Interpret the info about series and columns - this.getColumnDistribution(); - - // Interpret the values into right types - this.parseTypes(); - - // Handle columns if a handleColumns callback is given - if (this.parsed() !== false) { - - // Complete if a complete callback is given - this.complete(); - } - - }, - - /** - * Parse a CSV input string - */ - parseCSV: function (inOptions) { - var self = this, - options = inOptions || this.options, - csv = options.csv, - columns, - startRow = ( - typeof options.startRow !== 'undefined' && options.startRow ? - options.startRow : - 0 - ), - endRow = options.endRow || Number.MAX_VALUE, - startColumn = ( - typeof options.startColumn !== 'undefined' && - options.startColumn - ) ? options.startColumn : 0, - endColumn = options.endColumn || Number.MAX_VALUE, - itemDelimiter, - lines, - rowIt = 0, - // activeRowNo = 0, - dataTypes = [], - // We count potential delimiters in the prepass, and use the - // result as the basis of half-intelligent guesses. - potDelimiters = { - ',': 0, - ';': 0, - '\t': 0 - }; - - columns = this.columns = []; - - /* - This implementation is quite verbose. It will be shortened once - it's stable and passes all the test. - - It's also not written with speed in mind, instead everything is - very seggregated, and there a several redundant loops. - This is to make it easier to stabilize the code initially. - - We do a pre-pass on the first 4 rows to make some intelligent - guesses on the set. Guessed delimiters are in this pass counted. - - Auto detecting delimiters - - If we meet a quoted string, the next symbol afterwards - (that's not \s, \t) is the delimiter - - If we meet a date, the next symbol afterwards is the delimiter - - Date formats - - If we meet a column with date formats, check all of them to - see if one of the potential months crossing 12. If it does, - we now know the format - - It would make things easier to guess the delimiter before - doing the actual parsing. - - General rules: - - Quoting is allowed, e.g: "Col 1",123,321 - - Quoting is optional, e.g.: Col1,123,321 - - Doubble quoting is escaping, e.g. "Col ""Hello world""",123 - - Spaces are considered part of the data: Col1 ,123 - - New line is always the row delimiter - - Potential column delimiters are , ; \t - - First row may optionally contain headers - - The last row may or may not have a row delimiter - - Comments are optionally supported, in which case the comment - must start at the first column, and the rest of the line will - be ignored - */ - - // Parse a single row - function parseRow(columnStr, rowNumber, noAdd, callbacks) { - var i = 0, - c = '', - cl = '', - cn = '', - token = '', - actualColumn = 0, - column = 0; - - function read(j) { - c = columnStr[j]; - cl = columnStr[j - 1]; - cn = columnStr[j + 1]; - } - - function pushType(type) { - if (dataTypes.length < column + 1) { - dataTypes.push([type]); - } - if (dataTypes[column][dataTypes[column].length - 1] !== type) { - dataTypes[column].push(type); - } - } - - function push() { - if (startColumn > actualColumn || actualColumn > endColumn) { - // Skip this column, but increment the column count (#7272) - ++actualColumn; - token = ''; - return; - } - - if (!isNaN(parseFloat(token)) && isFinite(token)) { - token = parseFloat(token); - pushType('number'); - } else if (!isNaN(Date.parse(token))) { - token = token.replace(/\//g, '-'); - pushType('date'); - } else { - pushType('string'); - } - - - if (columns.length < column + 1) { - columns.push([]); - } - - if (!noAdd) { - // Don't push - if there's a varrying amount of columns - // for each row, pushing will skew everything down n slots - columns[column][rowNumber] = token; - } - - token = ''; - ++column; - ++actualColumn; - } - - if (!columnStr.trim().length) { - return; - } - - if (columnStr.trim()[0] === '#') { - return; - } - - for (; i < columnStr.length; i++) { - read(i); - - // Quoted string - if (c === '#') { - // The rest of the row is a comment - push(); - return; - } else if (c === '"') { - read(++i); - - while (i < columnStr.length) { - if (c === '"' && cl !== '"' && cn !== '"') { - break; - } - - if (c !== '"' || (c === '"' && cl !== '"')) { - token += c; - } - - read(++i); - } - - // Perform "plugin" handling - } else if (callbacks && callbacks[c]) { - if (callbacks[c](c, token)) { - push(); - } - - // Delimiter - push current token - } else if (c === itemDelimiter) { - push(); - - // Actual column data - } else { - token += c; - } - } - - push(); - - } - - // Attempt to guess the delimiter - // We do a separate parse pass here because we need - // to count potential delimiters softly without making any assumptions. - function guessDelimiter(lines) { - var points = 0, - commas = 0, - guessed = false; - - some(lines, function (columnStr, i) { - var inStr = false, - c, - cn, - cl, - token = '' - ; - - - // We should be able to detect dateformats within 13 rows - if (i > 13) { - return true; - } - - for (var j = 0; j < columnStr.length; j++) { - c = columnStr[j]; - cn = columnStr[j + 1]; - cl = columnStr[j - 1]; - - if (c === '#') { - // Skip the rest of the line - it's a comment - return; - } else if (c === '"') { - if (inStr) { - if (cl !== '"' && cn !== '"') { - while (cn === ' ' && j < columnStr.length) { - cn = columnStr[++j]; - } - - // After parsing a string, the next non-blank - // should be a delimiter if the CSV is properly - // formed. - - if (typeof potDelimiters[cn] !== 'undefined') { - potDelimiters[cn]++; - } - - inStr = false; - } - } else { - inStr = true; - } - } else if (typeof potDelimiters[c] !== 'undefined') { - - token = token.trim(); - - if (!isNaN(Date.parse(token))) { - potDelimiters[c]++; - } else if (isNaN(token) || !isFinite(token)) { - potDelimiters[c]++; - } - - token = ''; - - } else { - token += c; - } - - if (c === ',') { - commas++; - } - - if (c === '.') { - points++; - } - } - }); - - // Count the potential delimiters. - // This could be improved by checking if the number of delimiters - // equals the number of columns - 1 - - if (potDelimiters[';'] > potDelimiters[',']) { - guessed = ';'; - } else if (potDelimiters[','] > potDelimiters[';']) { - guessed = ','; - } else { - // No good guess could be made.. - guessed = ','; - } - - // Try to deduce the decimal point if it's not explicitly set. - // If both commas or points is > 0 there is likely an issue - if (!options.decimalPoint) { - if (points > commas) { - options.decimalPoint = '.'; - } else { - options.decimalPoint = ','; - } - - // Apply a new decimal regex based on the presumed decimal sep. - self.decimalRegex = new RegExp( // eslint-disable-line security/detect-non-literal-regexp - '^(-?[0-9]+)' + - options.decimalPoint + - '([0-9]+)$' - ); - } - - return guessed; - } - - /* Tries to guess the date format - * - Check if either month candidate exceeds 12 - * - Check if year is missing (use current year) - * - Check if a shortened year format is used (e.g. 1/1/99) - * - If no guess can be made, the user must be prompted - * data is the data to deduce a format based on - */ - function deduceDateFormat(data, limit) { - var format = 'YYYY/mm/dd', - thing, - guessedFormat, - calculatedFormat, - i = 0, - madeDeduction = false, - // candidates = {}, - stable = [], - max = [], - j; - - if (!limit || limit > data.length) { - limit = data.length; - } - - for (; i < limit; i++) { - if ( - typeof data[i] !== 'undefined' && - data[i] && data[i].length - ) { - thing = data[i] - .trim() - .replace(/\//g, ' ') - .replace(/\-/g, ' ') - .split(' '); - - guessedFormat = [ - '', - '', - '' - ]; - - - for (j = 0; j < thing.length; j++) { - if (j < guessedFormat.length) { - thing[j] = parseInt(thing[j], 10); - - if (thing[j]) { - - max[j] = (!max[j] || max[j] < thing[j]) ? - thing[j] : - max[j]; - - if (typeof stable[j] !== 'undefined') { - if (stable[j] !== thing[j]) { - stable[j] = false; - } - } else { - stable[j] = thing[j]; - } - - if (thing[j] > 31) { - if (thing[j] < 100) { - guessedFormat[j] = 'YY'; - } else { - guessedFormat[j] = 'YYYY'; - } - // madeDeduction = true; - } else if (thing[j] > 12 && thing[j] <= 31) { - guessedFormat[j] = 'dd'; - madeDeduction = true; - } else if (!guessedFormat[j].length) { - guessedFormat[j] = 'mm'; - } - } - } - } - } - } - - if (madeDeduction) { - - // This handles a few edge cases with hard to guess dates - for (j = 0; j < stable.length; j++) { - if (stable[j] !== false) { - if ( - max[j] > 12 && - guessedFormat[j] !== 'YY' && - guessedFormat[j] !== 'YYYY' - ) { - guessedFormat[j] = 'YY'; - } - } else if (max[j] > 12 && guessedFormat[j] === 'mm') { - guessedFormat[j] = 'dd'; - } - } - - // If the middle one is dd, and the last one is dd, - // the last should likely be year. - if (guessedFormat.length === 3 && - guessedFormat[1] === 'dd' && - guessedFormat[2] === 'dd') { - guessedFormat[2] = 'YY'; - } - - calculatedFormat = guessedFormat.join('/'); - - // If the caculated format is not valid, we need to present an - // error. - - if ( - !(options.dateFormats || self.dateFormats)[calculatedFormat] - ) { - // This should emit an event instead - fireEvent('deduceDateFailed'); - return format; - } - - return calculatedFormat; - } - - return format; - } - - /* Figure out the best axis types for the data - * - If the first column is a number, we're good - * - If the first column is a date, set to date/time - * - If the first column is a string, set to categories - */ - function deduceAxisTypes() { - - } - - if (csv) { - - lines = csv - .replace(/\r\n/g, '\n') // Unix - .replace(/\r/g, '\n') // Mac - .split(options.lineDelimiter || '\n'); - - if (!startRow || startRow < 0) { - startRow = 0; - } - - if (!endRow || endRow >= lines.length) { - endRow = lines.length - 1; - } - - if (options.itemDelimiter) { - itemDelimiter = options.itemDelimiter; - } else { - itemDelimiter = null; - itemDelimiter = guessDelimiter(lines); - } - - var offset = 0; - - for (rowIt = startRow; rowIt <= endRow; rowIt++) { - if (lines[rowIt][0] === '#') { - offset++; - } else { - parseRow(lines[rowIt], rowIt - startRow - offset); - } - } - - // //Make sure that there's header columns for everything - // each(columns, function (col) { - - // }); - - deduceAxisTypes(); - - if ((!options.columnTypes || options.columnTypes.length === 0) && - dataTypes.length && - dataTypes[0].length && - dataTypes[0][1] === 'date' && - !options.dateFormat) { - options.dateFormat = deduceDateFormat(columns[0]); - } - - - // each(lines, function (line, rowNo) { - // var trimmed = self.trim(line), - // isComment = trimmed.indexOf('#') === 0, - // isBlank = trimmed === '', - // items; - - // if ( - // rowNo >= startRow && - // rowNo <= endRow && - // !isComment && !isBlank - // ) { - // items = line.split(itemDelimiter); - // each(items, function (item, colNo) { - // if (colNo >= startColumn && colNo <= endColumn) { - // if (!columns[colNo - startColumn]) { - // columns[colNo - startColumn] = []; - // } - - // columns[colNo - startColumn][activeRowNo] = item; - // } - // }); - // activeRowNo += 1; - // } - // }); - // - - this.dataFound(); - } - - return columns; - }, - - /** - * Parse a HTML table - */ - parseTable: function () { - var options = this.options, - table = options.table, - columns = this.columns, - startRow = options.startRow || 0, - endRow = options.endRow || Number.MAX_VALUE, - startColumn = options.startColumn || 0, - endColumn = options.endColumn || Number.MAX_VALUE; - - if (table) { - - if (typeof table === 'string') { - table = doc.getElementById(table); - } - - each(table.getElementsByTagName('tr'), function (tr, rowNo) { - if (rowNo >= startRow && rowNo <= endRow) { - each(tr.children, function (item, colNo) { - if ( - (item.tagName === 'TD' || item.tagName === 'TH') && - colNo >= startColumn && - colNo <= endColumn - ) { - if (!columns[colNo - startColumn]) { - columns[colNo - startColumn] = []; - } - - columns[colNo - startColumn][rowNo - startRow] = - item.innerHTML; - } - }); - } - }); - - this.dataFound(); // continue - } - return columns; - }, - - - /** - * Fetch or refetch live data - */ - fetchLiveData: function () { - var chart = this.chart, - options = this.options, - maxRetries = 3, - currentRetries = 0, - pollingEnabled = options.enablePolling, - updateIntervalMs = (options.dataRefreshRate || 2) * 1000, - originalOptions = merge(options); - - if (!options || - (!options.csvURL && !options.rowsURL && !options.columnsURL) - ) { - return false; - } - - // Do not allow polling more than once a second - if (updateIntervalMs < 1000) { - updateIntervalMs = 1000; - } - - delete options.csvURL; - delete options.rowsURL; - delete options.columnsURL; - - function performFetch(initialFetch) { - - // Helper function for doing the data fetch + polling - function request(url, done, tp) { - if (!url || url.indexOf('http') !== 0) { - if (url && options.error) { - options.error('Invalid URL'); - } - return false; - } - - if (initialFetch) { - clearTimeout(chart.liveDataTimeout); - chart.liveDataURL = url; - } - - function poll() { - // Poll - if (pollingEnabled && chart.liveDataURL === url) { - // We need to stop doing this if the URL has changed - chart.liveDataTimeout = - setTimeout(performFetch, updateIntervalMs); - } - } - - Highcharts.ajax({ - url: url, - dataType: tp || 'json', - success: function (res) { - if (chart && chart.series) { - done(res); - } - - poll(); - - }, - error: function (xhr, text) { - if (++currentRetries < maxRetries) { - poll(); - } - - return options.error && options.error(text, xhr); - } - }); - - return true; - } - - if (!request(originalOptions.csvURL, function (res) { - chart.update({ - data: { - csv: res - } - }); - }, 'text')) { - if (!request(originalOptions.rowsURL, function (res) { - chart.update({ - data: { - rows: res - } - }); - })) { - request(originalOptions.columnsURL, function (res) { - chart.update({ - data: { - columns: res - } - }); - }); - } - } - } - - performFetch(true); - - return (options && - (options.csvURL || options.rowsURL || options.columnsURL) - ); - }, - - - /** - * Parse a Google spreadsheet. - */ - parseGoogleSpreadsheet: function () { - var options = this.options, - googleSpreadsheetKey = options.googleSpreadsheetKey, - chart = this.chart, - // use sheet 1 as the default rather than od6 - // as the latter sometimes cause issues (it looks like it can - // be renamed in some cases, ref. a fogbugz case). - worksheet = options.googleSpreadsheetWorksheet || 1, - startRow = options.startRow || 0, - endRow = options.endRow || Number.MAX_VALUE, - startColumn = options.startColumn || 0, - endColumn = options.endColumn || Number.MAX_VALUE, - refreshRate = (options.dataRefreshRate || 2) * 1000; - - if (refreshRate < 4000) { - refreshRate = 4000; - } - - /* - * Fetch the actual spreadsheet using XMLHttpRequest - */ - function fetchSheet(fn) { - var url = [ - 'https://spreadsheets.google.com/feeds/cells', - googleSpreadsheetKey, - worksheet, - 'public/values?alt=json' - ].join('/'); - - Highcharts.ajax({ - url: url, - dataType: 'json', - success: function (json) { - fn(json); - - if (options.enablePolling) { - setTimeout(function () { - fetchSheet(fn); - }, options.dataRefreshRate); - } - }, - error: function (xhr, text) { - return options.error && options.error(text, xhr); - } - }); - } - - if (googleSpreadsheetKey) { - - delete options.googleSpreadsheetKey; - - fetchSheet(function (json) { - // Prepare the data from the spreadsheat - var columns = [], - cells = json.feed.entry, - cell, - cellCount = (cells || []).length, - colCount = 0, - rowCount = 0, - val, - gr, - gc, - cellInner, - i; - - if (!cells || cells.length === 0) { - return false; - } - - // First, find the total number of columns and rows that - // are actually filled with data - for (i = 0; i < cellCount; i++) { - cell = cells[i]; - colCount = Math.max(colCount, cell.gs$cell.col); - rowCount = Math.max(rowCount, cell.gs$cell.row); - } - - // Set up arrays containing the column data - for (i = 0; i < colCount; i++) { - if (i >= startColumn && i <= endColumn) { - // Create new columns with the length of either - // end-start or rowCount - columns[i - startColumn] = []; - } - } - - // Loop over the cells and assign the value to the right - // place in the column arrays - for (i = 0; i < cellCount; i++) { - cell = cells[i]; - gr = cell.gs$cell.row - 1; // rows start at 1 - gc = cell.gs$cell.col - 1; // columns start at 1 - - // If both row and col falls inside start and end set the - // transposed cell value in the newly created columns - if (gc >= startColumn && gc <= endColumn && - gr >= startRow && gr <= endRow) { - - cellInner = cell.gs$cell || cell.content; - - val = null; - - if (cellInner.numericValue) { - if (cellInner.$t.indexOf('/') >= 0 || - cellInner.$t.indexOf('-') >= 0) { - // This is a date - for future reference. - val = cellInner.$t; - } else if (cellInner.$t.indexOf('%') > 0) { - // Percentage - val = parseFloat(cellInner.numericValue) * 100; - } else { - val = parseFloat(cellInner.numericValue); - } - } else if (cellInner.$t && cellInner.$t.length) { - val = cellInner.$t; - } - - columns[gc - startColumn][gr - startRow] = val; - } - } - - // Insert null for empty spreadsheet cells (#5298) - each(columns, function (column) { - for (i = 0; i < column.length; i++) { - if (column[i] === undefined) { - column[i] = null; - } - } - }); - - if (chart && chart.series) { - chart.update({ - data: { - columns: columns - } - }); - } - }); - } - - // This is an intermediate fetch, so always return false. - return false; - }, - - /** - * Trim a string from whitespace - */ - trim: function (str, inside) { - if (typeof str === 'string') { - str = str.replace(/^\s+|\s+$/g, ''); - - // Clear white space insdie the string, like thousands separators - if (inside && /^[0-9\s]+$/.test(str)) { - str = str.replace(/\s/g, ''); - } - - if (this.decimalRegex) { - str = str.replace(this.decimalRegex, '$1.$2'); - } - } - return str; - }, - - /** - * Parse numeric cells in to number types and date types in to true dates. - */ - parseTypes: function () { - var columns = this.columns, - col = columns.length; - - while (col--) { - this.parseColumn(columns[col], col); - } - - }, - - /** - * Parse a single column. Set properties like .isDatetime and .isNumeric. - */ - parseColumn: function (column, col) { - var rawColumns = this.rawColumns, - columns = this.columns, - row = column.length, - val, - floatVal, - trimVal, - trimInsideVal, - firstRowAsNames = this.firstRowAsNames, - isXColumn = inArray(col, this.valueCount.xColumns) !== -1, - dateVal, - backup = [], - diff, - chartOptions = this.chartOptions, - descending, - columnTypes = this.options.columnTypes || [], - columnType = columnTypes[col], - forceCategory = isXColumn && (( - chartOptions && - chartOptions.xAxis && - splat(chartOptions.xAxis)[0].type === 'category' - ) || columnType === 'string'); - - if (!rawColumns[col]) { - rawColumns[col] = []; - } - while (row--) { - val = backup[row] || column[row]; - - trimVal = this.trim(val); - trimInsideVal = this.trim(val, true); - floatVal = parseFloat(trimInsideVal); - - // Set it the first time - if (rawColumns[col][row] === undefined) { - rawColumns[col][row] = trimVal; - } - - // Disable number or date parsing by setting the X axis type to - // category - if (forceCategory || (row === 0 && firstRowAsNames)) { - column[row] = '' + trimVal; - - } else if (+trimInsideVal === floatVal) { // is numeric - - column[row] = floatVal; - - // If the number is greater than milliseconds in a year, assume - // datetime - if ( - floatVal > 365 * 24 * 3600 * 1000 && - columnType !== 'float' - ) { - column.isDatetime = true; - } else { - column.isNumeric = true; - } - - if (column[row + 1] !== undefined) { - descending = floatVal > column[row + 1]; - } - - // String, continue to determine if it is a date string or really a - // string - } else { - if (trimVal && trimVal.length) { - dateVal = this.parseDate(val); - } - - // Only allow parsing of dates if this column is an x-column - if (isXColumn && isNumber(dateVal) && columnType !== 'float') { - backup[row] = val; - column[row] = dateVal; - column.isDatetime = true; - - // Check if the dates are uniformly descending or ascending. - // If they are not, chances are that they are a different - // time format, so check for alternative. - if (column[row + 1] !== undefined) { - diff = dateVal > column[row + 1]; - if (diff !== descending && descending !== undefined) { - if (this.alternativeFormat) { - this.dateFormat = this.alternativeFormat; - row = column.length; - this.alternativeFormat = - this.dateFormats[this.dateFormat] - .alternative; - } else { - column.unsorted = true; - } - } - descending = diff; - } - - } else { // string - column[row] = trimVal === '' ? null : trimVal; - if (row !== 0 && (column.isDatetime || column.isNumeric)) { - column.mixed = true; - } - } - } - } - - // If strings are intermixed with numbers or dates in a parsed column, - // it is an indication that parsing went wrong or the data was not - // intended to display as numbers or dates and parsing is too - // aggressive. Fall back to categories. Demonstrated in the - // highcharts/demo/column-drilldown sample. - if (isXColumn && column.mixed) { - columns[col] = rawColumns[col]; - } - - // If the 0 column is date or number and descending, reverse all - // columns. - if (isXColumn && descending && this.options.sort) { - for (col = 0; col < columns.length; col++) { - columns[col].reverse(); - if (firstRowAsNames) { - columns[col].unshift(columns[col].pop()); - } - } - } - }, - - /** - * A collection of available date formats, extendable from the outside to - * support custom date formats. - */ - dateFormats: { - 'YYYY/mm/dd': { - regex: /^([0-9]{4})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{1,2})$/, - parser: function (match) { - return Date.UTC(+match[1], match[2] - 1, +match[3]); - } - }, - 'dd/mm/YYYY': { - regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, - parser: function (match) { - return Date.UTC(+match[3], match[2] - 1, +match[1]); - }, - alternative: 'mm/dd/YYYY' // different format with the same regex - }, - 'mm/dd/YYYY': { - regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, - parser: function (match) { - return Date.UTC(+match[3], match[1] - 1, +match[2]); - } - }, - 'dd/mm/YY': { - regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, - parser: function (match) { - var year = +match[3], - d = new Date() - ; - - if (year > (d.getFullYear() - 2000)) { - year += 1900; - } else { - year += 2000; - } - - return Date.UTC(year, match[2] - 1, +match[1]); - }, - alternative: 'mm/dd/YY' // different format with the same regex - }, - 'mm/dd/YY': { - regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, - parser: function (match) { - return Date.UTC(+match[3] + 2000, match[1] - 1, +match[2]); - } - } - }, - - /** - * Parse a date and return it as a number. Overridable through - * `options.parseDate`. - */ - parseDate: function (val) { - var parseDate = this.options.parseDate, - ret, - key, - format, - dateFormat = this.options.dateFormat || this.dateFormat, - match; - - if (parseDate) { - ret = parseDate(val); - - } else if (typeof val === 'string') { - // Auto-detect the date format the first time - if (!dateFormat) { - for (key in this.dateFormats) { - format = this.dateFormats[key]; - match = val.match(format.regex); - if (match) { - this.dateFormat = dateFormat = key; - this.alternativeFormat = format.alternative; - ret = format.parser(match); - break; - } - } - // Next time, use the one previously found - } else { - format = this.dateFormats[dateFormat]; - - - if (!format) { - // The selected format is invalid - format = this.dateFormats['YYYY/mm/dd']; - } - - match = val.match(format.regex); - if (match) { - ret = format.parser(match); - } - - } - // Fall back to Date.parse - if (!match) { - match = Date.parse(val); - // External tools like Date.js and MooTools extend Date object - // and returns a date. - if ( - typeof match === 'object' && - match !== null && - match.getTime - ) { - ret = match.getTime() - match.getTimezoneOffset() * 60000; - - // Timestamp - } else if (isNumber(match)) { - ret = match - (new Date(match)).getTimezoneOffset() * 60000; - } - } - } - return ret; - }, - - /** - * Reorganize rows into columns - */ - rowsToColumns: function (rows) { - var row, - rowsLength, - col, - colsLength, - columns; - - if (rows) { - columns = []; - rowsLength = rows.length; - for (row = 0; row < rowsLength; row++) { - colsLength = rows[row].length; - for (col = 0; col < colsLength; col++) { - if (!columns[col]) { - columns[col] = []; - } - columns[col][row] = rows[row][col]; - } - } - } - return columns; - }, - - /** - * A hook for working directly on the parsed columns - */ - parsed: function () { - if (this.options.parsed) { - return this.options.parsed.call(this, this.columns); - } - }, - - getFreeIndexes: function (numberOfColumns, seriesBuilders) { - var s, - i, - freeIndexes = [], - freeIndexValues = [], - referencedIndexes; - - // Add all columns as free - for (i = 0; i < numberOfColumns; i = i + 1) { - freeIndexes.push(true); - } - - // Loop all defined builders and remove their referenced columns - for (s = 0; s < seriesBuilders.length; s = s + 1) { - referencedIndexes = seriesBuilders[s].getReferencedColumnIndexes(); - - for (i = 0; i < referencedIndexes.length; i = i + 1) { - freeIndexes[referencedIndexes[i]] = false; - } - } - - // Collect the values for the free indexes - for (i = 0; i < freeIndexes.length; i = i + 1) { - if (freeIndexes[i]) { - freeIndexValues.push(i); - } - } - - return freeIndexValues; - }, - - /** - * If a complete callback function is provided in the options, interpret the - * columns into a Highcharts options object. - */ - complete: function () { - - var columns = this.columns, - xColumns = [], - type, - options = this.options, - series, - data, - i, - j, - r, - seriesIndex, - chartOptions, - allSeriesBuilders = [], - builder, - freeIndexes, - typeCol, - index; - - xColumns.length = columns.length; - if (options.complete || options.afterComplete) { - - // Get the names and shift the top row - if (this.firstRowAsNames) { - for (i = 0; i < columns.length; i++) { - columns[i].name = columns[i].shift(); - } - } - - // Use the next columns for series - series = []; - freeIndexes = this.getFreeIndexes( - columns.length, - this.valueCount.seriesBuilders - ); - - // Populate defined series - for ( - seriesIndex = 0; - seriesIndex < this.valueCount.seriesBuilders.length; - seriesIndex++ - ) { - builder = this.valueCount.seriesBuilders[seriesIndex]; - - // If the builder can be populated with remaining columns, then - // add it to allBuilders - if (builder.populateColumns(freeIndexes)) { - allSeriesBuilders.push(builder); - } - } - - // Populate dynamic series - while (freeIndexes.length > 0) { - builder = new SeriesBuilder(); - builder.addColumnReader(0, 'x'); - - // Mark index as used (not free) - index = inArray(0, freeIndexes); - if (index !== -1) { - freeIndexes.splice(index, 1); - } - - for (i = 0; i < this.valueCount.global; i++) { - // Create and add a column reader for the next free column - // index - builder.addColumnReader( - undefined, - this.valueCount.globalPointArrayMap[i] - ); - } - - // If the builder can be populated with remaining columns, then - // add it to allBuilders - if (builder.populateColumns(freeIndexes)) { - allSeriesBuilders.push(builder); - } - } - - // Get the data-type from the first series x column - if ( - allSeriesBuilders.length > 0 && - allSeriesBuilders[0].readers.length > 0 - ) { - typeCol = columns[allSeriesBuilders[0].readers[0].columnIndex]; - if (typeCol !== undefined) { - if (typeCol.isDatetime) { - type = 'datetime'; - } else if (!typeCol.isNumeric) { - type = 'category'; - } - } - } - // Axis type is category, then the "x" column should be called - // "name" - if (type === 'category') { - for ( - seriesIndex = 0; - seriesIndex < allSeriesBuilders.length; - seriesIndex++ - ) { - builder = allSeriesBuilders[seriesIndex]; - for (r = 0; r < builder.readers.length; r++) { - if (builder.readers[r].configName === 'x') { - builder.readers[r].configName = 'name'; - } - } - } - } - - // Read data for all builders - for ( - seriesIndex = 0; - seriesIndex < allSeriesBuilders.length; - seriesIndex++ - ) { - builder = allSeriesBuilders[seriesIndex]; - - // Iterate down the cells of each column and add data to the - // series - data = []; - for (j = 0; j < columns[0].length; j++) { - data[j] = builder.read(columns, j); - } - - // Add the series - series[seriesIndex] = { - data: data - }; - if (builder.name) { - series[seriesIndex].name = builder.name; - } - if (type === 'category') { - series[seriesIndex].turboThreshold = 0; - } - } - - - - // Do the callback - chartOptions = { - series: series - }; - if (type) { - chartOptions.xAxis = { - type: type - }; - if (type === 'category') { - chartOptions.xAxis.uniqueNames = false; - } - } - - if (options.complete) { - options.complete(chartOptions); - } - - // The afterComplete hook is used internally to avoid conflict with - // the externally available complete option. - if (options.afterComplete) { - options.afterComplete(chartOptions); - } - } - - }, - - update: function (options, redraw) { - var chart = this.chart; - if (options) { - // Set the complete handler - options.afterComplete = function (dataOptions) { - // Avoid setting axis options unless the type changes. Running - // Axis.update will cause the whole structure to be destroyed - // and rebuilt, and animation is lost. - if ( - dataOptions.xAxis && - chart.xAxis[0] && - dataOptions.xAxis.type === chart.xAxis[0].options.type - ) { - delete dataOptions.xAxis; - } - - chart.update(dataOptions, redraw, true); - }; - // Apply it - merge(true, this.options, options); - this.init(this.options); - } - } + /** + * Initialize the Data object with the given options + */ + init: function (options, chartOptions, chart) { + + var decimalPoint = options.decimalPoint, + hasData; + + if (chartOptions) { + this.chartOptions = chartOptions; + } + if (chart) { + this.chart = chart; + } + + if (decimalPoint !== '.' && decimalPoint !== ',') { + decimalPoint = undefined; + } + + this.options = options; + this.columns = ( + options.columns || + this.rowsToColumns(options.rows) || + [] + ); + + this.firstRowAsNames = pick( + options.firstRowAsNames, + this.firstRowAsNames, + true + ); + + this.decimalRegex = ( + decimalPoint && + new RegExp('^(-?[0-9]+)' + decimalPoint + '([0-9]+)$') // eslint-disable-line security/detect-non-literal-regexp + ); + + // This is a two-dimensional array holding the raw, trimmed string + // values with the same organisation as the columns array. It makes it + // possible for example to revert from interpreted timestamps to + // string-based categories. + this.rawColumns = []; + + // No need to parse or interpret anything + if (this.columns.length) { + this.dataFound(); + hasData = true; + } + + if (!hasData) { + // Fetch live data + hasData = this.fetchLiveData(); + } + + if (!hasData) { + // Parse a CSV string if options.csv is given. The parseCSV function + // returns a columns array, if it has no length, we have no data + hasData = Boolean(this.parseCSV().length); + } + + if (!hasData) { + // Parse a HTML table if options.table is given + hasData = Boolean(this.parseTable().length); + } + + if (!hasData) { + // Parse a Google Spreadsheet + hasData = this.parseGoogleSpreadsheet(); + } + + if (!hasData && options.afterComplete) { + options.afterComplete(); + } + }, + + /** + * Get the column distribution. For example, a line series takes a single + * column for Y values. A range series takes two columns for low and high + * values respectively, and an OHLC series takes four columns. + */ + getColumnDistribution: function () { + var chartOptions = this.chartOptions, + options = this.options, + xColumns = [], + getValueCount = function (type) { + return ( + Highcharts.seriesTypes[type || 'line'].prototype + .pointArrayMap || + [0] + ).length; + }, + getPointArrayMap = function (type) { + return Highcharts.seriesTypes[type || 'line'] + .prototype.pointArrayMap; + }, + globalType = ( + chartOptions && + chartOptions.chart && + chartOptions.chart.type + ), + individualCounts = [], + seriesBuilders = [], + seriesIndex = 0, + i; + + each((chartOptions && chartOptions.series) || [], function (series) { + individualCounts.push(getValueCount(series.type || globalType)); + }); + + // Collect the x-column indexes from seriesMapping + each((options && options.seriesMapping) || [], function (mapping) { + xColumns.push(mapping.x || 0); + }); + + // If there are no defined series with x-columns, use the first column + // as x column + if (xColumns.length === 0) { + xColumns.push(0); + } + + // Loop all seriesMappings and constructs SeriesBuilders from + // the mapping options. + each((options && options.seriesMapping) || [], function (mapping) { + var builder = new SeriesBuilder(), + numberOfValueColumnsNeeded = individualCounts[seriesIndex] || + getValueCount(globalType), + seriesArr = (chartOptions && chartOptions.series) || [], + series = seriesArr[seriesIndex] || {}, + pointArrayMap = getPointArrayMap(series.type || globalType) || + ['y']; + + // Add an x reader from the x property or from an undefined column + // if the property is not set. It will then be auto populated later. + builder.addColumnReader(mapping.x, 'x'); + + // Add all column mappings + objectEach(mapping, function (val, name) { + if (name !== 'x') { + builder.addColumnReader(val, name); + } + }); + + // Add missing columns + for (i = 0; i < numberOfValueColumnsNeeded; i++) { + if (!builder.hasReader(pointArrayMap[i])) { + // Create and add a column reader for the next free column + // index + builder.addColumnReader(undefined, pointArrayMap[i]); + } + } + + seriesBuilders.push(builder); + seriesIndex++; + }); + + var globalPointArrayMap = getPointArrayMap(globalType); + if (globalPointArrayMap === undefined) { + globalPointArrayMap = ['y']; + } + + this.valueCount = { + global: getValueCount(globalType), + xColumns: xColumns, + individual: individualCounts, + seriesBuilders: seriesBuilders, + globalPointArrayMap: globalPointArrayMap + }; + }, + + /** + * When the data is parsed into columns, either by CSV, table, GS or direct + * input, continue with other operations. + */ + dataFound: function () { + + if (this.options.switchRowsAndColumns) { + this.columns = this.rowsToColumns(this.columns); + } + + // Interpret the info about series and columns + this.getColumnDistribution(); + + // Interpret the values into right types + this.parseTypes(); + + // Handle columns if a handleColumns callback is given + if (this.parsed() !== false) { + + // Complete if a complete callback is given + this.complete(); + } + + }, + + /** + * Parse a CSV input string + */ + parseCSV: function (inOptions) { + var self = this, + options = inOptions || this.options, + csv = options.csv, + columns, + startRow = ( + typeof options.startRow !== 'undefined' && options.startRow ? + options.startRow : + 0 + ), + endRow = options.endRow || Number.MAX_VALUE, + startColumn = ( + typeof options.startColumn !== 'undefined' && + options.startColumn + ) ? options.startColumn : 0, + endColumn = options.endColumn || Number.MAX_VALUE, + itemDelimiter, + lines, + rowIt = 0, + // activeRowNo = 0, + dataTypes = [], + // We count potential delimiters in the prepass, and use the + // result as the basis of half-intelligent guesses. + potDelimiters = { + ',': 0, + ';': 0, + '\t': 0 + }; + + columns = this.columns = []; + + /* + This implementation is quite verbose. It will be shortened once + it's stable and passes all the test. + + It's also not written with speed in mind, instead everything is + very seggregated, and there a several redundant loops. + This is to make it easier to stabilize the code initially. + + We do a pre-pass on the first 4 rows to make some intelligent + guesses on the set. Guessed delimiters are in this pass counted. + + Auto detecting delimiters + - If we meet a quoted string, the next symbol afterwards + (that's not \s, \t) is the delimiter + - If we meet a date, the next symbol afterwards is the delimiter + + Date formats + - If we meet a column with date formats, check all of them to + see if one of the potential months crossing 12. If it does, + we now know the format + + It would make things easier to guess the delimiter before + doing the actual parsing. + + General rules: + - Quoting is allowed, e.g: "Col 1",123,321 + - Quoting is optional, e.g.: Col1,123,321 + - Doubble quoting is escaping, e.g. "Col ""Hello world""",123 + - Spaces are considered part of the data: Col1 ,123 + - New line is always the row delimiter + - Potential column delimiters are , ; \t + - First row may optionally contain headers + - The last row may or may not have a row delimiter + - Comments are optionally supported, in which case the comment + must start at the first column, and the rest of the line will + be ignored + */ + + // Parse a single row + function parseRow(columnStr, rowNumber, noAdd, callbacks) { + var i = 0, + c = '', + cl = '', + cn = '', + token = '', + actualColumn = 0, + column = 0; + + function read(j) { + c = columnStr[j]; + cl = columnStr[j - 1]; + cn = columnStr[j + 1]; + } + + function pushType(type) { + if (dataTypes.length < column + 1) { + dataTypes.push([type]); + } + if (dataTypes[column][dataTypes[column].length - 1] !== type) { + dataTypes[column].push(type); + } + } + + function push() { + if (startColumn > actualColumn || actualColumn > endColumn) { + // Skip this column, but increment the column count (#7272) + ++actualColumn; + token = ''; + return; + } + + if (!isNaN(parseFloat(token)) && isFinite(token)) { + token = parseFloat(token); + pushType('number'); + } else if (!isNaN(Date.parse(token))) { + token = token.replace(/\//g, '-'); + pushType('date'); + } else { + pushType('string'); + } + + + if (columns.length < column + 1) { + columns.push([]); + } + + if (!noAdd) { + // Don't push - if there's a varrying amount of columns + // for each row, pushing will skew everything down n slots + columns[column][rowNumber] = token; + } + + token = ''; + ++column; + ++actualColumn; + } + + if (!columnStr.trim().length) { + return; + } + + if (columnStr.trim()[0] === '#') { + return; + } + + for (; i < columnStr.length; i++) { + read(i); + + // Quoted string + if (c === '#') { + // The rest of the row is a comment + push(); + return; + } else if (c === '"') { + read(++i); + + while (i < columnStr.length) { + if (c === '"' && cl !== '"' && cn !== '"') { + break; + } + + if (c !== '"' || (c === '"' && cl !== '"')) { + token += c; + } + + read(++i); + } + + // Perform "plugin" handling + } else if (callbacks && callbacks[c]) { + if (callbacks[c](c, token)) { + push(); + } + + // Delimiter - push current token + } else if (c === itemDelimiter) { + push(); + + // Actual column data + } else { + token += c; + } + } + + push(); + + } + + // Attempt to guess the delimiter + // We do a separate parse pass here because we need + // to count potential delimiters softly without making any assumptions. + function guessDelimiter(lines) { + var points = 0, + commas = 0, + guessed = false; + + some(lines, function (columnStr, i) { + var inStr = false, + c, + cn, + cl, + token = '' + ; + + + // We should be able to detect dateformats within 13 rows + if (i > 13) { + return true; + } + + for (var j = 0; j < columnStr.length; j++) { + c = columnStr[j]; + cn = columnStr[j + 1]; + cl = columnStr[j - 1]; + + if (c === '#') { + // Skip the rest of the line - it's a comment + return; + } else if (c === '"') { + if (inStr) { + if (cl !== '"' && cn !== '"') { + while (cn === ' ' && j < columnStr.length) { + cn = columnStr[++j]; + } + + // After parsing a string, the next non-blank + // should be a delimiter if the CSV is properly + // formed. + + if (typeof potDelimiters[cn] !== 'undefined') { + potDelimiters[cn]++; + } + + inStr = false; + } + } else { + inStr = true; + } + } else if (typeof potDelimiters[c] !== 'undefined') { + + token = token.trim(); + + if (!isNaN(Date.parse(token))) { + potDelimiters[c]++; + } else if (isNaN(token) || !isFinite(token)) { + potDelimiters[c]++; + } + + token = ''; + + } else { + token += c; + } + + if (c === ',') { + commas++; + } + + if (c === '.') { + points++; + } + } + }); + + // Count the potential delimiters. + // This could be improved by checking if the number of delimiters + // equals the number of columns - 1 + + if (potDelimiters[';'] > potDelimiters[',']) { + guessed = ';'; + } else if (potDelimiters[','] > potDelimiters[';']) { + guessed = ','; + } else { + // No good guess could be made.. + guessed = ','; + } + + // Try to deduce the decimal point if it's not explicitly set. + // If both commas or points is > 0 there is likely an issue + if (!options.decimalPoint) { + if (points > commas) { + options.decimalPoint = '.'; + } else { + options.decimalPoint = ','; + } + + // Apply a new decimal regex based on the presumed decimal sep. + self.decimalRegex = new RegExp( // eslint-disable-line security/detect-non-literal-regexp + '^(-?[0-9]+)' + + options.decimalPoint + + '([0-9]+)$' + ); + } + + return guessed; + } + + /* Tries to guess the date format + * - Check if either month candidate exceeds 12 + * - Check if year is missing (use current year) + * - Check if a shortened year format is used (e.g. 1/1/99) + * - If no guess can be made, the user must be prompted + * data is the data to deduce a format based on + */ + function deduceDateFormat(data, limit) { + var format = 'YYYY/mm/dd', + thing, + guessedFormat, + calculatedFormat, + i = 0, + madeDeduction = false, + // candidates = {}, + stable = [], + max = [], + j; + + if (!limit || limit > data.length) { + limit = data.length; + } + + for (; i < limit; i++) { + if ( + typeof data[i] !== 'undefined' && + data[i] && data[i].length + ) { + thing = data[i] + .trim() + .replace(/\//g, ' ') + .replace(/\-/g, ' ') + .split(' '); + + guessedFormat = [ + '', + '', + '' + ]; + + + for (j = 0; j < thing.length; j++) { + if (j < guessedFormat.length) { + thing[j] = parseInt(thing[j], 10); + + if (thing[j]) { + + max[j] = (!max[j] || max[j] < thing[j]) ? + thing[j] : + max[j]; + + if (typeof stable[j] !== 'undefined') { + if (stable[j] !== thing[j]) { + stable[j] = false; + } + } else { + stable[j] = thing[j]; + } + + if (thing[j] > 31) { + if (thing[j] < 100) { + guessedFormat[j] = 'YY'; + } else { + guessedFormat[j] = 'YYYY'; + } + // madeDeduction = true; + } else if (thing[j] > 12 && thing[j] <= 31) { + guessedFormat[j] = 'dd'; + madeDeduction = true; + } else if (!guessedFormat[j].length) { + guessedFormat[j] = 'mm'; + } + } + } + } + } + } + + if (madeDeduction) { + + // This handles a few edge cases with hard to guess dates + for (j = 0; j < stable.length; j++) { + if (stable[j] !== false) { + if ( + max[j] > 12 && + guessedFormat[j] !== 'YY' && + guessedFormat[j] !== 'YYYY' + ) { + guessedFormat[j] = 'YY'; + } + } else if (max[j] > 12 && guessedFormat[j] === 'mm') { + guessedFormat[j] = 'dd'; + } + } + + // If the middle one is dd, and the last one is dd, + // the last should likely be year. + if (guessedFormat.length === 3 && + guessedFormat[1] === 'dd' && + guessedFormat[2] === 'dd') { + guessedFormat[2] = 'YY'; + } + + calculatedFormat = guessedFormat.join('/'); + + // If the caculated format is not valid, we need to present an + // error. + + if ( + !(options.dateFormats || self.dateFormats)[calculatedFormat] + ) { + // This should emit an event instead + fireEvent('deduceDateFailed'); + return format; + } + + return calculatedFormat; + } + + return format; + } + + /* Figure out the best axis types for the data + * - If the first column is a number, we're good + * - If the first column is a date, set to date/time + * - If the first column is a string, set to categories + */ + function deduceAxisTypes() { + + } + + if (csv) { + + lines = csv + .replace(/\r\n/g, '\n') // Unix + .replace(/\r/g, '\n') // Mac + .split(options.lineDelimiter || '\n'); + + if (!startRow || startRow < 0) { + startRow = 0; + } + + if (!endRow || endRow >= lines.length) { + endRow = lines.length - 1; + } + + if (options.itemDelimiter) { + itemDelimiter = options.itemDelimiter; + } else { + itemDelimiter = null; + itemDelimiter = guessDelimiter(lines); + } + + var offset = 0; + + for (rowIt = startRow; rowIt <= endRow; rowIt++) { + if (lines[rowIt][0] === '#') { + offset++; + } else { + parseRow(lines[rowIt], rowIt - startRow - offset); + } + } + + // //Make sure that there's header columns for everything + // each(columns, function (col) { + + // }); + + deduceAxisTypes(); + + if ((!options.columnTypes || options.columnTypes.length === 0) && + dataTypes.length && + dataTypes[0].length && + dataTypes[0][1] === 'date' && + !options.dateFormat) { + options.dateFormat = deduceDateFormat(columns[0]); + } + + + // each(lines, function (line, rowNo) { + // var trimmed = self.trim(line), + // isComment = trimmed.indexOf('#') === 0, + // isBlank = trimmed === '', + // items; + + // if ( + // rowNo >= startRow && + // rowNo <= endRow && + // !isComment && !isBlank + // ) { + // items = line.split(itemDelimiter); + // each(items, function (item, colNo) { + // if (colNo >= startColumn && colNo <= endColumn) { + // if (!columns[colNo - startColumn]) { + // columns[colNo - startColumn] = []; + // } + + // columns[colNo - startColumn][activeRowNo] = item; + // } + // }); + // activeRowNo += 1; + // } + // }); + // + + this.dataFound(); + } + + return columns; + }, + + /** + * Parse a HTML table + */ + parseTable: function () { + var options = this.options, + table = options.table, + columns = this.columns, + startRow = options.startRow || 0, + endRow = options.endRow || Number.MAX_VALUE, + startColumn = options.startColumn || 0, + endColumn = options.endColumn || Number.MAX_VALUE; + + if (table) { + + if (typeof table === 'string') { + table = doc.getElementById(table); + } + + each(table.getElementsByTagName('tr'), function (tr, rowNo) { + if (rowNo >= startRow && rowNo <= endRow) { + each(tr.children, function (item, colNo) { + if ( + (item.tagName === 'TD' || item.tagName === 'TH') && + colNo >= startColumn && + colNo <= endColumn + ) { + if (!columns[colNo - startColumn]) { + columns[colNo - startColumn] = []; + } + + columns[colNo - startColumn][rowNo - startRow] = + item.innerHTML; + } + }); + } + }); + + this.dataFound(); // continue + } + return columns; + }, + + + /** + * Fetch or refetch live data + */ + fetchLiveData: function () { + var chart = this.chart, + options = this.options, + maxRetries = 3, + currentRetries = 0, + pollingEnabled = options.enablePolling, + updateIntervalMs = (options.dataRefreshRate || 2) * 1000, + originalOptions = merge(options); + + if (!options || + (!options.csvURL && !options.rowsURL && !options.columnsURL) + ) { + return false; + } + + // Do not allow polling more than once a second + if (updateIntervalMs < 1000) { + updateIntervalMs = 1000; + } + + delete options.csvURL; + delete options.rowsURL; + delete options.columnsURL; + + function performFetch(initialFetch) { + + // Helper function for doing the data fetch + polling + function request(url, done, tp) { + if (!url || url.indexOf('http') !== 0) { + if (url && options.error) { + options.error('Invalid URL'); + } + return false; + } + + if (initialFetch) { + clearTimeout(chart.liveDataTimeout); + chart.liveDataURL = url; + } + + function poll() { + // Poll + if (pollingEnabled && chart.liveDataURL === url) { + // We need to stop doing this if the URL has changed + chart.liveDataTimeout = + setTimeout(performFetch, updateIntervalMs); + } + } + + Highcharts.ajax({ + url: url, + dataType: tp || 'json', + success: function (res) { + if (chart && chart.series) { + done(res); + } + + poll(); + + }, + error: function (xhr, text) { + if (++currentRetries < maxRetries) { + poll(); + } + + return options.error && options.error(text, xhr); + } + }); + + return true; + } + + if (!request(originalOptions.csvURL, function (res) { + chart.update({ + data: { + csv: res + } + }); + }, 'text')) { + if (!request(originalOptions.rowsURL, function (res) { + chart.update({ + data: { + rows: res + } + }); + })) { + request(originalOptions.columnsURL, function (res) { + chart.update({ + data: { + columns: res + } + }); + }); + } + } + } + + performFetch(true); + + return (options && + (options.csvURL || options.rowsURL || options.columnsURL) + ); + }, + + + /** + * Parse a Google spreadsheet. + */ + parseGoogleSpreadsheet: function () { + var options = this.options, + googleSpreadsheetKey = options.googleSpreadsheetKey, + chart = this.chart, + // use sheet 1 as the default rather than od6 + // as the latter sometimes cause issues (it looks like it can + // be renamed in some cases, ref. a fogbugz case). + worksheet = options.googleSpreadsheetWorksheet || 1, + startRow = options.startRow || 0, + endRow = options.endRow || Number.MAX_VALUE, + startColumn = options.startColumn || 0, + endColumn = options.endColumn || Number.MAX_VALUE, + refreshRate = (options.dataRefreshRate || 2) * 1000; + + if (refreshRate < 4000) { + refreshRate = 4000; + } + + /* + * Fetch the actual spreadsheet using XMLHttpRequest + */ + function fetchSheet(fn) { + var url = [ + 'https://spreadsheets.google.com/feeds/cells', + googleSpreadsheetKey, + worksheet, + 'public/values?alt=json' + ].join('/'); + + Highcharts.ajax({ + url: url, + dataType: 'json', + success: function (json) { + fn(json); + + if (options.enablePolling) { + setTimeout(function () { + fetchSheet(fn); + }, options.dataRefreshRate); + } + }, + error: function (xhr, text) { + return options.error && options.error(text, xhr); + } + }); + } + + if (googleSpreadsheetKey) { + + delete options.googleSpreadsheetKey; + + fetchSheet(function (json) { + // Prepare the data from the spreadsheat + var columns = [], + cells = json.feed.entry, + cell, + cellCount = (cells || []).length, + colCount = 0, + rowCount = 0, + val, + gr, + gc, + cellInner, + i; + + if (!cells || cells.length === 0) { + return false; + } + + // First, find the total number of columns and rows that + // are actually filled with data + for (i = 0; i < cellCount; i++) { + cell = cells[i]; + colCount = Math.max(colCount, cell.gs$cell.col); + rowCount = Math.max(rowCount, cell.gs$cell.row); + } + + // Set up arrays containing the column data + for (i = 0; i < colCount; i++) { + if (i >= startColumn && i <= endColumn) { + // Create new columns with the length of either + // end-start or rowCount + columns[i - startColumn] = []; + } + } + + // Loop over the cells and assign the value to the right + // place in the column arrays + for (i = 0; i < cellCount; i++) { + cell = cells[i]; + gr = cell.gs$cell.row - 1; // rows start at 1 + gc = cell.gs$cell.col - 1; // columns start at 1 + + // If both row and col falls inside start and end set the + // transposed cell value in the newly created columns + if (gc >= startColumn && gc <= endColumn && + gr >= startRow && gr <= endRow) { + + cellInner = cell.gs$cell || cell.content; + + val = null; + + if (cellInner.numericValue) { + if (cellInner.$t.indexOf('/') >= 0 || + cellInner.$t.indexOf('-') >= 0) { + // This is a date - for future reference. + val = cellInner.$t; + } else if (cellInner.$t.indexOf('%') > 0) { + // Percentage + val = parseFloat(cellInner.numericValue) * 100; + } else { + val = parseFloat(cellInner.numericValue); + } + } else if (cellInner.$t && cellInner.$t.length) { + val = cellInner.$t; + } + + columns[gc - startColumn][gr - startRow] = val; + } + } + + // Insert null for empty spreadsheet cells (#5298) + each(columns, function (column) { + for (i = 0; i < column.length; i++) { + if (column[i] === undefined) { + column[i] = null; + } + } + }); + + if (chart && chart.series) { + chart.update({ + data: { + columns: columns + } + }); + } + }); + } + + // This is an intermediate fetch, so always return false. + return false; + }, + + /** + * Trim a string from whitespace + */ + trim: function (str, inside) { + if (typeof str === 'string') { + str = str.replace(/^\s+|\s+$/g, ''); + + // Clear white space insdie the string, like thousands separators + if (inside && /^[0-9\s]+$/.test(str)) { + str = str.replace(/\s/g, ''); + } + + if (this.decimalRegex) { + str = str.replace(this.decimalRegex, '$1.$2'); + } + } + return str; + }, + + /** + * Parse numeric cells in to number types and date types in to true dates. + */ + parseTypes: function () { + var columns = this.columns, + col = columns.length; + + while (col--) { + this.parseColumn(columns[col], col); + } + + }, + + /** + * Parse a single column. Set properties like .isDatetime and .isNumeric. + */ + parseColumn: function (column, col) { + var rawColumns = this.rawColumns, + columns = this.columns, + row = column.length, + val, + floatVal, + trimVal, + trimInsideVal, + firstRowAsNames = this.firstRowAsNames, + isXColumn = inArray(col, this.valueCount.xColumns) !== -1, + dateVal, + backup = [], + diff, + chartOptions = this.chartOptions, + descending, + columnTypes = this.options.columnTypes || [], + columnType = columnTypes[col], + forceCategory = isXColumn && (( + chartOptions && + chartOptions.xAxis && + splat(chartOptions.xAxis)[0].type === 'category' + ) || columnType === 'string'); + + if (!rawColumns[col]) { + rawColumns[col] = []; + } + while (row--) { + val = backup[row] || column[row]; + + trimVal = this.trim(val); + trimInsideVal = this.trim(val, true); + floatVal = parseFloat(trimInsideVal); + + // Set it the first time + if (rawColumns[col][row] === undefined) { + rawColumns[col][row] = trimVal; + } + + // Disable number or date parsing by setting the X axis type to + // category + if (forceCategory || (row === 0 && firstRowAsNames)) { + column[row] = '' + trimVal; + + } else if (+trimInsideVal === floatVal) { // is numeric + + column[row] = floatVal; + + // If the number is greater than milliseconds in a year, assume + // datetime + if ( + floatVal > 365 * 24 * 3600 * 1000 && + columnType !== 'float' + ) { + column.isDatetime = true; + } else { + column.isNumeric = true; + } + + if (column[row + 1] !== undefined) { + descending = floatVal > column[row + 1]; + } + + // String, continue to determine if it is a date string or really a + // string + } else { + if (trimVal && trimVal.length) { + dateVal = this.parseDate(val); + } + + // Only allow parsing of dates if this column is an x-column + if (isXColumn && isNumber(dateVal) && columnType !== 'float') { + backup[row] = val; + column[row] = dateVal; + column.isDatetime = true; + + // Check if the dates are uniformly descending or ascending. + // If they are not, chances are that they are a different + // time format, so check for alternative. + if (column[row + 1] !== undefined) { + diff = dateVal > column[row + 1]; + if (diff !== descending && descending !== undefined) { + if (this.alternativeFormat) { + this.dateFormat = this.alternativeFormat; + row = column.length; + this.alternativeFormat = + this.dateFormats[this.dateFormat] + .alternative; + } else { + column.unsorted = true; + } + } + descending = diff; + } + + } else { // string + column[row] = trimVal === '' ? null : trimVal; + if (row !== 0 && (column.isDatetime || column.isNumeric)) { + column.mixed = true; + } + } + } + } + + // If strings are intermixed with numbers or dates in a parsed column, + // it is an indication that parsing went wrong or the data was not + // intended to display as numbers or dates and parsing is too + // aggressive. Fall back to categories. Demonstrated in the + // highcharts/demo/column-drilldown sample. + if (isXColumn && column.mixed) { + columns[col] = rawColumns[col]; + } + + // If the 0 column is date or number and descending, reverse all + // columns. + if (isXColumn && descending && this.options.sort) { + for (col = 0; col < columns.length; col++) { + columns[col].reverse(); + if (firstRowAsNames) { + columns[col].unshift(columns[col].pop()); + } + } + } + }, + + /** + * A collection of available date formats, extendable from the outside to + * support custom date formats. + */ + dateFormats: { + 'YYYY/mm/dd': { + regex: /^([0-9]{4})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{1,2})$/, + parser: function (match) { + return Date.UTC(+match[1], match[2] - 1, +match[3]); + } + }, + 'dd/mm/YYYY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, + parser: function (match) { + return Date.UTC(+match[3], match[2] - 1, +match[1]); + }, + alternative: 'mm/dd/YYYY' // different format with the same regex + }, + 'mm/dd/YYYY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, + parser: function (match) { + return Date.UTC(+match[3], match[1] - 1, +match[2]); + } + }, + 'dd/mm/YY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, + parser: function (match) { + var year = +match[3], + d = new Date() + ; + + if (year > (d.getFullYear() - 2000)) { + year += 1900; + } else { + year += 2000; + } + + return Date.UTC(year, match[2] - 1, +match[1]); + }, + alternative: 'mm/dd/YY' // different format with the same regex + }, + 'mm/dd/YY': { + regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, + parser: function (match) { + return Date.UTC(+match[3] + 2000, match[1] - 1, +match[2]); + } + } + }, + + /** + * Parse a date and return it as a number. Overridable through + * `options.parseDate`. + */ + parseDate: function (val) { + var parseDate = this.options.parseDate, + ret, + key, + format, + dateFormat = this.options.dateFormat || this.dateFormat, + match; + + if (parseDate) { + ret = parseDate(val); + + } else if (typeof val === 'string') { + // Auto-detect the date format the first time + if (!dateFormat) { + for (key in this.dateFormats) { + format = this.dateFormats[key]; + match = val.match(format.regex); + if (match) { + this.dateFormat = dateFormat = key; + this.alternativeFormat = format.alternative; + ret = format.parser(match); + break; + } + } + // Next time, use the one previously found + } else { + format = this.dateFormats[dateFormat]; + + + if (!format) { + // The selected format is invalid + format = this.dateFormats['YYYY/mm/dd']; + } + + match = val.match(format.regex); + if (match) { + ret = format.parser(match); + } + + } + // Fall back to Date.parse + if (!match) { + match = Date.parse(val); + // External tools like Date.js and MooTools extend Date object + // and returns a date. + if ( + typeof match === 'object' && + match !== null && + match.getTime + ) { + ret = match.getTime() - match.getTimezoneOffset() * 60000; + + // Timestamp + } else if (isNumber(match)) { + ret = match - (new Date(match)).getTimezoneOffset() * 60000; + } + } + } + return ret; + }, + + /** + * Reorganize rows into columns + */ + rowsToColumns: function (rows) { + var row, + rowsLength, + col, + colsLength, + columns; + + if (rows) { + columns = []; + rowsLength = rows.length; + for (row = 0; row < rowsLength; row++) { + colsLength = rows[row].length; + for (col = 0; col < colsLength; col++) { + if (!columns[col]) { + columns[col] = []; + } + columns[col][row] = rows[row][col]; + } + } + } + return columns; + }, + + /** + * A hook for working directly on the parsed columns + */ + parsed: function () { + if (this.options.parsed) { + return this.options.parsed.call(this, this.columns); + } + }, + + getFreeIndexes: function (numberOfColumns, seriesBuilders) { + var s, + i, + freeIndexes = [], + freeIndexValues = [], + referencedIndexes; + + // Add all columns as free + for (i = 0; i < numberOfColumns; i = i + 1) { + freeIndexes.push(true); + } + + // Loop all defined builders and remove their referenced columns + for (s = 0; s < seriesBuilders.length; s = s + 1) { + referencedIndexes = seriesBuilders[s].getReferencedColumnIndexes(); + + for (i = 0; i < referencedIndexes.length; i = i + 1) { + freeIndexes[referencedIndexes[i]] = false; + } + } + + // Collect the values for the free indexes + for (i = 0; i < freeIndexes.length; i = i + 1) { + if (freeIndexes[i]) { + freeIndexValues.push(i); + } + } + + return freeIndexValues; + }, + + /** + * If a complete callback function is provided in the options, interpret the + * columns into a Highcharts options object. + */ + complete: function () { + + var columns = this.columns, + xColumns = [], + type, + options = this.options, + series, + data, + i, + j, + r, + seriesIndex, + chartOptions, + allSeriesBuilders = [], + builder, + freeIndexes, + typeCol, + index; + + xColumns.length = columns.length; + if (options.complete || options.afterComplete) { + + // Get the names and shift the top row + if (this.firstRowAsNames) { + for (i = 0; i < columns.length; i++) { + columns[i].name = columns[i].shift(); + } + } + + // Use the next columns for series + series = []; + freeIndexes = this.getFreeIndexes( + columns.length, + this.valueCount.seriesBuilders + ); + + // Populate defined series + for ( + seriesIndex = 0; + seriesIndex < this.valueCount.seriesBuilders.length; + seriesIndex++ + ) { + builder = this.valueCount.seriesBuilders[seriesIndex]; + + // If the builder can be populated with remaining columns, then + // add it to allBuilders + if (builder.populateColumns(freeIndexes)) { + allSeriesBuilders.push(builder); + } + } + + // Populate dynamic series + while (freeIndexes.length > 0) { + builder = new SeriesBuilder(); + builder.addColumnReader(0, 'x'); + + // Mark index as used (not free) + index = inArray(0, freeIndexes); + if (index !== -1) { + freeIndexes.splice(index, 1); + } + + for (i = 0; i < this.valueCount.global; i++) { + // Create and add a column reader for the next free column + // index + builder.addColumnReader( + undefined, + this.valueCount.globalPointArrayMap[i] + ); + } + + // If the builder can be populated with remaining columns, then + // add it to allBuilders + if (builder.populateColumns(freeIndexes)) { + allSeriesBuilders.push(builder); + } + } + + // Get the data-type from the first series x column + if ( + allSeriesBuilders.length > 0 && + allSeriesBuilders[0].readers.length > 0 + ) { + typeCol = columns[allSeriesBuilders[0].readers[0].columnIndex]; + if (typeCol !== undefined) { + if (typeCol.isDatetime) { + type = 'datetime'; + } else if (!typeCol.isNumeric) { + type = 'category'; + } + } + } + // Axis type is category, then the "x" column should be called + // "name" + if (type === 'category') { + for ( + seriesIndex = 0; + seriesIndex < allSeriesBuilders.length; + seriesIndex++ + ) { + builder = allSeriesBuilders[seriesIndex]; + for (r = 0; r < builder.readers.length; r++) { + if (builder.readers[r].configName === 'x') { + builder.readers[r].configName = 'name'; + } + } + } + } + + // Read data for all builders + for ( + seriesIndex = 0; + seriesIndex < allSeriesBuilders.length; + seriesIndex++ + ) { + builder = allSeriesBuilders[seriesIndex]; + + // Iterate down the cells of each column and add data to the + // series + data = []; + for (j = 0; j < columns[0].length; j++) { + data[j] = builder.read(columns, j); + } + + // Add the series + series[seriesIndex] = { + data: data + }; + if (builder.name) { + series[seriesIndex].name = builder.name; + } + if (type === 'category') { + series[seriesIndex].turboThreshold = 0; + } + } + + + + // Do the callback + chartOptions = { + series: series + }; + if (type) { + chartOptions.xAxis = { + type: type + }; + if (type === 'category') { + chartOptions.xAxis.uniqueNames = false; + } + } + + if (options.complete) { + options.complete(chartOptions); + } + + // The afterComplete hook is used internally to avoid conflict with + // the externally available complete option. + if (options.afterComplete) { + options.afterComplete(chartOptions); + } + } + + }, + + update: function (options, redraw) { + var chart = this.chart; + if (options) { + // Set the complete handler + options.afterComplete = function (dataOptions) { + // Avoid setting axis options unless the type changes. Running + // Axis.update will cause the whole structure to be destroyed + // and rebuilt, and animation is lost. + if ( + dataOptions.xAxis && + chart.xAxis[0] && + dataOptions.xAxis.type === chart.xAxis[0].options.type + ) { + delete dataOptions.xAxis; + } + + chart.update(dataOptions, redraw, true); + }; + // Apply it + merge(true, this.options, options); + this.init(this.options); + } + } }); // Register the Data prototype and data function on Highcharts Highcharts.Data = Data; Highcharts.data = function (options, chartOptions) { - return new Data(options, chartOptions); + return new Data(options, chartOptions); }; // Extend Chart.init so that the Chart constructor accepts a new configuration // option group, data. addEvent( - Chart, - 'init', - function (e) { - var chart = this, - userOptions = e.args[0], - callback = e.args[1]; - - if (userOptions && userOptions.data && !chart.hasDataDef) { - chart.hasDataDef = true; - chart.data = new Data(Highcharts.extend(userOptions.data, { - - afterComplete: function (dataOptions) { - var i, series; - - // Merge series configs - if (userOptions.hasOwnProperty('series')) { - if (typeof userOptions.series === 'object') { - i = Math.max( - userOptions.series.length, - dataOptions && dataOptions.series ? - dataOptions.series.length : - 0 - ); - while (i--) { - series = userOptions.series[i] || {}; - userOptions.series[i] = merge( - series, - dataOptions && dataOptions.series ? - dataOptions.series[i] : - {} - ); - } - } else { // Allow merging in dataOptions.series (#2856) - delete userOptions.series; - } - } - - // Do the merge - userOptions = merge(dataOptions, userOptions); - - // Run chart.init again - chart.init(userOptions, callback); - } - }), userOptions, chart); - - e.preventDefault(); - } - } + Chart, + 'init', + function (e) { + var chart = this, + userOptions = e.args[0], + callback = e.args[1]; + + if (userOptions && userOptions.data && !chart.hasDataDef) { + chart.hasDataDef = true; + chart.data = new Data(Highcharts.extend(userOptions.data, { + + afterComplete: function (dataOptions) { + var i, series; + + // Merge series configs + if (userOptions.hasOwnProperty('series')) { + if (typeof userOptions.series === 'object') { + i = Math.max( + userOptions.series.length, + dataOptions && dataOptions.series ? + dataOptions.series.length : + 0 + ); + while (i--) { + series = userOptions.series[i] || {}; + userOptions.series[i] = merge( + series, + dataOptions && dataOptions.series ? + dataOptions.series[i] : + {} + ); + } + } else { // Allow merging in dataOptions.series (#2856) + delete userOptions.series; + } + } + + // Do the merge + userOptions = merge(dataOptions, userOptions); + + // Run chart.init again + chart.init(userOptions, callback); + } + }), userOptions, chart); + + e.preventDefault(); + } + } ); /** @@ -2072,8 +2072,8 @@ addEvent( * @constructor */ SeriesBuilder = function () { - this.readers = []; - this.pointIsArray = true; + this.readers = []; + this.pointIsArray = true; }; /** @@ -2083,28 +2083,28 @@ SeriesBuilder = function () { * @returns {boolean} */ SeriesBuilder.prototype.populateColumns = function (freeIndexes) { - var builder = this, - enoughColumns = true; - - // Loop each reader and give it an index if its missing. - // The freeIndexes.shift() will return undefined if there - // are no more columns. - each(builder.readers, function (reader) { - if (reader.columnIndex === undefined) { - reader.columnIndex = freeIndexes.shift(); - } - }); - - // Now, all readers should have columns mapped. If not - // then return false to signal that this series should - // not be added. - each(builder.readers, function (reader) { - if (reader.columnIndex === undefined) { - enoughColumns = false; - } - }); - - return enoughColumns; + var builder = this, + enoughColumns = true; + + // Loop each reader and give it an index if its missing. + // The freeIndexes.shift() will return undefined if there + // are no more columns. + each(builder.readers, function (reader) { + if (reader.columnIndex === undefined) { + reader.columnIndex = freeIndexes.shift(); + } + }); + + // Now, all readers should have columns mapped. If not + // then return false to signal that this series should + // not be added. + each(builder.readers, function (reader) { + if (reader.columnIndex === undefined) { + enoughColumns = false; + } + }); + + return enoughColumns; }; /** @@ -2115,45 +2115,45 @@ SeriesBuilder.prototype.populateColumns = function (freeIndexes) { * @returns {Array | Object} */ SeriesBuilder.prototype.read = function (columns, rowIndex) { - var builder = this, - pointIsArray = builder.pointIsArray, - point = pointIsArray ? [] : {}, - columnIndexes; - - // Loop each reader and ask it to read its value. - // Then, build an array or point based on the readers names. - each(builder.readers, function (reader) { - var value = columns[reader.columnIndex][rowIndex]; - if (pointIsArray) { - point.push(value); - } else { - if (reader.configName.indexOf('.') > 0) { - // Handle nested property names - Highcharts.Point.prototype.setNestedProperty( - point, value, reader.configName - ); - } else { - point[reader.configName] = value; - } - } - }); - - // The name comes from the first column (excluding the x column) - if (this.name === undefined && builder.readers.length >= 2) { - columnIndexes = builder.getReferencedColumnIndexes(); - if (columnIndexes.length >= 2) { - // remove the first one (x col) - columnIndexes.shift(); - - // Sort the remaining - columnIndexes.sort(); - - // Now use the lowest index as name column - this.name = columns[columnIndexes.shift()].name; - } - } - - return point; + var builder = this, + pointIsArray = builder.pointIsArray, + point = pointIsArray ? [] : {}, + columnIndexes; + + // Loop each reader and ask it to read its value. + // Then, build an array or point based on the readers names. + each(builder.readers, function (reader) { + var value = columns[reader.columnIndex][rowIndex]; + if (pointIsArray) { + point.push(value); + } else { + if (reader.configName.indexOf('.') > 0) { + // Handle nested property names + Highcharts.Point.prototype.setNestedProperty( + point, value, reader.configName + ); + } else { + point[reader.configName] = value; + } + } + }); + + // The name comes from the first column (excluding the x column) + if (this.name === undefined && builder.readers.length >= 2) { + columnIndexes = builder.getReferencedColumnIndexes(); + if (columnIndexes.length >= 2) { + // remove the first one (x col) + columnIndexes.shift(); + + // Sort the remaining + columnIndexes.sort(); + + // Now use the lowest index as name column + this.name = columns[columnIndexes.shift()].name; + } + } + + return point; }; /** @@ -2164,16 +2164,16 @@ SeriesBuilder.prototype.read = function (columns, rowIndex) { * @param configName */ SeriesBuilder.prototype.addColumnReader = function (columnIndex, configName) { - this.readers.push({ - columnIndex: columnIndex, - configName: configName - }); - - if ( - !(configName === 'x' || configName === 'y' || configName === undefined) - ) { - this.pointIsArray = false; - } + this.readers.push({ + columnIndex: columnIndex, + configName: configName + }); + + if ( + !(configName === 'x' || configName === 'y' || configName === undefined) + ) { + this.pointIsArray = false; + } }; /** @@ -2182,18 +2182,18 @@ SeriesBuilder.prototype.addColumnReader = function (columnIndex, configName) { * @returns {Array} */ SeriesBuilder.prototype.getReferencedColumnIndexes = function () { - var i, - referencedColumnIndexes = [], - columnReader; - - for (i = 0; i < this.readers.length; i = i + 1) { - columnReader = this.readers[i]; - if (columnReader.columnIndex !== undefined) { - referencedColumnIndexes.push(columnReader.columnIndex); - } - } - - return referencedColumnIndexes; + var i, + referencedColumnIndexes = [], + columnReader; + + for (i = 0; i < this.readers.length; i = i + 1) { + columnReader = this.readers[i]; + if (columnReader.columnIndex !== undefined) { + referencedColumnIndexes.push(columnReader.columnIndex); + } + } + + return referencedColumnIndexes; }; /** @@ -2202,12 +2202,12 @@ SeriesBuilder.prototype.getReferencedColumnIndexes = function () { * @returns {boolean} */ SeriesBuilder.prototype.hasReader = function (configName) { - var i, columnReader; - for (i = 0; i < this.readers.length; i = i + 1) { - columnReader = this.readers[i]; - if (columnReader.configName === configName) { - return true; - } - } - // Else return undefined + var i, columnReader; + for (i = 0; i < this.readers.length; i = i + 1) { + columnReader = this.readers[i]; + if (columnReader.configName === configName) { + return true; + } + } + // Else return undefined }; diff --git a/js/modules/drag-panes.src.js b/js/modules/drag-panes.src.js index e30352572bc..4551382628d 100644 --- a/js/modules/drag-panes.src.js +++ b/js/modules/drag-panes.src.js @@ -6,7 +6,7 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from '../parts/Globals.js'; import '../parts/Utilities.js'; @@ -14,179 +14,179 @@ import '../parts/Axis.js'; import '../parts/Pointer.js'; var hasTouch = H.hasTouch, - merge = H.merge, - wrap = H.wrap, - each = H.each, - isNumber = H.isNumber, - addEvent = H.addEvent, - relativeLength = H.relativeLength, - objectEach = H.objectEach, - Axis = H.Axis, - Pointer = H.Pointer, - - /** - * Default options for AxisResizer. - */ - resizerOptions = { - /** - * Minimal size of a resizable axis. Could be set as a percent - * of plot area or pixel size. - * - * This feature requires the `drag-panes.js` module. - * - * @type {Number|String} - * @product highstock - * @sample {highstock} stock/yaxis/resize-min-max-length - * minLength and maxLength - * @apioption yAxis.minLength - */ - minLength: '10%', - - /** - * Maximal size of a resizable axis. Could be set as a percent - * of plot area or pixel size. - * - * This feature requires the `drag-panes.js` module. - * - * @type {String|Number} - * @product highstock - * @sample {highstock} stock/yaxis/resize-min-max-length - * minLength and maxLength - * @apioption yAxis.maxLength - */ - maxLength: '100%', - - /** - * Options for axis resizing. This feature requires the - * `drag-panes.js` - - * [classic](http://code.highcharts.com/stock/modules/drag-panes.js) or - * [styled](http://code.highcharts.com/stock/js/modules/drag-panes.js) - * mode - module. - * - * @product highstock - * @sample {highstock} stock/demo/candlestick-and-volume - * Axis resizing enabled - * @optionparent yAxis.resize - */ - resize: { - - /** - * Contains two arrays of axes that are controlled by control line - * of the axis. - * - * This feature requires the `drag-panes.js` module. - */ - controlledAxis: { - - /** - * Array of axes that should move out of the way of resizing - * being done for the current axis. If not set, the next axis - * will be used. - * - * This feature requires the `drag-panes.js` module. - * - * @type {Array.} - * @default [] - * @sample {highstock} stock/yaxis/multiple-resizers - * Three panes with resizers - * @sample {highstock} stock/yaxis/resize-multiple-axes - * One resizer controlling multiple axes - */ - next: [], - - /** - * Array of axes that should move with the current axis - * while resizing. - * - * This feature requires the `drag-panes.js` module. - * - * @type {Array.} - * @sample {highstock} stock/yaxis/multiple-resizers - * Three panes with resizers - * @sample {highstock} stock/yaxis/resize-multiple-axes - * One resizer controlling multiple axes - */ - prev: [] - }, - - /** - * Enable or disable resize by drag for the axis. - * - * This feature requires the `drag-panes.js` module. - * - * @sample {highstock} stock/demo/candlestick-and-volume - * Enabled resizer - */ - enabled: false, - - /*= if (build.classic) { =*/ - - /** - * Cursor style for the control line. - * - * In styled mode use class `highcharts-axis-resizer` instead. - * - * This feature requires the `drag-panes.js` module. - */ - cursor: 'ns-resize', - - /** - * Color of the control line. - * - * In styled mode use class `highcharts-axis-resizer` instead. - * - * This feature requires the `drag-panes.js` module. - * - * @type {Color} - * @sample {highstock} stock/yaxis/styled-resizer Styled resizer - */ - lineColor: '${palette.neutralColor20}', - - /** - * Dash style of the control line. - * - * In styled mode use class `highcharts-axis-resizer` instead. - * - * This feature requires the `drag-panes.js` module. - * - * @sample {highstock} stock/yaxis/styled-resizer Styled resizer - * @see For supported options check - * [dashStyle](#plotOptions.series.dashStyle) - */ - lineDashStyle: 'Solid', - - /** - * Width of the control line. - * - * In styled mode use class `highcharts-axis-resizer` instead. - * - * This feature requires the `drag-panes.js` module. - * - * @sample {highstock} stock/yaxis/styled-resizer Styled resizer - */ - lineWidth: 4, - - /*= } =*/ - - /** - * Horizontal offset of the control line. - * - * This feature requires the `drag-panes.js` module. - * - * @sample {highstock} stock/yaxis/styled-resizer Styled resizer - */ - x: 0, - - /** - * Vertical offset of the control line. - * - * This feature requires the `drag-panes.js` module. - * - * @sample {highstock} stock/yaxis/styled-resizer Styled resizer - */ - y: 0 - } - }; + merge = H.merge, + wrap = H.wrap, + each = H.each, + isNumber = H.isNumber, + addEvent = H.addEvent, + relativeLength = H.relativeLength, + objectEach = H.objectEach, + Axis = H.Axis, + Pointer = H.Pointer, + + /** + * Default options for AxisResizer. + */ + resizerOptions = { + /** + * Minimal size of a resizable axis. Could be set as a percent + * of plot area or pixel size. + * + * This feature requires the `drag-panes.js` module. + * + * @type {Number|String} + * @product highstock + * @sample {highstock} stock/yaxis/resize-min-max-length + * minLength and maxLength + * @apioption yAxis.minLength + */ + minLength: '10%', + + /** + * Maximal size of a resizable axis. Could be set as a percent + * of plot area or pixel size. + * + * This feature requires the `drag-panes.js` module. + * + * @type {String|Number} + * @product highstock + * @sample {highstock} stock/yaxis/resize-min-max-length + * minLength and maxLength + * @apioption yAxis.maxLength + */ + maxLength: '100%', + + /** + * Options for axis resizing. This feature requires the + * `drag-panes.js` - + * [classic](http://code.highcharts.com/stock/modules/drag-panes.js) or + * [styled](http://code.highcharts.com/stock/js/modules/drag-panes.js) + * mode - module. + * + * @product highstock + * @sample {highstock} stock/demo/candlestick-and-volume + * Axis resizing enabled + * @optionparent yAxis.resize + */ + resize: { + + /** + * Contains two arrays of axes that are controlled by control line + * of the axis. + * + * This feature requires the `drag-panes.js` module. + */ + controlledAxis: { + + /** + * Array of axes that should move out of the way of resizing + * being done for the current axis. If not set, the next axis + * will be used. + * + * This feature requires the `drag-panes.js` module. + * + * @type {Array.} + * @default [] + * @sample {highstock} stock/yaxis/multiple-resizers + * Three panes with resizers + * @sample {highstock} stock/yaxis/resize-multiple-axes + * One resizer controlling multiple axes + */ + next: [], + + /** + * Array of axes that should move with the current axis + * while resizing. + * + * This feature requires the `drag-panes.js` module. + * + * @type {Array.} + * @sample {highstock} stock/yaxis/multiple-resizers + * Three panes with resizers + * @sample {highstock} stock/yaxis/resize-multiple-axes + * One resizer controlling multiple axes + */ + prev: [] + }, + + /** + * Enable or disable resize by drag for the axis. + * + * This feature requires the `drag-panes.js` module. + * + * @sample {highstock} stock/demo/candlestick-and-volume + * Enabled resizer + */ + enabled: false, + + /*= if (build.classic) { =*/ + + /** + * Cursor style for the control line. + * + * In styled mode use class `highcharts-axis-resizer` instead. + * + * This feature requires the `drag-panes.js` module. + */ + cursor: 'ns-resize', + + /** + * Color of the control line. + * + * In styled mode use class `highcharts-axis-resizer` instead. + * + * This feature requires the `drag-panes.js` module. + * + * @type {Color} + * @sample {highstock} stock/yaxis/styled-resizer Styled resizer + */ + lineColor: '${palette.neutralColor20}', + + /** + * Dash style of the control line. + * + * In styled mode use class `highcharts-axis-resizer` instead. + * + * This feature requires the `drag-panes.js` module. + * + * @sample {highstock} stock/yaxis/styled-resizer Styled resizer + * @see For supported options check + * [dashStyle](#plotOptions.series.dashStyle) + */ + lineDashStyle: 'Solid', + + /** + * Width of the control line. + * + * In styled mode use class `highcharts-axis-resizer` instead. + * + * This feature requires the `drag-panes.js` module. + * + * @sample {highstock} stock/yaxis/styled-resizer Styled resizer + */ + lineWidth: 4, + + /*= } =*/ + + /** + * Horizontal offset of the control line. + * + * This feature requires the `drag-panes.js` module. + * + * @sample {highstock} stock/yaxis/styled-resizer Styled resizer + */ + x: 0, + + /** + * Vertical offset of the control line. + * + * This feature requires the `drag-panes.js` module. + * + * @sample {highstock} stock/yaxis/styled-resizer Styled resizer + */ + y: 0 + } + }; merge(true, Axis.prototype.defaultYAxisOptions, resizerOptions); /** @@ -195,352 +195,352 @@ merge(true, Axis.prototype.defaultYAxisOptions, resizerOptions); * @class */ H.AxisResizer = function (axis) { - this.init(axis); + this.init(axis); }; H.AxisResizer.prototype = { - /** - * Initiate the AxisResizer object. - * @param {Object} axis - main axis for the AxisResizer. - */ - init: function (axis, update) { - this.axis = axis; - this.options = axis.options.resize; - this.render(); - - if (!update) { - // Add mouse events. - this.addMouseEvents(); - } - }, - - /** - * Render the AxisResizer - */ - render: function () { - var resizer = this, - axis = resizer.axis, - chart = axis.chart, - options = resizer.options, - x = options.x, - y = options.y, - // Normalize control line position according to the plot area - pos = Math.min( - Math.max( - axis.top + axis.height + y, - chart.plotTop - ), - chart.plotTop + chart.plotHeight - ), - attr = {}, - lineWidth; - - /*= if (build.classic) { =*/ - attr = { - cursor: options.cursor, - stroke: options.lineColor, - 'stroke-width': options.lineWidth, - dashstyle: options.lineDashStyle - }; - /*= } =*/ - - // Register current position for future reference. - resizer.lastPos = pos - y; - - if (!resizer.controlLine) { - resizer.controlLine = chart.renderer.path() - .addClass('highcharts-axis-resizer'); - } - - // Add to axisGroup after axis update, because the group is recreated - /*= if (!build.classic) { =*/ - // Do .add() before path is calculated because strokeWidth() needs it. - /*= } =*/ - resizer.controlLine.add(axis.axisGroup); - - /*= if (build.classic) { =*/ - lineWidth = options.lineWidth; - /*= } else { =*/ - lineWidth = resizer.controlLine.strokeWidth(); - /*= } =*/ - attr.d = chart.renderer.crispLine( - [ - 'M', axis.left + x, pos, - 'L', axis.left + axis.width + x, pos - ], - lineWidth - ); - - resizer.controlLine.attr(attr); - }, - - /** - * Set up the mouse and touch events for the control line. - */ - addMouseEvents: function () { - var resizer = this, - ctrlLineElem = resizer.controlLine.element, - container = resizer.axis.chart.container, - eventsToUnbind = [], - mouseMoveHandler, - mouseUpHandler, - mouseDownHandler; - - /** - * Create mouse events' handlers. - * Make them as separate functions to enable wrapping them: - */ - resizer.mouseMoveHandler = mouseMoveHandler = function (e) { - resizer.onMouseMove(e); - }; - resizer.mouseUpHandler = mouseUpHandler = function (e) { - resizer.onMouseUp(e); - }; - resizer.mouseDownHandler = mouseDownHandler = function (e) { - resizer.onMouseDown(e); - }; - - /** - * Add mouse move and mouseup events. These are bind to doc/container, - * because resizer.grabbed flag is stored in mousedown events. - */ - eventsToUnbind.push( - addEvent(container, 'mousemove', mouseMoveHandler), - addEvent(container.ownerDocument, 'mouseup', mouseUpHandler), - addEvent(ctrlLineElem, 'mousedown', mouseDownHandler) - ); - - // Touch events. - if (hasTouch) { - eventsToUnbind.push( - addEvent(container, 'touchmove', mouseMoveHandler), - addEvent(container.ownerDocument, 'touchend', mouseUpHandler), - addEvent(ctrlLineElem, 'touchstart', mouseDownHandler) - ); - } - - resizer.eventsToUnbind = eventsToUnbind; - }, - - /** - * Mouse move event based on x/y mouse position. - * @param {Object} e - mouse event. - */ - onMouseMove: function (e) { - /* - * 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. Borrowed from Navigator. - */ - if (!e.touches || e.touches[0].pageX !== 0) { - // Drag the control line - if (this.grabbed) { - this.hasDragged = true; - this.updateAxes(this.axis.chart.pointer.normalize(e).chartY - - this.options.y); - } - } - }, - - /** - * Mouse up event based on x/y mouse position. - * @param {Object} e - mouse event. - */ - onMouseUp: function (e) { - if (this.hasDragged) { - this.updateAxes(this.axis.chart.pointer.normalize(e).chartY - - this.options.y); - } - - // Restore runPointActions. - this.grabbed = this.hasDragged = this.axis.chart.activeResizer = null; - }, - - /** - * Mousedown on a control line. - * Will store necessary information for drag&drop. - */ - onMouseDown: function () { - // Clear all hover effects. - this.axis.chart.pointer.reset(false, 0); - - // Disable runPointActions. - this.grabbed = this.axis.chart.activeResizer = true; - }, - - /** - * Update all connected axes after a change of control line position - */ - updateAxes: function (chartY) { - var resizer = this, - chart = resizer.axis.chart, - axes = resizer.options.controlledAxis, - nextAxes = axes.next.length === 0 ? - [H.inArray(resizer.axis, chart.yAxis) + 1] : axes.next, - // Main axis is included in the prev array by default - prevAxes = [resizer.axis].concat(axes.prev), - axesConfigs = [], // prev and next configs - stopDrag = false, - plotTop = chart.plotTop, - plotHeight = chart.plotHeight, - plotBottom = plotTop + plotHeight, - yDelta, - normalize = function (val, min, max) { - return Math.round(Math.min(Math.max(val, min), max)); - }; - - // Normalize chartY to plot area limits - chartY = Math.max(Math.min(chartY, plotBottom), plotTop); - - yDelta = chartY - resizer.lastPos; - - // Update on changes of at least 1 pixel in the desired direction - if (yDelta * yDelta < 1) { - return; - } - - // First gather info how axes should behave - each([prevAxes, nextAxes], function (axesGroup, isNext) { - each(axesGroup, function (axisInfo, i) { - // Axes given as array index, axis object or axis id - var axis = isNumber(axisInfo) ? - // If it's a number - it's an index - chart.yAxis[axisInfo] : - ( - // If it's first elem. in first group - (!isNext && !i) ? - // then it's an Axis object - axisInfo : - // else it should be an id - chart.get(axisInfo) - ), - axisOptions = axis && axis.options, - optionsToUpdate = {}, - hDelta = 0, - height, top, - minLength, maxLength; - - // Skip if axis is not found - // or it is navigator's yAxis (#7732) - if ( - !axisOptions || - axisOptions.id === 'navigator-y-axis' - ) { - return; - } - - top = axis.top; - - minLength = Math.round( - relativeLength( - axisOptions.minLength, - plotHeight - ) - ); - maxLength = Math.round( - relativeLength( - axisOptions.maxLength, - plotHeight - ) - ); - - if (isNext) { - // Try to change height first. yDelta could had changed - yDelta = chartY - resizer.lastPos; - - // Normalize height to option limits - height = normalize(axis.len - yDelta, minLength, maxLength); - - // Adjust top, so the axis looks like shrinked from top - top = axis.top + yDelta; - - // Check for plot area limits - if (top + height > plotBottom) { - hDelta = plotBottom - height - top; - chartY += hDelta; - top += hDelta; - } - - // Fit to plot - when overflowing on top - if (top < plotTop) { - top = plotTop; - if (top + height > plotBottom) { - height = plotHeight; - } - } - - // If next axis meets min length, stop dragging: - if (height === minLength) { - stopDrag = true; - } - - axesConfigs.push({ - axis: axis, - options: { - top: Math.round(top), - height: height - } - }); - } else { - // Normalize height to option limits - height = normalize(chartY - top, minLength, maxLength); - - // If prev axis meets max length, stop dragging: - if (height === maxLength) { - stopDrag = true; - } - - // Check axis size limits - chartY = top + height; - axesConfigs.push({ - axis: axis, - options: { - height: height - } - }); - } - - optionsToUpdate.height = height; - }); - }); - - // If we hit the min/maxLength with dragging, don't do anything: - if (!stopDrag) { - // Now update axes: - each(axesConfigs, function (config) { - config.axis.update(config.options, false); - }); - - chart.redraw(false); - } - }, - - /** - * Destroy AxisResizer. Clear outside references, clear events, - * destroy elements, nullify properties. - */ - destroy: function () { - var resizer = this, - axis = resizer.axis; - - // Clear resizer in axis - delete axis.resizer; - - // Clear control line events - if (this.eventsToUnbind) { - each(this.eventsToUnbind, function (unbind) { - unbind(); - }); - } - - // Destroy AxisResizer elements - resizer.controlLine.destroy(); - - // Nullify properties - objectEach(resizer, function (val, key) { - resizer[key] = null; - }); - } + /** + * Initiate the AxisResizer object. + * @param {Object} axis - main axis for the AxisResizer. + */ + init: function (axis, update) { + this.axis = axis; + this.options = axis.options.resize; + this.render(); + + if (!update) { + // Add mouse events. + this.addMouseEvents(); + } + }, + + /** + * Render the AxisResizer + */ + render: function () { + var resizer = this, + axis = resizer.axis, + chart = axis.chart, + options = resizer.options, + x = options.x, + y = options.y, + // Normalize control line position according to the plot area + pos = Math.min( + Math.max( + axis.top + axis.height + y, + chart.plotTop + ), + chart.plotTop + chart.plotHeight + ), + attr = {}, + lineWidth; + + /*= if (build.classic) { =*/ + attr = { + cursor: options.cursor, + stroke: options.lineColor, + 'stroke-width': options.lineWidth, + dashstyle: options.lineDashStyle + }; + /*= } =*/ + + // Register current position for future reference. + resizer.lastPos = pos - y; + + if (!resizer.controlLine) { + resizer.controlLine = chart.renderer.path() + .addClass('highcharts-axis-resizer'); + } + + // Add to axisGroup after axis update, because the group is recreated + /*= if (!build.classic) { =*/ + // Do .add() before path is calculated because strokeWidth() needs it. + /*= } =*/ + resizer.controlLine.add(axis.axisGroup); + + /*= if (build.classic) { =*/ + lineWidth = options.lineWidth; + /*= } else { =*/ + lineWidth = resizer.controlLine.strokeWidth(); + /*= } =*/ + attr.d = chart.renderer.crispLine( + [ + 'M', axis.left + x, pos, + 'L', axis.left + axis.width + x, pos + ], + lineWidth + ); + + resizer.controlLine.attr(attr); + }, + + /** + * Set up the mouse and touch events for the control line. + */ + addMouseEvents: function () { + var resizer = this, + ctrlLineElem = resizer.controlLine.element, + container = resizer.axis.chart.container, + eventsToUnbind = [], + mouseMoveHandler, + mouseUpHandler, + mouseDownHandler; + + /** + * Create mouse events' handlers. + * Make them as separate functions to enable wrapping them: + */ + resizer.mouseMoveHandler = mouseMoveHandler = function (e) { + resizer.onMouseMove(e); + }; + resizer.mouseUpHandler = mouseUpHandler = function (e) { + resizer.onMouseUp(e); + }; + resizer.mouseDownHandler = mouseDownHandler = function (e) { + resizer.onMouseDown(e); + }; + + /** + * Add mouse move and mouseup events. These are bind to doc/container, + * because resizer.grabbed flag is stored in mousedown events. + */ + eventsToUnbind.push( + addEvent(container, 'mousemove', mouseMoveHandler), + addEvent(container.ownerDocument, 'mouseup', mouseUpHandler), + addEvent(ctrlLineElem, 'mousedown', mouseDownHandler) + ); + + // Touch events. + if (hasTouch) { + eventsToUnbind.push( + addEvent(container, 'touchmove', mouseMoveHandler), + addEvent(container.ownerDocument, 'touchend', mouseUpHandler), + addEvent(ctrlLineElem, 'touchstart', mouseDownHandler) + ); + } + + resizer.eventsToUnbind = eventsToUnbind; + }, + + /** + * Mouse move event based on x/y mouse position. + * @param {Object} e - mouse event. + */ + onMouseMove: function (e) { + /* + * 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. Borrowed from Navigator. + */ + if (!e.touches || e.touches[0].pageX !== 0) { + // Drag the control line + if (this.grabbed) { + this.hasDragged = true; + this.updateAxes(this.axis.chart.pointer.normalize(e).chartY - + this.options.y); + } + } + }, + + /** + * Mouse up event based on x/y mouse position. + * @param {Object} e - mouse event. + */ + onMouseUp: function (e) { + if (this.hasDragged) { + this.updateAxes(this.axis.chart.pointer.normalize(e).chartY - + this.options.y); + } + + // Restore runPointActions. + this.grabbed = this.hasDragged = this.axis.chart.activeResizer = null; + }, + + /** + * Mousedown on a control line. + * Will store necessary information for drag&drop. + */ + onMouseDown: function () { + // Clear all hover effects. + this.axis.chart.pointer.reset(false, 0); + + // Disable runPointActions. + this.grabbed = this.axis.chart.activeResizer = true; + }, + + /** + * Update all connected axes after a change of control line position + */ + updateAxes: function (chartY) { + var resizer = this, + chart = resizer.axis.chart, + axes = resizer.options.controlledAxis, + nextAxes = axes.next.length === 0 ? + [H.inArray(resizer.axis, chart.yAxis) + 1] : axes.next, + // Main axis is included in the prev array by default + prevAxes = [resizer.axis].concat(axes.prev), + axesConfigs = [], // prev and next configs + stopDrag = false, + plotTop = chart.plotTop, + plotHeight = chart.plotHeight, + plotBottom = plotTop + plotHeight, + yDelta, + normalize = function (val, min, max) { + return Math.round(Math.min(Math.max(val, min), max)); + }; + + // Normalize chartY to plot area limits + chartY = Math.max(Math.min(chartY, plotBottom), plotTop); + + yDelta = chartY - resizer.lastPos; + + // Update on changes of at least 1 pixel in the desired direction + if (yDelta * yDelta < 1) { + return; + } + + // First gather info how axes should behave + each([prevAxes, nextAxes], function (axesGroup, isNext) { + each(axesGroup, function (axisInfo, i) { + // Axes given as array index, axis object or axis id + var axis = isNumber(axisInfo) ? + // If it's a number - it's an index + chart.yAxis[axisInfo] : + ( + // If it's first elem. in first group + (!isNext && !i) ? + // then it's an Axis object + axisInfo : + // else it should be an id + chart.get(axisInfo) + ), + axisOptions = axis && axis.options, + optionsToUpdate = {}, + hDelta = 0, + height, top, + minLength, maxLength; + + // Skip if axis is not found + // or it is navigator's yAxis (#7732) + if ( + !axisOptions || + axisOptions.id === 'navigator-y-axis' + ) { + return; + } + + top = axis.top; + + minLength = Math.round( + relativeLength( + axisOptions.minLength, + plotHeight + ) + ); + maxLength = Math.round( + relativeLength( + axisOptions.maxLength, + plotHeight + ) + ); + + if (isNext) { + // Try to change height first. yDelta could had changed + yDelta = chartY - resizer.lastPos; + + // Normalize height to option limits + height = normalize(axis.len - yDelta, minLength, maxLength); + + // Adjust top, so the axis looks like shrinked from top + top = axis.top + yDelta; + + // Check for plot area limits + if (top + height > plotBottom) { + hDelta = plotBottom - height - top; + chartY += hDelta; + top += hDelta; + } + + // Fit to plot - when overflowing on top + if (top < plotTop) { + top = plotTop; + if (top + height > plotBottom) { + height = plotHeight; + } + } + + // If next axis meets min length, stop dragging: + if (height === minLength) { + stopDrag = true; + } + + axesConfigs.push({ + axis: axis, + options: { + top: Math.round(top), + height: height + } + }); + } else { + // Normalize height to option limits + height = normalize(chartY - top, minLength, maxLength); + + // If prev axis meets max length, stop dragging: + if (height === maxLength) { + stopDrag = true; + } + + // Check axis size limits + chartY = top + height; + axesConfigs.push({ + axis: axis, + options: { + height: height + } + }); + } + + optionsToUpdate.height = height; + }); + }); + + // If we hit the min/maxLength with dragging, don't do anything: + if (!stopDrag) { + // Now update axes: + each(axesConfigs, function (config) { + config.axis.update(config.options, false); + }); + + chart.redraw(false); + } + }, + + /** + * Destroy AxisResizer. Clear outside references, clear events, + * destroy elements, nullify properties. + */ + destroy: function () { + var resizer = this, + axis = resizer.axis; + + // Clear resizer in axis + delete axis.resizer; + + // Clear control line events + if (this.eventsToUnbind) { + each(this.eventsToUnbind, function (unbind) { + unbind(); + }); + } + + // Destroy AxisResizer elements + resizer.controlLine.destroy(); + + // Nullify properties + objectEach(resizer, function (val, key) { + resizer[key] = null; + }); + } }; // Keep resizer reference on axis update @@ -548,54 +548,54 @@ Axis.prototype.keepProps.push('resizer'); // Add new AxisResizer, update or remove it addEvent(Axis, 'afterRender', function () { - var axis = this, - resizer = axis.resizer, - resizerOptions = axis.options.resize, - enabled; - - if (resizerOptions) { - enabled = resizerOptions.enabled !== false; - - if (resizer) { - // Resizer present and enabled - if (enabled) { - // Update options - resizer.init(axis, true); - - // Resizer present, but disabled - } else { - // Destroy the resizer - resizer.destroy(); - } - } else { - // Resizer not present and enabled - if (enabled) { - // Add new resizer - axis.resizer = new H.AxisResizer(axis); - } - // Resizer not present and disabled, so do nothing - } - } + var axis = this, + resizer = axis.resizer, + resizerOptions = axis.options.resize, + enabled; + + if (resizerOptions) { + enabled = resizerOptions.enabled !== false; + + if (resizer) { + // Resizer present and enabled + if (enabled) { + // Update options + resizer.init(axis, true); + + // Resizer present, but disabled + } else { + // Destroy the resizer + resizer.destroy(); + } + } else { + // Resizer not present and enabled + if (enabled) { + // Add new resizer + axis.resizer = new H.AxisResizer(axis); + } + // Resizer not present and disabled, so do nothing + } + } }); // Clear resizer on axis remove. addEvent(Axis, 'destroy', function (e) { - if (!e.keepEvents && this.resizer) { - this.resizer.destroy(); - } + if (!e.keepEvents && this.resizer) { + this.resizer.destroy(); + } }); // Prevent any hover effects while dragging a control line of AxisResizer. wrap(Pointer.prototype, 'runPointActions', function (proceed) { - if (!this.chart.activeResizer) { - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + if (!this.chart.activeResizer) { + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } }); // Prevent default drag action detection while dragging a control line // of AxisResizer. (#7563) wrap(Pointer.prototype, 'drag', function (proceed) { - if (!this.chart.activeResizer) { - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + if (!this.chart.activeResizer) { + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } }); diff --git a/js/modules/drilldown.src.js b/js/modules/drilldown.src.js index 2e41f6f06b2..859b1a84a50 100644 --- a/js/modules/drilldown.src.js +++ b/js/modules/drilldown.src.js @@ -1,6 +1,6 @@ /** * Highcharts Drilldown module - * + * * Author: Torstein Honsi * License: www.highcharts.com/license * @@ -16,45 +16,45 @@ import '../parts/ColumnSeries.js'; import '../parts/Tick.js'; var animObject = H.animObject, - noop = H.noop, - color = H.color, - defaultOptions = H.defaultOptions, - each = H.each, - extend = H.extend, - format = H.format, - objectEach = H.objectEach, - pick = H.pick, - Chart = H.Chart, - seriesTypes = H.seriesTypes, - PieSeries = seriesTypes.pie, - ColumnSeries = seriesTypes.column, - Tick = H.Tick, - fireEvent = H.fireEvent, - inArray = H.inArray, - ddSeriesId = 1; + noop = H.noop, + color = H.color, + defaultOptions = H.defaultOptions, + each = H.each, + extend = H.extend, + format = H.format, + objectEach = H.objectEach, + pick = H.pick, + Chart = H.Chart, + seriesTypes = H.seriesTypes, + PieSeries = seriesTypes.pie, + ColumnSeries = seriesTypes.column, + Tick = H.Tick, + fireEvent = H.fireEvent, + inArray = H.inArray, + ddSeriesId = 1; // Add language extend(defaultOptions.lang, { - /** - * The text for the button that appears when drilling down, linking - * back to the parent series. The parent series' name is inserted for - * `{series.name}`. - * - * @type {String} - * @default Back to {series.name} - * @since 3.0.8 - * @product highcharts highmaps - * @apioption lang.drillUpText - */ - drillUpText: '◁ Back to {series.name}' + /** + * The text for the button that appears when drilling down, linking + * back to the parent series. The parent series' name is inserted for + * `{series.name}`. + * + * @type {String} + * @default Back to {series.name} + * @since 3.0.8 + * @product highcharts highmaps + * @apioption lang.drillUpText + */ + drillUpText: '◁ Back to {series.name}' }); /** - * Options for drill down, the concept of inspecting increasingly high + * Options for drill down, the concept of inspecting increasingly high * resolution data through clicking on chart items like columns or pie slices. * - * The drilldown feature requires the drilldown.js file to be loaded, - * found in the modules directory of the download package, or online at + * The drilldown feature requires the drilldown.js file to be loaded, + * found in the modules directory of the download package, or online at * (code.highcharts.com/modules/drilldown.js)[code.highcharts.com/modules/ * drilldown.js]. * @@ -63,197 +63,197 @@ extend(defaultOptions.lang, { */ defaultOptions.drilldown = { - /** - * When this option is false, clicking a single point will drill down - * all points in the same category, equivalent to clicking the X axis - * label. - * - * @type {Boolean} - * @sample {highcharts} highcharts/drilldown/allowpointdrilldown-false/ - * Don't allow point drilldown - * @default true - * @since 4.1.7 - * @product highcharts - * @apioption drilldown.allowPointDrilldown - */ - - /** - * An array of series configurations for the drill down. Each series - * configuration uses the same syntax as the [series](#series) option - * set. These drilldown series are hidden by default. The drilldown - * series is linked to the parent series' point by its `id`. - * - * @type {Array} - * @since 3.0.8 - * @product highcharts highmaps - * @apioption drilldown.series - */ - - /*= if (build.classic) { =*/ - - /** - * Additional styles to apply to the X axis label for a point that - * has drilldown data. By default it is underlined and blue to invite - * to interaction. - * - * @type {CSSObject} - * @see In styled mode, active label styles can be set with the - * `.highcharts-drilldown-axis-label` class. - * @sample {highcharts} highcharts/drilldown/labels/ Label styles - * @default { "cursor": "pointer", "color": "#003399", "fontWeight": "bold", "textDecoration": "underline" } - * @since 3.0.8 - * @product highcharts highmaps - */ - activeAxisLabelStyle: { - cursor: 'pointer', - color: '${palette.highlightColor100}', - fontWeight: 'bold', - textDecoration: 'underline' - }, - - /** - * Additional styles to apply to the data label of a point that has - * drilldown data. By default it is underlined and blue to invite to - * interaction. - * - * @type {CSSObject} - * @see In styled mode, active data label styles can be applied with - * the `.highcharts-drilldown-data-label` class. - * @sample {highcharts} highcharts/drilldown/labels/ Label styles - * @default { "cursor": "pointer", "color": "#003399", "fontWeight": "bold", "textDecoration": "underline" } - * @since 3.0.8 - * @product highcharts highmaps - */ - activeDataLabelStyle: { - cursor: 'pointer', - color: '${palette.highlightColor100}', - fontWeight: 'bold', - textDecoration: 'underline' - }, - /*= } =*/ - - /** - * Set the animation for all drilldown animations. Animation of a drilldown - * occurs when drilling between a column point and a column series, - * or a pie slice and a full pie series. Drilldown can still be used - * between series and points of different types, but animation will - * not occur. - * - * The animation can either be set as a boolean or a configuration - * object. If `true`, it will use the 'swing' jQuery easing and a duration - * of 500 ms. If used as a configuration object, the following properties - * are supported: - * - *
- * - *
duration
- * - *
The duration of the animation in milliseconds.
- * - *
easing
- * - *
A string reference to an easing function set on the `Math` object. - * See [the easing demo](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series- - * animation-easing/).
- * - *
- * - * @type {Boolean|Object} - * @since 3.0.8 - * @product highcharts highmaps - */ - animation: { - - /** - * Duration for the drilldown animation. - * @default 500 - */ - duration: 500 - }, - - /** - * Options for the drill up button that appears when drilling down - * on a series. The text for the button is defined in - * [lang.drillUpText](#lang.drillUpText). - * - * @type {Object} - * @sample {highcharts} highcharts/drilldown/drillupbutton/ Drill up button - * @sample {highmaps} highcharts/drilldown/drillupbutton/ Drill up button - * @since 3.0.8 - * @product highcharts highmaps - */ - drillUpButton: { - /** - * What box to align the button to. Can be either `plotBox` or - * `spacingBox`. - * - * @type {String} - * @default plotBox - * @validvalue ["plotBox", "spacingBox"] - * @since 3.0.8 - * @product highcharts highmaps - * @apioption drilldown.drillUpButton.relativeTo - */ - - /** - * A collection of attributes for the button. The object takes SVG - * attributes like `fill`, `stroke`, `stroke-width` or `r`, the border - * radius. The theme also supports `style`, a collection of CSS - * properties for the text. Equivalent attributes for the hover state - * are given in `theme.states.hover`. - * - * @type {Object} - * @see In styled mode, drill-up button styles can be applied with - * the `.highcharts-drillup-button` class. - * @sample {highcharts} highcharts/drilldown/drillupbutton/ - * Button theming - * @sample {highmaps} highcharts/drilldown/drillupbutton/ - * Button theming - * @since 3.0.8 - * @product highcharts highmaps - * @apioption drilldown.drillUpButton.theme - */ - - /** - * Positioning options for the button within the `relativeTo` box. - * Available properties are `x`, `y`, `align` and `verticalAlign`. - * - * @type {Object} - * @since 3.0.8 - * @product highcharts highmaps - */ - position: { - - /** - * Vertical alignment of the button. - * - * @type {String} - * @default top - * @validvalue ["top", "middle", "bottom"] - * @product highcharts highmaps - * @apioption drilldown.drillUpButton.position.verticalAlign - */ - - /** - * Horizontal alignment. - * @type {String} - */ - align: 'right', - - /** - * The X offset of the button. - * @type {Number} - */ - x: -10, - - /** - * The Y offset of the button. - * @type {Number} - */ - y: 10 - } - } -}; + /** + * When this option is false, clicking a single point will drill down + * all points in the same category, equivalent to clicking the X axis + * label. + * + * @type {Boolean} + * @sample {highcharts} highcharts/drilldown/allowpointdrilldown-false/ + * Don't allow point drilldown + * @default true + * @since 4.1.7 + * @product highcharts + * @apioption drilldown.allowPointDrilldown + */ + + /** + * An array of series configurations for the drill down. Each series + * configuration uses the same syntax as the [series](#series) option + * set. These drilldown series are hidden by default. The drilldown + * series is linked to the parent series' point by its `id`. + * + * @type {Array} + * @since 3.0.8 + * @product highcharts highmaps + * @apioption drilldown.series + */ + + /*= if (build.classic) { =*/ + + /** + * Additional styles to apply to the X axis label for a point that + * has drilldown data. By default it is underlined and blue to invite + * to interaction. + * + * @type {CSSObject} + * @see In styled mode, active label styles can be set with the + * `.highcharts-drilldown-axis-label` class. + * @sample {highcharts} highcharts/drilldown/labels/ Label styles + * @default { "cursor": "pointer", "color": "#003399", "fontWeight": "bold", "textDecoration": "underline" } + * @since 3.0.8 + * @product highcharts highmaps + */ + activeAxisLabelStyle: { + cursor: 'pointer', + color: '${palette.highlightColor100}', + fontWeight: 'bold', + textDecoration: 'underline' + }, + + /** + * Additional styles to apply to the data label of a point that has + * drilldown data. By default it is underlined and blue to invite to + * interaction. + * + * @type {CSSObject} + * @see In styled mode, active data label styles can be applied with + * the `.highcharts-drilldown-data-label` class. + * @sample {highcharts} highcharts/drilldown/labels/ Label styles + * @default { "cursor": "pointer", "color": "#003399", "fontWeight": "bold", "textDecoration": "underline" } + * @since 3.0.8 + * @product highcharts highmaps + */ + activeDataLabelStyle: { + cursor: 'pointer', + color: '${palette.highlightColor100}', + fontWeight: 'bold', + textDecoration: 'underline' + }, + /*= } =*/ + + /** + * Set the animation for all drilldown animations. Animation of a drilldown + * occurs when drilling between a column point and a column series, + * or a pie slice and a full pie series. Drilldown can still be used + * between series and points of different types, but animation will + * not occur. + * + * The animation can either be set as a boolean or a configuration + * object. If `true`, it will use the 'swing' jQuery easing and a duration + * of 500 ms. If used as a configuration object, the following properties + * are supported: + * + *
+ * + *
duration
+ * + *
The duration of the animation in milliseconds.
+ * + *
easing
+ * + *
A string reference to an easing function set on the `Math` object. + * See [the easing demo](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series- + * animation-easing/).
+ * + *
+ * + * @type {Boolean|Object} + * @since 3.0.8 + * @product highcharts highmaps + */ + animation: { + + /** + * Duration for the drilldown animation. + * @default 500 + */ + duration: 500 + }, + + /** + * Options for the drill up button that appears when drilling down + * on a series. The text for the button is defined in + * [lang.drillUpText](#lang.drillUpText). + * + * @type {Object} + * @sample {highcharts} highcharts/drilldown/drillupbutton/ Drill up button + * @sample {highmaps} highcharts/drilldown/drillupbutton/ Drill up button + * @since 3.0.8 + * @product highcharts highmaps + */ + drillUpButton: { + /** + * What box to align the button to. Can be either `plotBox` or + * `spacingBox`. + * + * @type {String} + * @default plotBox + * @validvalue ["plotBox", "spacingBox"] + * @since 3.0.8 + * @product highcharts highmaps + * @apioption drilldown.drillUpButton.relativeTo + */ + + /** + * A collection of attributes for the button. The object takes SVG + * attributes like `fill`, `stroke`, `stroke-width` or `r`, the border + * radius. The theme also supports `style`, a collection of CSS + * properties for the text. Equivalent attributes for the hover state + * are given in `theme.states.hover`. + * + * @type {Object} + * @see In styled mode, drill-up button styles can be applied with + * the `.highcharts-drillup-button` class. + * @sample {highcharts} highcharts/drilldown/drillupbutton/ + * Button theming + * @sample {highmaps} highcharts/drilldown/drillupbutton/ + * Button theming + * @since 3.0.8 + * @product highcharts highmaps + * @apioption drilldown.drillUpButton.theme + */ + + /** + * Positioning options for the button within the `relativeTo` box. + * Available properties are `x`, `y`, `align` and `verticalAlign`. + * + * @type {Object} + * @since 3.0.8 + * @product highcharts highmaps + */ + position: { + + /** + * Vertical alignment of the button. + * + * @type {String} + * @default top + * @validvalue ["top", "middle", "bottom"] + * @product highcharts highmaps + * @apioption drilldown.drillUpButton.position.verticalAlign + */ + + /** + * Horizontal alignment. + * @type {String} + */ + align: 'right', + + /** + * The X offset of the button. + * @type {Number} + */ + x: -10, + + /** + * The Y offset of the button. + * @type {Number} + */ + y: 10 + } + } +}; @@ -263,35 +263,35 @@ defaultOptions.drilldown = { * seriesOptions are not added by option, but rather loaded async. Note * that when clicking a category label to trigger multiple series drilldown, * one `drilldown` event is triggered per point in the category. - * + * * Event arguments: - * + * *
- * + * *
`category`
- * + * *
If a category label was clicked, which index.
- * + * *
`point`
- * + * *
The originating point.
- * + * *
`originalEvent`
- * + * *
The original browser event (usually click) that triggered the * drilldown.
- * + * *
`points`
- * + * *
If a category label was clicked, this array holds all points * corresponing to the category.
- * + * *
`seriesOptions`
- * + * *
Options for the new series
- * + * *
- * + * * @type {Function} * @context Chart * @sample {highcharts} highcharts/drilldown/async/ Async drilldown @@ -302,7 +302,7 @@ defaultOptions.drilldown = { /** * Fires when drilling up from a drilldown series. - * + * * @type {Function} * @context Chart * @since 3.0.8 @@ -313,7 +313,7 @@ defaultOptions.drilldown = { /** * In a chart with multiple drilldown series, this event fires after * all the series have been drilled up. - * + * * @type {Function} * @context Chart * @since 4.2.4 @@ -324,7 +324,7 @@ defaultOptions.drilldown = { /** * The `id` of a series in the [drilldown.series](#drilldown.series) * array to use for a drilldown for this point. - * + * * @type {String} * @sample {highcharts} highcharts/drilldown/basic/ Basic drilldown * @since 3.0.8 @@ -336,29 +336,29 @@ defaultOptions.drilldown = { * A general fadeIn method */ H.SVGRenderer.prototype.Element.prototype.fadeIn = function (animation) { - this - .attr({ - opacity: 0.1, - visibility: 'inherit' - }) - .animate({ - opacity: pick(this.newOpacity, 1) // newOpacity used in maps - }, animation || { - duration: 250 - }); + this + .attr({ + opacity: 0.1, + visibility: 'inherit' + }) + .animate({ + opacity: pick(this.newOpacity, 1) // newOpacity used in maps + }, animation || { + duration: 250 + }); }; /** * Add a series to the chart as drilldown from a specific point in the parent * series. This method is used for async drilldown, when clicking a point in a * series should result in loading and displaying a more high-resolution series. - * When not async, the setup is simpler using the {@link + * When not async, the setup is simpler using the {@link * https://api.highcharts.com/highcharts/drilldown.series|drilldown.series} * options structure. * * @memberOf Highcharts.Chart * @function #addSeriesAsDrilldown - * + * * @param {Highcharts.Point} point * The point from which the drilldown will start. * @param {SeriesOptions} options @@ -367,195 +367,195 @@ H.SVGRenderer.prototype.Element.prototype.fadeIn = function (animation) { * @sample highcharts/drilldown/async/ Async drilldown */ Chart.prototype.addSeriesAsDrilldown = function (point, options) { - this.addSingleSeriesAsDrilldown(point, options); - this.applyDrilldown(); + this.addSingleSeriesAsDrilldown(point, options); + this.applyDrilldown(); }; Chart.prototype.addSingleSeriesAsDrilldown = function (point, ddOptions) { - var oldSeries = point.series, - xAxis = oldSeries.xAxis, - yAxis = oldSeries.yAxis, - newSeries, - pointIndex, - levelSeries = [], - levelSeriesOptions = [], - level, - levelNumber, - last, - colorProp; - - - /*= if (build.classic) { =*/ - colorProp = { color: point.color || oldSeries.color }; - /*= } else { =*/ - colorProp = { colorIndex: pick(point.colorIndex, oldSeries.colorIndex) }; - /*= } =*/ - - if (!this.drilldownLevels) { - this.drilldownLevels = []; - } - - levelNumber = oldSeries.options._levelNumber || 0; - - // See if we can reuse the registered series from last run - last = this.drilldownLevels[this.drilldownLevels.length - 1]; - if (last && last.levelNumber !== levelNumber) { - last = undefined; - } - - ddOptions = extend(extend({ - _ddSeriesId: ddSeriesId++ - }, colorProp), ddOptions); - pointIndex = inArray(point, oldSeries.points); - - // Record options for all current series - each(oldSeries.chart.series, function (series) { - if (series.xAxis === xAxis && !series.isDrilling) { - series.options._ddSeriesId = - series.options._ddSeriesId || ddSeriesId++; - series.options._colorIndex = series.userOptions._colorIndex; - series.options._levelNumber = - series.options._levelNumber || levelNumber; // #3182 - - if (last) { - levelSeries = last.levelSeries; - levelSeriesOptions = last.levelSeriesOptions; - } else { - levelSeries.push(series); - levelSeriesOptions.push(series.options); - } - } - }); - - // Add a record of properties for each drilldown level - level = extend({ - levelNumber: levelNumber, - seriesOptions: oldSeries.options, - levelSeriesOptions: levelSeriesOptions, - levelSeries: levelSeries, - shapeArgs: point.shapeArgs, - // no graphic in line series with markers disabled - bBox: point.graphic ? point.graphic.getBBox() : {}, - color: point.isNull ? new H.Color(color).setOpacity(0).get() : color, - lowerSeriesOptions: ddOptions, - pointOptions: oldSeries.options.data[pointIndex], - pointIndex: pointIndex, - oldExtremes: { - xMin: xAxis && xAxis.userMin, - xMax: xAxis && xAxis.userMax, - yMin: yAxis && yAxis.userMin, - yMax: yAxis && yAxis.userMax - }, - resetZoomButton: this.resetZoomButton - }, colorProp); - - // Push it to the lookup array - this.drilldownLevels.push(level); - - // Reset names to prevent extending (#6704) - if (xAxis && xAxis.names) { - xAxis.names.length = 0; - } - - newSeries = level.lowerSeries = this.addSeries(ddOptions, false); - newSeries.options._levelNumber = levelNumber + 1; - if (xAxis) { - xAxis.oldPos = xAxis.pos; - xAxis.userMin = xAxis.userMax = null; - yAxis.userMin = yAxis.userMax = null; - } - - // Run fancy cross-animation on supported and equal types - if (oldSeries.type === newSeries.type) { - newSeries.animate = newSeries.animateDrilldown || noop; - newSeries.options.animation = true; - } + var oldSeries = point.series, + xAxis = oldSeries.xAxis, + yAxis = oldSeries.yAxis, + newSeries, + pointIndex, + levelSeries = [], + levelSeriesOptions = [], + level, + levelNumber, + last, + colorProp; + + + /*= if (build.classic) { =*/ + colorProp = { color: point.color || oldSeries.color }; + /*= } else { =*/ + colorProp = { colorIndex: pick(point.colorIndex, oldSeries.colorIndex) }; + /*= } =*/ + + if (!this.drilldownLevels) { + this.drilldownLevels = []; + } + + levelNumber = oldSeries.options._levelNumber || 0; + + // See if we can reuse the registered series from last run + last = this.drilldownLevels[this.drilldownLevels.length - 1]; + if (last && last.levelNumber !== levelNumber) { + last = undefined; + } + + ddOptions = extend(extend({ + _ddSeriesId: ddSeriesId++ + }, colorProp), ddOptions); + pointIndex = inArray(point, oldSeries.points); + + // Record options for all current series + each(oldSeries.chart.series, function (series) { + if (series.xAxis === xAxis && !series.isDrilling) { + series.options._ddSeriesId = + series.options._ddSeriesId || ddSeriesId++; + series.options._colorIndex = series.userOptions._colorIndex; + series.options._levelNumber = + series.options._levelNumber || levelNumber; // #3182 + + if (last) { + levelSeries = last.levelSeries; + levelSeriesOptions = last.levelSeriesOptions; + } else { + levelSeries.push(series); + levelSeriesOptions.push(series.options); + } + } + }); + + // Add a record of properties for each drilldown level + level = extend({ + levelNumber: levelNumber, + seriesOptions: oldSeries.options, + levelSeriesOptions: levelSeriesOptions, + levelSeries: levelSeries, + shapeArgs: point.shapeArgs, + // no graphic in line series with markers disabled + bBox: point.graphic ? point.graphic.getBBox() : {}, + color: point.isNull ? new H.Color(color).setOpacity(0).get() : color, + lowerSeriesOptions: ddOptions, + pointOptions: oldSeries.options.data[pointIndex], + pointIndex: pointIndex, + oldExtremes: { + xMin: xAxis && xAxis.userMin, + xMax: xAxis && xAxis.userMax, + yMin: yAxis && yAxis.userMin, + yMax: yAxis && yAxis.userMax + }, + resetZoomButton: this.resetZoomButton + }, colorProp); + + // Push it to the lookup array + this.drilldownLevels.push(level); + + // Reset names to prevent extending (#6704) + if (xAxis && xAxis.names) { + xAxis.names.length = 0; + } + + newSeries = level.lowerSeries = this.addSeries(ddOptions, false); + newSeries.options._levelNumber = levelNumber + 1; + if (xAxis) { + xAxis.oldPos = xAxis.pos; + xAxis.userMin = xAxis.userMax = null; + yAxis.userMin = yAxis.userMax = null; + } + + // Run fancy cross-animation on supported and equal types + if (oldSeries.type === newSeries.type) { + newSeries.animate = newSeries.animateDrilldown || noop; + newSeries.options.animation = true; + } }; Chart.prototype.applyDrilldown = function () { - var drilldownLevels = this.drilldownLevels, - levelToRemove; - - if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading - levelToRemove = drilldownLevels[drilldownLevels.length - 1].levelNumber; - each(this.drilldownLevels, function (level) { - if (level.levelNumber === levelToRemove) { - each(level.levelSeries, function (series) { - // Not removed, not added as part of a multi-series - // drilldown - if ( - series.options && - series.options._levelNumber === levelToRemove - ) { - series.remove(false); - } - }); - } - }); - } - - // We have a reset zoom button. Hide it and detatch it from the chart. It - // is preserved to the layer config above. - if (this.resetZoomButton) { - this.resetZoomButton.hide(); - delete this.resetZoomButton; - } - - this.pointer.reset(); - this.redraw(); - this.showDrillUpButton(); + var drilldownLevels = this.drilldownLevels, + levelToRemove; + + if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading + levelToRemove = drilldownLevels[drilldownLevels.length - 1].levelNumber; + each(this.drilldownLevels, function (level) { + if (level.levelNumber === levelToRemove) { + each(level.levelSeries, function (series) { + // Not removed, not added as part of a multi-series + // drilldown + if ( + series.options && + series.options._levelNumber === levelToRemove + ) { + series.remove(false); + } + }); + } + }); + } + + // We have a reset zoom button. Hide it and detatch it from the chart. It + // is preserved to the layer config above. + if (this.resetZoomButton) { + this.resetZoomButton.hide(); + delete this.resetZoomButton; + } + + this.pointer.reset(); + this.redraw(); + this.showDrillUpButton(); }; Chart.prototype.getDrilldownBackText = function () { - var drilldownLevels = this.drilldownLevels, - lastLevel; - if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading - lastLevel = drilldownLevels[drilldownLevels.length - 1]; - lastLevel.series = lastLevel.seriesOptions; - return format(this.options.lang.drillUpText, lastLevel); - } + var drilldownLevels = this.drilldownLevels, + lastLevel; + if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading + lastLevel = drilldownLevels[drilldownLevels.length - 1]; + lastLevel.series = lastLevel.seriesOptions; + return format(this.options.lang.drillUpText, lastLevel); + } }; Chart.prototype.showDrillUpButton = function () { - var chart = this, - backText = this.getDrilldownBackText(), - buttonOptions = chart.options.drilldown.drillUpButton, - attr, - states; - - - if (!this.drillUpButton) { - attr = buttonOptions.theme; - states = attr && attr.states; - - this.drillUpButton = this.renderer.button( - backText, - null, - null, - function () { - chart.drillUp(); - }, - attr, - states && states.hover, - states && states.select - ) - .addClass('highcharts-drillup-button') - .attr({ - align: buttonOptions.position.align, - zIndex: 7 - }) - .add() - .align( - buttonOptions.position, - false, - buttonOptions.relativeTo || 'plotBox' - ); - } else { - this.drillUpButton.attr({ - text: backText - }) - .align(); - } + var chart = this, + backText = this.getDrilldownBackText(), + buttonOptions = chart.options.drilldown.drillUpButton, + attr, + states; + + + if (!this.drillUpButton) { + attr = buttonOptions.theme; + states = attr && attr.states; + + this.drillUpButton = this.renderer.button( + backText, + null, + null, + function () { + chart.drillUp(); + }, + attr, + states && states.hover, + states && states.select + ) + .addClass('highcharts-drillup-button') + .attr({ + align: buttonOptions.position.align, + zIndex: 7 + }) + .add() + .align( + buttonOptions.position, + false, + buttonOptions.relativeTo || 'plotBox' + ); + } else { + this.drillUpButton.attr({ + text: backText + }) + .align(); + } }; /** @@ -566,174 +566,174 @@ Chart.prototype.showDrillUpButton = function () { * @memberOf Highcharts.Chart */ Chart.prototype.drillUp = function () { - if (!this.drilldownLevels || this.drilldownLevels.length === 0) { - return; - } - - var chart = this, - drilldownLevels = chart.drilldownLevels, - levelNumber = drilldownLevels[drilldownLevels.length - 1].levelNumber, - i = drilldownLevels.length, - chartSeries = chart.series, - seriesI, - level, - oldSeries, - newSeries, - oldExtremes, - addSeries = function (seriesOptions) { - var addedSeries; - each(chartSeries, function (series) { - if (series.options._ddSeriesId === seriesOptions._ddSeriesId) { - addedSeries = series; - } - }); - - addedSeries = addedSeries || chart.addSeries(seriesOptions, false); - if ( - addedSeries.type === oldSeries.type && - addedSeries.animateDrillupTo - ) { - addedSeries.animate = addedSeries.animateDrillupTo; - } - if (seriesOptions === level.seriesOptions) { - newSeries = addedSeries; - } - }; - - while (i--) { - - level = drilldownLevels[i]; - if (level.levelNumber === levelNumber) { - drilldownLevels.pop(); - - // Get the lower series by reference or id - oldSeries = level.lowerSeries; - if (!oldSeries.chart) { // #2786 - seriesI = chartSeries.length; // #2919 - while (seriesI--) { - if ( - chartSeries[seriesI].options.id === - level.lowerSeriesOptions.id && - chartSeries[seriesI].options._levelNumber === - levelNumber + 1 - ) { // #3867 - oldSeries = chartSeries[seriesI]; - break; - } - } - } - oldSeries.xData = []; // Overcome problems with minRange (#2898) - - each(level.levelSeriesOptions, addSeries); - - fireEvent(chart, 'drillup', { seriesOptions: level.seriesOptions }); - - if (newSeries.type === oldSeries.type) { - newSeries.drilldownLevel = level; - newSeries.options.animation = chart.options.drilldown.animation; - - if (oldSeries.animateDrillupFrom && oldSeries.chart) { // #2919 - oldSeries.animateDrillupFrom(level); - } - } - newSeries.options._levelNumber = levelNumber; - - oldSeries.remove(false); - - // Reset the zoom level of the upper series - if (newSeries.xAxis) { - oldExtremes = level.oldExtremes; - newSeries.xAxis.setExtremes( - oldExtremes.xMin, - oldExtremes.xMax, - false - ); - newSeries.yAxis.setExtremes( - oldExtremes.yMin, - oldExtremes.yMax, - false - ); - } - - // We have a resetZoomButton tucked away for this level. Attatch - // it to the chart and show it. - if (level.resetZoomButton) { - chart.resetZoomButton = level.resetZoomButton; - chart.resetZoomButton.show(); - } - } - } - - // Fire a once-off event after all series have been drilled up (#5158) - fireEvent(chart, 'drillupall'); - - this.redraw(); - - if (this.drilldownLevels.length === 0) { - this.drillUpButton = this.drillUpButton.destroy(); - } else { - this.drillUpButton.attr({ - text: this.getDrilldownBackText() - }) - .align(); - } - - this.ddDupes.length = []; // #3315 + if (!this.drilldownLevels || this.drilldownLevels.length === 0) { + return; + } + + var chart = this, + drilldownLevels = chart.drilldownLevels, + levelNumber = drilldownLevels[drilldownLevels.length - 1].levelNumber, + i = drilldownLevels.length, + chartSeries = chart.series, + seriesI, + level, + oldSeries, + newSeries, + oldExtremes, + addSeries = function (seriesOptions) { + var addedSeries; + each(chartSeries, function (series) { + if (series.options._ddSeriesId === seriesOptions._ddSeriesId) { + addedSeries = series; + } + }); + + addedSeries = addedSeries || chart.addSeries(seriesOptions, false); + if ( + addedSeries.type === oldSeries.type && + addedSeries.animateDrillupTo + ) { + addedSeries.animate = addedSeries.animateDrillupTo; + } + if (seriesOptions === level.seriesOptions) { + newSeries = addedSeries; + } + }; + + while (i--) { + + level = drilldownLevels[i]; + if (level.levelNumber === levelNumber) { + drilldownLevels.pop(); + + // Get the lower series by reference or id + oldSeries = level.lowerSeries; + if (!oldSeries.chart) { // #2786 + seriesI = chartSeries.length; // #2919 + while (seriesI--) { + if ( + chartSeries[seriesI].options.id === + level.lowerSeriesOptions.id && + chartSeries[seriesI].options._levelNumber === + levelNumber + 1 + ) { // #3867 + oldSeries = chartSeries[seriesI]; + break; + } + } + } + oldSeries.xData = []; // Overcome problems with minRange (#2898) + + each(level.levelSeriesOptions, addSeries); + + fireEvent(chart, 'drillup', { seriesOptions: level.seriesOptions }); + + if (newSeries.type === oldSeries.type) { + newSeries.drilldownLevel = level; + newSeries.options.animation = chart.options.drilldown.animation; + + if (oldSeries.animateDrillupFrom && oldSeries.chart) { // #2919 + oldSeries.animateDrillupFrom(level); + } + } + newSeries.options._levelNumber = levelNumber; + + oldSeries.remove(false); + + // Reset the zoom level of the upper series + if (newSeries.xAxis) { + oldExtremes = level.oldExtremes; + newSeries.xAxis.setExtremes( + oldExtremes.xMin, + oldExtremes.xMax, + false + ); + newSeries.yAxis.setExtremes( + oldExtremes.yMin, + oldExtremes.yMax, + false + ); + } + + // We have a resetZoomButton tucked away for this level. Attatch + // it to the chart and show it. + if (level.resetZoomButton) { + chart.resetZoomButton = level.resetZoomButton; + chart.resetZoomButton.show(); + } + } + } + + // Fire a once-off event after all series have been drilled up (#5158) + fireEvent(chart, 'drillupall'); + + this.redraw(); + + if (this.drilldownLevels.length === 0) { + this.drillUpButton = this.drillUpButton.destroy(); + } else { + this.drillUpButton.attr({ + text: this.getDrilldownBackText() + }) + .align(); + } + + this.ddDupes.length = []; // #3315 }; // Add update function to be called internally from Chart.update (#7600) Chart.prototype.callbacks.push(function () { - var chart = this; - chart.drilldown = { - update: function (options, redraw) { - H.merge(true, chart.options.drilldown, options); - if (pick(redraw, true)) { - chart.redraw(); - } - } - }; + var chart = this; + chart.drilldown = { + update: function (options, redraw) { + H.merge(true, chart.options.drilldown, options); + if (pick(redraw, true)) { + chart.redraw(); + } + } + }; }); // Don't show the reset button if we already are displaying the drillUp button. H.addEvent(Chart, 'beforeShowResetZoom', function () { - if (this.drillUpButton) { - return false; - } + if (this.drillUpButton) { + return false; + } }); H.addEvent(Chart, 'render', function setDDPoints() { - each(this.xAxis || [], function (axis) { - axis.ddPoints = {}; - each(axis.series, function (series) { - var i, - xData = series.xData || [], - points = series.points, - p; - - for (i = 0; i < xData.length; i++) { - p = series.options.data[i]; - - // The `drilldown` property can only be set on an array or an - // object - if (typeof p !== 'number') { - - // Convert array to object (#8008) - p = series.pointClass.prototype.optionsToObject - .call({ series: series }, p); - - if (p.drilldown) { - if (!axis.ddPoints[xData[i]]) { - axis.ddPoints[xData[i]] = []; - } - axis.ddPoints[xData[i]].push(points ? points[i] : true); - } - } - } - }); - - // Add drillability to ticks, and always keep it drillability updated - // (#3951) - objectEach(axis.ticks, Tick.prototype.drillable); - }); + each(this.xAxis || [], function (axis) { + axis.ddPoints = {}; + each(axis.series, function (series) { + var i, + xData = series.xData || [], + points = series.points, + p; + + for (i = 0; i < xData.length; i++) { + p = series.options.data[i]; + + // The `drilldown` property can only be set on an array or an + // object + if (typeof p !== 'number') { + + // Convert array to object (#8008) + p = series.pointClass.prototype.optionsToObject + .call({ series: series }, p); + + if (p.drilldown) { + if (!axis.ddPoints[xData[i]]) { + axis.ddPoints[xData[i]] = []; + } + axis.ddPoints[xData[i]].push(points ? points[i] : true); + } + } + } + }); + + // Add drillability to ticks, and always keep it drillability updated + // (#3951) + objectEach(axis.ticks, Tick.prototype.drillable); + }); }); @@ -742,113 +742,113 @@ H.addEvent(Chart, 'render', function setDDPoints() { * moved into place */ ColumnSeries.prototype.animateDrillupTo = function (init) { - if (!init) { - var newSeries = this, - level = newSeries.drilldownLevel; - - // First hide all items before animating in again - each(this.points, function (point) { - var dataLabel = point.dataLabel; - - if (point.graphic) { // #3407 - point.graphic.hide(); - } - - if (dataLabel) { - // The data label is initially hidden, make sure it is not faded - // in (#6127) - dataLabel.hidden = dataLabel.attr('visibility') === 'hidden'; - - if (!dataLabel.hidden) { - dataLabel.hide(); - if (point.connector) { - point.connector.hide(); - } - } - } - }); - - - // Do dummy animation on first point to get to complete - H.syncTimeout(function () { - if (newSeries.points) { // May be destroyed in the meantime, #3389 - each(newSeries.points, function (point, i) { - // Fade in other points - var verb = - i === (level && level.pointIndex) ? 'show' : 'fadeIn', - inherit = verb === 'show' ? true : undefined, - dataLabel = point.dataLabel; - - - if (point.graphic) { // #3407 - point.graphic[verb](inherit); - } - - if (dataLabel && !dataLabel.hidden) { // #6127 - dataLabel.fadeIn(); // #7384 - if (point.connector) { - point.connector.fadeIn(); - } - } - }); - } - }, Math.max(this.chart.options.drilldown.animation.duration - 50, 0)); - - // Reset - this.animate = noop; - } + if (!init) { + var newSeries = this, + level = newSeries.drilldownLevel; + + // First hide all items before animating in again + each(this.points, function (point) { + var dataLabel = point.dataLabel; + + if (point.graphic) { // #3407 + point.graphic.hide(); + } + + if (dataLabel) { + // The data label is initially hidden, make sure it is not faded + // in (#6127) + dataLabel.hidden = dataLabel.attr('visibility') === 'hidden'; + + if (!dataLabel.hidden) { + dataLabel.hide(); + if (point.connector) { + point.connector.hide(); + } + } + } + }); + + + // Do dummy animation on first point to get to complete + H.syncTimeout(function () { + if (newSeries.points) { // May be destroyed in the meantime, #3389 + each(newSeries.points, function (point, i) { + // Fade in other points + var verb = + i === (level && level.pointIndex) ? 'show' : 'fadeIn', + inherit = verb === 'show' ? true : undefined, + dataLabel = point.dataLabel; + + + if (point.graphic) { // #3407 + point.graphic[verb](inherit); + } + + if (dataLabel && !dataLabel.hidden) { // #6127 + dataLabel.fadeIn(); // #7384 + if (point.connector) { + point.connector.fadeIn(); + } + } + }); + } + }, Math.max(this.chart.options.drilldown.animation.duration - 50, 0)); + + // Reset + this.animate = noop; + } }; ColumnSeries.prototype.animateDrilldown = function (init) { - var series = this, - drilldownLevels = this.chart.drilldownLevels, - animateFrom, - animationOptions = animObject(this.chart.options.drilldown.animation), - xAxis = this.xAxis; - - if (!init) { - each(drilldownLevels, function (level) { - if ( - series.options._ddSeriesId === - level.lowerSeriesOptions._ddSeriesId - ) { - animateFrom = level.shapeArgs; - /*= if (build.classic) { =*/ - // Add the point colors to animate from - animateFrom.fill = level.color; - /*= } =*/ - } - }); - - animateFrom.x += (pick(xAxis.oldPos, xAxis.pos) - xAxis.pos); - - each(this.points, function (point) { - var animateTo = point.shapeArgs; - - /*= if (build.classic) { =*/ - // Add the point colors to animate to - animateTo.fill = point.color; - /*= } =*/ - - if (point.graphic) { - point.graphic - .attr(animateFrom) - .animate( - extend( - point.shapeArgs, - { fill: point.color || series.color } - ), - animationOptions - ); - } - if (point.dataLabel) { - point.dataLabel.fadeIn(animationOptions); - } - }); - this.animate = null; - } - + var series = this, + drilldownLevels = this.chart.drilldownLevels, + animateFrom, + animationOptions = animObject(this.chart.options.drilldown.animation), + xAxis = this.xAxis; + + if (!init) { + each(drilldownLevels, function (level) { + if ( + series.options._ddSeriesId === + level.lowerSeriesOptions._ddSeriesId + ) { + animateFrom = level.shapeArgs; + /*= if (build.classic) { =*/ + // Add the point colors to animate from + animateFrom.fill = level.color; + /*= } =*/ + } + }); + + animateFrom.x += (pick(xAxis.oldPos, xAxis.pos) - xAxis.pos); + + each(this.points, function (point) { + var animateTo = point.shapeArgs; + + /*= if (build.classic) { =*/ + // Add the point colors to animate to + animateTo.fill = point.color; + /*= } =*/ + + if (point.graphic) { + point.graphic + .attr(animateFrom) + .animate( + extend( + point.shapeArgs, + { fill: point.color || series.color } + ), + animationOptions + ); + } + if (point.dataLabel) { + point.dataLabel.fadeIn(animationOptions); + } + }); + this.animate = null; + } + }; /** @@ -856,144 +856,144 @@ ColumnSeries.prototype.animateDrilldown = function (init) { * series and animate them into the origin point in the upper series. */ ColumnSeries.prototype.animateDrillupFrom = function (level) { - var animationOptions = animObject(this.chart.options.drilldown.animation), - group = this.group, - // For 3d column series all columns are added to one group - // so we should not delete the whole group. #5297 - removeGroup = group !== this.chart.columnGroup, - series = this; - - // Cancel mouse events on the series group (#2787) - each(series.trackerGroups, function (key) { - if (series[key]) { // we don't always have dataLabelsGroup - series[key].on('mouseover'); - } - }); - - if (removeGroup) { - delete this.group; - } - - each(this.points, function (point) { - var graphic = point.graphic, - animateTo = level.shapeArgs, - complete = function () { - graphic.destroy(); - if (group && removeGroup) { - group = group.destroy(); - } - }; - - if (graphic) { - - delete point.graphic; - - /*= if (build.classic) { =*/ - animateTo.fill = level.color; - /*= } =*/ - - if (animationOptions.duration) { - graphic.animate( - animateTo, - H.merge(animationOptions, { complete: complete }) - ); - } else { - graphic.attr(animateTo); - complete(); - } - } - }); + var animationOptions = animObject(this.chart.options.drilldown.animation), + group = this.group, + // For 3d column series all columns are added to one group + // so we should not delete the whole group. #5297 + removeGroup = group !== this.chart.columnGroup, + series = this; + + // Cancel mouse events on the series group (#2787) + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key].on('mouseover'); + } + }); + + if (removeGroup) { + delete this.group; + } + + each(this.points, function (point) { + var graphic = point.graphic, + animateTo = level.shapeArgs, + complete = function () { + graphic.destroy(); + if (group && removeGroup) { + group = group.destroy(); + } + }; + + if (graphic) { + + delete point.graphic; + + /*= if (build.classic) { =*/ + animateTo.fill = level.color; + /*= } =*/ + + if (animationOptions.duration) { + graphic.animate( + animateTo, + H.merge(animationOptions, { complete: complete }) + ); + } else { + graphic.attr(animateTo); + complete(); + } + } + }); }; if (PieSeries) { - extend(PieSeries.prototype, { - animateDrillupTo: ColumnSeries.prototype.animateDrillupTo, - animateDrillupFrom: ColumnSeries.prototype.animateDrillupFrom, - - animateDrilldown: function (init) { - var level = this.chart.drilldownLevels[ - this.chart.drilldownLevels.length - 1 - ], - animationOptions = this.chart.options.drilldown.animation, - animateFrom = level.shapeArgs, - start = animateFrom.start, - angle = animateFrom.end - start, - startAngle = angle / this.points.length; - - if (!init) { - each(this.points, function (point, i) { - var animateTo = point.shapeArgs; - - /*= if (build.classic) { =*/ - animateFrom.fill = level.color; - animateTo.fill = point.color; - /*= } =*/ - - if (point.graphic) { - point.graphic - .attr(H.merge(animateFrom, { - start: start + i * startAngle, - end: start + (i + 1) * startAngle - }))[animationOptions ? 'animate' : 'attr']( - animateTo, - animationOptions - ); - } - }); - this.animate = null; - } - } - }); + extend(PieSeries.prototype, { + animateDrillupTo: ColumnSeries.prototype.animateDrillupTo, + animateDrillupFrom: ColumnSeries.prototype.animateDrillupFrom, + + animateDrilldown: function (init) { + var level = this.chart.drilldownLevels[ + this.chart.drilldownLevels.length - 1 + ], + animationOptions = this.chart.options.drilldown.animation, + animateFrom = level.shapeArgs, + start = animateFrom.start, + angle = animateFrom.end - start, + startAngle = angle / this.points.length; + + if (!init) { + each(this.points, function (point, i) { + var animateTo = point.shapeArgs; + + /*= if (build.classic) { =*/ + animateFrom.fill = level.color; + animateTo.fill = point.color; + /*= } =*/ + + if (point.graphic) { + point.graphic + .attr(H.merge(animateFrom, { + start: start + i * startAngle, + end: start + (i + 1) * startAngle + }))[animationOptions ? 'animate' : 'attr']( + animateTo, + animationOptions + ); + } + }); + this.animate = null; + } + } + }); } H.Point.prototype.doDrilldown = function ( - _holdRedraw, - category, - originalEvent + _holdRedraw, + category, + originalEvent ) { - var series = this.series, - chart = series.chart, - drilldown = chart.options.drilldown, - i = (drilldown.series || []).length, - seriesOptions; - - if (!chart.ddDupes) { - chart.ddDupes = []; - } - - while (i-- && !seriesOptions) { - if ( - drilldown.series[i].id === this.drilldown && - inArray(this.drilldown, chart.ddDupes) === -1 - ) { - seriesOptions = drilldown.series[i]; - chart.ddDupes.push(this.drilldown); - } - } - - // Fire the event. If seriesOptions is undefined, the implementer can check - // for seriesOptions, and call addSeriesAsDrilldown async if necessary. - fireEvent(chart, 'drilldown', { - point: this, - seriesOptions: seriesOptions, - category: category, - originalEvent: originalEvent, - points: ( - category !== undefined && - this.series.xAxis.getDDPoints(category).slice(0) - ) - }, function (e) { - var chart = e.point.series && e.point.series.chart, - seriesOptions = e.seriesOptions; - if (chart && seriesOptions) { - if (_holdRedraw) { - chart.addSingleSeriesAsDrilldown(e.point, seriesOptions); - } else { - chart.addSeriesAsDrilldown(e.point, seriesOptions); - } - } - }); - + var series = this.series, + chart = series.chart, + drilldown = chart.options.drilldown, + i = (drilldown.series || []).length, + seriesOptions; + + if (!chart.ddDupes) { + chart.ddDupes = []; + } + + while (i-- && !seriesOptions) { + if ( + drilldown.series[i].id === this.drilldown && + inArray(this.drilldown, chart.ddDupes) === -1 + ) { + seriesOptions = drilldown.series[i]; + chart.ddDupes.push(this.drilldown); + } + } + + // Fire the event. If seriesOptions is undefined, the implementer can check + // for seriesOptions, and call addSeriesAsDrilldown async if necessary. + fireEvent(chart, 'drilldown', { + point: this, + seriesOptions: seriesOptions, + category: category, + originalEvent: originalEvent, + points: ( + category !== undefined && + this.series.xAxis.getDDPoints(category).slice(0) + ) + }, function (e) { + var chart = e.point.series && e.point.series.chart, + seriesOptions = e.seriesOptions; + if (chart && seriesOptions) { + if (_holdRedraw) { + chart.addSingleSeriesAsDrilldown(e.point, seriesOptions); + } else { + chart.addSeriesAsDrilldown(e.point, seriesOptions); + } + } + }); + }; @@ -1002,24 +1002,24 @@ H.Point.prototype.doDrilldown = function ( * label. */ H.Axis.prototype.drilldownCategory = function (x, e) { - objectEach(this.getDDPoints(x), function (point) { - if ( - point && - point.series && - point.series.visible && - point.doDrilldown - ) { // #3197 - point.doDrilldown(true, x, e); - } - }); - this.chart.applyDrilldown(); + objectEach(this.getDDPoints(x), function (point) { + if ( + point && + point.series && + point.series.visible && + point.doDrilldown + ) { // #3197 + point.doDrilldown(true, x, e); + } + }); + this.chart.applyDrilldown(); }; /** * Return drillable points for this specific X value */ H.Axis.prototype.getDDPoints = function (x) { - return this.ddPoints && this.ddPoints[x]; + return this.ddPoints && this.ddPoints[x]; }; @@ -1027,42 +1027,42 @@ H.Axis.prototype.getDDPoints = function (x) { * Make a tick label drillable, or remove drilling on update */ Tick.prototype.drillable = function () { - var pos = this.pos, - label = this.label, - axis = this.axis, - isDrillable = axis.coll === 'xAxis' && axis.getDDPoints, - ddPointsX = isDrillable && axis.getDDPoints(pos); - - if (isDrillable) { - if (label && ddPointsX && ddPointsX.length) { - label.drillable = true; - - /*= if (build.classic) { =*/ - if (!label.basicStyles) { - label.basicStyles = H.merge(label.styles); - } - /*= } =*/ - - label - .addClass('highcharts-drilldown-axis-label') - /*= if (build.classic) { =*/ - .css(axis.chart.options.drilldown.activeAxisLabelStyle) - /*= } =*/ - .on('click', function (e) { - axis.drilldownCategory(pos, e); - }); - - } else if (label && label.drillable) { - - /*= if (build.classic) { =*/ - label.styles = {}; // reset for full overwrite of styles - label.css(label.basicStyles); - /*= } =*/ - - label.on('click', null); // #3806 - label.removeClass('highcharts-drilldown-axis-label'); - } - } + var pos = this.pos, + label = this.label, + axis = this.axis, + isDrillable = axis.coll === 'xAxis' && axis.getDDPoints, + ddPointsX = isDrillable && axis.getDDPoints(pos); + + if (isDrillable) { + if (label && ddPointsX && ddPointsX.length) { + label.drillable = true; + + /*= if (build.classic) { =*/ + if (!label.basicStyles) { + label.basicStyles = H.merge(label.styles); + } + /*= } =*/ + + label + .addClass('highcharts-drilldown-axis-label') + /*= if (build.classic) { =*/ + .css(axis.chart.options.drilldown.activeAxisLabelStyle) + /*= } =*/ + .on('click', function (e) { + axis.drilldownCategory(pos, e); + }); + + } else if (label && label.drillable) { + + /*= if (build.classic) { =*/ + label.styles = {}; // reset for full overwrite of styles + label.css(label.basicStyles); + /*= } =*/ + + label.on('click', null); // #3806 + label.removeClass('highcharts-drilldown-axis-label'); + } + } }; @@ -1071,88 +1071,88 @@ Tick.prototype.drillable = function () { * Also, provide a list of points associated to that label. */ H.addEvent(H.Point, 'afterInit', function () { - var point = this, - series = point.series; - - if (point.drilldown) { - - // Add the click event to the point - H.addEvent(point, 'click', function (e) { - if ( - series.xAxis && - series.chart.options.drilldown.allowPointDrilldown === false - ) { - series.xAxis.drilldownCategory(point.x, e); // #5822, x changed - } else { - point.doDrilldown(undefined, undefined, e); - } - }); - - } - - return point; + var point = this, + series = point.series; + + if (point.drilldown) { + + // Add the click event to the point + H.addEvent(point, 'click', function (e) { + if ( + series.xAxis && + series.chart.options.drilldown.allowPointDrilldown === false + ) { + series.xAxis.drilldownCategory(point.x, e); // #5822, x changed + } else { + point.doDrilldown(undefined, undefined, e); + } + }); + + } + + return point; }); H.addEvent(H.Series, 'afterDrawDataLabels', function () { - var css = this.chart.options.drilldown.activeDataLabelStyle, - renderer = this.chart.renderer; - - each(this.points, function (point) { - var dataLabelsOptions = point.options.dataLabels, - pointCSS = pick( - point.dlOptions, - dataLabelsOptions && dataLabelsOptions.style, - {} - ); - - if (point.drilldown && point.dataLabel) { - /*= if (build.classic) { =*/ - if (css.color === 'contrast') { - pointCSS.color = renderer.getContrast( - point.color || this.color - ); - } - /*= } =*/ - if (dataLabelsOptions && dataLabelsOptions.color) { - pointCSS.color = dataLabelsOptions.color; - } - point.dataLabel - .addClass('highcharts-drilldown-data-label'); - - /*= if (build.classic) { =*/ - point.dataLabel - .css(css) - .css(pointCSS); - /*= } =*/ - } - }, this); + var css = this.chart.options.drilldown.activeDataLabelStyle, + renderer = this.chart.renderer; + + each(this.points, function (point) { + var dataLabelsOptions = point.options.dataLabels, + pointCSS = pick( + point.dlOptions, + dataLabelsOptions && dataLabelsOptions.style, + {} + ); + + if (point.drilldown && point.dataLabel) { + /*= if (build.classic) { =*/ + if (css.color === 'contrast') { + pointCSS.color = renderer.getContrast( + point.color || this.color + ); + } + /*= } =*/ + if (dataLabelsOptions && dataLabelsOptions.color) { + pointCSS.color = dataLabelsOptions.color; + } + point.dataLabel + .addClass('highcharts-drilldown-data-label'); + + /*= if (build.classic) { =*/ + point.dataLabel + .css(css) + .css(pointCSS); + /*= } =*/ + } + }, this); }); var applyCursorCSS = function (element, cursor, addClass) { - element[addClass ? 'addClass' : 'removeClass']( - 'highcharts-drilldown-point' - ); + element[addClass ? 'addClass' : 'removeClass']( + 'highcharts-drilldown-point' + ); - /*= if (build.classic) { =*/ - element.css({ cursor: cursor }); - /*= } =*/ + /*= if (build.classic) { =*/ + element.css({ cursor: cursor }); + /*= } =*/ }; -// Mark the trackers with a pointer +// Mark the trackers with a pointer H.addEvent(H.Series, 'afterDrawTracker', function () { - each(this.points, function (point) { - if (point.drilldown && point.graphic) { - applyCursorCSS(point.graphic, 'pointer', true); - } - }); + each(this.points, function (point) { + if (point.drilldown && point.graphic) { + applyCursorCSS(point.graphic, 'pointer', true); + } + }); }); H.addEvent(H.Point, 'afterSetState', function () { - if (this.drilldown && this.series.halo && this.state === 'hover') { - applyCursorCSS(this.series.halo, 'pointer', true); - } else if (this.series.halo) { - applyCursorCSS(this.series.halo, 'auto', false); - } + if (this.drilldown && this.series.halo && this.state === 'hover') { + applyCursorCSS(this.series.halo, 'pointer', true); + } else if (this.series.halo) { + applyCursorCSS(this.series.halo, 'auto', false); + } }); diff --git a/js/modules/export-data.src.js b/js/modules/export-data.src.js index 317ec8342aa..e10b045b23a 100644 --- a/js/modules/export-data.src.js +++ b/js/modules/export-data.src.js @@ -16,12 +16,12 @@ import '../parts/Utilities.js'; import '../parts/Chart.js'; var defined = Highcharts.defined, - each = Highcharts.each, - pick = Highcharts.pick, - win = Highcharts.win, - doc = win.document, - seriesTypes = Highcharts.seriesTypes, - downloadAttrSupported = doc.createElement('a').download !== undefined; + each = Highcharts.each, + pick = Highcharts.pick, + win = Highcharts.win, + doc = win.document, + seriesTypes = Highcharts.seriesTypes, + downloadAttrSupported = doc.createElement('a').download !== undefined; // Can we add this to utils? Also used in screen-reader.js /** @@ -30,164 +30,164 @@ var defined = Highcharts.defined, * @return {string} The excaped string */ function htmlencode(html) { - return html - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\//g, '/'); + return html + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); } Highcharts.setOptions({ - /** - * @optionparent exporting - */ - exporting: { - - /** - * Export-data module required. Caption for the data table. Same as - * chart title by default. Set to `false` to disable. - * - * @type {Boolean|String} - * @since 6.0.4 - * @sample highcharts/export-data/multilevel-table - * Multiple table headers - * @default undefined - * @apioption exporting.tableCaption - */ - - /** - * Options for exporting data to CSV or ExCel, or displaying the data - * in a HTML table or a JavaScript structure. Requires the - * `export-data.js` module. This module adds data export options to the - * export menu and provides functions like `Chart.getCSV`, - * `Chart.getTable`, `Chart.getDataRows` and `Chart.viewData`. - * - * @sample highcharts/export-data/categorized/ Categorized data - * @sample highcharts/export-data/stock-timeaxis/ Highstock time axis - * - * @since 6.0.0 - */ - csv: { - /** - * Formatter callback for the column headers. Parameters are: - * - `item` - The series or axis object) - * - `key` - The point key, for example y or z - * - `keyLength` - The amount of value keys for this item, for - * example a range series has the keys `low` and `high` so the - * key length is 2. - * - * If [useMultiLevelHeaders](#exporting.useMultiLevelHeaders) is - * true, columnHeaderFormatter by default returns an object with - * columnTitle and topLevelColumnTitle for each key. Columns with - * the same topLevelColumnTitle have their titles merged into a - * single cell with colspan for table/Excel export. - * - * If `useMultiLevelHeaders` is false, or for CSV export, it returns - * the series name, followed by the key if there is more than one - * key. - * - * For the axis it returns the axis title or "Category" or - * "DateTime" by default. - * - * Return `false` to use Highcharts' proposed header. - * - * @sample highcharts/export-data/multilevel-table - * Multiple table headers - * @type {Function|null} - */ - columnHeaderFormatter: null, - /** - * Which date format to use for exported dates on a datetime X axis. - * See `Highcharts.dateFormat`. - */ - dateFormat: '%Y-%m-%d %H:%M:%S', - - /** - * Which decimal point to use for exported CSV. Defaults to the same - * as the browser locale, typically `.` (English) or `,` (German, - * French etc). - * @type {String} - * @since 6.0.4 - */ - decimalPoint: null, - /** - * The item delimiter in the exported data. Use `;` for direct - * exporting to Excel. Defaults to a best guess based on the browser - * locale. If the locale _decimal point_ is `,`, the `itemDelimiter` - * defaults to `;`, otherwise the `itemDelimiter` defaults to `,`. - * - * @type {String} - */ - itemDelimiter: null, - /** - * The line delimiter in the exported data, defaults to a newline. - */ - lineDelimiter: '\n' - }, - /** - * Export-data module required. Show a HTML table below the chart with - * the chart's current data. - * - * @sample highcharts/export-data/showtable/ Show the table - * @since 6.0.0 - */ - showTable: false, - - /** - * Export-data module required. Use multi level headers in data table. - * If [csv.columnHeaderFormatter](#exporting.csv.columnHeaderFormatter) - * is defined, it has to return objects in order for multi level headers - * to work. - * - * @sample highcharts/export-data/multilevel-table - * Multiple table headers - * @since 6.0.4 - */ - useMultiLevelHeaders: true, - - /** - * Export-data module required. If using multi level table headers, use - * rowspans for headers that have only one level. - * - * @sample highcharts/export-data/multilevel-table - * Multiple table headers - * @since 6.0.4 - */ - useRowspanHeaders: true - }, - /** - * @optionparent lang - */ - lang: { - /** - * Export-data module only. The text for the menu item. - * @since 6.0.0 - */ - downloadCSV: 'Download CSV', - /** - * Export-data module only. The text for the menu item. - * @since 6.0.0 - */ - downloadXLS: 'Download XLS', - /** - * Export-data module only. The text for the menu item. - * @since 6.0.0 - */ - viewData: 'View data table' - } + /** + * @optionparent exporting + */ + exporting: { + + /** + * Export-data module required. Caption for the data table. Same as + * chart title by default. Set to `false` to disable. + * + * @type {Boolean|String} + * @since 6.0.4 + * @sample highcharts/export-data/multilevel-table + * Multiple table headers + * @default undefined + * @apioption exporting.tableCaption + */ + + /** + * Options for exporting data to CSV or ExCel, or displaying the data + * in a HTML table or a JavaScript structure. Requires the + * `export-data.js` module. This module adds data export options to the + * export menu and provides functions like `Chart.getCSV`, + * `Chart.getTable`, `Chart.getDataRows` and `Chart.viewData`. + * + * @sample highcharts/export-data/categorized/ Categorized data + * @sample highcharts/export-data/stock-timeaxis/ Highstock time axis + * + * @since 6.0.0 + */ + csv: { + /** + * Formatter callback for the column headers. Parameters are: + * - `item` - The series or axis object) + * - `key` - The point key, for example y or z + * - `keyLength` - The amount of value keys for this item, for + * example a range series has the keys `low` and `high` so the + * key length is 2. + * + * If [useMultiLevelHeaders](#exporting.useMultiLevelHeaders) is + * true, columnHeaderFormatter by default returns an object with + * columnTitle and topLevelColumnTitle for each key. Columns with + * the same topLevelColumnTitle have their titles merged into a + * single cell with colspan for table/Excel export. + * + * If `useMultiLevelHeaders` is false, or for CSV export, it returns + * the series name, followed by the key if there is more than one + * key. + * + * For the axis it returns the axis title or "Category" or + * "DateTime" by default. + * + * Return `false` to use Highcharts' proposed header. + * + * @sample highcharts/export-data/multilevel-table + * Multiple table headers + * @type {Function|null} + */ + columnHeaderFormatter: null, + /** + * Which date format to use for exported dates on a datetime X axis. + * See `Highcharts.dateFormat`. + */ + dateFormat: '%Y-%m-%d %H:%M:%S', + + /** + * Which decimal point to use for exported CSV. Defaults to the same + * as the browser locale, typically `.` (English) or `,` (German, + * French etc). + * @type {String} + * @since 6.0.4 + */ + decimalPoint: null, + /** + * The item delimiter in the exported data. Use `;` for direct + * exporting to Excel. Defaults to a best guess based on the browser + * locale. If the locale _decimal point_ is `,`, the `itemDelimiter` + * defaults to `;`, otherwise the `itemDelimiter` defaults to `,`. + * + * @type {String} + */ + itemDelimiter: null, + /** + * The line delimiter in the exported data, defaults to a newline. + */ + lineDelimiter: '\n' + }, + /** + * Export-data module required. Show a HTML table below the chart with + * the chart's current data. + * + * @sample highcharts/export-data/showtable/ Show the table + * @since 6.0.0 + */ + showTable: false, + + /** + * Export-data module required. Use multi level headers in data table. + * If [csv.columnHeaderFormatter](#exporting.csv.columnHeaderFormatter) + * is defined, it has to return objects in order for multi level headers + * to work. + * + * @sample highcharts/export-data/multilevel-table + * Multiple table headers + * @since 6.0.4 + */ + useMultiLevelHeaders: true, + + /** + * Export-data module required. If using multi level table headers, use + * rowspans for headers that have only one level. + * + * @sample highcharts/export-data/multilevel-table + * Multiple table headers + * @since 6.0.4 + */ + useRowspanHeaders: true + }, + /** + * @optionparent lang + */ + lang: { + /** + * Export-data module only. The text for the menu item. + * @since 6.0.0 + */ + downloadCSV: 'Download CSV', + /** + * Export-data module only. The text for the menu item. + * @since 6.0.0 + */ + downloadXLS: 'Download XLS', + /** + * Export-data module only. The text for the menu item. + * @since 6.0.0 + */ + viewData: 'View data table' + } }); // Add an event listener to handle the showTable option Highcharts.addEvent(Highcharts.Chart, 'render', function () { - if ( - this.options && - this.options.exporting && - this.options.exporting.showTable - ) { - this.viewData(); - } + if ( + this.options && + this.options.exporting && + this.options.exporting.showTable + ) { + this.viewData(); + } }); // Set up key-to-axis bindings. This is used when the Y axis is datetime or @@ -195,12 +195,12 @@ Highcharts.addEvent(Highcharts.Chart, 'render', function () { // sholud be formatted according to the Y axis type, and in order to link them // we need this map. Highcharts.Chart.prototype.setUpKeyToAxis = function () { - if (seriesTypes.arearange) { - seriesTypes.arearange.prototype.keyToAxis = { - low: 'y', - high: 'y' - }; - } + if (seriesTypes.arearange) { + seriesTypes.arearange.prototype.keyToAxis = { + low: 'y', + high: 'y' + }; + } }; /** @@ -208,309 +208,309 @@ Highcharts.Chart.prototype.setUpKeyToAxis = function () { * current chart data. * * @param {Boolean} multiLevelHeaders - * Use multilevel headers for the rows by default. Adds an extra row - * with top level headers. If a custom columnHeaderFormatter is - * defined, this can override the behavior. + * Use multilevel headers for the rows by default. Adds an extra row + * with top level headers. If a custom columnHeaderFormatter is + * defined, this can override the behavior. * * @returns {Array.} - * The current chart data + * The current chart data */ Highcharts.Chart.prototype.getDataRows = function (multiLevelHeaders) { - var time = this.time, - csvOptions = (this.options.exporting && this.options.exporting.csv) || - {}, - xAxis, - xAxes = this.xAxis, - rows = {}, - rowArr = [], - dataRows, - topLevelColumnTitles = [], - columnTitles = [], - columnTitleObj, - i, - x, - xTitle, - // Options - columnHeaderFormatter = function (item, key, keyLength) { - if (csvOptions.columnHeaderFormatter) { - var s = csvOptions.columnHeaderFormatter(item, key, keyLength); - if (s !== false) { - return s; - } - } - - if (!item) { - return 'Category'; - } - - if (item instanceof Highcharts.Axis) { - return (item.options.title && item.options.title.text) || - (item.isDatetimeAxis ? 'DateTime' : 'Category'); - } - - if (multiLevelHeaders) { - return { - columnTitle: keyLength > 1 ? key : item.name, - topLevelColumnTitle: item.name - }; - } - - return item.name + (keyLength > 1 ? ' (' + key + ')' : ''); - }, - xAxisIndices = []; - - // Loop the series and index values - i = 0; - - this.setUpKeyToAxis(); - - each(this.series, function (series) { - var keys = series.options.keys, - pointArrayMap = keys || series.pointArrayMap || ['y'], - valueCount = pointArrayMap.length, - xTaken = !series.requireSorting && {}, - categoryMap = {}, - datetimeValueAxisMap = {}, - xAxisIndex = Highcharts.inArray(series.xAxis, xAxes), - mockSeries, - j; - - // Map the categories for value axes - each(pointArrayMap, function (prop) { - var axisName = ( - (series.keyToAxis && series.keyToAxis[prop]) || - prop - ) + 'Axis'; - - categoryMap[prop] = ( - series[axisName] && - series[axisName].categories - ) || []; - datetimeValueAxisMap[prop] = ( - series[axisName] && - series[axisName].isDatetimeAxis - ); - }); - - if ( - series.options.includeInCSVExport !== false && - series.visible !== false // #55 - ) { - - // Build a lookup for X axis index and the position of the first - // series that belongs to that X axis. Includes -1 for non-axis - // series types like pies. - if (!Highcharts.find(xAxisIndices, function (index) { - return index[0] === xAxisIndex; - })) { - xAxisIndices.push([xAxisIndex, i]); - } - - // Compute the column headers and top level headers, usually the - // same as series names - j = 0; - while (j < valueCount) { - columnTitleObj = columnHeaderFormatter( - series, - pointArrayMap[j], - pointArrayMap.length - ); - columnTitles.push( - columnTitleObj.columnTitle || columnTitleObj - ); - if (multiLevelHeaders) { - topLevelColumnTitles.push( - columnTitleObj.topLevelColumnTitle || columnTitleObj - ); - } - j++; - } - - mockSeries = { - chart: series.chart, - autoIncrement: series.autoIncrement, - options: series.options, - pointArrayMap: series.pointArrayMap - }; - - // Export directly from options.data because we need the uncropped - // data (#7913), and we need to support Boost (#7026). - each(series.options.data, function eachData(options, pIdx) { - var key, - prop, - val, - point; - - point = { series: mockSeries }; - series.pointClass.prototype.applyOptions.apply( - point, - [options] - ); - key = point.x; - - if (xTaken) { - if (xTaken[key]) { - key += '|' + pIdx; - } - xTaken[key] = true; - } - - j = 0; - - if (!rows[key]) { - // Generate the row - rows[key] = []; - // Contain the X values from one or more X axes - rows[key].xValues = []; - } - rows[key].x = point.x; - rows[key].xValues[xAxisIndex] = point.x; - - // Pies, funnels, geo maps etc. use point name in X row - if (!series.xAxis || series.exportKey === 'name') { - rows[key].name = ( - series.data[pIdx] && - series.data[pIdx].name - ); - } - - while (j < valueCount) { - prop = pointArrayMap[j]; // y, z etc - val = point[prop]; - rows[key][i + j] = pick( - categoryMap[prop][val], // Y axis category if present - datetimeValueAxisMap[prop] ? - time.dateFormat(csvOptions.dateFormat, val) : - null, - val - ); - j++; - } - - }); - i = i + j; - } - }); - - // Make a sortable array - for (x in rows) { - if (rows.hasOwnProperty(x)) { - rowArr.push(rows[x]); - } - } - - var xAxisIndex, column; - - // Add computed column headers and top level headers to final row set - dataRows = multiLevelHeaders ? [topLevelColumnTitles, columnTitles] : - [columnTitles]; - - i = xAxisIndices.length; - while (i--) { // Start from end to splice in - xAxisIndex = xAxisIndices[i][0]; - column = xAxisIndices[i][1]; - xAxis = xAxes[xAxisIndex]; - - // Sort it by X values - rowArr.sort(function (a, b) { // eslint-disable-line no-loop-func - return a.xValues[xAxisIndex] - b.xValues[xAxisIndex]; - }); - - // Add header row - xTitle = columnHeaderFormatter(xAxis); - dataRows[0].splice(column, 0, xTitle); - if (multiLevelHeaders && dataRows[1]) { - // If using multi level headers, we just added top level header. - // Also add for sub level - dataRows[1].splice(column, 0, xTitle); - } - - // Add the category column - each(rowArr, function (row) { // eslint-disable-line no-loop-func - var category = row.name; - if (!defined(category)) { - if (xAxis.isDatetimeAxis) { - if (row.x instanceof Date) { - row.x = row.x.getTime(); - } - category = time.dateFormat( - csvOptions.dateFormat, - row.x - ); - } else if (xAxis.categories) { - category = pick( - xAxis.names[row.x], - xAxis.categories[row.x], - row.x - ); - } else { - category = row.x; - } - } - - // Add the X/date/category - row.splice(column, 0, category); - }); - } - dataRows = dataRows.concat(rowArr); - - return dataRows; + var time = this.time, + csvOptions = (this.options.exporting && this.options.exporting.csv) || + {}, + xAxis, + xAxes = this.xAxis, + rows = {}, + rowArr = [], + dataRows, + topLevelColumnTitles = [], + columnTitles = [], + columnTitleObj, + i, + x, + xTitle, + // Options + columnHeaderFormatter = function (item, key, keyLength) { + if (csvOptions.columnHeaderFormatter) { + var s = csvOptions.columnHeaderFormatter(item, key, keyLength); + if (s !== false) { + return s; + } + } + + if (!item) { + return 'Category'; + } + + if (item instanceof Highcharts.Axis) { + return (item.options.title && item.options.title.text) || + (item.isDatetimeAxis ? 'DateTime' : 'Category'); + } + + if (multiLevelHeaders) { + return { + columnTitle: keyLength > 1 ? key : item.name, + topLevelColumnTitle: item.name + }; + } + + return item.name + (keyLength > 1 ? ' (' + key + ')' : ''); + }, + xAxisIndices = []; + + // Loop the series and index values + i = 0; + + this.setUpKeyToAxis(); + + each(this.series, function (series) { + var keys = series.options.keys, + pointArrayMap = keys || series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + xTaken = !series.requireSorting && {}, + categoryMap = {}, + datetimeValueAxisMap = {}, + xAxisIndex = Highcharts.inArray(series.xAxis, xAxes), + mockSeries, + j; + + // Map the categories for value axes + each(pointArrayMap, function (prop) { + var axisName = ( + (series.keyToAxis && series.keyToAxis[prop]) || + prop + ) + 'Axis'; + + categoryMap[prop] = ( + series[axisName] && + series[axisName].categories + ) || []; + datetimeValueAxisMap[prop] = ( + series[axisName] && + series[axisName].isDatetimeAxis + ); + }); + + if ( + series.options.includeInCSVExport !== false && + series.visible !== false // #55 + ) { + + // Build a lookup for X axis index and the position of the first + // series that belongs to that X axis. Includes -1 for non-axis + // series types like pies. + if (!Highcharts.find(xAxisIndices, function (index) { + return index[0] === xAxisIndex; + })) { + xAxisIndices.push([xAxisIndex, i]); + } + + // Compute the column headers and top level headers, usually the + // same as series names + j = 0; + while (j < valueCount) { + columnTitleObj = columnHeaderFormatter( + series, + pointArrayMap[j], + pointArrayMap.length + ); + columnTitles.push( + columnTitleObj.columnTitle || columnTitleObj + ); + if (multiLevelHeaders) { + topLevelColumnTitles.push( + columnTitleObj.topLevelColumnTitle || columnTitleObj + ); + } + j++; + } + + mockSeries = { + chart: series.chart, + autoIncrement: series.autoIncrement, + options: series.options, + pointArrayMap: series.pointArrayMap + }; + + // Export directly from options.data because we need the uncropped + // data (#7913), and we need to support Boost (#7026). + each(series.options.data, function eachData(options, pIdx) { + var key, + prop, + val, + point; + + point = { series: mockSeries }; + series.pointClass.prototype.applyOptions.apply( + point, + [options] + ); + key = point.x; + + if (xTaken) { + if (xTaken[key]) { + key += '|' + pIdx; + } + xTaken[key] = true; + } + + j = 0; + + if (!rows[key]) { + // Generate the row + rows[key] = []; + // Contain the X values from one or more X axes + rows[key].xValues = []; + } + rows[key].x = point.x; + rows[key].xValues[xAxisIndex] = point.x; + + // Pies, funnels, geo maps etc. use point name in X row + if (!series.xAxis || series.exportKey === 'name') { + rows[key].name = ( + series.data[pIdx] && + series.data[pIdx].name + ); + } + + while (j < valueCount) { + prop = pointArrayMap[j]; // y, z etc + val = point[prop]; + rows[key][i + j] = pick( + categoryMap[prop][val], // Y axis category if present + datetimeValueAxisMap[prop] ? + time.dateFormat(csvOptions.dateFormat, val) : + null, + val + ); + j++; + } + + }); + i = i + j; + } + }); + + // Make a sortable array + for (x in rows) { + if (rows.hasOwnProperty(x)) { + rowArr.push(rows[x]); + } + } + + var xAxisIndex, column; + + // Add computed column headers and top level headers to final row set + dataRows = multiLevelHeaders ? [topLevelColumnTitles, columnTitles] : + [columnTitles]; + + i = xAxisIndices.length; + while (i--) { // Start from end to splice in + xAxisIndex = xAxisIndices[i][0]; + column = xAxisIndices[i][1]; + xAxis = xAxes[xAxisIndex]; + + // Sort it by X values + rowArr.sort(function (a, b) { // eslint-disable-line no-loop-func + return a.xValues[xAxisIndex] - b.xValues[xAxisIndex]; + }); + + // Add header row + xTitle = columnHeaderFormatter(xAxis); + dataRows[0].splice(column, 0, xTitle); + if (multiLevelHeaders && dataRows[1]) { + // If using multi level headers, we just added top level header. + // Also add for sub level + dataRows[1].splice(column, 0, xTitle); + } + + // Add the category column + each(rowArr, function (row) { // eslint-disable-line no-loop-func + var category = row.name; + if (!defined(category)) { + if (xAxis.isDatetimeAxis) { + if (row.x instanceof Date) { + row.x = row.x.getTime(); + } + category = time.dateFormat( + csvOptions.dateFormat, + row.x + ); + } else if (xAxis.categories) { + category = pick( + xAxis.names[row.x], + xAxis.categories[row.x], + row.x + ); + } else { + category = row.x; + } + } + + // Add the X/date/category + row.splice(column, 0, category); + }); + } + dataRows = dataRows.concat(rowArr); + + return dataRows; }; /** * Export-data module required. Returns the current chart data as a CSV string. * * @param {Boolean} useLocalDecimalPoint - * Whether to use the local decimal point as detected from the browser. - * This makes it easier to export data to Excel in the same locale as - * the user is. + * Whether to use the local decimal point as detected from the browser. + * This makes it easier to export data to Excel in the same locale as + * the user is. * * @returns {String} - * CSV representation of the data + * CSV representation of the data */ Highcharts.Chart.prototype.getCSV = function (useLocalDecimalPoint) { - var csv = '', - rows = this.getDataRows(), - csvOptions = this.options.exporting.csv, - decimalPoint = pick( - csvOptions.decimalPoint, - csvOptions.itemDelimiter !== ',' && useLocalDecimalPoint ? - (1.1).toLocaleString()[1] : - '.' - ), - // use ';' for direct to Excel - itemDelimiter = pick( - csvOptions.itemDelimiter, - decimalPoint === ',' ? ';' : ',' - ), - // '\n' isn't working with the js csv data extraction - lineDelimiter = csvOptions.lineDelimiter; - - // Transform the rows to CSV - each(rows, function (row, i) { - var val = '', - j = row.length; - while (j--) { - val = row[j]; - if (typeof val === 'string') { - val = '"' + val + '"'; - } - if (typeof val === 'number') { - if (decimalPoint !== '.') { - val = val.toString().replace('.', decimalPoint); - } - } - row[j] = val; - } - // Add the values - csv += row.join(itemDelimiter); - - // Add the line delimiter - if (i < rows.length - 1) { - csv += lineDelimiter; - } - }); - return csv; + var csv = '', + rows = this.getDataRows(), + csvOptions = this.options.exporting.csv, + decimalPoint = pick( + csvOptions.decimalPoint, + csvOptions.itemDelimiter !== ',' && useLocalDecimalPoint ? + (1.1).toLocaleString()[1] : + '.' + ), + // use ';' for direct to Excel + itemDelimiter = pick( + csvOptions.itemDelimiter, + decimalPoint === ',' ? ';' : ',' + ), + // '\n' isn't working with the js csv data extraction + lineDelimiter = csvOptions.lineDelimiter; + + // Transform the rows to CSV + each(rows, function (row, i) { + var val = '', + j = row.length; + while (j--) { + val = row[j]; + if (typeof val === 'string') { + val = '"' + val + '"'; + } + if (typeof val === 'number') { + if (decimalPoint !== '.') { + val = val.toString().replace('.', decimalPoint); + } + } + row[j] = val; + } + // Add the values + csv += row.join(itemDelimiter); + + // Add the line delimiter + if (i < rows.length - 1) { + csv += lineDelimiter; + } + }); + return csv; }; /** @@ -518,179 +518,179 @@ Highcharts.Chart.prototype.getCSV = function (useLocalDecimalPoint) { * data. * * @sample highcharts/export-data/viewdata/ - * View the data from the export menu + * View the data from the export menu * @returns {String} - * HTML representation of the data. + * HTML representation of the data. */ Highcharts.Chart.prototype.getTable = function (useLocalDecimalPoint) { - var html = '', - options = this.options, - decimalPoint = useLocalDecimalPoint ? (1.1).toLocaleString()[1] : '.', - useMultiLevelHeaders = pick( - options.exporting.useMultiLevelHeaders, true - ), - rows = this.getDataRows(useMultiLevelHeaders), - rowLength = 0, - topHeaders = useMultiLevelHeaders ? rows.shift() : null, - subHeaders = rows.shift(), - // Compare two rows for equality - isRowEqual = function (row1, row2) { - var i = row1.length; - if (row2.length === i) { - while (i--) { - if (row1[i] !== row2[i]) { - return false; - } - } - } else { - return false; - } - return true; - }, - // Get table cell HTML from value - getCellHTMLFromValue = function (tag, classes, attrs, value) { - var val = pick(value, ''), - className = 'text' + (classes ? ' ' + classes : ''); - // Convert to string if number - if (typeof val === 'number') { - val = val.toString(); - if (decimalPoint === ',') { - val = val.replace('.', decimalPoint); - } - className = 'number'; - } else if (!value) { - className = 'empty'; - } - return '<' + tag + (attrs ? ' ' + attrs : '') + - ' class="' + className + '">' + - val + ''; - }, - // Get table header markup from row data - getTableHeaderHTML = function (topheaders, subheaders, rowLength) { - var html = '', - i = 0, - len = rowLength || subheaders && subheaders.length, - next, - cur, - curColspan = 0, - rowspan; - // Clean up multiple table headers. Chart.getDataRows() returns two - // levels of headers when using multilevel, not merged. We need to - // merge identical headers, remove redundant headers, and keep it - // all marked up nicely. - if ( - useMultiLevelHeaders && - topheaders && - subheaders && - !isRowEqual(topheaders, subheaders) - ) { - html += ''; - for (; i < len; ++i) { - cur = topheaders[i]; - next = topheaders[i + 1]; - if (cur === next) { - ++curColspan; - } else if (curColspan) { - // Ended colspan - // Add cur to HTML with colspan. - html += getCellHTMLFromValue( - 'th', - 'highcharts-table-topheading', - 'scope="col" ' + - 'colspan="' + (curColspan + 1) + '"', - cur - ); - curColspan = 0; - } else { - // Cur is standalone. If it is same as sublevel, - // remove sublevel and add just toplevel. - if (cur === subheaders[i]) { - if (options.exporting.useRowspanHeaders) { - rowspan = 2; - delete subheaders[i]; - } else { - rowspan = 1; - subheaders[i] = ''; - } - } else { - rowspan = 1; - } - html += getCellHTMLFromValue( - 'th', - 'highcharts-table-topheading', - 'scope="col"' + - (rowspan > 1 ? - ' valign="top" rowspan="' + rowspan + '"' : - ''), - cur - ); - } - } - html += ''; - } - - // Add the subheaders (the only headers if not using multilevels) - if (subheaders) { - html += ''; - for (i = 0, len = subheaders.length; i < len; ++i) { - if (subheaders[i] !== undefined) { - html += getCellHTMLFromValue( - 'th', null, 'scope="col"', subheaders[i] - ); - } - } - html += ''; - } - html += ''; - return html; - }; - - // Add table caption - if (options.exporting.tableCaption !== false) { - html += ''; - } - - // Find longest row - for (var i = 0, len = rows.length; i < len; ++i) { - if (rows[i].length > rowLength) { - rowLength = rows[i].length; - } - } - - // Add header - html += getTableHeaderHTML( - topHeaders, - subHeaders, - Math.max(rowLength, subHeaders.length) - ); - - // Transform the rows to HTML - html += ''; - each(rows, function (row) { - html += ''; - for (var j = 0; j < rowLength; j++) { - // Make first column a header too. Especially important for - // category axes, but also might make sense for datetime? Should - // await user feedback on this. - html += getCellHTMLFromValue( - j ? 'td' : 'th', - null, - j ? '' : 'scope="row"', - row[j] - ); - } - html += ''; - }); - html += '
' + pick( - options.exporting.tableCaption, - ( - options.title.text ? - htmlencode(options.title.text) : - 'Chart' - )) + - '
'; - - return html; + var html = '', + options = this.options, + decimalPoint = useLocalDecimalPoint ? (1.1).toLocaleString()[1] : '.', + useMultiLevelHeaders = pick( + options.exporting.useMultiLevelHeaders, true + ), + rows = this.getDataRows(useMultiLevelHeaders), + rowLength = 0, + topHeaders = useMultiLevelHeaders ? rows.shift() : null, + subHeaders = rows.shift(), + // Compare two rows for equality + isRowEqual = function (row1, row2) { + var i = row1.length; + if (row2.length === i) { + while (i--) { + if (row1[i] !== row2[i]) { + return false; + } + } + } else { + return false; + } + return true; + }, + // Get table cell HTML from value + getCellHTMLFromValue = function (tag, classes, attrs, value) { + var val = pick(value, ''), + className = 'text' + (classes ? ' ' + classes : ''); + // Convert to string if number + if (typeof val === 'number') { + val = val.toString(); + if (decimalPoint === ',') { + val = val.replace('.', decimalPoint); + } + className = 'number'; + } else if (!value) { + className = 'empty'; + } + return '<' + tag + (attrs ? ' ' + attrs : '') + + ' class="' + className + '">' + + val + ''; + }, + // Get table header markup from row data + getTableHeaderHTML = function (topheaders, subheaders, rowLength) { + var html = '', + i = 0, + len = rowLength || subheaders && subheaders.length, + next, + cur, + curColspan = 0, + rowspan; + // Clean up multiple table headers. Chart.getDataRows() returns two + // levels of headers when using multilevel, not merged. We need to + // merge identical headers, remove redundant headers, and keep it + // all marked up nicely. + if ( + useMultiLevelHeaders && + topheaders && + subheaders && + !isRowEqual(topheaders, subheaders) + ) { + html += ''; + for (; i < len; ++i) { + cur = topheaders[i]; + next = topheaders[i + 1]; + if (cur === next) { + ++curColspan; + } else if (curColspan) { + // Ended colspan + // Add cur to HTML with colspan. + html += getCellHTMLFromValue( + 'th', + 'highcharts-table-topheading', + 'scope="col" ' + + 'colspan="' + (curColspan + 1) + '"', + cur + ); + curColspan = 0; + } else { + // Cur is standalone. If it is same as sublevel, + // remove sublevel and add just toplevel. + if (cur === subheaders[i]) { + if (options.exporting.useRowspanHeaders) { + rowspan = 2; + delete subheaders[i]; + } else { + rowspan = 1; + subheaders[i] = ''; + } + } else { + rowspan = 1; + } + html += getCellHTMLFromValue( + 'th', + 'highcharts-table-topheading', + 'scope="col"' + + (rowspan > 1 ? + ' valign="top" rowspan="' + rowspan + '"' : + ''), + cur + ); + } + } + html += ''; + } + + // Add the subheaders (the only headers if not using multilevels) + if (subheaders) { + html += ''; + for (i = 0, len = subheaders.length; i < len; ++i) { + if (subheaders[i] !== undefined) { + html += getCellHTMLFromValue( + 'th', null, 'scope="col"', subheaders[i] + ); + } + } + html += ''; + } + html += ''; + return html; + }; + + // Add table caption + if (options.exporting.tableCaption !== false) { + html += ''; + } + + // Find longest row + for (var i = 0, len = rows.length; i < len; ++i) { + if (rows[i].length > rowLength) { + rowLength = rows[i].length; + } + } + + // Add header + html += getTableHeaderHTML( + topHeaders, + subHeaders, + Math.max(rowLength, subHeaders.length) + ); + + // Transform the rows to HTML + html += ''; + each(rows, function (row) { + html += ''; + for (var j = 0; j < rowLength; j++) { + // Make first column a header too. Especially important for + // category axes, but also might make sense for datetime? Should + // await user feedback on this. + html += getCellHTMLFromValue( + j ? 'td' : 'th', + null, + j ? '' : 'scope="row"', + row[j] + ); + } + html += ''; + }); + html += '
' + pick( + options.exporting.tableCaption, + ( + options.title.text ? + htmlencode(options.title.text) : + 'Chart' + )) + + '
'; + + return html; }; /** @@ -699,39 +699,39 @@ Highcharts.Chart.prototype.getTable = function (useLocalDecimalPoint) { * @private */ Highcharts.Chart.prototype.fileDownload = function (href, extension, content) { - var a, - blobObject, - name; - - if (this.options.exporting.filename) { - name = this.options.exporting.filename; - } else if (this.title && this.title.textStr) { - name = this.title.textStr.replace(/ /g, '-').toLowerCase(); - } else { - name = 'chart'; - } - - // MS specific. Check this first because of bug with Edge (#76) - if (win.Blob && win.navigator.msSaveOrOpenBlob) { - // Falls to msSaveOrOpenBlob if download attribute is not supported - blobObject = new win.Blob( - ['\uFEFF' + content], // #7084 - { type: 'text/csv' } - ); - win.navigator.msSaveOrOpenBlob(blobObject, name + '.' + extension); - - // Download attribute supported - } else if (downloadAttrSupported) { - a = doc.createElement('a'); - a.href = href; - a.download = name + '.' + extension; - this.container.appendChild(a); // #111 - a.click(); - a.remove(); - - } else { - Highcharts.error('The browser doesn\'t support downloading files'); - } + var a, + blobObject, + name; + + if (this.options.exporting.filename) { + name = this.options.exporting.filename; + } else if (this.title && this.title.textStr) { + name = this.title.textStr.replace(/ /g, '-').toLowerCase(); + } else { + name = 'chart'; + } + + // MS specific. Check this first because of bug with Edge (#76) + if (win.Blob && win.navigator.msSaveOrOpenBlob) { + // Falls to msSaveOrOpenBlob if download attribute is not supported + blobObject = new win.Blob( + ['\uFEFF' + content], // #7084 + { type: 'text/csv' } + ); + win.navigator.msSaveOrOpenBlob(blobObject, name + '.' + extension); + + // Download attribute supported + } else if (downloadAttrSupported) { + a = doc.createElement('a'); + a.href = href; + a.download = name + '.' + extension; + this.container.appendChild(a); // #111 + a.click(); + a.remove(); + + } else { + Highcharts.error('The browser doesn\'t support downloading files'); + } }; /** @@ -740,13 +740,13 @@ Highcharts.Chart.prototype.fileDownload = function (href, extension, content) { * @private */ Highcharts.Chart.prototype.downloadCSV = function () { - var csv = this.getCSV(true); - this.fileDownload( - 'data:text/csv,\uFEFF' + encodeURIComponent(csv), - 'csv', - csv, - 'text/csv' - ); + var csv = this.getCSV(true); + this.fileDownload( + 'data:text/csv,\uFEFF' + encodeURIComponent(csv), + 'csv', + csv, + 'text/csv' + ); }; /** @@ -755,51 +755,51 @@ Highcharts.Chart.prototype.downloadCSV = function () { * @private */ Highcharts.Chart.prototype.downloadXLS = function () { - var uri = 'data:application/vnd.ms-excel;base64,', - template = '' + - '' + - '' + - '' + - '' + - '' + - this.getTable(true) + - '', - base64 = function (s) { - return win.btoa(unescape(encodeURIComponent(s))); // #50 - }; - this.fileDownload( - uri + base64(template), - 'xls', - template, - 'application/vnd.ms-excel' - ); + var uri = 'data:application/vnd.ms-excel;base64,', + template = '' + + '' + + '' + + '' + + '' + + '' + + this.getTable(true) + + '', + base64 = function (s) { + return win.btoa(unescape(encodeURIComponent(s))); // #50 + }; + this.fileDownload( + uri + base64(template), + 'xls', + template, + 'application/vnd.ms-excel' + ); }; /** * Export-data module required. View the data in a table below the chart. */ Highcharts.Chart.prototype.viewData = function () { - if (!this.dataTableDiv) { - this.dataTableDiv = doc.createElement('div'); - this.dataTableDiv.className = 'highcharts-data-table'; - - // Insert after the chart container - this.renderTo.parentNode.insertBefore( - this.dataTableDiv, - this.renderTo.nextSibling - ); - } - - this.dataTableDiv.innerHTML = this.getTable(); + if (!this.dataTableDiv) { + this.dataTableDiv = doc.createElement('div'); + this.dataTableDiv.className = 'highcharts-data-table'; + + // Insert after the chart container + this.renderTo.parentNode.insertBefore( + this.dataTableDiv, + this.renderTo.nextSibling + ); + } + + this.dataTableDiv.innerHTML = this.getTable(); }; /** @@ -818,107 +818,107 @@ Highcharts.Chart.prototype.viewData = function () { */ Highcharts.Chart.prototype.editInCloud = function () { - var options, - paramObj, - params; - - // Recursively remove function callbacks - function removeFunctions(ob) { - Object.keys(ob).forEach(function (key) { - if (typeof ob[key] === 'function') { - delete ob[key]; - } - if (Highcharts.isObject(ob[key])) { // object and not an array - removeFunctions(ob[key]); - } - }); - } - - function openInCloud(data, direct) { - // Open new tab - var a = doc.createElement('a'); - a.href = 'https://cloud.highcharts.com/create?' + - (direct ? 'c' : 'q') + '=' + data; - a.target = '_blank'; - doc.body.appendChild(a); - a.click(); - doc.body.removeChild(a); - } - - options = Highcharts.merge(this.userOptions); - removeFunctions(options); - paramObj = { - name: (options.title && options.title.text) || 'Chart title', - options: options, - settings: { - constructor: 'Chart', - dataProvider: { - csv: this.getCSV() - } - } - }; - - params = JSON.stringify(paramObj); - params = win.btoa(encodeURIComponent(params)); - - if (params.length < 2500) { - // We can skip the storage and just open it directly - return openInCloud(params, true); - } - - Highcharts.ajax({ - url: 'https://cloud-api.highcharts.com/openincloud', - type: 'post', - dataType: 'json', - data: paramObj, - success: function (result) { - if (result && result.ok && result.id) { - openInCloud(result.id); - } - } - }); + var options, + paramObj, + params; + + // Recursively remove function callbacks + function removeFunctions(ob) { + Object.keys(ob).forEach(function (key) { + if (typeof ob[key] === 'function') { + delete ob[key]; + } + if (Highcharts.isObject(ob[key])) { // object and not an array + removeFunctions(ob[key]); + } + }); + } + + function openInCloud(data, direct) { + // Open new tab + var a = doc.createElement('a'); + a.href = 'https://cloud.highcharts.com/create?' + + (direct ? 'c' : 'q') + '=' + data; + a.target = '_blank'; + doc.body.appendChild(a); + a.click(); + doc.body.removeChild(a); + } + + options = Highcharts.merge(this.userOptions); + removeFunctions(options); + paramObj = { + name: (options.title && options.title.text) || 'Chart title', + options: options, + settings: { + constructor: 'Chart', + dataProvider: { + csv: this.getCSV() + } + } + }; + + params = JSON.stringify(paramObj); + params = win.btoa(encodeURIComponent(params)); + + if (params.length < 2500) { + // We can skip the storage and just open it directly + return openInCloud(params, true); + } + + Highcharts.ajax({ + url: 'https://cloud-api.highcharts.com/openincloud', + type: 'post', + dataType: 'json', + data: paramObj, + success: function (result) { + if (result && result.ok && result.id) { + openInCloud(result.id); + } + } + }); }; // Add "Download CSV" to the exporting menu. var exportingOptions = Highcharts.getOptions().exporting; if (exportingOptions) { - Highcharts.extend(exportingOptions.menuItemDefinitions, { - downloadCSV: { - textKey: 'downloadCSV', - onclick: function () { - this.downloadCSV(); - } - }, - downloadXLS: { - textKey: 'downloadXLS', - onclick: function () { - this.downloadXLS(); - } - }, - viewData: { - textKey: 'viewData', - onclick: function () { - this.viewData(); - } - } - }); - - exportingOptions.buttons.contextButton.menuItems.push( - 'separator', - 'downloadCSV', - 'downloadXLS', - 'viewData' - ); + Highcharts.extend(exportingOptions.menuItemDefinitions, { + downloadCSV: { + textKey: 'downloadCSV', + onclick: function () { + this.downloadCSV(); + } + }, + downloadXLS: { + textKey: 'downloadXLS', + onclick: function () { + this.downloadXLS(); + } + }, + viewData: { + textKey: 'viewData', + onclick: function () { + this.viewData(); + } + } + }); + + exportingOptions.buttons.contextButton.menuItems.push( + 'separator', + 'downloadCSV', + 'downloadXLS', + 'viewData' + ); } // Series specific if (seriesTypes.map) { - seriesTypes.map.prototype.exportKey = 'name'; + seriesTypes.map.prototype.exportKey = 'name'; } if (seriesTypes.mapbubble) { - seriesTypes.mapbubble.prototype.exportKey = 'name'; + seriesTypes.mapbubble.prototype.exportKey = 'name'; } if (seriesTypes.treemap) { - seriesTypes.treemap.prototype.exportKey = 'name'; + seriesTypes.treemap.prototype.exportKey = 'name'; } diff --git a/js/modules/exporting.src.js b/js/modules/exporting.src.js index f4ccb888907..6bba951f115 100644 --- a/js/modules/exporting.src.js +++ b/js/modules/exporting.src.js @@ -15,232 +15,232 @@ import '../parts/Chart.js'; // create shortcuts var defaultOptions = H.defaultOptions, - doc = H.doc, - Chart = H.Chart, - addEvent = H.addEvent, - removeEvent = H.removeEvent, - fireEvent = H.fireEvent, - createElement = H.createElement, - discardElement = H.discardElement, - css = H.css, - merge = H.merge, - pick = H.pick, - each = H.each, - objectEach = H.objectEach, - extend = H.extend, - isTouchDevice = H.isTouchDevice, - win = H.win, - userAgent = win.navigator.userAgent, - SVGRenderer = H.SVGRenderer, - symbols = H.Renderer.prototype.symbols, - isMSBrowser = /Edge\/|Trident\/|MSIE /.test(userAgent), - isFirefoxBrowser = /firefox/i.test(userAgent); + doc = H.doc, + Chart = H.Chart, + addEvent = H.addEvent, + removeEvent = H.removeEvent, + fireEvent = H.fireEvent, + createElement = H.createElement, + discardElement = H.discardElement, + css = H.css, + merge = H.merge, + pick = H.pick, + each = H.each, + objectEach = H.objectEach, + extend = H.extend, + isTouchDevice = H.isTouchDevice, + win = H.win, + userAgent = win.navigator.userAgent, + SVGRenderer = H.SVGRenderer, + symbols = H.Renderer.prototype.symbols, + isMSBrowser = /Edge\/|Trident\/|MSIE /.test(userAgent), + isFirefoxBrowser = /firefox/i.test(userAgent); // Add language extend(defaultOptions.lang, { - /** - * Exporting module only. The text for the menu item to print the chart. - * - * @type {String} - * @default Print chart - * @since 3.0.1 - * @apioption lang.printChart - */ - printChart: 'Print chart', - /** - * Exporting module only. The text for the PNG download menu item. - * - * @type {String} - * @default Download PNG image - * @since 2.0 - * @apioption lang.downloadPNG - */ - downloadPNG: 'Download PNG image', - /** - * Exporting module only. The text for the JPEG download menu item. - * - * @type {String} - * @default Download JPEG image - * @since 2.0 - * @apioption lang.downloadJPEG - */ - downloadJPEG: 'Download JPEG image', - /** - * Exporting module only. The text for the PDF download menu item. - * - * @type {String} - * @default Download PDF document - * @since 2.0 - * @apioption lang.downloadPDF - */ - downloadPDF: 'Download PDF document', - /** - * Exporting module only. The text for the SVG download menu item. - * - * @type {String} - * @default Download SVG vector image - * @since 2.0 - * @apioption lang.downloadSVG - */ - downloadSVG: 'Download SVG vector image', - /** - * Exporting module menu. The tooltip title for the context menu holding - * print and export menu items. - * - * @type {String} - * @default Chart context menu - * @since 3.0 - * @apioption lang.contextButtonTitle - */ - contextButtonTitle: 'Chart context menu' + /** + * Exporting module only. The text for the menu item to print the chart. + * + * @type {String} + * @default Print chart + * @since 3.0.1 + * @apioption lang.printChart + */ + printChart: 'Print chart', + /** + * Exporting module only. The text for the PNG download menu item. + * + * @type {String} + * @default Download PNG image + * @since 2.0 + * @apioption lang.downloadPNG + */ + downloadPNG: 'Download PNG image', + /** + * Exporting module only. The text for the JPEG download menu item. + * + * @type {String} + * @default Download JPEG image + * @since 2.0 + * @apioption lang.downloadJPEG + */ + downloadJPEG: 'Download JPEG image', + /** + * Exporting module only. The text for the PDF download menu item. + * + * @type {String} + * @default Download PDF document + * @since 2.0 + * @apioption lang.downloadPDF + */ + downloadPDF: 'Download PDF document', + /** + * Exporting module only. The text for the SVG download menu item. + * + * @type {String} + * @default Download SVG vector image + * @since 2.0 + * @apioption lang.downloadSVG + */ + downloadSVG: 'Download SVG vector image', + /** + * Exporting module menu. The tooltip title for the context menu holding + * print and export menu items. + * + * @type {String} + * @default Chart context menu + * @since 3.0 + * @apioption lang.contextButtonTitle + */ + contextButtonTitle: 'Chart context menu' }); // Buttons and menus are collected in a separate config option set called // 'navigation'. This can be extended later to add control buttons like zoom and // pan right click menus. defaultOptions.navigation = { - buttonOptions: { - theme: {}, - - /** - * Whether to enable buttons. - * - * @type {Boolean} - * @sample highcharts/navigation/buttonoptions-enabled/ - * Exporting module loaded but buttons disabled - * @default true - * @since 2.0 - * @apioption navigation.buttonOptions.enabled - */ - - /** - * The pixel size of the symbol on the button. - * - * @type {Number} - * @sample highcharts/navigation/buttonoptions-height/ - * Bigger buttons - * @default 14 - * @since 2.0 - * @apioption navigation.buttonOptions.symbolSize - */ - symbolSize: 14, - - /** - * The x position of the center of the symbol inside the button. - * - * @type {Number} - * @sample highcharts/navigation/buttonoptions-height/ - * Bigger buttons - * @default 12.5 - * @since 2.0 - * @apioption navigation.buttonOptions.symbolX - */ - symbolX: 12.5, - - /** - * The y position of the center of the symbol inside the button. - * - * @type {Number} - * @sample highcharts/navigation/buttonoptions-height/ - * Bigger buttons - * @default 10.5 - * @since 2.0 - * @apioption navigation.buttonOptions.symbolY - */ - symbolY: 10.5, - - /** - * Alignment for the buttons. - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @sample highcharts/navigation/buttonoptions-align/ - * Center aligned - * @default right - * @since 2.0 - * @apioption navigation.buttonOptions.align - */ - align: 'right', - - /** - * The pixel spacing between buttons. - * - * @type {Number} - * @default 3 - * @since 2.0 - * @apioption navigation.buttonOptions.buttonSpacing - */ - buttonSpacing: 3, - - /** - * Pixel height of the buttons. - * - * @type {Number} - * @sample highcharts/navigation/buttonoptions-height/ - * Bigger buttons - * @default 22 - * @since 2.0 - * @apioption navigation.buttonOptions.height - */ - height: 22, - - /** - * A text string to add to the individual button. - * - * @type {String} - * @sample highcharts/exporting/buttons-text/ - * Full text button - * @sample highcharts/exporting/buttons-text-symbol/ - * Combined symbol and text - * @default null - * @since 3.0 - * @apioption navigation.buttonOptions.text - */ - - /** - * The vertical offset of the button's position relative to its - * `verticalAlign`. - * - * @type {Number} - * @sample highcharts/navigation/buttonoptions-verticalalign/ - * Buttons at lower right - * @default 0 - * @since 2.0 - * @apioption navigation.buttonOptions.y - */ - - /** - * The vertical alignment of the buttons. Can be one of "top", "middle" - * or "bottom". - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @sample highcharts/navigation/buttonoptions-verticalalign/ - * Buttons at lower right - * @default top - * @since 2.0 - * @apioption navigation.buttonOptions.verticalAlign - */ - verticalAlign: 'top', - - /** - * The pixel width of the button. - * - * @type {Number} - * @sample highcharts/navigation/buttonoptions-height/ - * Bigger buttons - * @default 24 - * @since 2.0 - * @apioption navigation.buttonOptions.width - */ - width: 24 - } + buttonOptions: { + theme: {}, + + /** + * Whether to enable buttons. + * + * @type {Boolean} + * @sample highcharts/navigation/buttonoptions-enabled/ + * Exporting module loaded but buttons disabled + * @default true + * @since 2.0 + * @apioption navigation.buttonOptions.enabled + */ + + /** + * The pixel size of the symbol on the button. + * + * @type {Number} + * @sample highcharts/navigation/buttonoptions-height/ + * Bigger buttons + * @default 14 + * @since 2.0 + * @apioption navigation.buttonOptions.symbolSize + */ + symbolSize: 14, + + /** + * The x position of the center of the symbol inside the button. + * + * @type {Number} + * @sample highcharts/navigation/buttonoptions-height/ + * Bigger buttons + * @default 12.5 + * @since 2.0 + * @apioption navigation.buttonOptions.symbolX + */ + symbolX: 12.5, + + /** + * The y position of the center of the symbol inside the button. + * + * @type {Number} + * @sample highcharts/navigation/buttonoptions-height/ + * Bigger buttons + * @default 10.5 + * @since 2.0 + * @apioption navigation.buttonOptions.symbolY + */ + symbolY: 10.5, + + /** + * Alignment for the buttons. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample highcharts/navigation/buttonoptions-align/ + * Center aligned + * @default right + * @since 2.0 + * @apioption navigation.buttonOptions.align + */ + align: 'right', + + /** + * The pixel spacing between buttons. + * + * @type {Number} + * @default 3 + * @since 2.0 + * @apioption navigation.buttonOptions.buttonSpacing + */ + buttonSpacing: 3, + + /** + * Pixel height of the buttons. + * + * @type {Number} + * @sample highcharts/navigation/buttonoptions-height/ + * Bigger buttons + * @default 22 + * @since 2.0 + * @apioption navigation.buttonOptions.height + */ + height: 22, + + /** + * A text string to add to the individual button. + * + * @type {String} + * @sample highcharts/exporting/buttons-text/ + * Full text button + * @sample highcharts/exporting/buttons-text-symbol/ + * Combined symbol and text + * @default null + * @since 3.0 + * @apioption navigation.buttonOptions.text + */ + + /** + * The vertical offset of the button's position relative to its + * `verticalAlign`. + * + * @type {Number} + * @sample highcharts/navigation/buttonoptions-verticalalign/ + * Buttons at lower right + * @default 0 + * @since 2.0 + * @apioption navigation.buttonOptions.y + */ + + /** + * The vertical alignment of the buttons. Can be one of "top", "middle" + * or "bottom". + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @sample highcharts/navigation/buttonoptions-verticalalign/ + * Buttons at lower right + * @default top + * @since 2.0 + * @apioption navigation.buttonOptions.verticalAlign + */ + verticalAlign: 'top', + + /** + * The pixel width of the button. + * + * @type {Number} + * @sample highcharts/navigation/buttonoptions-height/ + * Bigger buttons + * @default 24 + * @since 2.0 + * @apioption navigation.buttonOptions.width + */ + width: 24 + } }; /*= if (build.classic) { =*/ // Presentational attributes -merge(true, defaultOptions.navigation, +merge(true, defaultOptions.navigation, /** * A collection of options for buttons and menus appearing in the exporting * module. @@ -249,136 +249,136 @@ merge(true, defaultOptions.navigation, */ { - /** - * CSS styles for the popup menu appearing by default when the export - * icon is clicked. This menu is rendered in HTML. - * - * @type {CSSObject} - * @see In styled mode, the menu is styled with the `.highcharts-menu` - * class. - * @sample highcharts/navigation/menustyle/ Light gray menu background - * @default { "border": "1px solid #999999", "background": "#ffffff", "padding": "5px 0" } - * @since 2.0 - */ - menuStyle: { - border: '1px solid ${palette.neutralColor40}', - background: '${palette.backgroundColor}', - padding: '5px 0' - }, - - /** - * CSS styles for the individual items within the popup menu appearing - * by default when the export icon is clicked. The menu items are rendered - * in HTML. - * - * @type {CSSObject} - * @see In styled mode, the menu items are styled with the - * `.highcharts-menu-item` class. - * @sample {highcharts} highcharts/navigation/menuitemstyle/ - * Add a grey stripe to the left - * @default { "padding": "0.5em 1em", "color": "#333333", "background": "none" } - * @since 2.0 - */ - menuItemStyle: { - padding: '0.5em 1em', - background: 'none', - color: '${palette.neutralColor80}', - /** - * Defaults to `14px` on touch devices and `11px` on desktop. - * @type {String} - */ - fontSize: isTouchDevice ? '14px' : '11px', - transition: 'background 250ms, color 250ms' - }, - - /** - * CSS styles for the hover state of the individual items within the - * popup menu appearing by default when the export icon is clicked. - * The menu items are rendered in HTML. - * - * @type {CSSObject} - * @see In styled mode, the menu items are styled with the - * `.highcharts-menu-item` class. - * @sample highcharts/navigation/menuitemhoverstyle/ Bold text on hover - * @default { "background": "#335cad", "color": "#ffffff" } - * @since 2.0 - */ - menuItemHoverStyle: { - background: '${palette.highlightColor80}', - color: '${palette.backgroundColor}' - }, - - /** - * A collection of options for buttons appearing in the exporting module. - * - * - * In styled mode, the buttons are styled with the - * `.highcharts-contextbutton` and `.highcharts-button-symbol` classes. - * - */ - buttonOptions: { - - /** - * Fill color for the symbol within the button. - * - * @type {Color} - * @sample highcharts/navigation/buttonoptions-symbolfill/ - * Blue symbol stroke for one of the buttons - * @default #666666 - * @since 2.0 - */ - symbolFill: '${palette.neutralColor60}', - - /** - * The color of the symbol's stroke or line. - * - * @type {Color} - * @sample highcharts/navigation/buttonoptions-symbolstroke/ - * Blue symbol stroke - * @default #666666 - * @since 2.0 - */ - symbolStroke: '${palette.neutralColor60}', - - /** - * The pixel stroke width of the symbol on the button. - * - * @type {Number} - * @sample highcharts/navigation/buttonoptions-height/ - * Bigger buttons - * @default 1 - * @since 2.0 - */ - symbolStrokeWidth: 3, - - /** - * A configuration object for the button theme. The object accepts - * SVG properties like `stroke-width`, `stroke` and `fill`. Tri-state - * button styles are supported by the `states.hover` and `states.select` - * objects. - * - * @type {Object} - * @sample highcharts/navigation/buttonoptions-theme/ - * Theming the buttons - * @since 3.0 - */ - theme: { - /** - * The default fill exists only to capture hover events. - * @type {String} - */ - fill: '${palette.backgroundColor}', - /** - * @type {String} - */ - stroke: 'none', - /** - * @type {Number} - * @default 5 - */ - padding: 5 - } - } + /** + * CSS styles for the popup menu appearing by default when the export + * icon is clicked. This menu is rendered in HTML. + * + * @type {CSSObject} + * @see In styled mode, the menu is styled with the `.highcharts-menu` + * class. + * @sample highcharts/navigation/menustyle/ Light gray menu background + * @default { "border": "1px solid #999999", "background": "#ffffff", "padding": "5px 0" } + * @since 2.0 + */ + menuStyle: { + border: '1px solid ${palette.neutralColor40}', + background: '${palette.backgroundColor}', + padding: '5px 0' + }, + + /** + * CSS styles for the individual items within the popup menu appearing + * by default when the export icon is clicked. The menu items are rendered + * in HTML. + * + * @type {CSSObject} + * @see In styled mode, the menu items are styled with the + * `.highcharts-menu-item` class. + * @sample {highcharts} highcharts/navigation/menuitemstyle/ + * Add a grey stripe to the left + * @default { "padding": "0.5em 1em", "color": "#333333", "background": "none" } + * @since 2.0 + */ + menuItemStyle: { + padding: '0.5em 1em', + background: 'none', + color: '${palette.neutralColor80}', + /** + * Defaults to `14px` on touch devices and `11px` on desktop. + * @type {String} + */ + fontSize: isTouchDevice ? '14px' : '11px', + transition: 'background 250ms, color 250ms' + }, + + /** + * CSS styles for the hover state of the individual items within the + * popup menu appearing by default when the export icon is clicked. + * The menu items are rendered in HTML. + * + * @type {CSSObject} + * @see In styled mode, the menu items are styled with the + * `.highcharts-menu-item` class. + * @sample highcharts/navigation/menuitemhoverstyle/ Bold text on hover + * @default { "background": "#335cad", "color": "#ffffff" } + * @since 2.0 + */ + menuItemHoverStyle: { + background: '${palette.highlightColor80}', + color: '${palette.backgroundColor}' + }, + + /** + * A collection of options for buttons appearing in the exporting module. + * + * + * In styled mode, the buttons are styled with the + * `.highcharts-contextbutton` and `.highcharts-button-symbol` classes. + * + */ + buttonOptions: { + + /** + * Fill color for the symbol within the button. + * + * @type {Color} + * @sample highcharts/navigation/buttonoptions-symbolfill/ + * Blue symbol stroke for one of the buttons + * @default #666666 + * @since 2.0 + */ + symbolFill: '${palette.neutralColor60}', + + /** + * The color of the symbol's stroke or line. + * + * @type {Color} + * @sample highcharts/navigation/buttonoptions-symbolstroke/ + * Blue symbol stroke + * @default #666666 + * @since 2.0 + */ + symbolStroke: '${palette.neutralColor60}', + + /** + * The pixel stroke width of the symbol on the button. + * + * @type {Number} + * @sample highcharts/navigation/buttonoptions-height/ + * Bigger buttons + * @default 1 + * @since 2.0 + */ + symbolStrokeWidth: 3, + + /** + * A configuration object for the button theme. The object accepts + * SVG properties like `stroke-width`, `stroke` and `fill`. Tri-state + * button styles are supported by the `states.hover` and `states.select` + * objects. + * + * @type {Object} + * @sample highcharts/navigation/buttonoptions-theme/ + * Theming the buttons + * @since 3.0 + */ + theme: { + /** + * The default fill exists only to capture hover events. + * @type {String} + */ + fill: '${palette.backgroundColor}', + /** + * @type {String} + */ + stroke: 'none', + /** + * @type {Number} + * @default 5 + */ + padding: 5 + } + } }); /*= } =*/ @@ -393,424 +393,424 @@ merge(true, defaultOptions.navigation, */ defaultOptions.exporting = { - /** - * Experimental setting to allow HTML inside the chart (added through - * the `useHTML` options), directly in the exported image. This allows - * you to preserve complicated HTML structures like tables or bi-directional - * text in exported charts. - * - * Disclaimer: The HTML is rendered in a `foreignObject` tag in the - * generated SVG. The official export server is based on PhantomJS, - * which supports this, but other SVG clients, like Batik, does not - * support it. This also applies to downloaded SVG that you want to - * open in a desktop client. - * - * @type {Boolean} - * @default false - * @since 4.1.8 - * @apioption exporting.allowHTML - */ - - /** - * Additional chart options to be merged into an exported chart. For - * example, a common use case is to add data labels to improve readability - * of the exported chart, or to add a printer-friendly color scheme. - * - * @type {Object} - * @sample {highcharts} highcharts/exporting/chartoptions-data-labels/ - * Added data labels - * @sample {highstock} highcharts/exporting/chartoptions-data-labels/ - * Added data labels - * @default null - * @apioption exporting.chartOptions - */ - - /** - * Whether to enable the exporting module. Disabling the module will - * hide the context button, but API methods will still be available. - * - * @type {Boolean} - * @sample {highcharts} highcharts/exporting/enabled-false/ - * Exporting module is loaded but disabled - * @sample {highstock} highcharts/exporting/enabled-false/ - * Exporting module is loaded but disabled - * @default true - * @since 2.0 - * @apioption exporting.enabled - */ - - /** - * Function to call if the offline-exporting module fails to export - * a chart on the client side, and [fallbackToExportServer]( - * #exporting.fallbackToExportServer) is disabled. If left undefined, an - * exception is thrown instead. - * - * @type {Function} - * @see [fallbackToExportServer](#exporting.fallbackToExportServer) - * @default undefined - * @since 5.0.0 - * @apioption exporting.error - */ - - /** - * Whether or not to fall back to the export server if the offline-exporting - * module is unable to export the chart on the client side. - * - * @type {Boolean} - * @default true - * @since 4.1.8 - * @apioption exporting.fallbackToExportServer - */ - - /** - * The filename, without extension, to use for the exported chart. - * - * @type {String} - * @sample {highcharts} highcharts/exporting/filename/ Custom file name - * @sample {highstock} highcharts/exporting/filename/ Custom file name - * @default chart - * @since 2.0 - * @apioption exporting.filename - */ - - /** - * An object containing additional attributes for the POST form that - * sends the SVG to the export server. For example, a `target` can be - * set to make sure the generated image is received in another frame, - * or a custom `enctype` or `encoding` can be set. - * - * @type {Object} - * @since 3.0.8 - * @apioption exporting.formAttributes - */ - - /** - * Path where Highcharts will look for export module dependencies to - * load on demand if they don't already exist on `window`. Should currently - * point to location of [CanVG](https://github.com/canvg/canvg) library, - * [RGBColor.js](https://github.com/canvg/canvg), [jsPDF](https://github. - * com/yWorks/jsPDF) and [svg2pdf.js](https://github.com/yWorks/svg2pdf. - * js), required for client side export in certain browsers. - * - * @type {String} - * @default https://code.highcharts.com/{version}/lib - * @since 5.0.0 - * @apioption exporting.libURL - */ - - /** - * Analogous to [sourceWidth](#exporting.sourceWidth). - * - * @type {Number} - * @since 3.0 - * @apioption exporting.sourceHeight - */ - - /** - * The width of the original chart when exported, unless an explicit - * [chart.width](#chart.width) is set. The width exported raster image - * is then multiplied by [scale](#exporting.scale). - * - * @type {Number} - * @sample {highcharts} highcharts/exporting/sourcewidth/ Source size demo - * @sample {highstock} highcharts/exporting/sourcewidth/ Source size demo - * @sample {highmaps} maps/exporting/sourcewidth/ Source size demo - * @since 3.0 - * @apioption exporting.sourceWidth - */ - - /** - * The pixel width of charts exported to PNG or JPG. As of Highcharts - * 3.0, the default pixel width is a function of the [chart.width]( - * #chart.width) or [exporting.sourceWidth](#exporting.sourceWidth) and the - * [exporting.scale](#exporting.scale). - * - * @type {Number} - * @sample {highcharts} highcharts/exporting/width/ - * Export to 200px wide images - * @sample {highstock} highcharts/exporting/width/ - * Export to 200px wide images - * @default undefined - * @since 2.0 - * @apioption exporting.width - */ - - /** - * Default MIME type for exporting if `chart.exportChart()` is called - * without specifying a `type` option. Possible values are `image/png`, - * `image/jpeg`, `application/pdf` and `image/svg+xml`. - * - * @validvalue ["image/png", "image/jpeg", "application/pdf", "image/svg+xml"] - * @since 2.0 - */ - type: 'image/png', - - /** - * The URL for the server module converting the SVG string to an image - * format. By default this points to Highchart's free web service. - * - * @type {String} - * @default https://export.highcharts.com - * @since 2.0 - */ - url: 'https://export.highcharts.com/', - /** - * When printing the chart from the menu item in the burger menu, if - * the on-screen chart exceeds this width, it is resized. After printing - * or cancelled, it is restored. The default width makes the chart - * fit into typical paper format. Note that this does not affect the - * chart when printing the web page as a whole. - * - * @type {Number} - * @default 780 - * @since 4.2.5 - */ - printMaxWidth: 780, - - /** - * Defines the scale or zoom factor for the exported image compared - * to the on-screen display. While for instance a 600px wide chart - * may look good on a website, it will look bad in print. The default - * scale of 2 makes this chart export to a 1200px PNG or JPG. - * - * @see [chart.width](#chart.width), - * [exporting.sourceWidth](#exporting.sourceWidth) - * @sample {highcharts} highcharts/exporting/scale/ Scale demonstrated - * @sample {highstock} highcharts/exporting/scale/ Scale demonstrated - * @sample {highmaps} maps/exporting/scale/ Scale demonstrated - * @since 3.0 - */ - scale: 2, - - /** - * Options for the export related buttons, print and export. In addition - * to the default buttons listed here, custom buttons can be added. - * See [navigation.buttonOptions](#navigation.buttonOptions) for general - * options. - * - */ - buttons: { - - /** - * Options for the export button. - * - * In styled mode, export button styles can be applied with the - * `.highcharts-contextbutton` class. - * - * @extends navigation.buttonOptions - */ - contextButton: { - - /** - * A click handler callback to use on the button directly instead of - * the popup menu. - * - * @type {Function} - * @sample highcharts/exporting/buttons-contextbutton-onclick/ - * Skip the menu and export the chart directly - * @since 2.0 - * @apioption exporting.buttons.contextButton.onclick - */ - - /** - * See [navigation.buttonOptions.symbolFill]( - * #navigation.buttonOptions.symbolFill). - * - * @type {Color} - * @default #666666 - * @since 2.0 - * @apioption exporting.buttons.contextButton.symbolFill - */ - - /** - * The horizontal position of the button relative to the `align` - * option. - * - * @type {Number} - * @default -10 - * @since 2.0 - * @apioption exporting.buttons.contextButton.x - */ - - /** - * The class name of the context button. - * @type {String} - */ - className: 'highcharts-contextbutton', - - /** - * The class name of the menu appearing from the button. - * @type {String} - */ - menuClassName: 'highcharts-contextmenu', - - /** - * The symbol for the button. Points to a definition function in - * the `Highcharts.Renderer.symbols` collection. The default - * `exportIcon` function is part of the exporting module. - * - * @validvalue ["circle", "square", "diamond", "triangle", "triangle-down", "menu"] - * @type {String} - * @sample highcharts/exporting/buttons-contextbutton-symbol/ - * Use a circle for symbol - * @sample highcharts/exporting/buttons-contextbutton-symbol-custom/ - * Custom shape as symbol - * @default menu - * @since 2.0 - */ - symbol: 'menu', - - /** - * The key to a [lang](#lang) option setting that is used for the - * button's title tooltip. When the key is `contextButtonTitle`, it - * refers to [lang.contextButtonTitle](#lang.contextButtonTitle) - * that defaults to "Chart context menu". - * @type {String} - */ - _titleKey: 'contextButtonTitle', - - /** - * A collection of strings pointing to config options for the menu - * items. The config options are defined in the - * `menuItemDefinitions` option. - * - * By default, there is the "Print" menu item plus one menu item - * for each of the available export types. - * - * Defaults to - *
-			 * [
-			 *	'printChart',
-			 *	'separator',
-			 *	'downloadPNG',
-			 *	'downloadJPEG',
-			 *	'downloadPDF',
-			 *	'downloadSVG'
-			 * ]
-			 * 
- * - * @type {Array|Array} - * @sample {highcharts} highcharts/exporting/menuitemdefinitions/ - * Menu item definitions - * @sample {highstock} highcharts/exporting/menuitemdefinitions/ - * Menu item definitions - * @sample {highmaps} highcharts/exporting/menuitemdefinitions/ - * Menu item definitions - * @since 2.0 - */ - menuItems: [ - 'printChart', - 'separator', - 'downloadPNG', - 'downloadJPEG', - 'downloadPDF', - 'downloadSVG' - ] - } - }, - /** - * An object consisting of definitions for the menu items in the context - * menu. Each key value pair has a `key` that is referenced in the - * [menuItems](#exporting.buttons.contextButton.menuItems) setting, - * and a `value`, which is an object with the following properties: - * - *
- * - *
onclick
- * - *
The click handler for the menu item
- * - *
text
- * - *
The text for the menu item
- * - *
textKey
- * - *
If internationalization is required, the key to a language string - *
- * - *
- * - * @type {Object} - * @sample {highcharts} highcharts/exporting/menuitemdefinitions/ - * Menu item definitions - * @sample {highstock} highcharts/exporting/menuitemdefinitions/ - * Menu item definitions - * @sample {highmaps} highcharts/exporting/menuitemdefinitions/ - * Menu item definitions - * @since 5.0.13 - */ - menuItemDefinitions: { - - /** - * @ignore - */ - printChart: { - textKey: 'printChart', - onclick: function () { - this.print(); - } - }, - - /** - * @ignore - */ - separator: { - separator: true - }, - - /** - * @ignore - */ - downloadPNG: { - textKey: 'downloadPNG', - onclick: function () { - this.exportChart(); - } - }, - - /** - * @ignore - */ - downloadJPEG: { - textKey: 'downloadJPEG', - onclick: function () { - this.exportChart({ - type: 'image/jpeg' - }); - } - }, - - /** - * @ignore - */ - downloadPDF: { - textKey: 'downloadPDF', - onclick: function () { - this.exportChart({ - type: 'application/pdf' - }); - } - }, - - /** - * @ignore - */ - downloadSVG: { - textKey: 'downloadSVG', - onclick: function () { - this.exportChart({ - type: 'image/svg+xml' - }); - } - } - } + /** + * Experimental setting to allow HTML inside the chart (added through + * the `useHTML` options), directly in the exported image. This allows + * you to preserve complicated HTML structures like tables or bi-directional + * text in exported charts. + * + * Disclaimer: The HTML is rendered in a `foreignObject` tag in the + * generated SVG. The official export server is based on PhantomJS, + * which supports this, but other SVG clients, like Batik, does not + * support it. This also applies to downloaded SVG that you want to + * open in a desktop client. + * + * @type {Boolean} + * @default false + * @since 4.1.8 + * @apioption exporting.allowHTML + */ + + /** + * Additional chart options to be merged into an exported chart. For + * example, a common use case is to add data labels to improve readability + * of the exported chart, or to add a printer-friendly color scheme. + * + * @type {Object} + * @sample {highcharts} highcharts/exporting/chartoptions-data-labels/ + * Added data labels + * @sample {highstock} highcharts/exporting/chartoptions-data-labels/ + * Added data labels + * @default null + * @apioption exporting.chartOptions + */ + + /** + * Whether to enable the exporting module. Disabling the module will + * hide the context button, but API methods will still be available. + * + * @type {Boolean} + * @sample {highcharts} highcharts/exporting/enabled-false/ + * Exporting module is loaded but disabled + * @sample {highstock} highcharts/exporting/enabled-false/ + * Exporting module is loaded but disabled + * @default true + * @since 2.0 + * @apioption exporting.enabled + */ + + /** + * Function to call if the offline-exporting module fails to export + * a chart on the client side, and [fallbackToExportServer]( + * #exporting.fallbackToExportServer) is disabled. If left undefined, an + * exception is thrown instead. + * + * @type {Function} + * @see [fallbackToExportServer](#exporting.fallbackToExportServer) + * @default undefined + * @since 5.0.0 + * @apioption exporting.error + */ + + /** + * Whether or not to fall back to the export server if the offline-exporting + * module is unable to export the chart on the client side. + * + * @type {Boolean} + * @default true + * @since 4.1.8 + * @apioption exporting.fallbackToExportServer + */ + + /** + * The filename, without extension, to use for the exported chart. + * + * @type {String} + * @sample {highcharts} highcharts/exporting/filename/ Custom file name + * @sample {highstock} highcharts/exporting/filename/ Custom file name + * @default chart + * @since 2.0 + * @apioption exporting.filename + */ + + /** + * An object containing additional attributes for the POST form that + * sends the SVG to the export server. For example, a `target` can be + * set to make sure the generated image is received in another frame, + * or a custom `enctype` or `encoding` can be set. + * + * @type {Object} + * @since 3.0.8 + * @apioption exporting.formAttributes + */ + + /** + * Path where Highcharts will look for export module dependencies to + * load on demand if they don't already exist on `window`. Should currently + * point to location of [CanVG](https://github.com/canvg/canvg) library, + * [RGBColor.js](https://github.com/canvg/canvg), [jsPDF](https://github. + * com/yWorks/jsPDF) and [svg2pdf.js](https://github.com/yWorks/svg2pdf. + * js), required for client side export in certain browsers. + * + * @type {String} + * @default https://code.highcharts.com/{version}/lib + * @since 5.0.0 + * @apioption exporting.libURL + */ + + /** + * Analogous to [sourceWidth](#exporting.sourceWidth). + * + * @type {Number} + * @since 3.0 + * @apioption exporting.sourceHeight + */ + + /** + * The width of the original chart when exported, unless an explicit + * [chart.width](#chart.width) is set. The width exported raster image + * is then multiplied by [scale](#exporting.scale). + * + * @type {Number} + * @sample {highcharts} highcharts/exporting/sourcewidth/ Source size demo + * @sample {highstock} highcharts/exporting/sourcewidth/ Source size demo + * @sample {highmaps} maps/exporting/sourcewidth/ Source size demo + * @since 3.0 + * @apioption exporting.sourceWidth + */ + + /** + * The pixel width of charts exported to PNG or JPG. As of Highcharts + * 3.0, the default pixel width is a function of the [chart.width]( + * #chart.width) or [exporting.sourceWidth](#exporting.sourceWidth) and the + * [exporting.scale](#exporting.scale). + * + * @type {Number} + * @sample {highcharts} highcharts/exporting/width/ + * Export to 200px wide images + * @sample {highstock} highcharts/exporting/width/ + * Export to 200px wide images + * @default undefined + * @since 2.0 + * @apioption exporting.width + */ + + /** + * Default MIME type for exporting if `chart.exportChart()` is called + * without specifying a `type` option. Possible values are `image/png`, + * `image/jpeg`, `application/pdf` and `image/svg+xml`. + * + * @validvalue ["image/png", "image/jpeg", "application/pdf", "image/svg+xml"] + * @since 2.0 + */ + type: 'image/png', + + /** + * The URL for the server module converting the SVG string to an image + * format. By default this points to Highchart's free web service. + * + * @type {String} + * @default https://export.highcharts.com + * @since 2.0 + */ + url: 'https://export.highcharts.com/', + /** + * When printing the chart from the menu item in the burger menu, if + * the on-screen chart exceeds this width, it is resized. After printing + * or cancelled, it is restored. The default width makes the chart + * fit into typical paper format. Note that this does not affect the + * chart when printing the web page as a whole. + * + * @type {Number} + * @default 780 + * @since 4.2.5 + */ + printMaxWidth: 780, + + /** + * Defines the scale or zoom factor for the exported image compared + * to the on-screen display. While for instance a 600px wide chart + * may look good on a website, it will look bad in print. The default + * scale of 2 makes this chart export to a 1200px PNG or JPG. + * + * @see [chart.width](#chart.width), + * [exporting.sourceWidth](#exporting.sourceWidth) + * @sample {highcharts} highcharts/exporting/scale/ Scale demonstrated + * @sample {highstock} highcharts/exporting/scale/ Scale demonstrated + * @sample {highmaps} maps/exporting/scale/ Scale demonstrated + * @since 3.0 + */ + scale: 2, + + /** + * Options for the export related buttons, print and export. In addition + * to the default buttons listed here, custom buttons can be added. + * See [navigation.buttonOptions](#navigation.buttonOptions) for general + * options. + * + */ + buttons: { + + /** + * Options for the export button. + * + * In styled mode, export button styles can be applied with the + * `.highcharts-contextbutton` class. + * + * @extends navigation.buttonOptions + */ + contextButton: { + + /** + * A click handler callback to use on the button directly instead of + * the popup menu. + * + * @type {Function} + * @sample highcharts/exporting/buttons-contextbutton-onclick/ + * Skip the menu and export the chart directly + * @since 2.0 + * @apioption exporting.buttons.contextButton.onclick + */ + + /** + * See [navigation.buttonOptions.symbolFill]( + * #navigation.buttonOptions.symbolFill). + * + * @type {Color} + * @default #666666 + * @since 2.0 + * @apioption exporting.buttons.contextButton.symbolFill + */ + + /** + * The horizontal position of the button relative to the `align` + * option. + * + * @type {Number} + * @default -10 + * @since 2.0 + * @apioption exporting.buttons.contextButton.x + */ + + /** + * The class name of the context button. + * @type {String} + */ + className: 'highcharts-contextbutton', + + /** + * The class name of the menu appearing from the button. + * @type {String} + */ + menuClassName: 'highcharts-contextmenu', + + /** + * The symbol for the button. Points to a definition function in + * the `Highcharts.Renderer.symbols` collection. The default + * `exportIcon` function is part of the exporting module. + * + * @validvalue ["circle", "square", "diamond", "triangle", "triangle-down", "menu"] + * @type {String} + * @sample highcharts/exporting/buttons-contextbutton-symbol/ + * Use a circle for symbol + * @sample highcharts/exporting/buttons-contextbutton-symbol-custom/ + * Custom shape as symbol + * @default menu + * @since 2.0 + */ + symbol: 'menu', + + /** + * The key to a [lang](#lang) option setting that is used for the + * button's title tooltip. When the key is `contextButtonTitle`, it + * refers to [lang.contextButtonTitle](#lang.contextButtonTitle) + * that defaults to "Chart context menu". + * @type {String} + */ + _titleKey: 'contextButtonTitle', + + /** + * A collection of strings pointing to config options for the menu + * items. The config options are defined in the + * `menuItemDefinitions` option. + * + * By default, there is the "Print" menu item plus one menu item + * for each of the available export types. + * + * Defaults to + *
+             * [
+             *    'printChart',
+             *    'separator',
+             *    'downloadPNG',
+             *    'downloadJPEG',
+             *    'downloadPDF',
+             *    'downloadSVG'
+             * ]
+             * 
+ * + * @type {Array|Array} + * @sample {highcharts} highcharts/exporting/menuitemdefinitions/ + * Menu item definitions + * @sample {highstock} highcharts/exporting/menuitemdefinitions/ + * Menu item definitions + * @sample {highmaps} highcharts/exporting/menuitemdefinitions/ + * Menu item definitions + * @since 2.0 + */ + menuItems: [ + 'printChart', + 'separator', + 'downloadPNG', + 'downloadJPEG', + 'downloadPDF', + 'downloadSVG' + ] + } + }, + /** + * An object consisting of definitions for the menu items in the context + * menu. Each key value pair has a `key` that is referenced in the + * [menuItems](#exporting.buttons.contextButton.menuItems) setting, + * and a `value`, which is an object with the following properties: + * + *
+ * + *
onclick
+ * + *
The click handler for the menu item
+ * + *
text
+ * + *
The text for the menu item
+ * + *
textKey
+ * + *
If internationalization is required, the key to a language string + *
+ * + *
+ * + * @type {Object} + * @sample {highcharts} highcharts/exporting/menuitemdefinitions/ + * Menu item definitions + * @sample {highstock} highcharts/exporting/menuitemdefinitions/ + * Menu item definitions + * @sample {highmaps} highcharts/exporting/menuitemdefinitions/ + * Menu item definitions + * @since 5.0.13 + */ + menuItemDefinitions: { + + /** + * @ignore + */ + printChart: { + textKey: 'printChart', + onclick: function () { + this.print(); + } + }, + + /** + * @ignore + */ + separator: { + separator: true + }, + + /** + * @ignore + */ + downloadPNG: { + textKey: 'downloadPNG', + onclick: function () { + this.exportChart(); + } + }, + + /** + * @ignore + */ + downloadJPEG: { + textKey: 'downloadJPEG', + onclick: function () { + this.exportChart({ + type: 'image/jpeg' + }); + } + }, + + /** + * @ignore + */ + downloadPDF: { + textKey: 'downloadPDF', + onclick: function () { + this.exportChart({ + type: 'application/pdf' + }); + } + }, + + /** + * @ignore + */ + downloadSVG: { + textKey: 'downloadSVG', + onclick: function () { + this.exportChart({ + type: 'image/svg+xml' + }); + } + } + } }; /** * Fires after a chart is printed through the context menu item or the * `Chart.print` method. Requires the exporting module. - * + * * @type {Function} * @context Chart * @sample highcharts/chart/events-beforeprint-afterprint/ @@ -822,7 +822,7 @@ defaultOptions.exporting = { /** * Fires before a chart is printed through the context menu item or * the `Chart.print` method. Requires the exporting module. - * + * * @type {Function} * @context Chart * @sample highcharts/chart/events-beforeprint-afterprint/ @@ -834,756 +834,756 @@ defaultOptions.exporting = { // Add the H.post utility H.post = function (url, data, formAttributes) { - // create the form - var form = createElement('form', merge({ - method: 'post', - action: url, - enctype: 'multipart/form-data' - }, formAttributes), { - display: 'none' - }, doc.body); - - // add the data - objectEach(data, function (val, name) { - createElement('input', { - type: 'hidden', - name: name, - value: val - }, null, form); - }); - - // submit - form.submit(); - - // clean up - discardElement(form); + // create the form + var form = createElement('form', merge({ + method: 'post', + action: url, + enctype: 'multipart/form-data' + }, formAttributes), { + display: 'none' + }, doc.body); + + // add the data + objectEach(data, function (val, name) { + createElement('input', { + type: 'hidden', + name: name, + value: val + }, null, form); + }); + + // submit + form.submit(); + + // clean up + discardElement(form); }; extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { - /** - * Exporting module only. A collection of fixes on the produced SVG to - * account for expando properties, browser bugs, VML problems and other. - * Returns a cleaned SVG. - * - * @private - */ - sanitizeSVG: function (svg, options) { - // Move HTML into a foreignObject - if (options && options.exporting && options.exporting.allowHTML) { - var html = svg.match(/<\/svg>(.*?$)/); - if (html && html[1]) { - html = '' + - '' + - html[1] + - '' + - ''; - svg = svg.replace('', html + ''); - } - } - - svg = svg - .replace(/zIndex="[^"]+"/g, '') - .replace(/isShadow="[^"]+"/g, '') - .replace(/symbolName="[^"]+"/g, '') - .replace(/jQuery[0-9]+="[^"]+"/g, '') - .replace(/url\(("|")(\S+)("|")\)/g, 'url($2)') - .replace(/url\([^#]+#/g, 'url(#') - .replace( - /.*?$/, '') - // Batik doesn't support rgba fills and strokes (#3095) - .replace( - /(fill|stroke)="rgba\(([ 0-9]+,[ 0-9]+,[ 0-9]+),([ 0-9\.]+)\)"/g, // eslint-disable-line max-len - '$1="rgb($2)" $1-opacity="$3"' - ) - - // Replace HTML entities, issue #347 - .replace(/ /g, '\u00A0') // no-break space - .replace(/­/g, '\u00AD'); // soft hyphen - - /*= if (build.classic) { =*/ - // Further sanitize for oldIE - if (this.ieSanitizeSVG) { - svg = this.ieSanitizeSVG(svg); - } - /*= } =*/ - - return svg; - }, - - /** - * Return the unfiltered innerHTML of the chart container. Used as hook for - * plugins. In styled mode, it also takes care of inlining CSS style rules. - * - * @see Chart#getSVG - * - * @returns {String} - * The unfiltered SVG of the chart. - */ - getChartHTML: function () { - /*= if (!build.classic) { =*/ - this.inlineStyles(); - /*= } =*/ - return this.container.innerHTML; - }, - - /** - * Return an SVG representation of the chart. - * - * @param chartOptions {Options} - * Additional chart options for the generated SVG representation. - * For collections like `xAxis`, `yAxis` or `series`, the additional - * options is either merged in to the orininal item of the same - * `id`, or to the first item if a common id is not found. - * @return {String} - * The SVG representation of the rendered chart. - * @sample highcharts/members/chart-getsvg/ - * View the SVG from a button - */ - getSVG: function (chartOptions) { - var chart = this, - chartCopy, - sandbox, - svg, - seriesOptions, - sourceWidth, - sourceHeight, - cssWidth, - cssHeight, - // Copy the options and add extra options - options = merge(chart.options, chartOptions); - - - // create a sandbox where a new chart will be generated - sandbox = createElement('div', null, { - position: 'absolute', - top: '-9999em', - width: chart.chartWidth + 'px', - height: chart.chartHeight + 'px' - }, doc.body); - - // get the source size - cssWidth = chart.renderTo.style.width; - cssHeight = chart.renderTo.style.height; - sourceWidth = options.exporting.sourceWidth || - options.chart.width || - (/px$/.test(cssWidth) && parseInt(cssWidth, 10)) || - 600; - sourceHeight = options.exporting.sourceHeight || - options.chart.height || - (/px$/.test(cssHeight) && parseInt(cssHeight, 10)) || - 400; - - // override some options - extend(options.chart, { - animation: false, - renderTo: sandbox, - forExport: true, - renderer: 'SVGRenderer', - width: sourceWidth, - height: sourceHeight - }); - options.exporting.enabled = false; // hide buttons in print - delete options.data; // #3004 - - // prepare for replicating the chart - options.series = []; - each(chart.series, function (serie) { - seriesOptions = merge(serie.userOptions, { // #4912 - animation: false, // turn off animation - enableMouseTracking: false, - showCheckbox: false, - visible: serie.visible - }); - - // Used for the navigator series that has its own option set - if (!seriesOptions.isInternal) { - options.series.push(seriesOptions); - } - }); - - // Assign an internal key to ensure a one-to-one mapping (#5924) - each(chart.axes, function (axis) { - if (!axis.userOptions.internalKey) { // #6444 - axis.userOptions.internalKey = H.uniqueKey(); - } - }); - - // generate the chart copy - chartCopy = new H.Chart(options, chart.callback); - - // Axis options and series options (#2022, #3900, #5982) - if (chartOptions) { - each(['xAxis', 'yAxis', 'series'], function (coll) { - var collOptions = {}; - if (chartOptions[coll]) { - collOptions[coll] = chartOptions[coll]; - chartCopy.update(collOptions); - } - }); - } - - // Reflect axis extremes in the export (#5924) - each(chart.axes, function (axis) { - var axisCopy = H.find(chartCopy.axes, function (copy) { - return copy.options.internalKey === - axis.userOptions.internalKey; - }), - extremes = axis.getExtremes(), - userMin = extremes.userMin, - userMax = extremes.userMax; - - if (axisCopy && (userMin !== undefined || userMax !== undefined)) { - axisCopy.setExtremes(userMin, userMax, true, false); - } - }); - - // Get the SVG from the container's innerHTML - svg = chartCopy.getChartHTML(); - - svg = chart.sanitizeSVG(svg, options); - - // free up memory - options = null; - chartCopy.destroy(); - discardElement(sandbox); - - return svg; - }, - - getSVGForExport: function (options, chartOptions) { - var chartExportingOptions = this.options.exporting; - - return this.getSVG(merge( - { chart: { borderRadius: 0 } }, - chartExportingOptions.chartOptions, - chartOptions, - { - exporting: { - sourceWidth: ( - (options && options.sourceWidth) || - chartExportingOptions.sourceWidth - ), - sourceHeight: ( - (options && options.sourceHeight) || - chartExportingOptions.sourceHeight - ) - } - } - )); - }, - - /** - * Exporting module required. Submit an SVG version of the chart to a server - * along with some parameters for conversion. - * @param {Object} exportingOptions - * Exporting options in addition to those defined in {@link - * https://api.highcharts.com/highcharts/exporting|exporting}. - * @param {String} exportingOptions.filename - * The file name for the export without extension. - * @param {String} exportingOptions.url - * The URL for the server module to do the conversion. - * @param {Number} exportingOptions.width - * The width of the PNG or JPG image generated on the server. - * @param {String} exportingOptions.type - * The MIME type of the converted image. - * @param {Number} exportingOptions.sourceWidth - * The pixel width of the source (in-page) chart. - * @param {Number} exportingOptions.sourceHeight - * The pixel height of the source (in-page) chart. - * @param {Options} chartOptions - * Additional chart options for the exported chart. For example a - * different background color can be added here, or `dataLabels` - * for export only. - * - * @sample highcharts/members/chart-exportchart/ - * Export with no options - * @sample highcharts/members/chart-exportchart-filename/ - * PDF type and custom filename - * @sample highcharts/members/chart-exportchart-custom-background/ - * Different chart background in export - * @sample stock/members/chart-exportchart/ - * Export with Highstock - */ - exportChart: function (exportingOptions, chartOptions) { - - var svg = this.getSVGForExport(exportingOptions, chartOptions); - - // merge the options - exportingOptions = merge(this.options.exporting, exportingOptions); - - // do the post - H.post(exportingOptions.url, { - filename: exportingOptions.filename || 'chart', - type: exportingOptions.type, - // IE8 fails to post undefined correctly, so use 0 - width: exportingOptions.width || 0, - scale: exportingOptions.scale, - svg: svg - }, exportingOptions.formAttributes); - - }, - - /** - * Exporting module required. Clears away other elements in the page and - * prints the chart as it is displayed. By default, when the exporting - * module is enabled, a context button with a drop down menu in the upper - * right corner accesses this function. - * - * @sample highcharts/members/chart-print/ - * Print from a HTML button - */ - print: function () { - - var chart = this, - container = chart.container, - origDisplay = [], - origParent = container.parentNode, - body = doc.body, - childNodes = body.childNodes, - printMaxWidth = chart.options.exporting.printMaxWidth, - resetParams, - handleMaxWidth; - - if (chart.isPrinting) { // block the button while in printing mode - return; - } - - chart.isPrinting = true; - chart.pointer.reset(null, 0); - - fireEvent(chart, 'beforePrint'); - - // Handle printMaxWidth - handleMaxWidth = printMaxWidth && chart.chartWidth > printMaxWidth; - if (handleMaxWidth) { - resetParams = [chart.options.chart.width, undefined, false]; - chart.setSize(printMaxWidth, undefined, false); - } - - // hide all body content - each(childNodes, function (node, i) { - if (node.nodeType === 1) { - origDisplay[i] = node.style.display; - node.style.display = 'none'; - } - }); - - // pull out the chart - body.appendChild(container); - - // print - win.focus(); // #1510 - win.print(); - - // allow the browser to prepare before reverting - setTimeout(function () { - - // put the chart back in - origParent.appendChild(container); - - // restore all body content - each(childNodes, function (node, i) { - if (node.nodeType === 1) { - node.style.display = origDisplay[i]; - } - }); - - chart.isPrinting = false; - - // Reset printMaxWidth - if (handleMaxWidth) { - chart.setSize.apply(chart, resetParams); - } - - fireEvent(chart, 'afterPrint'); - - }, 1000); - - }, - - /** - * Display a popup menu for choosing the export type. - * - * @private - * - * @param {String} className An identifier for the menu - * @param {Array} items A collection with text and onclicks for the items - * @param {Number} x The x position of the opener button - * @param {Number} y The y position of the opener button - * @param {Number} width The width of the opener button - * @param {Number} height The height of the opener button - */ - contextMenu: function (className, items, x, y, width, height, button) { - var chart = this, - navOptions = chart.options.navigation, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - cacheName = 'cache-' + className, - menu = chart[cacheName], - menuPadding = Math.max(width, height), // for mouse leave detection - innerMenu, - hide, - menuStyle; - - // create the menu only the first time - if (!menu) { - - // create a HTML element above the SVG - chart[cacheName] = menu = createElement('div', { - className: className - }, { - position: 'absolute', - zIndex: 1000, - padding: menuPadding + 'px' - }, chart.container); - - innerMenu = createElement( - 'div', - { className: 'highcharts-menu' }, - null, - menu - ); - - /*= if (build.classic) { =*/ - // Presentational CSS - css(innerMenu, extend({ - MozBoxShadow: '3px 3px 10px #888', - WebkitBoxShadow: '3px 3px 10px #888', - boxShadow: '3px 3px 10px #888' - }, navOptions.menuStyle)); - /*= } =*/ - - // hide on mouse out - hide = function () { - css(menu, { display: 'none' }); - if (button) { - button.setState(0); - } - chart.openMenu = false; - }; - - // Hide the menu some time after mouse leave (#1357) - chart.exportEvents.push( - addEvent(menu, 'mouseleave', function () { - menu.hideTimer = setTimeout(hide, 500); - }), - addEvent(menu, 'mouseenter', function () { - H.clearTimeout(menu.hideTimer); - }), - - // Hide it on clicking or touching outside the menu (#2258, - // #2335, #2407) - addEvent(doc, 'mouseup', function (e) { - if (!chart.pointer.inClass(e.target, className)) { - hide(); - } - }) - ); - - // create the items - each(items, function (item) { - - if (typeof item === 'string') { - item = chart.options.exporting.menuItemDefinitions[item]; - } - - if (H.isObject(item, true)) { - var element; - - if (item.separator) { - element = createElement('hr', null, null, innerMenu); - - } else { - element = createElement('div', { - className: 'highcharts-menu-item', - onclick: function (e) { - if (e) { // IE7 - e.stopPropagation(); - } - hide(); - if (item.onclick) { - item.onclick.apply(chart, arguments); - } - }, - innerHTML: ( - item.text || - chart.options.lang[item.textKey] - ) - }, null, innerMenu); - - /*= if (build.classic) { =*/ - element.onmouseover = function () { - css(this, navOptions.menuItemHoverStyle); - }; - element.onmouseout = function () { - css(this, navOptions.menuItemStyle); - }; - css(element, extend({ - cursor: 'pointer' - }, navOptions.menuItemStyle)); - /*= } =*/ - } - - // Keep references to menu divs to be able to destroy them - chart.exportDivElements.push(element); - } - }); - - // Keep references to menu and innerMenu div to be able to destroy - // them - chart.exportDivElements.push(innerMenu, menu); - - chart.exportMenuWidth = menu.offsetWidth; - chart.exportMenuHeight = menu.offsetHeight; - } - - menuStyle = { display: 'block' }; - - // if outside right, right align it - if (x + chart.exportMenuWidth > chartWidth) { - menuStyle.right = (chartWidth - x - width - menuPadding) + 'px'; - } else { - menuStyle.left = (x - menuPadding) + 'px'; - } - // if outside bottom, bottom align it - if ( - y + height + chart.exportMenuHeight > chartHeight && - button.alignOptions.verticalAlign !== 'top' - ) { - menuStyle.bottom = (chartHeight - y - menuPadding) + 'px'; - } else { - menuStyle.top = (y + height - menuPadding) + 'px'; - } - - css(menu, menuStyle); - chart.openMenu = true; - }, - - /** - * Add the export button to the chart, with options. - * - * @private - */ - addButton: function (options) { - var chart = this, - renderer = chart.renderer, - btnOptions = merge(chart.options.navigation.buttonOptions, options), - onclick = btnOptions.onclick, - menuItems = btnOptions.menuItems, - symbol, - button, - symbolSize = btnOptions.symbolSize || 12; - if (!chart.btnCount) { - chart.btnCount = 0; - } - - // Keeps references to the button elements - if (!chart.exportDivElements) { - chart.exportDivElements = []; - chart.exportSVGElements = []; - } - - if (btnOptions.enabled === false) { - return; - } - - - var attr = btnOptions.theme, - states = attr.states, - hover = states && states.hover, - select = states && states.select, - callback; - - delete attr.states; - - if (onclick) { - callback = function (e) { - e.stopPropagation(); - onclick.call(chart, e); - }; - - } else if (menuItems) { - callback = function () { - chart.contextMenu( - button.menuClassName, - menuItems, - button.translateX, - button.translateY, - button.width, - button.height, - button - ); - button.setState(2); - }; - } - - - if (btnOptions.text && btnOptions.symbol) { - attr.paddingLeft = pick(attr.paddingLeft, 25); - - } else if (!btnOptions.text) { - extend(attr, { - width: btnOptions.width, - height: btnOptions.height, - padding: 0 - }); - } - - button = renderer - .button(btnOptions.text, 0, 0, callback, attr, hover, select) - .addClass(options.className) - .attr({ - /*= if (build.classic) { =*/ - 'stroke-linecap': 'round', - /*= } =*/ - title: pick(chart.options.lang[btnOptions._titleKey], ''), - zIndex: 3 // #4955 - }); - button.menuClassName = ( - options.menuClassName || - 'highcharts-menu-' + chart.btnCount++ - ); - - if (btnOptions.symbol) { - symbol = renderer.symbol( - btnOptions.symbol, - btnOptions.symbolX - (symbolSize / 2), - btnOptions.symbolY - (symbolSize / 2), - symbolSize, - symbolSize, - // If symbol is an image, scale it (#7957) - { - width: symbolSize, - height: symbolSize - } - ) - .addClass('highcharts-button-symbol') - .attr({ - zIndex: 1 - }).add(button); - - /*= if (build.classic) { =*/ - symbol.attr({ - stroke: btnOptions.symbolStroke, - fill: btnOptions.symbolFill, - 'stroke-width': btnOptions.symbolStrokeWidth || 1 - }); - /*= } =*/ - } - - button.add() - .align(extend(btnOptions, { - width: button.width, - x: pick(btnOptions.x, chart.buttonOffset) // #1654 - }), true, 'spacingBox'); - - chart.buttonOffset += ( - (button.width + btnOptions.buttonSpacing) * - (btnOptions.align === 'right' ? -1 : 1) - ); - - chart.exportSVGElements.push(button, symbol); - - }, - - /** - * Destroy the export buttons. - * - * @private - */ - destroyExport: function (e) { - var chart = e ? e.target : this, - exportSVGElements = chart.exportSVGElements, - exportDivElements = chart.exportDivElements, - exportEvents = chart.exportEvents, - cacheName; - - // Destroy the extra buttons added - if (exportSVGElements) { - each(exportSVGElements, function (elem, i) { - - // Destroy and null the svg elements - if (elem) { // #1822 - elem.onclick = elem.ontouchstart = null; - cacheName = 'cache-' + elem.menuClassName; - - if (chart[cacheName]) { - delete chart[cacheName]; - } - - chart.exportSVGElements[i] = elem.destroy(); - } - }); - exportSVGElements.length = 0; - } - - // Destroy the divs for the menu - if (exportDivElements) { - each(exportDivElements, function (elem, i) { - - // Remove the event handler - H.clearTimeout(elem.hideTimer); // #5427 - removeEvent(elem, 'mouseleave'); - - // Remove inline events - chart.exportDivElements[i] = - elem.onmouseout = - elem.onmouseover = - elem.ontouchstart = - elem.onclick = null; - - // Destroy the div by moving to garbage bin - discardElement(elem); - }); - exportDivElements.length = 0; - } - - if (exportEvents) { - each(exportEvents, function (unbind) { - unbind(); - }); - exportEvents.length = 0; - } - } + /** + * Exporting module only. A collection of fixes on the produced SVG to + * account for expando properties, browser bugs, VML problems and other. + * Returns a cleaned SVG. + * + * @private + */ + sanitizeSVG: function (svg, options) { + // Move HTML into a foreignObject + if (options && options.exporting && options.exporting.allowHTML) { + var html = svg.match(/<\/svg>(.*?$)/); + if (html && html[1]) { + html = '' + + '' + + html[1] + + '' + + ''; + svg = svg.replace('', html + ''); + } + } + + svg = svg + .replace(/zIndex="[^"]+"/g, '') + .replace(/isShadow="[^"]+"/g, '') + .replace(/symbolName="[^"]+"/g, '') + .replace(/jQuery[0-9]+="[^"]+"/g, '') + .replace(/url\(("|")(\S+)("|")\)/g, 'url($2)') + .replace(/url\([^#]+#/g, 'url(#') + .replace( + /.*?$/, '') + // Batik doesn't support rgba fills and strokes (#3095) + .replace( + /(fill|stroke)="rgba\(([ 0-9]+,[ 0-9]+,[ 0-9]+),([ 0-9\.]+)\)"/g, // eslint-disable-line max-len + '$1="rgb($2)" $1-opacity="$3"' + ) + + // Replace HTML entities, issue #347 + .replace(/ /g, '\u00A0') // no-break space + .replace(/­/g, '\u00AD'); // soft hyphen + + /*= if (build.classic) { =*/ + // Further sanitize for oldIE + if (this.ieSanitizeSVG) { + svg = this.ieSanitizeSVG(svg); + } + /*= } =*/ + + return svg; + }, + + /** + * Return the unfiltered innerHTML of the chart container. Used as hook for + * plugins. In styled mode, it also takes care of inlining CSS style rules. + * + * @see Chart#getSVG + * + * @returns {String} + * The unfiltered SVG of the chart. + */ + getChartHTML: function () { + /*= if (!build.classic) { =*/ + this.inlineStyles(); + /*= } =*/ + return this.container.innerHTML; + }, + + /** + * Return an SVG representation of the chart. + * + * @param chartOptions {Options} + * Additional chart options for the generated SVG representation. + * For collections like `xAxis`, `yAxis` or `series`, the additional + * options is either merged in to the orininal item of the same + * `id`, or to the first item if a common id is not found. + * @return {String} + * The SVG representation of the rendered chart. + * @sample highcharts/members/chart-getsvg/ + * View the SVG from a button + */ + getSVG: function (chartOptions) { + var chart = this, + chartCopy, + sandbox, + svg, + seriesOptions, + sourceWidth, + sourceHeight, + cssWidth, + cssHeight, + // Copy the options and add extra options + options = merge(chart.options, chartOptions); + + + // create a sandbox where a new chart will be generated + sandbox = createElement('div', null, { + position: 'absolute', + top: '-9999em', + width: chart.chartWidth + 'px', + height: chart.chartHeight + 'px' + }, doc.body); + + // get the source size + cssWidth = chart.renderTo.style.width; + cssHeight = chart.renderTo.style.height; + sourceWidth = options.exporting.sourceWidth || + options.chart.width || + (/px$/.test(cssWidth) && parseInt(cssWidth, 10)) || + 600; + sourceHeight = options.exporting.sourceHeight || + options.chart.height || + (/px$/.test(cssHeight) && parseInt(cssHeight, 10)) || + 400; + + // override some options + extend(options.chart, { + animation: false, + renderTo: sandbox, + forExport: true, + renderer: 'SVGRenderer', + width: sourceWidth, + height: sourceHeight + }); + options.exporting.enabled = false; // hide buttons in print + delete options.data; // #3004 + + // prepare for replicating the chart + options.series = []; + each(chart.series, function (serie) { + seriesOptions = merge(serie.userOptions, { // #4912 + animation: false, // turn off animation + enableMouseTracking: false, + showCheckbox: false, + visible: serie.visible + }); + + // Used for the navigator series that has its own option set + if (!seriesOptions.isInternal) { + options.series.push(seriesOptions); + } + }); + + // Assign an internal key to ensure a one-to-one mapping (#5924) + each(chart.axes, function (axis) { + if (!axis.userOptions.internalKey) { // #6444 + axis.userOptions.internalKey = H.uniqueKey(); + } + }); + + // generate the chart copy + chartCopy = new H.Chart(options, chart.callback); + + // Axis options and series options (#2022, #3900, #5982) + if (chartOptions) { + each(['xAxis', 'yAxis', 'series'], function (coll) { + var collOptions = {}; + if (chartOptions[coll]) { + collOptions[coll] = chartOptions[coll]; + chartCopy.update(collOptions); + } + }); + } + + // Reflect axis extremes in the export (#5924) + each(chart.axes, function (axis) { + var axisCopy = H.find(chartCopy.axes, function (copy) { + return copy.options.internalKey === + axis.userOptions.internalKey; + }), + extremes = axis.getExtremes(), + userMin = extremes.userMin, + userMax = extremes.userMax; + + if (axisCopy && (userMin !== undefined || userMax !== undefined)) { + axisCopy.setExtremes(userMin, userMax, true, false); + } + }); + + // Get the SVG from the container's innerHTML + svg = chartCopy.getChartHTML(); + + svg = chart.sanitizeSVG(svg, options); + + // free up memory + options = null; + chartCopy.destroy(); + discardElement(sandbox); + + return svg; + }, + + getSVGForExport: function (options, chartOptions) { + var chartExportingOptions = this.options.exporting; + + return this.getSVG(merge( + { chart: { borderRadius: 0 } }, + chartExportingOptions.chartOptions, + chartOptions, + { + exporting: { + sourceWidth: ( + (options && options.sourceWidth) || + chartExportingOptions.sourceWidth + ), + sourceHeight: ( + (options && options.sourceHeight) || + chartExportingOptions.sourceHeight + ) + } + } + )); + }, + + /** + * Exporting module required. Submit an SVG version of the chart to a server + * along with some parameters for conversion. + * @param {Object} exportingOptions + * Exporting options in addition to those defined in {@link + * https://api.highcharts.com/highcharts/exporting|exporting}. + * @param {String} exportingOptions.filename + * The file name for the export without extension. + * @param {String} exportingOptions.url + * The URL for the server module to do the conversion. + * @param {Number} exportingOptions.width + * The width of the PNG or JPG image generated on the server. + * @param {String} exportingOptions.type + * The MIME type of the converted image. + * @param {Number} exportingOptions.sourceWidth + * The pixel width of the source (in-page) chart. + * @param {Number} exportingOptions.sourceHeight + * The pixel height of the source (in-page) chart. + * @param {Options} chartOptions + * Additional chart options for the exported chart. For example a + * different background color can be added here, or `dataLabels` + * for export only. + * + * @sample highcharts/members/chart-exportchart/ + * Export with no options + * @sample highcharts/members/chart-exportchart-filename/ + * PDF type and custom filename + * @sample highcharts/members/chart-exportchart-custom-background/ + * Different chart background in export + * @sample stock/members/chart-exportchart/ + * Export with Highstock + */ + exportChart: function (exportingOptions, chartOptions) { + + var svg = this.getSVGForExport(exportingOptions, chartOptions); + + // merge the options + exportingOptions = merge(this.options.exporting, exportingOptions); + + // do the post + H.post(exportingOptions.url, { + filename: exportingOptions.filename || 'chart', + type: exportingOptions.type, + // IE8 fails to post undefined correctly, so use 0 + width: exportingOptions.width || 0, + scale: exportingOptions.scale, + svg: svg + }, exportingOptions.formAttributes); + + }, + + /** + * Exporting module required. Clears away other elements in the page and + * prints the chart as it is displayed. By default, when the exporting + * module is enabled, a context button with a drop down menu in the upper + * right corner accesses this function. + * + * @sample highcharts/members/chart-print/ + * Print from a HTML button + */ + print: function () { + + var chart = this, + container = chart.container, + origDisplay = [], + origParent = container.parentNode, + body = doc.body, + childNodes = body.childNodes, + printMaxWidth = chart.options.exporting.printMaxWidth, + resetParams, + handleMaxWidth; + + if (chart.isPrinting) { // block the button while in printing mode + return; + } + + chart.isPrinting = true; + chart.pointer.reset(null, 0); + + fireEvent(chart, 'beforePrint'); + + // Handle printMaxWidth + handleMaxWidth = printMaxWidth && chart.chartWidth > printMaxWidth; + if (handleMaxWidth) { + resetParams = [chart.options.chart.width, undefined, false]; + chart.setSize(printMaxWidth, undefined, false); + } + + // hide all body content + each(childNodes, function (node, i) { + if (node.nodeType === 1) { + origDisplay[i] = node.style.display; + node.style.display = 'none'; + } + }); + + // pull out the chart + body.appendChild(container); + + // print + win.focus(); // #1510 + win.print(); + + // allow the browser to prepare before reverting + setTimeout(function () { + + // put the chart back in + origParent.appendChild(container); + + // restore all body content + each(childNodes, function (node, i) { + if (node.nodeType === 1) { + node.style.display = origDisplay[i]; + } + }); + + chart.isPrinting = false; + + // Reset printMaxWidth + if (handleMaxWidth) { + chart.setSize.apply(chart, resetParams); + } + + fireEvent(chart, 'afterPrint'); + + }, 1000); + + }, + + /** + * Display a popup menu for choosing the export type. + * + * @private + * + * @param {String} className An identifier for the menu + * @param {Array} items A collection with text and onclicks for the items + * @param {Number} x The x position of the opener button + * @param {Number} y The y position of the opener button + * @param {Number} width The width of the opener button + * @param {Number} height The height of the opener button + */ + contextMenu: function (className, items, x, y, width, height, button) { + var chart = this, + navOptions = chart.options.navigation, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + cacheName = 'cache-' + className, + menu = chart[cacheName], + menuPadding = Math.max(width, height), // for mouse leave detection + innerMenu, + hide, + menuStyle; + + // create the menu only the first time + if (!menu) { + + // create a HTML element above the SVG + chart[cacheName] = menu = createElement('div', { + className: className + }, { + position: 'absolute', + zIndex: 1000, + padding: menuPadding + 'px' + }, chart.container); + + innerMenu = createElement( + 'div', + { className: 'highcharts-menu' }, + null, + menu + ); + + /*= if (build.classic) { =*/ + // Presentational CSS + css(innerMenu, extend({ + MozBoxShadow: '3px 3px 10px #888', + WebkitBoxShadow: '3px 3px 10px #888', + boxShadow: '3px 3px 10px #888' + }, navOptions.menuStyle)); + /*= } =*/ + + // hide on mouse out + hide = function () { + css(menu, { display: 'none' }); + if (button) { + button.setState(0); + } + chart.openMenu = false; + }; + + // Hide the menu some time after mouse leave (#1357) + chart.exportEvents.push( + addEvent(menu, 'mouseleave', function () { + menu.hideTimer = setTimeout(hide, 500); + }), + addEvent(menu, 'mouseenter', function () { + H.clearTimeout(menu.hideTimer); + }), + + // Hide it on clicking or touching outside the menu (#2258, + // #2335, #2407) + addEvent(doc, 'mouseup', function (e) { + if (!chart.pointer.inClass(e.target, className)) { + hide(); + } + }) + ); + + // create the items + each(items, function (item) { + + if (typeof item === 'string') { + item = chart.options.exporting.menuItemDefinitions[item]; + } + + if (H.isObject(item, true)) { + var element; + + if (item.separator) { + element = createElement('hr', null, null, innerMenu); + + } else { + element = createElement('div', { + className: 'highcharts-menu-item', + onclick: function (e) { + if (e) { // IE7 + e.stopPropagation(); + } + hide(); + if (item.onclick) { + item.onclick.apply(chart, arguments); + } + }, + innerHTML: ( + item.text || + chart.options.lang[item.textKey] + ) + }, null, innerMenu); + + /*= if (build.classic) { =*/ + element.onmouseover = function () { + css(this, navOptions.menuItemHoverStyle); + }; + element.onmouseout = function () { + css(this, navOptions.menuItemStyle); + }; + css(element, extend({ + cursor: 'pointer' + }, navOptions.menuItemStyle)); + /*= } =*/ + } + + // Keep references to menu divs to be able to destroy them + chart.exportDivElements.push(element); + } + }); + + // Keep references to menu and innerMenu div to be able to destroy + // them + chart.exportDivElements.push(innerMenu, menu); + + chart.exportMenuWidth = menu.offsetWidth; + chart.exportMenuHeight = menu.offsetHeight; + } + + menuStyle = { display: 'block' }; + + // if outside right, right align it + if (x + chart.exportMenuWidth > chartWidth) { + menuStyle.right = (chartWidth - x - width - menuPadding) + 'px'; + } else { + menuStyle.left = (x - menuPadding) + 'px'; + } + // if outside bottom, bottom align it + if ( + y + height + chart.exportMenuHeight > chartHeight && + button.alignOptions.verticalAlign !== 'top' + ) { + menuStyle.bottom = (chartHeight - y - menuPadding) + 'px'; + } else { + menuStyle.top = (y + height - menuPadding) + 'px'; + } + + css(menu, menuStyle); + chart.openMenu = true; + }, + + /** + * Add the export button to the chart, with options. + * + * @private + */ + addButton: function (options) { + var chart = this, + renderer = chart.renderer, + btnOptions = merge(chart.options.navigation.buttonOptions, options), + onclick = btnOptions.onclick, + menuItems = btnOptions.menuItems, + symbol, + button, + symbolSize = btnOptions.symbolSize || 12; + if (!chart.btnCount) { + chart.btnCount = 0; + } + + // Keeps references to the button elements + if (!chart.exportDivElements) { + chart.exportDivElements = []; + chart.exportSVGElements = []; + } + + if (btnOptions.enabled === false) { + return; + } + + + var attr = btnOptions.theme, + states = attr.states, + hover = states && states.hover, + select = states && states.select, + callback; + + delete attr.states; + + if (onclick) { + callback = function (e) { + e.stopPropagation(); + onclick.call(chart, e); + }; + + } else if (menuItems) { + callback = function () { + chart.contextMenu( + button.menuClassName, + menuItems, + button.translateX, + button.translateY, + button.width, + button.height, + button + ); + button.setState(2); + }; + } + + + if (btnOptions.text && btnOptions.symbol) { + attr.paddingLeft = pick(attr.paddingLeft, 25); + + } else if (!btnOptions.text) { + extend(attr, { + width: btnOptions.width, + height: btnOptions.height, + padding: 0 + }); + } + + button = renderer + .button(btnOptions.text, 0, 0, callback, attr, hover, select) + .addClass(options.className) + .attr({ + /*= if (build.classic) { =*/ + 'stroke-linecap': 'round', + /*= } =*/ + title: pick(chart.options.lang[btnOptions._titleKey], ''), + zIndex: 3 // #4955 + }); + button.menuClassName = ( + options.menuClassName || + 'highcharts-menu-' + chart.btnCount++ + ); + + if (btnOptions.symbol) { + symbol = renderer.symbol( + btnOptions.symbol, + btnOptions.symbolX - (symbolSize / 2), + btnOptions.symbolY - (symbolSize / 2), + symbolSize, + symbolSize, + // If symbol is an image, scale it (#7957) + { + width: symbolSize, + height: symbolSize + } + ) + .addClass('highcharts-button-symbol') + .attr({ + zIndex: 1 + }).add(button); + + /*= if (build.classic) { =*/ + symbol.attr({ + stroke: btnOptions.symbolStroke, + fill: btnOptions.symbolFill, + 'stroke-width': btnOptions.symbolStrokeWidth || 1 + }); + /*= } =*/ + } + + button.add() + .align(extend(btnOptions, { + width: button.width, + x: pick(btnOptions.x, chart.buttonOffset) // #1654 + }), true, 'spacingBox'); + + chart.buttonOffset += ( + (button.width + btnOptions.buttonSpacing) * + (btnOptions.align === 'right' ? -1 : 1) + ); + + chart.exportSVGElements.push(button, symbol); + + }, + + /** + * Destroy the export buttons. + * + * @private + */ + destroyExport: function (e) { + var chart = e ? e.target : this, + exportSVGElements = chart.exportSVGElements, + exportDivElements = chart.exportDivElements, + exportEvents = chart.exportEvents, + cacheName; + + // Destroy the extra buttons added + if (exportSVGElements) { + each(exportSVGElements, function (elem, i) { + + // Destroy and null the svg elements + if (elem) { // #1822 + elem.onclick = elem.ontouchstart = null; + cacheName = 'cache-' + elem.menuClassName; + + if (chart[cacheName]) { + delete chart[cacheName]; + } + + chart.exportSVGElements[i] = elem.destroy(); + } + }); + exportSVGElements.length = 0; + } + + // Destroy the divs for the menu + if (exportDivElements) { + each(exportDivElements, function (elem, i) { + + // Remove the event handler + H.clearTimeout(elem.hideTimer); // #5427 + removeEvent(elem, 'mouseleave'); + + // Remove inline events + chart.exportDivElements[i] = + elem.onmouseout = + elem.onmouseover = + elem.ontouchstart = + elem.onclick = null; + + // Destroy the div by moving to garbage bin + discardElement(elem); + }); + exportDivElements.length = 0; + } + + if (exportEvents) { + each(exportEvents, function (unbind) { + unbind(); + }); + exportEvents.length = 0; + } + } }); /*= if (!build.classic) { =*/ // These ones are translated to attributes rather than styles SVGRenderer.prototype.inlineToAttributes = [ - 'fill', - 'stroke', - 'strokeLinecap', - 'strokeLinejoin', - 'strokeWidth', - 'textAnchor', - 'x', - 'y' + 'fill', + 'stroke', + 'strokeLinecap', + 'strokeLinejoin', + 'strokeWidth', + 'textAnchor', + 'x', + 'y' ]; // These CSS properties are not inlined. Remember camelCase. SVGRenderer.prototype.inlineBlacklist = [ - /-/, // In Firefox, both hyphened and camelCased names are listed - /^(clipPath|cssText|d|height|width)$/, // Full words - /^font$/, // more specific props are set - /[lL]ogical(Width|Height)$/, - /perspective/, - /TapHighlightColor/, - /^transition/, - /^length$/ // #7700 - // /^text (border|color|cursor|height|webkitBorder)/ + /-/, // In Firefox, both hyphened and camelCased names are listed + /^(clipPath|cssText|d|height|width)$/, // Full words + /^font$/, // more specific props are set + /[lL]ogical(Width|Height)$/, + /perspective/, + /TapHighlightColor/, + /^transition/, + /^length$/ // #7700 + // /^text (border|color|cursor|height|webkitBorder)/ ]; SVGRenderer.prototype.unstyledElements = [ - 'clipPath', - 'defs', - 'desc' + 'clipPath', + 'defs', + 'desc' ]; /** @@ -1594,273 +1594,273 @@ SVGRenderer.prototype.unstyledElements = [ * @todo: Make it work with IE9 and IE10. */ Chart.prototype.inlineStyles = function () { - var renderer = this.renderer, - inlineToAttributes = renderer.inlineToAttributes, - blacklist = renderer.inlineBlacklist, - whitelist = renderer.inlineWhitelist, // For IE - unstyledElements = renderer.unstyledElements, - defaultStyles = {}, - dummySVG, - iframe, - iframeDoc; - - // Create an iframe where we read default styles without pollution from this - // body - iframe = doc.createElement('iframe'); - css(iframe, { - width: '1px', - height: '1px', - visibility: 'hidden' - }); - doc.body.appendChild(iframe); - iframeDoc = iframe.contentWindow.document; - iframeDoc.open(); - iframeDoc.write(''); - iframeDoc.close(); - - - /** - * Make hyphenated property names out of camelCase - */ - function hyphenate(prop) { - return prop.replace( - /([A-Z])/g, - function (a, b) { - return '-' + b.toLowerCase(); - } - ); - } - - /** - * Call this on all elements and recurse to children - */ - function recurse(node) { - var styles, - parentStyles, - cssText = '', - dummy, - styleAttr, - blacklisted, - whitelisted, - i; - - // Check computed styles and whether they are in the white/blacklist for - // styles or atttributes - function filterStyles(val, prop) { - - // Check against whitelist & blacklist - blacklisted = whitelisted = false; - if (whitelist) { - // Styled mode in IE has a whitelist instead. - // Exclude all props not in this list. - i = whitelist.length; - while (i-- && !whitelisted) { - whitelisted = whitelist[i].test(prop); - } - blacklisted = !whitelisted; - } - - // Explicitly remove empty transforms - if (prop === 'transform' && val === 'none') { - blacklisted = true; - } - - i = blacklist.length; - while (i-- && !blacklisted) { - blacklisted = ( - blacklist[i].test(prop) || - typeof val === 'function' - ); - } - - if (!blacklisted) { - // If parent node has the same style, it gets inherited, no need - // to inline it. Top-level props should be diffed against parent - // (#7687). - if ( - (parentStyles[prop] !== val || node.nodeName === 'svg') && - defaultStyles[node.nodeName][prop] !== val - ) { - // Attributes - if (inlineToAttributes.indexOf(prop) !== -1) { - node.setAttribute(hyphenate(prop), val); - // Styles - } else { - cssText += hyphenate(prop) + ':' + val + ';'; - } - } - } - } - - if ( - node.nodeType === 1 && - unstyledElements.indexOf(node.nodeName) === -1 - ) { - styles = win.getComputedStyle(node, null); - parentStyles = node.nodeName === 'svg' ? - {} : - win.getComputedStyle(node.parentNode, null); - - // Get default styles from the browser so that we don't have to add - // these - if (!defaultStyles[node.nodeName]) { - /* - if (!dummySVG) { - dummySVG = doc.createElementNS(H.SVG_NS, 'svg'); - dummySVG.setAttribute('version', '1.1'); - doc.body.appendChild(dummySVG); - } - */ - dummySVG = iframeDoc.getElementsByTagName('svg')[0]; - dummy = iframeDoc.createElementNS( - node.namespaceURI, - node.nodeName - ); - dummySVG.appendChild(dummy); - // Copy, so we can remove the node - defaultStyles[node.nodeName] = merge( - win.getComputedStyle(dummy, null) - ); - dummySVG.removeChild(dummy); - } - - // Loop through all styles and add them inline if they are ok - if (isFirefoxBrowser || isMSBrowser) { - // Some browsers put lots of styles on the prototype - for (var p in styles) { - filterStyles(styles[p], p); - } - } else { - objectEach(styles, filterStyles); - } - - // Apply styles - if (cssText) { - styleAttr = node.getAttribute('style'); - node.setAttribute( - 'style', - (styleAttr ? styleAttr + ';' : '') + cssText - ); - } - - // Set default stroke width (needed at least for IE) - if (node.nodeName === 'svg') { - node.setAttribute('stroke-width', '1px'); - } - - if (node.nodeName === 'text') { - return; - } - - // Recurse - each(node.children || node.childNodes, recurse); - } - } - - /** - * Remove the dummy objects used to get defaults - */ - function tearDown() { - dummySVG.parentNode.removeChild(dummySVG); - } - - recurse(this.container.querySelector('svg')); - tearDown(); + var renderer = this.renderer, + inlineToAttributes = renderer.inlineToAttributes, + blacklist = renderer.inlineBlacklist, + whitelist = renderer.inlineWhitelist, // For IE + unstyledElements = renderer.unstyledElements, + defaultStyles = {}, + dummySVG, + iframe, + iframeDoc; + + // Create an iframe where we read default styles without pollution from this + // body + iframe = doc.createElement('iframe'); + css(iframe, { + width: '1px', + height: '1px', + visibility: 'hidden' + }); + doc.body.appendChild(iframe); + iframeDoc = iframe.contentWindow.document; + iframeDoc.open(); + iframeDoc.write(''); + iframeDoc.close(); + + + /** + * Make hyphenated property names out of camelCase + */ + function hyphenate(prop) { + return prop.replace( + /([A-Z])/g, + function (a, b) { + return '-' + b.toLowerCase(); + } + ); + } + + /** + * Call this on all elements and recurse to children + */ + function recurse(node) { + var styles, + parentStyles, + cssText = '', + dummy, + styleAttr, + blacklisted, + whitelisted, + i; + + // Check computed styles and whether they are in the white/blacklist for + // styles or atttributes + function filterStyles(val, prop) { + + // Check against whitelist & blacklist + blacklisted = whitelisted = false; + if (whitelist) { + // Styled mode in IE has a whitelist instead. + // Exclude all props not in this list. + i = whitelist.length; + while (i-- && !whitelisted) { + whitelisted = whitelist[i].test(prop); + } + blacklisted = !whitelisted; + } + + // Explicitly remove empty transforms + if (prop === 'transform' && val === 'none') { + blacklisted = true; + } + + i = blacklist.length; + while (i-- && !blacklisted) { + blacklisted = ( + blacklist[i].test(prop) || + typeof val === 'function' + ); + } + + if (!blacklisted) { + // If parent node has the same style, it gets inherited, no need + // to inline it. Top-level props should be diffed against parent + // (#7687). + if ( + (parentStyles[prop] !== val || node.nodeName === 'svg') && + defaultStyles[node.nodeName][prop] !== val + ) { + // Attributes + if (inlineToAttributes.indexOf(prop) !== -1) { + node.setAttribute(hyphenate(prop), val); + // Styles + } else { + cssText += hyphenate(prop) + ':' + val + ';'; + } + } + } + } + + if ( + node.nodeType === 1 && + unstyledElements.indexOf(node.nodeName) === -1 + ) { + styles = win.getComputedStyle(node, null); + parentStyles = node.nodeName === 'svg' ? + {} : + win.getComputedStyle(node.parentNode, null); + + // Get default styles from the browser so that we don't have to add + // these + if (!defaultStyles[node.nodeName]) { + /* + if (!dummySVG) { + dummySVG = doc.createElementNS(H.SVG_NS, 'svg'); + dummySVG.setAttribute('version', '1.1'); + doc.body.appendChild(dummySVG); + } + */ + dummySVG = iframeDoc.getElementsByTagName('svg')[0]; + dummy = iframeDoc.createElementNS( + node.namespaceURI, + node.nodeName + ); + dummySVG.appendChild(dummy); + // Copy, so we can remove the node + defaultStyles[node.nodeName] = merge( + win.getComputedStyle(dummy, null) + ); + dummySVG.removeChild(dummy); + } + + // Loop through all styles and add them inline if they are ok + if (isFirefoxBrowser || isMSBrowser) { + // Some browsers put lots of styles on the prototype + for (var p in styles) { + filterStyles(styles[p], p); + } + } else { + objectEach(styles, filterStyles); + } + + // Apply styles + if (cssText) { + styleAttr = node.getAttribute('style'); + node.setAttribute( + 'style', + (styleAttr ? styleAttr + ';' : '') + cssText + ); + } + + // Set default stroke width (needed at least for IE) + if (node.nodeName === 'svg') { + node.setAttribute('stroke-width', '1px'); + } + + if (node.nodeName === 'text') { + return; + } + + // Recurse + each(node.children || node.childNodes, recurse); + } + } + + /** + * Remove the dummy objects used to get defaults + */ + function tearDown() { + dummySVG.parentNode.removeChild(dummySVG); + } + + recurse(this.container.querySelector('svg')); + tearDown(); }; /*= } =*/ symbols.menu = function (x, y, width, height) { - var arr = [ - 'M', x, y + 2.5, - 'L', x + width, y + 2.5, - 'M', x, y + height / 2 + 0.5, - 'L', x + width, y + height / 2 + 0.5, - 'M', x, y + height - 1.5, - 'L', x + width, y + height - 1.5 - ]; - return arr; + var arr = [ + 'M', x, y + 2.5, + 'L', x + width, y + 2.5, + 'M', x, y + height / 2 + 0.5, + 'L', x + width, y + height / 2 + 0.5, + 'M', x, y + height - 1.5, + 'L', x + width, y + height - 1.5 + ]; + return arr; }; // Add the buttons on chart load Chart.prototype.renderExporting = function () { - var chart = this, - exportingOptions = chart.options.exporting, - buttons = exportingOptions.buttons, - isDirty = chart.isDirtyExporting || !chart.exportSVGElements; - - chart.buttonOffset = 0; - if (chart.isDirtyExporting) { - chart.destroyExport(); - } - - if (isDirty && exportingOptions.enabled !== false) { - chart.exportEvents = []; - - objectEach(buttons, function (button) { - chart.addButton(button); - }); - - chart.isDirtyExporting = false; - } - - // Destroy the export elements at chart destroy - addEvent(chart, 'destroy', chart.destroyExport); + var chart = this, + exportingOptions = chart.options.exporting, + buttons = exportingOptions.buttons, + isDirty = chart.isDirtyExporting || !chart.exportSVGElements; + + chart.buttonOffset = 0; + if (chart.isDirtyExporting) { + chart.destroyExport(); + } + + if (isDirty && exportingOptions.enabled !== false) { + chart.exportEvents = []; + + objectEach(buttons, function (button) { + chart.addButton(button); + }); + + chart.isDirtyExporting = false; + } + + // Destroy the export elements at chart destroy + addEvent(chart, 'destroy', chart.destroyExport); }; Chart.prototype.callbacks.push(function (chart) { - function update(prop, options, redraw) { - chart.isDirtyExporting = true; - merge(true, chart.options[prop], options); - if (pick(redraw, true)) { - chart.redraw(); - } - - } - - chart.renderExporting(); - - addEvent(chart, 'redraw', chart.renderExporting); - - // Add update methods to handle chart.update and chart.exporting.update - // and chart.navigation.update. - each(['exporting', 'navigation'], function (prop) { - chart[prop] = { - update: function (options, redraw) { - update(prop, options, redraw); - } - }; - }); - - // Uncomment this to see a button directly below the chart, for quick - // testing of export - /* - if (!chart.renderer.forExport) { - var button; - - // View SVG Image - button = doc.createElement('button'); - button.innerHTML = 'View SVG Image'; - chart.renderTo.parentNode.appendChild(button); - button.onclick = function () { - var div = doc.createElement('div'); - div.innerHTML = chart.getSVGForExport(); - chart.renderTo.parentNode.appendChild(div); - }; - - // View SVG Source - button = doc.createElement('button'); - button.innerHTML = 'View SVG Source'; - chart.renderTo.parentNode.appendChild(button); - button.onclick = function () { - var pre = doc.createElement('pre'); - pre.innerHTML = chart.getSVGForExport() - .replace(//g, '>'); - chart.renderTo.parentNode.appendChild(pre); - }; - } - //*/ + function update(prop, options, redraw) { + chart.isDirtyExporting = true; + merge(true, chart.options[prop], options); + if (pick(redraw, true)) { + chart.redraw(); + } + + } + + chart.renderExporting(); + + addEvent(chart, 'redraw', chart.renderExporting); + + // Add update methods to handle chart.update and chart.exporting.update + // and chart.navigation.update. + each(['exporting', 'navigation'], function (prop) { + chart[prop] = { + update: function (options, redraw) { + update(prop, options, redraw); + } + }; + }); + + // Uncomment this to see a button directly below the chart, for quick + // testing of export + /* + if (!chart.renderer.forExport) { + var button; + + // View SVG Image + button = doc.createElement('button'); + button.innerHTML = 'View SVG Image'; + chart.renderTo.parentNode.appendChild(button); + button.onclick = function () { + var div = doc.createElement('div'); + div.innerHTML = chart.getSVGForExport(); + chart.renderTo.parentNode.appendChild(div); + }; + + // View SVG Source + button = doc.createElement('button'); + button.innerHTML = 'View SVG Source'; + chart.renderTo.parentNode.appendChild(button); + button.onclick = function () { + var pre = doc.createElement('pre'); + pre.innerHTML = chart.getSVGForExport() + .replace(//g, '>'); + chart.renderTo.parentNode.appendChild(pre); + }; + } + //*/ }); diff --git a/js/modules/funnel.src.js b/js/modules/funnel.src.js index e5bde8be7e7..c7dccfa5655 100644 --- a/js/modules/funnel.src.js +++ b/js/modules/funnel.src.js @@ -14,16 +14,16 @@ import '../parts/Series.js'; // create shortcuts var seriesType = Highcharts.seriesType, - seriesTypes = Highcharts.seriesTypes, - noop = Highcharts.noop, - pick = Highcharts.pick, - each = Highcharts.each; + seriesTypes = Highcharts.seriesTypes, + noop = Highcharts.noop, + pick = Highcharts.pick, + each = Highcharts.each; -seriesType('funnel', 'pie', +seriesType('funnel', 'pie', /** - * Funnel charts are a type of chart often used to visualize stages in a sales - * project, where the top are the initial stages with the most clients. + * Funnel charts are a type of chart often used to visualize stages in a sales + * project, where the top are the initial stages with the most clients. * It requires that the modules/funnel.js file is loaded. * * @sample highcharts/demo/funnel/ Funnel demo @@ -34,370 +34,370 @@ seriesType('funnel', 'pie', */ { - /** - * Initial animation is by default disabled for the funnel chart. - */ - animation: false, - - /** - * The center of the series. By default, it is centered in the middle - * of the plot area, so it fills the plot area height. - * - * @type {Array} - * @default ["50%", "50%"] - * @since 3.0 - * @product highcharts - */ - center: ['50%', '50%'], - - /** - * The width of the funnel compared to the width of the plot area, - * or the pixel width if it is a number. - * - * @type {Number|String} - * @since 3.0 - * @product highcharts - */ - width: '90%', - - /** - * The width of the neck, the lower part of the funnel. A number defines - * pixel width, a percentage string defines a percentage of the plot - * area width. - * - * @type {Number|String} - * @sample {highcharts} highcharts/demo/funnel/ Funnel demo - * @since 3.0 - * @product highcharts - */ - neckWidth: '30%', - - /** - * The height of the funnel or pyramid. If it is a number it defines - * the pixel height, if it is a percentage string it is the percentage - * of the plot area height. - * - * @type {Number|String} - * @sample {highcharts} highcharts/demo/funnel/ Funnel demo - * @since 3.0 - * @product highcharts - */ - height: '100%', - - /** - * The height of the neck, the lower part of the funnel. A number defines - * pixel width, a percentage string defines a percentage of the plot - * area height. - * - * @type {Number|String} - * @product highcharts - */ - neckHeight: '25%', - - /** - * A reversed funnel has the widest area down. A reversed funnel with - * no neck width and neck height is a pyramid. - * - * @since 3.0.10 - * @product highcharts - */ - reversed: false, - - /** - * @ignore - */ - size: true, // to avoid adapting to data label size in Pie.drawDataLabels - - /*= if (build.classic) { =*/ - // Presentational - - dataLabels: { - connectorWidth: 1 - }, - - /** - * Options for the series states. - * - * @product highcharts - */ - states: { - /** - * @excluding halo,marker,lineWidth,lineWidthPlus - * @apioption plotOptions.funnel.states.hover - */ - - /** - * Options for a selected funnel item. - * - * @excluding halo,marker,lineWidth,lineWidthPlus - * @product highcharts - */ - select: { - /** - * A specific color for the selected point. - * - * @type {Color} - * @default #cccccc - * @product highcharts highstock - */ - color: '${palette.neutralColor20}', - - /** - * A specific border color for the selected point. - * - * @type {Color} - * @default #000000 - * @product highcharts highstock - */ - borderColor: '${palette.neutralColor100}' - } - } - /*= } =*/ + /** + * Initial animation is by default disabled for the funnel chart. + */ + animation: false, + + /** + * The center of the series. By default, it is centered in the middle + * of the plot area, so it fills the plot area height. + * + * @type {Array} + * @default ["50%", "50%"] + * @since 3.0 + * @product highcharts + */ + center: ['50%', '50%'], + + /** + * The width of the funnel compared to the width of the plot area, + * or the pixel width if it is a number. + * + * @type {Number|String} + * @since 3.0 + * @product highcharts + */ + width: '90%', + + /** + * The width of the neck, the lower part of the funnel. A number defines + * pixel width, a percentage string defines a percentage of the plot + * area width. + * + * @type {Number|String} + * @sample {highcharts} highcharts/demo/funnel/ Funnel demo + * @since 3.0 + * @product highcharts + */ + neckWidth: '30%', + + /** + * The height of the funnel or pyramid. If it is a number it defines + * the pixel height, if it is a percentage string it is the percentage + * of the plot area height. + * + * @type {Number|String} + * @sample {highcharts} highcharts/demo/funnel/ Funnel demo + * @since 3.0 + * @product highcharts + */ + height: '100%', + + /** + * The height of the neck, the lower part of the funnel. A number defines + * pixel width, a percentage string defines a percentage of the plot + * area height. + * + * @type {Number|String} + * @product highcharts + */ + neckHeight: '25%', + + /** + * A reversed funnel has the widest area down. A reversed funnel with + * no neck width and neck height is a pyramid. + * + * @since 3.0.10 + * @product highcharts + */ + reversed: false, + + /** + * @ignore + */ + size: true, // to avoid adapting to data label size in Pie.drawDataLabels + + /*= if (build.classic) { =*/ + // Presentational + + dataLabels: { + connectorWidth: 1 + }, + + /** + * Options for the series states. + * + * @product highcharts + */ + states: { + /** + * @excluding halo,marker,lineWidth,lineWidthPlus + * @apioption plotOptions.funnel.states.hover + */ + + /** + * Options for a selected funnel item. + * + * @excluding halo,marker,lineWidth,lineWidthPlus + * @product highcharts + */ + select: { + /** + * A specific color for the selected point. + * + * @type {Color} + * @default #cccccc + * @product highcharts highstock + */ + color: '${palette.neutralColor20}', + + /** + * A specific border color for the selected point. + * + * @type {Color} + * @default #000000 + * @product highcharts highstock + */ + borderColor: '${palette.neutralColor100}' + } + } + /*= } =*/ }, // Properties { - animate: noop, - - /** - * Overrides the pie translate method - */ - translate: function () { - - var - // Get positions - either an integer or a percentage string - // must be given - getLength = function (length, relativeTo) { - return (/%$/).test(length) ? - relativeTo * parseInt(length, 10) / 100 : - parseInt(length, 10); - }, - - sum = 0, - series = this, - chart = series.chart, - options = series.options, - reversed = options.reversed, - ignoreHiddenPoint = options.ignoreHiddenPoint, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - cumulative = 0, // start at top - center = options.center, - centerX = getLength(center[0], plotWidth), - centerY = getLength(center[1], plotHeight), - width = getLength(options.width, plotWidth), - tempWidth, - getWidthAt, - height = getLength(options.height, plotHeight), - neckWidth = getLength(options.neckWidth, plotWidth), - neckHeight = getLength(options.neckHeight, plotHeight), - neckY = (centerY - height / 2) + height - neckHeight, - data = series.data, - path, - fraction, - half = options.dataLabels.position === 'left' ? 1 : 0, - - x1, - y1, - x2, - x3, - y3, - x4, - y5; - - // Return the width at a specific y coordinate - series.getWidthAt = getWidthAt = function (y) { - var top = (centerY - height / 2); - - return (y > neckY || height === neckHeight) ? - neckWidth : - neckWidth + (width - neckWidth) * - (1 - (y - top) / (height - neckHeight)); - }; - series.getX = function (y, half, point) { - return centerX + (half ? -1 : 1) * - ((getWidthAt(reversed ? 2 * centerY - y : y) / 2) + - point.labelDistance); - }; - - // Expose - series.center = [centerX, centerY, height]; - series.centerX = centerX; - - /* - * Individual point coordinate naming: - * - * x1,y1 _________________ x2,y1 - * \ / - * \ / - * \ / - * \ / - * \ / - * x3,y3 _________ x4,y3 - * - * Additional for the base of the neck: - * - * | | - * | | - * | | - * x3,y5 _________ x4,y5 - */ - - - - - // get the total sum - each(data, function (point) { - if (!ignoreHiddenPoint || point.visible !== false) { - sum += point.y; - } - }); - - each(data, function (point) { - // set start and end positions - y5 = null; - fraction = sum ? point.y / sum : 0; - y1 = centerY - height / 2 + cumulative * height; - y3 = y1 + fraction * height; - tempWidth = getWidthAt(y1); - x1 = centerX - tempWidth / 2; - x2 = x1 + tempWidth; - tempWidth = getWidthAt(y3); - x3 = centerX - tempWidth / 2; - x4 = x3 + tempWidth; - - // the entire point is within the neck - if (y1 > neckY) { - x1 = x3 = centerX - neckWidth / 2; - x2 = x4 = centerX + neckWidth / 2; - - // the base of the neck - } else if (y3 > neckY) { - y5 = y3; - - tempWidth = getWidthAt(neckY); - x3 = centerX - tempWidth / 2; - x4 = x3 + tempWidth; - - y3 = neckY; - } - - if (reversed) { - y1 = 2 * centerY - y1; - y3 = 2 * centerY - y3; - y5 = (y5 ? 2 * centerY - y5 : null); - } - // save the path - path = [ - 'M', - x1, y1, - 'L', - x2, y1, - x4, y3 - ]; - if (y5) { - path.push(x4, y5, x3, y5); - } - path.push(x3, y3, 'Z'); - - // prepare for using shared dr - point.shapeType = 'path'; - point.shapeArgs = { d: path }; - - - // for tooltips and data labels - point.percentage = fraction * 100; - point.plotX = centerX; - point.plotY = (y1 + (y5 || y3)) / 2; - - // Placement of tooltips and data labels - point.tooltipPos = [ - centerX, - point.plotY - ]; - - // Slice is a noop on funnel points - point.slice = noop; - - // Mimicking pie data label placement logic - point.half = half; - - if (!ignoreHiddenPoint || point.visible !== false) { - cumulative += fraction; - } - }); - }, - - /** - * Funnel items don't have angles (#2289) - */ - sortByAngle: function (points) { - points.sort(function (a, b) { - return a.plotY - b.plotY; - }); - }, - - /** - * Extend the pie data label method - */ - drawDataLabels: function () { - var series = this, - data = series.data, - labelDistance = series.options.dataLabels.distance, - leftSide, - sign, - point, - i = data.length, - x, - y; - - /** - * In the original pie label anticollision logic, the slots are - * distributed from one labelDistance above to one labelDistance - * below the pie. In funnels we don't want this. - */ - series.center[2] -= 2 * labelDistance; - - // Set the label position array for each point. - while (i--) { - point = data[i]; - leftSide = point.half; - sign = leftSide ? 1 : -1; - y = point.plotY; - point.labelDistance = pick( - point.options.dataLabels && point.options.dataLabels.distance, - labelDistance - ); - - series.maxLabelDistance = Math.max( - point.labelDistance, - series.maxLabelDistance || 0 - ); - x = series.getX(y, leftSide, point); - - // set the anchor point for data labels - point.labelPos = [ - // first break of connector - 0, - y, - - // second break, right outside point shape - x + (point.labelDistance - 5) * sign, - y, - - // landing point for connector - x + point.labelDistance * sign, - y, - - // alignment - leftSide ? 'right' : 'left', - // center angle - 0 - ]; - } - - seriesTypes.pie.prototype.drawDataLabels.call(this); - } + animate: noop, + + /** + * Overrides the pie translate method + */ + translate: function () { + + var + // Get positions - either an integer or a percentage string + // must be given + getLength = function (length, relativeTo) { + return (/%$/).test(length) ? + relativeTo * parseInt(length, 10) / 100 : + parseInt(length, 10); + }, + + sum = 0, + series = this, + chart = series.chart, + options = series.options, + reversed = options.reversed, + ignoreHiddenPoint = options.ignoreHiddenPoint, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + cumulative = 0, // start at top + center = options.center, + centerX = getLength(center[0], plotWidth), + centerY = getLength(center[1], plotHeight), + width = getLength(options.width, plotWidth), + tempWidth, + getWidthAt, + height = getLength(options.height, plotHeight), + neckWidth = getLength(options.neckWidth, plotWidth), + neckHeight = getLength(options.neckHeight, plotHeight), + neckY = (centerY - height / 2) + height - neckHeight, + data = series.data, + path, + fraction, + half = options.dataLabels.position === 'left' ? 1 : 0, + + x1, + y1, + x2, + x3, + y3, + x4, + y5; + + // Return the width at a specific y coordinate + series.getWidthAt = getWidthAt = function (y) { + var top = (centerY - height / 2); + + return (y > neckY || height === neckHeight) ? + neckWidth : + neckWidth + (width - neckWidth) * + (1 - (y - top) / (height - neckHeight)); + }; + series.getX = function (y, half, point) { + return centerX + (half ? -1 : 1) * + ((getWidthAt(reversed ? 2 * centerY - y : y) / 2) + + point.labelDistance); + }; + + // Expose + series.center = [centerX, centerY, height]; + series.centerX = centerX; + + /* + * Individual point coordinate naming: + * + * x1,y1 _________________ x2,y1 + * \ / + * \ / + * \ / + * \ / + * \ / + * x3,y3 _________ x4,y3 + * + * Additional for the base of the neck: + * + * | | + * | | + * | | + * x3,y5 _________ x4,y5 + */ + + + + + // get the total sum + each(data, function (point) { + if (!ignoreHiddenPoint || point.visible !== false) { + sum += point.y; + } + }); + + each(data, function (point) { + // set start and end positions + y5 = null; + fraction = sum ? point.y / sum : 0; + y1 = centerY - height / 2 + cumulative * height; + y3 = y1 + fraction * height; + tempWidth = getWidthAt(y1); + x1 = centerX - tempWidth / 2; + x2 = x1 + tempWidth; + tempWidth = getWidthAt(y3); + x3 = centerX - tempWidth / 2; + x4 = x3 + tempWidth; + + // the entire point is within the neck + if (y1 > neckY) { + x1 = x3 = centerX - neckWidth / 2; + x2 = x4 = centerX + neckWidth / 2; + + // the base of the neck + } else if (y3 > neckY) { + y5 = y3; + + tempWidth = getWidthAt(neckY); + x3 = centerX - tempWidth / 2; + x4 = x3 + tempWidth; + + y3 = neckY; + } + + if (reversed) { + y1 = 2 * centerY - y1; + y3 = 2 * centerY - y3; + y5 = (y5 ? 2 * centerY - y5 : null); + } + // save the path + path = [ + 'M', + x1, y1, + 'L', + x2, y1, + x4, y3 + ]; + if (y5) { + path.push(x4, y5, x3, y5); + } + path.push(x3, y3, 'Z'); + + // prepare for using shared dr + point.shapeType = 'path'; + point.shapeArgs = { d: path }; + + + // for tooltips and data labels + point.percentage = fraction * 100; + point.plotX = centerX; + point.plotY = (y1 + (y5 || y3)) / 2; + + // Placement of tooltips and data labels + point.tooltipPos = [ + centerX, + point.plotY + ]; + + // Slice is a noop on funnel points + point.slice = noop; + + // Mimicking pie data label placement logic + point.half = half; + + if (!ignoreHiddenPoint || point.visible !== false) { + cumulative += fraction; + } + }); + }, + + /** + * Funnel items don't have angles (#2289) + */ + sortByAngle: function (points) { + points.sort(function (a, b) { + return a.plotY - b.plotY; + }); + }, + + /** + * Extend the pie data label method + */ + drawDataLabels: function () { + var series = this, + data = series.data, + labelDistance = series.options.dataLabels.distance, + leftSide, + sign, + point, + i = data.length, + x, + y; + + /** + * In the original pie label anticollision logic, the slots are + * distributed from one labelDistance above to one labelDistance + * below the pie. In funnels we don't want this. + */ + series.center[2] -= 2 * labelDistance; + + // Set the label position array for each point. + while (i--) { + point = data[i]; + leftSide = point.half; + sign = leftSide ? 1 : -1; + y = point.plotY; + point.labelDistance = pick( + point.options.dataLabels && point.options.dataLabels.distance, + labelDistance + ); + + series.maxLabelDistance = Math.max( + point.labelDistance, + series.maxLabelDistance || 0 + ); + x = series.getX(y, leftSide, point); + + // set the anchor point for data labels + point.labelPos = [ + // first break of connector + 0, + y, + + // second break, right outside point shape + x + (point.labelDistance - 5) * sign, + y, + + // landing point for connector + x + point.labelDistance * sign, + y, + + // alignment + leftSide ? 'right' : 'left', + // center angle + 0 + ]; + } + + seriesTypes.pie.prototype.drawDataLabels.call(this); + } }); @@ -405,7 +405,7 @@ seriesType('funnel', 'pie', /** * A `funnel` series. If the [type](#series.funnel.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.funnel * @excluding dataParser,dataURL,stack,xAxis,yAxis @@ -416,19 +416,19 @@ seriesType('funnel', 'pie', /** * An array of data points for the series. For the `funnel` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 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.funnel.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * y: 3, @@ -440,7 +440,7 @@ seriesType('funnel', 'pie', * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.pie.data * @excluding sliced @@ -458,11 +458,11 @@ seriesType('funnel', 'pie', * @apioption series.funnel.data */ -/** +/** * Pyramid series type. */ -seriesType('pyramid', 'funnel', -/** +seriesType('pyramid', 'funnel', +/** * A pyramid series is a special type of funnel, without neck and reversed by * default. * @@ -474,38 +474,38 @@ seriesType('pyramid', 'funnel', */ { - /** - * The pyramid neck width is zero by default, as opposed to the funnel, - * which shares the same layout logic. - * - * @since 3.0.10 - * @product highcharts - */ - neckWidth: '0%', - - /** - * The pyramid neck width is zero by default, as opposed to the funnel, - * which shares the same layout logic. - * - * @since 3.0.10 - * @product highcharts - */ - neckHeight: '0%', - - /** - * The pyramid is reversed by default, as opposed to the funnel, which - * shares the layout engine, and is not reversed. - * - * @since 3.0.10 - * @product highcharts - */ - reversed: true + /** + * The pyramid neck width is zero by default, as opposed to the funnel, + * which shares the same layout logic. + * + * @since 3.0.10 + * @product highcharts + */ + neckWidth: '0%', + + /** + * The pyramid neck width is zero by default, as opposed to the funnel, + * which shares the same layout logic. + * + * @since 3.0.10 + * @product highcharts + */ + neckHeight: '0%', + + /** + * The pyramid is reversed by default, as opposed to the funnel, which + * shares the layout engine, and is not reversed. + * + * @since 3.0.10 + * @product highcharts + */ + reversed: true }); /** * A `pyramid` series. If the [type](#series.pyramid.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.pyramid * @excluding dataParser,dataURL,stack,xAxis,yAxis @@ -516,19 +516,19 @@ seriesType('pyramid', 'funnel', /** * An array of data points for the series. For the `pyramid` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 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.pyramid.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * y: 9, @@ -540,7 +540,7 @@ seriesType('pyramid', 'funnel', * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.pie.data * @excluding sliced diff --git a/js/modules/histogram.src.js b/js/modules/histogram.src.js index 694c59bf168..96b1b7d8cb2 100644 --- a/js/modules/histogram.src.js +++ b/js/modules/histogram.src.js @@ -12,13 +12,13 @@ import H from '../parts/Globals.js'; import derivedSeriesMixin from '../mixins/derived-series.js'; var each = H.each, - objectEach = H.objectEach, - seriesType = H.seriesType, - correctFloat = H.correctFloat, - isNumber = H.isNumber, - arrayMax = H.arrayMax, - arrayMin = H.arrayMin, - merge = H.merge; + objectEach = H.objectEach, + seriesType = H.seriesType, + correctFloat = H.correctFloat, + isNumber = H.isNumber, + arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + merge = H.merge; /* *************************************************************************** * @@ -31,29 +31,29 @@ var each = H.each, * base series **/ var binsNumberFormulas = { - 'square-root': function (baseSeries) { - return Math.round(Math.sqrt(baseSeries.options.data.length)); - }, + 'square-root': function (baseSeries) { + return Math.round(Math.sqrt(baseSeries.options.data.length)); + }, - 'sturges': function (baseSeries) { - return Math.ceil(Math.log(baseSeries.options.data.length) * Math.LOG2E); - }, + 'sturges': function (baseSeries) { + return Math.ceil(Math.log(baseSeries.options.data.length) * Math.LOG2E); + }, - 'rice': function (baseSeries) { - return Math.ceil(2 * Math.pow(baseSeries.options.data.length, 1 / 3)); - } + 'rice': function (baseSeries) { + return Math.ceil(2 * Math.pow(baseSeries.options.data.length, 1 / 3)); + } }; /** * Returns a function for mapping number to the closed (right opened) bins - * + * * @param {number} binWidth - width of the bin * @returns {function} **/ function fitToBinLeftClosed(binWidth) { - return function (y) { - return Math.floor(y / binWidth) * binWidth; - }; + return function (y) { + return Math.floor(y / binWidth) * binWidth; + }; } /** @@ -64,12 +64,12 @@ function fitToBinLeftClosed(binWidth) { * @returns {number} **/ function identity(y) { - return y; + return y; } /** * Histogram class - * + * * @constructor seriesTypes.histogram * @augments seriesTypes.column * @mixes DerivedSeriesMixin @@ -89,113 +89,113 @@ function identity(y) { **/ seriesType('histogram', 'column', { /** - * A preferable number of bins. It is a suggestion, so a histogram may have - * a different number of bins. By default it is set to the square root - * of the base series' data length. Available options are: `square-root`, - * `sturges`, `rice`. You can also define a function which takes a - * `baseSeries` as a parameter and should return a positive integer. - * - * @type {String|Number|Function} - * @validvalue ["square-root", "sturges", "rice"] - */ - binsNumber: 'square-root', + * A preferable number of bins. It is a suggestion, so a histogram may have + * a different number of bins. By default it is set to the square root + * of the base series' data length. Available options are: `square-root`, + * `sturges`, `rice`. You can also define a function which takes a + * `baseSeries` as a parameter and should return a positive integer. + * + * @type {String|Number|Function} + * @validvalue ["square-root", "sturges", "rice"] + */ + binsNumber: 'square-root', /** - * Width of each bin. By default the bin's width is calculated as - * `(max - min) / number of bins`. This option takes precedence over - * [binsNumber](#plotOptions.histogram.binsNumber). - * - * @type {Number} - */ - binWidth: undefined, - pointPadding: 0, - groupPadding: 0, - grouping: false, - pointPlacement: 'between', - tooltip: { - headerFormat: '', - pointFormat: '{point.x} - {point.x2}' + - '
' + - '\u25CF' + - ' {series.name} {point.y}
' - } + * Width of each bin. By default the bin's width is calculated as + * `(max - min) / number of bins`. This option takes precedence over + * [binsNumber](#plotOptions.histogram.binsNumber). + * + * @type {Number} + */ + binWidth: undefined, + pointPadding: 0, + groupPadding: 0, + grouping: false, + pointPlacement: 'between', + tooltip: { + headerFormat: '', + pointFormat: '{point.x} - {point.x2}' + + '
' + + '\u25CF' + + ' {series.name} {point.y}
' + } }, merge(derivedSeriesMixin, { - setDerivedData: function () { - var data = this.derivedData( - this.baseSeries.yData, - this.binsNumber(), - this.options.binWidth - ); - - this.setData(data, false); - }, - - derivedData: function (baseData, binsNumber, binWidth) { - var max = arrayMax(baseData), - min = arrayMin(baseData), - frequencies = {}, - data = [], - x, - fitToBin; - - binWidth = this.binWidth = isNumber(binWidth) ? - binWidth : - (max - min) / binsNumber; - - fitToBin = binWidth ? fitToBinLeftClosed(binWidth) : identity; - - // If binWidth is 0 then max and min are equaled, - // increment the x with some positive value to quit the loop - for ( - x = fitToBin(min); - x <= max; - x = correctFloat(x + (binWidth || 1)) - ) { - frequencies[correctFloat(fitToBin((x)))] = 0; - } - - each(baseData, function (y) { - var x = correctFloat(fitToBin(y)); - frequencies[x]++; - }); - - objectEach(frequencies, function (frequency, x) { - data.push({ - x: Number(x), - y: frequency, - x2: correctFloat(Number(x) + binWidth) - }); - }); - - data.sort(function (a, b) { - return a.x - b.x; - }); - - return data; - }, - - binsNumber: function () { - var binsNumberOption = this.options.binsNumber; - var binsNumber = binsNumberFormulas[binsNumberOption] || - // #7457 - (typeof binsNumberOption === 'function' && binsNumberOption); - - return Math.ceil( - (binsNumber && binsNumber(this.baseSeries)) || - ( - isNumber(binsNumberOption) ? - binsNumberOption : - binsNumberFormulas['square-root'](this.baseSeries) - ) - ); - } + setDerivedData: function () { + var data = this.derivedData( + this.baseSeries.yData, + this.binsNumber(), + this.options.binWidth + ); + + this.setData(data, false); + }, + + derivedData: function (baseData, binsNumber, binWidth) { + var max = arrayMax(baseData), + min = arrayMin(baseData), + frequencies = {}, + data = [], + x, + fitToBin; + + binWidth = this.binWidth = isNumber(binWidth) ? + binWidth : + (max - min) / binsNumber; + + fitToBin = binWidth ? fitToBinLeftClosed(binWidth) : identity; + + // If binWidth is 0 then max and min are equaled, + // increment the x with some positive value to quit the loop + for ( + x = fitToBin(min); + x <= max; + x = correctFloat(x + (binWidth || 1)) + ) { + frequencies[correctFloat(fitToBin((x)))] = 0; + } + + each(baseData, function (y) { + var x = correctFloat(fitToBin(y)); + frequencies[x]++; + }); + + objectEach(frequencies, function (frequency, x) { + data.push({ + x: Number(x), + y: frequency, + x2: correctFloat(Number(x) + binWidth) + }); + }); + + data.sort(function (a, b) { + return a.x - b.x; + }); + + return data; + }, + + binsNumber: function () { + var binsNumberOption = this.options.binsNumber; + var binsNumber = binsNumberFormulas[binsNumberOption] || + // #7457 + (typeof binsNumberOption === 'function' && binsNumberOption); + + return Math.ceil( + (binsNumber && binsNumber(this.baseSeries)) || + ( + isNumber(binsNumberOption) ? + binsNumberOption : + binsNumberFormulas['square-root'](this.baseSeries) + ) + ); + } })); /** * A `histogram` series. If the [type](#series.histogram.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.histogram @@ -217,7 +217,7 @@ seriesType('histogram', 'column', { * An array of data points for the series. For the `histogram` series type, * points are calculated dynamically. See * [histogram.baseSeries](#series.histogram.baseSeries). - * + * * @type {Array} * @since 6.0.0 * @extends series.column.data diff --git a/js/modules/item-series.src.js b/js/modules/item-series.src.js index 118f24e3d24..462055e01c9 100644 --- a/js/modules/item-series.src.js +++ b/js/modules/item-series.src.js @@ -17,93 +17,93 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Series.js'; var each = H.each, - extend = H.extend, - pick = H.pick, - seriesType = H.seriesType; + extend = H.extend, + pick = H.pick, + seriesType = H.seriesType; seriesType('item', 'column', { - itemPadding: 0.2, - marker: { - symbol: 'circle', - states: { - hover: {}, - select: {} - } - } + itemPadding: 0.2, + marker: { + symbol: 'circle', + states: { + hover: {}, + select: {} + } + } }, { - drawPoints: function () { - var series = this, - renderer = series.chart.renderer, - seriesMarkerOptions = this.options.marker; + drawPoints: function () { + var series = this, + renderer = series.chart.renderer, + seriesMarkerOptions = this.options.marker; - each(this.points, function (point) { - var yPos, - attr, - graphics, - itemY, - pointAttr, - pointMarkerOptions = point.marker || {}, - symbol = ( - pointMarkerOptions.symbol || - seriesMarkerOptions.symbol - ), - size, - yTop; + each(this.points, function (point) { + var yPos, + attr, + graphics, + itemY, + pointAttr, + pointMarkerOptions = point.marker || {}, + symbol = ( + pointMarkerOptions.symbol || + seriesMarkerOptions.symbol + ), + size, + yTop; - point.graphics = graphics = point.graphics || {}; - pointAttr = point.pointAttr ? - ( - point.pointAttr[point.selected ? 'selected' : ''] || - series.pointAttr[''] - ) : - series.pointAttribs(point, point.selected && 'select'); - delete pointAttr.r; + point.graphics = graphics = point.graphics || {}; + pointAttr = point.pointAttr ? + ( + point.pointAttr[point.selected ? 'selected' : ''] || + series.pointAttr[''] + ) : + series.pointAttribs(point, point.selected && 'select'); + delete pointAttr.r; - if (point.y !== null) { + if (point.y !== null) { - if (!point.graphic) { - point.graphic = renderer.g('point').add(series.group); - } + if (!point.graphic) { + point.graphic = renderer.g('point').add(series.group); + } - itemY = point.y; - yTop = pick(point.stackY, point.y); - size = Math.min( - point.pointWidth, - ( - series.yAxis.transA * - (1 - series.options.itemPadding) - ) - ); - for (yPos = yTop; yPos > yTop - point.y; yPos--) { + itemY = point.y; + yTop = pick(point.stackY, point.y); + size = Math.min( + point.pointWidth, + ( + series.yAxis.transA * + (1 - series.options.itemPadding) + ) + ); + for (yPos = yTop; yPos > yTop - point.y; yPos--) { - attr = { - x: point.barX + point.pointWidth / 2 - size / 2, - y: series.yAxis.toPixels(yPos, true) - size / 2, - width: size, - height: size - }; - - if (graphics[itemY]) { - graphics[itemY].animate(attr); - } else { - graphics[itemY] = renderer.symbol(symbol) - .attr(extend(attr, pointAttr)) - .add(point.graphic); - } - graphics[itemY].isActive = true; - itemY--; - } - } - H.objectEach(graphics, function (graphic, key) { - if (!graphic.isActive) { - graphic.destroy(); - delete graphic[key]; - } else { - graphic.isActive = false; - } - }); - }); + attr = { + x: point.barX + point.pointWidth / 2 - size / 2, + y: series.yAxis.toPixels(yPos, true) - size / 2, + width: size, + height: size + }; - } + if (graphics[itemY]) { + graphics[itemY].animate(attr); + } else { + graphics[itemY] = renderer.symbol(symbol) + .attr(extend(attr, pointAttr)) + .add(point.graphic); + } + graphics[itemY].isActive = true; + itemY--; + } + } + H.objectEach(graphics, function (graphic, key) { + if (!graphic.isActive) { + graphic.destroy(); + delete graphic[key]; + } else { + graphic.isActive = false; + } + }); + }); + + } }); diff --git a/js/modules/keyboard-navigation.src.js b/js/modules/keyboard-navigation.src.js index b1ce37b32ad..63fa0887ec6 100644 --- a/js/modules/keyboard-navigation.src.js +++ b/js/modules/keyboard-navigation.src.js @@ -6,7 +6,7 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from '../parts/Globals.js'; import '../parts/Utilities.js'; @@ -17,51 +17,51 @@ import '../parts/Tooltip.js'; import '../parts/SvgRenderer.js'; var win = H.win, - doc = win.document, - each = H.each, - addEvent = H.addEvent, - fireEvent = H.fireEvent, - merge = H.merge, - pick = H.pick, - hasSVGFocusSupport; + doc = win.document, + each = H.each, + addEvent = H.addEvent, + fireEvent = H.fireEvent, + merge = H.merge, + pick = H.pick, + hasSVGFocusSupport; // Add focus border functionality to SVGElements. // Draws a new rect on top of element around its bounding box. H.extend(H.SVGElement.prototype, { - addFocusBorder: function (margin, style) { - // Allow updating by just adding new border - if (this.focusBorder) { - this.removeFocusBorder(); - } - // Add the border rect - var bb = this.getBBox(), - pad = pick(margin, 3); - this.focusBorder = this.renderer.rect( - bb.x - pad, - bb.y - pad, - bb.width + 2 * pad, - bb.height + 2 * pad, - style && style.borderRadius - ) - .addClass('highcharts-focus-border') - /*= if (build.classic) { =*/ - .attr({ - stroke: style && style.stroke, - 'stroke-width': style && style.strokeWidth - }) - /*= } =*/ - .attr({ - zIndex: 99 - }) + addFocusBorder: function (margin, style) { + // Allow updating by just adding new border + if (this.focusBorder) { + this.removeFocusBorder(); + } + // Add the border rect + var bb = this.getBBox(), + pad = pick(margin, 3); + this.focusBorder = this.renderer.rect( + bb.x - pad, + bb.y - pad, + bb.width + 2 * pad, + bb.height + 2 * pad, + style && style.borderRadius + ) + .addClass('highcharts-focus-border') + /*= if (build.classic) { =*/ + .attr({ + stroke: style && style.stroke, + 'stroke-width': style && style.strokeWidth + }) + /*= } =*/ + .attr({ + zIndex: 99 + }) .add(this.parentGroup); - }, - - removeFocusBorder: function () { - if (this.focusBorder) { - this.focusBorder.destroy(); - delete this.focusBorder; - } - } + }, + + removeFocusBorder: function () { + if (this.focusBorder) { + this.focusBorder.destroy(); + delete this.focusBorder; + } + } }); @@ -69,9 +69,9 @@ H.extend(H.SVGElement.prototype, { // up/down arrows, and which series types should just move to next series. H.Series.prototype.keyboardMoveVertical = true; each(['column', 'pie'], function (type) { - if (H.seriesTypes[type]) { - H.seriesTypes[type].prototype.keyboardMoveVertical = false; - } + if (H.seriesTypes[type]) { + H.seriesTypes[type].prototype.keyboardMoveVertical = false; + } }); @@ -82,7 +82,7 @@ each(['column', 'pie'], function (type) { * @return {String} The filtered string */ function stripTags(s) { - return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s; + return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s; } @@ -90,143 +90,143 @@ function stripTags(s) { * Set default keyboard navigation options */ H.setOptions({ - accessibility: { - - /** - * Options for keyboard navigation. - * - * @type {Object} - * @since 5.0.0 - * @apioption accessibility.keyboardNavigation - */ - keyboardNavigation: { - - /** - * Enable keyboard navigation for the chart. - * - * @type {Boolean} - * @default true - * @since 5.0.0 - * @apioption accessibility.keyboardNavigation.enabled - */ - enabled: true, - - - /** - * Options for the focus border drawn around elements while - * navigating through them. - * - * @type {Object} - * @sample highcharts/accessibility/custom-focus - * Custom focus ring - * @since 6.0.3 - * @apioption accessibility.keyboardNavigation.focusBorder - */ - focusBorder: { - /** - * Enable/disable focus border for chart. - * - * @type {Boolean} - * @default true - * @since 6.0.3 - * @apioption accessibility.keyboardNavigation.focusBorder.enabled - */ - enabled: true, - - /** - * Hide the browser's default focus indicator. - * - * @type {Boolean} - * @default true - * @since 6.0.4 - * @apioption accessibility.keyboardNavigation.focusBorder.hideBrowserFocusOutline - */ - hideBrowserFocusOutline: true, - - /** - * Style options for the focus border drawn around elements - * while navigating through them. Note that some browsers in - * addition draw their own borders for focused elements. These - * automatic borders can not be styled by Highcharts. - * - * In styled mode, the border is given the - * `.highcharts-focus-border` class. - * - * @type {Object} - * @since 6.0.3 - * @apioption accessibility.keyboardNavigation.focusBorder.style - */ - style: { - /** - * Color of the focus border. - * - * @type {Color} - * @default #000000 - * @since 6.0.3 - * @apioption accessibility.keyboardNavigation.focusBorder.style.color - */ - color: '${palette.highlightColor80}', - /** - * Line width of the focus border. - * - * @type {Number} - * @default 2 - * @since 6.0.3 - * @apioption accessibility.keyboardNavigation.focusBorder.style.lineWidth - */ - lineWidth: 2, - /** - * Border radius of the focus border. - * - * @type {Number} - * @default 3 - * @since 6.0.3 - * @apioption accessibility.keyboardNavigation.focusBorder.style.borderRadius - */ - borderRadius: 3 - }, - - /** - * Focus border margin around the elements. - * - * @type {Number} - * @default 2 - * @since 6.0.3 - * @apioption accessibility.keyboardNavigation.focusBorder.margin - */ - margin: 2 - }, - - /** - * Set the keyboard navigation mode for the chart. Can be "normal" - * or "serialize". In normal mode, left/right arrow keys move - * between points in a series, while up/down arrow keys move between - * series. Up/down navigation acts intelligently to figure out which - * series makes sense to move to from any given point. - * - * In "serialize" mode, points are instead navigated as a single - * list. Left/right behaves as in "normal" mode. Up/down arrow keys - * will behave like left/right. This is useful for unifying - * navigation behavior with/without screen readers enabled. - * - * @type {String} - * @default normal - * @since 6.0.4 - * @apioption accessibility.keyboardNavigation.mode - */ - - /** - * Skip null points when navigating through points with the - * keyboard. - * - * @type {Boolean} - * @default true - * @since 5.0.0 - * @apioption accessibility.keyboardNavigation.skipNullPoints - */ - skipNullPoints: true - } - } + accessibility: { + + /** + * Options for keyboard navigation. + * + * @type {Object} + * @since 5.0.0 + * @apioption accessibility.keyboardNavigation + */ + keyboardNavigation: { + + /** + * Enable keyboard navigation for the chart. + * + * @type {Boolean} + * @default true + * @since 5.0.0 + * @apioption accessibility.keyboardNavigation.enabled + */ + enabled: true, + + + /** + * Options for the focus border drawn around elements while + * navigating through them. + * + * @type {Object} + * @sample highcharts/accessibility/custom-focus + * Custom focus ring + * @since 6.0.3 + * @apioption accessibility.keyboardNavigation.focusBorder + */ + focusBorder: { + /** + * Enable/disable focus border for chart. + * + * @type {Boolean} + * @default true + * @since 6.0.3 + * @apioption accessibility.keyboardNavigation.focusBorder.enabled + */ + enabled: true, + + /** + * Hide the browser's default focus indicator. + * + * @type {Boolean} + * @default true + * @since 6.0.4 + * @apioption accessibility.keyboardNavigation.focusBorder.hideBrowserFocusOutline + */ + hideBrowserFocusOutline: true, + + /** + * Style options for the focus border drawn around elements + * while navigating through them. Note that some browsers in + * addition draw their own borders for focused elements. These + * automatic borders can not be styled by Highcharts. + * + * In styled mode, the border is given the + * `.highcharts-focus-border` class. + * + * @type {Object} + * @since 6.0.3 + * @apioption accessibility.keyboardNavigation.focusBorder.style + */ + style: { + /** + * Color of the focus border. + * + * @type {Color} + * @default #000000 + * @since 6.0.3 + * @apioption accessibility.keyboardNavigation.focusBorder.style.color + */ + color: '${palette.highlightColor80}', + /** + * Line width of the focus border. + * + * @type {Number} + * @default 2 + * @since 6.0.3 + * @apioption accessibility.keyboardNavigation.focusBorder.style.lineWidth + */ + lineWidth: 2, + /** + * Border radius of the focus border. + * + * @type {Number} + * @default 3 + * @since 6.0.3 + * @apioption accessibility.keyboardNavigation.focusBorder.style.borderRadius + */ + borderRadius: 3 + }, + + /** + * Focus border margin around the elements. + * + * @type {Number} + * @default 2 + * @since 6.0.3 + * @apioption accessibility.keyboardNavigation.focusBorder.margin + */ + margin: 2 + }, + + /** + * Set the keyboard navigation mode for the chart. Can be "normal" + * or "serialize". In normal mode, left/right arrow keys move + * between points in a series, while up/down arrow keys move between + * series. Up/down navigation acts intelligently to figure out which + * series makes sense to move to from any given point. + * + * In "serialize" mode, points are instead navigated as a single + * list. Left/right behaves as in "normal" mode. Up/down arrow keys + * will behave like left/right. This is useful for unifying + * navigation behavior with/without screen readers enabled. + * + * @type {String} + * @default normal + * @since 6.0.4 + * @apioption accessibility.keyboardNavigation.mode + */ + + /** + * Skip null points when navigating through points with the + * keyboard. + * + * @type {Boolean} + * @default true + * @since 5.0.0 + * @apioption accessibility.keyboardNavigation.skipNullPoints + */ + skipNullPoints: true + } + } }); /** @@ -238,7 +238,7 @@ H.setOptions({ /** * Enable/disable keyboard navigation for the legend. Requires the Accessibility * module. - * + * * @type {Boolean} * @see [accessibility.keyboardNavigation]( * #accessibility.keyboardNavigation.enabled) @@ -257,209 +257,209 @@ H.setOptions({ // once before moving to next/prev module. // The chart object keeps track of a list of KeyboardNavigationModules. function KeyboardNavigationModule(chart, options) { - this.chart = chart; - this.id = options.id; - this.keyCodeMap = options.keyCodeMap; - this.validate = options.validate; - this.init = options.init; - this.terminate = options.terminate; + this.chart = chart; + this.id = options.id; + this.keyCodeMap = options.keyCodeMap; + this.validate = options.validate; + this.init = options.init; + this.terminate = options.terminate; } KeyboardNavigationModule.prototype = { - // Find handler function(s) for key code in the keyCodeMap and run it. - run: function (e) { - var navModule = this, - keyCode = e.which || e.keyCode, - found = false, - handled = false; - each(this.keyCodeMap, function (codeSet) { - if (codeSet[0].indexOf(keyCode) > -1) { - found = true; - handled = codeSet[1].call(navModule, keyCode, e) === false ? - // If explicitly returning false, we haven't handled it - false : - true; - } - }); - // Default tab handler, move to next/prev module - if (!found && keyCode === 9) { - handled = this.move(e.shiftKey ? -1 : 1); - } - return handled; - }, - - // Move to next/prev valid module, or undefined if none, and init - // it. Returns true on success and false if there is no valid module - // to move to. - move: function (direction) { - var chart = this.chart; - if (this.terminate) { - this.terminate(direction); - } - chart.keyboardNavigationModuleIndex += direction; - var newModule = chart.keyboardNavigationModules[ - chart.keyboardNavigationModuleIndex - ]; - - // Remove existing focus border if any - if (chart.focusElement) { - chart.focusElement.removeFocusBorder(); - } - - // Verify new module - if (newModule) { - if (newModule.validate && !newModule.validate()) { - return this.move(direction); // Invalid module, recurse - } - if (newModule.init) { - newModule.init(direction); // Valid module, init it - return true; - } - } - // No module - chart.keyboardNavigationModuleIndex = 0; // Reset counter - - // Set focus to chart or exit anchor depending on direction - if (direction > 0) { - this.chart.exiting = true; - this.chart.tabExitAnchor.focus(); - } else { - this.chart.renderTo.focus(); - } - - return false; - } + // Find handler function(s) for key code in the keyCodeMap and run it. + run: function (e) { + var navModule = this, + keyCode = e.which || e.keyCode, + found = false, + handled = false; + each(this.keyCodeMap, function (codeSet) { + if (codeSet[0].indexOf(keyCode) > -1) { + found = true; + handled = codeSet[1].call(navModule, keyCode, e) === false ? + // If explicitly returning false, we haven't handled it + false : + true; + } + }); + // Default tab handler, move to next/prev module + if (!found && keyCode === 9) { + handled = this.move(e.shiftKey ? -1 : 1); + } + return handled; + }, + + // Move to next/prev valid module, or undefined if none, and init + // it. Returns true on success and false if there is no valid module + // to move to. + move: function (direction) { + var chart = this.chart; + if (this.terminate) { + this.terminate(direction); + } + chart.keyboardNavigationModuleIndex += direction; + var newModule = chart.keyboardNavigationModules[ + chart.keyboardNavigationModuleIndex + ]; + + // Remove existing focus border if any + if (chart.focusElement) { + chart.focusElement.removeFocusBorder(); + } + + // Verify new module + if (newModule) { + if (newModule.validate && !newModule.validate()) { + return this.move(direction); // Invalid module, recurse + } + if (newModule.init) { + newModule.init(direction); // Valid module, init it + return true; + } + } + // No module + chart.keyboardNavigationModuleIndex = 0; // Reset counter + + // Set focus to chart or exit anchor depending on direction + if (direction > 0) { + this.chart.exiting = true; + this.chart.tabExitAnchor.focus(); + } else { + this.chart.renderTo.focus(); + } + + return false; + } }; // Utility function to attempt to fake a click event on an element function fakeClickEvent(element) { - var fakeEvent; - if (element && element.onclick && doc.createEvent) { - fakeEvent = doc.createEvent('Events'); - fakeEvent.initEvent('click', true, false); - element.onclick(fakeEvent); - } + var fakeEvent; + if (element && element.onclick && doc.createEvent) { + fakeEvent = doc.createEvent('Events'); + fakeEvent.initEvent('click', true, false); + element.onclick(fakeEvent); + } } // Determine if a point should be skipped function isSkipPoint(point) { - var a11yOptions = point.series.chart.options.accessibility; - return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints || - point.series.options.skipKeyboardNavigation || - !point.series.visible || - point.visible === false || - // Skip all points in a series where pointDescriptionThreshold is - // reached - (a11yOptions.pointDescriptionThreshold && - a11yOptions.pointDescriptionThreshold <= point.series.points.length); + var a11yOptions = point.series.chart.options.accessibility; + return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints || + point.series.options.skipKeyboardNavigation || + !point.series.visible || + point.visible === false || + // Skip all points in a series where pointDescriptionThreshold is + // reached + (a11yOptions.pointDescriptionThreshold && + a11yOptions.pointDescriptionThreshold <= point.series.points.length); } // Get the point in a series that is closest (in distance) to a reference point // Optionally supply weight factors for x and y directions function getClosestPoint(point, series, xWeight, yWeight) { - var minDistance = Infinity, - dPoint, - minIx, - distance, - i = series.points.length; - if (point.plotX === undefined || point.plotY === undefined) { - return; - } - while (i--) { - dPoint = series.points[i]; - if (dPoint.plotX === undefined || dPoint.plotY === undefined) { - continue; - } - distance = (point.plotX - dPoint.plotX) * - (point.plotX - dPoint.plotX) * (xWeight || 1) + - (point.plotY - dPoint.plotY) * - (point.plotY - dPoint.plotY) * (yWeight || 1); - if (distance < minDistance) { - minDistance = distance; - minIx = i; - } - } - return minIx !== undefined && series.points[minIx]; + var minDistance = Infinity, + dPoint, + minIx, + distance, + i = series.points.length; + if (point.plotX === undefined || point.plotY === undefined) { + return; + } + while (i--) { + dPoint = series.points[i]; + if (dPoint.plotX === undefined || dPoint.plotY === undefined) { + continue; + } + distance = (point.plotX - dPoint.plotX) * + (point.plotX - dPoint.plotX) * (xWeight || 1) + + (point.plotY - dPoint.plotY) * + (point.plotY - dPoint.plotY) * (yWeight || 1); + if (distance < minDistance) { + minDistance = distance; + minIx = i; + } + } + return minIx !== undefined && series.points[minIx]; } // Pan along axis in a direction (1 or -1), optionally with a defined // granularity (number of steps it takes to walk across current view) H.Axis.prototype.panStep = function (direction, granularity) { - var gran = granularity || 3, - extremes = this.getExtremes(), - step = (extremes.max - extremes.min) / gran * direction, - newMax = extremes.max + step, - newMin = extremes.min + step, - size = newMax - newMin; - if (direction < 0 && newMin < extremes.dataMin) { - newMin = extremes.dataMin; - newMax = newMin + size; - } else if (direction > 0 && newMax > extremes.dataMax) { - newMax = extremes.dataMax; - newMin = newMax - size; - } - this.setExtremes(newMin, newMax); + var gran = granularity || 3, + extremes = this.getExtremes(), + step = (extremes.max - extremes.min) / gran * direction, + newMax = extremes.max + step, + newMin = extremes.min + step, + size = newMax - newMin; + if (direction < 0 && newMin < extremes.dataMin) { + newMin = extremes.dataMin; + newMax = newMin + size; + } else if (direction > 0 && newMax > extremes.dataMax) { + newMax = extremes.dataMax; + newMin = newMax - size; + } + this.setExtremes(newMin, newMax); }; // Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus -// border. If the focusElement argument is supplied, it draws the border around +// border. If the focusElement argument is supplied, it draws the border around // svgElement and sets the focus to focusElement. H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) { - var focusBorderOptions = this.options.accessibility - .keyboardNavigation.focusBorder, - browserFocusElement = focusElement || svgElement; - // Set browser focus if possible - if ( - browserFocusElement.element && - browserFocusElement.element.focus - ) { - browserFocusElement.element.focus(); - // Hide default focus ring - if (focusBorderOptions.hideBrowserFocusOutline) { - browserFocusElement.css({ outline: 'none' }); - } - } - if (focusBorderOptions.enabled) { - // Remove old focus border - if (this.focusElement) { - this.focusElement.removeFocusBorder(); - } - // Draw focus border (since some browsers don't do it automatically) - svgElement.addFocusBorder(focusBorderOptions.margin, { - stroke: focusBorderOptions.style.color, - strokeWidth: focusBorderOptions.style.lineWidth, - borderRadius: focusBorderOptions.style.borderRadius - }); - this.focusElement = svgElement; - } + var focusBorderOptions = this.options.accessibility + .keyboardNavigation.focusBorder, + browserFocusElement = focusElement || svgElement; + // Set browser focus if possible + if ( + browserFocusElement.element && + browserFocusElement.element.focus + ) { + browserFocusElement.element.focus(); + // Hide default focus ring + if (focusBorderOptions.hideBrowserFocusOutline) { + browserFocusElement.css({ outline: 'none' }); + } + } + if (focusBorderOptions.enabled) { + // Remove old focus border + if (this.focusElement) { + this.focusElement.removeFocusBorder(); + } + // Draw focus border (since some browsers don't do it automatically) + svgElement.addFocusBorder(focusBorderOptions.margin, { + stroke: focusBorderOptions.style.color, + strokeWidth: focusBorderOptions.style.lineWidth, + borderRadius: focusBorderOptions.style.borderRadius + }); + this.focusElement = svgElement; + } }; // Highlight a point (show tooltip and display hover state). Returns the // highlighted point. H.Point.prototype.highlight = function () { - var chart = this.series.chart; - if (!this.isNull) { - this.onMouseOver(); // Show the hover marker and tooltip - } else { - if (chart.tooltip) { - chart.tooltip.hide(0); - } - // Don't call blur on the element, as it messes up the chart div's focus - } - - // We focus only after calling onMouseOver because the state change can - // change z-index and mess up the element. - if (this.graphic) { - chart.setFocusToElement(this.graphic); - } - - chart.highlightedPoint = this; - return this; + var chart = this.series.chart; + if (!this.isNull) { + this.onMouseOver(); // Show the hover marker and tooltip + } else { + if (chart.tooltip) { + chart.tooltip.hide(0); + } + // Don't call blur on the element, as it messes up the chart div's focus + } + + // We focus only after calling onMouseOver because the state change can + // change z-index and mess up the element. + if (this.graphic) { + chart.setFocusToElement(this.graphic); + } + + chart.highlightedPoint = this; + return this; }; @@ -467,60 +467,60 @@ H.Point.prototype.highlight = function () { // Returns highlighted point on success, false on failure (no adjacent point to // highlight in chosen direction) H.Chart.prototype.highlightAdjacentPoint = function (next) { - var chart = this, - series = chart.series, - curPoint = chart.highlightedPoint, - curPointIndex = curPoint && curPoint.index || 0, - curPoints = curPoint && curPoint.series.points, - lastSeries = chart.series && chart.series[chart.series.length - 1], - lastPoint = lastSeries && lastSeries.points && - lastSeries.points[lastSeries.points.length - 1], - newSeries, - newPoint; - - // If no points, return false - if (!series[0] || !series[0].points) { - return false; - } - - if (!curPoint) { - // No point is highlighted yet. Try first/last point depending on move - // direction - newPoint = next ? series[0].points[0] : lastPoint; - } else { - // We have a highlighted point. - // Find index of current point in series.points array. Necessary for - // dataGrouping (and maybe zoom?) - if (curPoints[curPointIndex] !== curPoint) { - for (var i = 0; i < curPoints.length; ++i) { - if (curPoints[i] === curPoint) { - curPointIndex = i; - break; - } - } - } - - // Grab next/prev point & series - newSeries = series[curPoint.series.index + (next ? 1 : -1)]; - newPoint = curPoints[curPointIndex + (next ? 1 : -1)] || - // Done with this series, try next one - newSeries && - newSeries.points[next ? 0 : newSeries.points.length - 1]; - - // If there is no adjacent point, we return false - if (!newPoint) { - return false; - } - } - - // Recursively skip null points or points in series that should be skipped - if (isSkipPoint(newPoint)) { - chart.highlightedPoint = newPoint; - return chart.highlightAdjacentPoint(next); - } - - // There is an adjacent point, highlight it - return newPoint.highlight(); + var chart = this, + series = chart.series, + curPoint = chart.highlightedPoint, + curPointIndex = curPoint && curPoint.index || 0, + curPoints = curPoint && curPoint.series.points, + lastSeries = chart.series && chart.series[chart.series.length - 1], + lastPoint = lastSeries && lastSeries.points && + lastSeries.points[lastSeries.points.length - 1], + newSeries, + newPoint; + + // If no points, return false + if (!series[0] || !series[0].points) { + return false; + } + + if (!curPoint) { + // No point is highlighted yet. Try first/last point depending on move + // direction + newPoint = next ? series[0].points[0] : lastPoint; + } else { + // We have a highlighted point. + // Find index of current point in series.points array. Necessary for + // dataGrouping (and maybe zoom?) + if (curPoints[curPointIndex] !== curPoint) { + for (var i = 0; i < curPoints.length; ++i) { + if (curPoints[i] === curPoint) { + curPointIndex = i; + break; + } + } + } + + // Grab next/prev point & series + newSeries = series[curPoint.series.index + (next ? 1 : -1)]; + newPoint = curPoints[curPointIndex + (next ? 1 : -1)] || + // Done with this series, try next one + newSeries && + newSeries.points[next ? 0 : newSeries.points.length - 1]; + + // If there is no adjacent point, we return false + if (!newPoint) { + return false; + } + } + + // Recursively skip null points or points in series that should be skipped + if (isSkipPoint(newPoint)) { + chart.highlightedPoint = newPoint; + return chart.highlightAdjacentPoint(next); + } + + // There is an adjacent point, highlight it + return newPoint.highlight(); }; @@ -528,640 +528,640 @@ H.Chart.prototype.highlightAdjacentPoint = function (next) { // highlighted, otherwise false. If there is a highlighted point in the series, // use that as starting point. H.Series.prototype.highlightFirstValidPoint = function () { - var curPoint = this.chart.highlightedPoint, - start = (curPoint && curPoint.series) === this ? curPoint.index : 0, - points = this.points; - - if (points) { - for (var i = start, len = points.length; i < len; ++i) { - if (!isSkipPoint(points[i])) { - return points[i].highlight(); - } - } - for (var j = start; j >= 0; --j) { - if (!isSkipPoint(points[j])) { - return points[j].highlight(); - } - } - } - return false; + var curPoint = this.chart.highlightedPoint, + start = (curPoint && curPoint.series) === this ? curPoint.index : 0, + points = this.points; + + if (points) { + for (var i = start, len = points.length; i < len; ++i) { + if (!isSkipPoint(points[i])) { + return points[i].highlight(); + } + } + for (var j = start; j >= 0; --j) { + if (!isSkipPoint(points[j])) { + return points[j].highlight(); + } + } + } + return false; }; // Highlight next/previous series in chart. Returns false if no adjacent series // in the direction, otherwise returns new highlighted point. H.Chart.prototype.highlightAdjacentSeries = function (down) { - var chart = this, - newSeries, - newPoint, - adjacentNewPoint, - curPoint = chart.highlightedPoint, - lastSeries = chart.series && chart.series[chart.series.length - 1], - lastPoint = lastSeries && lastSeries.points && - lastSeries.points[lastSeries.points.length - 1]; - - // If no point is highlighted, highlight the first/last point - if (!chart.highlightedPoint) { - newSeries = down ? (chart.series && chart.series[0]) : lastSeries; - newPoint = down ? - (newSeries && newSeries.points && newSeries.points[0]) : lastPoint; - return newPoint ? newPoint.highlight() : false; - } - - newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)]; - - if (!newSeries) { - return false; - } - - // We have a new series in this direction, find the right point - // Weigh xDistance as counting much higher than Y distance - newPoint = getClosestPoint(curPoint, newSeries, 4); - - if (!newPoint) { - return false; - } - - // New series and point exists, but we might want to skip it - if (!newSeries.visible) { - // Skip the series - newPoint.highlight(); - adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse - if (!adjacentNewPoint) { - // Recurse failed - curPoint.highlight(); - return false; - } - // Recurse succeeded - return adjacentNewPoint; - } - - // Highlight the new point or any first valid point back or forwards from it - newPoint.highlight(); - return newPoint.series.highlightFirstValidPoint(); + var chart = this, + newSeries, + newPoint, + adjacentNewPoint, + curPoint = chart.highlightedPoint, + lastSeries = chart.series && chart.series[chart.series.length - 1], + lastPoint = lastSeries && lastSeries.points && + lastSeries.points[lastSeries.points.length - 1]; + + // If no point is highlighted, highlight the first/last point + if (!chart.highlightedPoint) { + newSeries = down ? (chart.series && chart.series[0]) : lastSeries; + newPoint = down ? + (newSeries && newSeries.points && newSeries.points[0]) : lastPoint; + return newPoint ? newPoint.highlight() : false; + } + + newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)]; + + if (!newSeries) { + return false; + } + + // We have a new series in this direction, find the right point + // Weigh xDistance as counting much higher than Y distance + newPoint = getClosestPoint(curPoint, newSeries, 4); + + if (!newPoint) { + return false; + } + + // New series and point exists, but we might want to skip it + if (!newSeries.visible) { + // Skip the series + newPoint.highlight(); + adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse + if (!adjacentNewPoint) { + // Recurse failed + curPoint.highlight(); + return false; + } + // Recurse succeeded + return adjacentNewPoint; + } + + // Highlight the new point or any first valid point back or forwards from it + newPoint.highlight(); + return newPoint.series.highlightFirstValidPoint(); }; // Highlight the closest point vertically H.Chart.prototype.highlightAdjacentPointVertical = function (down) { - var curPoint = this.highlightedPoint, - minDistance = Infinity, - bestPoint; - - if (curPoint.plotX === undefined || curPoint.plotY === undefined) { - return false; - } - each(this.series, function (series) { - each(series.points, function (point) { - if (point.plotY === undefined || point.plotX === undefined || - point === curPoint) { - return; - } - var yDistance = point.plotY - curPoint.plotY, - width = Math.abs(point.plotX - curPoint.plotX), - distance = Math.abs(yDistance) * Math.abs(yDistance) + - width * width * 4; // Weigh horizontal distance highly - - // Reverse distance number if axis is reversed - if (series.yAxis.reversed) { - yDistance *= -1; - } - - if ( - yDistance < 0 && down || yDistance > 0 && !down || // Wrong dir - distance < 5 || // Points in same spot => infinite loop - isSkipPoint(point) - ) { - return; - } - - if (distance < minDistance) { - minDistance = distance; - bestPoint = point; - } - }); - }); - - return bestPoint ? bestPoint.highlight() : false; + var curPoint = this.highlightedPoint, + minDistance = Infinity, + bestPoint; + + if (curPoint.plotX === undefined || curPoint.plotY === undefined) { + return false; + } + each(this.series, function (series) { + each(series.points, function (point) { + if (point.plotY === undefined || point.plotX === undefined || + point === curPoint) { + return; + } + var yDistance = point.plotY - curPoint.plotY, + width = Math.abs(point.plotX - curPoint.plotX), + distance = Math.abs(yDistance) * Math.abs(yDistance) + + width * width * 4; // Weigh horizontal distance highly + + // Reverse distance number if axis is reversed + if (series.yAxis.reversed) { + yDistance *= -1; + } + + if ( + yDistance < 0 && down || yDistance > 0 && !down || // Wrong dir + distance < 5 || // Points in same spot => infinite loop + isSkipPoint(point) + ) { + return; + } + + if (distance < minDistance) { + minDistance = distance; + bestPoint = point; + } + }); + }); + + return bestPoint ? bestPoint.highlight() : false; }; // Show the export menu and focus the first item (if exists) H.Chart.prototype.showExportMenu = function () { - if (this.exportSVGElements && this.exportSVGElements[0]) { - this.exportSVGElements[0].element.onclick(); - this.highlightExportItem(0); - } + if (this.exportSVGElements && this.exportSVGElements[0]) { + this.exportSVGElements[0].element.onclick(); + this.highlightExportItem(0); + } }; // Hide export menu H.Chart.prototype.hideExportMenu = function () { - var exportList = this.exportDivElements; - if (exportList) { - each(exportList, function (el) { - fireEvent(el, 'mouseleave'); - }); - if ( - exportList[this.highlightedExportItem] && - exportList[this.highlightedExportItem].onmouseout - ) { - exportList[this.highlightedExportItem].onmouseout(); - } - this.highlightedExportItem = 0; - if (hasSVGFocusSupport) { - // Only focus if we can set focus back to the elements after - // destroying the menu (#7422) - this.renderTo.focus(); - } - } + var exportList = this.exportDivElements; + if (exportList) { + each(exportList, function (el) { + fireEvent(el, 'mouseleave'); + }); + if ( + exportList[this.highlightedExportItem] && + exportList[this.highlightedExportItem].onmouseout + ) { + exportList[this.highlightedExportItem].onmouseout(); + } + this.highlightedExportItem = 0; + if (hasSVGFocusSupport) { + // Only focus if we can set focus back to the elements after + // destroying the menu (#7422) + this.renderTo.focus(); + } + } }; // Highlight export menu item by index H.Chart.prototype.highlightExportItem = function (ix) { - var listItem = this.exportDivElements && this.exportDivElements[ix], - curHighlighted = - this.exportDivElements && - this.exportDivElements[this.highlightedExportItem]; - - if ( - listItem && - listItem.tagName === 'DIV' && - !(listItem.children && listItem.children.length) - ) { - if (listItem.focus && hasSVGFocusSupport) { - // Only focus if we can set focus back to the elements after - // destroying the menu (#7422) - listItem.focus(); - } - if (curHighlighted && curHighlighted.onmouseout) { - curHighlighted.onmouseout(); - } - if (listItem.onmouseover) { - listItem.onmouseover(); - } - this.highlightedExportItem = ix; - return true; - } + var listItem = this.exportDivElements && this.exportDivElements[ix], + curHighlighted = + this.exportDivElements && + this.exportDivElements[this.highlightedExportItem]; + + if ( + listItem && + listItem.tagName === 'DIV' && + !(listItem.children && listItem.children.length) + ) { + if (listItem.focus && hasSVGFocusSupport) { + // Only focus if we can set focus back to the elements after + // destroying the menu (#7422) + listItem.focus(); + } + if (curHighlighted && curHighlighted.onmouseout) { + curHighlighted.onmouseout(); + } + if (listItem.onmouseover) { + listItem.onmouseover(); + } + this.highlightedExportItem = ix; + return true; + } }; // Try to highlight the last valid export menu item H.Chart.prototype.highlightLastExportItem = function () { - var chart = this, - i; - if (chart.exportDivElements) { - i = chart.exportDivElements.length; - while (i--) { - if (chart.highlightExportItem(i)) { - break; - } - } - } + var chart = this, + i; + if (chart.exportDivElements) { + i = chart.exportDivElements.length; + while (i--) { + if (chart.highlightExportItem(i)) { + break; + } + } + } }; // Highlight range selector button by index H.Chart.prototype.highlightRangeSelectorButton = function (ix) { - var buttons = this.rangeSelector.buttons; - // Deselect old - if (buttons[this.highlightedRangeSelectorItemIx]) { - buttons[this.highlightedRangeSelectorItemIx].setState( - this.oldRangeSelectorItemState || 0 - ); - } - // Select new - this.highlightedRangeSelectorItemIx = ix; - if (buttons[ix]) { - this.setFocusToElement(buttons[ix].box, buttons[ix]); - this.oldRangeSelectorItemState = buttons[ix].state; - buttons[ix].setState(2); - return true; - } - return false; + var buttons = this.rangeSelector.buttons; + // Deselect old + if (buttons[this.highlightedRangeSelectorItemIx]) { + buttons[this.highlightedRangeSelectorItemIx].setState( + this.oldRangeSelectorItemState || 0 + ); + } + // Select new + this.highlightedRangeSelectorItemIx = ix; + if (buttons[ix]) { + this.setFocusToElement(buttons[ix].box, buttons[ix]); + this.oldRangeSelectorItemState = buttons[ix].state; + buttons[ix].setState(2); + return true; + } + return false; }; // Highlight legend item by index H.Chart.prototype.highlightLegendItem = function (ix) { - var items = this.legend.allItems, - oldIx = this.highlightedLegendItemIx; - if (items[ix]) { - if (items[oldIx]) { - fireEvent( - items[oldIx].legendGroup.element, - 'mouseout' - ); - } - // Scroll if we have to - if (items[ix].pageIx !== undefined && - items[ix].pageIx + 1 !== this.legend.currentPage) { - this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage); - } - // Focus - this.highlightedLegendItemIx = ix; - this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup); - fireEvent(items[ix].legendGroup.element, 'mouseover'); - return true; - } - return false; + var items = this.legend.allItems, + oldIx = this.highlightedLegendItemIx; + if (items[ix]) { + if (items[oldIx]) { + fireEvent( + items[oldIx].legendGroup.element, + 'mouseout' + ); + } + // Scroll if we have to + if (items[ix].pageIx !== undefined && + items[ix].pageIx + 1 !== this.legend.currentPage) { + this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage); + } + // Focus + this.highlightedLegendItemIx = ix; + this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup); + fireEvent(items[ix].legendGroup.element, 'mouseover'); + return true; + } + return false; }; // Add keyboard navigation handling modules to chart H.Chart.prototype.addKeyboardNavigationModules = function () { - var chart = this; - - function navModuleFactory(id, keyMap, options) { - return new KeyboardNavigationModule(chart, merge({ - keyCodeMap: keyMap - }, { id: id }, options)); - } - - // List of the different keyboard handling modes we use depending on where - // we are in the chart. Each mode has a set of handling functions mapped to - // key codes. Each mode determines when to move to the next/prev mode. - chart.keyboardNavigationModules = [ - // Entry point catching the first tab, allowing users to tab into points - // more intuitively. - navModuleFactory('entry', []), - - // Points - navModuleFactory('points', [ - // Left/Right - [[37, 39], function (keyCode) { - var right = keyCode === 39; - if (!chart.highlightAdjacentPoint(right)) { - // Failed to highlight next, wrap to last/first - return this.init(right ? 1 : -1); - } - return true; - }], - // Up/Down - [[38, 40], function (keyCode) { - var down = keyCode !== 38, - navOptions = chart.options.accessibility.keyboardNavigation; - if (navOptions.mode && navOptions.mode === 'serialize') { - // Act like left/right - if (!chart.highlightAdjacentPoint(down)) { - return this.init(down ? 1 : -1); - } - return true; - } - // Normal mode, move between series - var highlightMethod = chart.highlightedPoint && - chart.highlightedPoint.series.keyboardMoveVertical ? - 'highlightAdjacentPointVertical' : - 'highlightAdjacentSeries'; - chart[highlightMethod](down); - return true; - }], - // Enter/Spacebar - [[13, 32], function () { - if (chart.highlightedPoint) { - chart.highlightedPoint.firePointEvent('click'); - } - }] - ], { - // Always start highlighting from scratch when entering this module - init: function (dir) { - var numSeries = chart.series.length, - i = dir > 0 ? 0 : numSeries, - res; - if (dir > 0) { - delete chart.highlightedPoint; - // Find first valid point to highlight - while (i < numSeries) { - res = chart.series[i].highlightFirstValidPoint(); - if (res) { - return res; - } - ++i; - } - } else { - // Find last valid point to highlight - while (i--) { - chart.highlightedPoint = chart.series[i].points[ - chart.series[i].points.length - 1 - ]; - // Highlight first valid point in the series will also - // look backwards. It always starts from currently - // highlighted point. - res = chart.series[i].highlightFirstValidPoint(); - if (res) { - return res; - } - } - } - }, - // If leaving points, don't show tooltip anymore - terminate: function () { - if (chart.tooltip) { - chart.tooltip.hide(0); - } - delete chart.highlightedPoint; - } - }), - - // Exporting - navModuleFactory('exporting', [ - // Left/Up - [[37, 38], function () { - var i = chart.highlightedExportItem || 0, - reachedEnd = true; - // Try to highlight prev item in list. Highlighting e.g. - // separators will fail. - while (i--) { - if (chart.highlightExportItem(i)) { - reachedEnd = false; - break; - } - } - if (reachedEnd) { - chart.highlightLastExportItem(); - return true; - } - }], - // Right/Down - [[39, 40], function () { - var highlightedExportItem = chart.highlightedExportItem || 0, - reachedEnd = true; - // Try to highlight next item in list. Highlighting e.g. - // separators will fail. - for ( - var i = highlightedExportItem + 1; - i < chart.exportDivElements.length; - ++i - ) { - if (chart.highlightExportItem(i)) { - reachedEnd = false; - break; - } - } - if (reachedEnd) { - chart.highlightExportItem(0); - return true; - } - }], - // Enter/Spacebar - [[13, 32], function () { - fakeClickEvent( - chart.exportDivElements[chart.highlightedExportItem] - ); - }] - ], { - // Only run exporting navigation if exporting support exists and is - // enabled on chart - validate: function () { - return ( - chart.exportChart && - !( - chart.options.exporting && - chart.options.exporting.enabled === false - ) - ); - }, - // Show export menu - init: function (direction) { - chart.highlightedPoint = null; - chart.showExportMenu(); - // If coming back to export menu from other module, try to - // highlight last item in menu - if (direction < 0) { - chart.highlightLastExportItem(); - } - }, - // Hide the menu - terminate: function () { - chart.hideExportMenu(); - } - }), - - // Map zoom - navModuleFactory('mapZoom', [ - // Up/down/left/right - [[38, 40, 37, 39], function (keyCode) { - chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0] - .panStep(keyCode < 39 ? -1 : 1); - }], - - // Tabs - [[9], function (keyCode, e) { - var button; - // Deselect old - chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0); - if ( - e.shiftKey && !chart.focusedMapNavButtonIx || - !e.shiftKey && chart.focusedMapNavButtonIx - ) { // trying to go somewhere we can't? - chart.mapZoom(); // Reset zoom - // Nowhere to go, go to prev/next module - return this.move(e.shiftKey ? -1 : 1); - } - chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1; - button = chart.mapNavButtons[chart.focusedMapNavButtonIx]; - chart.setFocusToElement(button.box, button); - button.setState(2); - }], - - // Enter/Spacebar - [[13, 32], function () { - fakeClickEvent( - chart.mapNavButtons[chart.focusedMapNavButtonIx].element - ); - }] - ], { - // Only run this module if we have map zoom on the chart - validate: function () { - return ( - chart.mapZoom && - chart.mapNavButtons && - chart.mapNavButtons.length === 2 - ); - }, - - // Make zoom buttons do their magic - init: function (direction) { - var zoomIn = chart.mapNavButtons[0], - zoomOut = chart.mapNavButtons[1], - initialButton = direction > 0 ? zoomIn : zoomOut; - - each(chart.mapNavButtons, function (button, i) { - button.element.setAttribute('tabindex', -1); - button.element.setAttribute('role', 'button'); - button.element.setAttribute( - 'aria-label', - chart.langFormat( - 'accessibility.mapZoom' + (i ? 'Out' : 'In'), - { chart: chart } - ) - ); - }); - - chart.setFocusToElement(initialButton.box, initialButton); - initialButton.setState(2); - chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1; - } - }), - - // Highstock range selector (minus input boxes) - navModuleFactory('rangeSelector', [ - // Left/Right/Up/Down - [[37, 39, 38, 40], function (keyCode) { - var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1; - // Try to highlight next/prev button - if ( - !chart.highlightRangeSelectorButton( - chart.highlightedRangeSelectorItemIx + direction - ) - ) { - return this.move(direction); - } - }], - // Enter/Spacebar - [[13, 32], function () { - // Don't allow click if button used to be disabled - if (chart.oldRangeSelectorItemState !== 3) { - fakeClickEvent( - chart.rangeSelector.buttons[ - chart.highlightedRangeSelectorItemIx - ].element - ); - } - }] - ], { - // Only run this module if we have range selector - validate: function () { - return ( - chart.rangeSelector && - chart.rangeSelector.buttons && - chart.rangeSelector.buttons.length - ); - }, - - // Make elements focusable and accessible - init: function (direction) { - each(chart.rangeSelector.buttons, function (button) { - button.element.setAttribute('tabindex', '-1'); - button.element.setAttribute('role', 'button'); - button.element.setAttribute( - 'aria-label', - chart.langFormat( - 'accessibility.rangeSelectorButton', - { - chart: chart, - buttonText: button.text && button.text.textStr - } - ) - ); - }); - // Focus first/last button - chart.highlightRangeSelectorButton( - direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1 - ); - } - }), - - // Highstock range selector, input boxes - navModuleFactory('rangeSelectorInput', [ - // Tab/Up/Down - [[9, 38, 40], function (keyCode, e) { - var direction = - (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1, - - newIx = chart.highlightedInputRangeIx = - chart.highlightedInputRangeIx + direction; - - // Try to highlight next/prev item in list. - if (newIx > 1 || newIx < 0) { // Out of range - return this.move(direction); - } - chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus(); - }] - ], { - // Only run if we have range selector with input boxes - validate: function () { - var inputVisible = ( - chart.rangeSelector && - chart.rangeSelector.inputGroup && - chart.rangeSelector.inputGroup.element - .getAttribute('visibility') !== 'hidden' - ); - return ( - inputVisible && - chart.options.rangeSelector.inputEnabled !== false && - chart.rangeSelector.minInput && - chart.rangeSelector.maxInput - ); - }, - - // Highlight first/last input box - init: function (direction) { - chart.highlightedInputRangeIx = direction > 0 ? 0 : 1; - chart.rangeSelector[ - chart.highlightedInputRangeIx ? 'maxInput' : 'minInput' - ].focus(); - } - }), - - // Legend navigation - navModuleFactory('legend', [ - // Left/Right/Up/Down - [[37, 39, 38, 40], function (keyCode) { - var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1; - // Try to highlight next/prev legend item - if (!chart.highlightLegendItem( - chart.highlightedLegendItemIx + direction - ) && chart.legend.allItems.length > 1) { - // Wrap around if more than 1 item - this.init(direction); - } - }], - // Enter/Spacebar - [[13, 32], function () { - fakeClickEvent( - chart.legend.allItems[ - chart.highlightedLegendItemIx - ].legendItem.element.parentNode - ); - }] - ], { - // Only run this module if we have at least one legend - wait for - // it - item. Don't run if the legend is populated by a colorAxis. - // Don't run if legend navigation is disabled. - validate: function () { - return chart.legend && chart.legend.allItems && - chart.legend.display && - !(chart.colorAxis && chart.colorAxis.length) && - (chart.options.legend && - chart.options.legend.keyboardNavigation && - chart.options.legend.keyboardNavigation.enabled) !== false; - }, - - // Make elements focusable and accessible - init: function (direction) { - each(chart.legend.allItems, function (item) { - item.legendGroup.element.setAttribute('tabindex', '-1'); - item.legendGroup.element.setAttribute('role', 'button'); - item.legendGroup.element.setAttribute( - 'aria-label', - chart.langFormat( - 'accessibility.legendItem', - { - chart: chart, - itemName: stripTags(item.name) - } - ) - ); - }); - // Focus first/last item - chart.highlightLegendItem( - direction > 0 ? 0 : chart.legend.allItems.length - 1 - ); - } - }) - ]; + var chart = this; + + function navModuleFactory(id, keyMap, options) { + return new KeyboardNavigationModule(chart, merge({ + keyCodeMap: keyMap + }, { id: id }, options)); + } + + // List of the different keyboard handling modes we use depending on where + // we are in the chart. Each mode has a set of handling functions mapped to + // key codes. Each mode determines when to move to the next/prev mode. + chart.keyboardNavigationModules = [ + // Entry point catching the first tab, allowing users to tab into points + // more intuitively. + navModuleFactory('entry', []), + + // Points + navModuleFactory('points', [ + // Left/Right + [[37, 39], function (keyCode) { + var right = keyCode === 39; + if (!chart.highlightAdjacentPoint(right)) { + // Failed to highlight next, wrap to last/first + return this.init(right ? 1 : -1); + } + return true; + }], + // Up/Down + [[38, 40], function (keyCode) { + var down = keyCode !== 38, + navOptions = chart.options.accessibility.keyboardNavigation; + if (navOptions.mode && navOptions.mode === 'serialize') { + // Act like left/right + if (!chart.highlightAdjacentPoint(down)) { + return this.init(down ? 1 : -1); + } + return true; + } + // Normal mode, move between series + var highlightMethod = chart.highlightedPoint && + chart.highlightedPoint.series.keyboardMoveVertical ? + 'highlightAdjacentPointVertical' : + 'highlightAdjacentSeries'; + chart[highlightMethod](down); + return true; + }], + // Enter/Spacebar + [[13, 32], function () { + if (chart.highlightedPoint) { + chart.highlightedPoint.firePointEvent('click'); + } + }] + ], { + // Always start highlighting from scratch when entering this module + init: function (dir) { + var numSeries = chart.series.length, + i = dir > 0 ? 0 : numSeries, + res; + if (dir > 0) { + delete chart.highlightedPoint; + // Find first valid point to highlight + while (i < numSeries) { + res = chart.series[i].highlightFirstValidPoint(); + if (res) { + return res; + } + ++i; + } + } else { + // Find last valid point to highlight + while (i--) { + chart.highlightedPoint = chart.series[i].points[ + chart.series[i].points.length - 1 + ]; + // Highlight first valid point in the series will also + // look backwards. It always starts from currently + // highlighted point. + res = chart.series[i].highlightFirstValidPoint(); + if (res) { + return res; + } + } + } + }, + // If leaving points, don't show tooltip anymore + terminate: function () { + if (chart.tooltip) { + chart.tooltip.hide(0); + } + delete chart.highlightedPoint; + } + }), + + // Exporting + navModuleFactory('exporting', [ + // Left/Up + [[37, 38], function () { + var i = chart.highlightedExportItem || 0, + reachedEnd = true; + // Try to highlight prev item in list. Highlighting e.g. + // separators will fail. + while (i--) { + if (chart.highlightExportItem(i)) { + reachedEnd = false; + break; + } + } + if (reachedEnd) { + chart.highlightLastExportItem(); + return true; + } + }], + // Right/Down + [[39, 40], function () { + var highlightedExportItem = chart.highlightedExportItem || 0, + reachedEnd = true; + // Try to highlight next item in list. Highlighting e.g. + // separators will fail. + for ( + var i = highlightedExportItem + 1; + i < chart.exportDivElements.length; + ++i + ) { + if (chart.highlightExportItem(i)) { + reachedEnd = false; + break; + } + } + if (reachedEnd) { + chart.highlightExportItem(0); + return true; + } + }], + // Enter/Spacebar + [[13, 32], function () { + fakeClickEvent( + chart.exportDivElements[chart.highlightedExportItem] + ); + }] + ], { + // Only run exporting navigation if exporting support exists and is + // enabled on chart + validate: function () { + return ( + chart.exportChart && + !( + chart.options.exporting && + chart.options.exporting.enabled === false + ) + ); + }, + // Show export menu + init: function (direction) { + chart.highlightedPoint = null; + chart.showExportMenu(); + // If coming back to export menu from other module, try to + // highlight last item in menu + if (direction < 0) { + chart.highlightLastExportItem(); + } + }, + // Hide the menu + terminate: function () { + chart.hideExportMenu(); + } + }), + + // Map zoom + navModuleFactory('mapZoom', [ + // Up/down/left/right + [[38, 40, 37, 39], function (keyCode) { + chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0] + .panStep(keyCode < 39 ? -1 : 1); + }], + + // Tabs + [[9], function (keyCode, e) { + var button; + // Deselect old + chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0); + if ( + e.shiftKey && !chart.focusedMapNavButtonIx || + !e.shiftKey && chart.focusedMapNavButtonIx + ) { // trying to go somewhere we can't? + chart.mapZoom(); // Reset zoom + // Nowhere to go, go to prev/next module + return this.move(e.shiftKey ? -1 : 1); + } + chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1; + button = chart.mapNavButtons[chart.focusedMapNavButtonIx]; + chart.setFocusToElement(button.box, button); + button.setState(2); + }], + + // Enter/Spacebar + [[13, 32], function () { + fakeClickEvent( + chart.mapNavButtons[chart.focusedMapNavButtonIx].element + ); + }] + ], { + // Only run this module if we have map zoom on the chart + validate: function () { + return ( + chart.mapZoom && + chart.mapNavButtons && + chart.mapNavButtons.length === 2 + ); + }, + + // Make zoom buttons do their magic + init: function (direction) { + var zoomIn = chart.mapNavButtons[0], + zoomOut = chart.mapNavButtons[1], + initialButton = direction > 0 ? zoomIn : zoomOut; + + each(chart.mapNavButtons, function (button, i) { + button.element.setAttribute('tabindex', -1); + button.element.setAttribute('role', 'button'); + button.element.setAttribute( + 'aria-label', + chart.langFormat( + 'accessibility.mapZoom' + (i ? 'Out' : 'In'), + { chart: chart } + ) + ); + }); + + chart.setFocusToElement(initialButton.box, initialButton); + initialButton.setState(2); + chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1; + } + }), + + // Highstock range selector (minus input boxes) + navModuleFactory('rangeSelector', [ + // Left/Right/Up/Down + [[37, 39, 38, 40], function (keyCode) { + var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1; + // Try to highlight next/prev button + if ( + !chart.highlightRangeSelectorButton( + chart.highlightedRangeSelectorItemIx + direction + ) + ) { + return this.move(direction); + } + }], + // Enter/Spacebar + [[13, 32], function () { + // Don't allow click if button used to be disabled + if (chart.oldRangeSelectorItemState !== 3) { + fakeClickEvent( + chart.rangeSelector.buttons[ + chart.highlightedRangeSelectorItemIx + ].element + ); + } + }] + ], { + // Only run this module if we have range selector + validate: function () { + return ( + chart.rangeSelector && + chart.rangeSelector.buttons && + chart.rangeSelector.buttons.length + ); + }, + + // Make elements focusable and accessible + init: function (direction) { + each(chart.rangeSelector.buttons, function (button) { + button.element.setAttribute('tabindex', '-1'); + button.element.setAttribute('role', 'button'); + button.element.setAttribute( + 'aria-label', + chart.langFormat( + 'accessibility.rangeSelectorButton', + { + chart: chart, + buttonText: button.text && button.text.textStr + } + ) + ); + }); + // Focus first/last button + chart.highlightRangeSelectorButton( + direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1 + ); + } + }), + + // Highstock range selector, input boxes + navModuleFactory('rangeSelectorInput', [ + // Tab/Up/Down + [[9, 38, 40], function (keyCode, e) { + var direction = + (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1, + + newIx = chart.highlightedInputRangeIx = + chart.highlightedInputRangeIx + direction; + + // Try to highlight next/prev item in list. + if (newIx > 1 || newIx < 0) { // Out of range + return this.move(direction); + } + chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus(); + }] + ], { + // Only run if we have range selector with input boxes + validate: function () { + var inputVisible = ( + chart.rangeSelector && + chart.rangeSelector.inputGroup && + chart.rangeSelector.inputGroup.element + .getAttribute('visibility') !== 'hidden' + ); + return ( + inputVisible && + chart.options.rangeSelector.inputEnabled !== false && + chart.rangeSelector.minInput && + chart.rangeSelector.maxInput + ); + }, + + // Highlight first/last input box + init: function (direction) { + chart.highlightedInputRangeIx = direction > 0 ? 0 : 1; + chart.rangeSelector[ + chart.highlightedInputRangeIx ? 'maxInput' : 'minInput' + ].focus(); + } + }), + + // Legend navigation + navModuleFactory('legend', [ + // Left/Right/Up/Down + [[37, 39, 38, 40], function (keyCode) { + var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1; + // Try to highlight next/prev legend item + if (!chart.highlightLegendItem( + chart.highlightedLegendItemIx + direction + ) && chart.legend.allItems.length > 1) { + // Wrap around if more than 1 item + this.init(direction); + } + }], + // Enter/Spacebar + [[13, 32], function () { + fakeClickEvent( + chart.legend.allItems[ + chart.highlightedLegendItemIx + ].legendItem.element.parentNode + ); + }] + ], { + // Only run this module if we have at least one legend - wait for + // it - item. Don't run if the legend is populated by a colorAxis. + // Don't run if legend navigation is disabled. + validate: function () { + return chart.legend && chart.legend.allItems && + chart.legend.display && + !(chart.colorAxis && chart.colorAxis.length) && + (chart.options.legend && + chart.options.legend.keyboardNavigation && + chart.options.legend.keyboardNavigation.enabled) !== false; + }, + + // Make elements focusable and accessible + init: function (direction) { + each(chart.legend.allItems, function (item) { + item.legendGroup.element.setAttribute('tabindex', '-1'); + item.legendGroup.element.setAttribute('role', 'button'); + item.legendGroup.element.setAttribute( + 'aria-label', + chart.langFormat( + 'accessibility.legendItem', + { + chart: chart, + itemName: stripTags(item.name) + } + ) + ); + }); + // Focus first/last item + chart.highlightLegendItem( + direction > 0 ? 0 : chart.legend.allItems.length - 1 + ); + } + }) + ]; }; @@ -1172,77 +1172,77 @@ H.Chart.prototype.addKeyboardNavigationModules = function () { // order to navigate from the end of the chart. // Function returns the unbind function for the exit anchor's event handler. H.Chart.prototype.addExitAnchor = function () { - var chart = this; - chart.tabExitAnchor = doc.createElement('div'); - chart.tabExitAnchor.setAttribute('tabindex', '0'); - - // Hide exit anchor - merge(true, chart.tabExitAnchor.style, { - position: 'absolute', - left: '-9999px', - top: 'auto', - width: '1px', - height: '1px', - overflow: 'hidden' - }); - - chart.renderTo.appendChild(chart.tabExitAnchor); - return addEvent(chart.tabExitAnchor, 'focus', - function (ev) { - var e = ev || win.event, - curModule; - - // If focusing and we are exiting, do nothing once. - if (!chart.exiting) { - - // Not exiting, means we are coming in backwards - chart.renderTo.focus(); - e.preventDefault(); - - // Move to last valid keyboard nav module - // Note the we don't run it, just set the index - chart.keyboardNavigationModuleIndex = - chart.keyboardNavigationModules.length - 1; - curModule = chart.keyboardNavigationModules[ - chart.keyboardNavigationModuleIndex - ]; - - // Validate the module - if (curModule.validate && !curModule.validate()) { - // Invalid. - // Move inits next valid module in direction - curModule.move(-1); - } else { - // We have a valid module, init it - curModule.init(-1); - } - - } else { - // Don't skip the next focus, we only skip once. - chart.exiting = false; - } - } - ); + var chart = this; + chart.tabExitAnchor = doc.createElement('div'); + chart.tabExitAnchor.setAttribute('tabindex', '0'); + + // Hide exit anchor + merge(true, chart.tabExitAnchor.style, { + position: 'absolute', + left: '-9999px', + top: 'auto', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + chart.renderTo.appendChild(chart.tabExitAnchor); + return addEvent(chart.tabExitAnchor, 'focus', + function (ev) { + var e = ev || win.event, + curModule; + + // If focusing and we are exiting, do nothing once. + if (!chart.exiting) { + + // Not exiting, means we are coming in backwards + chart.renderTo.focus(); + e.preventDefault(); + + // Move to last valid keyboard nav module + // Note the we don't run it, just set the index + chart.keyboardNavigationModuleIndex = + chart.keyboardNavigationModules.length - 1; + curModule = chart.keyboardNavigationModules[ + chart.keyboardNavigationModuleIndex + ]; + + // Validate the module + if (curModule.validate && !curModule.validate()) { + // Invalid. + // Move inits next valid module in direction + curModule.move(-1); + } else { + // We have a valid module, init it + curModule.init(-1); + } + + } else { + // Don't skip the next focus, we only skip once. + chart.exiting = false; + } + } + ); }; // Clear the chart and reset the navigation state H.Chart.prototype.resetKeyboardNavigation = function () { - var chart = this, - curMod = ( - chart.keyboardNavigationModules && - chart.keyboardNavigationModules[ - chart.keyboardNavigationModuleIndex || 0 - ] - ); - if (curMod && curMod.terminate) { - curMod.terminate(); - } - if (chart.focusElement) { - chart.focusElement.removeFocusBorder(); - } - chart.keyboardNavigationModuleIndex = 0; - chart.keyboardReset = true; + var chart = this, + curMod = ( + chart.keyboardNavigationModules && + chart.keyboardNavigationModules[ + chart.keyboardNavigationModuleIndex || 0 + ] + ); + if (curMod && curMod.terminate) { + curMod.terminate(); + } + if (chart.focusElement) { + chart.focusElement.removeFocusBorder(); + } + chart.keyboardNavigationModuleIndex = 0; + chart.keyboardReset = true; }; @@ -1250,85 +1250,85 @@ H.Chart.prototype.resetKeyboardNavigation = function () { * On destroy, we need to clean up the focus border and the state */ H.addEvent(H.Series, 'destroy', function () { - var chart = this.chart; - if (chart.highlightedPoint && chart.highlightedPoint.series === this) { - delete chart.highlightedPoint; - if (chart.focusElement) { - chart.focusElement.removeFocusBorder(); - } - } + var chart = this.chart; + if (chart.highlightedPoint && chart.highlightedPoint.series === this) { + delete chart.highlightedPoint; + if (chart.focusElement) { + chart.focusElement.removeFocusBorder(); + } + } }); // Add keyboard navigation events on chart load H.Chart.prototype.callbacks.push(function (chart) { - var a11yOptions = chart.options.accessibility; - if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) { - - // Test if we have focus support for SVG elements - hasSVGFocusSupport = !!chart.renderTo - .getElementsByTagName('g')[0].focus; - - // Init nav modules. We start at the first module, and as the user - // navigates through the chart the index will increase to use different - // handler modules. - chart.addKeyboardNavigationModules(); - chart.keyboardNavigationModuleIndex = 0; - - // Make chart container reachable by tab - if ( - chart.container.hasAttribute && - !chart.container.hasAttribute('tabIndex') - ) { - chart.container.setAttribute('tabindex', '0'); - } - - // Add tab exit anchor - if (!chart.tabExitAnchor) { - chart.unbindExitAnchorFocus = chart.addExitAnchor(); - } - - // Handle keyboard events by routing them to active keyboard nav module - chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown', - function (ev) { - var e = ev || win.event, - curNavModule = chart.keyboardNavigationModules[ - chart.keyboardNavigationModuleIndex - ]; - chart.keyboardReset = false; - // If there is a nav module for the current index, run it. - // Otherwise, we are outside of the chart in some direction. - if (curNavModule) { - if (curNavModule.run(e)) { - // Successfully handled this key event, stop default - e.preventDefault(); - } - } - }); - - // Reset chart navigation state if we click outside the chart and it's - // not already reset - chart.unbindBlurHandler = addEvent(doc, 'mouseup', function () { - if ( - !chart.keyboardReset && - !(chart.pointer && chart.pointer.chartPosition) - ) { - chart.resetKeyboardNavigation(); - } - }); - - // Add cleanup handlers - addEvent(chart, 'destroy', function () { - chart.resetKeyboardNavigation(); - if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) { - chart.unbindExitAnchorFocus(); - } - if (chart.unbindKeydownHandler && chart.renderTo) { - chart.unbindKeydownHandler(); - } - if (chart.unbindBlurHandler) { - chart.unbindBlurHandler(); - } - }); - } + var a11yOptions = chart.options.accessibility; + if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) { + + // Test if we have focus support for SVG elements + hasSVGFocusSupport = !!chart.renderTo + .getElementsByTagName('g')[0].focus; + + // Init nav modules. We start at the first module, and as the user + // navigates through the chart the index will increase to use different + // handler modules. + chart.addKeyboardNavigationModules(); + chart.keyboardNavigationModuleIndex = 0; + + // Make chart container reachable by tab + if ( + chart.container.hasAttribute && + !chart.container.hasAttribute('tabIndex') + ) { + chart.container.setAttribute('tabindex', '0'); + } + + // Add tab exit anchor + if (!chart.tabExitAnchor) { + chart.unbindExitAnchorFocus = chart.addExitAnchor(); + } + + // Handle keyboard events by routing them to active keyboard nav module + chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown', + function (ev) { + var e = ev || win.event, + curNavModule = chart.keyboardNavigationModules[ + chart.keyboardNavigationModuleIndex + ]; + chart.keyboardReset = false; + // If there is a nav module for the current index, run it. + // Otherwise, we are outside of the chart in some direction. + if (curNavModule) { + if (curNavModule.run(e)) { + // Successfully handled this key event, stop default + e.preventDefault(); + } + } + }); + + // Reset chart navigation state if we click outside the chart and it's + // not already reset + chart.unbindBlurHandler = addEvent(doc, 'mouseup', function () { + if ( + !chart.keyboardReset && + !(chart.pointer && chart.pointer.chartPosition) + ) { + chart.resetKeyboardNavigation(); + } + }); + + // Add cleanup handlers + addEvent(chart, 'destroy', function () { + chart.resetKeyboardNavigation(); + if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) { + chart.unbindExitAnchorFocus(); + } + if (chart.unbindKeydownHandler && chart.renderTo) { + chart.unbindKeydownHandler(); + } + if (chart.unbindBlurHandler) { + chart.unbindBlurHandler(); + } + }); + } }); diff --git a/js/modules/map-parser.src.js b/js/modules/map-parser.src.js index 756b67d3263..a89f592bcbc 100644 --- a/js/modules/map-parser.src.js +++ b/js/modules/map-parser.src.js @@ -4,7 +4,7 @@ * License: www.highcharts.com/license */ /** - * SVG map parser. + * SVG map parser. * This file requires data.js. */ /* global document, jQuery, $ */ @@ -16,446 +16,446 @@ import './data.src.js'; var each = H.each; H.wrap(H.Data.prototype, 'init', function (proceed, options) { - proceed.call(this, options); + proceed.call(this, options); - if (options.svg) { - this.loadSVG(); - } + if (options.svg) { + this.loadSVG(); + } }); H.extend(H.Data.prototype, { - /** - * Parse an SVG path into a simplified array that Highcharts can read - */ - pathToArray: function (path, matrix) { - var i = 0, - position = 0, - point, - positions, - fixedPoint = [0, 0], - startPoint = [0, 0], - isRelative, - isString, - operator, - matrixTransform = function (p, m) { - return [ - m.a * p[0] + m.c * p[1] + m.e, - m.b * p[0] + m.d * p[1] + m.f - ]; - }; - - path = path - // Scientific notation - .replace(/[0-9]+e-?[0-9]+/g, function (a) { - return +a; // cast to number - }) - // Move letters apart - .replace(/([A-Za-z])/g, ' $1 ') - // Add space before minus - .replace(/-/g, ' -') - // Trim - .replace(/^\s*/, '').replace(/\s*$/, '') - // Remove newlines, tabs etc - .replace(/\s+/g, ' ') - - // Split on spaces, minus and commas - .split(/[ ,]+/); - - // Blank path - if (path.length === 1) { - return []; - } - - // Real path - for (i = 0; i < path.length; i++) { - isString = /[a-zA-Z]/.test(path[i]); - - // Handle strings - if (isString) { - operator = path[i]; - positions = 2; - - // Curves have six positions - if (operator === 'c' || operator === 'C') { - positions = 6; - } - - // When moving after a closed subpath, start again from previous - // subpath's starting point - if (operator === 'm') { - startPoint = [ - parseFloat(path[i + 1]) + startPoint[0], - parseFloat(path[i + 2]) + startPoint[1] - ]; - } else if (operator === 'M') { - startPoint = [ - parseFloat(path[i + 1]), - parseFloat(path[i + 2]) - ]; - } - - // Enter or exit relative mode - if (operator === 'm' || operator === 'l' || operator === 'c') { - path[i] = operator.toUpperCase(); - isRelative = true; - } else if ( - operator === 'M' || - operator === 'L' || - operator === 'C' - ) { - isRelative = false; - - - // Horizontal and vertical line to - } else if (operator === 'h') { - isRelative = true; - path[i] = 'L'; - path.splice(i + 2, 0, 0); - } else if (operator === 'v') { - isRelative = true; - path[i] = 'L'; - path.splice(i + 1, 0, 0); - } else if (operator === 's') { - isRelative = true; - path[i] = 'L'; - path.splice(i + 1, 2); - } else if (operator === 'S') { - isRelative = false; - path[i] = 'L'; - path.splice(i + 1, 2); - } else if (operator === 'H' || operator === 'h') { - isRelative = false; - path[i] = 'L'; - path.splice(i + 2, 0, fixedPoint[1]); - } else if (operator === 'V' || operator === 'v') { - isRelative = false; - path[i] = 'L'; - path.splice(i + 1, 0, fixedPoint[0]); - } else if (operator === 'z' || operator === 'Z') { - fixedPoint = startPoint; - } - - // Handle numbers - } else { - path[i] = parseFloat(path[i]); - if (isRelative) { - path[i] += fixedPoint[position % 2]; - - } - - if (position % 2 === 1) { // y - // only translate absolute points or initial moveTo - if ( - matrix && - (!isRelative || (operator === 'm' && i < 3)) - ) { - point = matrixTransform([path[i - 1], path[i]], matrix); - path[i - 1] = point[0]; - path[i] = point[1]; - } - - } - - - // Reset to zero position (x/y switching) - if (position === positions - 1) { - // Set the fixed point for the next pair - fixedPoint = [path[i - 1], path[i]]; - - position = 0; - } else { - position += 1; - } - - } - } - - // Handle polygon points - if (typeof path[0] === 'number' && path.length >= 4) { - path.unshift('M'); - path.splice(3, 0, 'L'); - } - return path; - }, - - /** - * Join the path back to a string for compression - */ - pathToString: function (arr) { - each(arr, function (point) { - var path = point.path; - - // Join all by commas - path = path.join(','); - - // Remove commas next to a letter - path = path.replace(/,?([a-zA-Z]),?/g, '$1'); - - // Reinsert - point.path = path; - }); - - return arr; - }, - - /** - * Scale the path to fit within a given box and round all numbers - */ - roundPaths: function (arr, scale) { - var mapProto = H.seriesTypes.map.prototype, - fakeSeries, - origSize, - transA; - - fakeSeries = { - xAxis: { - translate: H.Axis.prototype.translate, - options: {}, - minPixelPadding: 0 - }, - yAxis: { - translate: H.Axis.prototype.translate, - options: {}, - minPixelPadding: 0 - } - }; - - // Borrow the map series type's getBox method - mapProto.getBox.call(fakeSeries, arr); - - origSize = Math.max( - fakeSeries.maxX - fakeSeries.minX, - fakeSeries.maxY - fakeSeries.minY - ); - scale = scale || 1000; - transA = scale / origSize; - - fakeSeries.xAxis.transA = fakeSeries.yAxis.transA = transA; - fakeSeries.xAxis.len = fakeSeries.yAxis.len = scale; - fakeSeries.xAxis.min = fakeSeries.minX; - fakeSeries.yAxis.min = (fakeSeries.minY + scale) / transA; - - each(arr, function (point) { - - var i, - path; - point.path = path = - mapProto.translatePath.call(fakeSeries, point.path, true); - i = path.length; - while (i--) { - if (typeof path[i] === 'number') { - path[i] = Math.round(path[i]); - } - } - delete point._foundBox; - - }); - - return arr; - }, - - /** - * Load an SVG file and extract the paths - * @param {Object} url - */ - loadSVG: function () { - - var data = this, - options = this.options; - - function getPathLikeChildren(parent) { - return Array.prototype.slice - .call(parent.getElementsByTagName('path')) - .concat( - Array.prototype.slice.call( - parent.getElementsByTagName('polygon') - ) - ) - .concat( - Array.prototype.slice.call( - parent.getElementsByTagName('rect') - ) - ); - } - - function getPathDefinition(node) { - if (node.nodeName === 'path') { - return node.getAttribute('d'); - } - if (node.nodeName === 'polygon') { - return node.getAttribute('points'); - } - if (node.nodeName === 'rect') { - var x = +node.getAttribute('x'), - y = +node.getAttribute('y'), - w = +node.getAttribute('width'), - h = +node.getAttribute('height'); - - // Return polygon definition - return [x, y, x + w, y, x + w, y + h, x, y + h, x, y].join(' '); - } - } - - function getTranslate(elem) { - var ctm = elem.getCTM(); - if (!isNaN(ctm.f)) { - return ctm; - } - } - - - function getName(elem) { - var desc = elem.getElementsByTagName('desc'), - nameTag = desc[0] && desc[0].getElementsByTagName('name'), - name = nameTag && nameTag[0] && nameTag[0].innerText; - - return ( - name || - elem.getAttribute('inkscape:label') || - elem.getAttribute('id') || - elem.getAttribute('class') - ); - } - - function hasFill(elem) { - return ( - !/fill[\s]?\:[\s]?none/.test(elem.getAttribute('style')) && - elem.getAttribute('fill') !== 'none' - ); - } - - function handleSVG(xml) { - - var arr = [], - currentParent, - allPaths, - commonLineage, - lastCommonAncestor, - handleGroups; - - // Make a hidden frame where the SVG is rendered - data.$frame = data.$frame || $('
') - .css({ - position: 'absolute', // https://bugzilla.mozilla.org/show_bug.cgi?id=756985 - top: '-9999em' - }) - .appendTo($(document.body)); - data.$frame.html(xml); - xml = $('svg', data.$frame)[0]; - - xml.removeAttribute('viewBox'); - - - allPaths = getPathLikeChildren(xml); - - // Skip clip paths - each(['defs', 'clipPath'], function (nodeName) { - each(xml.getElementsByTagName(nodeName), function (parent) { - each(parent.getElementsByTagName('path'), function (path) { - path.skip = true; - }); - }); - }); - - // If not all paths belong to the same group, handle groups - each(allPaths, function (path, i) { - if (!path.skip) { - var itemLineage = [], - parentNode, - j; - - if (i > 0 && path.parentNode !== currentParent) { - handleGroups = true; - } - currentParent = path.parentNode; - - // Handle common lineage - parentNode = path; - while (parentNode) { - itemLineage.push(parentNode); - parentNode = parentNode.parentNode; - } - itemLineage.reverse(); - - if (!commonLineage) { - commonLineage = itemLineage; // first iteration - } else { - for (j = 0; j < commonLineage.length; j++) { - if (commonLineage[j] !== itemLineage[j]) { - commonLineage = commonLineage.slice(0, j); - } - } - } - } - }); - lastCommonAncestor = commonLineage[commonLineage.length - 1]; - - // Iterate groups to find sub paths - if (handleGroups) { - each( - lastCommonAncestor.getElementsByTagName('g'), - function (g) { - var groupPath = [], - pathHasFill; - - each(getPathLikeChildren(g), function (path) { - if (!path.skip) { - groupPath = groupPath.concat( - data.pathToArray( - getPathDefinition(path), - getTranslate(path) - ) - ); - - if (hasFill(path)) { - pathHasFill = true; - } - - path.skip = true; - } - }); - arr.push({ - name: getName(g), - path: groupPath, - hasFill: pathHasFill - }); - } - ); - } - - // Iterate the remaining paths that are not parts of groups - each(allPaths, function (path) { - if (!path.skip) { - arr.push({ - name: getName(path), - path: data.pathToArray( - getPathDefinition(path), - getTranslate(path) - ), - hasFill: hasFill(path) - }); - } - }); - - // Round off to compress - data.roundPaths(arr); - - // Do the callback - options.complete({ - series: [{ - data: arr - }] - }); - } - - if (options.svg.indexOf('= 4) { + path.unshift('M'); + path.splice(3, 0, 'L'); + } + return path; + }, + + /** + * Join the path back to a string for compression + */ + pathToString: function (arr) { + each(arr, function (point) { + var path = point.path; + + // Join all by commas + path = path.join(','); + + // Remove commas next to a letter + path = path.replace(/,?([a-zA-Z]),?/g, '$1'); + + // Reinsert + point.path = path; + }); + + return arr; + }, + + /** + * Scale the path to fit within a given box and round all numbers + */ + roundPaths: function (arr, scale) { + var mapProto = H.seriesTypes.map.prototype, + fakeSeries, + origSize, + transA; + + fakeSeries = { + xAxis: { + translate: H.Axis.prototype.translate, + options: {}, + minPixelPadding: 0 + }, + yAxis: { + translate: H.Axis.prototype.translate, + options: {}, + minPixelPadding: 0 + } + }; + + // Borrow the map series type's getBox method + mapProto.getBox.call(fakeSeries, arr); + + origSize = Math.max( + fakeSeries.maxX - fakeSeries.minX, + fakeSeries.maxY - fakeSeries.minY + ); + scale = scale || 1000; + transA = scale / origSize; + + fakeSeries.xAxis.transA = fakeSeries.yAxis.transA = transA; + fakeSeries.xAxis.len = fakeSeries.yAxis.len = scale; + fakeSeries.xAxis.min = fakeSeries.minX; + fakeSeries.yAxis.min = (fakeSeries.minY + scale) / transA; + + each(arr, function (point) { + + var i, + path; + point.path = path = + mapProto.translatePath.call(fakeSeries, point.path, true); + i = path.length; + while (i--) { + if (typeof path[i] === 'number') { + path[i] = Math.round(path[i]); + } + } + delete point._foundBox; + + }); + + return arr; + }, + + /** + * Load an SVG file and extract the paths + * @param {Object} url + */ + loadSVG: function () { + + var data = this, + options = this.options; + + function getPathLikeChildren(parent) { + return Array.prototype.slice + .call(parent.getElementsByTagName('path')) + .concat( + Array.prototype.slice.call( + parent.getElementsByTagName('polygon') + ) + ) + .concat( + Array.prototype.slice.call( + parent.getElementsByTagName('rect') + ) + ); + } + + function getPathDefinition(node) { + if (node.nodeName === 'path') { + return node.getAttribute('d'); + } + if (node.nodeName === 'polygon') { + return node.getAttribute('points'); + } + if (node.nodeName === 'rect') { + var x = +node.getAttribute('x'), + y = +node.getAttribute('y'), + w = +node.getAttribute('width'), + h = +node.getAttribute('height'); + + // Return polygon definition + return [x, y, x + w, y, x + w, y + h, x, y + h, x, y].join(' '); + } + } + + function getTranslate(elem) { + var ctm = elem.getCTM(); + if (!isNaN(ctm.f)) { + return ctm; + } + } + + + function getName(elem) { + var desc = elem.getElementsByTagName('desc'), + nameTag = desc[0] && desc[0].getElementsByTagName('name'), + name = nameTag && nameTag[0] && nameTag[0].innerText; + + return ( + name || + elem.getAttribute('inkscape:label') || + elem.getAttribute('id') || + elem.getAttribute('class') + ); + } + + function hasFill(elem) { + return ( + !/fill[\s]?\:[\s]?none/.test(elem.getAttribute('style')) && + elem.getAttribute('fill') !== 'none' + ); + } + + function handleSVG(xml) { + + var arr = [], + currentParent, + allPaths, + commonLineage, + lastCommonAncestor, + handleGroups; + + // Make a hidden frame where the SVG is rendered + data.$frame = data.$frame || $('
') + .css({ + position: 'absolute', // https://bugzilla.mozilla.org/show_bug.cgi?id=756985 + top: '-9999em' + }) + .appendTo($(document.body)); + data.$frame.html(xml); + xml = $('svg', data.$frame)[0]; + + xml.removeAttribute('viewBox'); + + + allPaths = getPathLikeChildren(xml); + + // Skip clip paths + each(['defs', 'clipPath'], function (nodeName) { + each(xml.getElementsByTagName(nodeName), function (parent) { + each(parent.getElementsByTagName('path'), function (path) { + path.skip = true; + }); + }); + }); + + // If not all paths belong to the same group, handle groups + each(allPaths, function (path, i) { + if (!path.skip) { + var itemLineage = [], + parentNode, + j; + + if (i > 0 && path.parentNode !== currentParent) { + handleGroups = true; + } + currentParent = path.parentNode; + + // Handle common lineage + parentNode = path; + while (parentNode) { + itemLineage.push(parentNode); + parentNode = parentNode.parentNode; + } + itemLineage.reverse(); + + if (!commonLineage) { + commonLineage = itemLineage; // first iteration + } else { + for (j = 0; j < commonLineage.length; j++) { + if (commonLineage[j] !== itemLineage[j]) { + commonLineage = commonLineage.slice(0, j); + } + } + } + } + }); + lastCommonAncestor = commonLineage[commonLineage.length - 1]; + + // Iterate groups to find sub paths + if (handleGroups) { + each( + lastCommonAncestor.getElementsByTagName('g'), + function (g) { + var groupPath = [], + pathHasFill; + + each(getPathLikeChildren(g), function (path) { + if (!path.skip) { + groupPath = groupPath.concat( + data.pathToArray( + getPathDefinition(path), + getTranslate(path) + ) + ); + + if (hasFill(path)) { + pathHasFill = true; + } + + path.skip = true; + } + }); + arr.push({ + name: getName(g), + path: groupPath, + hasFill: pathHasFill + }); + } + ); + } + + // Iterate the remaining paths that are not parts of groups + each(allPaths, function (path) { + if (!path.skip) { + arr.push({ + name: getName(path), + path: data.pathToArray( + getPathDefinition(path), + getTranslate(path) + ), + hasFill: hasFill(path) + }); + } + }); + + // Round off to compress + data.roundPaths(arr); + + // Do the callback + options.complete({ + series: [{ + data: arr + }] + }); + } + + if (options.svg.indexOf(' 2000000) { - dataURL = Highcharts.dataURLtoBlob(dataURL); - if (!dataURL) { - throw 'Data URL length limit reached'; - } - } - - // Try HTML5 download attr if supported - if (a.download !== undefined) { - a.href = dataURL; - a.download = filename; // HTML5 download attribute - doc.body.appendChild(a); - a.click(); - doc.body.removeChild(a); - } else { - // No download attr, just opening data URI - try { - windowRef = win.open(dataURL, 'chart'); - if (windowRef === undefined || windowRef === null) { - throw 'Failed to open window'; - } - } catch (e) { - // window.open failed, trying location.href - win.location.href = dataURL; - } - } + var a = doc.createElement('a'), + windowRef; + + // IE specific blob implementation + // Don't use for normal dataURLs + if ( + typeof dataURL !== 'string' && + !(dataURL instanceof String) && + nav.msSaveOrOpenBlob + ) { + nav.msSaveOrOpenBlob(dataURL, filename); + return; + } + + // Some browsers have limitations for data URL lengths. Try to convert to + // Blob or fall back. Edge always needs that blob. + if (isEdgeBrowser || dataURL.length > 2000000) { + dataURL = Highcharts.dataURLtoBlob(dataURL); + if (!dataURL) { + throw 'Data URL length limit reached'; + } + } + + // Try HTML5 download attr if supported + if (a.download !== undefined) { + a.href = dataURL; + a.download = filename; // HTML5 download attribute + doc.body.appendChild(a); + a.click(); + doc.body.removeChild(a); + } else { + // No download attr, just opening data URI + try { + windowRef = win.open(dataURL, 'chart'); + if (windowRef === undefined || windowRef === null) { + throw 'Failed to open window'; + } + } catch (e) { + // window.open failed, trying location.href + win.location.href = dataURL; + } + } }; // Get blob URL from SVG code. Falls back to normal data URI. Highcharts.svgToDataUrl = function (svg) { - // Webkit and not chrome - var webKit = ( - nav.userAgent.indexOf('WebKit') > -1 && - nav.userAgent.indexOf('Chrome') < 0 - ); - try { - // Safari requires data URI since it doesn't allow navigation to blob - // URLs. Firefox has an issue with Blobs and internal references, - // leading to gradients not working using Blobs (#4550) - if (!webKit && nav.userAgent.toLowerCase().indexOf('firefox') < 0) { - return domurl.createObjectURL(new win.Blob([svg], { - type: 'image/svg+xml;charset-utf-16' - })); - } - } catch (e) { - // Ignore - } - return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg); + // Webkit and not chrome + var webKit = ( + nav.userAgent.indexOf('WebKit') > -1 && + nav.userAgent.indexOf('Chrome') < 0 + ); + try { + // Safari requires data URI since it doesn't allow navigation to blob + // URLs. Firefox has an issue with Blobs and internal references, + // leading to gradients not working using Blobs (#4550) + if (!webKit && nav.userAgent.toLowerCase().indexOf('firefox') < 0) { + return domurl.createObjectURL(new win.Blob([svg], { + type: 'image/svg+xml;charset-utf-16' + })); + } + } catch (e) { + // Ignore + } + return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg); }; // Get data:URL from image URL @@ -145,91 +145,91 @@ Highcharts.svgToDataUrl = function (svg) { // receive four arguments: imageURL, imageType, callbackArgs and scale. // callbackArgs is used only by callbacks and can contain whatever. Highcharts.imageToDataUrl = function ( - imageURL, - imageType, - callbackArgs, - scale, - successCallback, - taintedCallback, - noCanvasSupportCallback, - failedLoadCallback, - finallyCallback + imageURL, + imageType, + callbackArgs, + scale, + successCallback, + taintedCallback, + noCanvasSupportCallback, + failedLoadCallback, + finallyCallback ) { - var img = new win.Image(), - taintedHandler, - loadHandler = function () { - setTimeout(function () { - var canvas = doc.createElement('canvas'), - ctx = canvas.getContext && canvas.getContext('2d'), - dataURL; - try { - if (!ctx) { - noCanvasSupportCallback( - imageURL, - imageType, - callbackArgs, - scale - ); - } else { - canvas.height = img.height * scale; - canvas.width = img.width * scale; - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - - // Now we try to get the contents of the canvas. - try { - dataURL = canvas.toDataURL(imageType); - successCallback( - dataURL, - imageType, - callbackArgs, - scale - ); - } catch (e) { - taintedHandler( - imageURL, - imageType, - callbackArgs, - scale - ); - } - } - } finally { - if (finallyCallback) { - finallyCallback( - imageURL, - imageType, - callbackArgs, - scale - ); - } - } - // IE bug where image is not always ready despite calling load - // event. - }, loadEventDeferDelay); - }, - // Image load failed (e.g. invalid URL) - errorHandler = function () { - failedLoadCallback(imageURL, imageType, callbackArgs, scale); - if (finallyCallback) { - finallyCallback(imageURL, imageType, callbackArgs, scale); - } - }; - - // This is called on load if the image drawing to canvas failed with a - // security error. We retry the drawing with crossOrigin set to Anonymous. - taintedHandler = function () { - img = new win.Image(); - taintedHandler = taintedCallback; - // Must be set prior to loading image source - img.crossOrigin = 'Anonymous'; - img.onload = loadHandler; - img.onerror = errorHandler; - img.src = imageURL; - }; - - img.onload = loadHandler; - img.onerror = errorHandler; - img.src = imageURL; + var img = new win.Image(), + taintedHandler, + loadHandler = function () { + setTimeout(function () { + var canvas = doc.createElement('canvas'), + ctx = canvas.getContext && canvas.getContext('2d'), + dataURL; + try { + if (!ctx) { + noCanvasSupportCallback( + imageURL, + imageType, + callbackArgs, + scale + ); + } else { + canvas.height = img.height * scale; + canvas.width = img.width * scale; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + // Now we try to get the contents of the canvas. + try { + dataURL = canvas.toDataURL(imageType); + successCallback( + dataURL, + imageType, + callbackArgs, + scale + ); + } catch (e) { + taintedHandler( + imageURL, + imageType, + callbackArgs, + scale + ); + } + } + } finally { + if (finallyCallback) { + finallyCallback( + imageURL, + imageType, + callbackArgs, + scale + ); + } + } + // IE bug where image is not always ready despite calling load + // event. + }, loadEventDeferDelay); + }, + // Image load failed (e.g. invalid URL) + errorHandler = function () { + failedLoadCallback(imageURL, imageType, callbackArgs, scale); + if (finallyCallback) { + finallyCallback(imageURL, imageType, callbackArgs, scale); + } + }; + + // This is called on load if the image drawing to canvas failed with a + // security error. We retry the drawing with crossOrigin set to Anonymous. + taintedHandler = function () { + img = new win.Image(); + taintedHandler = taintedCallback; + // Must be set prior to loading image source + img.crossOrigin = 'Anonymous'; + img.onload = loadHandler; + img.onerror = errorHandler; + img.src = imageURL; + }; + + img.onload = loadHandler; + img.onerror = errorHandler; + img.src = imageURL; }; /** @@ -243,308 +243,308 @@ Highcharts.imageToDataUrl = function ( * demand */ Highcharts.downloadSVGLocal = function ( - svg, - options, - failCallback, - successCallback + svg, + options, + failCallback, + successCallback ) { - var svgurl, - blob, - objectURLRevoke = true, - finallyHandler, - libURL = options.libURL || Highcharts.getOptions().exporting.libURL, - dummySVGContainer = doc.createElement('div'), - imageType = options.type || 'image/png', - filename = ( - (options.filename || 'chart') + - '.' + - (imageType === 'image/svg+xml' ? 'svg' : imageType.split('/')[1]) - ), - scale = options.scale || 1; - - // Allow libURL to end with or without fordward slash - libURL = libURL.slice(-1) !== '/' ? libURL + '/' : libURL; - - function svgToPdf(svgElement, margin) { - var width = svgElement.width.baseVal.value + 2 * margin, - height = svgElement.height.baseVal.value + 2 * margin, - pdf = new win.jsPDF( // eslint-disable-line new-cap - 'l', - 'pt', - [width, height] - ); - - // Workaround for #7090, hidden elements were drawn anyway. It comes - // down to https://github.com/yWorks/svg2pdf.js/issues/28. Check this - // later. - each( - svgElement.querySelectorAll('*[visibility="hidden"]'), - function (node) { - node.parentNode.removeChild(node); - } - ); - - win.svg2pdf(svgElement, pdf, { removeInvalid: true }); - return pdf.output('datauristring'); - } - - function downloadPDF() { - dummySVGContainer.innerHTML = svg; - var textElements = dummySVGContainer.getElementsByTagName('text'), - titleElements, - svgData, - // Copy style property to element from parents if it's not there. - // Searches up hierarchy until it finds prop, or hits the chart - // container. - setStylePropertyFromParents = function (el, propName) { - var curParent = el; - while (curParent && curParent !== dummySVGContainer) { - if (curParent.style[propName]) { - el.style[propName] = curParent.style[propName]; - break; - } - curParent = curParent.parentNode; - } - }; - - // Workaround for the text styling. Making sure it does pick up settings - // for parent elements. - each(textElements, function (el) { - // Workaround for the text styling. making sure it does pick up the - // root element - each(['font-family', 'font-size'], function (property) { - setStylePropertyFromParents(el, property); - }); - el.style['font-family'] = ( - el.style['font-family'] && - el.style['font-family'].split(' ').splice(-1) - ); - - // Workaround for plotband with width, removing title from text - // nodes - titleElements = el.getElementsByTagName('title'); - each(titleElements, function (titleElement) { - el.removeChild(titleElement); - }); - }); - svgData = svgToPdf(dummySVGContainer.firstChild, 0); - try { - Highcharts.downloadURL(svgData, filename); - if (successCallback) { - successCallback(); - } - } catch (e) { - failCallback(); - } - } - - // Initiate download depending on file type - if (imageType === 'image/svg+xml') { - // SVG download. In this case, we want to use Microsoft specific Blob if - // available - try { - if (nav.msSaveOrOpenBlob) { - blob = new MSBlobBuilder(); - blob.append(svg); - svgurl = blob.getBlob('image/svg+xml'); - } else { - svgurl = Highcharts.svgToDataUrl(svg); - } - Highcharts.downloadURL(svgurl, filename); - if (successCallback) { - successCallback(); - } - } catch (e) { - failCallback(); - } - } else if (imageType === 'application/pdf') { - if (win.jsPDF && win.svg2pdf) { - downloadPDF(); - } else { - // Must load pdf libraries first. // Don't destroy the object URL - // yet since we are doing things asynchronously. A cleaner solution - // would be nice, but this will do for now. - objectURLRevoke = true; - getScript(libURL + 'jspdf.js', function () { - getScript(libURL + 'svg2pdf.js', function () { - downloadPDF(); - }); - }); - } - } else { - // PNG/JPEG download - create bitmap from SVG - - svgurl = Highcharts.svgToDataUrl(svg); - finallyHandler = function () { - try { - domurl.revokeObjectURL(svgurl); - } catch (e) { - // Ignore - } - }; - // First, try to get PNG by rendering on canvas - Highcharts.imageToDataUrl( - svgurl, - imageType, - {}, - scale, - function (imageURL) { - // Success - try { - Highcharts.downloadURL(imageURL, filename); - if (successCallback) { - successCallback(); - } - } catch (e) { - failCallback(); - } - }, function () { - // Failed due to tainted canvas - // Create new and untainted canvas - var canvas = doc.createElement('canvas'), - ctx = canvas.getContext('2d'), - imageWidth = svg.match( - /^]*width\s*=\s*\"?(\d+)\"?[^>]*>/ - )[1] * scale, - imageHeight = svg.match( - /^]*height\s*=\s*\"?(\d+)\"?[^>]*>/ - )[1] * scale, - downloadWithCanVG = function () { - ctx.drawSvg(svg, 0, 0, imageWidth, imageHeight); - try { - Highcharts.downloadURL( - nav.msSaveOrOpenBlob ? - canvas.msToBlob() : - canvas.toDataURL(imageType), - filename - ); - if (successCallback) { - successCallback(); - } - } catch (e) { - failCallback(); - } finally { - finallyHandler(); - } - }; - - canvas.width = imageWidth; - canvas.height = imageHeight; - if (win.canvg) { - // Use preloaded canvg - downloadWithCanVG(); - } else { - // Must load canVG first. // Don't destroy the object URL - // yet since we are doing things asynchronously. A cleaner - // solution would be nice, but this will do for now. - objectURLRevoke = true; - // Get RGBColor.js first, then canvg - getScript(libURL + 'rgbcolor.js', function () { - getScript(libURL + 'canvg.js', function () { - downloadWithCanVG(); - }); - }); - } - }, - // No canvas support - failCallback, - // Failed to load image - failCallback, - // Finally - function () { - if (objectURLRevoke) { - finallyHandler(); - } - } - ); - } + var svgurl, + blob, + objectURLRevoke = true, + finallyHandler, + libURL = options.libURL || Highcharts.getOptions().exporting.libURL, + dummySVGContainer = doc.createElement('div'), + imageType = options.type || 'image/png', + filename = ( + (options.filename || 'chart') + + '.' + + (imageType === 'image/svg+xml' ? 'svg' : imageType.split('/')[1]) + ), + scale = options.scale || 1; + + // Allow libURL to end with or without fordward slash + libURL = libURL.slice(-1) !== '/' ? libURL + '/' : libURL; + + function svgToPdf(svgElement, margin) { + var width = svgElement.width.baseVal.value + 2 * margin, + height = svgElement.height.baseVal.value + 2 * margin, + pdf = new win.jsPDF( // eslint-disable-line new-cap + 'l', + 'pt', + [width, height] + ); + + // Workaround for #7090, hidden elements were drawn anyway. It comes + // down to https://github.com/yWorks/svg2pdf.js/issues/28. Check this + // later. + each( + svgElement.querySelectorAll('*[visibility="hidden"]'), + function (node) { + node.parentNode.removeChild(node); + } + ); + + win.svg2pdf(svgElement, pdf, { removeInvalid: true }); + return pdf.output('datauristring'); + } + + function downloadPDF() { + dummySVGContainer.innerHTML = svg; + var textElements = dummySVGContainer.getElementsByTagName('text'), + titleElements, + svgData, + // Copy style property to element from parents if it's not there. + // Searches up hierarchy until it finds prop, or hits the chart + // container. + setStylePropertyFromParents = function (el, propName) { + var curParent = el; + while (curParent && curParent !== dummySVGContainer) { + if (curParent.style[propName]) { + el.style[propName] = curParent.style[propName]; + break; + } + curParent = curParent.parentNode; + } + }; + + // Workaround for the text styling. Making sure it does pick up settings + // for parent elements. + each(textElements, function (el) { + // Workaround for the text styling. making sure it does pick up the + // root element + each(['font-family', 'font-size'], function (property) { + setStylePropertyFromParents(el, property); + }); + el.style['font-family'] = ( + el.style['font-family'] && + el.style['font-family'].split(' ').splice(-1) + ); + + // Workaround for plotband with width, removing title from text + // nodes + titleElements = el.getElementsByTagName('title'); + each(titleElements, function (titleElement) { + el.removeChild(titleElement); + }); + }); + svgData = svgToPdf(dummySVGContainer.firstChild, 0); + try { + Highcharts.downloadURL(svgData, filename); + if (successCallback) { + successCallback(); + } + } catch (e) { + failCallback(); + } + } + + // Initiate download depending on file type + if (imageType === 'image/svg+xml') { + // SVG download. In this case, we want to use Microsoft specific Blob if + // available + try { + if (nav.msSaveOrOpenBlob) { + blob = new MSBlobBuilder(); + blob.append(svg); + svgurl = blob.getBlob('image/svg+xml'); + } else { + svgurl = Highcharts.svgToDataUrl(svg); + } + Highcharts.downloadURL(svgurl, filename); + if (successCallback) { + successCallback(); + } + } catch (e) { + failCallback(); + } + } else if (imageType === 'application/pdf') { + if (win.jsPDF && win.svg2pdf) { + downloadPDF(); + } else { + // Must load pdf libraries first. // Don't destroy the object URL + // yet since we are doing things asynchronously. A cleaner solution + // would be nice, but this will do for now. + objectURLRevoke = true; + getScript(libURL + 'jspdf.js', function () { + getScript(libURL + 'svg2pdf.js', function () { + downloadPDF(); + }); + }); + } + } else { + // PNG/JPEG download - create bitmap from SVG + + svgurl = Highcharts.svgToDataUrl(svg); + finallyHandler = function () { + try { + domurl.revokeObjectURL(svgurl); + } catch (e) { + // Ignore + } + }; + // First, try to get PNG by rendering on canvas + Highcharts.imageToDataUrl( + svgurl, + imageType, + {}, + scale, + function (imageURL) { + // Success + try { + Highcharts.downloadURL(imageURL, filename); + if (successCallback) { + successCallback(); + } + } catch (e) { + failCallback(); + } + }, function () { + // Failed due to tainted canvas + // Create new and untainted canvas + var canvas = doc.createElement('canvas'), + ctx = canvas.getContext('2d'), + imageWidth = svg.match( + /^]*width\s*=\s*\"?(\d+)\"?[^>]*>/ + )[1] * scale, + imageHeight = svg.match( + /^]*height\s*=\s*\"?(\d+)\"?[^>]*>/ + )[1] * scale, + downloadWithCanVG = function () { + ctx.drawSvg(svg, 0, 0, imageWidth, imageHeight); + try { + Highcharts.downloadURL( + nav.msSaveOrOpenBlob ? + canvas.msToBlob() : + canvas.toDataURL(imageType), + filename + ); + if (successCallback) { + successCallback(); + } + } catch (e) { + failCallback(); + } finally { + finallyHandler(); + } + }; + + canvas.width = imageWidth; + canvas.height = imageHeight; + if (win.canvg) { + // Use preloaded canvg + downloadWithCanVG(); + } else { + // Must load canVG first. // Don't destroy the object URL + // yet since we are doing things asynchronously. A cleaner + // solution would be nice, but this will do for now. + objectURLRevoke = true; + // Get RGBColor.js first, then canvg + getScript(libURL + 'rgbcolor.js', function () { + getScript(libURL + 'canvg.js', function () { + downloadWithCanVG(); + }); + }); + } + }, + // No canvas support + failCallback, + // Failed to load image + failCallback, + // Finally + function () { + if (objectURLRevoke) { + finallyHandler(); + } + } + ); + } }; // Get SVG of chart prepared for client side export. This converts embedded // images in the SVG to data URIs. The options and chartOptions arguments are // passed to the getSVGForExport function. Highcharts.Chart.prototype.getSVGForLocalExport = function ( - options, - chartOptions, - failCallback, - successCallback + options, + chartOptions, + failCallback, + successCallback ) { - var chart = this, - images, - imagesEmbedded = 0, - chartCopyContainer, - chartCopyOptions, - el, - i, - l, - // After grabbing the SVG of the chart's copy container we need to do - // sanitation on the SVG - sanitize = function (svg) { - return chart.sanitizeSVG(svg, chartCopyOptions); - }, - // Success handler, we converted image to base64! - embeddedSuccess = function (imageURL, imageType, callbackArgs) { - ++imagesEmbedded; - - // Change image href in chart copy - callbackArgs.imageElement.setAttributeNS( - 'http://www.w3.org/1999/xlink', - 'href', - imageURL - ); - - // When done with last image we have our SVG - if (imagesEmbedded === images.length) { - successCallback(sanitize(chartCopyContainer.innerHTML)); - } - }; - - // Hook into getSVG to get a copy of the chart copy's container - Highcharts.wrap( - Highcharts.Chart.prototype, - 'getChartHTML', - function (proceed) { - var ret = proceed.apply( - this, - Array.prototype.slice.call(arguments, 1) - ); - chartCopyOptions = this.options; - chartCopyContainer = this.container.cloneNode(true); - return ret; - } - ); - - // Trigger hook to get chart copy - chart.getSVGForExport(options, chartOptions); - images = chartCopyContainer.getElementsByTagName('image'); - - try { - // If there are no images to embed, the SVG is okay now. - if (!images.length) { - // Use SVG of chart copy - successCallback(sanitize(chartCopyContainer.innerHTML)); - return; - } - - // Go through the images we want to embed - for (i = 0, l = images.length; i < l; ++i) { - el = images[i]; - Highcharts.imageToDataUrl(el.getAttributeNS( - 'http://www.w3.org/1999/xlink', - 'href' - ), 'image/png', { imageElement: el }, options.scale, - embeddedSuccess, - // Tainted canvas - failCallback, - // No canvas support - failCallback, - // Failed to load source - failCallback - ); - } - } catch (e) { - failCallback(); - } + var chart = this, + images, + imagesEmbedded = 0, + chartCopyContainer, + chartCopyOptions, + el, + i, + l, + // After grabbing the SVG of the chart's copy container we need to do + // sanitation on the SVG + sanitize = function (svg) { + return chart.sanitizeSVG(svg, chartCopyOptions); + }, + // Success handler, we converted image to base64! + embeddedSuccess = function (imageURL, imageType, callbackArgs) { + ++imagesEmbedded; + + // Change image href in chart copy + callbackArgs.imageElement.setAttributeNS( + 'http://www.w3.org/1999/xlink', + 'href', + imageURL + ); + + // When done with last image we have our SVG + if (imagesEmbedded === images.length) { + successCallback(sanitize(chartCopyContainer.innerHTML)); + } + }; + + // Hook into getSVG to get a copy of the chart copy's container + Highcharts.wrap( + Highcharts.Chart.prototype, + 'getChartHTML', + function (proceed) { + var ret = proceed.apply( + this, + Array.prototype.slice.call(arguments, 1) + ); + chartCopyOptions = this.options; + chartCopyContainer = this.container.cloneNode(true); + return ret; + } + ); + + // Trigger hook to get chart copy + chart.getSVGForExport(options, chartOptions); + images = chartCopyContainer.getElementsByTagName('image'); + + try { + // If there are no images to embed, the SVG is okay now. + if (!images.length) { + // Use SVG of chart copy + successCallback(sanitize(chartCopyContainer.innerHTML)); + return; + } + + // Go through the images we want to embed + for (i = 0, l = images.length; i < l; ++i) { + el = images[i]; + Highcharts.imageToDataUrl(el.getAttributeNS( + 'http://www.w3.org/1999/xlink', + 'href' + ), 'image/png', { imageElement: el }, options.scale, + embeddedSuccess, + // Tainted canvas + failCallback, + // No canvas support + failCallback, + // Failed to load source + failCallback + ); + } + } catch (e) { + failCallback(); + } }; /** @@ -560,142 +560,142 @@ Highcharts.Chart.prototype.getSVGForLocalExport = function ( * for export only. */ Highcharts.Chart.prototype.exportChartLocal = function ( - exportingOptions, - chartOptions + exportingOptions, + chartOptions ) { - var chart = this, - options = Highcharts.merge(chart.options.exporting, exportingOptions), - fallbackToExportServer = function () { - if (options.fallbackToExportServer === false) { - if (options.error) { - options.error(options); - } else { - throw 'Fallback to export server disabled'; - } - } else { - chart.exportChart(options); - } - }, - svgSuccess = function (svg) { - // If SVG contains foreignObjects all exports except SVG will fail, - // as both CanVG and svg2pdf choke on this. Gracefully fall back. - if ( - svg.indexOf(' -1 && - options.type !== 'image/svg+xml' - ) { - fallbackToExportServer(); - } else { - Highcharts.downloadSVGLocal( - svg, - options, - fallbackToExportServer - ); - } - }; - - // If we are on IE and in styled mode, add a whitelist to the renderer for - // inline styles that we want to pass through. There are so many styles by - // default in IE that we don't want to blacklist them all. - /*= if (!build.classic) { =*/ - if (isMSBrowser) { - Highcharts.SVGRenderer.prototype.inlineWhitelist = [ - /^blockSize/, - /^border/, - /^caretColor/, - /^color/, - /^columnRule/, - /^columnRuleColor/, - /^cssFloat/, - /^cursor/, - /^fill$/, - /^fillOpacity/, - /^font/, - /^inlineSize/, - /^length/, - /^lineHeight/, - /^opacity/, - /^outline/, - /^parentRule/, - /^rx$/, - /^ry$/, - /^stroke/, - /^textAlign/, - /^textAnchor/, - /^textDecoration/, - /^transform/, - /^vectorEffect/, - /^visibility/, - /^x$/, - /^y$/ - ]; - } - /*= } =*/ - - // Always fall back on: - // - MS browsers: Embedded images JPEG/PNG, or any PDF - // - Embedded images and PDF - if ( - ( - isMSBrowser && - ( - options.type === 'application/pdf' || - chart.container.getElementsByTagName('image').length && - options.type !== 'image/svg+xml' - ) - ) || ( - options.type === 'application/pdf' && - chart.container.getElementsByTagName('image').length - ) - ) { - fallbackToExportServer(); - return; - } - - chart.getSVGForLocalExport( - options, - chartOptions, - fallbackToExportServer, - svgSuccess - ); + var chart = this, + options = Highcharts.merge(chart.options.exporting, exportingOptions), + fallbackToExportServer = function () { + if (options.fallbackToExportServer === false) { + if (options.error) { + options.error(options); + } else { + throw 'Fallback to export server disabled'; + } + } else { + chart.exportChart(options); + } + }, + svgSuccess = function (svg) { + // If SVG contains foreignObjects all exports except SVG will fail, + // as both CanVG and svg2pdf choke on this. Gracefully fall back. + if ( + svg.indexOf(' -1 && + options.type !== 'image/svg+xml' + ) { + fallbackToExportServer(); + } else { + Highcharts.downloadSVGLocal( + svg, + options, + fallbackToExportServer + ); + } + }; + + // If we are on IE and in styled mode, add a whitelist to the renderer for + // inline styles that we want to pass through. There are so many styles by + // default in IE that we don't want to blacklist them all. + /*= if (!build.classic) { =*/ + if (isMSBrowser) { + Highcharts.SVGRenderer.prototype.inlineWhitelist = [ + /^blockSize/, + /^border/, + /^caretColor/, + /^color/, + /^columnRule/, + /^columnRuleColor/, + /^cssFloat/, + /^cursor/, + /^fill$/, + /^fillOpacity/, + /^font/, + /^inlineSize/, + /^length/, + /^lineHeight/, + /^opacity/, + /^outline/, + /^parentRule/, + /^rx$/, + /^ry$/, + /^stroke/, + /^textAlign/, + /^textAnchor/, + /^textDecoration/, + /^transform/, + /^vectorEffect/, + /^visibility/, + /^x$/, + /^y$/ + ]; + } + /*= } =*/ + + // Always fall back on: + // - MS browsers: Embedded images JPEG/PNG, or any PDF + // - Embedded images and PDF + if ( + ( + isMSBrowser && + ( + options.type === 'application/pdf' || + chart.container.getElementsByTagName('image').length && + options.type !== 'image/svg+xml' + ) + ) || ( + options.type === 'application/pdf' && + chart.container.getElementsByTagName('image').length + ) + ) { + fallbackToExportServer(); + return; + } + + chart.getSVGForLocalExport( + options, + chartOptions, + fallbackToExportServer, + svgSuccess + ); }; // Extend the default options to use the local exporter logic merge(true, Highcharts.getOptions().exporting, { - libURL: 'https://code.highcharts.com/@product.version@/lib/', - - // When offline-exporting is loaded, redefine the menu item definitions - // related to download. - menuItemDefinitions: { - downloadPNG: { - textKey: 'downloadPNG', - onclick: function () { - this.exportChartLocal(); - } - }, - downloadJPEG: { - textKey: 'downloadJPEG', - onclick: function () { - this.exportChartLocal({ - type: 'image/jpeg' - }); - } - }, - downloadSVG: { - textKey: 'downloadSVG', - onclick: function () { - this.exportChartLocal({ - type: 'image/svg+xml' - }); - } - }, - downloadPDF: { - textKey: 'downloadPDF', - onclick: function () { - this.exportChartLocal({ - type: 'application/pdf' - }); - } - } - - } + libURL: 'https://code.highcharts.com/@product.version@/lib/', + + // When offline-exporting is loaded, redefine the menu item definitions + // related to download. + menuItemDefinitions: { + downloadPNG: { + textKey: 'downloadPNG', + onclick: function () { + this.exportChartLocal(); + } + }, + downloadJPEG: { + textKey: 'downloadJPEG', + onclick: function () { + this.exportChartLocal({ + type: 'image/jpeg' + }); + } + }, + downloadSVG: { + textKey: 'downloadSVG', + onclick: function () { + this.exportChartLocal({ + type: 'image/svg+xml' + }); + } + }, + downloadPDF: { + textKey: 'downloadPDF', + onclick: function () { + this.exportChartLocal({ + type: 'application/pdf' + }); + } + } + + } }); diff --git a/js/modules/oldie.src.js b/js/modules/oldie.src.js index 76a138b7327..9fe7d1ab9d4 100644 --- a/js/modules/oldie.src.js +++ b/js/modules/oldie.src.js @@ -1,7 +1,7 @@ /** * (c) 2010-2017 Torstein Honsi * - * Support for old IE browsers (6, 7 and 8) in Highcharts v6+. + * Support for old IE browsers (6, 7 and 8) in Highcharts v6+. * * License: www.highcharts.com/license */ @@ -11,32 +11,32 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/SvgRenderer.js'; var VMLRenderer, - VMLRendererExtension, - VMLElement, - - Chart = H.Chart, - createElement = H.createElement, - css = H.css, - defined = H.defined, - deg2rad = H.deg2rad, - discardElement = H.discardElement, - doc = H.doc, - each = H.each, - erase = H.erase, - extend = H.extend, - extendClass = H.extendClass, - isArray = H.isArray, - isNumber = H.isNumber, - isObject = H.isObject, - merge = H.merge, - noop = H.noop, - pick = H.pick, - pInt = H.pInt, - svg = H.svg, - SVGElement = H.SVGElement, - SVGRenderer = H.SVGRenderer, - win = H.win, - wrap = H.wrap; + VMLRendererExtension, + VMLElement, + + Chart = H.Chart, + createElement = H.createElement, + css = H.css, + defined = H.defined, + deg2rad = H.deg2rad, + discardElement = H.discardElement, + doc = H.doc, + each = H.each, + erase = H.erase, + extend = H.extend, + extendClass = H.extendClass, + isArray = H.isArray, + isNumber = H.isNumber, + isObject = H.isObject, + merge = H.merge, + noop = H.noop, + pick = H.pick, + pInt = H.pInt, + svg = H.svg, + SVGElement = H.SVGElement, + SVGRenderer = H.SVGRenderer, + win = H.win, + wrap = H.wrap; /** @@ -54,1463 +54,1463 @@ var VMLRenderer, * @since 2.3.0 */ H.getOptions().global.VMLRadialGradientURL = - 'http://code.highcharts.com/@product.version@/gfx/vml-radial-gradient.png'; + 'http://code.highcharts.com/@product.version@/gfx/vml-radial-gradient.png'; // Utilites if (doc && !doc.defaultView) { - H.getStyle = function (el, prop) { - var val, - alias = { width: 'clientWidth', height: 'clientHeight' }[prop]; - - if (el.style[prop]) { - return H.pInt(el.style[prop]); - } - if (prop === 'opacity') { - prop = 'filter'; - } - - // Getting the rendered width and height - if (alias) { - el.style.zoom = 1; - return Math.max(el[alias] - 2 * H.getStyle(el, 'padding'), 0); - } - - val = el.currentStyle[prop.replace(/\-(\w)/g, function (a, b) { - return b.toUpperCase(); - })]; - if (prop === 'filter') { - val = val.replace( - /alpha\(opacity=([0-9]+)\)/, - function (a, b) { - return b / 100; - } - ); - } - - return val === '' ? 1 : H.pInt(val); - }; + H.getStyle = function (el, prop) { + var val, + alias = { width: 'clientWidth', height: 'clientHeight' }[prop]; + + if (el.style[prop]) { + return H.pInt(el.style[prop]); + } + if (prop === 'opacity') { + prop = 'filter'; + } + + // Getting the rendered width and height + if (alias) { + el.style.zoom = 1; + return Math.max(el[alias] - 2 * H.getStyle(el, 'padding'), 0); + } + + val = el.currentStyle[prop.replace(/\-(\w)/g, function (a, b) { + return b.toUpperCase(); + })]; + if (prop === 'filter') { + val = val.replace( + /alpha\(opacity=([0-9]+)\)/, + function (a, b) { + return b / 100; + } + ); + } + + return val === '' ? 1 : H.pInt(val); + }; } if (!Array.prototype.forEach) { - H.forEachPolyfill = function (fn, ctx) { - var i = 0, - len = this.length; - for (; i < len; i++) { - if (fn.call(ctx, this[i], i, this) === false) { - return i; - } - } - }; + H.forEachPolyfill = function (fn, ctx) { + var i = 0, + len = this.length; + for (; i < len; i++) { + if (fn.call(ctx, this[i], i, this) === false) { + return i; + } + } + }; } if (!Array.prototype.indexOf) { - H.indexOfPolyfill = function (arr) { - var len, - i = 0; - - if (arr) { - len = arr.length; - - for (; i < len; i++) { - if (arr[i] === this) { - return i; - } - } - } - - return -1; - }; + H.indexOfPolyfill = function (arr) { + var len, + i = 0; + + if (arr) { + len = arr.length; + + for (; i < len; i++) { + if (arr[i] === this) { + return i; + } + } + } + + return -1; + }; } if (!Array.prototype.filter) { - H.filterPolyfill = function (fn) { - var ret = [], - i = 0, - length = this.length; - - for (; i < length; i++) { - if (fn(this[i], i)) { - ret.push(this[i]); - } - } - - return ret; - }; + H.filterPolyfill = function (fn) { + var ret = [], + i = 0, + length = this.length; + + for (; i < length; i++) { + if (fn(this[i], i)) { + ret.push(this[i]); + } + } + + return ret; + }; } if (!Array.prototype.some) { - H.some = function (fn, ctx) { // legacy - var i = 0, - len = this.length; - - for (; i < len; i++) { - if (fn.call(ctx, this[i], i, this) === true) { - return; - } - } - }; + H.some = function (fn, ctx) { // legacy + var i = 0, + len = this.length; + + for (; i < len; i++) { + if (fn.call(ctx, this[i], i, this) === true) { + return; + } + } + }; } if (!Object.prototype.keys) { - H.keysPolyfill = function (obj) { - var result = [], - hasOwnProperty = Object.prototype.hasOwnProperty, - prop; - for (prop in obj) { - if (hasOwnProperty.call(obj, prop)) { - result.push(prop); - } - } - return result; - }; + H.keysPolyfill = function (obj) { + var result = [], + hasOwnProperty = Object.prototype.hasOwnProperty, + prop; + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + return result; + }; } if (!Array.prototype.reduce) { - H.reducePolyfill = function (func, initialValue) { - var context = this, - accumulator = initialValue || {}, - len = this.length; - for (var i = 0; i < len; ++i) { - accumulator = func.call(context, accumulator, this[i], i, this); - } - return accumulator; - }; + H.reducePolyfill = function (func, initialValue) { + var context = this, + accumulator = initialValue || {}, + len = this.length; + for (var i = 0; i < len; ++i) { + accumulator = func.call(context, accumulator, this[i], i, this); + } + return accumulator; + }; } if (!svg) { - // Prevent wrapping from creating false offsetWidths in export in legacy IE. - // This applies only to charts for export, where IE runs the SVGRenderer - // instead of the VMLRenderer - // (#1079, #1063) - wrap(H.SVGRenderer.prototype, 'text', function (proceed) { - return proceed.apply( - this, - Array.prototype.slice.call(arguments, 1) - ).css({ - position: 'absolute' - }); - }); - - /** - * Old IE override for pointer normalize, adds chartX and chartY to event - * arguments. - */ - H.Pointer.prototype.normalize = function (e, chartPosition) { - - e = e || win.event; - if (!e.target) { - e.target = e.srcElement; - } - - // Get mouse position - if (!chartPosition) { - this.chartPosition = chartPosition = H.offset(this.chart.container); - } - - return H.extend(e, { - // #2005, #2129: the second case is for IE10 quirks mode within - // framesets - chartX: Math.round(Math.max(e.x, e.clientX - chartPosition.left)), - chartY: Math.round(e.y) - }); - }; - - /** - * Further sanitize the mock-SVG that is generated when exporting charts in - * oldIE. - */ - Chart.prototype.ieSanitizeSVG = function (svg) { - svg = svg - .replace(//g, '<$1title>') - .replace(/height=([^" ]+)/g, 'height="$1"') - .replace(/width=([^" ]+)/g, 'width="$1"') - .replace(/hc-svg-href="([^"]+)">/g, 'xlink:href="$1"/>') - .replace(/ id=([^" >]+)/g, ' id="$1"') // #4003 - .replace(/class=([^" >]+)/g, 'class="$1"') - .replace(/ transform /g, ' ') - .replace(/:(path|rect)/g, '$1') - .replace(/style="([^"]+)"/g, function (s) { - return s.toLowerCase(); - }); - - return svg; - }; - - /** - * VML namespaces can't be added until after complete. Listening - * for Perini's doScroll hack is not enough. - * - * @todo: Move this to the oldie.js module. - * @private - */ - Chart.prototype.isReadyToRender = function () { - var chart = this; - - // Note: win == win.top is required - if ( - !svg && - ( - win == win.top && // eslint-disable-line eqeqeq - doc.readyState !== 'complete' - ) - ) { - doc.attachEvent('onreadystatechange', function () { - doc.detachEvent('onreadystatechange', chart.firstRender); - if (doc.readyState === 'complete') { - chart.firstRender(); - } - }); - return false; - } - return true; - }; - - // IE compatibility hack for generating SVG content that it doesn't really - // understand. Used by the exporting module. - if (!doc.createElementNS) { - doc.createElementNS = function (ns, tagName) { - return doc.createElement(tagName); - }; - } - - /** - * Old IE polyfill for addEventListener, called from inside the addEvent - * function. - */ - H.addEventListenerPolyfill = function (type, fn) { - var el = this; - function wrappedFn(e) { - e.target = e.srcElement || win; // #2820 - fn.call(el, e); - } - - if (el.attachEvent) { - if (!el.hcEventsIE) { - el.hcEventsIE = {}; - } - - // unique function string (#6746) - if (!fn.hcKey) { - fn.hcKey = H.uniqueKey(); - } - - // Link wrapped fn with original fn, so we can get this in - // removeEvent - el.hcEventsIE[fn.hcKey] = wrappedFn; - - el.attachEvent('on' + type, wrappedFn); - } - - }; - H.removeEventListenerPolyfill = function (type, fn) { - if (this.detachEvent) { - fn = this.hcEventsIE[fn.hcKey]; - this.detachEvent('on' + type, fn); - } - }; - - - /** - * The VML element wrapper. - */ - VMLElement = { - - docMode8: doc && doc.documentMode === 8, - - /** - * Initialize a new VML element wrapper. It builds the markup as a - * string to minimize DOM traffic. - * @param {Object} renderer - * @param {Object} nodeName - */ - init: function (renderer, nodeName) { - var wrapper = this, - markup = ['<', nodeName, ' filled="f" stroked="f"'], - style = ['position: ', 'absolute', ';'], - isDiv = nodeName === 'div'; - - // divs and shapes need size - if (nodeName === 'shape' || isDiv) { - style.push('left:0;top:0;width:1px;height:1px;'); - } - style.push('visibility: ', isDiv ? 'hidden' : 'visible'); - - markup.push(' style="', style.join(''), '"/>'); - - // create element with default attributes and style - if (nodeName) { - markup = isDiv || nodeName === 'span' || nodeName === 'img' ? - markup.join('') : - renderer.prepVML(markup); - wrapper.element = createElement(markup); - } - - wrapper.renderer = renderer; - }, - - /** - * Add the node to the given parent - * @param {Object} parent - */ - add: function (parent) { - var wrapper = this, - renderer = wrapper.renderer, - element = wrapper.element, - box = renderer.box, - inverted = parent && parent.inverted, - - // get the parent node - parentNode = parent ? - parent.element || parent : - box; - - if (parent) { - this.parentGroup = parent; - } - - // if the parent group is inverted, apply inversion on all children - if (inverted) { // only on groups - renderer.invertChild(element, parentNode); - } - - // append it - parentNode.appendChild(element); - - // align text after adding to be able to read offset - wrapper.added = true; - if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { - wrapper.updateTransform(); - } - - // fire an event for internal hooks - if (wrapper.onAdd) { - wrapper.onAdd(); - } - - // IE8 Standards can't set the class name before the element is - // appended - if (this.className) { - this.attr('class', this.className); - } - - return wrapper; - }, - - /** - * VML always uses htmlUpdateTransform - */ - updateTransform: SVGElement.prototype.htmlUpdateTransform, - - /** - * Set the rotation of a span with oldIE's filter - */ - setSpanRotation: function () { - // Adjust for alignment and rotation. Rotation of useHTML content is - // not yet implemented but it can probably be implemented for - // Firefox 3.5+ on user request. FF3.5+ has support for CSS3 - // transform. The getBBox method also needs to be updated to - // compensate for the rotation, like it currently does for SVG. - // Test case: http://jsfiddle.net/highcharts/Ybt44/ - - var rotation = this.rotation, - costheta = Math.cos(rotation * deg2rad), - sintheta = Math.sin(rotation * deg2rad); - - css(this.element, { - filter: rotation ? [ - 'progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, - ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, - ', sizingMethod=\'auto expand\')' - ].join('') : 'none' - }); - }, - - /** - * Get the positioning correction for the span after rotating. - */ - getSpanCorrection: function ( - width, - baseline, - alignCorrection, - rotation, - align - ) { - - var costheta = rotation ? Math.cos(rotation * deg2rad) : 1, - sintheta = rotation ? Math.sin(rotation * deg2rad) : 0, - height = pick(this.elemHeight, this.element.offsetHeight), - quad, - nonLeft = align && align !== 'left'; - - // correct x and y - this.xCorr = costheta < 0 && -width; - this.yCorr = sintheta < 0 && -height; - - // correct for baseline and corners spilling out after rotation - quad = costheta * sintheta < 0; - this.xCorr += ( - sintheta * - baseline * - (quad ? 1 - alignCorrection : alignCorrection) - ); - this.yCorr -= ( - costheta * - baseline * - (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1) - ); - // correct for the length/height of the text - if (nonLeft) { - this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); - if (rotation) { - this.yCorr -= ( - height * - alignCorrection * - (sintheta < 0 ? -1 : 1) - ); - } - css(this.element, { - textAlign: align - }); - } - }, - - /** - * Converts a subset of an SVG path definition to its VML counterpart. - * Takes an array as the parameter and returns a string. - */ - pathToVML: function (value) { - // convert paths - var i = value.length, - path = []; - - while (i--) { - - // Multiply by 10 to allow subpixel precision. - // Substracting half a pixel seems to make the coordinates - // align with SVG, but this hasn't been tested thoroughly - if (isNumber(value[i])) { - path[i] = Math.round(value[i] * 10) - 5; - } else if (value[i] === 'Z') { // close the path - path[i] = 'x'; - } else { - path[i] = value[i]; - - // When the start X and end X coordinates of an arc are too - // close, they are rounded to the same value above. In this - // case, substract or add 1 from the end X and Y positions. - // #186, #760, #1371, #1410. - if ( - value.isArc && - (value[i] === 'wa' || value[i] === 'at') - ) { - // Start and end X - if (path[i + 5] === path[i + 7]) { - path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1; - } - // Start and end Y - if (path[i + 6] === path[i + 8]) { - path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1; - } - } - } - } - - return path.join(' ') || 'x'; - }, - - /** - * Set the element's clipping to a predefined rectangle - * - * @param {String} id The id of the clip rectangle - */ - clip: function (clipRect) { - var wrapper = this, - clipMembers, - cssRet; - - if (clipRect) { - clipMembers = clipRect.members; - - // Ensure unique list of elements (#1258) - erase(clipMembers, wrapper); - clipMembers.push(wrapper); - wrapper.destroyClip = function () { - erase(clipMembers, wrapper); - }; - cssRet = clipRect.getCSS(wrapper); - - } else { - if (wrapper.destroyClip) { - wrapper.destroyClip(); - } - cssRet = { - clip: wrapper.docMode8 ? 'inherit' : 'rect(auto)' - }; // #1214 - } - - return wrapper.css(cssRet); - - }, - - /** - * Set styles for the element - * @param {Object} styles - */ - css: SVGElement.prototype.htmlCss, - - /** - * Removes a child either by removeChild or move to garbageBin. - * Issue 490; in VML removeChild results in Orphaned nodes according to - * sIEve, discardElement does not. - */ - safeRemoveChild: function (element) { - // discardElement will detach the node from its parent before - // attaching it to the garbage bin. Therefore it is important that - // the node is attached and have parent. - if (element.parentNode) { - discardElement(element); - } - }, - - /** - * Extend element.destroy by removing it from the clip members array - */ - destroy: function () { - if (this.destroyClip) { - this.destroyClip(); - } - - return SVGElement.prototype.destroy.apply(this); - }, - - /** - * Add an event listener. VML override for normalizing event parameters. - * @param {String} eventType - * @param {Function} handler - */ - on: function (eventType, handler) { - // simplest possible event model for internal use - this.element['on' + eventType] = function () { - var evt = win.event; - evt.target = evt.srcElement; - handler(evt); - }; - return this; - }, - - /** - * In stacked columns, cut off the shadows so that they don't overlap - */ - cutOffPath: function (path, length) { - - var len; - - // The extra comma tricks the trailing comma remover in - // "gulp scripts" task - path = path.split(/[ ,,]/); - len = path.length; - - if (len === 9 || len === 11) { - path[len - 4] = path[len - 2] = - pInt(path[len - 2]) - 10 * length; - } - return path.join(' '); - }, - - /** - * Apply a drop shadow by copying elements and giving them different - * strokes - * @param {Boolean|Object} shadowOptions - */ - shadow: function (shadowOptions, group, cutOff) { - var shadows = [], - i, - element = this.element, - renderer = this.renderer, - shadow, - elemStyle = element.style, - markup, - path = element.path, - strokeWidth, - modifiedPath, - shadowWidth, - shadowElementOpacity; - - // some times empty paths are not strings - if (path && typeof path.value !== 'string') { - path = 'x'; - } - modifiedPath = path; - - if (shadowOptions) { - shadowWidth = pick(shadowOptions.width, 3); - shadowElementOpacity = - (shadowOptions.opacity || 0.15) / shadowWidth; - for (i = 1; i <= 3; i++) { - - strokeWidth = (shadowWidth * 2) + 1 - (2 * i); - - // Cut off shadows for stacked column items - if (cutOff) { - modifiedPath = this.cutOffPath( - path.value, - strokeWidth + 0.5 - ); - } - - markup = [ - '' - ]; - - shadow = createElement(renderer.prepVML(markup), - null, { - left: pInt(elemStyle.left) + - pick(shadowOptions.offsetX, 1), - top: pInt(elemStyle.top) + - pick(shadowOptions.offsetY, 1) - } - ); - if (cutOff) { - shadow.cutOff = strokeWidth + 1; - } - - // apply the opacity - markup = [ - '']; - createElement(renderer.prepVML(markup), null, null, shadow); - - - // insert it - if (group) { - group.element.appendChild(shadow); - } else { - element.parentNode.insertBefore(shadow, element); - } - - // record it - shadows.push(shadow); - - } - - this.shadows = shadows; - } - return this; - }, - updateShadows: noop, // Used in SVG only - - setAttr: function (key, value) { - if (this.docMode8) { // IE8 setAttribute bug - this.element[key] = value; - } else { - this.element.setAttribute(key, value); - } - }, - getAttr: function (key) { - if (this.docMode8) { // IE8 setAttribute bug - return this.element[key]; - } - return this.element.getAttribute(key); - }, - classSetter: function (value) { - // IE8 Standards mode has problems retrieving the className unless - // set like this. IE8 Standards can't set the class name before the - // element is appended. - (this.added ? this.element : this).className = value; - }, - dashstyleSetter: function (value, key, element) { - var strokeElem = element.getElementsByTagName('stroke')[0] || - createElement( - this.renderer.prepVML(['']), - null, - null, - element - ); - strokeElem[key] = value || 'solid'; - // Because changing stroke-width will change the dash length and - // cause an epileptic effect - this[key] = value; - }, - dSetter: function (value, key, element) { - var i, - shadows = this.shadows; - value = value || []; - // Used in getter for animation - this.d = value.join && value.join(' '); - - element.path = value = this.pathToVML(value); - - // update shadows - if (shadows) { - i = shadows.length; - while (i--) { - shadows[i].path = shadows[i].cutOff ? - this.cutOffPath(value, shadows[i].cutOff) : - value; - } - } - this.setAttr(key, value); - }, - fillSetter: function (value, key, element) { - var nodeName = element.nodeName; - if (nodeName === 'SPAN') { // text color - element.style.color = value; - } else if (nodeName !== 'IMG') { // #1336 - element.filled = value !== 'none'; - this.setAttr( - 'fillcolor', - this.renderer.color(value, element, key, this) - ); - } - }, - 'fill-opacitySetter': function (value, key, element) { - createElement( - this.renderer.prepVML( - ['<', key.split('-')[0], ' opacity="', value, '"/>'] - ), - null, - null, - element - ); - }, - // Don't bother - animation is too slow and filters introduce artifacts - opacitySetter: noop, - rotationSetter: function (value, key, element) { - var style = element.style; - this[key] = style[key] = value; // style is for #1873 - - // Correction for the 1x1 size of the shape container. Used in gauge - // needles. - style.left = -Math.round(Math.sin(value * deg2rad) + 1) + 'px'; - style.top = Math.round(Math.cos(value * deg2rad)) + 'px'; - }, - strokeSetter: function (value, key, element) { - this.setAttr( - 'strokecolor', - this.renderer.color(value, element, key, this) - ); - }, - 'stroke-widthSetter': function (value, key, element) { - element.stroked = !!value; // VML "stroked" attribute - this[key] = value; // used in getter, issue #113 - if (isNumber(value)) { - value += 'px'; - } - this.setAttr('strokeweight', value); - }, - titleSetter: function (value, key) { - this.setAttr(key, value); - }, - visibilitySetter: function (value, key, element) { - - // Handle inherited visibility - if (value === 'inherit') { - value = 'visible'; - } - - // Let the shadow follow the main element - if (this.shadows) { - each(this.shadows, function (shadow) { - shadow.style[key] = value; - }); - } - - // Instead of toggling the visibility CSS property, move the div out - // of the viewport. This works around #61 and #586 - if (element.nodeName === 'DIV') { - value = value === 'hidden' ? '-999em' : 0; - - // In order to redraw, IE7 needs the div to be visible when - // tucked away outside the viewport. So the visibility is - // actually opposite of the expected value. This applies to the - // tooltip only. - if (!this.docMode8) { - element.style[key] = value ? 'visible' : 'hidden'; - } - key = 'top'; - } - element.style[key] = value; - }, - xSetter: function (value, key, element) { - this[key] = value; // used in getter - - if (key === 'x') { - key = 'left'; - } else if (key === 'y') { - key = 'top'; - } - - // clipping rectangle special - if (this.updateClipping) { - // the key is now 'left' or 'top' for 'x' and 'y' - this[key] = value; - this.updateClipping(); - } else { - // normal - element.style[key] = value; - } - }, - zIndexSetter: function (value, key, element) { - element.style[key] = value; - }, - fillGetter: function () { - return this.getAttr('fillcolor') || ''; - }, - strokeGetter: function () { - return this.getAttr('strokecolor') || ''; - }, - // #7850 - classGetter: function () { - return this.getAttr('className') || ''; - } - }; - VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter']; - H.VMLElement = VMLElement = extendClass(SVGElement, VMLElement); - - // Some shared setters - VMLElement.prototype.ySetter = - VMLElement.prototype.widthSetter = - VMLElement.prototype.heightSetter = - VMLElement.prototype.xSetter; - - - /** - * The VML renderer - */ - VMLRendererExtension = { // inherit SVGRenderer - - Element: VMLElement, - isIE8: win.navigator.userAgent.indexOf('MSIE 8.0') > -1, - - - /** - * Initialize the VMLRenderer - * @param {Object} container - * @param {Number} width - * @param {Number} height - */ - init: function (container, width, height) { - var renderer = this, - boxWrapper, - box, - css; - - renderer.alignedObjects = []; - - boxWrapper = renderer.createElement('div') - .css({ position: 'relative' }); - box = boxWrapper.element; - container.appendChild(boxWrapper.element); - - - // generate the containing box - renderer.isVML = true; - renderer.box = box; - renderer.boxWrapper = boxWrapper; - renderer.gradients = {}; - renderer.cache = {}; // Cache for numerical bounding boxes - renderer.cacheKeys = []; - renderer.imgCount = 0; - - - renderer.setSize(width, height, false); - - // The only way to make IE6 and IE7 print is to use a global - // namespace. However, with IE8 the only way to make the dynamic - // shapes visible in screen and print mode seems to be to add the - // xmlns attribute and the behaviour style inline. - if (!doc.namespaces.hcv) { - - doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); - - // Setup default CSS (#2153, #2368, #2384) - css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + - '{ behavior:url(#default#VML); display: inline-block; } '; - try { - doc.createStyleSheet().cssText = css; - } catch (e) { - doc.styleSheets[0].cssText += css; - } - - } - }, - - - /** - * Detect whether the renderer is hidden. This happens when one of the - * parent elements has display: none - */ - isHidden: function () { - return !this.box.offsetWidth; - }, - - /** - * Define a clipping rectangle. In VML it is accomplished by storing the - * values for setting the CSS style to all associated members. - * - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - clipRect: function (x, y, width, height) { - - // create a dummy element - var clipRect = this.createElement(), - isObj = isObject(x); - - // mimic a rectangle with its style object for automatic updating in - // attr - return extend(clipRect, { - members: [], - count: 0, - left: (isObj ? x.x : x) + 1, - top: (isObj ? x.y : y) + 1, - width: (isObj ? x.width : width) - 1, - height: (isObj ? x.height : height) - 1, - getCSS: function (wrapper) { - var element = wrapper.element, - nodeName = element.nodeName, - isShape = nodeName === 'shape', - inverted = wrapper.inverted, - rect = this, - top = rect.top - (isShape ? element.offsetTop : 0), - left = rect.left, - right = left + rect.width, - bottom = top + rect.height, - ret = { - clip: 'rect(' + - Math.round(inverted ? left : top) + 'px,' + - Math.round(inverted ? bottom : right) + 'px,' + - Math.round(inverted ? right : bottom) + 'px,' + - Math.round(inverted ? top : left) + 'px)' - }; - - // issue 74 workaround - if (!inverted && wrapper.docMode8 && nodeName === 'DIV') { - extend(ret, { - width: right + 'px', - height: bottom + 'px' - }); - } - return ret; - }, - - // used in attr and animation to update the clipping of all - // members - updateClipping: function () { - each(clipRect.members, function (member) { - // Member.element is falsy on deleted series, like in - // stock/members/series-remove demo. Should be removed - // from members, but this will do. - if (member.element) { - member.css(clipRect.getCSS(member)); - } - }); - } - }); - - }, - - - /** - * Take a color and return it if it's a string, make it a gradient if - * it's a gradient configuration object, and apply opacity. - * - * @param {Object} color The color or config object - */ - color: function (color, elem, prop, wrapper) { - var renderer = this, - colorObject, - regexRgba = /^rgba/, - markup, - fillType, - ret = 'none'; - - // Check for linear or radial gradient - if (color && color.linearGradient) { - fillType = 'gradient'; - } else if (color && color.radialGradient) { - fillType = 'pattern'; - } - - - if (fillType) { - - var stopColor, - stopOpacity, - gradient = color.linearGradient || color.radialGradient, - x1, - y1, - x2, - y2, - opacity1, - opacity2, - color1, - color2, - fillAttr = '', - stops = color.stops, - firstStop, - lastStop, - colors = [], - addFillNode = function () { - // Add the fill subnode. When colors attribute is used, - // the meanings of opacity and o:opacity2 are reversed. - markup = ['']; - createElement( - renderer.prepVML(markup), - null, - null, - elem - ); - }; - - // Extend from 0 to 1 - firstStop = stops[0]; - lastStop = stops[stops.length - 1]; - if (firstStop[0] > 0) { - stops.unshift([ - 0, - firstStop[1] - ]); - } - if (lastStop[0] < 1) { - stops.push([ - 1, - lastStop[1] - ]); - } - - // Compute the stops - each(stops, function (stop, i) { - if (regexRgba.test(stop[1])) { - colorObject = H.color(stop[1]); - stopColor = colorObject.get('rgb'); - stopOpacity = colorObject.get('a'); - } else { - stopColor = stop[1]; - stopOpacity = 1; - } - - // Build the color attribute - colors.push((stop[0] * 100) + '% ' + stopColor); - - // Only start and end opacities are allowed, so we use the - // first and the last - if (!i) { - opacity1 = stopOpacity; - color2 = stopColor; - } else { - opacity2 = stopOpacity; - color1 = stopColor; - } - }); - - // Apply the gradient to fills only. - if (prop === 'fill') { - - // Handle linear gradient angle - if (fillType === 'gradient') { - x1 = gradient.x1 || gradient[0] || 0; - y1 = gradient.y1 || gradient[1] || 0; - x2 = gradient.x2 || gradient[2] || 0; - y2 = gradient.y2 || gradient[3] || 0; - fillAttr = 'angle="' + (90 - Math.atan( - (y2 - y1) / // y vector - (x2 - x1) // x vector - ) * 180 / Math.PI) + '"'; - - addFillNode(); - - // Radial (circular) gradient - } else { - - var r = gradient.r, - sizex = r * 2, - sizey = r * 2, - cx = gradient.cx, - cy = gradient.cy, - radialReference = elem.radialReference, - bBox, - applyRadialGradient = function () { - if (radialReference) { - bBox = wrapper.getBBox(); - cx += (radialReference[0] - bBox.x) / - bBox.width - 0.5; - cy += (radialReference[1] - bBox.y) / - bBox.height - 0.5; - sizex *= radialReference[2] / bBox.width; - sizey *= radialReference[2] / bBox.height; - } - fillAttr = 'src="' + - H.getOptions().global.VMLRadialGradientURL + - '" ' + - 'size="' + sizex + ',' + sizey + '" ' + - 'origin="0.5,0.5" ' + - 'position="' + cx + ',' + cy + '" ' + - 'color2="' + color2 + '" '; - - addFillNode(); - }; - - // Apply radial gradient - if (wrapper.added) { - applyRadialGradient(); - } else { - // We need to know the bounding box to get the size - // and position right - wrapper.onAdd = applyRadialGradient; - } - - // The fill element's color attribute is broken in IE8 - // standards mode, so we need to set the parent shape's - // fillcolor attribute instead. - ret = color1; - } - - // Gradients are not supported for VML stroke, return the first - // color. #722. - } else { - ret = stopColor; - } - - // If the color is an rgba color, split it and add a fill node - // to hold the opacity component - } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { - - colorObject = H.color(color); - - wrapper[prop + '-opacitySetter']( - colorObject.get('a'), - prop, - elem - ); - - ret = colorObject.get('rgb'); - - - } else { - // 'stroke' or 'fill' node - var propNodes = elem.getElementsByTagName(prop); - if (propNodes.length) { - propNodes[0].opacity = 1; - propNodes[0].type = 'solid'; - } - ret = color; - } - - return ret; - }, - - /** - * Take a VML string and prepare it for either IE8 or IE6/IE7. - * @param {Array} markup A string array of the VML markup to prepare - */ - prepVML: function (markup) { - var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', - isIE8 = this.isIE8; - - markup = markup.join(''); - - if (isIE8) { // add xmlns and style inline - markup = markup.replace( - '/>', - ' xmlns="urn:schemas-microsoft-com:vml" />' - ); - if (markup.indexOf('style="') === -1) { - markup = markup.replace( - '/>', - ' style="' + vmlStyle + '" />' - ); - } else { - markup = markup.replace('style="', 'style="' + vmlStyle); - } - - } else { // add namespace - markup = markup.replace('<', ' 1) { - obj.attr({ - x: x, - y: y, - width: width, - height: height - }); - } - return obj; - }, - - /** - * For rectangles, VML uses a shape for rect to overcome bugs and - * rotation problems - */ - createElement: function (nodeName) { - return nodeName === 'rect' ? - this.symbol(nodeName) : - SVGRenderer.prototype.createElement.call(this, nodeName); - }, - - /** - * In the VML renderer, each child of an inverted div (group) is - * inverted - * @param {Object} element - * @param {Object} parentNode - */ - invertChild: function (element, parentNode) { - var ren = this, - parentStyle = parentNode.style, - imgStyle = element.tagName === 'IMG' && element.style; // #1111 - - css(element, { - flip: 'x', - left: pInt(parentStyle.width) - - (imgStyle ? pInt(imgStyle.top) : 1), - top: pInt(parentStyle.height) - - (imgStyle ? pInt(imgStyle.left) : 1), - rotation: -90 - }); - - // Recursively invert child elements, needed for nested composite - // shapes like box plots and error bars. #1680, #1806. - each(element.childNodes, function (child) { - ren.invertChild(child, element); - }); - }, - - /** - * Symbol definitions that override the parent SVG renderer's symbols - * - */ - symbols: { - // VML specific arc function - arc: function (x, y, w, h, options) { - var start = options.start, - end = options.end, - radius = options.r || w || h, - innerRadius = options.innerR, - cosStart = Math.cos(start), - sinStart = Math.sin(start), - cosEnd = Math.cos(end), - sinEnd = Math.sin(end), - ret; - - if (end - start === 0) { // no angle, don't show it. - return ['x']; - } - - ret = [ - 'wa', // clockwise arc to - x - radius, // left - y - radius, // top - x + radius, // right - y + radius, // bottom - x + radius * cosStart, // start x - y + radius * sinStart, // start y - x + radius * cosEnd, // end x - y + radius * sinEnd // end y - ]; - - if (options.open && !innerRadius) { - ret.push( - 'e', - 'M', - x, // - innerRadius, - y // - innerRadius - ); - } - - ret.push( - 'at', // anti clockwise arc to - x - innerRadius, // left - y - innerRadius, // top - x + innerRadius, // right - y + innerRadius, // bottom - x + innerRadius * cosEnd, // start x - y + innerRadius * sinEnd, // start y - x + innerRadius * cosStart, // end x - y + innerRadius * sinStart, // end y - 'x', // finish path - 'e' // close - ); - - ret.isArc = true; - return ret; - - }, - // Add circle symbol path. This performs significantly faster than - // v:oval. - circle: function (x, y, w, h, wrapper) { - - if (wrapper && defined(wrapper.r)) { - w = h = 2 * wrapper.r; - } - - // Center correction, #1682 - if (wrapper && wrapper.isCircle) { - x -= w / 2; - y -= h / 2; - } - - // Return the path - return [ - 'wa', // clockwisearcto - x, // left - y, // top - x + w, // right - y + h, // bottom - x + w, // start x - y + h / 2, // start y - x + w, // end x - y + h / 2, // end y - 'e' // close - ]; - }, - /** - * Add rectangle symbol path which eases rotation and omits arcsize - * problems compared to the built-in VML roundrect shape. When - * borders are not rounded, use the simpler square path, else use - * the callout path without the arrow. - */ - rect: function (x, y, w, h, options) { - return SVGRenderer.prototype.symbols[ - !defined(options) || !options.r ? 'square' : 'callout' - ].call(0, x, y, w, h, options); - } - } - }; - H.VMLRenderer = VMLRenderer = function () { - this.init.apply(this, arguments); - }; - VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); - - // general renderer - H.Renderer = VMLRenderer; + // Prevent wrapping from creating false offsetWidths in export in legacy IE. + // This applies only to charts for export, where IE runs the SVGRenderer + // instead of the VMLRenderer + // (#1079, #1063) + wrap(H.SVGRenderer.prototype, 'text', function (proceed) { + return proceed.apply( + this, + Array.prototype.slice.call(arguments, 1) + ).css({ + position: 'absolute' + }); + }); + + /** + * Old IE override for pointer normalize, adds chartX and chartY to event + * arguments. + */ + H.Pointer.prototype.normalize = function (e, chartPosition) { + + e = e || win.event; + if (!e.target) { + e.target = e.srcElement; + } + + // Get mouse position + if (!chartPosition) { + this.chartPosition = chartPosition = H.offset(this.chart.container); + } + + return H.extend(e, { + // #2005, #2129: the second case is for IE10 quirks mode within + // framesets + chartX: Math.round(Math.max(e.x, e.clientX - chartPosition.left)), + chartY: Math.round(e.y) + }); + }; + + /** + * Further sanitize the mock-SVG that is generated when exporting charts in + * oldIE. + */ + Chart.prototype.ieSanitizeSVG = function (svg) { + svg = svg + .replace(//g, '<$1title>') + .replace(/height=([^" ]+)/g, 'height="$1"') + .replace(/width=([^" ]+)/g, 'width="$1"') + .replace(/hc-svg-href="([^"]+)">/g, 'xlink:href="$1"/>') + .replace(/ id=([^" >]+)/g, ' id="$1"') // #4003 + .replace(/class=([^" >]+)/g, 'class="$1"') + .replace(/ transform /g, ' ') + .replace(/:(path|rect)/g, '$1') + .replace(/style="([^"]+)"/g, function (s) { + return s.toLowerCase(); + }); + + return svg; + }; + + /** + * VML namespaces can't be added until after complete. Listening + * for Perini's doScroll hack is not enough. + * + * @todo: Move this to the oldie.js module. + * @private + */ + Chart.prototype.isReadyToRender = function () { + var chart = this; + + // Note: win == win.top is required + if ( + !svg && + ( + win == win.top && // eslint-disable-line eqeqeq + doc.readyState !== 'complete' + ) + ) { + doc.attachEvent('onreadystatechange', function () { + doc.detachEvent('onreadystatechange', chart.firstRender); + if (doc.readyState === 'complete') { + chart.firstRender(); + } + }); + return false; + } + return true; + }; + + // IE compatibility hack for generating SVG content that it doesn't really + // understand. Used by the exporting module. + if (!doc.createElementNS) { + doc.createElementNS = function (ns, tagName) { + return doc.createElement(tagName); + }; + } + + /** + * Old IE polyfill for addEventListener, called from inside the addEvent + * function. + */ + H.addEventListenerPolyfill = function (type, fn) { + var el = this; + function wrappedFn(e) { + e.target = e.srcElement || win; // #2820 + fn.call(el, e); + } + + if (el.attachEvent) { + if (!el.hcEventsIE) { + el.hcEventsIE = {}; + } + + // unique function string (#6746) + if (!fn.hcKey) { + fn.hcKey = H.uniqueKey(); + } + + // Link wrapped fn with original fn, so we can get this in + // removeEvent + el.hcEventsIE[fn.hcKey] = wrappedFn; + + el.attachEvent('on' + type, wrappedFn); + } + + }; + H.removeEventListenerPolyfill = function (type, fn) { + if (this.detachEvent) { + fn = this.hcEventsIE[fn.hcKey]; + this.detachEvent('on' + type, fn); + } + }; + + + /** + * The VML element wrapper. + */ + VMLElement = { + + docMode8: doc && doc.documentMode === 8, + + /** + * Initialize a new VML element wrapper. It builds the markup as a + * string to minimize DOM traffic. + * @param {Object} renderer + * @param {Object} nodeName + */ + init: function (renderer, nodeName) { + var wrapper = this, + markup = ['<', nodeName, ' filled="f" stroked="f"'], + style = ['position: ', 'absolute', ';'], + isDiv = nodeName === 'div'; + + // divs and shapes need size + if (nodeName === 'shape' || isDiv) { + style.push('left:0;top:0;width:1px;height:1px;'); + } + style.push('visibility: ', isDiv ? 'hidden' : 'visible'); + + markup.push(' style="', style.join(''), '"/>'); + + // create element with default attributes and style + if (nodeName) { + markup = isDiv || nodeName === 'span' || nodeName === 'img' ? + markup.join('') : + renderer.prepVML(markup); + wrapper.element = createElement(markup); + } + + wrapper.renderer = renderer; + }, + + /** + * Add the node to the given parent + * @param {Object} parent + */ + add: function (parent) { + var wrapper = this, + renderer = wrapper.renderer, + element = wrapper.element, + box = renderer.box, + inverted = parent && parent.inverted, + + // get the parent node + parentNode = parent ? + parent.element || parent : + box; + + if (parent) { + this.parentGroup = parent; + } + + // if the parent group is inverted, apply inversion on all children + if (inverted) { // only on groups + renderer.invertChild(element, parentNode); + } + + // append it + parentNode.appendChild(element); + + // align text after adding to be able to read offset + wrapper.added = true; + if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { + wrapper.updateTransform(); + } + + // fire an event for internal hooks + if (wrapper.onAdd) { + wrapper.onAdd(); + } + + // IE8 Standards can't set the class name before the element is + // appended + if (this.className) { + this.attr('class', this.className); + } + + return wrapper; + }, + + /** + * VML always uses htmlUpdateTransform + */ + updateTransform: SVGElement.prototype.htmlUpdateTransform, + + /** + * Set the rotation of a span with oldIE's filter + */ + setSpanRotation: function () { + // Adjust for alignment and rotation. Rotation of useHTML content is + // not yet implemented but it can probably be implemented for + // Firefox 3.5+ on user request. FF3.5+ has support for CSS3 + // transform. The getBBox method also needs to be updated to + // compensate for the rotation, like it currently does for SVG. + // Test case: http://jsfiddle.net/highcharts/Ybt44/ + + var rotation = this.rotation, + costheta = Math.cos(rotation * deg2rad), + sintheta = Math.sin(rotation * deg2rad); + + css(this.element, { + filter: rotation ? [ + 'progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod=\'auto expand\')' + ].join('') : 'none' + }); + }, + + /** + * Get the positioning correction for the span after rotating. + */ + getSpanCorrection: function ( + width, + baseline, + alignCorrection, + rotation, + align + ) { + + var costheta = rotation ? Math.cos(rotation * deg2rad) : 1, + sintheta = rotation ? Math.sin(rotation * deg2rad) : 0, + height = pick(this.elemHeight, this.element.offsetHeight), + quad, + nonLeft = align && align !== 'left'; + + // correct x and y + this.xCorr = costheta < 0 && -width; + this.yCorr = sintheta < 0 && -height; + + // correct for baseline and corners spilling out after rotation + quad = costheta * sintheta < 0; + this.xCorr += ( + sintheta * + baseline * + (quad ? 1 - alignCorrection : alignCorrection) + ); + this.yCorr -= ( + costheta * + baseline * + (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1) + ); + // correct for the length/height of the text + if (nonLeft) { + this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); + if (rotation) { + this.yCorr -= ( + height * + alignCorrection * + (sintheta < 0 ? -1 : 1) + ); + } + css(this.element, { + textAlign: align + }); + } + }, + + /** + * Converts a subset of an SVG path definition to its VML counterpart. + * Takes an array as the parameter and returns a string. + */ + pathToVML: function (value) { + // convert paths + var i = value.length, + path = []; + + while (i--) { + + // Multiply by 10 to allow subpixel precision. + // Substracting half a pixel seems to make the coordinates + // align with SVG, but this hasn't been tested thoroughly + if (isNumber(value[i])) { + path[i] = Math.round(value[i] * 10) - 5; + } else if (value[i] === 'Z') { // close the path + path[i] = 'x'; + } else { + path[i] = value[i]; + + // When the start X and end X coordinates of an arc are too + // close, they are rounded to the same value above. In this + // case, substract or add 1 from the end X and Y positions. + // #186, #760, #1371, #1410. + if ( + value.isArc && + (value[i] === 'wa' || value[i] === 'at') + ) { + // Start and end X + if (path[i + 5] === path[i + 7]) { + path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1; + } + // Start and end Y + if (path[i + 6] === path[i + 8]) { + path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1; + } + } + } + } + + return path.join(' ') || 'x'; + }, + + /** + * Set the element's clipping to a predefined rectangle + * + * @param {String} id The id of the clip rectangle + */ + clip: function (clipRect) { + var wrapper = this, + clipMembers, + cssRet; + + if (clipRect) { + clipMembers = clipRect.members; + + // Ensure unique list of elements (#1258) + erase(clipMembers, wrapper); + clipMembers.push(wrapper); + wrapper.destroyClip = function () { + erase(clipMembers, wrapper); + }; + cssRet = clipRect.getCSS(wrapper); + + } else { + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + cssRet = { + clip: wrapper.docMode8 ? 'inherit' : 'rect(auto)' + }; // #1214 + } + + return wrapper.css(cssRet); + + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: SVGElement.prototype.htmlCss, + + /** + * Removes a child either by removeChild or move to garbageBin. + * Issue 490; in VML removeChild results in Orphaned nodes according to + * sIEve, discardElement does not. + */ + safeRemoveChild: function (element) { + // discardElement will detach the node from its parent before + // attaching it to the garbage bin. Therefore it is important that + // the node is attached and have parent. + if (element.parentNode) { + discardElement(element); + } + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function () { + if (this.destroyClip) { + this.destroyClip(); + } + + return SVGElement.prototype.destroy.apply(this); + }, + + /** + * Add an event listener. VML override for normalizing event parameters. + * @param {String} eventType + * @param {Function} handler + */ + on: function (eventType, handler) { + // simplest possible event model for internal use + this.element['on' + eventType] = function () { + var evt = win.event; + evt.target = evt.srcElement; + handler(evt); + }; + return this; + }, + + /** + * In stacked columns, cut off the shadows so that they don't overlap + */ + cutOffPath: function (path, length) { + + var len; + + // The extra comma tricks the trailing comma remover in + // "gulp scripts" task + path = path.split(/[ ,,]/); + len = path.length; + + if (len === 9 || len === 11) { + path[len - 4] = path[len - 2] = + pInt(path[len - 2]) - 10 * length; + } + return path.join(' '); + }, + + /** + * Apply a drop shadow by copying elements and giving them different + * strokes + * @param {Boolean|Object} shadowOptions + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path, + strokeWidth, + modifiedPath, + shadowWidth, + shadowElementOpacity; + + // some times empty paths are not strings + if (path && typeof path.value !== 'string') { + path = 'x'; + } + modifiedPath = path; + + if (shadowOptions) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = + (shadowOptions.opacity || 0.15) / shadowWidth; + for (i = 1; i <= 3; i++) { + + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + + // Cut off shadows for stacked column items + if (cutOff) { + modifiedPath = this.cutOffPath( + path.value, + strokeWidth + 0.5 + ); + } + + markup = [ + '' + ]; + + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + + pick(shadowOptions.offsetX, 1), + top: pInt(elemStyle.top) + + pick(shadowOptions.offsetY, 1) + } + ); + if (cutOff) { + shadow.cutOff = strokeWidth + 1; + } + + // apply the opacity + markup = [ + '']; + createElement(renderer.prepVML(markup), null, null, shadow); + + + // insert it + if (group) { + group.element.appendChild(shadow); + } else { + element.parentNode.insertBefore(shadow, element); + } + + // record it + shadows.push(shadow); + + } + + this.shadows = shadows; + } + return this; + }, + updateShadows: noop, // Used in SVG only + + setAttr: function (key, value) { + if (this.docMode8) { // IE8 setAttribute bug + this.element[key] = value; + } else { + this.element.setAttribute(key, value); + } + }, + getAttr: function (key) { + if (this.docMode8) { // IE8 setAttribute bug + return this.element[key]; + } + return this.element.getAttribute(key); + }, + classSetter: function (value) { + // IE8 Standards mode has problems retrieving the className unless + // set like this. IE8 Standards can't set the class name before the + // element is appended. + (this.added ? this.element : this).className = value; + }, + dashstyleSetter: function (value, key, element) { + var strokeElem = element.getElementsByTagName('stroke')[0] || + createElement( + this.renderer.prepVML(['']), + null, + null, + element + ); + strokeElem[key] = value || 'solid'; + // Because changing stroke-width will change the dash length and + // cause an epileptic effect + this[key] = value; + }, + dSetter: function (value, key, element) { + var i, + shadows = this.shadows; + value = value || []; + // Used in getter for animation + this.d = value.join && value.join(' '); + + element.path = value = this.pathToVML(value); + + // update shadows + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].path = shadows[i].cutOff ? + this.cutOffPath(value, shadows[i].cutOff) : + value; + } + } + this.setAttr(key, value); + }, + fillSetter: function (value, key, element) { + var nodeName = element.nodeName; + if (nodeName === 'SPAN') { // text color + element.style.color = value; + } else if (nodeName !== 'IMG') { // #1336 + element.filled = value !== 'none'; + this.setAttr( + 'fillcolor', + this.renderer.color(value, element, key, this) + ); + } + }, + 'fill-opacitySetter': function (value, key, element) { + createElement( + this.renderer.prepVML( + ['<', key.split('-')[0], ' opacity="', value, '"/>'] + ), + null, + null, + element + ); + }, + // Don't bother - animation is too slow and filters introduce artifacts + opacitySetter: noop, + rotationSetter: function (value, key, element) { + var style = element.style; + this[key] = style[key] = value; // style is for #1873 + + // Correction for the 1x1 size of the shape container. Used in gauge + // needles. + style.left = -Math.round(Math.sin(value * deg2rad) + 1) + 'px'; + style.top = Math.round(Math.cos(value * deg2rad)) + 'px'; + }, + strokeSetter: function (value, key, element) { + this.setAttr( + 'strokecolor', + this.renderer.color(value, element, key, this) + ); + }, + 'stroke-widthSetter': function (value, key, element) { + element.stroked = !!value; // VML "stroked" attribute + this[key] = value; // used in getter, issue #113 + if (isNumber(value)) { + value += 'px'; + } + this.setAttr('strokeweight', value); + }, + titleSetter: function (value, key) { + this.setAttr(key, value); + }, + visibilitySetter: function (value, key, element) { + + // Handle inherited visibility + if (value === 'inherit') { + value = 'visible'; + } + + // Let the shadow follow the main element + if (this.shadows) { + each(this.shadows, function (shadow) { + shadow.style[key] = value; + }); + } + + // Instead of toggling the visibility CSS property, move the div out + // of the viewport. This works around #61 and #586 + if (element.nodeName === 'DIV') { + value = value === 'hidden' ? '-999em' : 0; + + // In order to redraw, IE7 needs the div to be visible when + // tucked away outside the viewport. So the visibility is + // actually opposite of the expected value. This applies to the + // tooltip only. + if (!this.docMode8) { + element.style[key] = value ? 'visible' : 'hidden'; + } + key = 'top'; + } + element.style[key] = value; + }, + xSetter: function (value, key, element) { + this[key] = value; // used in getter + + if (key === 'x') { + key = 'left'; + } else if (key === 'y') { + key = 'top'; + } + + // clipping rectangle special + if (this.updateClipping) { + // the key is now 'left' or 'top' for 'x' and 'y' + this[key] = value; + this.updateClipping(); + } else { + // normal + element.style[key] = value; + } + }, + zIndexSetter: function (value, key, element) { + element.style[key] = value; + }, + fillGetter: function () { + return this.getAttr('fillcolor') || ''; + }, + strokeGetter: function () { + return this.getAttr('strokecolor') || ''; + }, + // #7850 + classGetter: function () { + return this.getAttr('className') || ''; + } + }; + VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter']; + H.VMLElement = VMLElement = extendClass(SVGElement, VMLElement); + + // Some shared setters + VMLElement.prototype.ySetter = + VMLElement.prototype.widthSetter = + VMLElement.prototype.heightSetter = + VMLElement.prototype.xSetter; + + + /** + * The VML renderer + */ + VMLRendererExtension = { // inherit SVGRenderer + + Element: VMLElement, + isIE8: win.navigator.userAgent.indexOf('MSIE 8.0') > -1, + + + /** + * Initialize the VMLRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + */ + init: function (container, width, height) { + var renderer = this, + boxWrapper, + box, + css; + + renderer.alignedObjects = []; + + boxWrapper = renderer.createElement('div') + .css({ position: 'relative' }); + box = boxWrapper.element; + container.appendChild(boxWrapper.element); + + + // generate the containing box + renderer.isVML = true; + renderer.box = box; + renderer.boxWrapper = boxWrapper; + renderer.gradients = {}; + renderer.cache = {}; // Cache for numerical bounding boxes + renderer.cacheKeys = []; + renderer.imgCount = 0; + + + renderer.setSize(width, height, false); + + // The only way to make IE6 and IE7 print is to use a global + // namespace. However, with IE8 the only way to make the dynamic + // shapes visible in screen and print mode seems to be to add the + // xmlns attribute and the behaviour style inline. + if (!doc.namespaces.hcv) { + + doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); + + // Setup default CSS (#2153, #2368, #2384) + css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + + '{ behavior:url(#default#VML); display: inline-block; } '; + try { + doc.createStyleSheet().cssText = css; + } catch (e) { + doc.styleSheets[0].cssText += css; + } + + } + }, + + + /** + * Detect whether the renderer is hidden. This happens when one of the + * parent elements has display: none + */ + isHidden: function () { + return !this.box.offsetWidth; + }, + + /** + * Define a clipping rectangle. In VML it is accomplished by storing the + * values for setting the CSS style to all associated members. + * + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + + // create a dummy element + var clipRect = this.createElement(), + isObj = isObject(x); + + // mimic a rectangle with its style object for automatic updating in + // attr + return extend(clipRect, { + members: [], + count: 0, + left: (isObj ? x.x : x) + 1, + top: (isObj ? x.y : y) + 1, + width: (isObj ? x.width : width) - 1, + height: (isObj ? x.height : height) - 1, + getCSS: function (wrapper) { + var element = wrapper.element, + nodeName = element.nodeName, + isShape = nodeName === 'shape', + inverted = wrapper.inverted, + rect = this, + top = rect.top - (isShape ? element.offsetTop : 0), + left = rect.left, + right = left + rect.width, + bottom = top + rect.height, + ret = { + clip: 'rect(' + + Math.round(inverted ? left : top) + 'px,' + + Math.round(inverted ? bottom : right) + 'px,' + + Math.round(inverted ? right : bottom) + 'px,' + + Math.round(inverted ? top : left) + 'px)' + }; + + // issue 74 workaround + if (!inverted && wrapper.docMode8 && nodeName === 'DIV') { + extend(ret, { + width: right + 'px', + height: bottom + 'px' + }); + } + return ret; + }, + + // used in attr and animation to update the clipping of all + // members + updateClipping: function () { + each(clipRect.members, function (member) { + // Member.element is falsy on deleted series, like in + // stock/members/series-remove demo. Should be removed + // from members, but this will do. + if (member.element) { + member.css(clipRect.getCSS(member)); + } + }); + } + }); + + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if + * it's a gradient configuration object, and apply opacity. + * + * @param {Object} color The color or config object + */ + color: function (color, elem, prop, wrapper) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + markup, + fillType, + ret = 'none'; + + // Check for linear or radial gradient + if (color && color.linearGradient) { + fillType = 'gradient'; + } else if (color && color.radialGradient) { + fillType = 'pattern'; + } + + + if (fillType) { + + var stopColor, + stopOpacity, + gradient = color.linearGradient || color.radialGradient, + x1, + y1, + x2, + y2, + opacity1, + opacity2, + color1, + color2, + fillAttr = '', + stops = color.stops, + firstStop, + lastStop, + colors = [], + addFillNode = function () { + // Add the fill subnode. When colors attribute is used, + // the meanings of opacity and o:opacity2 are reversed. + markup = ['']; + createElement( + renderer.prepVML(markup), + null, + null, + elem + ); + }; + + // Extend from 0 to 1 + firstStop = stops[0]; + lastStop = stops[stops.length - 1]; + if (firstStop[0] > 0) { + stops.unshift([ + 0, + firstStop[1] + ]); + } + if (lastStop[0] < 1) { + stops.push([ + 1, + lastStop[1] + ]); + } + + // Compute the stops + each(stops, function (stop, i) { + if (regexRgba.test(stop[1])) { + colorObject = H.color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + + // Build the color attribute + colors.push((stop[0] * 100) + '% ' + stopColor); + + // Only start and end opacities are allowed, so we use the + // first and the last + if (!i) { + opacity1 = stopOpacity; + color2 = stopColor; + } else { + opacity2 = stopOpacity; + color1 = stopColor; + } + }); + + // Apply the gradient to fills only. + if (prop === 'fill') { + + // Handle linear gradient angle + if (fillType === 'gradient') { + x1 = gradient.x1 || gradient[0] || 0; + y1 = gradient.y1 || gradient[1] || 0; + x2 = gradient.x2 || gradient[2] || 0; + y2 = gradient.y2 || gradient[3] || 0; + fillAttr = 'angle="' + (90 - Math.atan( + (y2 - y1) / // y vector + (x2 - x1) // x vector + ) * 180 / Math.PI) + '"'; + + addFillNode(); + + // Radial (circular) gradient + } else { + + var r = gradient.r, + sizex = r * 2, + sizey = r * 2, + cx = gradient.cx, + cy = gradient.cy, + radialReference = elem.radialReference, + bBox, + applyRadialGradient = function () { + if (radialReference) { + bBox = wrapper.getBBox(); + cx += (radialReference[0] - bBox.x) / + bBox.width - 0.5; + cy += (radialReference[1] - bBox.y) / + bBox.height - 0.5; + sizex *= radialReference[2] / bBox.width; + sizey *= radialReference[2] / bBox.height; + } + fillAttr = 'src="' + + H.getOptions().global.VMLRadialGradientURL + + '" ' + + 'size="' + sizex + ',' + sizey + '" ' + + 'origin="0.5,0.5" ' + + 'position="' + cx + ',' + cy + '" ' + + 'color2="' + color2 + '" '; + + addFillNode(); + }; + + // Apply radial gradient + if (wrapper.added) { + applyRadialGradient(); + } else { + // We need to know the bounding box to get the size + // and position right + wrapper.onAdd = applyRadialGradient; + } + + // The fill element's color attribute is broken in IE8 + // standards mode, so we need to set the parent shape's + // fillcolor attribute instead. + ret = color1; + } + + // Gradients are not supported for VML stroke, return the first + // color. #722. + } else { + ret = stopColor; + } + + // If the color is an rgba color, split it and add a fill node + // to hold the opacity component + } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { + + colorObject = H.color(color); + + wrapper[prop + '-opacitySetter']( + colorObject.get('a'), + prop, + elem + ); + + ret = colorObject.get('rgb'); + + + } else { + // 'stroke' or 'fill' node + var propNodes = elem.getElementsByTagName(prop); + if (propNodes.length) { + propNodes[0].opacity = 1; + propNodes[0].type = 'solid'; + } + ret = color; + } + + return ret; + }, + + /** + * Take a VML string and prepare it for either IE8 or IE6/IE7. + * @param {Array} markup A string array of the VML markup to prepare + */ + prepVML: function (markup) { + var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', + isIE8 = this.isIE8; + + markup = markup.join(''); + + if (isIE8) { // add xmlns and style inline + markup = markup.replace( + '/>', + ' xmlns="urn:schemas-microsoft-com:vml" />' + ); + if (markup.indexOf('style="') === -1) { + markup = markup.replace( + '/>', + ' style="' + vmlStyle + '" />' + ); + } else { + markup = markup.replace('style="', 'style="' + vmlStyle); + } + + } else { // add namespace + markup = markup.replace('<', ' 1) { + obj.attr({ + x: x, + y: y, + width: width, + height: height + }); + } + return obj; + }, + + /** + * For rectangles, VML uses a shape for rect to overcome bugs and + * rotation problems + */ + createElement: function (nodeName) { + return nodeName === 'rect' ? + this.symbol(nodeName) : + SVGRenderer.prototype.createElement.call(this, nodeName); + }, + + /** + * In the VML renderer, each child of an inverted div (group) is + * inverted + * @param {Object} element + * @param {Object} parentNode + */ + invertChild: function (element, parentNode) { + var ren = this, + parentStyle = parentNode.style, + imgStyle = element.tagName === 'IMG' && element.style; // #1111 + + css(element, { + flip: 'x', + left: pInt(parentStyle.width) - + (imgStyle ? pInt(imgStyle.top) : 1), + top: pInt(parentStyle.height) - + (imgStyle ? pInt(imgStyle.left) : 1), + rotation: -90 + }); + + // Recursively invert child elements, needed for nested composite + // shapes like box plots and error bars. #1680, #1806. + each(element.childNodes, function (child) { + ren.invertChild(child, element); + }); + }, + + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, w, h, options) { + var start = options.start, + end = options.end, + radius = options.r || w || h, + innerRadius = options.innerR, + cosStart = Math.cos(start), + sinStart = Math.sin(start), + cosEnd = Math.cos(end), + sinEnd = Math.sin(end), + ret; + + if (end - start === 0) { // no angle, don't show it. + return ['x']; + } + + ret = [ + 'wa', // clockwise arc to + x - radius, // left + y - radius, // top + x + radius, // right + y + radius, // bottom + x + radius * cosStart, // start x + y + radius * sinStart, // start y + x + radius * cosEnd, // end x + y + radius * sinEnd // end y + ]; + + if (options.open && !innerRadius) { + ret.push( + 'e', + 'M', + x, // - innerRadius, + y // - innerRadius + ); + } + + ret.push( + 'at', // anti clockwise arc to + x - innerRadius, // left + y - innerRadius, // top + x + innerRadius, // right + y + innerRadius, // bottom + x + innerRadius * cosEnd, // start x + y + innerRadius * sinEnd, // start y + x + innerRadius * cosStart, // end x + y + innerRadius * sinStart, // end y + 'x', // finish path + 'e' // close + ); + + ret.isArc = true; + return ret; + + }, + // Add circle symbol path. This performs significantly faster than + // v:oval. + circle: function (x, y, w, h, wrapper) { + + if (wrapper && defined(wrapper.r)) { + w = h = 2 * wrapper.r; + } + + // Center correction, #1682 + if (wrapper && wrapper.isCircle) { + x -= w / 2; + y -= h / 2; + } + + // Return the path + return [ + 'wa', // clockwisearcto + x, // left + y, // top + x + w, // right + y + h, // bottom + x + w, // start x + y + h / 2, // start y + x + w, // end x + y + h / 2, // end y + 'e' // close + ]; + }, + /** + * Add rectangle symbol path which eases rotation and omits arcsize + * problems compared to the built-in VML roundrect shape. When + * borders are not rounded, use the simpler square path, else use + * the callout path without the arrow. + */ + rect: function (x, y, w, h, options) { + return SVGRenderer.prototype.symbols[ + !defined(options) || !options.r ? 'square' : 'callout' + ].call(0, x, y, w, h, options); + } + } + }; + H.VMLRenderer = VMLRenderer = function () { + this.init.apply(this, arguments); + }; + VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); + + // general renderer + H.Renderer = VMLRenderer; } SVGRenderer.prototype.getSpanWidth = function (wrapper, tspan) { - var renderer = this, - bBox = wrapper.getBBox(true), - actualWidth = bBox.width; - - // Old IE cannot measure the actualWidth for SVG elements (#2314) - if (!svg && renderer.forExport) { - actualWidth = renderer.measureSpanWidth( - tspan.firstChild.data, - wrapper.styles - ); - } - return actualWidth; + var renderer = this, + bBox = wrapper.getBBox(true), + actualWidth = bBox.width; + + // Old IE cannot measure the actualWidth for SVG elements (#2314) + if (!svg && renderer.forExport) { + actualWidth = renderer.measureSpanWidth( + tspan.firstChild.data, + wrapper.styles + ); + } + return actualWidth; }; // This method is used with exporting in old IE, when emulating SVG (see #2314) SVGRenderer.prototype.measureSpanWidth = function (text, styles) { - var measuringSpan = doc.createElement('span'), - offsetWidth, - textNode = doc.createTextNode(text); - - measuringSpan.appendChild(textNode); - css(measuringSpan, styles); - this.box.appendChild(measuringSpan); - offsetWidth = measuringSpan.offsetWidth; - discardElement(measuringSpan); // #2463 - return offsetWidth; + var measuringSpan = doc.createElement('span'), + offsetWidth, + textNode = doc.createTextNode(text); + + measuringSpan.appendChild(textNode); + css(measuringSpan, styles); + this.box.appendChild(measuringSpan); + offsetWidth = measuringSpan.offsetWidth; + discardElement(measuringSpan); // #2463 + return offsetWidth; }; diff --git a/js/modules/overlapping-datalabels.src.js b/js/modules/overlapping-datalabels.src.js index 2cffa2f26ad..8c2dd0e0245 100644 --- a/js/modules/overlapping-datalabels.src.js +++ b/js/modules/overlapping-datalabels.src.js @@ -12,180 +12,180 @@ import '../parts/Chart.js'; * Highcharts. */ var Chart = H.Chart, - each = H.each, - objectEach = H.objectEach, - pick = H.pick, - addEvent = H.addEvent; + each = H.each, + objectEach = H.objectEach, + pick = H.pick, + addEvent = H.addEvent; // Collect potensial overlapping data labels. Stack labels probably don't need // to be considered because they are usually accompanied by data labels that lie // inside the columns. addEvent(Chart, 'render', function collectAndHide() { - var labels = []; - - // Consider external label collectors - each(this.labelCollectors || [], function (collector) { - labels = labels.concat(collector()); - }); - - each(this.yAxis || [], function (yAxis) { - if ( - yAxis.options.stackLabels && - !yAxis.options.stackLabels.allowOverlap - ) { - objectEach(yAxis.stacks, function (stack) { - objectEach(stack, function (stackItem) { - labels.push(stackItem.label); - }); - }); - } - }); - - each(this.series || [], function (series) { - var dlOptions = series.options.dataLabels, - // Range series have two collections - collections = series.dataLabelCollections || ['dataLabel']; - - if ( - (dlOptions.enabled || series._hasPointLabels) && - !dlOptions.allowOverlap && - series.visible - ) { // #3866 - each(collections, function (coll) { - each(series.points, function (point) { - if (point[coll]) { - point[coll].labelrank = pick( - point.labelrank, - point.shapeArgs && point.shapeArgs.height - ); // #4118 - labels.push(point[coll]); - } - }); - }); - } - }); - this.hideOverlappingLabels(labels); + var labels = []; + + // Consider external label collectors + each(this.labelCollectors || [], function (collector) { + labels = labels.concat(collector()); + }); + + each(this.yAxis || [], function (yAxis) { + if ( + yAxis.options.stackLabels && + !yAxis.options.stackLabels.allowOverlap + ) { + objectEach(yAxis.stacks, function (stack) { + objectEach(stack, function (stackItem) { + labels.push(stackItem.label); + }); + }); + } + }); + + each(this.series || [], function (series) { + var dlOptions = series.options.dataLabels, + // Range series have two collections + collections = series.dataLabelCollections || ['dataLabel']; + + if ( + (dlOptions.enabled || series._hasPointLabels) && + !dlOptions.allowOverlap && + series.visible + ) { // #3866 + each(collections, function (coll) { + each(series.points, function (point) { + if (point[coll]) { + point[coll].labelrank = pick( + point.labelrank, + point.shapeArgs && point.shapeArgs.height + ); // #4118 + labels.push(point[coll]); + } + }); + }); + } + }); + this.hideOverlappingLabels(labels); }); /** * Hide overlapping labels. Labels are moved and faded in and out on zoom to * provide a smooth visual imression. - */ + */ Chart.prototype.hideOverlappingLabels = function (labels) { - var len = labels.length, - label, - i, - j, - label1, - label2, - isIntersecting, - pos1, - pos2, - parent1, - parent2, - padding, - bBox, - intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { - return !( - x2 > x1 + w1 || - x2 + w2 < x1 || - y2 > y1 + h1 || - y2 + h2 < y1 - ); - }; - - for (i = 0; i < len; i++) { - label = labels[i]; - if (label) { - - // Mark with initial opacity - label.oldOpacity = label.opacity; - label.newOpacity = 1; - - // Get width and height if pure text nodes (stack labels) - if (!label.width) { - bBox = label.getBBox(); - label.width = bBox.width; - label.height = bBox.height; - } - } - } - - // Prevent a situation in a gradually rising slope, that each label will - // hide the previous one because the previous one always has lower rank. - labels.sort(function (a, b) { - return (b.labelrank || 0) - (a.labelrank || 0); - }); - - // Detect overlapping labels - for (i = 0; i < len; i++) { - label1 = labels[i]; - - for (j = i + 1; j < len; ++j) { - label2 = labels[j]; - if ( - label1 && label2 && - label1 !== label2 && // #6465, polar chart with connectEnds - label1.placed && label2.placed && - label1.newOpacity !== 0 && label2.newOpacity !== 0 - ) { - pos1 = label1.alignAttr; - pos2 = label2.alignAttr; - // Different panes have different positions - parent1 = label1.parentGroup; - parent2 = label2.parentGroup; - // Substract the padding if no background or border (#4333) - padding = 2 * (label1.box ? 0 : (label1.padding || 0)); - isIntersecting = intersectRect( - pos1.x + parent1.translateX, - pos1.y + parent1.translateY, - label1.width - padding, - label1.height - padding, - pos2.x + parent2.translateX, - pos2.y + parent2.translateY, - label2.width - padding, - label2.height - padding - ); - - if (isIntersecting) { - (label1.labelrank < label2.labelrank ? label1 : label2) - .newOpacity = 0; - } - } - } - } - - // Hide or show - each(labels, function (label) { - var complete, - newOpacity; - - if (label) { - newOpacity = label.newOpacity; - - if (label.oldOpacity !== newOpacity && label.placed) { - - // Make sure the label is completely hidden to avoid catching - // clicks (#4362) - if (newOpacity) { - label.show(true); - } else { - complete = function () { - label.hide(); - }; - } - - // Animate or set the opacity - label.alignAttr.opacity = newOpacity; - label[label.isOld ? 'animate' : 'attr']( - label.alignAttr, - null, - complete - ); - - } - label.isOld = true; - } - }); + var len = labels.length, + label, + i, + j, + label1, + label2, + isIntersecting, + pos1, + pos2, + parent1, + parent2, + padding, + bBox, + intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { + return !( + x2 > x1 + w1 || + x2 + w2 < x1 || + y2 > y1 + h1 || + y2 + h2 < y1 + ); + }; + + for (i = 0; i < len; i++) { + label = labels[i]; + if (label) { + + // Mark with initial opacity + label.oldOpacity = label.opacity; + label.newOpacity = 1; + + // Get width and height if pure text nodes (stack labels) + if (!label.width) { + bBox = label.getBBox(); + label.width = bBox.width; + label.height = bBox.height; + } + } + } + + // Prevent a situation in a gradually rising slope, that each label will + // hide the previous one because the previous one always has lower rank. + labels.sort(function (a, b) { + return (b.labelrank || 0) - (a.labelrank || 0); + }); + + // Detect overlapping labels + for (i = 0; i < len; i++) { + label1 = labels[i]; + + for (j = i + 1; j < len; ++j) { + label2 = labels[j]; + if ( + label1 && label2 && + label1 !== label2 && // #6465, polar chart with connectEnds + label1.placed && label2.placed && + label1.newOpacity !== 0 && label2.newOpacity !== 0 + ) { + pos1 = label1.alignAttr; + pos2 = label2.alignAttr; + // Different panes have different positions + parent1 = label1.parentGroup; + parent2 = label2.parentGroup; + // Substract the padding if no background or border (#4333) + padding = 2 * (label1.box ? 0 : (label1.padding || 0)); + isIntersecting = intersectRect( + pos1.x + parent1.translateX, + pos1.y + parent1.translateY, + label1.width - padding, + label1.height - padding, + pos2.x + parent2.translateX, + pos2.y + parent2.translateY, + label2.width - padding, + label2.height - padding + ); + + if (isIntersecting) { + (label1.labelrank < label2.labelrank ? label1 : label2) + .newOpacity = 0; + } + } + } + } + + // Hide or show + each(labels, function (label) { + var complete, + newOpacity; + + if (label) { + newOpacity = label.newOpacity; + + if (label.oldOpacity !== newOpacity && label.placed) { + + // Make sure the label is completely hidden to avoid catching + // clicks (#4362) + if (newOpacity) { + label.show(true); + } else { + complete = function () { + label.hide(); + }; + } + + // Animate or set the opacity + label.alignAttr.opacity = newOpacity; + label[label.isOld ? 'animate' : 'attr']( + label.alignAttr, + null, + complete + ); + + } + label.isOld = true; + } + }); }; diff --git a/js/modules/parallel-coordinates.src.js b/js/modules/parallel-coordinates.src.js index 2f9d8a591de..b416c722ce0 100644 --- a/js/modules/parallel-coordinates.src.js +++ b/js/modules/parallel-coordinates.src.js @@ -5,7 +5,7 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from '../parts/Globals.js'; import '../parts/Axis.js'; @@ -16,220 +16,220 @@ import '../parts/Series.js'; * Extensions for parallel coordinates plot. */ var Axis = H.Axis, - Chart = H.Chart, - SeriesProto = H.Series.prototype, - ChartProto = Chart.prototype, - AxisProto = H.Axis.prototype; + Chart = H.Chart, + SeriesProto = H.Series.prototype, + ChartProto = Chart.prototype, + AxisProto = H.Axis.prototype; var addEvent = H.addEvent, - pick = H.pick, - each = H.each, - wrap = H.wrap, - merge = H.merge, - erase = H.erase, - splat = H.splat, - extend = H.extend, - defined = H.defined, - arrayMin = H.arrayMin, - arrayMax = H.arrayMax; + pick = H.pick, + each = H.each, + wrap = H.wrap, + merge = H.merge, + erase = H.erase, + splat = H.splat, + extend = H.extend, + defined = H.defined, + arrayMin = H.arrayMin, + arrayMax = H.arrayMax; var defaultXAxisOptions = { - /*= if (build.classic) { =*/ - lineWidth: 0, - tickLength: 0, - /*= } =*/ - opposite: true, - type: 'category' + /*= if (build.classic) { =*/ + lineWidth: 0, + tickLength: 0, + /*= } =*/ + opposite: true, + type: 'category' }; /** * @optionparent chart */ var defaultParallelOptions = { - /** - * Flag to render charts as a parallel coordinates plot. In a parallel - * coordinates plot (||-coords) by default all required yAxes are generated - * and the legend is disabled. This feature requires - * `modules/parallel-coordinates.js`. - * - * @sample {highcharts} /highcharts/demo/parallel-coordinates/ - * Parallel coordinates demo - * @since 6.0.0 - * @product highcharts - */ - parallelCoordinates: false, - /** - * Common options for all yAxes rendered in a parallel coordinates plot. - * This feature requires `modules/parallel-coordinates.js`. - * - * The default options are: - *
-	 * parallelAxes: {
-	 *	lineWidth: 1,       // classic mode only
-	 *	gridlinesWidth: 0,  // classic mode only
-	 *	title: {
-	 *		text: '',
-	 *		reserveSpace: false
-	 *	},
-	 *	labels: {
-	 *		x: 0,
-	 *		y: 0,
-	 *		align: 'center',
-	 *		reserveSpace: false
-	 *	},
-	 *	offset: 0
-	 * }
- * - * @extends {yAxis} - * @excluding alternateGridColor,breaks,id,gridLineColor,gridLineDashStyle, - * gridLineWidth,minorGridLineColor,minorGridLineDashStyle, - * minorGridLineWidth,plotBands,plotLines,angle, - * gridLineInterpolation,maxColor,maxZoom,minColor,scrollbar, - * stackLabels,stops - * - * @product highcharts - * @sample {highcharts} highcharts/parallel-coordinates/parallelaxes/ - * Set the same tickAmount for all yAxes - * @since 6.0.0 - */ - parallelAxes: { - /*= if (build.classic) { =*/ - lineWidth: 1, - /*= } =*/ - /** - * Titles for yAxes are taken from - * [xAxis.categories](#xAxis.categories). All options for - * `xAxis.labels` applies to parallel coordinates titles. - * For example, to style categories, use - * [xAxis.labels.style](#xAxis.labels.style). - * - * @excluding align,enabled,margin,offset,position3d,reserveSpace, - * rotation,skew3d,style,text,useHTML,x,y - */ - title: { - text: '', - reserveSpace: false - }, - labels: { - x: 0, - y: 4, - align: 'center', - reserveSpace: false - }, - offset: 0 - } + /** + * Flag to render charts as a parallel coordinates plot. In a parallel + * coordinates plot (||-coords) by default all required yAxes are generated + * and the legend is disabled. This feature requires + * `modules/parallel-coordinates.js`. + * + * @sample {highcharts} /highcharts/demo/parallel-coordinates/ + * Parallel coordinates demo + * @since 6.0.0 + * @product highcharts + */ + parallelCoordinates: false, + /** + * Common options for all yAxes rendered in a parallel coordinates plot. + * This feature requires `modules/parallel-coordinates.js`. + * + * The default options are: + *
+     * parallelAxes: {
+     *    lineWidth: 1,       // classic mode only
+     *    gridlinesWidth: 0,  // classic mode only
+     *    title: {
+     *        text: '',
+     *        reserveSpace: false
+     *    },
+     *    labels: {
+     *        x: 0,
+     *        y: 0,
+     *        align: 'center',
+     *        reserveSpace: false
+     *    },
+     *    offset: 0
+     * }
+ * + * @extends {yAxis} + * @excluding alternateGridColor,breaks,id,gridLineColor,gridLineDashStyle, + * gridLineWidth,minorGridLineColor,minorGridLineDashStyle, + * minorGridLineWidth,plotBands,plotLines,angle, + * gridLineInterpolation,maxColor,maxZoom,minColor,scrollbar, + * stackLabels,stops + * + * @product highcharts + * @sample {highcharts} highcharts/parallel-coordinates/parallelaxes/ + * Set the same tickAmount for all yAxes + * @since 6.0.0 + */ + parallelAxes: { + /*= if (build.classic) { =*/ + lineWidth: 1, + /*= } =*/ + /** + * Titles for yAxes are taken from + * [xAxis.categories](#xAxis.categories). All options for + * `xAxis.labels` applies to parallel coordinates titles. + * For example, to style categories, use + * [xAxis.labels.style](#xAxis.labels.style). + * + * @excluding align,enabled,margin,offset,position3d,reserveSpace, + * rotation,skew3d,style,text,useHTML,x,y + */ + title: { + text: '', + reserveSpace: false + }, + labels: { + x: 0, + y: 4, + align: 'center', + reserveSpace: false + }, + offset: 0 + } }; H.setOptions({ - chart: defaultParallelOptions + chart: defaultParallelOptions }); /** * Initialize parallelCoordinates */ addEvent(Chart, 'init', function (e) { - var options = e.args[0], - defaultyAxis = splat(options.yAxis || {}), - yAxisLength = defaultyAxis.length, - newYAxes = []; - /** - * Flag used in parallel coordinates plot to check if chart has ||-coords. - * - * @name hasParallelCoordinates - * @memberOf Chart - * @type {Boolean} - */ - this.hasParallelCoordinates = options.chart && - options.chart.parallelCoordinates; - - if (this.hasParallelCoordinates) { - - this.setParallelInfo(options); - - // Push empty yAxes in case user did not define them: - for (; yAxisLength <= this.parallelInfo.counter; yAxisLength++) { - newYAxes.push({}); - } - - if (!options.legend) { - options.legend = {}; - } - options.legend.enabled = false; - merge( - true, - options, - // Disable boost - { - boost: { - seriesThreshold: Number.MAX_SAFE_INTEGER - }, - plotOptions: { - series: { - boostThreshold: Number.MAX_SAFE_INTEGER - } - } - } - ); - - options.yAxis = defaultyAxis.concat(newYAxes); - options.xAxis = merge( - defaultXAxisOptions, // docs - splat(options.xAxis || {})[0] - ); - } + var options = e.args[0], + defaultyAxis = splat(options.yAxis || {}), + yAxisLength = defaultyAxis.length, + newYAxes = []; + /** + * Flag used in parallel coordinates plot to check if chart has ||-coords. + * + * @name hasParallelCoordinates + * @memberOf Chart + * @type {Boolean} + */ + this.hasParallelCoordinates = options.chart && + options.chart.parallelCoordinates; + + if (this.hasParallelCoordinates) { + + this.setParallelInfo(options); + + // Push empty yAxes in case user did not define them: + for (; yAxisLength <= this.parallelInfo.counter; yAxisLength++) { + newYAxes.push({}); + } + + if (!options.legend) { + options.legend = {}; + } + options.legend.enabled = false; + merge( + true, + options, + // Disable boost + { + boost: { + seriesThreshold: Number.MAX_SAFE_INTEGER + }, + plotOptions: { + series: { + boostThreshold: Number.MAX_SAFE_INTEGER + } + } + } + ); + + options.yAxis = defaultyAxis.concat(newYAxes); + options.xAxis = merge( + defaultXAxisOptions, // docs + splat(options.xAxis || {})[0] + ); + } }); /** * Initialize parallelCoordinates */ addEvent(Chart, 'update', function (e) { - var options = e.options; - if (options.chart) { - if (defined(options.chart.parallelCoordinates)) { - this.hasParallelCoordinates = options.chart.parallelCoordinates; - } - - if (this.hasParallelCoordinates && options.chart.parallelAxes) { - this.options.chart.parallelAxes = merge( - this.options.chart.parallelAxes, - options.chart.parallelAxes - ); - each(this.yAxis, function (axis) { - axis.update({}, false); - }); - } - } + var options = e.options; + if (options.chart) { + if (defined(options.chart.parallelCoordinates)) { + this.hasParallelCoordinates = options.chart.parallelCoordinates; + } + + if (this.hasParallelCoordinates && options.chart.parallelAxes) { + this.options.chart.parallelAxes = merge( + this.options.chart.parallelAxes, + options.chart.parallelAxes + ); + each(this.yAxis, function (axis) { + axis.update({}, false); + }); + } + } }); extend(ChartProto, /** @lends Highcharts.Chart.prototype */ { - /** - * Define how many parellel axes we have according to the longest dataset - * This is quite heavy - loop over all series and check series.data.length - * Consider: - * - make this an option, so user needs to set this to get better - * performance - * - check only first series for number of points and assume the rest is the - * same - * - * @param {Object} options User options - */ - setParallelInfo: function (options) { - var chart = this, - seriesOptions = options.series; - - chart.parallelInfo = { - counter: 0 - }; - - each(seriesOptions, function (series) { - if (series.data) { - chart.parallelInfo.counter = Math.max( - chart.parallelInfo.counter, - series.data.length - 1 - ); - } - }); - } + /** + * Define how many parellel axes we have according to the longest dataset + * This is quite heavy - loop over all series and check series.data.length + * Consider: + * - make this an option, so user needs to set this to get better + * performance + * - check only first series for number of points and assume the rest is the + * same + * + * @param {Object} options User options + */ + setParallelInfo: function (options) { + var chart = this, + seriesOptions = options.series; + + chart.parallelInfo = { + counter: 0 + }; + + each(seriesOptions, function (series) { + if (series.data) { + chart.parallelInfo.counter = Math.max( + chart.parallelInfo.counter, + series.data.length - 1 + ); + } + }); + } }); @@ -242,34 +242,34 @@ AxisProto.keepProps.push('parallelPosition'); * Update default options with predefined for a parallel coords. */ addEvent(Axis, 'afterSetOptions', function (e) { - var axis = this, - chart = axis.chart, - axisPosition = ['left', 'width', 'height', 'top']; - - if (chart.hasParallelCoordinates) { - if (chart.inverted) { - axisPosition = axisPosition.reverse(); - } - - if (axis.isXAxis) { - axis.options = merge( - axis.options, - defaultXAxisOptions, - e.userOptions - ); - } else { - axis.options = merge( - axis.options, - axis.chart.options.chart.parallelAxes, - e.userOptions - ); - axis.parallelPosition = pick( - axis.parallelPosition, - chart.yAxis.length - ); - axis.setParallelPosition(axisPosition, axis.options); - } - } + var axis = this, + chart = axis.chart, + axisPosition = ['left', 'width', 'height', 'top']; + + if (chart.hasParallelCoordinates) { + if (chart.inverted) { + axisPosition = axisPosition.reverse(); + } + + if (axis.isXAxis) { + axis.options = merge( + axis.options, + defaultXAxisOptions, + e.userOptions + ); + } else { + axis.options = merge( + axis.options, + axis.chart.options.chart.parallelAxes, + e.userOptions + ); + axis.parallelPosition = pick( + axis.parallelPosition, + chart.yAxis.length + ); + axis.setParallelPosition(axisPosition, axis.options); + } + } }); @@ -281,42 +281,42 @@ addEvent(Axis, 'afterSetOptions', function (e) { * - using series.points instead of series.yData */ addEvent(Axis, 'getSeriesExtremes', function (e) { - if (this.chart && this.chart.hasParallelCoordinates && !this.isXAxis) { - var index = this.parallelPosition, - currentPoints = []; - each(this.series, function (series) { - if (defined(series.yData[index])) { - // We need to use push() beacause of null points - currentPoints.push(series.yData[index]); - } - }); - this.dataMin = arrayMin(currentPoints); - this.dataMax = arrayMax(currentPoints); - - e.preventDefault(); - } + if (this.chart && this.chart.hasParallelCoordinates && !this.isXAxis) { + var index = this.parallelPosition, + currentPoints = []; + each(this.series, function (series) { + if (defined(series.yData[index])) { + // We need to use push() beacause of null points + currentPoints.push(series.yData[index]); + } + }); + this.dataMin = arrayMin(currentPoints); + this.dataMax = arrayMax(currentPoints); + + e.preventDefault(); + } }); extend(AxisProto, /** @lends Highcharts.Axis.prototype */ { - /** - * Set predefined left+width and top+height (inverted) for yAxes. This - * method modifies options param. - * - * @param {Array} axisPosition - * ['left', 'width', 'height', 'top'] or - * ['top', 'height', 'width', 'left'] for an inverted chart. - * @param {Object} options {@link Highcharts.Axis#options}. - */ - setParallelPosition: function (axisPosition, options) { - options[axisPosition[0]] = 100 * (this.parallelPosition + 0.5) / - (this.chart.parallelInfo.counter + 1) + '%'; - this[axisPosition[1]] = options[axisPosition[1]] = 0; - - // In case of chart.update(inverted), remove old options: - this[axisPosition[2]] = options[axisPosition[2]] = null; - this[axisPosition[3]] = options[axisPosition[3]] = null; - } + /** + * Set predefined left+width and top+height (inverted) for yAxes. This + * method modifies options param. + * + * @param {Array} axisPosition + * ['left', 'width', 'height', 'top'] or + * ['top', 'height', 'width', 'left'] for an inverted chart. + * @param {Object} options {@link Highcharts.Axis#options}. + */ + setParallelPosition: function (axisPosition, options) { + options[axisPosition[0]] = 100 * (this.parallelPosition + 0.5) / + (this.chart.parallelInfo.counter + 1) + '%'; + this[axisPosition[1]] = options[axisPosition[1]] = 0; + + // In case of chart.update(inverted), remove old options: + this[axisPosition[2]] = options[axisPosition[2]] = null; + this[axisPosition[3]] = options[axisPosition[3]] = null; + } }); @@ -325,17 +325,17 @@ extend(AxisProto, /** @lends Highcharts.Axis.prototype */ { * yAxis needs a reference to all series to calculate extremes. */ wrap(SeriesProto, 'bindAxes', function (proceed) { - if (this.chart.hasParallelCoordinates) { - var series = this; - each(this.chart.axes, function (axis) { - series.insert(axis.series); - axis.isDirty = true; - }); - series.xAxis = this.chart.xAxis[0]; - series.yAxis = this.chart.yAxis[0]; - } else { - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + if (this.chart.hasParallelCoordinates) { + var series = this; + each(this.chart.axes, function (axis) { + series.insert(axis.series); + axis.isDirty = true; + }); + series.xAxis = this.chart.xAxis[0]; + series.yAxis = this.chart.yAxis[0]; + } else { + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } }); @@ -343,136 +343,136 @@ wrap(SeriesProto, 'bindAxes', function (proceed) { * Translate each point using corresponding yAxis. */ addEvent(H.Series, 'afterTranslate', function () { - var series = this, - chart = this.chart, - points = series.points, - dataLength = points && points.length, - closestPointRangePx = Number.MAX_VALUE, - lastPlotX, - point, - i; - - if (this.chart.hasParallelCoordinates) { - for (i = 0; i < dataLength; i++) { - point = points[i]; - if (defined(point.y)) { - point.plotX = point.clientX = chart.inverted ? - chart.plotHeight - chart.yAxis[i].top + chart.plotTop : - chart.yAxis[i].left - chart.plotLeft; - - point.plotY = chart.yAxis[i] - .translate(point.y, false, true, null, true); - - if (lastPlotX !== undefined) { - closestPointRangePx = Math.min( - closestPointRangePx, - Math.abs(point.plotX - lastPlotX) - ); - } - lastPlotX = point.plotX; - point.isInside = chart.isInsidePlot( - point.plotX, - point.plotY, - chart.inverted - ); - } else { - point.isNull = true; - } - } - this.closestPointRangePx = closestPointRangePx; - } + var series = this, + chart = this.chart, + points = series.points, + dataLength = points && points.length, + closestPointRangePx = Number.MAX_VALUE, + lastPlotX, + point, + i; + + if (this.chart.hasParallelCoordinates) { + for (i = 0; i < dataLength; i++) { + point = points[i]; + if (defined(point.y)) { + point.plotX = point.clientX = chart.inverted ? + chart.plotHeight - chart.yAxis[i].top + chart.plotTop : + chart.yAxis[i].left - chart.plotLeft; + + point.plotY = chart.yAxis[i] + .translate(point.y, false, true, null, true); + + if (lastPlotX !== undefined) { + closestPointRangePx = Math.min( + closestPointRangePx, + Math.abs(point.plotX - lastPlotX) + ); + } + lastPlotX = point.plotX; + point.isInside = chart.isInsidePlot( + point.plotX, + point.plotY, + chart.inverted + ); + } else { + point.isNull = true; + } + } + this.closestPointRangePx = closestPointRangePx; + } }); /** * On destroy, we need to remove series from each axis.series */ H.addEvent(H.Series, 'destroy', function () { - if (this.chart.hasParallelCoordinates) { - each(this.chart.axes || [], function (axis) { - if (axis && axis.series) { - erase(axis.series, this); - axis.isDirty = axis.forceRedraw = true; - } - }, this); - } + if (this.chart.hasParallelCoordinates) { + each(this.chart.axes || [], function (axis) { + if (axis && axis.series) { + erase(axis.series, this); + axis.isDirty = axis.forceRedraw = true; + } + }, this); + } }); function addFormattedValue(proceed) { - var chart = this.series && this.series.chart, - config = proceed.apply(this, Array.prototype.slice.call(arguments, 1)), - formattedValue, - yAxisOptions, - labelFormat, - yAxis; - - if ( - chart && - chart.hasParallelCoordinates && - !defined(config.formattedValue) - ) { - yAxis = chart.yAxis[this.x]; - yAxisOptions = yAxis.options; - - labelFormat = pick( - /** - * Parallel coordinates only. Format that will be used for point.y - * and available in [tooltip.pointFormat](#tooltip.pointFormat) as - * `{point.formattedValue}`. If not set, `{point.formattedValue}` - * will use other options, in this order: - * - * 1. [yAxis.labels.format](#yAxis.labels.format) will be used if - * set - * 2. if yAxis is a category, then category name will be displayed - * 3. if yAxis is a datetime, then value will use the same format as - * yAxis labels - * 4. if yAxis is linear/logarithmic type, then simple value will be - * used - * - * @default undefined - * @memberOf yAxis - * @sample {highcharts} - * /highcharts/parallel-coordinates/tooltipvalueformat/ - * Different tooltipValueFormats's - * @apioption yAxis.tooltipValueFormat - * @product highcharts - * @since 6.0.0 - * @type {String} - */ - yAxisOptions.tooltipValueFormat, - yAxisOptions.labels.format - ); - if (labelFormat) { - formattedValue = H.format( - labelFormat, - extend( - this, - { value: this.y } - ), - chart.time - ); - } else if (yAxis.isDatetimeAxis) { - formattedValue = chart.time.dateFormat( - yAxisOptions.dateTimeLabelFormats[ - yAxis.tickPositions.info.unitName - ], - this.y - ); - } else if (yAxisOptions.categories) { - formattedValue = yAxisOptions.categories[this.y]; - } else { - formattedValue = this.y; - } - - config.formattedValue = config.point.formattedValue = formattedValue; - } - - return config; + var chart = this.series && this.series.chart, + config = proceed.apply(this, Array.prototype.slice.call(arguments, 1)), + formattedValue, + yAxisOptions, + labelFormat, + yAxis; + + if ( + chart && + chart.hasParallelCoordinates && + !defined(config.formattedValue) + ) { + yAxis = chart.yAxis[this.x]; + yAxisOptions = yAxis.options; + + labelFormat = pick( + /** + * Parallel coordinates only. Format that will be used for point.y + * and available in [tooltip.pointFormat](#tooltip.pointFormat) as + * `{point.formattedValue}`. If not set, `{point.formattedValue}` + * will use other options, in this order: + * + * 1. [yAxis.labels.format](#yAxis.labels.format) will be used if + * set + * 2. if yAxis is a category, then category name will be displayed + * 3. if yAxis is a datetime, then value will use the same format as + * yAxis labels + * 4. if yAxis is linear/logarithmic type, then simple value will be + * used + * + * @default undefined + * @memberOf yAxis + * @sample {highcharts} + * /highcharts/parallel-coordinates/tooltipvalueformat/ + * Different tooltipValueFormats's + * @apioption yAxis.tooltipValueFormat + * @product highcharts + * @since 6.0.0 + * @type {String} + */ + yAxisOptions.tooltipValueFormat, + yAxisOptions.labels.format + ); + if (labelFormat) { + formattedValue = H.format( + labelFormat, + extend( + this, + { value: this.y } + ), + chart.time + ); + } else if (yAxis.isDatetimeAxis) { + formattedValue = chart.time.dateFormat( + yAxisOptions.dateTimeLabelFormats[ + yAxis.tickPositions.info.unitName + ], + this.y + ); + } else if (yAxisOptions.categories) { + formattedValue = yAxisOptions.categories[this.y]; + } else { + formattedValue = this.y; + } + + config.formattedValue = config.point.formattedValue = formattedValue; + } + + return config; } each(['line', 'spline'], function (seriesName) { - wrap( - H.seriesTypes[seriesName].prototype.pointClass.prototype, - 'getLabelConfig', - addFormattedValue - ); + wrap( + H.seriesTypes[seriesName].prototype.pointClass.prototype, + 'getLabelConfig', + addFormattedValue + ); }); diff --git a/js/modules/pareto.src.js b/js/modules/pareto.src.js index ed6e22f0885..80b9bdcfbd5 100644 --- a/js/modules/pareto.src.js +++ b/js/modules/pareto.src.js @@ -11,9 +11,9 @@ import '../parts/Options.js'; import derivedSeriesMixin from '../mixins/derived-series.js'; var each = H.each, - correctFloat = H.correctFloat, - seriesType = H.seriesType, - merge = H.merge; + correctFloat = H.correctFloat, + seriesType = H.seriesType, + merge = H.merge; /** @@ -25,9 +25,9 @@ var each = H.each, /** * A pareto diagram is a type of chart that contains both bars and a line graph, - * where individual values are represented in descending order by bars, + * where individual values are represented in descending order by bars, * and the cumulative total is represented by the line. - * + * * @extends {plotOptions.line} * @product highcharts * @sample {highcharts} highcharts/demo/pareto/ @@ -44,66 +44,66 @@ var each = H.each, */ seriesType('pareto', 'line', { - /** - * Higher zIndex than column series to draw line above shapes. - */ - zIndex: 3 + /** + * Higher zIndex than column series to draw line above shapes. + */ + zIndex: 3 }, merge(derivedSeriesMixin, { - /** - * calculate sum and return percent points - * - * @param {Object} series - * @return {Array} Returns array of points [x,y] - */ - setDerivedData: function () { - if (this.baseSeries.yData.length > 1) { - var xValues = this.baseSeries.xData, - yValues = this.baseSeries.yData, - sum = this.sumPointsPercents(yValues, xValues, null, true); + /** + * calculate sum and return percent points + * + * @param {Object} series + * @return {Array} Returns array of points [x,y] + */ + setDerivedData: function () { + if (this.baseSeries.yData.length > 1) { + var xValues = this.baseSeries.xData, + yValues = this.baseSeries.yData, + sum = this.sumPointsPercents(yValues, xValues, null, true); - this.setData( - this.sumPointsPercents(yValues, xValues, sum, false), - false - ); - } - }, - /** - * calculate y sum and each percent point - * - * @param {Array} yValues y values - * @param {Array} xValues x values - * @param {Number} sum of all y values - * @param {Boolean} isSum declares if calculate sum of all points - * @return {Array} Returns sum of points or array of points [x,y] - */ - sumPointsPercents: function (yValues, xValues, sum, isSum) { - var sumY = 0, - sumPercent = 0, - percentPoints = [], - percentPoint; + this.setData( + this.sumPointsPercents(yValues, xValues, sum, false), + false + ); + } + }, + /** + * calculate y sum and each percent point + * + * @param {Array} yValues y values + * @param {Array} xValues x values + * @param {Number} sum of all y values + * @param {Boolean} isSum declares if calculate sum of all points + * @return {Array} Returns sum of points or array of points [x,y] + */ + sumPointsPercents: function (yValues, xValues, sum, isSum) { + var sumY = 0, + sumPercent = 0, + percentPoints = [], + percentPoint; - each(yValues, function (point, i) { - if (point !== null) { - if (isSum) { - sumY += point; - } else { - percentPoint = (point / sum) * 100; - percentPoints.push( - [xValues[i], correctFloat(sumPercent + percentPoint)] - ); - sumPercent += percentPoint; - } - } - }); + each(yValues, function (point, i) { + if (point !== null) { + if (isSum) { + sumY += point; + } else { + percentPoint = (point / sum) * 100; + percentPoints.push( + [xValues[i], correctFloat(sumPercent + percentPoint)] + ); + sumPercent += percentPoint; + } + } + }); - return isSum ? sumY : percentPoints; - } + return isSum ? sumY : percentPoints; + } })); /** * A `pareto` series. If the [type](#series.pareto.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @since 6.0.0 * @extends series,plotOptions.pareto @@ -124,7 +124,7 @@ seriesType('pareto', 'line', { /** * An array of data points for the series. For the `pareto` series type, * points are calculated dynamically. - * + * * @type {Array} * @since 6.0.0 * @extends series.column.data diff --git a/js/modules/pattern-fill.src.js b/js/modules/pattern-fill.src.js index f2ce8130a1e..c3e222af96a 100644 --- a/js/modules/pattern-fill.src.js +++ b/js/modules/pattern-fill.src.js @@ -11,8 +11,8 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var wrap = H.wrap, - each = H.each, - merge = H.merge; + each = H.each, + merge = H.merge; /** @@ -26,28 +26,28 @@ var wrap = H.wrap, * @return {String} The computed hash. */ function hashFromObject(obj, preSeed) { - var str = JSON.stringify(obj), - strLen = str.length || 0, - hash = 0, - i = 0, - char, - seedStep; - - if (preSeed) { - seedStep = Math.max(Math.floor(strLen / 500), 1); - for (var a = 0; a < strLen; a += seedStep) { - hash += str.charCodeAt(a); - } - hash = hash & hash; - } - - for (; i < strLen; ++i) { - char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - - return hash.toString(16).replace('-', '1'); + var str = JSON.stringify(obj), + strLen = str.length || 0, + hash = 0, + i = 0, + char, + seedStep; + + if (preSeed) { + seedStep = Math.max(Math.floor(strLen / 500), 1); + for (var a = 0; a < strLen; a += seedStep) { + hash += str.charCodeAt(a); + } + hash = hash & hash; + } + + for (; i < strLen; ++i) { + char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + + return hash.toString(16).replace('-', '1'); } @@ -62,75 +62,75 @@ function hashFromObject(obj, preSeed) { * @param {Object} pattern The pattern to set dimensions on. */ H.Point.prototype.calculatePatternDimensions = function (pattern) { - if (pattern.width && pattern.height) { - return; - } - - var bBox = this.graphic && ( - this.graphic.getBBox && - this.graphic.getBBox(true) || - this.graphic.element && - this.graphic.element.getBBox() - ) || {}, - shapeArgs = this.shapeArgs; - - // Prefer using shapeArgs, as it is animation agnostic - if (shapeArgs) { - bBox.width = shapeArgs.width || bBox.width; - bBox.height = shapeArgs.height || bBox.height; - bBox.x = shapeArgs.x || bBox.x; - bBox.y = shapeArgs.y || bBox.y; - } - - // For images we stretch to bounding box - if (pattern.image) { - // If we do not have a bounding box at this point, simply add a defer - // key and pick this up in the fillSetter handler, where the bounding - // box should exist. - if (!bBox.width || !bBox.height) { - pattern._width = 'defer'; - pattern._height = 'defer'; - return; - } - - // Handle aspect ratio filling - if (pattern.aspectRatio) { - bBox.aspectRatio = bBox.width / bBox.height; - if (pattern.aspectRatio > bBox.aspectRatio) { - // Height of bBox will determine width - bBox.aspectWidth = bBox.height * pattern.aspectRatio; - } else { - // Width of bBox will determine height - bBox.aspectHeight = bBox.width / pattern.aspectRatio; - } - } - - // We set the width/height on internal properties to differentiate - // between the options set by a user and by this function. - pattern._width = pattern.width || - Math.ceil(bBox.aspectWidth || bBox.width); - pattern._height = pattern.height || - Math.ceil(bBox.aspectHeight || bBox.height); - } - - // Set x/y accordingly, centering if using aspect ratio, otherwise adjusting - // so bounding box corner is 0,0 of pattern. - if (!pattern.width) { - pattern._x = pattern.x || 0; - pattern._x += bBox.x - Math.round( - bBox.aspectWidth ? - Math.abs(bBox.aspectWidth - bBox.width) / 2 : - 0 - ); - } - if (!pattern.height) { - pattern._y = pattern.y || 0; - pattern._y += bBox.y - Math.round( - bBox.aspectHeight ? - Math.abs(bBox.aspectHeight - bBox.height) / 2 : - 0 - ); - } + if (pattern.width && pattern.height) { + return; + } + + var bBox = this.graphic && ( + this.graphic.getBBox && + this.graphic.getBBox(true) || + this.graphic.element && + this.graphic.element.getBBox() + ) || {}, + shapeArgs = this.shapeArgs; + + // Prefer using shapeArgs, as it is animation agnostic + if (shapeArgs) { + bBox.width = shapeArgs.width || bBox.width; + bBox.height = shapeArgs.height || bBox.height; + bBox.x = shapeArgs.x || bBox.x; + bBox.y = shapeArgs.y || bBox.y; + } + + // For images we stretch to bounding box + if (pattern.image) { + // If we do not have a bounding box at this point, simply add a defer + // key and pick this up in the fillSetter handler, where the bounding + // box should exist. + if (!bBox.width || !bBox.height) { + pattern._width = 'defer'; + pattern._height = 'defer'; + return; + } + + // Handle aspect ratio filling + if (pattern.aspectRatio) { + bBox.aspectRatio = bBox.width / bBox.height; + if (pattern.aspectRatio > bBox.aspectRatio) { + // Height of bBox will determine width + bBox.aspectWidth = bBox.height * pattern.aspectRatio; + } else { + // Width of bBox will determine height + bBox.aspectHeight = bBox.width / pattern.aspectRatio; + } + } + + // We set the width/height on internal properties to differentiate + // between the options set by a user and by this function. + pattern._width = pattern.width || + Math.ceil(bBox.aspectWidth || bBox.width); + pattern._height = pattern.height || + Math.ceil(bBox.aspectHeight || bBox.height); + } + + // Set x/y accordingly, centering if using aspect ratio, otherwise adjusting + // so bounding box corner is 0,0 of pattern. + if (!pattern.width) { + pattern._x = pattern.x || 0; + pattern._x += bBox.x - Math.round( + bBox.aspectWidth ? + Math.abs(bBox.aspectWidth - bBox.width) / 2 : + 0 + ); + } + if (!pattern.height) { + pattern._y = pattern.y || 0; + pattern._y += bBox.y - Math.round( + bBox.aspectHeight ? + Math.abs(bBox.aspectHeight - bBox.height) / 2 : + 0 + ); + } }; @@ -139,7 +139,7 @@ H.Point.prototype.calculatePatternDimensions = function (pattern) { * @property {Object} pattern Holds a pattern definition. * @property {String} pattern.image URL to an image to use as the pattern. * @property {Number} pattern.width Width of the pattern. For images this is - * automatically set to the width of the element bounding box if not supplied. + * automatically set to the width of the element bounding box if not supplied. * For non-image patterns the default is 32px. Note that automatic resizing of * image patterns to fill a bounding box dynamically is only supported for * patterns with an automatically calculated ID. @@ -156,11 +156,11 @@ H.Point.prototype.calculatePatternDimensions = function (pattern) { * ignored. * @property {String} pattern.color Pattern color, used as default path stroke. * @property {Number} pattern.opacity Opacity of the pattern as a float value - * from 0 to 1. + * from 0 to 1. * @property {String} pattern.id ID to assign to the pattern. This is - * automatically computed if not added, and identical patterns are reused. To - * refer to an existing pattern for a Highcharts color, use - * `color: "url(#pattern-id)"`. + * automatically computed if not added, and identical patterns are reused. To + * refer to an existing pattern for a Highcharts color, use + * `color: "url(#pattern-id)"`. * @property {Object|Boolean} animation Animation options for the image pattern * loading. * @@ -168,14 +168,14 @@ H.Point.prototype.calculatePatternDimensions = function (pattern) { * // Pattern used as a color option * color: { * pattern: { - * path: { - * d: 'M 3 3 L 8 3 L 8 8 Z', - * fill: '#102045' - * }, - * width: 12, - * height: 12, - * color: '#907000', - * opacity: 0.5 + * path: { + * d: 'M 3 3 L 8 3 L 8 8 Z', + * fill: '#102045' + * }, + * width: 12, + * height: 12, + * color: '#907000', + * opacity: 0.5 * } * } * @@ -195,94 +195,94 @@ H.Point.prototype.calculatePatternDimensions = function (pattern) { * @return {Object} The added pattern. Undefined if the pattern already exists. */ H.SVGRenderer.prototype.addPattern = function (options, animation) { - var pattern, - animate = H.pick(animation, true), - path, - defaultSize = 32, - width = options.width || options._width || defaultSize, - height = options.height || options._height || defaultSize, - color = options.color || '#343434', - id = options.id, - ren = this, - rect = function (fill) { - ren.rect(0, 0, width, height) - .attr({ - fill: fill - }) - .add(pattern); - }; - - if (!id) { - this.idCounter = this.idCounter || 0; - id = 'highcharts-pattern-' + this.idCounter; - ++this.idCounter; - } - - // Do nothing if ID already exists - this.defIds = this.defIds || []; - if (H.inArray(id, this.defIds) > -1) { - return; - } - - // Store ID in list to avoid duplicates - this.defIds.push(id); - - // Create pattern element - pattern = this.createElement('pattern').attr({ - id: id, - patternUnits: 'userSpaceOnUse', - width: width, - height: height, - x: options._x || options.x || 0, - y: options._y || options.y || 0 - }).add(this.defs); - - // Set id on the SVGRenderer object - pattern.id = id; - - // Use an SVG path for the pattern - if (options.path) { - path = options.path; - - // The background - if (path.fill) { - rect(path.fill); - } - - // The pattern - this.createElement('path').attr({ - 'd': path.d || path, - 'stroke': path.stroke || color, - 'stroke-width': path.strokeWidth || 2 - }).add(pattern); - pattern.color = color; - - // Image pattern - } else if (options.image) { - if (animate) { - this.image( - options.image, 0, 0, width, height, function () { - // Onload - this.animate({ opacity: 1 }, animate); - H.removeEvent(this.element, 'load'); - } - ).attr({ opacity: 0 }).add(pattern); - } else { - this.image(options.image, 0, 0, width, height).add(pattern); - } - } - - if (options.opacity !== undefined) { - each(pattern.element.children, function (child) { - child.setAttribute('opacity', options.opacity); - }); - } - - // Store for future reference - this.patternElements = this.patternElements || {}; - this.patternElements[id] = pattern; - - return pattern; + var pattern, + animate = H.pick(animation, true), + path, + defaultSize = 32, + width = options.width || options._width || defaultSize, + height = options.height || options._height || defaultSize, + color = options.color || '#343434', + id = options.id, + ren = this, + rect = function (fill) { + ren.rect(0, 0, width, height) + .attr({ + fill: fill + }) + .add(pattern); + }; + + if (!id) { + this.idCounter = this.idCounter || 0; + id = 'highcharts-pattern-' + this.idCounter; + ++this.idCounter; + } + + // Do nothing if ID already exists + this.defIds = this.defIds || []; + if (H.inArray(id, this.defIds) > -1) { + return; + } + + // Store ID in list to avoid duplicates + this.defIds.push(id); + + // Create pattern element + pattern = this.createElement('pattern').attr({ + id: id, + patternUnits: 'userSpaceOnUse', + width: width, + height: height, + x: options._x || options.x || 0, + y: options._y || options.y || 0 + }).add(this.defs); + + // Set id on the SVGRenderer object + pattern.id = id; + + // Use an SVG path for the pattern + if (options.path) { + path = options.path; + + // The background + if (path.fill) { + rect(path.fill); + } + + // The pattern + this.createElement('path').attr({ + 'd': path.d || path, + 'stroke': path.stroke || color, + 'stroke-width': path.strokeWidth || 2 + }).add(pattern); + pattern.color = color; + + // Image pattern + } else if (options.image) { + if (animate) { + this.image( + options.image, 0, 0, width, height, function () { + // Onload + this.animate({ opacity: 1 }, animate); + H.removeEvent(this.element, 'load'); + } + ).attr({ opacity: 0 }).add(pattern); + } else { + this.image(options.image, 0, 0, width, height).add(pattern); + } + } + + if (options.opacity !== undefined) { + each(pattern.element.children, function (child) { + child.setAttribute('opacity', options.opacity); + }); + } + + // Store for future reference + this.patternElements = this.patternElements || {}; + this.patternElements[id] = pattern; + + return pattern; }; @@ -290,19 +290,19 @@ H.SVGRenderer.prototype.addPattern = function (options, animation) { * Make sure we have a series color */ wrap(H.Series.prototype, 'getColor', function (proceed) { - var oldColor = this.options.color; - // Temporarely remove color options to get defaults - if (oldColor && oldColor.pattern && !oldColor.pattern.color) { - delete this.options.color; - // Get default - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - // Replace with old, but add default color - oldColor.pattern.color = this.color; - this.color = this.options.color = oldColor; - } else { - // We have a color, no need to do anything special - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + var oldColor = this.options.color; + // Temporarely remove color options to get defaults + if (oldColor && oldColor.pattern && !oldColor.pattern.color) { + delete this.options.color; + // Get default + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + // Replace with old, but add default color + oldColor.pattern.color = this.color; + this.color = this.options.color = oldColor; + } else { + // We have a color, no need to do anything special + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } }); @@ -310,32 +310,32 @@ wrap(H.Series.prototype, 'getColor', function (proceed) { * Calculate pattern dimensions on points that have their own pattern. */ wrap(H.Series.prototype, 'render', function (proceed) { - var isResizing = this.chart.isResizing; - if (this.isDirtyData || isResizing || !this.chart.hasRendered) { - each(this.points || [], function (point) { - var colorOptions = point.options && point.options.color; - if (colorOptions && colorOptions.pattern) { - // For most points we want to recalculate the dimensions on - // render, where we have the shape args and bbox. But if we - // are resizing and don't have the shape args, defer it, since - // the bounding box is still not resized. - if ( - isResizing && - !( - point.shapeArgs && - point.shapeArgs.width && - point.shapeArgs.height - ) - ) { - colorOptions.pattern._width = 'defer'; - colorOptions.pattern._height = 'defer'; - } else { - point.calculatePatternDimensions(colorOptions.pattern); - } - } - }); - } - return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + var isResizing = this.chart.isResizing; + if (this.isDirtyData || isResizing || !this.chart.hasRendered) { + each(this.points || [], function (point) { + var colorOptions = point.options && point.options.color; + if (colorOptions && colorOptions.pattern) { + // For most points we want to recalculate the dimensions on + // render, where we have the shape args and bbox. But if we + // are resizing and don't have the shape args, defer it, since + // the bounding box is still not resized. + if ( + isResizing && + !( + point.shapeArgs && + point.shapeArgs.width && + point.shapeArgs.height + ) + ) { + colorOptions.pattern._width = 'defer'; + colorOptions.pattern._height = 'defer'; + } else { + point.calculatePatternDimensions(colorOptions.pattern); + } + } + }); + } + return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); }); @@ -343,25 +343,25 @@ wrap(H.Series.prototype, 'render', function (proceed) { * Merge series color options to points */ wrap(H.Point.prototype, 'applyOptions', function (proceed) { - var point = proceed.apply(this, Array.prototype.slice.call(arguments, 1)), - colorOptions = point.options.color; - - // Only do this if we have defined a specific color on this point. Otherwise - // we will end up trying to re-add the series color for each point. - if (colorOptions && colorOptions.pattern) { - // Move path definition to object, allows for merge with series path - // definition - if (typeof colorOptions.pattern.path === 'string') { - colorOptions.pattern.path = { - d: colorOptions.pattern.path - }; - } - // Merge with series options - point.color = point.options.color = merge( - point.series.options.color, colorOptions - ); - } - return point; + var point = proceed.apply(this, Array.prototype.slice.call(arguments, 1)), + colorOptions = point.options.color; + + // Only do this if we have defined a specific color on this point. Otherwise + // we will end up trying to re-add the series color for each point. + if (colorOptions && colorOptions.pattern) { + // Move path definition to object, allows for merge with series path + // definition + if (typeof colorOptions.pattern.path === 'string') { + colorOptions.pattern.path = { + d: colorOptions.pattern.path + }; + } + // Merge with series options + point.color = point.options.color = merge( + point.series.options.color, colorOptions + ); + } + return point; }); @@ -369,80 +369,80 @@ wrap(H.Point.prototype, 'applyOptions', function (proceed) { * Add functionality to SVG renderer to handle patterns as complex colors */ H.addEvent(H.SVGRenderer, 'complexColor', function (args) { - var color = args.args[0], - prop = args.args[1], - element = args.args[2], - pattern = color.pattern, - value = '#343434', - forceHashId; - - // Skip and call default if there is no pattern - if (!pattern) { - return true; - } - - // We have a pattern. - if ( - pattern.image || - typeof pattern.path === 'string' || - pattern.path && pattern.path.d - ) { - // Real pattern. Add it and set the color value to be a reference. - - // Force Hash-based IDs for legend items, as they are drawn before - // point render, meaning they are drawn before autocalculated image - // width/heights. We don't want them to highjack the width/height for - // this ID if it is defined by users. - forceHashId = element.parentNode && - element.parentNode.getAttribute('class'); - forceHashId = forceHashId && - forceHashId.indexOf('highcharts-legend') > -1; - - // If we don't have a width/height yet, handle it. Try faking a point - // and running the algorithm again. - if (pattern._width === 'defer' || pattern._height === 'defer') { - H.Point.prototype.calculatePatternDimensions.call( - { graphic: { element: element } }, pattern - ); - } - - // If we don't have an explicit ID, compute a hash from the - // definition and use that as the ID. This ensures that points with - // the same pattern definition reuse existing pattern elements by - // default. We combine two hashes, the second with an additional - // preSeed algorithm, to minimize collision probability. - if (forceHashId || !pattern.id) { - // Make a copy so we don't accidentally edit options when setting ID - pattern = merge({}, pattern); - pattern.id = 'highcharts-pattern-' + hashFromObject(pattern) + - hashFromObject(pattern, true); - } - - // Add it. This function does nothing if an element with this ID - // already exists. - this.addPattern(pattern, !this.forExport && H.animObject(H.pick( - pattern.animation, - this.globalAnimation, - { duration: 100 } - ))); - - value = 'url(' + this.url + '#' + pattern.id + ')'; - - } else { - // Not a full pattern definition, just add color - value = pattern.color || value; - } - - // Set the fill/stroke prop on the element - element.setAttribute(prop, value); - - // Allow the color to be concatenated into tooltips formatters etc. - color.toString = function () { - return value; - }; - - // Skip default handler - return false; + var color = args.args[0], + prop = args.args[1], + element = args.args[2], + pattern = color.pattern, + value = '#343434', + forceHashId; + + // Skip and call default if there is no pattern + if (!pattern) { + return true; + } + + // We have a pattern. + if ( + pattern.image || + typeof pattern.path === 'string' || + pattern.path && pattern.path.d + ) { + // Real pattern. Add it and set the color value to be a reference. + + // Force Hash-based IDs for legend items, as they are drawn before + // point render, meaning they are drawn before autocalculated image + // width/heights. We don't want them to highjack the width/height for + // this ID if it is defined by users. + forceHashId = element.parentNode && + element.parentNode.getAttribute('class'); + forceHashId = forceHashId && + forceHashId.indexOf('highcharts-legend') > -1; + + // If we don't have a width/height yet, handle it. Try faking a point + // and running the algorithm again. + if (pattern._width === 'defer' || pattern._height === 'defer') { + H.Point.prototype.calculatePatternDimensions.call( + { graphic: { element: element } }, pattern + ); + } + + // If we don't have an explicit ID, compute a hash from the + // definition and use that as the ID. This ensures that points with + // the same pattern definition reuse existing pattern elements by + // default. We combine two hashes, the second with an additional + // preSeed algorithm, to minimize collision probability. + if (forceHashId || !pattern.id) { + // Make a copy so we don't accidentally edit options when setting ID + pattern = merge({}, pattern); + pattern.id = 'highcharts-pattern-' + hashFromObject(pattern) + + hashFromObject(pattern, true); + } + + // Add it. This function does nothing if an element with this ID + // already exists. + this.addPattern(pattern, !this.forExport && H.animObject(H.pick( + pattern.animation, + this.globalAnimation, + { duration: 100 } + ))); + + value = 'url(' + this.url + '#' + pattern.id + ')'; + + } else { + // Not a full pattern definition, just add color + value = pattern.color || value; + } + + // Set the fill/stroke prop on the element + element.setAttribute(prop, value); + + // Allow the color to be concatenated into tooltips formatters etc. + color.toString = function () { + return value; + }; + + // Skip default handler + return false; }); @@ -451,25 +451,25 @@ H.addEvent(H.SVGRenderer, 'complexColor', function (args) { * resize, as the bounding boxes are not available until then. */ H.addEvent(H.Chart, 'endResize', function () { - if ( - H.grep(this.renderer.defIds || [], function (id) { - return id && id.indexOf && id.indexOf('highcharts-pattern-') === 0; - }).length - ) { - // We have non-default patterns to fix. Find them by looping through - // all points. - each(this.series, function (series) { - each(series.points, function (point) { - var colorOptions = point.options && point.options.color; - if (colorOptions && colorOptions.pattern) { - colorOptions.pattern._width = 'defer'; - colorOptions.pattern._height = 'defer'; - } - }); - }); - // Redraw without animation - this.redraw(false); - } + if ( + H.grep(this.renderer.defIds || [], function (id) { + return id && id.indexOf && id.indexOf('highcharts-pattern-') === 0; + }).length + ) { + // We have non-default patterns to fix. Find them by looping through + // all points. + each(this.series, function (series) { + each(series.points, function (point) { + var colorOptions = point.options && point.options.color; + if (colorOptions && colorOptions.pattern) { + colorOptions.pattern._width = 'defer'; + colorOptions.pattern._height = 'defer'; + } + }); + }); + // Redraw without animation + this.redraw(false); + } }); @@ -478,44 +478,44 @@ H.addEvent(H.Chart, 'endResize', function () { * are no longer being referenced. */ H.addEvent(H.Chart, 'redraw', function () { - var usedIds = [], - renderer = this.renderer, - // Get the autocomputed patterns - these are the ones we might delete - patterns = H.grep(renderer.defIds || [], function (pattern) { - return pattern.indexOf && - pattern.indexOf('highcharts-pattern-') === 0; - }); - - if (patterns.length) { - // Look through the DOM for usage of the patterns. This can be points, - // series, tooltips etc. - each(this.renderTo.querySelectorAll( - '[color^="url(#"], [fill^="url(#"], [stroke^="url(#"]' - ), function (node) { - var id = node.getAttribute('fill') || - node.getAttribute('color') || - node.getAttribute('stroke'); - if (id) { - usedIds.push(id - .substring(id.indexOf('url(#') + 5) - .replace(')', '') - ); - } - }); - - // Loop through the patterns that exist and see if they are used - each(patterns, function (id) { - if (H.inArray(id, usedIds) === -1) { - // Remove id from used id list - H.erase(renderer.defIds, id); - // Remove pattern element - if (renderer.patternElements[id]) { - renderer.patternElements[id].destroy(); - delete renderer.patternElements[id]; - } - } - }); - } + var usedIds = [], + renderer = this.renderer, + // Get the autocomputed patterns - these are the ones we might delete + patterns = H.grep(renderer.defIds || [], function (pattern) { + return pattern.indexOf && + pattern.indexOf('highcharts-pattern-') === 0; + }); + + if (patterns.length) { + // Look through the DOM for usage of the patterns. This can be points, + // series, tooltips etc. + each(this.renderTo.querySelectorAll( + '[color^="url(#"], [fill^="url(#"], [stroke^="url(#"]' + ), function (node) { + var id = node.getAttribute('fill') || + node.getAttribute('color') || + node.getAttribute('stroke'); + if (id) { + usedIds.push(id + .substring(id.indexOf('url(#') + 5) + .replace(')', '') + ); + } + }); + + // Loop through the patterns that exist and see if they are used + each(patterns, function (id) { + if (H.inArray(id, usedIds) === -1) { + // Remove id from used id list + H.erase(renderer.defIds, id); + // Remove pattern element + if (renderer.patternElements[id]) { + renderer.patternElements[id].destroy(); + delete renderer.patternElements[id]; + } + } + }); + } }); @@ -523,25 +523,25 @@ H.addEvent(H.Chart, 'redraw', function () { * Add the predefined patterns */ H.Chart.prototype.callbacks.push(function (chart) { - var colors = H.getOptions().colors; - each([ - 'M 0 0 L 10 10 M 9 -1 L 11 1 M -1 9 L 1 11', - 'M 0 10 L 10 0 M -1 1 L 1 -1 M 9 11 L 11 9', - 'M 3 0 L 3 10 M 8 0 L 8 10', - 'M 0 3 L 10 3 M 0 8 L 10 8', - 'M 0 3 L 5 3 L 5 0 M 5 10 L 5 7 L 10 7', - 'M 3 3 L 8 3 L 8 8 L 3 8 Z', - 'M 5 5 m -4 0 a 4 4 0 1 1 8 0 a 4 4 0 1 1 -8 0', - 'M 10 3 L 5 3 L 5 0 M 5 10 L 5 7 L 0 7', - 'M 2 5 L 5 2 L 8 5 L 5 8 Z', - 'M 0 0 L 5 10 L 10 0' - ], function (pattern, i) { - chart.renderer.addPattern({ - id: 'highcharts-default-pattern-' + i, - path: pattern, - color: colors[i], - width: 10, - height: 10 - }); - }); + var colors = H.getOptions().colors; + each([ + 'M 0 0 L 10 10 M 9 -1 L 11 1 M -1 9 L 1 11', + 'M 0 10 L 10 0 M -1 1 L 1 -1 M 9 11 L 11 9', + 'M 3 0 L 3 10 M 8 0 L 8 10', + 'M 0 3 L 10 3 M 0 8 L 10 8', + 'M 0 3 L 5 3 L 5 0 M 5 10 L 5 7 L 10 7', + 'M 3 3 L 8 3 L 8 8 L 3 8 Z', + 'M 5 5 m -4 0 a 4 4 0 1 1 8 0 a 4 4 0 1 1 -8 0', + 'M 10 3 L 5 3 L 5 0 M 5 10 L 5 7 L 0 7', + 'M 2 5 L 5 2 L 8 5 L 5 8 Z', + 'M 0 0 L 5 10 L 10 0' + ], function (pattern, i) { + chart.renderer.addPattern({ + id: 'highcharts-default-pattern-' + i, + path: pattern, + color: colors[i], + width: 10, + height: 10 + }); + }); }); diff --git a/js/modules/sankey.src.js b/js/modules/sankey.src.js index 2bed62d8b98..3ef47e6c55c 100644 --- a/js/modules/sankey.src.js +++ b/js/modules/sankey.src.js @@ -12,16 +12,16 @@ import '../parts/Utilities.js'; import '../parts/Options.js'; var defined = H.defined, - each = H.each, - extend = H.extend, - seriesType = H.seriesType, - pick = H.pick, - Point = H.Point; + each = H.each, + extend = H.extend, + seriesType = H.seriesType, + pick = H.pick, + Point = H.Point; /** - * A sankey diagram is a type of flow diagram, in which the width of the + * A sankey diagram is a type of flow diagram, in which the width of the * link between two nodes is shown proportionally to the flow quantity. - * + * * @extends {plotOptions.column} * @product highcharts * @sample highcharts/demo/sankey-diagram/ @@ -40,519 +40,519 @@ var defined = H.defined, * @optionparent plotOptions.sankey */ seriesType('sankey', 'column', { - colorByPoint: true, - /** - * Higher numbers makes the links in a sankey diagram render more curved. - * A `curveFactor` of 0 makes the lines straight. - */ - curveFactor: 0.33, - /** - * Options for the data labels appearing on top of the nodes and links. For - * sankey charts, data labels are visible for the nodes by default, but - * hidden for links. This is controlled by modifying the `nodeFormat`, and - * the `format` that applies to links and is an empty string by default. - */ - dataLabels: { - enabled: true, - backgroundColor: 'none', // enable padding - crop: false, - /** - * The [format string](http://www.highcharts.com/docs/chart- - * concepts/labels-and-string-formatting) specifying what to show - * for _nodes_ in the sankey diagram. By default the - * `nodeFormatter` returns `{point.name}`. - * - * @type {String} - */ - nodeFormat: undefined, - - /** - * Callback to format data labels for _nodes_ in the sankey diagram. - * The `nodeFormat` option takes precedence over the `nodeFormatter`. - * - * @type {Function} - * @since 6.0.2 - */ - nodeFormatter: function () { - return this.point.name; - }, - /** - * The [format string](http://www.highcharts.com/docs/chart- - * concepts/labels-and-string-formatting) specifying what to show for - * _links_ in the sankey diagram. Defaults to an empty string returned - * from the `formatter`, in effect disabling the labels. - */ - format: undefined, - /** - * Callback to format data labels for _links_ in the sankey diagram. - * The `format` option takes precedence over the `formatter`. - * - * @since 6.0.2 - */ - formatter: function () { - return ''; - }, - inside: true - }, - /*= if (build.classic) { =*/ - /** - * Opacity for the links between nodes in the sankey diagram. - */ - linkOpacity: 0.5, - /*= } =*/ - /** - * The pixel width of each node in a sankey diagram, or the height in case - * the chart is inverted. - */ - nodeWidth: 20, - /** - * The padding between nodes in a sankey diagram, in pixels. - */ - nodePadding: 10, - showInLegend: false, - states: { - hover: { - /** - * Opacity for the links between nodes in the sankey diagram in - * hover mode. - */ - linkOpacity: 1 - } - }, - tooltip: { - /** - * A callback for defining the format for _nodes_ in the sankey chart's - * tooltip, as opposed to links. - * - * @type {Function} - * @since 6.0.2 - * @apioption plotOptions.sankey.tooltip.nodeFormatter - */ - - /** - * Whether the tooltip should follow the pointer or stay fixed on the - * item. - */ - followPointer: true, - - /*= if (build.classic) { =*/ - headerFormat: - '{series.name}
', - /*= } else { =*/ - headerFormat: // eslint-disable-line no-dupe-keys - '{series.name}
', - /*= } =*/ - pointFormat: '{point.fromNode.name} \u2192 {point.toNode.name}: {point.weight}
', - /** - * The [format string](http://www.highcharts.com/docs/chart- - * concepts/labels-and-string-formatting) specifying what to - * show for _nodes_ in tooltip - * of a sankey diagram series, as opposed to links. - */ - nodeFormat: '{point.name}: {point.sum}
' - } + colorByPoint: true, + /** + * Higher numbers makes the links in a sankey diagram render more curved. + * A `curveFactor` of 0 makes the lines straight. + */ + curveFactor: 0.33, + /** + * Options for the data labels appearing on top of the nodes and links. For + * sankey charts, data labels are visible for the nodes by default, but + * hidden for links. This is controlled by modifying the `nodeFormat`, and + * the `format` that applies to links and is an empty string by default. + */ + dataLabels: { + enabled: true, + backgroundColor: 'none', // enable padding + crop: false, + /** + * The [format string](http://www.highcharts.com/docs/chart- + * concepts/labels-and-string-formatting) specifying what to show + * for _nodes_ in the sankey diagram. By default the + * `nodeFormatter` returns `{point.name}`. + * + * @type {String} + */ + nodeFormat: undefined, + + /** + * Callback to format data labels for _nodes_ in the sankey diagram. + * The `nodeFormat` option takes precedence over the `nodeFormatter`. + * + * @type {Function} + * @since 6.0.2 + */ + nodeFormatter: function () { + return this.point.name; + }, + /** + * The [format string](http://www.highcharts.com/docs/chart- + * concepts/labels-and-string-formatting) specifying what to show for + * _links_ in the sankey diagram. Defaults to an empty string returned + * from the `formatter`, in effect disabling the labels. + */ + format: undefined, + /** + * Callback to format data labels for _links_ in the sankey diagram. + * The `format` option takes precedence over the `formatter`. + * + * @since 6.0.2 + */ + formatter: function () { + return ''; + }, + inside: true + }, + /*= if (build.classic) { =*/ + /** + * Opacity for the links between nodes in the sankey diagram. + */ + linkOpacity: 0.5, + /*= } =*/ + /** + * The pixel width of each node in a sankey diagram, or the height in case + * the chart is inverted. + */ + nodeWidth: 20, + /** + * The padding between nodes in a sankey diagram, in pixels. + */ + nodePadding: 10, + showInLegend: false, + states: { + hover: { + /** + * Opacity for the links between nodes in the sankey diagram in + * hover mode. + */ + linkOpacity: 1 + } + }, + tooltip: { + /** + * A callback for defining the format for _nodes_ in the sankey chart's + * tooltip, as opposed to links. + * + * @type {Function} + * @since 6.0.2 + * @apioption plotOptions.sankey.tooltip.nodeFormatter + */ + + /** + * Whether the tooltip should follow the pointer or stay fixed on the + * item. + */ + followPointer: true, + + /*= if (build.classic) { =*/ + headerFormat: + '{series.name}
', + /*= } else { =*/ + headerFormat: // eslint-disable-line no-dupe-keys + '{series.name}
', + /*= } =*/ + pointFormat: '{point.fromNode.name} \u2192 {point.toNode.name}: {point.weight}
', + /** + * The [format string](http://www.highcharts.com/docs/chart- + * concepts/labels-and-string-formatting) specifying what to + * show for _nodes_ in tooltip + * of a sankey diagram series, as opposed to links. + */ + nodeFormat: '{point.name}: {point.sum}
' + } }, { - isCartesian: false, - forceDL: true, - /** - * Create a single node that holds information on incoming and outgoing - * links. - */ - createNode: function (id) { - - function findById(nodes, id) { - return H.find(nodes, function (node) { - return node.id === id; - }); - } - - var node = findById(this.nodes, id), - options; - - if (!node) { - options = this.options.nodes && findById(this.options.nodes, id); - node = (new Point()).init( - this, - extend({ - className: 'highcharts-node', - isNode: true, - id: id, - y: 1 // Pass isNull test - }, options) - ); - node.linksTo = []; - node.linksFrom = []; - node.formatPrefix = 'node'; - node.name = node.name || node.id; // for use in formats - - /** - * Return the largest sum of either the incoming or outgoing links. - */ - node.getSum = function () { - var sumTo = 0, - sumFrom = 0; - each(node.linksTo, function (link) { - sumTo += link.weight; - }); - each(node.linksFrom, function (link) { - sumFrom += link.weight; - }); - return Math.max(sumTo, sumFrom); - }; - /** - * Get the offset in weight values of a point/link. - */ - node.offset = function (point, coll) { - var offset = 0; - for (var i = 0; i < node[coll].length; i++) { - if (node[coll][i] === point) { - return offset; - } - offset += node[coll][i].weight; - } - }; - - /** - * Return true if the node has a shape, otherwise all links are - * outgoing. - */ - node.hasShape = function () { - var outgoing = 0; - each(node.linksTo, function (link) { - if (link.outgoing) { - outgoing++; - } - }); - return !node.linksTo.length || outgoing !== node.linksTo.length; - }; - - this.nodes.push(node); - } - return node; - }, - - /** - * Create a node column. - */ - createNodeColumn: function () { - var chart = this.chart, - column = [], - nodePadding = this.options.nodePadding; - - column.sum = function () { - var sum = 0; - each(this, function (node) { - sum += node.getSum(); - }); - return sum; - }; - /** - * Get the offset in pixels of a node inside the column. - */ - column.offset = function (node, factor) { - var offset = 0; - for (var i = 0; i < column.length; i++) { - if (column[i] === node) { - return offset + (node.options.offset || 0); - } - offset += column[i].getSum() * factor + nodePadding; - } - }; - - /** - * Get the column height in pixels. - */ - column.top = function (factor) { - var height = 0; - for (var i = 0; i < column.length; i++) { - if (i > 0) { - height += nodePadding; - } - height += column[i].getSum() * factor; - } - return (chart.plotSizeY - height) / 2; - }; - - return column; - }, - - /** - * Create node columns by analyzing the nodes and the relations between - * incoming and outgoing links. - */ - createNodeColumns: function () { - var columns = []; - each(this.nodes, function (node) { - var fromColumn = 0, - i, - point; - - if (!H.defined(node.options.column)) { - // No links to this node, place it left - if (node.linksTo.length === 0) { - node.column = 0; - - // There are incoming links, place it to the right of the - // highest order column that links to this one. - } else { - for (i = 0; i < node.linksTo.length; i++) { - point = node.linksTo[0]; - if (point.fromNode.column > fromColumn) { - fromColumn = point.fromNode.column; - } - } - node.column = fromColumn + 1; - } - } - - if (!columns[node.column]) { - columns[node.column] = this.createNodeColumn(); - } - - columns[node.column].push(node); - - }, this); - return columns; - }, - - /*= if (build.classic) { =*/ - /** - * Return the presentational attributes. - */ - pointAttribs: function (point, state) { - - var opacity = this.options.linkOpacity; - - if (state) { - opacity = this.options.states[state].linkOpacity || opacity; - } - - return { - fill: point.isNode ? - point.color : - H.color(point.color).setOpacity(opacity).get() - }; - }, - /*= } =*/ - - /** - * Extend generatePoints by adding the nodes, which are Point objects - * but pushed to the this.nodes array. - */ - generatePoints: function () { - - var nodeLookup = {}; - - H.Series.prototype.generatePoints.call(this); - - if (!this.nodes) { - this.nodes = []; // List of Point-like node items - } - this.colorCounter = 0; - - // Reset links from previous run - each(this.nodes, function (node) { - node.linksFrom.length = 0; - node.linksTo.length = 0; - }); - - // Create the node list and set up links - each(this.points, function (point) { - if (defined(point.from)) { - if (!nodeLookup[point.from]) { - nodeLookup[point.from] = this.createNode(point.from); - } - nodeLookup[point.from].linksFrom.push(point); - point.fromNode = nodeLookup[point.from]; - - // Point color defaults to the fromNode's color - /*= if (build.classic) { =*/ - point.color = - point.options.color || nodeLookup[point.from].color; - /*= } else { =*/ - point.colorIndex = pick( - point.options.colorIndex, - nodeLookup[point.from].colorIndex - ); - /*= } =*/ - - } - if (defined(point.to)) { - if (!nodeLookup[point.to]) { - nodeLookup[point.to] = this.createNode(point.to); - } - nodeLookup[point.to].linksTo.push(point); - point.toNode = nodeLookup[point.to]; - } - - point.name = point.name || point.id; // for use in formats - - }, this); - }, - - /** - * Run pre-translation by generating the nodeColumns. - */ - translate: function () { - if (!this.processedXData) { - this.processData(); - } - this.generatePoints(); - - this.nodeColumns = this.createNodeColumns(); - - var chart = this.chart, - inverted = chart.inverted, - options = this.options, - left = 0, - nodeWidth = options.nodeWidth, - nodeColumns = this.nodeColumns, - colDistance = (chart.plotSizeX - nodeWidth) / - (nodeColumns.length - 1), - curvy = ( - (inverted ? -colDistance : colDistance) * - options.curveFactor - ), - factor = Infinity; - - // Find out how much space is needed. Base it on the translation - // factor of the most spaceous column. - each(this.nodeColumns, function (column) { - var height = chart.plotSizeY - - (column.length - 1) * options.nodePadding; - - factor = Math.min(factor, height / column.sum()); - }); - - each(this.nodeColumns, function (column) { - each(column, function (node) { - var sum = node.getSum(), - height = sum * factor, - fromNodeTop = ( - column.top(factor) + - column.offset(node, factor) - ), - nodeLeft = inverted ? - chart.plotSizeX - left : - left; - - node.sum = sum; - - // Draw the node - node.shapeType = 'rect'; - if (!inverted) { - node.shapeArgs = { - x: nodeLeft, - y: fromNodeTop, - width: nodeWidth, - height: height - }; - } else { - node.shapeArgs = { - x: nodeLeft - nodeWidth, - y: chart.plotSizeY - fromNodeTop - height, - width: nodeWidth, - height: height - }; - } - node.shapeArgs.display = node.hasShape() ? '' : 'none'; - - // Pass test in drawPoints - node.plotY = 1; - - // Draw the links from this node - each(node.linksFrom, function (point) { - var linkHeight = point.weight * factor, - fromLinkTop = node.offset(point, 'linksFrom') * - factor, - fromY = fromNodeTop + fromLinkTop, - toNode = point.toNode, - toColTop = nodeColumns[toNode.column].top(factor), - toY = ( - toColTop + - (toNode.offset(point, 'linksTo') * factor) + - nodeColumns[toNode.column].offset( - toNode, - factor - ) - ), - nodeW = nodeWidth, - right = toNode.column * colDistance, - outgoing = point.outgoing; - - if (inverted) { - fromY = chart.plotSizeY - fromY; - toY = chart.plotSizeY - toY; - right = chart.plotSizeX - right; - nodeW = -nodeW; - linkHeight = -linkHeight; - } - - point.shapeType = 'path'; - point.shapeArgs = { - d: [ - 'M', nodeLeft + nodeW, fromY, - 'C', nodeLeft + nodeW + curvy, fromY, - right - curvy, toY, - right, toY, - 'L', - right + (outgoing ? nodeW : 0), - toY + linkHeight / 2, - 'L', - right, - toY + linkHeight, - 'C', right - curvy, toY + linkHeight, - nodeLeft + nodeW + curvy, fromY + linkHeight, - nodeLeft + nodeW, fromY + linkHeight, - 'z' - ] - }; - - // Place data labels in the middle - point.dlBox = { - x: nodeLeft + (right - nodeLeft + nodeW) / 2, - y: fromY + (toY - fromY) / 2, - height: linkHeight, - width: 0 - }; - // Pass test in drawPoints - point.y = point.plotY = 1; - - if (!point.color) { - point.color = node.color; - } - }); - }); - left += colDistance; - - }, this); - }, - /** - * Extend the render function to also render this.nodes together with - * the points. - */ - render: function () { - var points = this.points; - this.points = this.points.concat(this.nodes); - H.seriesTypes.column.prototype.render.call(this); - this.points = points; - }, - animate: H.Series.prototype.animate + isCartesian: false, + forceDL: true, + /** + * Create a single node that holds information on incoming and outgoing + * links. + */ + createNode: function (id) { + + function findById(nodes, id) { + return H.find(nodes, function (node) { + return node.id === id; + }); + } + + var node = findById(this.nodes, id), + options; + + if (!node) { + options = this.options.nodes && findById(this.options.nodes, id); + node = (new Point()).init( + this, + extend({ + className: 'highcharts-node', + isNode: true, + id: id, + y: 1 // Pass isNull test + }, options) + ); + node.linksTo = []; + node.linksFrom = []; + node.formatPrefix = 'node'; + node.name = node.name || node.id; // for use in formats + + /** + * Return the largest sum of either the incoming or outgoing links. + */ + node.getSum = function () { + var sumTo = 0, + sumFrom = 0; + each(node.linksTo, function (link) { + sumTo += link.weight; + }); + each(node.linksFrom, function (link) { + sumFrom += link.weight; + }); + return Math.max(sumTo, sumFrom); + }; + /** + * Get the offset in weight values of a point/link. + */ + node.offset = function (point, coll) { + var offset = 0; + for (var i = 0; i < node[coll].length; i++) { + if (node[coll][i] === point) { + return offset; + } + offset += node[coll][i].weight; + } + }; + + /** + * Return true if the node has a shape, otherwise all links are + * outgoing. + */ + node.hasShape = function () { + var outgoing = 0; + each(node.linksTo, function (link) { + if (link.outgoing) { + outgoing++; + } + }); + return !node.linksTo.length || outgoing !== node.linksTo.length; + }; + + this.nodes.push(node); + } + return node; + }, + + /** + * Create a node column. + */ + createNodeColumn: function () { + var chart = this.chart, + column = [], + nodePadding = this.options.nodePadding; + + column.sum = function () { + var sum = 0; + each(this, function (node) { + sum += node.getSum(); + }); + return sum; + }; + /** + * Get the offset in pixels of a node inside the column. + */ + column.offset = function (node, factor) { + var offset = 0; + for (var i = 0; i < column.length; i++) { + if (column[i] === node) { + return offset + (node.options.offset || 0); + } + offset += column[i].getSum() * factor + nodePadding; + } + }; + + /** + * Get the column height in pixels. + */ + column.top = function (factor) { + var height = 0; + for (var i = 0; i < column.length; i++) { + if (i > 0) { + height += nodePadding; + } + height += column[i].getSum() * factor; + } + return (chart.plotSizeY - height) / 2; + }; + + return column; + }, + + /** + * Create node columns by analyzing the nodes and the relations between + * incoming and outgoing links. + */ + createNodeColumns: function () { + var columns = []; + each(this.nodes, function (node) { + var fromColumn = 0, + i, + point; + + if (!H.defined(node.options.column)) { + // No links to this node, place it left + if (node.linksTo.length === 0) { + node.column = 0; + + // There are incoming links, place it to the right of the + // highest order column that links to this one. + } else { + for (i = 0; i < node.linksTo.length; i++) { + point = node.linksTo[0]; + if (point.fromNode.column > fromColumn) { + fromColumn = point.fromNode.column; + } + } + node.column = fromColumn + 1; + } + } + + if (!columns[node.column]) { + columns[node.column] = this.createNodeColumn(); + } + + columns[node.column].push(node); + + }, this); + return columns; + }, + + /*= if (build.classic) { =*/ + /** + * Return the presentational attributes. + */ + pointAttribs: function (point, state) { + + var opacity = this.options.linkOpacity; + + if (state) { + opacity = this.options.states[state].linkOpacity || opacity; + } + + return { + fill: point.isNode ? + point.color : + H.color(point.color).setOpacity(opacity).get() + }; + }, + /*= } =*/ + + /** + * Extend generatePoints by adding the nodes, which are Point objects + * but pushed to the this.nodes array. + */ + generatePoints: function () { + + var nodeLookup = {}; + + H.Series.prototype.generatePoints.call(this); + + if (!this.nodes) { + this.nodes = []; // List of Point-like node items + } + this.colorCounter = 0; + + // Reset links from previous run + each(this.nodes, function (node) { + node.linksFrom.length = 0; + node.linksTo.length = 0; + }); + + // Create the node list and set up links + each(this.points, function (point) { + if (defined(point.from)) { + if (!nodeLookup[point.from]) { + nodeLookup[point.from] = this.createNode(point.from); + } + nodeLookup[point.from].linksFrom.push(point); + point.fromNode = nodeLookup[point.from]; + + // Point color defaults to the fromNode's color + /*= if (build.classic) { =*/ + point.color = + point.options.color || nodeLookup[point.from].color; + /*= } else { =*/ + point.colorIndex = pick( + point.options.colorIndex, + nodeLookup[point.from].colorIndex + ); + /*= } =*/ + + } + if (defined(point.to)) { + if (!nodeLookup[point.to]) { + nodeLookup[point.to] = this.createNode(point.to); + } + nodeLookup[point.to].linksTo.push(point); + point.toNode = nodeLookup[point.to]; + } + + point.name = point.name || point.id; // for use in formats + + }, this); + }, + + /** + * Run pre-translation by generating the nodeColumns. + */ + translate: function () { + if (!this.processedXData) { + this.processData(); + } + this.generatePoints(); + + this.nodeColumns = this.createNodeColumns(); + + var chart = this.chart, + inverted = chart.inverted, + options = this.options, + left = 0, + nodeWidth = options.nodeWidth, + nodeColumns = this.nodeColumns, + colDistance = (chart.plotSizeX - nodeWidth) / + (nodeColumns.length - 1), + curvy = ( + (inverted ? -colDistance : colDistance) * + options.curveFactor + ), + factor = Infinity; + + // Find out how much space is needed. Base it on the translation + // factor of the most spaceous column. + each(this.nodeColumns, function (column) { + var height = chart.plotSizeY - + (column.length - 1) * options.nodePadding; + + factor = Math.min(factor, height / column.sum()); + }); + + each(this.nodeColumns, function (column) { + each(column, function (node) { + var sum = node.getSum(), + height = sum * factor, + fromNodeTop = ( + column.top(factor) + + column.offset(node, factor) + ), + nodeLeft = inverted ? + chart.plotSizeX - left : + left; + + node.sum = sum; + + // Draw the node + node.shapeType = 'rect'; + if (!inverted) { + node.shapeArgs = { + x: nodeLeft, + y: fromNodeTop, + width: nodeWidth, + height: height + }; + } else { + node.shapeArgs = { + x: nodeLeft - nodeWidth, + y: chart.plotSizeY - fromNodeTop - height, + width: nodeWidth, + height: height + }; + } + node.shapeArgs.display = node.hasShape() ? '' : 'none'; + + // Pass test in drawPoints + node.plotY = 1; + + // Draw the links from this node + each(node.linksFrom, function (point) { + var linkHeight = point.weight * factor, + fromLinkTop = node.offset(point, 'linksFrom') * + factor, + fromY = fromNodeTop + fromLinkTop, + toNode = point.toNode, + toColTop = nodeColumns[toNode.column].top(factor), + toY = ( + toColTop + + (toNode.offset(point, 'linksTo') * factor) + + nodeColumns[toNode.column].offset( + toNode, + factor + ) + ), + nodeW = nodeWidth, + right = toNode.column * colDistance, + outgoing = point.outgoing; + + if (inverted) { + fromY = chart.plotSizeY - fromY; + toY = chart.plotSizeY - toY; + right = chart.plotSizeX - right; + nodeW = -nodeW; + linkHeight = -linkHeight; + } + + point.shapeType = 'path'; + point.shapeArgs = { + d: [ + 'M', nodeLeft + nodeW, fromY, + 'C', nodeLeft + nodeW + curvy, fromY, + right - curvy, toY, + right, toY, + 'L', + right + (outgoing ? nodeW : 0), + toY + linkHeight / 2, + 'L', + right, + toY + linkHeight, + 'C', right - curvy, toY + linkHeight, + nodeLeft + nodeW + curvy, fromY + linkHeight, + nodeLeft + nodeW, fromY + linkHeight, + 'z' + ] + }; + + // Place data labels in the middle + point.dlBox = { + x: nodeLeft + (right - nodeLeft + nodeW) / 2, + y: fromY + (toY - fromY) / 2, + height: linkHeight, + width: 0 + }; + // Pass test in drawPoints + point.y = point.plotY = 1; + + if (!point.color) { + point.color = node.color; + } + }); + }); + left += colDistance; + + }, this); + }, + /** + * Extend the render function to also render this.nodes together with + * the points. + */ + render: function () { + var points = this.points; + this.points = this.points.concat(this.nodes); + H.seriesTypes.column.prototype.render.call(this); + this.points = points; + }, + animate: H.Series.prototype.animate }, { - getClassName: function () { - return 'highcharts-link ' + Point.prototype.getClassName.call(this); - }, - isValid: function () { - return this.isNode || typeof this.weight === 'number'; - } + getClassName: function () { + return 'highcharts-link ' + Point.prototype.getClassName.call(this); + }, + isValid: function () { + return this.isNode || typeof this.weight === 'number'; + } }); /** * A `sankey` series. If the [type](#series.sankey.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.sankey * @excluding animationLimit,boostThreshold,borderColor,borderRadius, @@ -568,7 +568,7 @@ seriesType('sankey', 'column', { /** - * A collection of options for the individual nodes. The nodes in a sankey + * A collection of options for the individual nodes. The nodes in a sankey * diagram are auto-generated instances of `Highcharts.Point`, but options can * be applied here and linked by the `id`. * @@ -646,12 +646,12 @@ seriesType('sankey', 'column', { /** * An array of data points for the series. For the `sankey` series type, * points can be given in the following way: - * + * * 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.area.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * from: 'Category1', @@ -663,7 +663,7 @@ seriesType('sankey', 'column', { * weight: 5 * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding drilldown,marker,x,y @@ -686,7 +686,7 @@ seriesType('sankey', 'column', { * as the node it extends from. The `series.fillOpacity` option also applies to * the points, so when setting a specific link color, consider setting the * `fillOpacity` to 1. - * + * * @type {String} * @product highcharts * @apioption series.sankey.data.color @@ -694,7 +694,7 @@ seriesType('sankey', 'column', { /** * The node that the link runs from. - * + * * @type {String} * @product highcharts * @apioption series.sankey.data.from @@ -702,7 +702,7 @@ seriesType('sankey', 'column', { /** * The node that the link runs to. - * + * * @type {String} * @product highcharts * @apioption series.sankey.data.to @@ -710,7 +710,7 @@ seriesType('sankey', 'column', { /** * Whether the link goes out of the system. - * + * * @type {Boolean} * @default false * @sample highcharts/plotoptions/sankey-outgoing @@ -721,7 +721,7 @@ seriesType('sankey', 'column', { /** * The weight of the link. - * + * * @type {Number} * @product highcharts * @apioption series.sankey.data.weight diff --git a/js/modules/screen-reader.src.js b/js/modules/screen-reader.src.js index 876829b9102..51b4231981b 100644 --- a/js/modules/screen-reader.src.js +++ b/js/modules/screen-reader.src.js @@ -15,32 +15,32 @@ import '../parts/Series.js'; import '../parts/Point.js'; var win = H.win, - doc = win.document, - each = H.each, - map = H.map, - erase = H.erase, - addEvent = H.addEvent, - merge = H.merge, - // CSS style to hide element from visual users while still exposing it to - // screen readers - hiddenStyle = { - position: 'absolute', - left: '-9999px', - top: 'auto', - width: '1px', - height: '1px', - overflow: 'hidden' - }; + doc = win.document, + each = H.each, + map = H.map, + erase = H.erase, + addEvent = H.addEvent, + merge = H.merge, + // CSS style to hide element from visual users while still exposing it to + // screen readers + hiddenStyle = { + position: 'absolute', + left: '-9999px', + top: 'auto', + width: '1px', + height: '1px', + overflow: 'hidden' + }; // If a point has one of the special keys defined, we expose all keys to the // screen reader. H.Series.prototype.commonKeys = ['name', 'id', 'category', 'x', 'value', 'y']; H.Series.prototype.specialKeys = [ - 'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close' -]; + 'z', 'open', 'high', 'q3', 'median', 'q1', 'low', 'close' +]; if (H.seriesTypes.pie) { - // A pie is always simple. Don't quote me on that. - H.seriesTypes.pie.prototype.specialKeys = []; + // A pie is always simple. Don't quote me on that. + H.seriesTypes.pie.prototype.specialKeys = []; } @@ -50,13 +50,13 @@ if (H.seriesTypes.pie) { * @return {string} The excaped string */ function htmlencode(html) { - return html - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\//g, '/'); + return html + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); } @@ -67,7 +67,7 @@ function htmlencode(html) { * @return {String} The filtered string */ function stripTags(s) { - return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s; + return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s; } @@ -76,188 +76,188 @@ function stripTags(s) { */ H.setOptions({ - /** - * Options for configuring accessibility for the chart. Requires the - * [accessibility module](//code.highcharts.com/modules/accessibility. - * js) to be loaded. For a description of the module and information - * on its features, see [Highcharts Accessibility](http://www.highcharts. - * com/docs/chart-concepts/accessibility). - * - * @since 5.0.0 - * @type {Object} - * @optionparent accessibility - */ - accessibility: { - - /** - * Whether or not to add series descriptions to charts with a single - * series. - * - * @type {Boolean} - * @default false - * @since 5.0.0 - * @apioption accessibility.describeSingleSeries - */ - - /** - * Function to run upon clicking the "View as Data Table" link in the - * screen reader region. - * - * By default Highcharts will insert and set focus to a data table - * representation of the chart. - * - * @type {Function} - * @since 5.0.0 - * @apioption accessibility.onTableAnchorClick - */ - - /** - * Date format to use for points on datetime axes when describing them - * to screen reader users. - * - * Defaults to the same format as in tooltip. - * - * For an overview of the replacement codes, see - * [dateFormat](#Highcharts.dateFormat). - * - * @type {String} - * @see [pointDateFormatter](#accessibility.pointDateFormatter) - * @since 5.0.0 - * @apioption accessibility.pointDateFormat - */ - - /** - * Formatter function to determine the date/time format used with - * points on datetime axes when describing them to screen reader users. - * Receives one argument, `point`, referring to the point to describe. - * Should return a date format string compatible with - * [dateFormat](#Highcharts.dateFormat). - * - * @type {Function} - * @see [pointDateFormat](#accessibility.pointDateFormat) - * @since 5.0.0 - * @apioption accessibility.pointDateFormatter - */ - - /** - * Formatter function to use instead of the default for point - * descriptions. - * Receives one argument, `point`, referring to the point to describe. - * Should return a String with the description of the point for a screen - * reader user. - * - * @type {Function} - * @see [point.description](#series.line.data.description) - * @since 5.0.0 - * @apioption accessibility.pointDescriptionFormatter - */ - - /** - * Formatter function to use instead of the default for series - * descriptions. Receives one argument, `series`, referring to the - * series to describe. Should return a String with the description of - * the series for a screen reader user. - * - * @type {Function} - * @see [series.description](#plotOptions.series.description) - * @since 5.0.0 - * @apioption accessibility.seriesDescriptionFormatter - */ - - /** - * Enable accessibility features for the chart. - * - * @type {Boolean} - * @default true - * @since 5.0.0 - */ - enabled: true, - - /** - * When a series contains more points than this, we no longer expose - * information about individual points to screen readers. - * - * Set to `false` to disable. - * - * @type {Number|Boolean} - * @since 5.0.0 - */ - pointDescriptionThreshold: false, // set to false to disable - - /** - * A formatter function to create the HTML contents of the hidden screen - * reader information region. Receives one argument, `chart`, referring - * to the chart object. Should return a String with the HTML content - * of the region. - * - * The link to view the chart as a data table will be added - * automatically after the custom HTML content. - * - * @type {Function} - * @default undefined - * @since 5.0.0 - */ - screenReaderSectionFormatter: function (chart) { - var options = chart.options, - chartTypes = chart.types || [], - formatContext = { - chart: chart, - numSeries: chart.series && chart.series.length - }, - // Build axis info - but not for pies and maps. Consider not - // adding for certain other types as well (funnel, pyramid?) - axesDesc = ( - chartTypes.length === 1 && chartTypes[0] === 'pie' || - chartTypes[0] === 'map' - ) && {} || chart.getAxesDescription(); - - return '
' + chart.langFormat( - 'accessibility.navigationHint', formatContext - ) + '

' + - ( - options.title.text ? - htmlencode(options.title.text) : - chart.langFormat( - 'accessibility.defaultChartTitle', formatContext - ) - ) + - ( - options.subtitle && options.subtitle.text ? - '. ' + htmlencode(options.subtitle.text) : - '' - ) + - '

' + chart.langFormat( - 'accessibility.longDescriptionHeading', formatContext - ) + '

' + - ( - options.chart.description || chart.langFormat( - 'accessibility.noDescription', formatContext - ) - ) + - '

' + chart.langFormat( - 'accessibility.structureHeading', formatContext - ) + '

' + - ( - options.chart.typeDescription || - chart.getTypeDescription() - ) + '
' + - (axesDesc.xAxis ? ( - '
' + axesDesc.xAxis + '
' - ) : '') + - (axesDesc.yAxis ? ( - '
' + axesDesc.yAxis + '
' - ) : ''); - } - } + /** + * Options for configuring accessibility for the chart. Requires the + * [accessibility module](//code.highcharts.com/modules/accessibility. + * js) to be loaded. For a description of the module and information + * on its features, see [Highcharts Accessibility](http://www.highcharts. + * com/docs/chart-concepts/accessibility). + * + * @since 5.0.0 + * @type {Object} + * @optionparent accessibility + */ + accessibility: { + + /** + * Whether or not to add series descriptions to charts with a single + * series. + * + * @type {Boolean} + * @default false + * @since 5.0.0 + * @apioption accessibility.describeSingleSeries + */ + + /** + * Function to run upon clicking the "View as Data Table" link in the + * screen reader region. + * + * By default Highcharts will insert and set focus to a data table + * representation of the chart. + * + * @type {Function} + * @since 5.0.0 + * @apioption accessibility.onTableAnchorClick + */ + + /** + * Date format to use for points on datetime axes when describing them + * to screen reader users. + * + * Defaults to the same format as in tooltip. + * + * For an overview of the replacement codes, see + * [dateFormat](#Highcharts.dateFormat). + * + * @type {String} + * @see [pointDateFormatter](#accessibility.pointDateFormatter) + * @since 5.0.0 + * @apioption accessibility.pointDateFormat + */ + + /** + * Formatter function to determine the date/time format used with + * points on datetime axes when describing them to screen reader users. + * Receives one argument, `point`, referring to the point to describe. + * Should return a date format string compatible with + * [dateFormat](#Highcharts.dateFormat). + * + * @type {Function} + * @see [pointDateFormat](#accessibility.pointDateFormat) + * @since 5.0.0 + * @apioption accessibility.pointDateFormatter + */ + + /** + * Formatter function to use instead of the default for point + * descriptions. + * Receives one argument, `point`, referring to the point to describe. + * Should return a String with the description of the point for a screen + * reader user. + * + * @type {Function} + * @see [point.description](#series.line.data.description) + * @since 5.0.0 + * @apioption accessibility.pointDescriptionFormatter + */ + + /** + * Formatter function to use instead of the default for series + * descriptions. Receives one argument, `series`, referring to the + * series to describe. Should return a String with the description of + * the series for a screen reader user. + * + * @type {Function} + * @see [series.description](#plotOptions.series.description) + * @since 5.0.0 + * @apioption accessibility.seriesDescriptionFormatter + */ + + /** + * Enable accessibility features for the chart. + * + * @type {Boolean} + * @default true + * @since 5.0.0 + */ + enabled: true, + + /** + * When a series contains more points than this, we no longer expose + * information about individual points to screen readers. + * + * Set to `false` to disable. + * + * @type {Number|Boolean} + * @since 5.0.0 + */ + pointDescriptionThreshold: false, // set to false to disable + + /** + * A formatter function to create the HTML contents of the hidden screen + * reader information region. Receives one argument, `chart`, referring + * to the chart object. Should return a String with the HTML content + * of the region. + * + * The link to view the chart as a data table will be added + * automatically after the custom HTML content. + * + * @type {Function} + * @default undefined + * @since 5.0.0 + */ + screenReaderSectionFormatter: function (chart) { + var options = chart.options, + chartTypes = chart.types || [], + formatContext = { + chart: chart, + numSeries: chart.series && chart.series.length + }, + // Build axis info - but not for pies and maps. Consider not + // adding for certain other types as well (funnel, pyramid?) + axesDesc = ( + chartTypes.length === 1 && chartTypes[0] === 'pie' || + chartTypes[0] === 'map' + ) && {} || chart.getAxesDescription(); + + return '
' + chart.langFormat( + 'accessibility.navigationHint', formatContext + ) + '

' + + ( + options.title.text ? + htmlencode(options.title.text) : + chart.langFormat( + 'accessibility.defaultChartTitle', formatContext + ) + ) + + ( + options.subtitle && options.subtitle.text ? + '. ' + htmlencode(options.subtitle.text) : + '' + ) + + '

' + chart.langFormat( + 'accessibility.longDescriptionHeading', formatContext + ) + '

' + + ( + options.chart.description || chart.langFormat( + 'accessibility.noDescription', formatContext + ) + ) + + '

' + chart.langFormat( + 'accessibility.structureHeading', formatContext + ) + '

' + + ( + options.chart.typeDescription || + chart.getTypeDescription() + ) + '
' + + (axesDesc.xAxis ? ( + '
' + axesDesc.xAxis + '
' + ) : '') + + (axesDesc.yAxis ? ( + '
' + axesDesc.yAxis + '
' + ) : ''); + } + } }); /** * A text description of the chart. - * + * * If the Accessibility module is loaded, this is included by default * as a long description of the chart and its contents in the hidden * screen reader information region. - * + * * @type {String} * @see [typeDescription](#chart.typeDescription) * @default undefined @@ -267,15 +267,15 @@ H.setOptions({ /** * A text description of the chart type. - * + * * If the Accessibility module is loaded, this will be included in the * description of the chart in the screen reader information region. - * - * + * + * * Highcharts will by default attempt to guess the chart type, but for * more complex charts it is recommended to specify this property for * clarity. - * + * * @type {String} * @default undefined * @since 5.0.0 @@ -285,239 +285,239 @@ H.setOptions({ // Utility function. Reverses child nodes of a DOM element function reverseChildNodes(node) { - var i = node.childNodes.length; - while (i--) { - node.appendChild(node.childNodes[i]); - } + var i = node.childNodes.length; + while (i--) { + node.appendChild(node.childNodes[i]); + } } // Whenever drawing series, put info on DOM elements H.addEvent(H.Series, 'afterRender', function () { - if (this.chart.options.accessibility.enabled) { - this.setA11yDescription(); - } + if (this.chart.options.accessibility.enabled) { + this.setA11yDescription(); + } }); // Put accessible info on series and points of a series H.Series.prototype.setA11yDescription = function () { - var a11yOptions = this.chart.options.accessibility, - firstPointEl = ( - this.points && - this.points.length && - this.points[0].graphic && - this.points[0].graphic.element - ), - seriesEl = ( - firstPointEl && - firstPointEl.parentNode || this.graph && - this.graph.element || this.group && - this.group.element - ); // Could be tracker series depending on series type - - if (seriesEl) { - // For some series types the order of elements do not match the order of - // points in series. In that case we have to reverse them in order for - // AT to read them out in an understandable order - if (seriesEl.lastChild === firstPointEl) { - reverseChildNodes(seriesEl); - } - // Make individual point elements accessible if possible. Note: If - // markers are disabled there might not be any elements there to make - // accessible. - if ( - this.points && ( - this.points.length < a11yOptions.pointDescriptionThreshold || - a11yOptions.pointDescriptionThreshold === false - ) - ) { - each(this.points, function (point) { - if (point.graphic) { - point.graphic.element.setAttribute('role', 'img'); - point.graphic.element.setAttribute('tabindex', '-1'); - point.graphic.element.setAttribute('aria-label', stripTags( - point.series.options.pointDescriptionFormatter && - point.series.options.pointDescriptionFormatter(point) || - a11yOptions.pointDescriptionFormatter && - a11yOptions.pointDescriptionFormatter(point) || - point.buildPointInfoString() - )); - } - }); - } - // Make series element accessible - if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) { - seriesEl.setAttribute( - 'role', - this.options.exposeElementToA11y ? 'img' : 'region' - ); - seriesEl.setAttribute('tabindex', '-1'); - seriesEl.setAttribute( - 'aria-label', - stripTags( - a11yOptions.seriesDescriptionFormatter && - a11yOptions.seriesDescriptionFormatter(this) || - this.buildSeriesInfoString() - ) - ); - } - } + var a11yOptions = this.chart.options.accessibility, + firstPointEl = ( + this.points && + this.points.length && + this.points[0].graphic && + this.points[0].graphic.element + ), + seriesEl = ( + firstPointEl && + firstPointEl.parentNode || this.graph && + this.graph.element || this.group && + this.group.element + ); // Could be tracker series depending on series type + + if (seriesEl) { + // For some series types the order of elements do not match the order of + // points in series. In that case we have to reverse them in order for + // AT to read them out in an understandable order + if (seriesEl.lastChild === firstPointEl) { + reverseChildNodes(seriesEl); + } + // Make individual point elements accessible if possible. Note: If + // markers are disabled there might not be any elements there to make + // accessible. + if ( + this.points && ( + this.points.length < a11yOptions.pointDescriptionThreshold || + a11yOptions.pointDescriptionThreshold === false + ) + ) { + each(this.points, function (point) { + if (point.graphic) { + point.graphic.element.setAttribute('role', 'img'); + point.graphic.element.setAttribute('tabindex', '-1'); + point.graphic.element.setAttribute('aria-label', stripTags( + point.series.options.pointDescriptionFormatter && + point.series.options.pointDescriptionFormatter(point) || + a11yOptions.pointDescriptionFormatter && + a11yOptions.pointDescriptionFormatter(point) || + point.buildPointInfoString() + )); + } + }); + } + // Make series element accessible + if (this.chart.series.length > 1 || a11yOptions.describeSingleSeries) { + seriesEl.setAttribute( + 'role', + this.options.exposeElementToA11y ? 'img' : 'region' + ); + seriesEl.setAttribute('tabindex', '-1'); + seriesEl.setAttribute( + 'aria-label', + stripTags( + a11yOptions.seriesDescriptionFormatter && + a11yOptions.seriesDescriptionFormatter(this) || + this.buildSeriesInfoString() + ) + ); + } + } }; // Return string with information about series H.Series.prototype.buildSeriesInfoString = function () { - var chart = this.chart, - desc = this.description || this.options.description, - description = desc && chart.langFormat( - 'accessibility.series.description', { - description: desc, - series: this - } - ), - xAxisInfo = chart.langFormat( - 'accessibility.series.xAxisDescription', - { - name: this.xAxis && this.xAxis.getDescription(), - series: this - } - ), - yAxisInfo = chart.langFormat( - 'accessibility.series.yAxisDescription', - { - name: this.yAxis && this.yAxis.getDescription(), - series: this - } - ), - summaryContext = { - name: this.name || '', - ix: this.index + 1, - numSeries: chart.series.length, - numPoints: this.points.length, - series: this - }, - combination = chart.types.length === 1 ? '' : 'Combination', - summary = chart.langFormat( - 'accessibility.series.summary.' + this.type + combination, - summaryContext - ) || chart.langFormat( - 'accessibility.series.summary.default' + combination, - summaryContext - ); - - return summary + (description ? ' ' + description : '') + ( - chart.yAxis.length > 1 && this.yAxis ? - ' ' + yAxisInfo : '' - ) + ( - chart.xAxis.length > 1 && this.xAxis ? - ' ' + xAxisInfo : '' - ); + var chart = this.chart, + desc = this.description || this.options.description, + description = desc && chart.langFormat( + 'accessibility.series.description', { + description: desc, + series: this + } + ), + xAxisInfo = chart.langFormat( + 'accessibility.series.xAxisDescription', + { + name: this.xAxis && this.xAxis.getDescription(), + series: this + } + ), + yAxisInfo = chart.langFormat( + 'accessibility.series.yAxisDescription', + { + name: this.yAxis && this.yAxis.getDescription(), + series: this + } + ), + summaryContext = { + name: this.name || '', + ix: this.index + 1, + numSeries: chart.series.length, + numPoints: this.points.length, + series: this + }, + combination = chart.types.length === 1 ? '' : 'Combination', + summary = chart.langFormat( + 'accessibility.series.summary.' + this.type + combination, + summaryContext + ) || chart.langFormat( + 'accessibility.series.summary.default' + combination, + summaryContext + ); + + return summary + (description ? ' ' + description : '') + ( + chart.yAxis.length > 1 && this.yAxis ? + ' ' + yAxisInfo : '' + ) + ( + chart.xAxis.length > 1 && this.xAxis ? + ' ' + xAxisInfo : '' + ); }; // Return string with information about point H.Point.prototype.buildPointInfoString = function () { - var point = this, - series = point.series, - a11yOptions = series.chart.options.accessibility, - infoString = '', - dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis, - timeDesc = - dateTimePoint && - series.chart.time.dateFormat( - a11yOptions.pointDateFormatter && - a11yOptions.pointDateFormatter(point) || - a11yOptions.pointDateFormat || - H.Tooltip.prototype.getXDateFormat.call( - { - getDateFormat: H.Tooltip.prototype.getDateFormat, - chart: series.chart - }, - point, - series.chart.options.tooltip, - series.xAxis - ), - point.x - ), - hasSpecialKey = H.find(series.specialKeys, function (key) { - return point[key] !== undefined; - }); - - // If the point has one of the less common properties defined, display all - // that are defined - if (hasSpecialKey) { - if (dateTimePoint) { - infoString = timeDesc; - } - each(series.commonKeys.concat(series.specialKeys), function (key) { - if (point[key] !== undefined && !(dateTimePoint && key === 'x')) { - infoString += (infoString ? '. ' : '') + - key + ', ' + - point[key]; - } - }); - } else { - // Pick and choose properties for a succint label - infoString = - ( - this.name || - timeDesc || - this.category || - this.id || - 'x, ' + this.x - ) + ', ' + - (this.value !== undefined ? this.value : this.y); - } - - return (this.index + 1) + '. ' + infoString + '.' + - (this.description ? ' ' + this.description : ''); + var point = this, + series = point.series, + a11yOptions = series.chart.options.accessibility, + infoString = '', + dateTimePoint = series.xAxis && series.xAxis.isDatetimeAxis, + timeDesc = + dateTimePoint && + series.chart.time.dateFormat( + a11yOptions.pointDateFormatter && + a11yOptions.pointDateFormatter(point) || + a11yOptions.pointDateFormat || + H.Tooltip.prototype.getXDateFormat.call( + { + getDateFormat: H.Tooltip.prototype.getDateFormat, + chart: series.chart + }, + point, + series.chart.options.tooltip, + series.xAxis + ), + point.x + ), + hasSpecialKey = H.find(series.specialKeys, function (key) { + return point[key] !== undefined; + }); + + // If the point has one of the less common properties defined, display all + // that are defined + if (hasSpecialKey) { + if (dateTimePoint) { + infoString = timeDesc; + } + each(series.commonKeys.concat(series.specialKeys), function (key) { + if (point[key] !== undefined && !(dateTimePoint && key === 'x')) { + infoString += (infoString ? '. ' : '') + + key + ', ' + + point[key]; + } + }); + } else { + // Pick and choose properties for a succint label + infoString = + ( + this.name || + timeDesc || + this.category || + this.id || + 'x, ' + this.x + ) + ', ' + + (this.value !== undefined ? this.value : this.y); + } + + return (this.index + 1) + '. ' + infoString + '.' + + (this.description ? ' ' + this.description : ''); }; // Get descriptive label for axis H.Axis.prototype.getDescription = function () { - return ( - this.userOptions && this.userOptions.description || - this.axisTitle && this.axisTitle.textStr || - this.options.id || - this.categories && 'categories' || - this.isDatetimeAxis && 'Time' || - 'values' - ); + return ( + this.userOptions && this.userOptions.description || + this.axisTitle && this.axisTitle.textStr || + this.options.id || + this.categories && 'categories' || + this.isDatetimeAxis && 'Time' || + 'values' + ); }; // Whenever adding or removing series, keep track of types present in chart addEvent(H.Series, 'afterInit', function () { - var chart = this.chart; - if (chart.options.accessibility.enabled) { - chart.types = chart.types || []; - - // Add type to list if does not exist - if (chart.types.indexOf(this.type) < 0) { - chart.types.push(this.type); - } - } + var chart = this.chart; + if (chart.options.accessibility.enabled) { + chart.types = chart.types || []; + + // Add type to list if does not exist + if (chart.types.indexOf(this.type) < 0) { + chart.types.push(this.type); + } + } }); addEvent(H.Series, 'remove', function () { - var chart = this.chart, - removedSeries = this, - hasType = false; - - // Check if any of the other series have the same type as this one. - // Otherwise remove it from the list. - each(chart.series, function (s) { - if ( - s !== removedSeries && - chart.types.indexOf(removedSeries.type) < 0 - ) { - hasType = true; - } - }); - if (!hasType) { - erase(chart.types, removedSeries.type); - } + var chart = this.chart, + removedSeries = this, + hasType = false; + + // Check if any of the other series have the same type as this one. + // Otherwise remove it from the list. + each(chart.series, function (s) { + if ( + s !== removedSeries && + chart.types.indexOf(removedSeries.type) < 0 + ) { + hasType = true; + } + }); + if (!hasType) { + erase(chart.types, removedSeries.type); + } }); @@ -525,116 +525,116 @@ addEvent(H.Series, 'remove', function () { // to most screen reader users, but in those cases we try to add a description // of the type. H.Chart.prototype.getTypeDescription = function () { - var firstType = this.types && this.types[0], - firstSeries = this.series && this.series[0] || {}, - mapTitle = firstSeries.mapTitle, - typeDesc = this.langFormat( - 'accessibility.seriesTypeDescriptions.' + firstType, - { chart: this } - ), - formatContext = { - numSeries: this.series.length, - numPoints: firstSeries.points && firstSeries.points.length, - chart: this, - mapTitle: mapTitle - }, - multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple'; - - if (!firstType) { - return this.langFormat( - 'accessibility.chartTypes.emptyChart', formatContext - ); - } else if (firstType === 'map') { - return mapTitle ? - this.langFormat( - 'accessibility.chartTypes.mapTypeDescription', - formatContext - ) : - this.langFormat( - 'accessibility.chartTypes.unknownMap', - formatContext - ); - } else if (this.types.length > 1) { - return this.langFormat( - 'accessibility.chartTypes.combinationChart', formatContext - ); - } - - return ( - this.langFormat( - 'accessibility.chartTypes.' + firstType + multi, - formatContext - ) || - this.langFormat( - 'accessibility.chartTypes.default' + multi, - formatContext - ) - ) + - (typeDesc ? ' ' + typeDesc : ''); + var firstType = this.types && this.types[0], + firstSeries = this.series && this.series[0] || {}, + mapTitle = firstSeries.mapTitle, + typeDesc = this.langFormat( + 'accessibility.seriesTypeDescriptions.' + firstType, + { chart: this } + ), + formatContext = { + numSeries: this.series.length, + numPoints: firstSeries.points && firstSeries.points.length, + chart: this, + mapTitle: mapTitle + }, + multi = this.series && this.series.length === 1 ? 'Single' : 'Multiple'; + + if (!firstType) { + return this.langFormat( + 'accessibility.chartTypes.emptyChart', formatContext + ); + } else if (firstType === 'map') { + return mapTitle ? + this.langFormat( + 'accessibility.chartTypes.mapTypeDescription', + formatContext + ) : + this.langFormat( + 'accessibility.chartTypes.unknownMap', + formatContext + ); + } else if (this.types.length > 1) { + return this.langFormat( + 'accessibility.chartTypes.combinationChart', formatContext + ); + } + + return ( + this.langFormat( + 'accessibility.chartTypes.' + firstType + multi, + formatContext + ) || + this.langFormat( + 'accessibility.chartTypes.default' + multi, + formatContext + ) + ) + + (typeDesc ? ' ' + typeDesc : ''); }; // Return object with text description of each of the chart's axes H.Chart.prototype.getAxesDescription = function () { - var numXAxes = this.xAxis.length, - numYAxes = this.yAxis.length, - desc = {}; - - if (numXAxes) { - desc.xAxis = this.langFormat( - 'accessibility.axis.xAxisDescription' + ( - numXAxes > 1 ? 'Plural' : 'Singular' - ), - { - chart: this, - names: map(this.xAxis, function (axis) { - return axis.getDescription(); - }), - numAxes: numXAxes - } - ); - } - - if (numYAxes) { - desc.yAxis = this.langFormat( - 'accessibility.axis.yAxisDescription' + ( - numYAxes > 1 ? 'Plural' : 'Singular' - ), - { - chart: this, - names: map(this.yAxis, function (axis) { - return axis.getDescription(); - }), - numAxes: numYAxes - } - ); - } - - return desc; + var numXAxes = this.xAxis.length, + numYAxes = this.yAxis.length, + desc = {}; + + if (numXAxes) { + desc.xAxis = this.langFormat( + 'accessibility.axis.xAxisDescription' + ( + numXAxes > 1 ? 'Plural' : 'Singular' + ), + { + chart: this, + names: map(this.xAxis, function (axis) { + return axis.getDescription(); + }), + numAxes: numXAxes + } + ); + } + + if (numYAxes) { + desc.yAxis = this.langFormat( + 'accessibility.axis.yAxisDescription' + ( + numYAxes > 1 ? 'Plural' : 'Singular' + ), + { + chart: this, + names: map(this.yAxis, function (axis) { + return axis.getDescription(); + }), + numAxes: numYAxes + } + ); + } + + return desc; }; // Set a11y attribs on exporting menu -H.Chart.prototype.addAccessibleContextMenuAttribs = function () { - var exportList = this.exportDivElements; - if (exportList) { - // Set tabindex on the menu items to allow focusing by script - // Set role to give screen readers a chance to pick up the contents - each(exportList, function (item) { - if (item.tagName === 'DIV' && - !(item.children && item.children.length)) { - item.setAttribute('role', 'menuitem'); - item.setAttribute('tabindex', -1); - } - }); - // Set accessibility properties on parent div - exportList[0].parentNode.setAttribute('role', 'menu'); - exportList[0].parentNode.setAttribute('aria-label', - this.langFormat( - 'accessibility.exporting.chartMenuLabel', { chart: this } - ) - ); - } +H.Chart.prototype.addAccessibleContextMenuAttribs = function () { + var exportList = this.exportDivElements; + if (exportList) { + // Set tabindex on the menu items to allow focusing by script + // Set role to give screen readers a chance to pick up the contents + each(exportList, function (item) { + if (item.tagName === 'DIV' && + !(item.children && item.children.length)) { + item.setAttribute('role', 'menuitem'); + item.setAttribute('tabindex', -1); + } + }); + // Set accessibility properties on parent div + exportList[0].parentNode.setAttribute('role', 'menu'); + exportList[0].parentNode.setAttribute('aria-label', + this.langFormat( + 'accessibility.exporting.chartMenuLabel', { chart: this } + ) + ); + } }; @@ -642,172 +642,172 @@ H.Chart.prototype.addAccessibleContextMenuAttribs = function () { // tableId is the HTML id of the table to focus when clicking the table anchor // in the screen reader region. H.Chart.prototype.addScreenReaderRegion = function (id, tableId) { - var chart = this, - hiddenSection = chart.screenReaderRegion = doc.createElement('div'), - tableShortcut = doc.createElement('h4'), - tableShortcutAnchor = doc.createElement('a'), - chartHeading = doc.createElement('h4'); - - hiddenSection.setAttribute('id', id); - hiddenSection.setAttribute('role', 'region'); - hiddenSection.setAttribute( - 'aria-label', - chart.langFormat( - 'accessibility.screenReaderRegionLabel', { chart: this } - ) - ); - - hiddenSection.innerHTML = chart.options.accessibility - .screenReaderSectionFormatter(chart); - - // Add shortcut to data table if export-data is loaded - if (chart.getCSV) { - tableShortcutAnchor.innerHTML = chart.langFormat( - 'accessibility.viewAsDataTable', { chart: chart } - ); - tableShortcutAnchor.href = '#' + tableId; - // Make this unreachable by user tabbing - tableShortcutAnchor.setAttribute('tabindex', '-1'); - tableShortcutAnchor.onclick = - chart.options.accessibility.onTableAnchorClick || function () { - chart.viewData(); - doc.getElementById(tableId).focus(); - }; - tableShortcut.appendChild(tableShortcutAnchor); - hiddenSection.appendChild(tableShortcut); - } - - // Note: JAWS seems to refuse to read aria-label on the container, so add an - // h4 element as title for the chart. - chartHeading.innerHTML = chart.langFormat( - 'accessibility.chartHeading', { chart: chart } - ); - chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild); - chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild); - - // Hide the section and the chart heading - merge(true, chartHeading.style, hiddenStyle); - merge(true, hiddenSection.style, hiddenStyle); + var chart = this, + hiddenSection = chart.screenReaderRegion = doc.createElement('div'), + tableShortcut = doc.createElement('h4'), + tableShortcutAnchor = doc.createElement('a'), + chartHeading = doc.createElement('h4'); + + hiddenSection.setAttribute('id', id); + hiddenSection.setAttribute('role', 'region'); + hiddenSection.setAttribute( + 'aria-label', + chart.langFormat( + 'accessibility.screenReaderRegionLabel', { chart: this } + ) + ); + + hiddenSection.innerHTML = chart.options.accessibility + .screenReaderSectionFormatter(chart); + + // Add shortcut to data table if export-data is loaded + if (chart.getCSV) { + tableShortcutAnchor.innerHTML = chart.langFormat( + 'accessibility.viewAsDataTable', { chart: chart } + ); + tableShortcutAnchor.href = '#' + tableId; + // Make this unreachable by user tabbing + tableShortcutAnchor.setAttribute('tabindex', '-1'); + tableShortcutAnchor.onclick = + chart.options.accessibility.onTableAnchorClick || function () { + chart.viewData(); + doc.getElementById(tableId).focus(); + }; + tableShortcut.appendChild(tableShortcutAnchor); + hiddenSection.appendChild(tableShortcut); + } + + // Note: JAWS seems to refuse to read aria-label on the container, so add an + // h4 element as title for the chart. + chartHeading.innerHTML = chart.langFormat( + 'accessibility.chartHeading', { chart: chart } + ); + chart.renderTo.insertBefore(chartHeading, chart.renderTo.firstChild); + chart.renderTo.insertBefore(hiddenSection, chart.renderTo.firstChild); + + // Hide the section and the chart heading + merge(true, chartHeading.style, hiddenStyle); + merge(true, hiddenSection.style, hiddenStyle); }; // Make chart container accessible, and wrap table functionality H.Chart.prototype.callbacks.push(function (chart) { - var options = chart.options, - a11yOptions = options.accessibility; - - if (!a11yOptions.enabled) { - return; - } - - var titleElement, - exportGroupElement = doc.createElementNS( - 'http://www.w3.org/2000/svg', - 'g' - ), - descElement = chart.container.getElementsByTagName('desc')[0], - textElements = chart.container.getElementsByTagName('text'), - titleId = 'highcharts-title-' + chart.index, - tableId = 'highcharts-data-table-' + chart.index, - hiddenSectionId = 'highcharts-information-region-' + chart.index, - chartTitle = options.title.text || chart.langFormat( - 'accessibility.defaultChartTitle', { chart: chart } - ), - svgContainerTitle = stripTags(chart.langFormat( - 'accessibility.svgContainerTitle', { - chartTitle: chartTitle - } - )); - - // Add SVG title tag if it is set - if (svgContainerTitle.length) { - titleElement = doc.createElementNS( - 'http://www.w3.org/2000/svg', - 'title' - ); - titleElement.textContent = svgContainerTitle; - titleElement.id = titleId; - descElement.parentNode.insertBefore(titleElement, descElement); - } - - chart.renderTo.setAttribute('role', 'region'); - chart.renderTo.setAttribute( - 'aria-label', - chart.langFormat( - 'accessibility.chartContainerLabel', - { - title: stripTags(chartTitle), - chart: chart - } - ) - ); - - // Set screen reader properties on export menu - if ( - chart.exportSVGElements && - chart.exportSVGElements[0] && - chart.exportSVGElements[0].element - ) { - var oldExportCallback = chart.exportSVGElements[0].element.onclick, - parent = chart.exportSVGElements[0].element.parentNode; - chart.exportSVGElements[0].element.onclick = function () { - oldExportCallback.apply( - this, - Array.prototype.slice.call(arguments) - ); - chart.addAccessibleContextMenuAttribs(); - chart.highlightExportItem(0); - }; - chart.exportSVGElements[0].element.setAttribute('role', 'button'); - chart.exportSVGElements[0].element.setAttribute( - 'aria-label', - chart.langFormat( - 'accessibility.exporting.menuButtonLabel', { chart: chart } - ) - ); - exportGroupElement.appendChild(chart.exportSVGElements[0].element); - exportGroupElement.setAttribute('role', 'region'); - exportGroupElement.setAttribute('aria-label', chart.langFormat( - 'accessibility.exporting.exportRegionLabel', { chart: chart } - )); - parent.appendChild(exportGroupElement); - } - - // Set screen reader properties on input boxes for range selector. We need - // to do this regardless of whether or not these are visible, as they are - // by default part of the page's tabindex unless we set them to -1. - if (chart.rangeSelector) { - each(['minInput', 'maxInput'], function (key, i) { - if (chart.rangeSelector[key]) { - chart.rangeSelector[key].setAttribute('tabindex', '-1'); - chart.rangeSelector[key].setAttribute('role', 'textbox'); - chart.rangeSelector[key].setAttribute( - 'aria-label', - chart.langFormat( - 'accessibility.rangeSelector' + - (i ? 'MaxInput' : 'MinInput'), { chart: chart } - ) - ); - } - }); - } - - // Hide text elements from screen readers - each(textElements, function (el) { - el.setAttribute('aria-hidden', 'true'); - }); - - // Add top-secret screen reader region - chart.addScreenReaderRegion(hiddenSectionId, tableId); - - // Add ID and summary attr to table HTML - H.wrap(chart, 'getTable', function (proceed) { - return proceed.apply(this, Array.prototype.slice.call(arguments, 1)) - .replace( - '', - '
' - ); - }); + var options = chart.options, + a11yOptions = options.accessibility; + + if (!a11yOptions.enabled) { + return; + } + + var titleElement, + exportGroupElement = doc.createElementNS( + 'http://www.w3.org/2000/svg', + 'g' + ), + descElement = chart.container.getElementsByTagName('desc')[0], + textElements = chart.container.getElementsByTagName('text'), + titleId = 'highcharts-title-' + chart.index, + tableId = 'highcharts-data-table-' + chart.index, + hiddenSectionId = 'highcharts-information-region-' + chart.index, + chartTitle = options.title.text || chart.langFormat( + 'accessibility.defaultChartTitle', { chart: chart } + ), + svgContainerTitle = stripTags(chart.langFormat( + 'accessibility.svgContainerTitle', { + chartTitle: chartTitle + } + )); + + // Add SVG title tag if it is set + if (svgContainerTitle.length) { + titleElement = doc.createElementNS( + 'http://www.w3.org/2000/svg', + 'title' + ); + titleElement.textContent = svgContainerTitle; + titleElement.id = titleId; + descElement.parentNode.insertBefore(titleElement, descElement); + } + + chart.renderTo.setAttribute('role', 'region'); + chart.renderTo.setAttribute( + 'aria-label', + chart.langFormat( + 'accessibility.chartContainerLabel', + { + title: stripTags(chartTitle), + chart: chart + } + ) + ); + + // Set screen reader properties on export menu + if ( + chart.exportSVGElements && + chart.exportSVGElements[0] && + chart.exportSVGElements[0].element + ) { + var oldExportCallback = chart.exportSVGElements[0].element.onclick, + parent = chart.exportSVGElements[0].element.parentNode; + chart.exportSVGElements[0].element.onclick = function () { + oldExportCallback.apply( + this, + Array.prototype.slice.call(arguments) + ); + chart.addAccessibleContextMenuAttribs(); + chart.highlightExportItem(0); + }; + chart.exportSVGElements[0].element.setAttribute('role', 'button'); + chart.exportSVGElements[0].element.setAttribute( + 'aria-label', + chart.langFormat( + 'accessibility.exporting.menuButtonLabel', { chart: chart } + ) + ); + exportGroupElement.appendChild(chart.exportSVGElements[0].element); + exportGroupElement.setAttribute('role', 'region'); + exportGroupElement.setAttribute('aria-label', chart.langFormat( + 'accessibility.exporting.exportRegionLabel', { chart: chart } + )); + parent.appendChild(exportGroupElement); + } + + // Set screen reader properties on input boxes for range selector. We need + // to do this regardless of whether or not these are visible, as they are + // by default part of the page's tabindex unless we set them to -1. + if (chart.rangeSelector) { + each(['minInput', 'maxInput'], function (key, i) { + if (chart.rangeSelector[key]) { + chart.rangeSelector[key].setAttribute('tabindex', '-1'); + chart.rangeSelector[key].setAttribute('role', 'textbox'); + chart.rangeSelector[key].setAttribute( + 'aria-label', + chart.langFormat( + 'accessibility.rangeSelector' + + (i ? 'MaxInput' : 'MinInput'), { chart: chart } + ) + ); + } + }); + } + + // Hide text elements from screen readers + each(textElements, function (el) { + el.setAttribute('aria-hidden', 'true'); + }); + + // Add top-secret screen reader region + chart.addScreenReaderRegion(hiddenSectionId, tableId); + + // Add ID and summary attr to table HTML + H.wrap(chart, 'getTable', function (proceed) { + return proceed.apply(this, Array.prototype.slice.call(arguments, 1)) + .replace( + '
', + '
' + ); + }); }); diff --git a/js/modules/series-label.src.js b/js/modules/series-label.src.js index addfa071eaa..e8880fbcf8f 100644 --- a/js/modules/series-label.src.js +++ b/js/modules/series-label.src.js @@ -10,7 +10,7 @@ * - add column support (box collision detection, boxesToAvoid logic) * - avoid data labels, when data labels above, show series label below. * - add more options (connector, format, formatter) - * + * * http://jsfiddle.net/highcharts/L2u9rpwr/ * http://jsfiddle.net/highcharts/y5A37/ * http://jsfiddle.net/highcharts/264Nm/ @@ -24,174 +24,174 @@ import '../parts/Chart.js'; import '../parts/Series.js'; var labelDistance = 3, - addEvent = H.addEvent, - each = H.each, - extend = H.extend, - isNumber = H.isNumber, - pick = H.pick, - Series = H.Series, - SVGRenderer = H.SVGRenderer, - Chart = H.Chart; + addEvent = H.addEvent, + each = H.each, + extend = H.extend, + isNumber = H.isNumber, + pick = H.pick, + Series = H.Series, + SVGRenderer = H.SVGRenderer, + Chart = H.Chart; H.setOptions({ - /** - * @optionparent plotOptions - */ - plotOptions: { - series: { - /** - * Series labels are placed as close to the series as possible in a - * natural way, seeking to avoid other series. The goal of this - * feature is to make the chart more easily readable, like if a - * human designer placed the labels in the optimal position. - * - * The series labels currently work with series types having a - * `graph` or an `area`. - * - * Requires the `series-label.js` module. - * - * @sample highcharts/series-label/line-chart - * Line chart - * @sample highcharts/demo/streamgraph - * Stream graph - * @sample highcharts/series-label/stock-chart - * Stock chart - * @since 6.0.0 - * @product highcharts highstock - */ - label: { - /** - * Enable the series label per series. - */ - enabled: true, - /** - * Allow labels to be placed distant to the graph if necessary, - * and draw a connector line to the graph. Setting this option - * to true may decrease the performance significantly, since the - * algorithm with systematically search for open spaces in the - * while plot area. Visually, it may also result in a more - * cluttered chart, though more of the series will be labeled. - */ - connectorAllowed: false, - /** - * If the label is closer than this to a neighbour graph, draw a - * connector. - */ - connectorNeighbourDistance: 24, - /** - * For area-like series, allow the font size to vary so that - * small areas get a smaller font size. The default applies this - * effect to area-like series but not line-like series. - * - * @type {Number} - */ - minFontSize: null, - /** - * For area-like series, allow the font size to vary so that - * small areas get a smaller font size. The default applies this - * effect to area-like series but not line-like series. - * - * @type {Number} - */ - maxFontSize: null, - /** - * Draw the label on the area of an area series. By default it - * is drawn on the area. Set it to `false` to draw it next to - * the graph instead. - * - * @type {Boolean} - */ - onArea: null, - - /** - * Styles for the series label. The color defaults to the series - * color, or a contrast color if `onArea`. - */ - style: { - fontWeight: 'bold' - }, - - /** - * An array of boxes to avoid when laying out the labels. Each - * item has a `left`, `right`, `top` and `bottom` property. - * - * @type {Array.} - */ - boxesToAvoid: [] - } - } - } + /** + * @optionparent plotOptions + */ + plotOptions: { + series: { + /** + * Series labels are placed as close to the series as possible in a + * natural way, seeking to avoid other series. The goal of this + * feature is to make the chart more easily readable, like if a + * human designer placed the labels in the optimal position. + * + * The series labels currently work with series types having a + * `graph` or an `area`. + * + * Requires the `series-label.js` module. + * + * @sample highcharts/series-label/line-chart + * Line chart + * @sample highcharts/demo/streamgraph + * Stream graph + * @sample highcharts/series-label/stock-chart + * Stock chart + * @since 6.0.0 + * @product highcharts highstock + */ + label: { + /** + * Enable the series label per series. + */ + enabled: true, + /** + * Allow labels to be placed distant to the graph if necessary, + * and draw a connector line to the graph. Setting this option + * to true may decrease the performance significantly, since the + * algorithm with systematically search for open spaces in the + * while plot area. Visually, it may also result in a more + * cluttered chart, though more of the series will be labeled. + */ + connectorAllowed: false, + /** + * If the label is closer than this to a neighbour graph, draw a + * connector. + */ + connectorNeighbourDistance: 24, + /** + * For area-like series, allow the font size to vary so that + * small areas get a smaller font size. The default applies this + * effect to area-like series but not line-like series. + * + * @type {Number} + */ + minFontSize: null, + /** + * For area-like series, allow the font size to vary so that + * small areas get a smaller font size. The default applies this + * effect to area-like series but not line-like series. + * + * @type {Number} + */ + maxFontSize: null, + /** + * Draw the label on the area of an area series. By default it + * is drawn on the area. Set it to `false` to draw it next to + * the graph instead. + * + * @type {Boolean} + */ + onArea: null, + + /** + * Styles for the series label. The color defaults to the series + * color, or a contrast color if `onArea`. + */ + style: { + fontWeight: 'bold' + }, + + /** + * An array of boxes to avoid when laying out the labels. Each + * item has a `left`, `right`, `top` and `bottom` property. + * + * @type {Array.} + */ + boxesToAvoid: [] + } + } + } }); /** * Counter-clockwise, part of the fast line intersection logic */ function ccw(x1, y1, x2, y2, x3, y3) { - var cw = ((y3 - y1) * (x2 - x1)) - ((y2 - y1) * (x3 - x1)); - return cw > 0 ? true : cw < 0 ? false : true; + var cw = ((y3 - y1) * (x2 - x1)) - ((y2 - y1) * (x3 - x1)); + return cw > 0 ? true : cw < 0 ? false : true; } /** * Detect if two lines intersect */ function intersectLine(x1, y1, x2, y2, x3, y3, x4, y4) { - return ccw(x1, y1, x3, y3, x4, y4) !== ccw(x2, y2, x3, y3, x4, y4) && - ccw(x1, y1, x2, y2, x3, y3) !== ccw(x1, y1, x2, y2, x4, y4); + return ccw(x1, y1, x3, y3, x4, y4) !== ccw(x2, y2, x3, y3, x4, y4) && + ccw(x1, y1, x2, y2, x3, y3) !== ccw(x1, y1, x2, y2, x4, y4); } /** * Detect if a box intersects with a line */ function boxIntersectLine(x, y, w, h, x1, y1, x2, y2) { - return ( - intersectLine(x, y, x + w, y, x1, y1, x2, y2) || // top of label - intersectLine(x + w, y, x + w, y + h, x1, y1, x2, y2) || // right - intersectLine(x, y + h, x + w, y + h, x1, y1, x2, y2) || // bottom - intersectLine(x, y, x, y + h, x1, y1, x2, y2) // left of label - ); + return ( + intersectLine(x, y, x + w, y, x1, y1, x2, y2) || // top of label + intersectLine(x + w, y, x + w, y + h, x1, y1, x2, y2) || // right + intersectLine(x, y + h, x + w, y + h, x1, y1, x2, y2) || // bottom + intersectLine(x, y, x, y + h, x1, y1, x2, y2) // left of label + ); } /** * General symbol definition for labels with connector */ SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) { - var anchorX = options && options.anchorX, - anchorY = options && options.anchorY, - path, - yOffset, - lateral = w / 2; - - if (isNumber(anchorX) && isNumber(anchorY)) { - - path = ['M', anchorX, anchorY]; - - // Prefer 45 deg connectors - yOffset = y - anchorY; - if (yOffset < 0) { - yOffset = -h - yOffset; - } - if (yOffset < w) { - lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset; - } - - // Anchor below label - if (anchorY > y + h) { - path.push('L', x + lateral, y + h); - - // Anchor above label - } else if (anchorY < y) { - path.push('L', x + lateral, y); - - // Anchor left of label - } else if (anchorX < x) { - path.push('L', x, y + h / 2); - - // Anchor right of label - } else if (anchorX > x + w) { - path.push('L', x + w, y + h / 2); - } - } - return path || []; + var anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path, + yOffset, + lateral = w / 2; + + if (isNumber(anchorX) && isNumber(anchorY)) { + + path = ['M', anchorX, anchorY]; + + // Prefer 45 deg connectors + yOffset = y - anchorY; + if (yOffset < 0) { + yOffset = -h - yOffset; + } + if (yOffset < w) { + lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset; + } + + // Anchor below label + if (anchorY > y + h) { + path.push('L', x + lateral, y + h); + + // Anchor above label + } else if (anchorY < y) { + path.push('L', x + lateral, y); + + // Anchor left of label + } else if (anchorX < x) { + path.push('L', x, y + h / 2); + + // Anchor right of label + } else if (anchorX > x + w) { + path.push('L', x + w, y + h / 2); + } + } + return path || []; }; /** @@ -200,123 +200,123 @@ SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) { */ Series.prototype.getPointsOnGraph = function () { - if (!this.xAxis && !this.yAxis) { - return; - } - - var distance = 16, - points = this.points, - point, - last, - interpolated = [], - i, - deltaX, - deltaY, - delta, - len, - n, - j, - d, - graph = this.graph || this.area, - node = graph.element, - inverted = this.chart.inverted, - xAxis = this.xAxis, - yAxis = this.yAxis, - paneLeft = inverted ? yAxis.pos : xAxis.pos, - paneTop = inverted ? xAxis.pos : yAxis.pos, - onArea = pick(this.options.label.onArea, !!this.area), - translatedThreshold = yAxis.getThreshold(this.options.threshold); - - // For splines, get the point at length (possible caveat: peaks are not - // correctly detected) - if (this.getPointSpline && node.getPointAtLength && !onArea) { - // If it is animating towards a path definition, use that briefly, and - // reset - if (graph.toD) { - d = graph.attr('d'); - graph.attr({ d: graph.toD }); - } - len = node.getTotalLength(); - for (i = 0; i < len; i += distance) { - point = node.getPointAtLength(i); - interpolated.push({ - chartX: paneLeft + point.x, - chartY: paneTop + point.y, - plotX: point.x, - plotY: point.y - }); - } - if (d) { - graph.attr({ d: d }); - } - // Last point - point = points[points.length - 1]; - point.chartX = paneLeft + point.plotX; - point.chartY = paneTop + point.plotY; - interpolated.push(point); - - // Interpolate - } else { - len = points.length; - for (i = 0; i < len; i += 1) { - - point = points[i]; - last = points[i - 1]; - - // Absolute coordinates so we can compare different panes - point.chartX = paneLeft + point.plotX; - point.chartY = paneTop + point.plotY; - if (onArea) { - // Vertically centered inside area - point.chartCenterY = paneTop + ( - point.plotY + - pick(point.yBottom, translatedThreshold) - ) / 2; - } - - // Add interpolated points - if (i > 0) { - deltaX = Math.abs(point.chartX - last.chartX); - deltaY = Math.abs(point.chartY - last.chartY); - delta = Math.max(deltaX, deltaY); - if (delta > distance) { - - n = Math.ceil(delta / distance); - - for (j = 1; j < n; j += 1) { - interpolated.push({ - chartX: last.chartX + - (point.chartX - last.chartX) * (j / n), - chartY: last.chartY + - (point.chartY - last.chartY) * (j / n), - chartCenterY: last.chartCenterY + - (point.chartCenterY - last.chartCenterY) * - (j / n), - plotX: last.plotX + - (point.plotX - last.plotX) * (j / n), - plotY: last.plotY + - (point.plotY - last.plotY) * (j / n) - }); - } - } - } - - // Add the real point in order to find positive and negative peaks - if (isNumber(point.plotY)) { - interpolated.push(point); - } - } - } - - // Get the bounding box so we can do a quick check first if the bounding - // boxes overlap. - /* - interpolated.bBox = node.getBBox(); - interpolated.bBox.x += paneLeft; - interpolated.bBox.y += paneTop; - */ - - return interpolated; + if (!this.xAxis && !this.yAxis) { + return; + } + + var distance = 16, + points = this.points, + point, + last, + interpolated = [], + i, + deltaX, + deltaY, + delta, + len, + n, + j, + d, + graph = this.graph || this.area, + node = graph.element, + inverted = this.chart.inverted, + xAxis = this.xAxis, + yAxis = this.yAxis, + paneLeft = inverted ? yAxis.pos : xAxis.pos, + paneTop = inverted ? xAxis.pos : yAxis.pos, + onArea = pick(this.options.label.onArea, !!this.area), + translatedThreshold = yAxis.getThreshold(this.options.threshold); + + // For splines, get the point at length (possible caveat: peaks are not + // correctly detected) + if (this.getPointSpline && node.getPointAtLength && !onArea) { + // If it is animating towards a path definition, use that briefly, and + // reset + if (graph.toD) { + d = graph.attr('d'); + graph.attr({ d: graph.toD }); + } + len = node.getTotalLength(); + for (i = 0; i < len; i += distance) { + point = node.getPointAtLength(i); + interpolated.push({ + chartX: paneLeft + point.x, + chartY: paneTop + point.y, + plotX: point.x, + plotY: point.y + }); + } + if (d) { + graph.attr({ d: d }); + } + // Last point + point = points[points.length - 1]; + point.chartX = paneLeft + point.plotX; + point.chartY = paneTop + point.plotY; + interpolated.push(point); + + // Interpolate + } else { + len = points.length; + for (i = 0; i < len; i += 1) { + + point = points[i]; + last = points[i - 1]; + + // Absolute coordinates so we can compare different panes + point.chartX = paneLeft + point.plotX; + point.chartY = paneTop + point.plotY; + if (onArea) { + // Vertically centered inside area + point.chartCenterY = paneTop + ( + point.plotY + + pick(point.yBottom, translatedThreshold) + ) / 2; + } + + // Add interpolated points + if (i > 0) { + deltaX = Math.abs(point.chartX - last.chartX); + deltaY = Math.abs(point.chartY - last.chartY); + delta = Math.max(deltaX, deltaY); + if (delta > distance) { + + n = Math.ceil(delta / distance); + + for (j = 1; j < n; j += 1) { + interpolated.push({ + chartX: last.chartX + + (point.chartX - last.chartX) * (j / n), + chartY: last.chartY + + (point.chartY - last.chartY) * (j / n), + chartCenterY: last.chartCenterY + + (point.chartCenterY - last.chartCenterY) * + (j / n), + plotX: last.plotX + + (point.plotX - last.plotX) * (j / n), + plotY: last.plotY + + (point.plotY - last.plotY) * (j / n) + }); + } + } + } + + // Add the real point in order to find positive and negative peaks + if (isNumber(point.plotY)) { + interpolated.push(point); + } + } + } + + // Get the bounding box so we can do a quick check first if the bounding + // boxes overlap. + /* + interpolated.bBox = node.getBBox(); + interpolated.bBox.x += paneLeft; + interpolated.bBox.y += paneTop; + */ + + return interpolated; }; /** @@ -325,174 +325,174 @@ Series.prototype.getPointsOnGraph = function () { * values. */ Series.prototype.labelFontSize = function (minFontSize, maxFontSize) { - return minFontSize + ( - (this.sum / this.chart.labelSeriesMaxSum) * - (maxFontSize - minFontSize) - ) + 'px'; + return minFontSize + ( + (this.sum / this.chart.labelSeriesMaxSum) * + (maxFontSize - minFontSize) + ) + 'px'; }; /** * Check whether a proposed label position is clear of other elements */ Series.prototype.checkClearPoint = function (x, y, bBox, checkDistance) { - var distToOthersSquared = Number.MAX_VALUE, // distance to other graphs - distToPointSquared = Number.MAX_VALUE, - dist, - connectorPoint, - connectorEnabled = this.options.label.connectorAllowed, - onArea = pick(this.options.label.onArea, !!this.area), - chart = this.chart, - series, - points, - leastDistance = 16, - withinRange, - xDist, - yDist, - i, - j; - - function intersectRect(r1, r2) { - return !(r2.left > r1.right || - r2.right < r1.left || - r2.top > r1.bottom || - r2.bottom < r1.top); - } - - /** - * Get the weight in order to determine the ideal position. Larger distance - * to other series gives more weight. Smaller distance to the actual point - * (connector points only) gives more weight. - */ - function getWeight(distToOthersSquared, distToPointSquared) { - return distToOthersSquared - distToPointSquared; - } - - // First check for collision with existing labels - for (i = 0; i < chart.boxesToAvoid.length; i += 1) { - if (intersectRect(chart.boxesToAvoid[i], { - left: x, - right: x + bBox.width, - top: y, - bottom: y + bBox.height - })) { - return false; - } - } - - // For each position, check if the lines around the label intersect with any - // of the graphs. - for (i = 0; i < chart.series.length; i += 1) { - series = chart.series[i]; - points = series.interpolatedPoints; - if (series.visible && points) { - for (j = 1; j < points.length; j += 1) { - - if ( - // To avoid processing, only check intersection if the X - // values are close to the box. - points[j].chartX >= x - leastDistance && - points[j - 1].chartX <= x + bBox.width + leastDistance - ) { - // If any of the box sides intersect with the line, return. - if (boxIntersectLine( - x, - y, - bBox.width, - bBox.height, - points[j - 1].chartX, - points[j - 1].chartY, - points[j].chartX, - points[j].chartY - )) { - return false; - } - - // But if it is too far away (a padded box doesn't - // intersect), also return. - if (this === series && !withinRange && checkDistance) { - withinRange = boxIntersectLine( - x - leastDistance, - y - leastDistance, - bBox.width + 2 * leastDistance, - bBox.height + 2 * leastDistance, - points[j - 1].chartX, - points[j - 1].chartY, - points[j].chartX, - points[j].chartY - ); - } - } - - // Find the squared distance from the center of the label. On - // area series, avoid its own graph. - if ( - (connectorEnabled || withinRange) && - (this !== series || onArea) - ) { - xDist = x + bBox.width / 2 - points[j].chartX; - yDist = y + bBox.height / 2 - points[j].chartY; - distToOthersSquared = Math.min( - distToOthersSquared, - xDist * xDist + yDist * yDist - ); - } - } - - // Do we need a connector? - if ( - !onArea && - connectorEnabled && - this === series && - ( - (checkDistance && !withinRange) || - distToOthersSquared < Math.pow( - this.options.label.connectorNeighbourDistance, - 2 - ) - ) - ) { - for (j = 1; j < points.length; j += 1) { - dist = Math.min( - ( - Math.pow(x + bBox.width / 2 - points[j].chartX, 2) + - Math.pow(y + bBox.height / 2 - points[j].chartY, 2) - ), - ( - Math.pow(x - points[j].chartX, 2) + - Math.pow(y - points[j].chartY, 2) - ), - ( - Math.pow(x + bBox.width - points[j].chartX, 2) + - Math.pow(y - points[j].chartY, 2) - ), - ( - Math.pow(x + bBox.width - points[j].chartX, 2) + - Math.pow(y + bBox.height - points[j].chartY, 2) - ), - ( - Math.pow(x - points[j].chartX, 2) + - Math.pow(y + bBox.height - points[j].chartY, 2) - ) - ); - if (dist < distToPointSquared) { - distToPointSquared = dist; - connectorPoint = points[j]; - } - } - withinRange = true; - } - } - } - - return !checkDistance || withinRange ? { - x: x, - y: y, - weight: getWeight( - distToOthersSquared, - connectorPoint ? distToPointSquared : 0 - ), - connectorPoint: connectorPoint - } : false; + var distToOthersSquared = Number.MAX_VALUE, // distance to other graphs + distToPointSquared = Number.MAX_VALUE, + dist, + connectorPoint, + connectorEnabled = this.options.label.connectorAllowed, + onArea = pick(this.options.label.onArea, !!this.area), + chart = this.chart, + series, + points, + leastDistance = 16, + withinRange, + xDist, + yDist, + i, + j; + + function intersectRect(r1, r2) { + return !(r2.left > r1.right || + r2.right < r1.left || + r2.top > r1.bottom || + r2.bottom < r1.top); + } + + /** + * Get the weight in order to determine the ideal position. Larger distance + * to other series gives more weight. Smaller distance to the actual point + * (connector points only) gives more weight. + */ + function getWeight(distToOthersSquared, distToPointSquared) { + return distToOthersSquared - distToPointSquared; + } + + // First check for collision with existing labels + for (i = 0; i < chart.boxesToAvoid.length; i += 1) { + if (intersectRect(chart.boxesToAvoid[i], { + left: x, + right: x + bBox.width, + top: y, + bottom: y + bBox.height + })) { + return false; + } + } + + // For each position, check if the lines around the label intersect with any + // of the graphs. + for (i = 0; i < chart.series.length; i += 1) { + series = chart.series[i]; + points = series.interpolatedPoints; + if (series.visible && points) { + for (j = 1; j < points.length; j += 1) { + + if ( + // To avoid processing, only check intersection if the X + // values are close to the box. + points[j].chartX >= x - leastDistance && + points[j - 1].chartX <= x + bBox.width + leastDistance + ) { + // If any of the box sides intersect with the line, return. + if (boxIntersectLine( + x, + y, + bBox.width, + bBox.height, + points[j - 1].chartX, + points[j - 1].chartY, + points[j].chartX, + points[j].chartY + )) { + return false; + } + + // But if it is too far away (a padded box doesn't + // intersect), also return. + if (this === series && !withinRange && checkDistance) { + withinRange = boxIntersectLine( + x - leastDistance, + y - leastDistance, + bBox.width + 2 * leastDistance, + bBox.height + 2 * leastDistance, + points[j - 1].chartX, + points[j - 1].chartY, + points[j].chartX, + points[j].chartY + ); + } + } + + // Find the squared distance from the center of the label. On + // area series, avoid its own graph. + if ( + (connectorEnabled || withinRange) && + (this !== series || onArea) + ) { + xDist = x + bBox.width / 2 - points[j].chartX; + yDist = y + bBox.height / 2 - points[j].chartY; + distToOthersSquared = Math.min( + distToOthersSquared, + xDist * xDist + yDist * yDist + ); + } + } + + // Do we need a connector? + if ( + !onArea && + connectorEnabled && + this === series && + ( + (checkDistance && !withinRange) || + distToOthersSquared < Math.pow( + this.options.label.connectorNeighbourDistance, + 2 + ) + ) + ) { + for (j = 1; j < points.length; j += 1) { + dist = Math.min( + ( + Math.pow(x + bBox.width / 2 - points[j].chartX, 2) + + Math.pow(y + bBox.height / 2 - points[j].chartY, 2) + ), + ( + Math.pow(x - points[j].chartX, 2) + + Math.pow(y - points[j].chartY, 2) + ), + ( + Math.pow(x + bBox.width - points[j].chartX, 2) + + Math.pow(y - points[j].chartY, 2) + ), + ( + Math.pow(x + bBox.width - points[j].chartX, 2) + + Math.pow(y + bBox.height - points[j].chartY, 2) + ), + ( + Math.pow(x - points[j].chartX, 2) + + Math.pow(y + bBox.height - points[j].chartY, 2) + ) + ); + if (dist < distToPointSquared) { + distToPointSquared = dist; + connectorPoint = points[j]; + } + } + withinRange = true; + } + } + } + + return !checkDistance || withinRange ? { + x: x, + y: y, + weight: getWeight( + distToOthersSquared, + connectorPoint ? distToPointSquared : 0 + ), + connectorPoint: connectorPoint + } : false; }; @@ -502,257 +502,257 @@ Series.prototype.checkClearPoint = function (x, y, bBox, checkDistance) { * taking all series and labels into account when placing the labels. */ Chart.prototype.drawSeriesLabels = function () { - - // console.time('drawSeriesLabels'); - - var chart = this, - labelSeries = this.labelSeries; - - chart.boxesToAvoid = []; - - // Build the interpolated points - each(labelSeries, function (series) { - series.interpolatedPoints = series.getPointsOnGraph(); - - each(series.options.label.boxesToAvoid || [], function (box) { - chart.boxesToAvoid.push(box); - }); - }); - - each(chart.series, function (series) { - - if (!series.xAxis && !series.yAxis) { - return; - } - - var bBox, - x, - y, - results = [], - clearPoint, - i, - best, - labelOptions = series.options.label, - inverted = chart.inverted, - paneLeft = inverted ? series.yAxis.pos : series.xAxis.pos, - paneTop = inverted ? series.xAxis.pos : series.yAxis.pos, - paneWidth = chart.inverted ? series.yAxis.len : series.xAxis.len, - paneHeight = chart.inverted ? series.xAxis.len : series.yAxis.len, - points = series.interpolatedPoints, - onArea = pick(labelOptions.onArea, !!series.area), - label = series.labelBySeries, - minFontSize = labelOptions.minFontSize, - maxFontSize = labelOptions.maxFontSize; - - function insidePane(x, y, bBox) { - return x > paneLeft && x <= paneLeft + paneWidth - bBox.width && - y >= paneTop && y <= paneTop + paneHeight - bBox.height; - } - - if (series.visible && !series.isSeriesBoosting && points) { - if (!label) { - series.labelBySeries = label = chart.renderer - .label(series.name, 0, -9999, 'connector') - .css(extend({ - color: onArea ? - chart.renderer.getContrast(series.color) : - series.color - }, series.options.label.style)); - - // Adapt label sizes to the sum of the data - if (minFontSize && maxFontSize) { - label.css({ - fontSize: series.labelFontSize(minFontSize, maxFontSize) - }); - } - - label - .attr({ - padding: 0, - opacity: chart.renderer.forExport ? 1 : 0, - stroke: series.color, - 'stroke-width': 1, - zIndex: 3 - }) - .add(series.group) - .animate({ opacity: 1 }, { duration: 200 }); - } - - bBox = label.getBBox(); - bBox.width = Math.round(bBox.width); - - // Ideal positions are centered above or below a point on right side - // of chart - for (i = points.length - 1; i > 0; i -= 1) { - - if (onArea) { - - // Centered - x = points[i].chartX - bBox.width / 2; - y = points[i].chartCenterY - bBox.height / 2; - if (insidePane(x, y, bBox)) { - best = series.checkClearPoint( - x, - y, - bBox - ); - } - if (best) { - results.push(best); - } - - - } else { - - // Right - up - x = points[i].chartX + labelDistance; - y = points[i].chartY - bBox.height - labelDistance; - if (insidePane(x, y, bBox)) { - best = series.checkClearPoint( - x, - y, - bBox - ); - } - if (best) { - results.push(best); - } - - // Right - down - x = points[i].chartX + labelDistance; - y = points[i].chartY + labelDistance; - if (insidePane(x, y, bBox)) { - best = series.checkClearPoint( - x, - y, - bBox - ); - } - if (best) { - results.push(best); - } - - // Left - down - x = points[i].chartX - bBox.width - labelDistance; - y = points[i].chartY + labelDistance; - if (insidePane(x, y, bBox)) { - best = series.checkClearPoint( - x, - y, - bBox - ); - } - if (best) { - results.push(best); - } - - // Left - up - x = points[i].chartX - bBox.width - labelDistance; - y = points[i].chartY - bBox.height - labelDistance; - if (insidePane(x, y, bBox)) { - best = series.checkClearPoint( - x, - y, - bBox - ); - } - if (best) { - results.push(best); - } - } - } - - // Brute force, try all positions on the chart in a 16x16 grid - if (labelOptions.connectorAllowed && !results.length && !onArea) { - for ( - x = paneLeft + paneWidth - bBox.width; - x >= paneLeft; - x -= 16 - ) { - for ( - y = paneTop; - y < paneTop + paneHeight - bBox.height; - y += 16 - ) { - clearPoint = series.checkClearPoint(x, y, bBox, true); - if (clearPoint) { - results.push(clearPoint); - } - } - } - } - - if (results.length) { - - results.sort(function (a, b) { - return b.weight - a.weight; - }); - - best = results[0]; - - chart.boxesToAvoid.push({ - left: best.x, - right: best.x + bBox.width, - top: best.y, - bottom: best.y + bBox.height - }); - - // Move it if needed - var dist = Math.sqrt( - Math.pow(Math.abs(best.x - label.x), 2), - Math.pow(Math.abs(best.y - label.y), 2) - ); - - if (dist) { - - // Move fast and fade in - pure animation movement is - // distractive... - var attr = { - opacity: chart.renderer.forExport ? 1 : 0, - x: best.x - paneLeft, - y: best.y - paneTop - }, - anim = { - opacity: 1 - }; - // ... unless we're just moving a short distance - if (dist <= 10) { - anim = { - x: attr.x, - y: attr.y - }; - attr = {}; - } - series.labelBySeries - .attr(extend(attr, { - anchorX: best.connectorPoint && - best.connectorPoint.plotX, - anchorY: best.connectorPoint && - best.connectorPoint.plotY - })) - .animate(anim); - - // Record closest point to stick to for sync redraw - series.options.kdNow = true; - series.buildKDTree(); - var closest = series.searchPoint({ - chartX: best.x, - chartY: best.y - }, true); - label.closest = [ - closest, - best.x - paneLeft - closest.plotX, - best.y - paneTop - closest.plotY - ]; - - } - - } else if (label) { - series.labelBySeries = label.destroy(); - } - } - }); - // console.timeEnd('drawSeriesLabels'); + + // console.time('drawSeriesLabels'); + + var chart = this, + labelSeries = this.labelSeries; + + chart.boxesToAvoid = []; + + // Build the interpolated points + each(labelSeries, function (series) { + series.interpolatedPoints = series.getPointsOnGraph(); + + each(series.options.label.boxesToAvoid || [], function (box) { + chart.boxesToAvoid.push(box); + }); + }); + + each(chart.series, function (series) { + + if (!series.xAxis && !series.yAxis) { + return; + } + + var bBox, + x, + y, + results = [], + clearPoint, + i, + best, + labelOptions = series.options.label, + inverted = chart.inverted, + paneLeft = inverted ? series.yAxis.pos : series.xAxis.pos, + paneTop = inverted ? series.xAxis.pos : series.yAxis.pos, + paneWidth = chart.inverted ? series.yAxis.len : series.xAxis.len, + paneHeight = chart.inverted ? series.xAxis.len : series.yAxis.len, + points = series.interpolatedPoints, + onArea = pick(labelOptions.onArea, !!series.area), + label = series.labelBySeries, + minFontSize = labelOptions.minFontSize, + maxFontSize = labelOptions.maxFontSize; + + function insidePane(x, y, bBox) { + return x > paneLeft && x <= paneLeft + paneWidth - bBox.width && + y >= paneTop && y <= paneTop + paneHeight - bBox.height; + } + + if (series.visible && !series.isSeriesBoosting && points) { + if (!label) { + series.labelBySeries = label = chart.renderer + .label(series.name, 0, -9999, 'connector') + .css(extend({ + color: onArea ? + chart.renderer.getContrast(series.color) : + series.color + }, series.options.label.style)); + + // Adapt label sizes to the sum of the data + if (minFontSize && maxFontSize) { + label.css({ + fontSize: series.labelFontSize(minFontSize, maxFontSize) + }); + } + + label + .attr({ + padding: 0, + opacity: chart.renderer.forExport ? 1 : 0, + stroke: series.color, + 'stroke-width': 1, + zIndex: 3 + }) + .add(series.group) + .animate({ opacity: 1 }, { duration: 200 }); + } + + bBox = label.getBBox(); + bBox.width = Math.round(bBox.width); + + // Ideal positions are centered above or below a point on right side + // of chart + for (i = points.length - 1; i > 0; i -= 1) { + + if (onArea) { + + // Centered + x = points[i].chartX - bBox.width / 2; + y = points[i].chartCenterY - bBox.height / 2; + if (insidePane(x, y, bBox)) { + best = series.checkClearPoint( + x, + y, + bBox + ); + } + if (best) { + results.push(best); + } + + + } else { + + // Right - up + x = points[i].chartX + labelDistance; + y = points[i].chartY - bBox.height - labelDistance; + if (insidePane(x, y, bBox)) { + best = series.checkClearPoint( + x, + y, + bBox + ); + } + if (best) { + results.push(best); + } + + // Right - down + x = points[i].chartX + labelDistance; + y = points[i].chartY + labelDistance; + if (insidePane(x, y, bBox)) { + best = series.checkClearPoint( + x, + y, + bBox + ); + } + if (best) { + results.push(best); + } + + // Left - down + x = points[i].chartX - bBox.width - labelDistance; + y = points[i].chartY + labelDistance; + if (insidePane(x, y, bBox)) { + best = series.checkClearPoint( + x, + y, + bBox + ); + } + if (best) { + results.push(best); + } + + // Left - up + x = points[i].chartX - bBox.width - labelDistance; + y = points[i].chartY - bBox.height - labelDistance; + if (insidePane(x, y, bBox)) { + best = series.checkClearPoint( + x, + y, + bBox + ); + } + if (best) { + results.push(best); + } + } + } + + // Brute force, try all positions on the chart in a 16x16 grid + if (labelOptions.connectorAllowed && !results.length && !onArea) { + for ( + x = paneLeft + paneWidth - bBox.width; + x >= paneLeft; + x -= 16 + ) { + for ( + y = paneTop; + y < paneTop + paneHeight - bBox.height; + y += 16 + ) { + clearPoint = series.checkClearPoint(x, y, bBox, true); + if (clearPoint) { + results.push(clearPoint); + } + } + } + } + + if (results.length) { + + results.sort(function (a, b) { + return b.weight - a.weight; + }); + + best = results[0]; + + chart.boxesToAvoid.push({ + left: best.x, + right: best.x + bBox.width, + top: best.y, + bottom: best.y + bBox.height + }); + + // Move it if needed + var dist = Math.sqrt( + Math.pow(Math.abs(best.x - label.x), 2), + Math.pow(Math.abs(best.y - label.y), 2) + ); + + if (dist) { + + // Move fast and fade in - pure animation movement is + // distractive... + var attr = { + opacity: chart.renderer.forExport ? 1 : 0, + x: best.x - paneLeft, + y: best.y - paneTop + }, + anim = { + opacity: 1 + }; + // ... unless we're just moving a short distance + if (dist <= 10) { + anim = { + x: attr.x, + y: attr.y + }; + attr = {}; + } + series.labelBySeries + .attr(extend(attr, { + anchorX: best.connectorPoint && + best.connectorPoint.plotX, + anchorY: best.connectorPoint && + best.connectorPoint.plotY + })) + .animate(anim); + + // Record closest point to stick to for sync redraw + series.options.kdNow = true; + series.buildKDTree(); + var closest = series.searchPoint({ + chartX: best.x, + chartY: best.y + }, true); + label.closest = [ + closest, + best.x - paneLeft - closest.plotX, + best.y - paneTop - closest.plotY + ]; + + } + + } else if (label) { + series.labelBySeries = label.destroy(); + } + } + }); + // console.timeEnd('drawSeriesLabels'); }; /** @@ -760,69 +760,69 @@ Chart.prototype.drawSeriesLabels = function () { */ function drawLabels() { - var chart = this, - delay = Math.max( - H.animObject(chart.renderer.globalAnimation).duration, - 250 - ), - initial = !chart.hasRendered; - - chart.labelSeries = []; - chart.labelSeriesMaxSum = 0; - - H.clearTimeout(chart.seriesLabelTimer); - - // Which series should have labels - each(chart.series, function (series) { - var options = series.options.label, - label = series.labelBySeries, - closest = label && label.closest; - - if ( - options.enabled && - series.visible && - (series.graph || series.area) && - !series.isSeriesBoosting - ) { - chart.labelSeries.push(series); - - if (options.minFontSize && options.maxFontSize) { - series.sum = H.reduce(series.yData, function (pv, cv) { - return (pv || 0) + (cv || 0); - }, 0); - chart.labelSeriesMaxSum = Math.max( - chart.labelSeriesMaxSum, - series.sum - ); - } - - // The labels are processing heavy, wait until the animation is done - if (initial) { - delay = Math.max( - delay, - H.animObject(series.options.animation).duration - ); - } - - // Keep the position updated to the axis while redrawing - if (closest) { - if (closest[0].plotX !== undefined) { - label.animate({ - x: closest[0].plotX + closest[1], - y: closest[0].plotY + closest[2] - }); - } else { - label.attr({ opacity: 0 }); - } - } - } - }); - - chart.seriesLabelTimer = H.syncTimeout(function () { - if (chart.series && chart.labelSeries) { // #7931, chart destroyed - chart.drawSeriesLabels(); - } - }, chart.renderer.forExport ? 0 : delay); + var chart = this, + delay = Math.max( + H.animObject(chart.renderer.globalAnimation).duration, + 250 + ), + initial = !chart.hasRendered; + + chart.labelSeries = []; + chart.labelSeriesMaxSum = 0; + + H.clearTimeout(chart.seriesLabelTimer); + + // Which series should have labels + each(chart.series, function (series) { + var options = series.options.label, + label = series.labelBySeries, + closest = label && label.closest; + + if ( + options.enabled && + series.visible && + (series.graph || series.area) && + !series.isSeriesBoosting + ) { + chart.labelSeries.push(series); + + if (options.minFontSize && options.maxFontSize) { + series.sum = H.reduce(series.yData, function (pv, cv) { + return (pv || 0) + (cv || 0); + }, 0); + chart.labelSeriesMaxSum = Math.max( + chart.labelSeriesMaxSum, + series.sum + ); + } + + // The labels are processing heavy, wait until the animation is done + if (initial) { + delay = Math.max( + delay, + H.animObject(series.options.animation).duration + ); + } + + // Keep the position updated to the axis while redrawing + if (closest) { + if (closest[0].plotX !== undefined) { + label.animate({ + x: closest[0].plotX + closest[1], + y: closest[0].plotY + closest[2] + }); + } else { + label.attr({ opacity: 0 }); + } + } + } + }); + + chart.seriesLabelTimer = H.syncTimeout(function () { + if (chart.series && chart.labelSeries) { // #7931, chart destroyed + chart.drawSeriesLabels(); + } + }, chart.renderer.forExport ? 0 : delay); } addEvent(Chart, 'render', drawLabels); diff --git a/js/modules/solid-gauge.src.js b/js/modules/solid-gauge.src.js index a30c56a40d4..c2b09ac023f 100644 --- a/js/modules/solid-gauge.src.js +++ b/js/modules/solid-gauge.src.js @@ -12,16 +12,16 @@ import '../parts/Options.js'; import '../parts-more/GaugeSeries.js'; var pInt = H.pInt, - pick = H.pick, - each = H.each, - isNumber = H.isNumber, - wrap = H.wrap, - Renderer = H.Renderer, - colorAxisMethods; + pick = H.pick, + each = H.each, + isNumber = H.isNumber, + wrap = H.wrap, + Renderer = H.Renderer, + colorAxisMethods; /** * Symbol definition of an arc with round edges. - * + * * @param {Number} x - The X coordinate for the top left position. * @param {Number} y - The Y coordinate for the top left position. * @param {Number} w - The pixel width. @@ -29,31 +29,31 @@ var pInt = H.pInt, * @param {Object} [options] - Additional options, depending on the actual * symbol drawn. * @param {boolean} [options.rounded] - Whether to draw rounded edges. - * @return {Array} Path of the created arc. + * @return {Array} Path of the created arc. */ wrap( - Renderer.prototype.symbols, - 'arc', - function (proceed, x, y, w, h, options) { - var arc = proceed, - path = arc(x, y, w, h, options); - if (options.rounded) { - var r = options.r || w, - smallR = (r - options.innerR) / 2, - x1 = path[1], - y1 = path[2], - x2 = path[12], - y2 = path[13], - roundStart = ['A', smallR, smallR, 0, 1, 1, x1, y1], - roundEnd = ['A', smallR, smallR, 0, 1, 1, x2, y2]; - // Insert rounded edge on end, and remove line. - path.splice.apply(path, [path.length - 1, 0].concat(roundStart)); - // Insert rounded edge on end, and remove line. - path.splice.apply(path, [11, 3].concat(roundEnd)); - } - - return path; - } + Renderer.prototype.symbols, + 'arc', + function (proceed, x, y, w, h, options) { + var arc = proceed, + path = arc(x, y, w, h, options); + if (options.rounded) { + var r = options.r || w, + smallR = (r - options.innerR) / 2, + x1 = path[1], + y1 = path[2], + x2 = path[12], + y2 = path[13], + roundStart = ['A', smallR, smallR, 0, 1, 1, x1, y1], + roundEnd = ['A', smallR, smallR, 0, 1, 1, x2, y2]; + // Insert rounded edge on end, and remove line. + path.splice.apply(path, [path.length - 1, 0].concat(roundStart)); + // Insert rounded edge on end, and remove line. + path.splice.apply(path, [11, 3].concat(roundEnd)); + } + + return path; + } ); // These methods are defined in the ColorAxis object, and copied here. @@ -61,103 +61,103 @@ wrap( colorAxisMethods = { - initDataClasses: function (userOptions) { - var chart = this.chart, - dataClasses, - colorCounter = 0, - options = this.options; - this.dataClasses = dataClasses = []; - - each(userOptions.dataClasses, function (dataClass, i) { - var colors; - - dataClass = H.merge(dataClass); - dataClasses.push(dataClass); - if (!dataClass.color) { - if (options.dataClassColor === 'category') { - colors = chart.options.colors; - dataClass.color = colors[colorCounter++]; - // loop back to zero - if (colorCounter === colors.length) { - colorCounter = 0; - } - } else { - dataClass.color = H.color(options.minColor).tweenTo( - H.color(options.maxColor), - i / (userOptions.dataClasses.length - 1) - ); - } - } - }); - }, - - initStops: function (userOptions) { - this.stops = userOptions.stops || [ - [0, this.options.minColor], - [1, this.options.maxColor] - ]; - each(this.stops, function (stop) { - stop.color = H.color(stop[1]); - }); - }, - /** - * Translate from a value to a color - */ - toColor: function (value, point) { - var pos, - stops = this.stops, - from, - to, - color, - dataClasses = this.dataClasses, - dataClass, - i; - - if (dataClasses) { - i = dataClasses.length; - while (i--) { - dataClass = dataClasses[i]; - from = dataClass.from; - to = dataClass.to; - if ( - (from === undefined || value >= from) && - (to === undefined || value <= to) - ) { - color = dataClass.color; - if (point) { - point.dataClass = i; - } - break; - } - } - - } else { - - if (this.isLog) { - value = this.val2lin(value); - } - pos = 1 - ((this.max - value) / (this.max - this.min)); - i = stops.length; - while (i--) { - if (pos > stops[i][0]) { - break; - } - } - from = stops[i] || stops[i + 1]; - to = stops[i + 1] || from; - - // The position within the gradient - pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1); - - color = from.color.tweenTo( - to.color, - pos - ); - } - return color; - } + initDataClasses: function (userOptions) { + var chart = this.chart, + dataClasses, + colorCounter = 0, + options = this.options; + this.dataClasses = dataClasses = []; + + each(userOptions.dataClasses, function (dataClass, i) { + var colors; + + dataClass = H.merge(dataClass); + dataClasses.push(dataClass); + if (!dataClass.color) { + if (options.dataClassColor === 'category') { + colors = chart.options.colors; + dataClass.color = colors[colorCounter++]; + // loop back to zero + if (colorCounter === colors.length) { + colorCounter = 0; + } + } else { + dataClass.color = H.color(options.minColor).tweenTo( + H.color(options.maxColor), + i / (userOptions.dataClasses.length - 1) + ); + } + } + }); + }, + + initStops: function (userOptions) { + this.stops = userOptions.stops || [ + [0, this.options.minColor], + [1, this.options.maxColor] + ]; + each(this.stops, function (stop) { + stop.color = H.color(stop[1]); + }); + }, + /** + * Translate from a value to a color + */ + toColor: function (value, point) { + var pos, + stops = this.stops, + from, + to, + color, + dataClasses = this.dataClasses, + dataClass, + i; + + if (dataClasses) { + i = dataClasses.length; + while (i--) { + dataClass = dataClasses[i]; + from = dataClass.from; + to = dataClass.to; + if ( + (from === undefined || value >= from) && + (to === undefined || value <= to) + ) { + color = dataClass.color; + if (point) { + point.dataClass = i; + } + break; + } + } + + } else { + + if (this.isLog) { + value = this.val2lin(value); + } + pos = 1 - ((this.max - value) / (this.max - this.min)); + i = stops.length; + while (i--) { + if (pos > stops[i][0]) { + break; + } + } + from = stops[i] || stops[i + 1]; + to = stops[i + 1] || from; + + // The position within the gradient + pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1); + + color = from.color.tweenTo( + to.color, + pos + ); + } + return color; + } }; -/** +/** * A solid gauge is a circular gauge where the value is indicated by a filled * arc, and the color of the arc may variate with the value. * @@ -168,58 +168,58 @@ colorAxisMethods = { * @optionparent plotOptions.solidgauge */ var solidGaugeOptions = { - /** - * Whether the strokes of the solid gauge should be `round` or `square`. - * - * @validvalue ["square", "round"] - * @type {String} - * @sample {highcharts} highcharts/demo/gauge-activity/ Rounded gauge - * @default round - * @since 4.2.2 - * @product highcharts - * @apioption plotOptions.solidgauge.linecap - */ - - /** - * Allow the gauge to overshoot the end of the perimeter axis by this - * many degrees. Say if the gauge axis goes from 0 to 60, a value of - * 100, or 1000, will show 5 degrees beyond the end of the axis when this - * option is set to 5. - * - * @type {Number} - * @default 0 - * @since 3.0.10 - * @product highcharts - * @apioption plotOptions.solidgauge.overshoot - */ - - /** - * Wether to draw rounded edges on the gauge. - * - * @type {Boolean} - * @sample {highcharts} highcharts/demo/gauge-activity/ Activity Gauge - * @default false - * @since 5.0.8 - * @product highcharts - * @apioption plotOptions.solidgauge.rounded - */ - - /** - * The threshold or base level for the gauge. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/solidgauge-threshold/ - * Zero threshold with negative and positive values - * @default null - * @since 5.0.3 - * @product highcharts - * @apioption plotOptions.solidgauge.threshold - */ - - /** - * Whether to give each point an individual color. - */ - colorByPoint: true + /** + * Whether the strokes of the solid gauge should be `round` or `square`. + * + * @validvalue ["square", "round"] + * @type {String} + * @sample {highcharts} highcharts/demo/gauge-activity/ Rounded gauge + * @default round + * @since 4.2.2 + * @product highcharts + * @apioption plotOptions.solidgauge.linecap + */ + + /** + * Allow the gauge to overshoot the end of the perimeter axis by this + * many degrees. Say if the gauge axis goes from 0 to 60, a value of + * 100, or 1000, will show 5 degrees beyond the end of the axis when this + * option is set to 5. + * + * @type {Number} + * @default 0 + * @since 3.0.10 + * @product highcharts + * @apioption plotOptions.solidgauge.overshoot + */ + + /** + * Wether to draw rounded edges on the gauge. + * + * @type {Boolean} + * @sample {highcharts} highcharts/demo/gauge-activity/ Activity Gauge + * @default false + * @since 5.0.8 + * @product highcharts + * @apioption plotOptions.solidgauge.rounded + */ + + /** + * The threshold or base level for the gauge. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/solidgauge-threshold/ + * Zero threshold with negative and positive values + * @default null + * @since 5.0.3 + * @product highcharts + * @apioption plotOptions.solidgauge.threshold + */ + + /** + * Whether to give each point an individual color. + */ + colorByPoint: true }; @@ -227,159 +227,159 @@ var solidGaugeOptions = { // The solidgauge series type H.seriesType('solidgauge', 'gauge', solidGaugeOptions, { - /** - * Extend the translate function to extend the Y axis with the necessary - * decoration (#5895). - */ - translate: function () { - var axis = this.yAxis; - H.extend(axis, colorAxisMethods); - - // Prepare data classes - if (!axis.dataClasses && axis.options.dataClasses) { - axis.initDataClasses(axis.options); - } - axis.initStops(axis.options); - - // Generate points and inherit data label position - H.seriesTypes.gauge.prototype.translate.call(this); - }, - - /** - * Draw the points where each point is one needle - */ - drawPoints: function () { - var series = this, - yAxis = series.yAxis, - center = yAxis.center, - options = series.options, - renderer = series.chart.renderer, - overshoot = options.overshoot, - overshootVal = isNumber(overshoot) ? overshoot / 180 * Math.PI : 0, - thresholdAngleRad; - - // Handle the threshold option - if (isNumber(options.threshold)) { - thresholdAngleRad = yAxis.startAngleRad + yAxis.translate( - options.threshold, - null, - null, - null, - true - ); - } - this.thresholdAngleRad = pick(thresholdAngleRad, yAxis.startAngleRad); - - - each(series.points, function (point) { - var graphic = point.graphic, - rotation = yAxis.startAngleRad + - yAxis.translate(point.y, null, null, null, true), - radius = ( - pInt( - pick(point.options.radius, options.radius, 100) - ) * center[2] - ) / 200, - innerRadius = ( - pInt( - pick(point.options.innerRadius, options.innerRadius, 60) - ) * center[2] - ) / 200, - shapeArgs, - d, - toColor = yAxis.toColor(point.y, point), - axisMinAngle = Math.min(yAxis.startAngleRad, yAxis.endAngleRad), - axisMaxAngle = Math.max(yAxis.startAngleRad, yAxis.endAngleRad), - minAngle, - maxAngle; - - if (toColor === 'none') { // #3708 - toColor = point.color || series.color || 'none'; - } - if (toColor !== 'none') { - point.color = toColor; - } - - // Handle overshoot and clipping to axis max/min - rotation = Math.max( - axisMinAngle - overshootVal, - Math.min(axisMaxAngle + overshootVal, rotation) - ); - - // Handle the wrap option - if (options.wrap === false) { - rotation = Math.max( - axisMinAngle, - Math.min(axisMaxAngle, rotation) - ); - } - - minAngle = Math.min(rotation, series.thresholdAngleRad); - maxAngle = Math.max(rotation, series.thresholdAngleRad); - - if (maxAngle - minAngle > 2 * Math.PI) { - maxAngle = minAngle + 2 * Math.PI; - } - - point.shapeArgs = shapeArgs = { - x: center[0], - y: center[1], - r: radius, - innerR: innerRadius, - start: minAngle, - end: maxAngle, - rounded: options.rounded - }; - point.startR = radius; // For PieSeries.animate - - if (graphic) { - d = shapeArgs.d; - graphic.animate(H.extend({ fill: toColor }, shapeArgs)); - if (d) { - shapeArgs.d = d; // animate alters it - } - } else { - point.graphic = renderer.arc(shapeArgs) - .addClass(point.getClassName(), true) - .attr({ - fill: toColor, - 'sweep-flag': 0 - }) - .add(series.group); - - /*= if (build.classic) { =*/ - if (options.linecap !== 'square') { - point.graphic.attr({ - 'stroke-linecap': 'round', - 'stroke-linejoin': 'round' - }); - } - point.graphic.attr({ - stroke: options.borderColor || 'none', - 'stroke-width': options.borderWidth || 0 - }); - /*= } =*/ - } - }); - }, - - /** - * Extend the pie slice animation by animating from start angle and up - */ - animate: function (init) { - - if (!init) { - this.startAngleRad = this.thresholdAngleRad; - H.seriesTypes.pie.prototype.animate.call(this, init); - } - } + /** + * Extend the translate function to extend the Y axis with the necessary + * decoration (#5895). + */ + translate: function () { + var axis = this.yAxis; + H.extend(axis, colorAxisMethods); + + // Prepare data classes + if (!axis.dataClasses && axis.options.dataClasses) { + axis.initDataClasses(axis.options); + } + axis.initStops(axis.options); + + // Generate points and inherit data label position + H.seriesTypes.gauge.prototype.translate.call(this); + }, + + /** + * Draw the points where each point is one needle + */ + drawPoints: function () { + var series = this, + yAxis = series.yAxis, + center = yAxis.center, + options = series.options, + renderer = series.chart.renderer, + overshoot = options.overshoot, + overshootVal = isNumber(overshoot) ? overshoot / 180 * Math.PI : 0, + thresholdAngleRad; + + // Handle the threshold option + if (isNumber(options.threshold)) { + thresholdAngleRad = yAxis.startAngleRad + yAxis.translate( + options.threshold, + null, + null, + null, + true + ); + } + this.thresholdAngleRad = pick(thresholdAngleRad, yAxis.startAngleRad); + + + each(series.points, function (point) { + var graphic = point.graphic, + rotation = yAxis.startAngleRad + + yAxis.translate(point.y, null, null, null, true), + radius = ( + pInt( + pick(point.options.radius, options.radius, 100) + ) * center[2] + ) / 200, + innerRadius = ( + pInt( + pick(point.options.innerRadius, options.innerRadius, 60) + ) * center[2] + ) / 200, + shapeArgs, + d, + toColor = yAxis.toColor(point.y, point), + axisMinAngle = Math.min(yAxis.startAngleRad, yAxis.endAngleRad), + axisMaxAngle = Math.max(yAxis.startAngleRad, yAxis.endAngleRad), + minAngle, + maxAngle; + + if (toColor === 'none') { // #3708 + toColor = point.color || series.color || 'none'; + } + if (toColor !== 'none') { + point.color = toColor; + } + + // Handle overshoot and clipping to axis max/min + rotation = Math.max( + axisMinAngle - overshootVal, + Math.min(axisMaxAngle + overshootVal, rotation) + ); + + // Handle the wrap option + if (options.wrap === false) { + rotation = Math.max( + axisMinAngle, + Math.min(axisMaxAngle, rotation) + ); + } + + minAngle = Math.min(rotation, series.thresholdAngleRad); + maxAngle = Math.max(rotation, series.thresholdAngleRad); + + if (maxAngle - minAngle > 2 * Math.PI) { + maxAngle = minAngle + 2 * Math.PI; + } + + point.shapeArgs = shapeArgs = { + x: center[0], + y: center[1], + r: radius, + innerR: innerRadius, + start: minAngle, + end: maxAngle, + rounded: options.rounded + }; + point.startR = radius; // For PieSeries.animate + + if (graphic) { + d = shapeArgs.d; + graphic.animate(H.extend({ fill: toColor }, shapeArgs)); + if (d) { + shapeArgs.d = d; // animate alters it + } + } else { + point.graphic = renderer.arc(shapeArgs) + .addClass(point.getClassName(), true) + .attr({ + fill: toColor, + 'sweep-flag': 0 + }) + .add(series.group); + + /*= if (build.classic) { =*/ + if (options.linecap !== 'square') { + point.graphic.attr({ + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round' + }); + } + point.graphic.attr({ + stroke: options.borderColor || 'none', + 'stroke-width': options.borderWidth || 0 + }); + /*= } =*/ + } + }); + }, + + /** + * Extend the pie slice animation by animating from start angle and up + */ + animate: function (init) { + + if (!init) { + this.startAngleRad = this.thresholdAngleRad; + H.seriesTypes.pie.prototype.animate.call(this, init); + } + } }); /** * A `solidgauge` series. If the [type](#series.solidgauge.type) option * is not specified, it is inherited from [chart.type](#chart.type). - * - * + * + * * @type {Object} * @extends series,plotOptions.solidgauge * @excluding animationLimit,boostThreshold,connectEnds,connectNulls, @@ -394,19 +394,19 @@ H.seriesType('solidgauge', 'gauge', solidGaugeOptions, { /** * An array of data points for the series. For the `solidgauge` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 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.solidgauge.turboThreshold), this option is not available. - * + * * ```js * data: [{ * y: 5, @@ -418,9 +418,9 @@ H.seriesType('solidgauge', 'gauge', solidGaugeOptions, { * color: "#FF00FF" * }] * ``` - * + * * The typical gauge only contains a single data value. - * + * * @type {Array} * @extends series.gauge.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -432,7 +432,7 @@ H.seriesType('solidgauge', 'gauge', solidGaugeOptions, { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts * @apioption series.solidgauge.data */ @@ -440,7 +440,7 @@ H.seriesType('solidgauge', 'gauge', solidGaugeOptions, { /** * The inner radius of an individual point in a solid gauge. Can be * given as a number (pixels) or percentage string. - * + * * @type {Number|String} * @sample {highcharts} highcharts/plotoptions/solidgauge-radius/ Individual radius and innerRadius * @since 4.1.6 @@ -451,7 +451,7 @@ H.seriesType('solidgauge', 'gauge', solidGaugeOptions, { /** * The outer radius of an individual point in a solid gauge. Can be * given as a number (pixels) or percentage string. - * + * * @type {Number|String} * @sample {highcharts} highcharts/plotoptions/solidgauge-radius/ Individual radius and innerRadius * @since 4.1.6 diff --git a/js/modules/static-scale.src.js b/js/modules/static-scale.src.js index c81cd53e477..dd442ca98ec 100644 --- a/js/modules/static-scale.src.js +++ b/js/modules/static-scale.src.js @@ -8,38 +8,38 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var Chart = H.Chart, - each = H.each, - pick = H.pick; + each = H.each, + pick = H.pick; Chart.prototype.adjustHeight = function () { - each(this.axes || [], function (axis) { - var chart = axis.chart, - animate = !!chart.initiatedScale && chart.options.animation, - staticScale = axis.options.staticScale, - height, - diff; - if ( - H.isNumber(staticScale) && - !axis.horiz && - H.defined(axis.min) - ) { - height = pick( - axis.unitLength, - axis.max + axis.tickInterval - axis.min - ) * staticScale; - - // Minimum height is 1 x staticScale. - height = Math.max(height, staticScale); - - diff = height - chart.plotHeight; - - if (Math.abs(diff) >= 1) { - chart.plotHeight = height; - chart.setSize(null, chart.chartHeight + diff, animate); - } - } - - }); - this.initiatedScale = true; + each(this.axes || [], function (axis) { + var chart = axis.chart, + animate = !!chart.initiatedScale && chart.options.animation, + staticScale = axis.options.staticScale, + height, + diff; + if ( + H.isNumber(staticScale) && + !axis.horiz && + H.defined(axis.min) + ) { + height = pick( + axis.unitLength, + axis.max + axis.tickInterval - axis.min + ) * staticScale; + + // Minimum height is 1 x staticScale. + height = Math.max(height, staticScale); + + diff = height - chart.plotHeight; + + if (Math.abs(diff) >= 1) { + chart.plotHeight = height; + chart.setSize(null, chart.chartHeight + diff, animate); + } + } + + }); + this.initiatedScale = true; }; H.addEvent(Chart, 'render', Chart.prototype.adjustHeight); diff --git a/js/modules/streamgraph.src.js b/js/modules/streamgraph.src.js index 6c9be58b623..a166ed6b367 100644 --- a/js/modules/streamgraph.src.js +++ b/js/modules/streamgraph.src.js @@ -14,7 +14,7 @@ var seriesType = H.seriesType; /** * A streamgraph is a type of stacked area graph which is displaced around a * central axis, resulting in a flowing, organic shape. - * + * * @extends {plotOptions.areaspline} * @product highcharts highstock * @sample {highcharts|highstock} highcharts/demo/streamgraph/ @@ -23,36 +23,36 @@ var seriesType = H.seriesType; * @optionparent plotOptions.streamgraph */ seriesType('streamgraph', 'areaspline', { - fillOpacity: 1, - lineWidth: 0, - marker: { - enabled: false - }, - stacking: 'stream' + fillOpacity: 1, + lineWidth: 0, + marker: { + enabled: false + }, + stacking: 'stream' // Prototype functions }, { - negStacks: false, + negStacks: false, - /** - * Modifier function for stream stacks. It simply moves the point up or down - * in order to center the full stack vertically. - */ - streamStacker: function (pointExtremes, stack, i) { - // Y bottom value - pointExtremes[0] -= stack.total / 2; - // Y value - pointExtremes[1] -= stack.total / 2; + /** + * Modifier function for stream stacks. It simply moves the point up or down + * in order to center the full stack vertically. + */ + streamStacker: function (pointExtremes, stack, i) { + // Y bottom value + pointExtremes[0] -= stack.total / 2; + // Y value + pointExtremes[1] -= stack.total / 2; - // Record the Y data for use when getting axis extremes - this.stackedYData[i] = pointExtremes; - } + // Record the Y data for use when getting axis extremes + this.stackedYData[i] = pointExtremes; + } }); /** * A `streamgraph` series. If the [type](#series.streamgraph.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.streamgraph * @excluding dataParser,dataURL @@ -63,21 +63,21 @@ seriesType('streamgraph', 'areaspline', { /** * An array of data points for the series. For the `streamgraph` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 9], @@ -85,12 +85,12 @@ seriesType('streamgraph', 'areaspline', { * [2, 6] * ] * ``` - * + * * 3. 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.area.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -104,7 +104,7 @@ seriesType('streamgraph', 'areaspline', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -116,7 +116,7 @@ seriesType('streamgraph', 'areaspline', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts highstock * @apioption series.streamgraph.data */ diff --git a/js/modules/sunburst.src.js b/js/modules/sunburst.src.js index a570efb0299..504951190a7 100644 --- a/js/modules/sunburst.src.js +++ b/js/modules/sunburst.src.js @@ -14,41 +14,41 @@ import mixinTreeSeries from '../mixins/tree-series.js'; import '../parts/Series.js'; import './treemap.src.js'; var CenteredSeriesMixin = H.CenteredSeriesMixin, - Series = H.Series, - each = H.each, - extend = H.extend, - getCenter = CenteredSeriesMixin.getCenter, - getColor = mixinTreeSeries.getColor, - getLevelOptions = mixinTreeSeries.getLevelOptions, - getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians, - grep = H.grep, - inArray = H.inArray, - isBoolean = function (x) { - return typeof x === 'boolean'; - }, - isNumber = H.isNumber, - isObject = H.isObject, - isString = H.isString, - keys = H.keys, - merge = H.merge, - noop = H.noop, - rad2deg = 180 / Math.PI, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes, - setTreeValues = mixinTreeSeries.setTreeValues, - reduce = H.reduce, - updateRootId = mixinTreeSeries.updateRootId; + Series = H.Series, + each = H.each, + extend = H.extend, + getCenter = CenteredSeriesMixin.getCenter, + getColor = mixinTreeSeries.getColor, + getLevelOptions = mixinTreeSeries.getLevelOptions, + getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians, + grep = H.grep, + inArray = H.inArray, + isBoolean = function (x) { + return typeof x === 'boolean'; + }, + isNumber = H.isNumber, + isObject = H.isObject, + isString = H.isString, + keys = H.keys, + merge = H.merge, + noop = H.noop, + rad2deg = 180 / Math.PI, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes, + setTreeValues = mixinTreeSeries.setTreeValues, + reduce = H.reduce, + updateRootId = mixinTreeSeries.updateRootId; // TODO introduce step, which should default to 1. var range = function range(from, to) { - var result = [], - i; - if (isNumber(from) && isNumber(to) && from <= to) { - for (i = from; i <= to; i++) { - result.push(i); - } - } - return result; + var result = [], + i; + if (isNumber(from) && isNumber(to) && from <= to) { + for (i = from; i <= to; i++) { + result.push(i); + } + } + return result; }; /** @@ -59,68 +59,68 @@ var range = function range(from, to) { * @ */ var calculateLevelSizes = function calculateLevelSizes(levelOptions, params) { - var result, - p = isObject(params) ? params : {}, - totalWeight = 0, - diffRadius, - levels, - levelsNotIncluded, - remainingSize, - from, - to; - - if (isObject(levelOptions)) { - result = merge({}, levelOptions); // Copy levelOptions - from = isNumber(p.from) ? p.from : 0; - to = isNumber(p.to) ? p.to : 0; - levels = range(from, to); - levelsNotIncluded = grep(keys(result), function (k) { - return inArray(+k, levels) === -1; - }); - diffRadius = remainingSize = isNumber(p.diffRadius) ? p.diffRadius : 0; - /** - * Convert percentage to pixels. - * Calculate the remaining size to divide between "weight" levels. - * Calculate total weight to use in convertion from weight to pixels. - */ - each(levels, function (level) { - var options = result[level], - unit = options.levelSize.unit, - value = options.levelSize.value; - if (unit === 'weight') { - totalWeight += value; - } else if (unit === 'percentage') { - options.levelSize = { - unit: 'pixels', - value: (value / 100) * diffRadius - }; - remainingSize -= options.levelSize.value; - } else if (unit === 'pixels') { - remainingSize -= value; - } - }); - - // Convert weight to pixels. - each(levels, function (level) { - var options = result[level], - weight; - if (options.levelSize.unit === 'weight') { - weight = options.levelSize.value; - result[level].levelSize = { - unit: 'pixels', - value: (weight / totalWeight) * remainingSize - }; - } - }); - // Set all levels not included in interval [from,to] to have 0 pixels. - each(levelsNotIncluded, function (level) { - result[level].levelSize = { - value: 0, - unit: 'pixels' - }; - }); - } - return result; + var result, + p = isObject(params) ? params : {}, + totalWeight = 0, + diffRadius, + levels, + levelsNotIncluded, + remainingSize, + from, + to; + + if (isObject(levelOptions)) { + result = merge({}, levelOptions); // Copy levelOptions + from = isNumber(p.from) ? p.from : 0; + to = isNumber(p.to) ? p.to : 0; + levels = range(from, to); + levelsNotIncluded = grep(keys(result), function (k) { + return inArray(+k, levels) === -1; + }); + diffRadius = remainingSize = isNumber(p.diffRadius) ? p.diffRadius : 0; + /** + * Convert percentage to pixels. + * Calculate the remaining size to divide between "weight" levels. + * Calculate total weight to use in convertion from weight to pixels. + */ + each(levels, function (level) { + var options = result[level], + unit = options.levelSize.unit, + value = options.levelSize.value; + if (unit === 'weight') { + totalWeight += value; + } else if (unit === 'percentage') { + options.levelSize = { + unit: 'pixels', + value: (value / 100) * diffRadius + }; + remainingSize -= options.levelSize.value; + } else if (unit === 'pixels') { + remainingSize -= value; + } + }); + + // Convert weight to pixels. + each(levels, function (level) { + var options = result[level], + weight; + if (options.levelSize.unit === 'weight') { + weight = options.levelSize.value; + result[level].levelSize = { + unit: 'pixels', + value: (weight / totalWeight) * remainingSize + }; + } + }); + // Set all levels not included in interval [from,to] to have 0 pixels. + each(levelsNotIncluded, function (level) { + result[level].levelSize = { + value: 0, + unit: 'pixels' + }; + }); + } + return result; }; /** @@ -134,201 +134,201 @@ var calculateLevelSizes = function calculateLevelSizes(levelOptions, params) { * @return {object} Returns the end coordinates, x and y. */ var getEndPoint = function getEndPoint(x, y, angle, distance) { - return { - x: x + (Math.cos(angle) * distance), - y: y + (Math.sin(angle) * distance) - }; + return { + x: x + (Math.cos(angle) * distance), + y: y + (Math.sin(angle) * distance) + }; }; var layoutAlgorithm = function layoutAlgorithm(parent, children, options) { - var startAngle = parent.start, - range = parent.end - startAngle, - total = parent.val, - x = parent.x, - y = parent.y, - radius = ( - isObject(options.levelSize) && isNumber(options.levelSize.value) ? - options.levelSize.value : - 0 - ), - innerRadius = parent.r, - outerRadius = innerRadius + radius, - slicedOffset = isNumber(options.slicedOffset) ? - options.slicedOffset : - 0; - - return reduce(children || [], function (arr, child) { - var percentage = (1 / total) * child.val, - radians = percentage * range, - radiansCenter = startAngle + (radians / 2), - offsetPosition = getEndPoint(x, y, radiansCenter, slicedOffset), - values = { - x: child.sliced ? offsetPosition.x : x, - y: child.sliced ? offsetPosition.y : y, - innerR: innerRadius, - r: outerRadius, - radius: radius, - start: startAngle, - end: startAngle + radians - }; - arr.push(values); - startAngle = values.end; - return arr; - }, []); + var startAngle = parent.start, + range = parent.end - startAngle, + total = parent.val, + x = parent.x, + y = parent.y, + radius = ( + isObject(options.levelSize) && isNumber(options.levelSize.value) ? + options.levelSize.value : + 0 + ), + innerRadius = parent.r, + outerRadius = innerRadius + radius, + slicedOffset = isNumber(options.slicedOffset) ? + options.slicedOffset : + 0; + + return reduce(children || [], function (arr, child) { + var percentage = (1 / total) * child.val, + radians = percentage * range, + radiansCenter = startAngle + (radians / 2), + offsetPosition = getEndPoint(x, y, radiansCenter, slicedOffset), + values = { + x: child.sliced ? offsetPosition.x : x, + y: child.sliced ? offsetPosition.y : y, + innerR: innerRadius, + r: outerRadius, + radius: radius, + start: startAngle, + end: startAngle + radians + }; + arr.push(values); + startAngle = values.end; + return arr; + }, []); }; var getDlOptions = function getDlOptions(params) { - // Set options to new object to avoid problems with scope - var shape = isObject(params.shapeArgs) ? params.shapeArgs : {}, - optionsPoint = ( - isObject(params.optionsPoint) ? - params.optionsPoint.dataLabels : - {} - ), - optionsLevel = ( - isObject(params.level) ? - params.level.dataLabels : - {} - ), - options = merge({ - rotationMode: 'perpendicular', - style: { - width: shape.radius - } - }, optionsLevel, optionsPoint), - rotationRad, - rotation; - if (!isNumber(options.rotation)) { - rotationRad = (shape.end - (shape.end - shape.start) / 2); - rotation = (rotationRad * rad2deg) % 180; - if (options.rotationMode === 'parallel') { - rotation -= 90; - } - // Data labels should not rotate beyond 90 degrees, for readability. - if (rotation > 90) { - rotation -= 180; - } - options.rotation = rotation; - } - // NOTE: alignDataLabel positions the data label differntly when rotation is - // 0. Avoiding this by setting rotation to a small number. - if (options.rotation === 0) { - options.rotation = 0.001; - } - return options; + // Set options to new object to avoid problems with scope + var shape = isObject(params.shapeArgs) ? params.shapeArgs : {}, + optionsPoint = ( + isObject(params.optionsPoint) ? + params.optionsPoint.dataLabels : + {} + ), + optionsLevel = ( + isObject(params.level) ? + params.level.dataLabels : + {} + ), + options = merge({ + rotationMode: 'perpendicular', + style: { + width: shape.radius + } + }, optionsLevel, optionsPoint), + rotationRad, + rotation; + if (!isNumber(options.rotation)) { + rotationRad = (shape.end - (shape.end - shape.start) / 2); + rotation = (rotationRad * rad2deg) % 180; + if (options.rotationMode === 'parallel') { + rotation -= 90; + } + // Data labels should not rotate beyond 90 degrees, for readability. + if (rotation > 90) { + rotation -= 180; + } + options.rotation = rotation; + } + // NOTE: alignDataLabel positions the data label differntly when rotation is + // 0. Avoiding this by setting rotation to a small number. + if (options.rotation === 0) { + options.rotation = 0.001; + } + return options; }; var getAnimation = function getAnimation(shape, params) { - var point = params.point, - radians = params.radians, - innerR = params.innerR, - idRoot = params.idRoot, - idPreviousRoot = params.idPreviousRoot, - shapeExisting = params.shapeExisting, - shapeRoot = params.shapeRoot, - shapePreviousRoot = params.shapePreviousRoot, - visible = params.visible, - from = {}, - to = { - end: shape.end, - start: shape.start, - innerR: shape.innerR, - r: shape.r, - x: shape.x, - y: shape.y - }; - if (visible) { - // Animate points in - if (!point.graphic && shapePreviousRoot) { - if (idRoot === point.id) { - from = { - start: radians.start, - end: radians.end - }; - } else { - from = (shapePreviousRoot.end <= shape.start) ? { - start: radians.end, - end: radians.end - } : { - start: radians.start, - end: radians.start - }; - } - // Animate from center and outwards. - from.innerR = from.r = innerR; - } - } else { - // Animate points out - if (point.graphic) { - if (idPreviousRoot === point.id) { - to = { - innerR: innerR, - r: innerR - }; - } else if (shapeRoot) { - to = (shapeRoot.end <= shapeExisting.start) ? - { - innerR: innerR, - r: innerR, - start: radians.end, - end: radians.end - } : { - innerR: innerR, - r: innerR, - start: radians.start, - end: radians.start - }; - } - } - } - return { - from: from, - to: to - }; + var point = params.point, + radians = params.radians, + innerR = params.innerR, + idRoot = params.idRoot, + idPreviousRoot = params.idPreviousRoot, + shapeExisting = params.shapeExisting, + shapeRoot = params.shapeRoot, + shapePreviousRoot = params.shapePreviousRoot, + visible = params.visible, + from = {}, + to = { + end: shape.end, + start: shape.start, + innerR: shape.innerR, + r: shape.r, + x: shape.x, + y: shape.y + }; + if (visible) { + // Animate points in + if (!point.graphic && shapePreviousRoot) { + if (idRoot === point.id) { + from = { + start: radians.start, + end: radians.end + }; + } else { + from = (shapePreviousRoot.end <= shape.start) ? { + start: radians.end, + end: radians.end + } : { + start: radians.start, + end: radians.start + }; + } + // Animate from center and outwards. + from.innerR = from.r = innerR; + } + } else { + // Animate points out + if (point.graphic) { + if (idPreviousRoot === point.id) { + to = { + innerR: innerR, + r: innerR + }; + } else if (shapeRoot) { + to = (shapeRoot.end <= shapeExisting.start) ? + { + innerR: innerR, + r: innerR, + start: radians.end, + end: radians.end + } : { + innerR: innerR, + r: innerR, + start: radians.start, + end: radians.start + }; + } + } + } + return { + from: from, + to: to + }; }; var getDrillId = function getDrillId(point, idRoot, mapIdToNode) { - var drillId, - node = point.node, - nodeRoot; - if (!node.isLeaf) { - // When it is the root node, the drillId should be set to parent. - if (idRoot === point.id) { - nodeRoot = mapIdToNode[idRoot]; - drillId = nodeRoot.parent; - } else { - drillId = point.id; - } - } - return drillId; + var drillId, + node = point.node, + nodeRoot; + if (!node.isLeaf) { + // When it is the root node, the drillId should be set to parent. + if (idRoot === point.id) { + nodeRoot = mapIdToNode[idRoot]; + drillId = nodeRoot.parent; + } else { + drillId = point.id; + } + } + return drillId; }; var cbSetTreeValuesBefore = function before(node, options) { - var mapIdToNode = options.mapIdToNode, - nodeParent = mapIdToNode[node.parent], - series = options.series, - chart = series.chart, - points = series.points, - point = points[node.i], - colorInfo = getColor(node, { - colors: chart && chart.options && chart.options.colors, - colorIndex: series.colorIndex, - index: options.index, - mapOptionsToLevel: options.mapOptionsToLevel, - parentColor: nodeParent && nodeParent.color, - parentColorIndex: nodeParent && nodeParent.colorIndex, - series: options.series, - siblings: options.siblings - }); - node.color = colorInfo.color; - node.colorIndex = colorInfo.colorIndex; - if (point) { - point.color = node.color; - point.colorIndex = node.colorIndex; - // Set slicing on node, but avoid slicing the top node. - node.sliced = (node.id !== options.idRoot) ? point.sliced : false; - } - return node; + var mapIdToNode = options.mapIdToNode, + nodeParent = mapIdToNode[node.parent], + series = options.series, + chart = series.chart, + points = series.points, + point = points[node.i], + colorInfo = getColor(node, { + colors: chart && chart.options && chart.options.colors, + colorIndex: series.colorIndex, + index: options.index, + mapOptionsToLevel: options.mapOptionsToLevel, + parentColor: nodeParent && nodeParent.color, + parentColorIndex: nodeParent && nodeParent.colorIndex, + series: options.series, + siblings: options.siblings + }); + node.color = colorInfo.color; + node.colorIndex = colorInfo.colorIndex; + if (point) { + point.color = node.color; + point.colorIndex = node.colorIndex; + // Set slicing on node, but avoid slicing the top node. + node.sliced = (node.id !== options.idRoot) ? point.sliced : false; + } + return node; }; /** @@ -347,515 +347,515 @@ var cbSetTreeValuesBefore = function before(node, options) { */ var sunburstOptions = { - /** - * Set options on specific levels. Takes precedence over series options, - * but not point options. - * - * @type {Array} - * @sample highcharts/demo/sunburst Sunburst chart - * @apioption plotOptions.sunburst.levels - */ - - /** - * Can set a `borderColor` on all points which lies on the same level. - * - * @type {Color} - * @apioption plotOptions.sunburst.levels.borderColor - */ - - /** - * Can set a `borderWidth` on all points which lies on the same level. - * - * @type {Number} - * @apioption plotOptions.sunburst.levels.borderWidth - */ - - /** - * Can set a `borderDashStyle` on all points which lies on the same level. - * - * @type {String} - * @apioption plotOptions.sunburst.levels.borderDashStyle - */ - - /** - * Can set a `color` on all points which lies on the same level. - * - * @type {Color} - * @apioption plotOptions.sunburst.levels.color - */ - - /** - * Can set a `colorVariation` on all points which lies on the same level. - * - * @type {Object} - * @apioption plotOptions.sunburst.levels.colorVariation - */ - - /** - * The key of a color variation. Currently supports `brightness` only. - * - * @type {String} - * @apioption plotOptions.sunburst.levels.colorVariation.key - */ - - /** - * The ending value of a color variation. The last sibling will receive this - * value. - * - * @type {Number} - * @apioption plotOptions.sunburst.levels.colorVariation.to - */ - - /** - * Can set a `dataLabels` on all points which lies on the same level. - * - * @type {Object} - * @apioption plotOptions.sunburst.levels.dataLabels - */ - - /** - * Can set a `levelSize` on all points which lies on the same level. - * - * @type {Object} - * @apioption plotOptions.sunburst.levels.levelSize - */ - - /** - * Can set a `rotation` on all points which lies on the same level. - * - * @type {Number} - * @apioption plotOptions.sunburst.levels.rotation - */ - - /** - * Can set a `rotationMode` on all points which lies on the same level. - * - * @type {String} - * @apioption plotOptions.sunburst.levels.rotationMode - */ - - /** - * When enabled the user can click on a point which is a parent and - * zoom in on its children. - * - * @sample highcharts/demo/sunburst - * Allow drill to node - * @type {Boolean} - * @default false - * @apioption plotOptions.sunburst.allowDrillToNode - */ - - /** - * The center of the sunburst chart relative to the plot area. Can be - * percentages or pixel values. - * - * @type {Array} - * @sample {highcharts} highcharts/plotoptions/pie-center/ - * Centered at 100, 100 - * @product highcharts - */ - center: ['50%', '50%'], - colorByPoint: false, - /** - * @extends plotOptions.series.dataLabels - * @excluding align,allowOverlap,staggerLines,step - */ - dataLabels: { - defer: true, - style: { - textOverflow: 'ellipsis' - }, - /** - * Decides how the data label will be rotated according to the perimeter - * of the sunburst. It can either be parallel or perpendicular to the - * perimeter. - * `series.rotation` takes precedence over `rotationMode`. - * @since 6.0.0 - * @validvalue ["perpendicular", "parallel"] - */ - rotationMode: 'perpendicular' - }, - /** - * Which point to use as a root in the visualization. - * - * @type {String|undefined} - * @default undefined - */ - rootId: undefined, - - /** - * Used together with the levels and `allowDrillToNode` options. When - * set to false the first level visible when drilling is considered - * to be level one. Otherwise the level will be the same as the tree - * structure. - */ - levelIsConstant: true, - /** - * Determines the width of the ring per level. - * @since 6.0.5 - * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/ - * Sunburst with various sizes per level - */ - levelSize: { - /** - * The value used for calculating the width of the ring. Its' affect is - * determined by `levelSize.unit`. - * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/ - * Sunburst with various sizes per level - */ - value: 1, - /** - * How to interpret `levelSize.value`. - * `percentage` gives a width relative to result of outer radius minus - * inner radius. - * `pixels` gives the ring a fixed width in pixels. - * `weight` takes the remaining width after percentage and pixels, and - * distributes it accross all "weighted" levels. The value relative to - * the sum of all weights determines the width. - * @validvalue ["percentage", "pixels", "weight"] - * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/ - * Sunburst with various sizes per level - */ - unit: 'weight' - }, - /** - * If a point is sliced, moved out from the center, how many pixels - * should it be moved?. - * - * @since 6.0.4 - * @sample highcharts/plotoptions/sunburst-sliced Sliced sunburst - */ - slicedOffset: 10 + /** + * Set options on specific levels. Takes precedence over series options, + * but not point options. + * + * @type {Array} + * @sample highcharts/demo/sunburst Sunburst chart + * @apioption plotOptions.sunburst.levels + */ + + /** + * Can set a `borderColor` on all points which lies on the same level. + * + * @type {Color} + * @apioption plotOptions.sunburst.levels.borderColor + */ + + /** + * Can set a `borderWidth` on all points which lies on the same level. + * + * @type {Number} + * @apioption plotOptions.sunburst.levels.borderWidth + */ + + /** + * Can set a `borderDashStyle` on all points which lies on the same level. + * + * @type {String} + * @apioption plotOptions.sunburst.levels.borderDashStyle + */ + + /** + * Can set a `color` on all points which lies on the same level. + * + * @type {Color} + * @apioption plotOptions.sunburst.levels.color + */ + + /** + * Can set a `colorVariation` on all points which lies on the same level. + * + * @type {Object} + * @apioption plotOptions.sunburst.levels.colorVariation + */ + + /** + * The key of a color variation. Currently supports `brightness` only. + * + * @type {String} + * @apioption plotOptions.sunburst.levels.colorVariation.key + */ + + /** + * The ending value of a color variation. The last sibling will receive this + * value. + * + * @type {Number} + * @apioption plotOptions.sunburst.levels.colorVariation.to + */ + + /** + * Can set a `dataLabels` on all points which lies on the same level. + * + * @type {Object} + * @apioption plotOptions.sunburst.levels.dataLabels + */ + + /** + * Can set a `levelSize` on all points which lies on the same level. + * + * @type {Object} + * @apioption plotOptions.sunburst.levels.levelSize + */ + + /** + * Can set a `rotation` on all points which lies on the same level. + * + * @type {Number} + * @apioption plotOptions.sunburst.levels.rotation + */ + + /** + * Can set a `rotationMode` on all points which lies on the same level. + * + * @type {String} + * @apioption plotOptions.sunburst.levels.rotationMode + */ + + /** + * When enabled the user can click on a point which is a parent and + * zoom in on its children. + * + * @sample highcharts/demo/sunburst + * Allow drill to node + * @type {Boolean} + * @default false + * @apioption plotOptions.sunburst.allowDrillToNode + */ + + /** + * The center of the sunburst chart relative to the plot area. Can be + * percentages or pixel values. + * + * @type {Array} + * @sample {highcharts} highcharts/plotoptions/pie-center/ + * Centered at 100, 100 + * @product highcharts + */ + center: ['50%', '50%'], + colorByPoint: false, + /** + * @extends plotOptions.series.dataLabels + * @excluding align,allowOverlap,staggerLines,step + */ + dataLabels: { + defer: true, + style: { + textOverflow: 'ellipsis' + }, + /** + * Decides how the data label will be rotated according to the perimeter + * of the sunburst. It can either be parallel or perpendicular to the + * perimeter. + * `series.rotation` takes precedence over `rotationMode`. + * @since 6.0.0 + * @validvalue ["perpendicular", "parallel"] + */ + rotationMode: 'perpendicular' + }, + /** + * Which point to use as a root in the visualization. + * + * @type {String|undefined} + * @default undefined + */ + rootId: undefined, + + /** + * Used together with the levels and `allowDrillToNode` options. When + * set to false the first level visible when drilling is considered + * to be level one. Otherwise the level will be the same as the tree + * structure. + */ + levelIsConstant: true, + /** + * Determines the width of the ring per level. + * @since 6.0.5 + * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/ + * Sunburst with various sizes per level + */ + levelSize: { + /** + * The value used for calculating the width of the ring. Its' affect is + * determined by `levelSize.unit`. + * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/ + * Sunburst with various sizes per level + */ + value: 1, + /** + * How to interpret `levelSize.value`. + * `percentage` gives a width relative to result of outer radius minus + * inner radius. + * `pixels` gives the ring a fixed width in pixels. + * `weight` takes the remaining width after percentage and pixels, and + * distributes it accross all "weighted" levels. The value relative to + * the sum of all weights determines the width. + * @validvalue ["percentage", "pixels", "weight"] + * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/ + * Sunburst with various sizes per level + */ + unit: 'weight' + }, + /** + * If a point is sliced, moved out from the center, how many pixels + * should it be moved?. + * + * @since 6.0.4 + * @sample highcharts/plotoptions/sunburst-sliced Sliced sunburst + */ + slicedOffset: 10 }; /** * Properties of the Sunburst series. */ var sunburstSeries = { - drawDataLabels: noop, // drawDataLabels is called in drawPoints - drawPoints: function drawPoints() { - var series = this, - mapOptionsToLevel = series.mapOptionsToLevel, - shapeRoot = series.shapeRoot, - group = series.group, - hasRendered = series.hasRendered, - idRoot = series.rootId, - idPreviousRoot = series.idPreviousRoot, - nodeMap = series.nodeMap, - nodePreviousRoot = nodeMap[idPreviousRoot], - shapePreviousRoot = nodePreviousRoot && nodePreviousRoot.shapeArgs, - points = series.points, - radians = series.startAndEndRadians, - chart = series.chart, - optionsChart = chart && chart.options && chart.options.chart || {}, - animation = ( - isBoolean(optionsChart.animation) ? - optionsChart.animation : - true - ), - positions = series.center, - center = { - x: positions[0], - y: positions[1] - }, - innerR = positions[3] / 2, - renderer = series.chart.renderer, - animateLabels, - animateLabelsCalled = false, - addedHack = false, - hackDataLabelAnimation = !!( - animation && - hasRendered && - idRoot !== idPreviousRoot && - series.dataLabelsGroup - ); - - if (hackDataLabelAnimation) { - series.dataLabelsGroup.attr({ opacity: 0 }); - animateLabels = function () { - var s = series; - animateLabelsCalled = true; - if (s.dataLabelsGroup) { - s.dataLabelsGroup.animate({ - opacity: 1, - visibility: 'visible' - }); - } - }; - } - each(points, function (point) { - var node = point.node, - level = mapOptionsToLevel[node.level], - shapeExisting = point.shapeExisting || {}, - shape = node.shapeArgs || {}, - animationInfo, - onComplete, - visible = !!(node.visible && node.shapeArgs); - if (hasRendered && animation) { - animationInfo = getAnimation(shape, { - center: center, - point: point, - radians: radians, - innerR: innerR, - idRoot: idRoot, - idPreviousRoot: idPreviousRoot, - shapeExisting: shapeExisting, - shapeRoot: shapeRoot, - shapePreviousRoot: shapePreviousRoot, - visible: visible - }); - } else { - // When animation is disabled, attr is called from animation. - animationInfo = { - to: shape, - from: {} - }; - } - extend(point, { - shapeExisting: shape, // Store for use in animation - tooltipPos: [shape.plotX, shape.plotY], - drillId: getDrillId(point, idRoot, nodeMap), - name: '' + (point.name || point.id || point.index), - plotX: shape.plotX, // used for data label position - plotY: shape.plotY, // used for data label position - value: node.val, - isNull: !visible // used for dataLabels & point.draw - }); - point.dlOptions = getDlOptions({ - level: level, - optionsPoint: point.options, - shapeArgs: shape - }); - if (!addedHack && visible) { - addedHack = true; - onComplete = animateLabels; - } - point.draw({ - animate: animationInfo.to, - attr: extend( - animationInfo.from, - series.pointAttribs && series.pointAttribs( - point, - point.selected && 'select' - ) - ), - onComplete: onComplete, - group: group, - renderer: renderer, - shapeType: 'arc', - shapeArgs: shape - }); - }); - // Draw data labels after points - // TODO draw labels one by one to avoid addtional looping - if (hackDataLabelAnimation && addedHack) { - series.hasRendered = false; - series.options.dataLabels.defer = true; - Series.prototype.drawDataLabels.call(series); - series.hasRendered = true; - // If animateLabels is called before labels were hidden, then call - // it again. - if (animateLabelsCalled) { - animateLabels(); - } - } else { - Series.prototype.drawDataLabels.call(series); - } - }, - /*= if (build.classic) { =*/ - pointAttribs: seriesTypes.column.prototype.pointAttribs, - /*= } =*/ - - /* - * The layout algorithm for the levels - */ - layoutAlgorithm: layoutAlgorithm, - /* - * Set the shape arguments on the nodes. Recursive from root down. - */ - setShapeArgs: function (parent, parentValues, mapOptionsToLevel) { - var childrenValues = [], - level = parent.level + 1, - options = mapOptionsToLevel[level], - // Collect all children which should be included - children = grep(parent.children, function (n) { - return n.visible; - }), - twoPi = 6.28; // Two times Pi. - childrenValues = this.layoutAlgorithm(parentValues, children, options); - each(children, function (child, index) { - var values = childrenValues[index], - angle = values.start + ((values.end - values.start) / 2), - radius = values.innerR + ((values.r - values.innerR) / 2), - radians = (values.end - values.start), - isCircle = (values.innerR === 0 && radians > twoPi), - center = ( - isCircle ? - { x: values.x, y: values.y } : - getEndPoint(values.x, values.y, angle, radius) - ), - val = ( - child.val ? - ( - child.childrenTotal > child.val ? - child.childrenTotal : - child.val - ) : - child.childrenTotal - ); - // The inner arc length is a convenience for data label filters. - if (this.points[child.i]) { - this.points[child.i].innerArcLength = radians * values.innerR; - this.points[child.i].outerArcLength = radians * values.r; - } - - child.shapeArgs = merge(values, { - plotX: center.x, - plotY: center.y - }); - child.values = merge(values, { - val: val - }); - // If node has children, then call method recursively - if (child.children.length) { - this.setShapeArgs(child, child.values, mapOptionsToLevel); - } - }, this); - }, - - - translate: function translate() { - var series = this, - options = series.options, - positions = series.center = getCenter.call(series), - radians = series.startAndEndRadians = getStartAndEndRadians( - options.startAngle, - options.endAngle - ), - innerRadius = positions[3] / 2, - outerRadius = positions[2] / 2, - diffRadius = outerRadius - innerRadius, - // NOTE: updateRootId modifies series. - rootId = updateRootId(series), - mapIdToNode = series.nodeMap, - mapOptionsToLevel, - idTop, - nodeRoot = mapIdToNode && mapIdToNode[rootId], - nodeTop, - tree, - values; - series.shapeRoot = nodeRoot && nodeRoot.shapeArgs; - // Call prototype function - Series.prototype.translate.call(series); - // @todo Only if series.isDirtyData is true - tree = series.tree = series.getTree(); - mapIdToNode = series.nodeMap; - nodeRoot = mapIdToNode[rootId]; - idTop = isString(nodeRoot.parent) ? nodeRoot.parent : ''; - nodeTop = mapIdToNode[idTop]; - mapOptionsToLevel = getLevelOptions({ - from: nodeRoot.level > 0 ? nodeRoot.level : 1, - levels: series.options.levels, - to: tree.height, - defaults: { - colorByPoint: options.colorByPoint, - dataLabels: options.dataLabels, - levelIsConstant: options.levelIsConstant, - levelSize: options.levelSize, - slicedOffset: options.slicedOffset - } - }); - // NOTE consider doing calculateLevelSizes in a callback to - // getLevelOptions - mapOptionsToLevel = calculateLevelSizes(mapOptionsToLevel, { - diffRadius: diffRadius, - from: nodeRoot.level > 0 ? nodeRoot.level : 1, - to: tree.height - }); - // TODO Try to combine setTreeValues & setColorRecursive to avoid - // unnecessary looping. - setTreeValues(tree, { - before: cbSetTreeValuesBefore, - idRoot: rootId, - levelIsConstant: options.levelIsConstant, - mapOptionsToLevel: mapOptionsToLevel, - mapIdToNode: mapIdToNode, - points: series.points, - series: series - }); - values = mapIdToNode[''].shapeArgs = { - end: radians.end, - r: innerRadius, - start: radians.start, - val: nodeRoot.val, - x: positions[0], - y: positions[1] - }; - this.setShapeArgs(nodeTop, values, mapOptionsToLevel); - // Set mapOptionsToLevel on series for use in drawPoints. - series.mapOptionsToLevel = mapOptionsToLevel; - }, - - /** - * Animate the slices in. Similar to the animation of polar charts. - */ - animate: function (init) { - var chart = this.chart, - center = [ - chart.plotWidth / 2, - chart.plotHeight / 2 - ], - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - attribs, - group = this.group; - - // Initialize the animation - if (init) { - - // Scale down the group and place it in the center - attribs = { - translateX: center[0] + plotLeft, - translateY: center[1] + plotTop, - scaleX: 0.001, // #1499 - scaleY: 0.001, - rotation: 10, - opacity: 0.01 - }; - - group.attr(attribs); - - // Run the animation - } else { - attribs = { - translateX: plotLeft, - translateY: plotTop, - scaleX: 1, - scaleY: 1, - rotation: 0, - opacity: 1 - }; - group.animate(attribs, this.options.animation); - - // Delete this function to allow it only once - this.animate = null; - } - }, - utils: { - calculateLevelSizes: calculateLevelSizes, - range: range - } + drawDataLabels: noop, // drawDataLabels is called in drawPoints + drawPoints: function drawPoints() { + var series = this, + mapOptionsToLevel = series.mapOptionsToLevel, + shapeRoot = series.shapeRoot, + group = series.group, + hasRendered = series.hasRendered, + idRoot = series.rootId, + idPreviousRoot = series.idPreviousRoot, + nodeMap = series.nodeMap, + nodePreviousRoot = nodeMap[idPreviousRoot], + shapePreviousRoot = nodePreviousRoot && nodePreviousRoot.shapeArgs, + points = series.points, + radians = series.startAndEndRadians, + chart = series.chart, + optionsChart = chart && chart.options && chart.options.chart || {}, + animation = ( + isBoolean(optionsChart.animation) ? + optionsChart.animation : + true + ), + positions = series.center, + center = { + x: positions[0], + y: positions[1] + }, + innerR = positions[3] / 2, + renderer = series.chart.renderer, + animateLabels, + animateLabelsCalled = false, + addedHack = false, + hackDataLabelAnimation = !!( + animation && + hasRendered && + idRoot !== idPreviousRoot && + series.dataLabelsGroup + ); + + if (hackDataLabelAnimation) { + series.dataLabelsGroup.attr({ opacity: 0 }); + animateLabels = function () { + var s = series; + animateLabelsCalled = true; + if (s.dataLabelsGroup) { + s.dataLabelsGroup.animate({ + opacity: 1, + visibility: 'visible' + }); + } + }; + } + each(points, function (point) { + var node = point.node, + level = mapOptionsToLevel[node.level], + shapeExisting = point.shapeExisting || {}, + shape = node.shapeArgs || {}, + animationInfo, + onComplete, + visible = !!(node.visible && node.shapeArgs); + if (hasRendered && animation) { + animationInfo = getAnimation(shape, { + center: center, + point: point, + radians: radians, + innerR: innerR, + idRoot: idRoot, + idPreviousRoot: idPreviousRoot, + shapeExisting: shapeExisting, + shapeRoot: shapeRoot, + shapePreviousRoot: shapePreviousRoot, + visible: visible + }); + } else { + // When animation is disabled, attr is called from animation. + animationInfo = { + to: shape, + from: {} + }; + } + extend(point, { + shapeExisting: shape, // Store for use in animation + tooltipPos: [shape.plotX, shape.plotY], + drillId: getDrillId(point, idRoot, nodeMap), + name: '' + (point.name || point.id || point.index), + plotX: shape.plotX, // used for data label position + plotY: shape.plotY, // used for data label position + value: node.val, + isNull: !visible // used for dataLabels & point.draw + }); + point.dlOptions = getDlOptions({ + level: level, + optionsPoint: point.options, + shapeArgs: shape + }); + if (!addedHack && visible) { + addedHack = true; + onComplete = animateLabels; + } + point.draw({ + animate: animationInfo.to, + attr: extend( + animationInfo.from, + series.pointAttribs && series.pointAttribs( + point, + point.selected && 'select' + ) + ), + onComplete: onComplete, + group: group, + renderer: renderer, + shapeType: 'arc', + shapeArgs: shape + }); + }); + // Draw data labels after points + // TODO draw labels one by one to avoid addtional looping + if (hackDataLabelAnimation && addedHack) { + series.hasRendered = false; + series.options.dataLabels.defer = true; + Series.prototype.drawDataLabels.call(series); + series.hasRendered = true; + // If animateLabels is called before labels were hidden, then call + // it again. + if (animateLabelsCalled) { + animateLabels(); + } + } else { + Series.prototype.drawDataLabels.call(series); + } + }, + /*= if (build.classic) { =*/ + pointAttribs: seriesTypes.column.prototype.pointAttribs, + /*= } =*/ + + /* + * The layout algorithm for the levels + */ + layoutAlgorithm: layoutAlgorithm, + /* + * Set the shape arguments on the nodes. Recursive from root down. + */ + setShapeArgs: function (parent, parentValues, mapOptionsToLevel) { + var childrenValues = [], + level = parent.level + 1, + options = mapOptionsToLevel[level], + // Collect all children which should be included + children = grep(parent.children, function (n) { + return n.visible; + }), + twoPi = 6.28; // Two times Pi. + childrenValues = this.layoutAlgorithm(parentValues, children, options); + each(children, function (child, index) { + var values = childrenValues[index], + angle = values.start + ((values.end - values.start) / 2), + radius = values.innerR + ((values.r - values.innerR) / 2), + radians = (values.end - values.start), + isCircle = (values.innerR === 0 && radians > twoPi), + center = ( + isCircle ? + { x: values.x, y: values.y } : + getEndPoint(values.x, values.y, angle, radius) + ), + val = ( + child.val ? + ( + child.childrenTotal > child.val ? + child.childrenTotal : + child.val + ) : + child.childrenTotal + ); + // The inner arc length is a convenience for data label filters. + if (this.points[child.i]) { + this.points[child.i].innerArcLength = radians * values.innerR; + this.points[child.i].outerArcLength = radians * values.r; + } + + child.shapeArgs = merge(values, { + plotX: center.x, + plotY: center.y + }); + child.values = merge(values, { + val: val + }); + // If node has children, then call method recursively + if (child.children.length) { + this.setShapeArgs(child, child.values, mapOptionsToLevel); + } + }, this); + }, + + + translate: function translate() { + var series = this, + options = series.options, + positions = series.center = getCenter.call(series), + radians = series.startAndEndRadians = getStartAndEndRadians( + options.startAngle, + options.endAngle + ), + innerRadius = positions[3] / 2, + outerRadius = positions[2] / 2, + diffRadius = outerRadius - innerRadius, + // NOTE: updateRootId modifies series. + rootId = updateRootId(series), + mapIdToNode = series.nodeMap, + mapOptionsToLevel, + idTop, + nodeRoot = mapIdToNode && mapIdToNode[rootId], + nodeTop, + tree, + values; + series.shapeRoot = nodeRoot && nodeRoot.shapeArgs; + // Call prototype function + Series.prototype.translate.call(series); + // @todo Only if series.isDirtyData is true + tree = series.tree = series.getTree(); + mapIdToNode = series.nodeMap; + nodeRoot = mapIdToNode[rootId]; + idTop = isString(nodeRoot.parent) ? nodeRoot.parent : ''; + nodeTop = mapIdToNode[idTop]; + mapOptionsToLevel = getLevelOptions({ + from: nodeRoot.level > 0 ? nodeRoot.level : 1, + levels: series.options.levels, + to: tree.height, + defaults: { + colorByPoint: options.colorByPoint, + dataLabels: options.dataLabels, + levelIsConstant: options.levelIsConstant, + levelSize: options.levelSize, + slicedOffset: options.slicedOffset + } + }); + // NOTE consider doing calculateLevelSizes in a callback to + // getLevelOptions + mapOptionsToLevel = calculateLevelSizes(mapOptionsToLevel, { + diffRadius: diffRadius, + from: nodeRoot.level > 0 ? nodeRoot.level : 1, + to: tree.height + }); + // TODO Try to combine setTreeValues & setColorRecursive to avoid + // unnecessary looping. + setTreeValues(tree, { + before: cbSetTreeValuesBefore, + idRoot: rootId, + levelIsConstant: options.levelIsConstant, + mapOptionsToLevel: mapOptionsToLevel, + mapIdToNode: mapIdToNode, + points: series.points, + series: series + }); + values = mapIdToNode[''].shapeArgs = { + end: radians.end, + r: innerRadius, + start: radians.start, + val: nodeRoot.val, + x: positions[0], + y: positions[1] + }; + this.setShapeArgs(nodeTop, values, mapOptionsToLevel); + // Set mapOptionsToLevel on series for use in drawPoints. + series.mapOptionsToLevel = mapOptionsToLevel; + }, + + /** + * Animate the slices in. Similar to the animation of polar charts. + */ + animate: function (init) { + var chart = this.chart, + center = [ + chart.plotWidth / 2, + chart.plotHeight / 2 + ], + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + attribs, + group = this.group; + + // Initialize the animation + if (init) { + + // Scale down the group and place it in the center + attribs = { + translateX: center[0] + plotLeft, + translateY: center[1] + plotTop, + scaleX: 0.001, // #1499 + scaleY: 0.001, + rotation: 10, + opacity: 0.01 + }; + + group.attr(attribs); + + // Run the animation + } else { + attribs = { + translateX: plotLeft, + translateY: plotTop, + scaleX: 1, + scaleY: 1, + rotation: 0, + opacity: 1 + }; + group.animate(attribs, this.options.animation); + + // Delete this function to allow it only once + this.animate = null; + } + }, + utils: { + calculateLevelSizes: calculateLevelSizes, + range: range + } }; /** * Properties of the Sunburst series. */ var sunburstPoint = { - draw: drawPoint, - shouldDraw: function shouldDraw() { - var point = this; - return !point.isNull; - } + draw: drawPoint, + shouldDraw: function shouldDraw() { + var point = this; + return !point.isNull; + } }; /** @@ -901,7 +901,7 @@ var sunburstPoint = { */ /** - * Whether to display a slice offset from the center. When a sunburst point is + * Whether to display a slice offset from the center. When a sunburst point is * sliced, its children are also offset. * * @type {Boolean} @@ -912,9 +912,9 @@ var sunburstPoint = { * @apioption series.sunburst.data.sliced */ seriesType( - 'sunburst', - 'treemap', - sunburstOptions, - sunburstSeries, - sunburstPoint + 'sunburst', + 'treemap', + sunburstOptions, + sunburstSeries, + sunburstPoint ); diff --git a/js/modules/tilemap.src.js b/js/modules/tilemap.src.js index 1d5df1d13e4..fb6b12b3283 100644 --- a/js/modules/tilemap.src.js +++ b/js/modules/tilemap.src.js @@ -11,413 +11,413 @@ import H from '../parts/Globals.js'; import '../parts-map/HeatmapSeries.js'; var seriesType = H.seriesType, - each = H.each, - reduce = H.reduce, - pick = H.pick, - // Utility func to get the middle number of 3 - between = function (x, a, b) { - return Math.min(Math.max(a, x), b); - }, - // Utility func to get padding definition from tile size division - tilePaddingFromTileSize = function (series, xDiv, yDiv) { - var options = series.options; - return { - xPad: (options.colsize || 1) / -xDiv, - yPad: (options.rowsize || 1) / -yDiv - }; - }; + each = H.each, + reduce = H.reduce, + pick = H.pick, + // Utility func to get the middle number of 3 + between = function (x, a, b) { + return Math.min(Math.max(a, x), b); + }, + // Utility func to get padding definition from tile size division + tilePaddingFromTileSize = function (series, xDiv, yDiv) { + var options = series.options; + return { + xPad: (options.colsize || 1) / -xDiv, + yPad: (options.rowsize || 1) / -yDiv + }; + }; // Map of shape types H.tileShapeTypes = { - /** Hexagon shape type **/ - hexagon: { - alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel, - getSeriesPadding: function (series) { - return tilePaddingFromTileSize(series, 3, 2); - }, - haloPath: function (size) { - if (!size) { - return []; - } - var hexagon = this.tileEdges; - return [ - 'M', hexagon.x2 - size, hexagon.y1 + size, - 'L', hexagon.x3 + size, hexagon.y1 + size, - hexagon.x4 + size * 1.5, hexagon.y2, - hexagon.x3 + size, hexagon.y3 - size, - hexagon.x2 - size, hexagon.y3 - size, - hexagon.x1 - size * 1.5, hexagon.y2, - 'Z' - ]; - }, - translate: function () { - var series = this, - options = series.options, - xAxis = series.xAxis, - yAxis = series.yAxis, - seriesPointPadding = options.pointPadding || 0, - xPad = (options.colsize || 1) / 3, - yPad = (options.rowsize || 1) / 2, - yShift; - - series.generatePoints(); - - each(series.points, function (point) { - var x1 = between( - Math.floor( - xAxis.len - - xAxis.translate(point.x - xPad * 2, 0, 1, 0, 1) - ), -xAxis.len, 2 * xAxis.len - ), - x2 = between( - Math.floor( - xAxis.len - - xAxis.translate(point.x - xPad, 0, 1, 0, 1) - ), -xAxis.len, 2 * xAxis.len - ), - x3 = between( - Math.floor( - xAxis.len - - xAxis.translate(point.x + xPad, 0, 1, 0, 1) - ), -xAxis.len, 2 * xAxis.len - ), - x4 = between( - Math.floor( - xAxis.len - - xAxis.translate(point.x + xPad * 2, 0, 1, 0, 1) - ), -xAxis.len, 2 * xAxis.len - ), - y1 = between( - Math.floor(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), - -yAxis.len, - 2 * yAxis.len - ), - y2 = between( - Math.floor(yAxis.translate(point.y, 0, 1, 0, 1)), - -yAxis.len, - 2 * yAxis.len - ), - y3 = between( - Math.floor(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), - -yAxis.len, - 2 * yAxis.len - ), - pointPadding = pick(point.pointPadding, seriesPointPadding), - // We calculate the point padding of the midpoints to - // preserve the angles of the shape. - midPointPadding = pointPadding * - Math.abs(x2 - x1) / Math.abs(y3 - y2), - xMidPadding = xAxis.reversed ? - -midPointPadding : midPointPadding, - xPointPadding = xAxis.reversed ? - -pointPadding : pointPadding, - yPointPadding = yAxis.reversed ? - -pointPadding : pointPadding; - - // Shift y-values for every second grid column - if (point.x % 2) { - yShift = yShift || Math.round(Math.abs(y3 - y1) / 2) * - // We have to reverse the shift for reversed y-axes - (yAxis.reversed ? -1 : 1); - y1 += yShift; - y2 += yShift; - y3 += yShift; - } - - // Set plotX and plotY for use in K-D-Tree and more - point.plotX = point.clientX = (x2 + x3) / 2; - point.plotY = y2; - - // Apply point padding to translated coordinates - x1 += xMidPadding + xPointPadding; - x2 += xPointPadding; - x3 -= xPointPadding; - x4 -= xMidPadding + xPointPadding; - y1 -= yPointPadding; - y3 += yPointPadding; - - // Store points for halo creation - point.tileEdges = { - x1: x1, x2: x2, x3: x3, x4: x4, y1: y1, y2: y2, y3: y3 - }; - - // Finally set the shape for this point - point.shapeType = 'path'; - point.shapeArgs = { - d: [ - 'M', x2, y1, - 'L', x3, y1, - x4, y2, - x3, y3, - x2, y3, - x1, y2, - 'Z' - ] - }; - }); - - series.translateColors(); - } - }, - - - /** Diamond shape type **/ - diamond: { - alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel, - getSeriesPadding: function (series) { - return tilePaddingFromTileSize(series, 2, 2); - }, - haloPath: function (size) { - if (!size) { - return []; - } - var diamond = this.tileEdges; - return [ - 'M', diamond.x2, diamond.y1 + size, - 'L', diamond.x3 + size, diamond.y2, - diamond.x2, diamond.y3 - size, - diamond.x1 - size, diamond.y2, - 'Z' - ]; - }, - translate: function () { - var series = this, - options = series.options, - xAxis = series.xAxis, - yAxis = series.yAxis, - seriesPointPadding = options.pointPadding || 0, - xPad = (options.colsize || 1), - yPad = (options.rowsize || 1) / 2, - yShift; - - series.generatePoints(); - - each(series.points, function (point) { - var x1 = between( - Math.round( - xAxis.len - - xAxis.translate(point.x - xPad, 0, 1, 0, 0) - ), -xAxis.len, 2 * xAxis.len - ), - x2 = between( - Math.round( - xAxis.len - - xAxis.translate(point.x, 0, 1, 0, 0) - ), -xAxis.len, 2 * xAxis.len - ), - x3 = between( - Math.round( - xAxis.len - - xAxis.translate(point.x + xPad, 0, 1, 0, 0) - ), -xAxis.len, 2 * xAxis.len - ), - y1 = between( - Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 0)), - -yAxis.len, - 2 * yAxis.len - ), - y2 = between( - Math.round(yAxis.translate(point.y, 0, 1, 0, 0)), - -yAxis.len, - 2 * yAxis.len - ), - y3 = between( - Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 0)), - -yAxis.len, - 2 * yAxis.len - ), - pointPadding = pick(point.pointPadding, seriesPointPadding), - // We calculate the point padding of the midpoints to - // preserve the angles of the shape. - midPointPadding = pointPadding * - Math.abs(x2 - x1) / Math.abs(y3 - y2), - xPointPadding = xAxis.reversed ? - -midPointPadding : midPointPadding, - yPointPadding = yAxis.reversed ? - -pointPadding : pointPadding; - - // Shift y-values for every second grid column - // We have to reverse the shift for reversed y-axes - if (point.x % 2) { - yShift = Math.abs(y3 - y1) / 2 * (yAxis.reversed ? -1 : 1); - y1 += yShift; - y2 += yShift; - y3 += yShift; - } - - // Set plotX and plotY for use in K-D-Tree and more - point.plotX = point.clientX = x2; - point.plotY = y2; - - // Apply point padding to translated coordinates - x1 += xPointPadding; - x3 -= xPointPadding; - y1 -= yPointPadding; - y3 += yPointPadding; - - // Store points for halo creation - point.tileEdges = { - x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3 - }; - - // Set this point's shape parameters - point.shapeType = 'path'; - point.shapeArgs = { - d: [ - 'M', x2, y1, - 'L', x3, y2, - x2, y3, - x1, y2, - 'Z' - ] - }; - }); - - series.translateColors(); - } - }, - - - /** Circle shape type **/ - circle: { - alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel, - getSeriesPadding: function (series) { - return tilePaddingFromTileSize(series, 2, 2); - }, - haloPath: function (size) { - return H.seriesTypes.scatter.prototype.pointClass.prototype.haloPath - .call(this, - size + (size && this.radius) - ); - }, - translate: function () { - var series = this, - options = series.options, - xAxis = series.xAxis, - yAxis = series.yAxis, - seriesPointPadding = options.pointPadding || 0, - yRadius = (options.rowsize || 1) / 2, - colsize = (options.colsize || 1), - colsizePx, - yRadiusPx, - xRadiusPx, - radius, - forceNextRadiusCompute = false; - - series.generatePoints(); - - each(series.points, function (point) { - var x = between( - Math.round( - xAxis.len - - xAxis.translate(point.x, 0, 1, 0, 0) - ), -xAxis.len, 2 * xAxis.len - ), - y = between( - Math.round(yAxis.translate(point.y, 0, 1, 0, 0)), - -yAxis.len, - 2 * yAxis.len - ), - pointPadding = seriesPointPadding, - hasPerPointPadding = false; - - // If there is point padding defined on a single point, add it - if (point.pointPadding !== undefined) { - pointPadding = point.pointPadding; - hasPerPointPadding = true; - forceNextRadiusCompute = true; - } - - // Find radius if not found already. - // Use the smallest one (x vs y) to avoid overlap. - // Note that the radius will be recomputed for each series. - // Ideal (max) x radius is dependent on y radius: - /* - * (circle 2) - - * (circle 3) - | yRadiusPx - (circle 1) *-------| - colsizePx - - The distance between circle 1 and 3 (and circle 2 and 3) is - 2r, which is the hypotenuse of the triangle created by - colsizePx and yRadiusPx. If the distance between circle 2 - and circle 1 is less than 2r, we use half of that distance - instead (yRadiusPx). - */ - if (!radius || forceNextRadiusCompute) { - colsizePx = Math.abs( - between( - Math.floor( - xAxis.len - - xAxis.translate(point.x + colsize, 0, 1, 0, 0) - ), -xAxis.len, 2 * xAxis.len - ) - x - ); - yRadiusPx = Math.abs( - between( - Math.floor( - yAxis.translate(point.y + yRadius, 0, 1, 0, 0) - ), -yAxis.len, 2 * yAxis.len - ) - y - ); - xRadiusPx = Math.floor( - Math.sqrt( - (colsizePx * colsizePx + yRadiusPx * yRadiusPx) - ) / 2 - ); - radius = Math.min( - colsizePx, xRadiusPx, yRadiusPx - ) - pointPadding; - - // If we have per point padding we need to always compute - // the radius for this point and the next. If we used to - // have per point padding but don't anymore, don't force - // compute next radius. - if (forceNextRadiusCompute && !hasPerPointPadding) { - forceNextRadiusCompute = false; - } - } - - // Shift y-values for every second grid column. - // Note that we always use the optimal y axis radius for this. - // Also note: We have to reverse the shift for reversed y-axes. - if (point.x % 2) { - y += yRadiusPx * (yAxis.reversed ? -1 : 1); - } - - // Set plotX and plotY for use in K-D-Tree and more - point.plotX = point.clientX = x; - point.plotY = y; - - // Save radius for halo - point.radius = radius; - - // Set this point's shape parameters - point.shapeType = 'circle'; - point.shapeArgs = { - x: x, - y: y, - r: radius - }; - }); - - series.translateColors(); - } - }, - - - /** Square shape type **/ - square: { - alignDataLabel: H.seriesTypes.heatmap.prototype.alignDataLabel, - translate: H.seriesTypes.heatmap.prototype.translate, - getSeriesPadding: function () { - return; - }, - haloPath: H.seriesTypes.heatmap.prototype.pointClass.prototype.haloPath - } + /** Hexagon shape type **/ + hexagon: { + alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel, + getSeriesPadding: function (series) { + return tilePaddingFromTileSize(series, 3, 2); + }, + haloPath: function (size) { + if (!size) { + return []; + } + var hexagon = this.tileEdges; + return [ + 'M', hexagon.x2 - size, hexagon.y1 + size, + 'L', hexagon.x3 + size, hexagon.y1 + size, + hexagon.x4 + size * 1.5, hexagon.y2, + hexagon.x3 + size, hexagon.y3 - size, + hexagon.x2 - size, hexagon.y3 - size, + hexagon.x1 - size * 1.5, hexagon.y2, + 'Z' + ]; + }, + translate: function () { + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + seriesPointPadding = options.pointPadding || 0, + xPad = (options.colsize || 1) / 3, + yPad = (options.rowsize || 1) / 2, + yShift; + + series.generatePoints(); + + each(series.points, function (point) { + var x1 = between( + Math.floor( + xAxis.len - + xAxis.translate(point.x - xPad * 2, 0, 1, 0, 1) + ), -xAxis.len, 2 * xAxis.len + ), + x2 = between( + Math.floor( + xAxis.len - + xAxis.translate(point.x - xPad, 0, 1, 0, 1) + ), -xAxis.len, 2 * xAxis.len + ), + x3 = between( + Math.floor( + xAxis.len - + xAxis.translate(point.x + xPad, 0, 1, 0, 1) + ), -xAxis.len, 2 * xAxis.len + ), + x4 = between( + Math.floor( + xAxis.len - + xAxis.translate(point.x + xPad * 2, 0, 1, 0, 1) + ), -xAxis.len, 2 * xAxis.len + ), + y1 = between( + Math.floor(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), + -yAxis.len, + 2 * yAxis.len + ), + y2 = between( + Math.floor(yAxis.translate(point.y, 0, 1, 0, 1)), + -yAxis.len, + 2 * yAxis.len + ), + y3 = between( + Math.floor(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), + -yAxis.len, + 2 * yAxis.len + ), + pointPadding = pick(point.pointPadding, seriesPointPadding), + // We calculate the point padding of the midpoints to + // preserve the angles of the shape. + midPointPadding = pointPadding * + Math.abs(x2 - x1) / Math.abs(y3 - y2), + xMidPadding = xAxis.reversed ? + -midPointPadding : midPointPadding, + xPointPadding = xAxis.reversed ? + -pointPadding : pointPadding, + yPointPadding = yAxis.reversed ? + -pointPadding : pointPadding; + + // Shift y-values for every second grid column + if (point.x % 2) { + yShift = yShift || Math.round(Math.abs(y3 - y1) / 2) * + // We have to reverse the shift for reversed y-axes + (yAxis.reversed ? -1 : 1); + y1 += yShift; + y2 += yShift; + y3 += yShift; + } + + // Set plotX and plotY for use in K-D-Tree and more + point.plotX = point.clientX = (x2 + x3) / 2; + point.plotY = y2; + + // Apply point padding to translated coordinates + x1 += xMidPadding + xPointPadding; + x2 += xPointPadding; + x3 -= xPointPadding; + x4 -= xMidPadding + xPointPadding; + y1 -= yPointPadding; + y3 += yPointPadding; + + // Store points for halo creation + point.tileEdges = { + x1: x1, x2: x2, x3: x3, x4: x4, y1: y1, y2: y2, y3: y3 + }; + + // Finally set the shape for this point + point.shapeType = 'path'; + point.shapeArgs = { + d: [ + 'M', x2, y1, + 'L', x3, y1, + x4, y2, + x3, y3, + x2, y3, + x1, y2, + 'Z' + ] + }; + }); + + series.translateColors(); + } + }, + + + /** Diamond shape type **/ + diamond: { + alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel, + getSeriesPadding: function (series) { + return tilePaddingFromTileSize(series, 2, 2); + }, + haloPath: function (size) { + if (!size) { + return []; + } + var diamond = this.tileEdges; + return [ + 'M', diamond.x2, diamond.y1 + size, + 'L', diamond.x3 + size, diamond.y2, + diamond.x2, diamond.y3 - size, + diamond.x1 - size, diamond.y2, + 'Z' + ]; + }, + translate: function () { + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + seriesPointPadding = options.pointPadding || 0, + xPad = (options.colsize || 1), + yPad = (options.rowsize || 1) / 2, + yShift; + + series.generatePoints(); + + each(series.points, function (point) { + var x1 = between( + Math.round( + xAxis.len - + xAxis.translate(point.x - xPad, 0, 1, 0, 0) + ), -xAxis.len, 2 * xAxis.len + ), + x2 = between( + Math.round( + xAxis.len - + xAxis.translate(point.x, 0, 1, 0, 0) + ), -xAxis.len, 2 * xAxis.len + ), + x3 = between( + Math.round( + xAxis.len - + xAxis.translate(point.x + xPad, 0, 1, 0, 0) + ), -xAxis.len, 2 * xAxis.len + ), + y1 = between( + Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 0)), + -yAxis.len, + 2 * yAxis.len + ), + y2 = between( + Math.round(yAxis.translate(point.y, 0, 1, 0, 0)), + -yAxis.len, + 2 * yAxis.len + ), + y3 = between( + Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 0)), + -yAxis.len, + 2 * yAxis.len + ), + pointPadding = pick(point.pointPadding, seriesPointPadding), + // We calculate the point padding of the midpoints to + // preserve the angles of the shape. + midPointPadding = pointPadding * + Math.abs(x2 - x1) / Math.abs(y3 - y2), + xPointPadding = xAxis.reversed ? + -midPointPadding : midPointPadding, + yPointPadding = yAxis.reversed ? + -pointPadding : pointPadding; + + // Shift y-values for every second grid column + // We have to reverse the shift for reversed y-axes + if (point.x % 2) { + yShift = Math.abs(y3 - y1) / 2 * (yAxis.reversed ? -1 : 1); + y1 += yShift; + y2 += yShift; + y3 += yShift; + } + + // Set plotX and plotY for use in K-D-Tree and more + point.plotX = point.clientX = x2; + point.plotY = y2; + + // Apply point padding to translated coordinates + x1 += xPointPadding; + x3 -= xPointPadding; + y1 -= yPointPadding; + y3 += yPointPadding; + + // Store points for halo creation + point.tileEdges = { + x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3 + }; + + // Set this point's shape parameters + point.shapeType = 'path'; + point.shapeArgs = { + d: [ + 'M', x2, y1, + 'L', x3, y2, + x2, y3, + x1, y2, + 'Z' + ] + }; + }); + + series.translateColors(); + } + }, + + + /** Circle shape type **/ + circle: { + alignDataLabel: H.seriesTypes.scatter.prototype.alignDataLabel, + getSeriesPadding: function (series) { + return tilePaddingFromTileSize(series, 2, 2); + }, + haloPath: function (size) { + return H.seriesTypes.scatter.prototype.pointClass.prototype.haloPath + .call(this, + size + (size && this.radius) + ); + }, + translate: function () { + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + seriesPointPadding = options.pointPadding || 0, + yRadius = (options.rowsize || 1) / 2, + colsize = (options.colsize || 1), + colsizePx, + yRadiusPx, + xRadiusPx, + radius, + forceNextRadiusCompute = false; + + series.generatePoints(); + + each(series.points, function (point) { + var x = between( + Math.round( + xAxis.len - + xAxis.translate(point.x, 0, 1, 0, 0) + ), -xAxis.len, 2 * xAxis.len + ), + y = between( + Math.round(yAxis.translate(point.y, 0, 1, 0, 0)), + -yAxis.len, + 2 * yAxis.len + ), + pointPadding = seriesPointPadding, + hasPerPointPadding = false; + + // If there is point padding defined on a single point, add it + if (point.pointPadding !== undefined) { + pointPadding = point.pointPadding; + hasPerPointPadding = true; + forceNextRadiusCompute = true; + } + + // Find radius if not found already. + // Use the smallest one (x vs y) to avoid overlap. + // Note that the radius will be recomputed for each series. + // Ideal (max) x radius is dependent on y radius: + /* + * (circle 2) + + * (circle 3) + | yRadiusPx + (circle 1) *-------| + colsizePx + + The distance between circle 1 and 3 (and circle 2 and 3) is + 2r, which is the hypotenuse of the triangle created by + colsizePx and yRadiusPx. If the distance between circle 2 + and circle 1 is less than 2r, we use half of that distance + instead (yRadiusPx). + */ + if (!radius || forceNextRadiusCompute) { + colsizePx = Math.abs( + between( + Math.floor( + xAxis.len - + xAxis.translate(point.x + colsize, 0, 1, 0, 0) + ), -xAxis.len, 2 * xAxis.len + ) - x + ); + yRadiusPx = Math.abs( + between( + Math.floor( + yAxis.translate(point.y + yRadius, 0, 1, 0, 0) + ), -yAxis.len, 2 * yAxis.len + ) - y + ); + xRadiusPx = Math.floor( + Math.sqrt( + (colsizePx * colsizePx + yRadiusPx * yRadiusPx) + ) / 2 + ); + radius = Math.min( + colsizePx, xRadiusPx, yRadiusPx + ) - pointPadding; + + // If we have per point padding we need to always compute + // the radius for this point and the next. If we used to + // have per point padding but don't anymore, don't force + // compute next radius. + if (forceNextRadiusCompute && !hasPerPointPadding) { + forceNextRadiusCompute = false; + } + } + + // Shift y-values for every second grid column. + // Note that we always use the optimal y axis radius for this. + // Also note: We have to reverse the shift for reversed y-axes. + if (point.x % 2) { + y += yRadiusPx * (yAxis.reversed ? -1 : 1); + } + + // Set plotX and plotY for use in K-D-Tree and more + point.plotX = point.clientX = x; + point.plotY = y; + + // Save radius for halo + point.radius = radius; + + // Set this point's shape parameters + point.shapeType = 'circle'; + point.shapeArgs = { + x: x, + y: y, + r: radius + }; + }); + + series.translateColors(); + } + }, + + + /** Square shape type **/ + square: { + alignDataLabel: H.seriesTypes.heatmap.prototype.alignDataLabel, + translate: H.seriesTypes.heatmap.prototype.translate, + getSeriesPadding: function () { + return; + }, + haloPath: H.seriesTypes.heatmap.prototype.pointClass.prototype.haloPath + } }; @@ -426,33 +426,33 @@ H.tileShapeTypes = { // defined, we add nothing. H.wrap(H.Axis.prototype, 'setAxisTranslation', function (proceed) { - // We need to run the original func first, so that we know the translation - // formula to use for computing the padding - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - - var axis = this, - // Find which series' padding to use - seriesPadding = reduce(H.map(axis.series, function (series) { - return series.getSeriesPixelPadding && - series.getSeriesPixelPadding(axis); - }), function (a, b) { - return (a && a.padding) > (b && b.padding) ? a : b; - }) || { - padding: 0, - axisLengthFactor: 1 - }, - lengthPadding = Math.round( - seriesPadding.padding * seriesPadding.axisLengthFactor - ); - - // Don't waste time on this if we're not adding extra padding - if (seriesPadding.padding) { - // Recompute translation with new axis length now (minus padding) - axis.len -= lengthPadding; - proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); - axis.minPixelPadding += seriesPadding.padding; - axis.len += lengthPadding; - } + // We need to run the original func first, so that we know the translation + // formula to use for computing the padding + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + + var axis = this, + // Find which series' padding to use + seriesPadding = reduce(H.map(axis.series, function (series) { + return series.getSeriesPixelPadding && + series.getSeriesPixelPadding(axis); + }), function (a, b) { + return (a && a.padding) > (b && b.padding) ? a : b; + }) || { + padding: 0, + axisLengthFactor: 1 + }, + lengthPadding = Math.round( + seriesPadding.padding * seriesPadding.axisLengthFactor + ); + + // Don't waste time on this if we're not adding extra padding + if (seriesPadding.padding) { + // Recompute translation with new axis length now (minus padding) + axis.len -= lengthPadding; + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + axis.minPixelPadding += seriesPadding.padding; + axis.len += lengthPadding; + } }); @@ -472,145 +472,145 @@ H.wrap(H.Axis.prototype, 'setAxisTranslation', function (proceed) { * @sample maps/demo/circlemap-africa/ * Circlemap tilemap, Africa * @sample maps/demo/diamondmap - * Diamondmap tilemap + * Diamondmap tilemap * @since 6.0.0 * @excluding joinBy, shadow, allAreas, mapData, data * @optionparent plotOptions.tilemap */ seriesType('tilemap', 'heatmap', { // Default options - states: { - hover: { - halo: { - enabled: true, - size: 2, - opacity: 0.5, - attributes: { - zIndex: 3 - } - } - } - }, + states: { + hover: { + halo: { + enabled: true, + size: 2, + opacity: 0.5, + attributes: { + zIndex: 3 + } + } + } + }, /** - * The padding between points in the tilemap. - * - * @sample maps/plotoptions/tilemap-pointpadding Point padding on tiles - */ - pointPadding: 2, - - /** - * The column size - how many X axis units each column in the tilemap - * should span. Works as in [Heatmaps](#plotOptions.heatmap.colsize). - * - * @type {Number} - * @sample {highcharts} maps/demo/heatmap/ One day - * @sample {highmaps} maps/demo/heatmap/ One day - * @default 1 - * @product highcharts highmaps - * @apioption plotOptions.tilemap.colsize - */ - - /** - * The row size - how many Y axis units each tilemap row should span. - * Analogous to [colsize](#plotOptions.tilemap.colsize). - * - * @type {Number} - * @sample {highcharts} maps/demo/heatmap/ 1 by default - * @sample {highmaps} maps/demo/heatmap/ 1 by default - * @default 1 - * @product highcharts highmaps - * @apioption plotOptions.tilemap.rowsize - */ - - /** - * The shape of the tiles in the tilemap. Possible values are `hexagon`, - * `circle`, `diamond`, and `square`. - * - * @sample maps/demo/circlemap-africa Circular tile shapes - * @sample maps/demo/diamondmap Diamond tile shapes - */ - tileShape: 'hexagon' + * The padding between points in the tilemap. + * + * @sample maps/plotoptions/tilemap-pointpadding Point padding on tiles + */ + pointPadding: 2, + + /** + * The column size - how many X axis units each column in the tilemap + * should span. Works as in [Heatmaps](#plotOptions.heatmap.colsize). + * + * @type {Number} + * @sample {highcharts} maps/demo/heatmap/ One day + * @sample {highmaps} maps/demo/heatmap/ One day + * @default 1 + * @product highcharts highmaps + * @apioption plotOptions.tilemap.colsize + */ + + /** + * The row size - how many Y axis units each tilemap row should span. + * Analogous to [colsize](#plotOptions.tilemap.colsize). + * + * @type {Number} + * @sample {highcharts} maps/demo/heatmap/ 1 by default + * @sample {highmaps} maps/demo/heatmap/ 1 by default + * @default 1 + * @product highcharts highmaps + * @apioption plotOptions.tilemap.rowsize + */ + + /** + * The shape of the tiles in the tilemap. Possible values are `hexagon`, + * `circle`, `diamond`, and `square`. + * + * @sample maps/demo/circlemap-africa Circular tile shapes + * @sample maps/demo/diamondmap Diamond tile shapes + */ + tileShape: 'hexagon' // Prototype functions }, { - // Set tile shape object on series - setOptions: function () { - // Call original function - var ret = H.seriesTypes.heatmap.prototype.setOptions.apply(this, - Array.prototype.slice.call(arguments) - ); - - this.tileShape = H.tileShapeTypes[ret.tileShape]; - return ret; - }, - - // Use the shape's defined data label alignment function - alignDataLabel: function () { - return this.tileShape.alignDataLabel.apply(this, - Array.prototype.slice.call(arguments) - ); - }, - - // Get metrics for padding of axis for this series - getSeriesPixelPadding: function (axis) { - var isX = axis.isXAxis, - padding = this.tileShape.getSeriesPadding(this), - coord1, - coord2; - - // If the shape type does not require padding, return no-op padding - if (!padding) { - return { - padding: 0, - axisLengthFactor: 1 - }; - } - - // Use translate to compute how far outside the points we - // draw, and use this difference as padding. - coord1 = Math.round( - axis.translate( - isX ? - padding.xPad * 2 : - padding.yPad, - 0, 1, 0, 1 - ) - ); - coord2 = Math.round( - axis.translate( - isX ? padding.xPad : 0, - 0, 1, 0, 1 - ) - ); - - return { - padding: Math.abs(coord1 - coord2) || 0, - - // Offset the yAxis length to compensate for shift. - // Setting the length factor to 2 would add the same margin to max - // as min. Now we only add a slight bit of the min margin to max, as - // we don't actually draw outside the max bounds. For the xAxis we - // draw outside on both sides so we add the same margin to min and - // max. - axisLengthFactor: isX ? 2 : 1.1 - }; - }, - - // Use translate from tileShape - translate: function () { - return this.tileShape.translate.apply(this, - Array.prototype.slice.call(arguments) - ); - } + // Set tile shape object on series + setOptions: function () { + // Call original function + var ret = H.seriesTypes.heatmap.prototype.setOptions.apply(this, + Array.prototype.slice.call(arguments) + ); + + this.tileShape = H.tileShapeTypes[ret.tileShape]; + return ret; + }, + + // Use the shape's defined data label alignment function + alignDataLabel: function () { + return this.tileShape.alignDataLabel.apply(this, + Array.prototype.slice.call(arguments) + ); + }, + + // Get metrics for padding of axis for this series + getSeriesPixelPadding: function (axis) { + var isX = axis.isXAxis, + padding = this.tileShape.getSeriesPadding(this), + coord1, + coord2; + + // If the shape type does not require padding, return no-op padding + if (!padding) { + return { + padding: 0, + axisLengthFactor: 1 + }; + } + + // Use translate to compute how far outside the points we + // draw, and use this difference as padding. + coord1 = Math.round( + axis.translate( + isX ? + padding.xPad * 2 : + padding.yPad, + 0, 1, 0, 1 + ) + ); + coord2 = Math.round( + axis.translate( + isX ? padding.xPad : 0, + 0, 1, 0, 1 + ) + ); + + return { + padding: Math.abs(coord1 - coord2) || 0, + + // Offset the yAxis length to compensate for shift. + // Setting the length factor to 2 would add the same margin to max + // as min. Now we only add a slight bit of the min margin to max, as + // we don't actually draw outside the max bounds. For the xAxis we + // draw outside on both sides so we add the same margin to min and + // max. + axisLengthFactor: isX ? 2 : 1.1 + }; + }, + + // Use translate from tileShape + translate: function () { + return this.tileShape.translate.apply(this, + Array.prototype.slice.call(arguments) + ); + } }, H.extend({ - haloPath: function () { - return this.series.tileShape.haloPath.apply(this, - Array.prototype.slice.call(arguments) - ); - } + haloPath: function () { + return this.series.tileShape.haloPath.apply(this, + Array.prototype.slice.call(arguments) + ); + } }, H.colorPointMixin)); /** diff --git a/js/modules/treemap.src.js b/js/modules/treemap.src.js index b5373465e52..66c96614764 100644 --- a/js/modules/treemap.src.js +++ b/js/modules/treemap.src.js @@ -13,43 +13,43 @@ import '../parts/Series.js'; import '../parts/Color.js'; var seriesType = H.seriesType, - seriesTypes = H.seriesTypes, - map = H.map, - merge = H.merge, - extend = H.extend, - noop = H.noop, - each = H.each, - getColor = mixinTreeSeries.getColor, - getLevelOptions = mixinTreeSeries.getLevelOptions, - grep = H.grep, - isBoolean = function (x) { - return typeof x === 'boolean'; - }, - isNumber = H.isNumber, - isObject = H.isObject, - isString = H.isString, - pick = H.pick, - Series = H.Series, - stableSort = H.stableSort, - color = H.Color, - eachObject = function (list, func, context) { - context = context || this; - H.objectEach(list, function (val, key) { - func.call(context, val, key, list); - }); - }, - reduce = H.reduce, - // @todo find correct name for this function. - // @todo Similar to reduce, this function is likely redundant - recursive = function (item, func, context) { - var next; - context = context || this; - next = func.call(context, item); - if (next !== false) { - recursive(next, func, context); - } - }, - updateRootId = mixinTreeSeries.updateRootId; + seriesTypes = H.seriesTypes, + map = H.map, + merge = H.merge, + extend = H.extend, + noop = H.noop, + each = H.each, + getColor = mixinTreeSeries.getColor, + getLevelOptions = mixinTreeSeries.getLevelOptions, + grep = H.grep, + isBoolean = function (x) { + return typeof x === 'boolean'; + }, + isNumber = H.isNumber, + isObject = H.isObject, + isString = H.isString, + pick = H.pick, + Series = H.Series, + stableSort = H.stableSort, + color = H.Color, + eachObject = function (list, func, context) { + context = context || this; + H.objectEach(list, function (val, key) { + func.call(context, val, key, list); + }); + }, + reduce = H.reduce, + // @todo find correct name for this function. + // @todo Similar to reduce, this function is likely redundant + recursive = function (item, func, context) { + var next; + context = context || this; + next = func.call(context, item); + if (next !== false) { + recursive(next, func, context); + } + }, + updateRootId = mixinTreeSeries.updateRootId; /** * A treemap displays hierarchical data using nested rectangles. The data can be @@ -64,1413 +64,1413 @@ var seriesType = H.seriesType, */ seriesType('treemap', 'scatter', { - /** - * When enabled the user can click on a point which is a parent and - * zoom in on its children. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/treemap-allowdrilltonode/ Enabled - * @default false - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.allowDrillToNode - */ - - /** - * When the series contains less points than the crop threshold, all - * points are drawn, event if the points fall outside the visible plot - * area at the current zoom. The advantage of drawing all points (including - * markers and columns), is that animation is performed on updates. - * On the other hand, when the series contains more points than the - * crop threshold, the series data is cropped to only contain points - * that fall within the plot area. The advantage of cropping away invisible - * points is to increase performance on large series. - * - * @type {Number} - * @default 300 - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.cropThreshold - */ - - /** - * This option decides if the user can interact with the parent nodes - * or just the leaf nodes. When this option is undefined, it will be - * true by default. However when allowDrillToNode is true, then it will - * be false by default. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/treemap-interactbyleaf-false/ False - * @sample {highcharts} highcharts/plotoptions/treemap-interactbyleaf-true-and-allowdrilltonode/ InteractByLeaf and allowDrillToNode is true - * @since 4.1.2 - * @product highcharts - * @apioption plotOptions.treemap.interactByLeaf - */ - - /** - * The sort index of the point inside the treemap level. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/treemap-sortindex/ Sort by years - * @since 4.1.10 - * @product highcharts - * @apioption plotOptions.treemap.sortIndex - */ - - /** - * When using automatic point colors pulled from the `options.colors` - * collection, this option determines whether the chart should receive - * one color per series or one color per point. - * - * @type {Boolean} - * @see [series colors](#plotOptions.treemap.colors) - * @default false - * @since 2.0 - * @apioption plotOptions.treemap.colorByPoint - */ - - /** - * A series specific or series type specific color set to apply instead - * of the global [colors](#colors) when [colorByPoint]( - * #plotOptions.treemap.colorByPoint) is true. - * - * @type {Array} - * @since 3.0 - * @apioption plotOptions.treemap.colors - */ - - /** - * Whether to display this series type or specific series item in the - * legend. - * - * @type {Boolean} - * @default false - * @product highcharts - */ - showInLegend: false, - - /** - * @ignore - */ - marker: false, - colorByPoint: false, - /** - * @extends plotOptions.heatmap.dataLabels - * @since 4.1.0 - * @product highcharts - */ - dataLabels: { - enabled: true, - defer: false, - verticalAlign: 'middle', - formatter: function () { // #2945 - return this.point.name || this.point.id; - }, - inside: true - }, - - tooltip: { - headerFormat: '', - pointFormat: '{point.name}: {point.value}
' - }, - - /** - * Whether to ignore hidden points when the layout algorithm runs. - * If `false`, hidden points will leave open spaces. - * - * @type {Boolean} - * @default true - * @since 5.0.8 - * @product highcharts - */ - ignoreHiddenPoint: true, - - /** - * This option decides which algorithm is used for setting position - * and dimensions of the points. Can be one of `sliceAndDice`, `stripes`, - * `squarified` or `strip`. - * - * @validvalue ["sliceAndDice", "stripes", "squarified", "strip"] - * @type {String} - * @see [How to write your own algorithm](http://www.highcharts.com/docs/chart- - * and-series-types/treemap) - * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-sliceanddice/ SliceAndDice by default - * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-stripes/ Stripes - * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-squarified/ Squarified - * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-strip/ Strip - * @default sliceAndDice - * @since 4.1.0 - * @product highcharts - */ - layoutAlgorithm: 'sliceAndDice', - - /** - * Defines which direction the layout algorithm will start drawing. - * Possible values are "vertical" and "horizontal". - * - * @validvalue ["vertical", "horizontal"] - * @type {String} - * @default vertical - * @since 4.1.0 - * @product highcharts - */ - layoutStartingDirection: 'vertical', - - /** - * Enabling this option will make the treemap alternate the drawing - * direction between vertical and horizontal. The next levels starting - * direction will always be the opposite of the previous. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/treemap-alternatestartingdirection-true/ Enabled - * @default false - * @since 4.1.0 - * @product highcharts - */ - alternateStartingDirection: false, - - /** - * Used together with the levels and allowDrillToNode options. When - * set to false the first level visible when drilling is considered - * to be level one. Otherwise the level will be the same as the tree - * structure. - * - * @type {Boolean} - * @default true - * @since 4.1.0 - * @product highcharts - */ - levelIsConstant: true, - - /** - * Options for the button appearing when drilling down in a treemap. - */ - drillUpButton: { - - /** - * The position of the button. - */ - position: { - - /** - * Vertical alignment of the button. - * - * @default top - * @validvalue ["top", "middle", "bottom"] - * @apioption plotOptions.treemap.drillUpButton.position.verticalAlign - */ - - /** - * Horizontal alignment of the button. - * @validvalue ["left", "center", "right"] - */ - align: 'right', - - /** - * Horizontal offset of the button. - * @default -10 - * @type {Number} - */ - x: -10, - - /** - * Vertical offset of the button. - */ - y: 10 - } - }, - - - /** - * Set options on specific levels. Takes precedence over series options, - * but not point options. - * - * @type {Array} - * @sample {highcharts} highcharts/plotoptions/treemap-levels/ - * Styling dataLabels and borders - * @sample {highcharts} highcharts/demo/treemap-with-levels/ - * Different layoutAlgorithm - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels - */ - - /** - * Can set a `borderColor` on all points which lies on the same level. - * - * @type {Color} - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.borderColor - */ - - /** - * Set the dash style of the border of all the point which lies on the - * level. See - * plotOptions.scatter.dashStyle for possible options. - * - * @type {String} - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.borderDashStyle - */ - - /** - * Can set the borderWidth on all points which lies on the same level. - * - * @type {Number} - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.borderWidth - */ - - /** - * Can set a color on all points which lies on the same level. - * - * @type {Color} - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.color - */ - - /** - * A configuration object to define how the color of a child varies from the - * parent's color. The variation is distributed among the children of node. - * For example when setting brightness, the brightness change will range - * from the parent's original brightness on the first child, to the amount - * set in the `to` setting on the last node. This allows a gradient-like - * color scheme that sets children out from each other while highlighting - * the grouping on treemaps and sectors on sunburst charts. - * - * @type {Object} - * @sample highcharts/demo/sunburst/ Sunburst with color variation - * @since 6.0.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.colorVariation - */ - - /** - * The key of a color variation. Currently supports `brightness` only. - * - * @type {String} - * @validvalue ["brightness"] - * @since 6.0.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.colorVariation.key - */ - - /** - * The ending value of a color variation. The last sibling will receive this - * value. - * - * @type {Number} - * @since 6.0.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.colorVariation.to - */ - - /** - * Can set the options of dataLabels on each point which lies on the - * level. [plotOptions.treemap.dataLabels](#plotOptions.treemap.dataLabels) - * for possible values. - * - * @type {Object} - * @default undefined - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.dataLabels - */ - - /** - * Can set the layoutAlgorithm option on a specific level. - * - * @validvalue ["sliceAndDice", "stripes", "squarified", "strip"] - * @type {String} - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.layoutAlgorithm - */ - - /** - * Can set the layoutStartingDirection option on a specific level. - * - * @validvalue ["vertical", "horizontal"] - * @type {String} - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.layoutStartingDirection - */ - - /** - * Decides which level takes effect from the options set in the levels - * object. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/treemap-levels/ - * Styling of both levels - * @since 4.1.0 - * @product highcharts - * @apioption plotOptions.treemap.levels.level - */ - - - /*= if (build.classic) { =*/ - // Presentational options - - /** - * The color of the border surrounding each tree map item. - * - * @type {Color} - * @default #e6e6e6 - * @product highcharts - */ - borderColor: '${palette.neutralColor10}', - - /** - * The width of the border surrounding each tree map item. - */ - borderWidth: 1, - - /** - * The opacity of a point in treemap. When a point has children, the - * visibility of the children is determined by the opacity. - * - * @type {Number} - * @default 0.15 - * @since 4.2.4 - * @product highcharts - */ - opacity: 0.15, - - /** - * A wrapper object for all the series options in specific states. - * - * @extends plotOptions.heatmap.states - * @product highcharts - */ - states: { - - /** - * Options for the hovered series - * - * @extends plotOptions.heatmap.states.hover - * @excluding halo - * @product highcharts - */ - hover: { - - /** - * The border color for the hovered state. - */ - borderColor: '${palette.neutralColor40}', - - /** - * Brightness for the hovered point. Defaults to 0 if the heatmap - * series is loaded, otherwise 0.1. - * - * @default null - * @type {Number} - */ - brightness: seriesTypes.heatmap ? 0 : 0.1, - /** - * @extends plotOptions.heatmap.states.hover.halo - */ - halo: false, - /** - * The opacity of a point in treemap. When a point has children, - * the visibility of the children is determined by the opacity. - * - * @type {Number} - * @default 0.75 - * @since 4.2.4 - * @product highcharts - */ - opacity: 0.75, - - /** - * The shadow option for hovered state. - */ - shadow: false - } - } - /*= } =*/ + /** + * When enabled the user can click on a point which is a parent and + * zoom in on its children. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/treemap-allowdrilltonode/ Enabled + * @default false + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.allowDrillToNode + */ + + /** + * When the series contains less points than the crop threshold, all + * points are drawn, event if the points fall outside the visible plot + * area at the current zoom. The advantage of drawing all points (including + * markers and columns), is that animation is performed on updates. + * On the other hand, when the series contains more points than the + * crop threshold, the series data is cropped to only contain points + * that fall within the plot area. The advantage of cropping away invisible + * points is to increase performance on large series. + * + * @type {Number} + * @default 300 + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.cropThreshold + */ + + /** + * This option decides if the user can interact with the parent nodes + * or just the leaf nodes. When this option is undefined, it will be + * true by default. However when allowDrillToNode is true, then it will + * be false by default. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/treemap-interactbyleaf-false/ False + * @sample {highcharts} highcharts/plotoptions/treemap-interactbyleaf-true-and-allowdrilltonode/ InteractByLeaf and allowDrillToNode is true + * @since 4.1.2 + * @product highcharts + * @apioption plotOptions.treemap.interactByLeaf + */ + + /** + * The sort index of the point inside the treemap level. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/treemap-sortindex/ Sort by years + * @since 4.1.10 + * @product highcharts + * @apioption plotOptions.treemap.sortIndex + */ + + /** + * When using automatic point colors pulled from the `options.colors` + * collection, this option determines whether the chart should receive + * one color per series or one color per point. + * + * @type {Boolean} + * @see [series colors](#plotOptions.treemap.colors) + * @default false + * @since 2.0 + * @apioption plotOptions.treemap.colorByPoint + */ + + /** + * A series specific or series type specific color set to apply instead + * of the global [colors](#colors) when [colorByPoint]( + * #plotOptions.treemap.colorByPoint) is true. + * + * @type {Array} + * @since 3.0 + * @apioption plotOptions.treemap.colors + */ + + /** + * Whether to display this series type or specific series item in the + * legend. + * + * @type {Boolean} + * @default false + * @product highcharts + */ + showInLegend: false, + + /** + * @ignore + */ + marker: false, + colorByPoint: false, + /** + * @extends plotOptions.heatmap.dataLabels + * @since 4.1.0 + * @product highcharts + */ + dataLabels: { + enabled: true, + defer: false, + verticalAlign: 'middle', + formatter: function () { // #2945 + return this.point.name || this.point.id; + }, + inside: true + }, + + tooltip: { + headerFormat: '', + pointFormat: '{point.name}: {point.value}
' + }, + + /** + * Whether to ignore hidden points when the layout algorithm runs. + * If `false`, hidden points will leave open spaces. + * + * @type {Boolean} + * @default true + * @since 5.0.8 + * @product highcharts + */ + ignoreHiddenPoint: true, + + /** + * This option decides which algorithm is used for setting position + * and dimensions of the points. Can be one of `sliceAndDice`, `stripes`, + * `squarified` or `strip`. + * + * @validvalue ["sliceAndDice", "stripes", "squarified", "strip"] + * @type {String} + * @see [How to write your own algorithm](http://www.highcharts.com/docs/chart- + * and-series-types/treemap) + * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-sliceanddice/ SliceAndDice by default + * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-stripes/ Stripes + * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-squarified/ Squarified + * @sample {highcharts} highcharts/plotoptions/treemap-layoutalgorithm-strip/ Strip + * @default sliceAndDice + * @since 4.1.0 + * @product highcharts + */ + layoutAlgorithm: 'sliceAndDice', + + /** + * Defines which direction the layout algorithm will start drawing. + * Possible values are "vertical" and "horizontal". + * + * @validvalue ["vertical", "horizontal"] + * @type {String} + * @default vertical + * @since 4.1.0 + * @product highcharts + */ + layoutStartingDirection: 'vertical', + + /** + * Enabling this option will make the treemap alternate the drawing + * direction between vertical and horizontal. The next levels starting + * direction will always be the opposite of the previous. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/treemap-alternatestartingdirection-true/ Enabled + * @default false + * @since 4.1.0 + * @product highcharts + */ + alternateStartingDirection: false, + + /** + * Used together with the levels and allowDrillToNode options. When + * set to false the first level visible when drilling is considered + * to be level one. Otherwise the level will be the same as the tree + * structure. + * + * @type {Boolean} + * @default true + * @since 4.1.0 + * @product highcharts + */ + levelIsConstant: true, + + /** + * Options for the button appearing when drilling down in a treemap. + */ + drillUpButton: { + + /** + * The position of the button. + */ + position: { + + /** + * Vertical alignment of the button. + * + * @default top + * @validvalue ["top", "middle", "bottom"] + * @apioption plotOptions.treemap.drillUpButton.position.verticalAlign + */ + + /** + * Horizontal alignment of the button. + * @validvalue ["left", "center", "right"] + */ + align: 'right', + + /** + * Horizontal offset of the button. + * @default -10 + * @type {Number} + */ + x: -10, + + /** + * Vertical offset of the button. + */ + y: 10 + } + }, + + + /** + * Set options on specific levels. Takes precedence over series options, + * but not point options. + * + * @type {Array} + * @sample {highcharts} highcharts/plotoptions/treemap-levels/ + * Styling dataLabels and borders + * @sample {highcharts} highcharts/demo/treemap-with-levels/ + * Different layoutAlgorithm + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels + */ + + /** + * Can set a `borderColor` on all points which lies on the same level. + * + * @type {Color} + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.borderColor + */ + + /** + * Set the dash style of the border of all the point which lies on the + * level. See + * plotOptions.scatter.dashStyle for possible options. + * + * @type {String} + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.borderDashStyle + */ + + /** + * Can set the borderWidth on all points which lies on the same level. + * + * @type {Number} + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.borderWidth + */ + + /** + * Can set a color on all points which lies on the same level. + * + * @type {Color} + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.color + */ + + /** + * A configuration object to define how the color of a child varies from the + * parent's color. The variation is distributed among the children of node. + * For example when setting brightness, the brightness change will range + * from the parent's original brightness on the first child, to the amount + * set in the `to` setting on the last node. This allows a gradient-like + * color scheme that sets children out from each other while highlighting + * the grouping on treemaps and sectors on sunburst charts. + * + * @type {Object} + * @sample highcharts/demo/sunburst/ Sunburst with color variation + * @since 6.0.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.colorVariation + */ + + /** + * The key of a color variation. Currently supports `brightness` only. + * + * @type {String} + * @validvalue ["brightness"] + * @since 6.0.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.colorVariation.key + */ + + /** + * The ending value of a color variation. The last sibling will receive this + * value. + * + * @type {Number} + * @since 6.0.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.colorVariation.to + */ + + /** + * Can set the options of dataLabels on each point which lies on the + * level. [plotOptions.treemap.dataLabels](#plotOptions.treemap.dataLabels) + * for possible values. + * + * @type {Object} + * @default undefined + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.dataLabels + */ + + /** + * Can set the layoutAlgorithm option on a specific level. + * + * @validvalue ["sliceAndDice", "stripes", "squarified", "strip"] + * @type {String} + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.layoutAlgorithm + */ + + /** + * Can set the layoutStartingDirection option on a specific level. + * + * @validvalue ["vertical", "horizontal"] + * @type {String} + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.layoutStartingDirection + */ + + /** + * Decides which level takes effect from the options set in the levels + * object. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/treemap-levels/ + * Styling of both levels + * @since 4.1.0 + * @product highcharts + * @apioption plotOptions.treemap.levels.level + */ + + + /*= if (build.classic) { =*/ + // Presentational options + + /** + * The color of the border surrounding each tree map item. + * + * @type {Color} + * @default #e6e6e6 + * @product highcharts + */ + borderColor: '${palette.neutralColor10}', + + /** + * The width of the border surrounding each tree map item. + */ + borderWidth: 1, + + /** + * The opacity of a point in treemap. When a point has children, the + * visibility of the children is determined by the opacity. + * + * @type {Number} + * @default 0.15 + * @since 4.2.4 + * @product highcharts + */ + opacity: 0.15, + + /** + * A wrapper object for all the series options in specific states. + * + * @extends plotOptions.heatmap.states + * @product highcharts + */ + states: { + + /** + * Options for the hovered series + * + * @extends plotOptions.heatmap.states.hover + * @excluding halo + * @product highcharts + */ + hover: { + + /** + * The border color for the hovered state. + */ + borderColor: '${palette.neutralColor40}', + + /** + * Brightness for the hovered point. Defaults to 0 if the heatmap + * series is loaded, otherwise 0.1. + * + * @default null + * @type {Number} + */ + brightness: seriesTypes.heatmap ? 0 : 0.1, + /** + * @extends plotOptions.heatmap.states.hover.halo + */ + halo: false, + /** + * The opacity of a point in treemap. When a point has children, + * the visibility of the children is determined by the opacity. + * + * @type {Number} + * @default 0.75 + * @since 4.2.4 + * @product highcharts + */ + opacity: 0.75, + + /** + * The shadow option for hovered state. + */ + shadow: false + } + } + /*= } =*/ // Prototype members }, { - pointArrayMap: ['value'], - axisTypes: seriesTypes.heatmap ? - ['xAxis', 'yAxis', 'colorAxis'] : - ['xAxis', 'yAxis'], - directTouch: true, - optionalAxis: 'colorAxis', - getSymbol: noop, - parallelArrays: ['x', 'y', 'value', 'colorValue'], - colorKey: 'colorValue', // Point color option key - translateColors: ( - seriesTypes.heatmap && - seriesTypes.heatmap.prototype.translateColors - ), - colorAttribs: ( - seriesTypes.heatmap && - seriesTypes.heatmap.prototype.colorAttribs - ), - trackerGroups: ['group', 'dataLabelsGroup'], - /** - * Creates an object map from parent id to childrens index. - * @param {Array} data List of points set in options. - * @param {string} data[].parent Parent id of point. - * @param {Array} ids List of all point ids. - * @return {Object} Map from parent id to children index in data. - */ - getListOfParents: function (data, ids) { - var listOfParents = reduce(data || [], function (prev, curr, i) { - var parent = pick(curr.parent, ''); - if (prev[parent] === undefined) { - prev[parent] = []; - } - prev[parent].push(i); - return prev; - }, {}); - - // If parent does not exist, hoist parent to root of tree. - eachObject(listOfParents, function (children, parent, list) { - if ((parent !== '') && (H.inArray(parent, ids) === -1)) { - each(children, function (child) { - list[''].push(child); - }); - delete list[parent]; - } - }); - return listOfParents; - }, - /** - * Creates a tree structured object from the series points - */ - getTree: function () { - var series = this, - allIds = map(this.data, function (d) { - return d.id; - }), - parentList = series.getListOfParents(this.data, allIds); - - series.nodeMap = []; - return series.buildNode('', -1, 0, parentList, null); - }, - init: function (chart, options) { - var series = this; - Series.prototype.init.call(series, chart, options); - if (series.options.allowDrillToNode) { - H.addEvent(series, 'click', series.onClickDrillToNode); - } - }, - buildNode: function (id, i, level, list, parent) { - var series = this, - children = [], - point = series.points[i], - height = 0, - node, - child; - - // Actions - each((list[id] || []), function (i) { - child = series.buildNode( - series.points[i].id, - i, - (level + 1), - list, - id - ); - height = Math.max(child.height + 1, height); - children.push(child); - }); - node = { - id: id, - i: i, - children: children, - height: height, - level: level, - parent: parent, - visible: false // @todo move this to better location - }; - series.nodeMap[node.id] = node; - if (point) { - point.node = node; - } - return node; - }, - setTreeValues: function (tree) { - var series = this, - options = series.options, - idRoot = series.rootNode, - mapIdToNode = series.nodeMap, - nodeRoot = mapIdToNode[idRoot], - levelIsConstant = ( - isBoolean(options.levelIsConstant) ? - options.levelIsConstant : - true - ), - childrenTotal = 0, - children = [], - val, - point = series.points[tree.i]; - - // First give the children some values - each(tree.children, function (child) { - child = series.setTreeValues(child); - children.push(child); - if (!child.ignore) { - childrenTotal += child.val; - } - }); - // Sort the children - stableSort(children, function (a, b) { - return a.sortIndex - b.sortIndex; - }); - // Set the values - val = pick(point && point.options.value, childrenTotal); - if (point) { - point.value = val; - } - extend(tree, { - children: children, - childrenTotal: childrenTotal, - // Ignore this node if point is not visible - ignore: !(pick(point && point.visible, true) && (val > 0)), - isLeaf: tree.visible && !childrenTotal, - levelDynamic: tree.level - (levelIsConstant ? 0 : nodeRoot.level), - name: pick(point && point.name, ''), - sortIndex: pick(point && point.sortIndex, -val), - val: val - }); - return tree; - }, - /** - * Recursive function which calculates the area for all children of a node. - * @param {Object} node The node which is parent to the children. - * @param {Object} area The rectangular area of the parent. - */ - calculateChildrenAreas: function (parent, area) { - var series = this, - options = series.options, - mapOptionsToLevel = series.mapOptionsToLevel, - level = mapOptionsToLevel[parent.level + 1], - algorithm = pick( - ( - series[level && - level.layoutAlgorithm] && - level.layoutAlgorithm - ), - options.layoutAlgorithm - ), - alternate = options.alternateStartingDirection, - childrenValues = [], - children; - - // Collect all children which should be included - children = grep(parent.children, function (n) { - return !n.ignore; - }); - - if (level && level.layoutStartingDirection) { - area.direction = level.layoutStartingDirection === 'vertical' ? - 0 : - 1; - } - childrenValues = series[algorithm](area, children); - each(children, function (child, index) { - var values = childrenValues[index]; - child.values = merge(values, { - val: child.childrenTotal, - direction: (alternate ? 1 - area.direction : area.direction) - }); - child.pointValues = merge(values, { - x: (values.x / series.axisRatio), - width: (values.width / series.axisRatio) - }); - // If node has children, then call method recursively - if (child.children.length) { - series.calculateChildrenAreas(child, child.values); - } - }); - }, - setPointValues: function () { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis; - each(series.points, function (point) { - var node = point.node, - values = node.pointValues, - x1, - x2, - y1, - y2, - crispCorr = 0; - - /*= if (build.classic) { =*/ - // Get the crisp correction in classic mode. For this to work in - // styled mode, we would need to first add the shape (without x, y, - // width and height), then read the rendered stroke width using - // point.graphic.strokeWidth(), then modify and apply the shapeArgs. - // This applies also to column series, but the downside is - // performance and code complexity. - crispCorr = ( - (series.pointAttribs(point)['stroke-width'] || 0) % 2 - ) / 2; - /*= } =*/ - - // Points which is ignored, have no values. - if (values && node.visible) { - x1 = Math.round( - xAxis.translate(values.x, 0, 0, 0, 1) - ) - crispCorr; - x2 = Math.round( - xAxis.translate(values.x + values.width, 0, 0, 0, 1) - ) - crispCorr; - y1 = Math.round( - yAxis.translate(values.y, 0, 0, 0, 1) - ) - crispCorr; - y2 = Math.round( - yAxis.translate(values.y + values.height, 0, 0, 0, 1) - ) - crispCorr; - // Set point values - point.shapeType = 'rect'; - point.shapeArgs = { - x: Math.min(x1, x2), - y: Math.min(y1, y2), - width: Math.abs(x2 - x1), - height: Math.abs(y2 - y1) - }; - point.plotX = point.shapeArgs.x + (point.shapeArgs.width / 2); - point.plotY = point.shapeArgs.y + (point.shapeArgs.height / 2); - } else { - // Reset visibility - delete point.plotX; - delete point.plotY; - } - }); - }, - - /** - * Set the node's color recursively, from the parent down. - */ - setColorRecursive: function ( - node, - parentColor, - colorIndex, - index, - siblings - ) { - var series = this, - chart = series && series.chart, - colors = chart && chart.options && chart.options.colors, - colorInfo, - point; - - if (node) { - colorInfo = getColor(node, { - colors: colors, - index: index, - mapOptionsToLevel: series.mapOptionsToLevel, - parentColor: parentColor, - parentColorIndex: colorIndex, - series: series, - siblings: siblings - }); - - point = series.points[node.i]; - if (point) { - point.color = colorInfo.color; - point.colorIndex = colorInfo.colorIndex; - } - - // Do it all again with the children - each(node.children || [], function (child, i) { - series.setColorRecursive( - child, - colorInfo.color, - colorInfo.colorIndex, - i, - node.children.length - ); - }); - } - }, - algorithmGroup: function (h, w, d, p) { - this.height = h; - this.width = w; - this.plot = p; - this.direction = d; - this.startDirection = d; - this.total = 0; - this.nW = 0; - this.lW = 0; - this.nH = 0; - this.lH = 0; - this.elArr = []; - this.lP = { - total: 0, - lH: 0, - nH: 0, - lW: 0, - nW: 0, - nR: 0, - lR: 0, - aspectRatio: function (w, h) { - return Math.max((w / h), (h / w)); - } - }; - this.addElement = function (el) { - this.lP.total = this.elArr[this.elArr.length - 1]; - this.total = this.total + el; - if (this.direction === 0) { - // Calculate last point old aspect ratio - this.lW = this.nW; - this.lP.lH = this.lP.total / this.lW; - this.lP.lR = this.lP.aspectRatio(this.lW, this.lP.lH); - // Calculate last point new aspect ratio - this.nW = this.total / this.height; - this.lP.nH = this.lP.total / this.nW; - this.lP.nR = this.lP.aspectRatio(this.nW, this.lP.nH); - } else { - // Calculate last point old aspect ratio - this.lH = this.nH; - this.lP.lW = this.lP.total / this.lH; - this.lP.lR = this.lP.aspectRatio(this.lP.lW, this.lH); - // Calculate last point new aspect ratio - this.nH = this.total / this.width; - this.lP.nW = this.lP.total / this.nH; - this.lP.nR = this.lP.aspectRatio(this.lP.nW, this.nH); - } - this.elArr.push(el); - }; - this.reset = function () { - this.nW = 0; - this.lW = 0; - this.elArr = []; - this.total = 0; - }; - }, - algorithmCalcPoints: function (directionChange, last, group, childrenArea) { - var pX, - pY, - pW, - pH, - gW = group.lW, - gH = group.lH, - plot = group.plot, - keep, - i = 0, - end = group.elArr.length - 1; - if (last) { - gW = group.nW; - gH = group.nH; - } else { - keep = group.elArr[group.elArr.length - 1]; - } - each(group.elArr, function (p) { - if (last || (i < end)) { - if (group.direction === 0) { - pX = plot.x; - pY = plot.y; - pW = gW; - pH = p / pW; - } else { - pX = plot.x; - pY = plot.y; - pH = gH; - pW = p / pH; - } - childrenArea.push({ - x: pX, - y: pY, - width: pW, - height: pH - }); - if (group.direction === 0) { - plot.y = plot.y + pH; - } else { - plot.x = plot.x + pW; - } - } - i = i + 1; - }); - // Reset variables - group.reset(); - if (group.direction === 0) { - group.width = group.width - gW; - } else { - group.height = group.height - gH; - } - plot.y = plot.parent.y + (plot.parent.height - group.height); - plot.x = plot.parent.x + (plot.parent.width - group.width); - if (directionChange) { - group.direction = 1 - group.direction; - } - // If not last, then add uncalculated element - if (!last) { - group.addElement(keep); - } - }, - algorithmLowAspectRatio: function (directionChange, parent, children) { - var childrenArea = [], - series = this, - pTot, - plot = { - x: parent.x, - y: parent.y, - parent: parent - }, - direction = parent.direction, - i = 0, - end = children.length - 1, - group = new this.algorithmGroup( // eslint-disable-line new-cap - parent.height, - parent.width, - direction, - plot - ); - // Loop through and calculate all areas - each(children, function (child) { - pTot = (parent.width * parent.height) * (child.val / parent.val); - group.addElement(pTot); - if (group.lP.nR > group.lP.lR) { - series.algorithmCalcPoints( - directionChange, - false, - group, - childrenArea, - plot - ); - } - // If last child, then calculate all remaining areas - if (i === end) { - series.algorithmCalcPoints( - directionChange, - true, - group, - childrenArea, - plot - ); - } - i = i + 1; - }); - return childrenArea; - }, - algorithmFill: function (directionChange, parent, children) { - var childrenArea = [], - pTot, - direction = parent.direction, - x = parent.x, - y = parent.y, - width = parent.width, - height = parent.height, - pX, - pY, - pW, - pH; - each(children, function (child) { - pTot = (parent.width * parent.height) * (child.val / parent.val); - pX = x; - pY = y; - if (direction === 0) { - pH = height; - pW = pTot / pH; - width = width - pW; - x = x + pW; - } else { - pW = width; - pH = pTot / pW; - height = height - pH; - y = y + pH; - } - childrenArea.push({ - x: pX, - y: pY, - width: pW, - height: pH - }); - if (directionChange) { - direction = 1 - direction; - } - }); - return childrenArea; - }, - strip: function (parent, children) { - return this.algorithmLowAspectRatio(false, parent, children); - }, - squarified: function (parent, children) { - return this.algorithmLowAspectRatio(true, parent, children); - }, - sliceAndDice: function (parent, children) { - return this.algorithmFill(true, parent, children); - }, - stripes: function (parent, children) { - return this.algorithmFill(false, parent, children); - }, - translate: function () { - var series = this, - options = series.options, - // NOTE: updateRootId modifies series. - rootId = updateRootId(series), - rootNode, - pointValues, - seriesArea, - tree, - val; - - // Call prototype function - Series.prototype.translate.call(series); - - // @todo Only if series.isDirtyData is true - tree = series.tree = series.getTree(); - rootNode = series.nodeMap[rootId]; - series.mapOptionsToLevel = getLevelOptions({ - from: rootNode.level + 1, - levels: options.levels, - to: tree.height, - defaults: { - levelIsConstant: series.options.levelIsConstant, - colorByPoint: options.colorByPoint - } - }); - if ( - rootId !== '' && - (!rootNode || !rootNode.children.length) - ) { - series.drillToNode('', false); - rootId = series.rootNode; - rootNode = series.nodeMap[rootId]; - } - // Parents of the root node is by default visible - recursive(series.nodeMap[series.rootNode], function (node) { - var next = false, - p = node.parent; - node.visible = true; - if (p || p === '') { - next = series.nodeMap[p]; - } - return next; - }); - // Children of the root node is by default visible - recursive( - series.nodeMap[series.rootNode].children, - function (children) { - var next = false; - each(children, function (child) { - child.visible = true; - if (child.children.length) { - next = (next || []).concat(child.children); - } - }); - return next; - } - ); - series.setTreeValues(tree); - - // Calculate plotting values. - series.axisRatio = (series.xAxis.len / series.yAxis.len); - series.nodeMap[''].pointValues = pointValues = - { x: 0, y: 0, width: 100, height: 100 }; - series.nodeMap[''].values = seriesArea = merge(pointValues, { - width: (pointValues.width * series.axisRatio), - direction: (options.layoutStartingDirection === 'vertical' ? 0 : 1), - val: tree.val - }); - series.calculateChildrenAreas(tree, seriesArea); - - // Logic for point colors - if (series.colorAxis) { - series.translateColors(); - } else if (!options.colorByPoint) { - series.setColorRecursive(series.tree); - } - - // Update axis extremes according to the root node. - if (options.allowDrillToNode) { - val = rootNode.pointValues; - series.xAxis.setExtremes(val.x, val.x + val.width, false); - series.yAxis.setExtremes(val.y, val.y + val.height, false); - series.xAxis.setScale(); - series.yAxis.setScale(); - } - - // Assign values to points. - series.setPointValues(); - }, - /** - * Extend drawDataLabels with logic to handle custom options related to the - * treemap series: - * - Points which is not a leaf node, has dataLabels disabled by default. - * - Options set on series.levels is merged in. - * - Width of the dataLabel is set to match the width of the point shape. - */ - drawDataLabels: function () { - var series = this, - mapOptionsToLevel = series.mapOptionsToLevel, - points = grep(series.points, function (n) { - return n.node.visible; - }), - options, - level; - each(points, function (point) { - level = mapOptionsToLevel[point.node.level]; - // Set options to new object to avoid problems with scope - options = { style: {} }; - - // If not a leaf, then label should be disabled as default - if (!point.node.isLeaf) { - options.enabled = false; - } - - // If options for level exists, include them as well - if (level && level.dataLabels) { - options = merge(options, level.dataLabels); - series._hasPointLabels = true; - } - - // Set dataLabel width to the width of the point shape. - if (point.shapeArgs) { - options.style.width = point.shapeArgs.width; - if (point.dataLabel) { - point.dataLabel.css({ - width: point.shapeArgs.width + 'px' - }); - } - } - - // Merge custom options with point options - point.dlOptions = merge(options, point.options.dataLabels); - }); - Series.prototype.drawDataLabels.call(this); - }, - - /** - * Over the alignment method by setting z index - */ - alignDataLabel: function (point) { - seriesTypes.column.prototype.alignDataLabel.apply(this, arguments); - if (point.dataLabel) { - // point.node.zIndex could be undefined (#6956) - point.dataLabel.attr({ zIndex: (point.node.zIndex || 0) + 1 }); - } - }, - - /*= if (build.classic) { =*/ - /** - * Get presentational attributes - */ - pointAttribs: function (point, state) { - var series = this, - mapOptionsToLevel = ( - isObject(series.mapOptionsToLevel) ? - series.mapOptionsToLevel : - {} - ), - level = point && mapOptionsToLevel[point.node.level] || {}, - options = this.options, - attr, - stateOptions = (state && options.states[state]) || {}, - className = (point && point.getClassName()) || '', - opacity; - - // Set attributes by precedence. Point trumps level trumps series. - // Stroke width uses pick because it can be 0. - attr = { - 'stroke': - (point && point.borderColor) || - level.borderColor || - stateOptions.borderColor || - options.borderColor, - 'stroke-width': pick( - point && point.borderWidth, - level.borderWidth, - stateOptions.borderWidth, - options.borderWidth - ), - 'dashstyle': - (point && point.borderDashStyle) || - level.borderDashStyle || - stateOptions.borderDashStyle || - options.borderDashStyle, - 'fill': (point && point.color) || this.color - }; - - // Hide levels above the current view - if (className.indexOf('highcharts-above-level') !== -1) { - attr.fill = 'none'; - attr['stroke-width'] = 0; - - // Nodes with children that accept interaction - } else if ( - className.indexOf('highcharts-internal-node-interactive') !== -1 - ) { - opacity = pick(stateOptions.opacity, options.opacity); - attr.fill = color(attr.fill).setOpacity(opacity).get(); - attr.cursor = 'pointer'; - // Hide nodes that have children - } else if (className.indexOf('highcharts-internal-node') !== -1) { - attr.fill = 'none'; - - } else if (state) { - // Brighten and hoist the hover nodes - attr.fill = color(attr.fill) - .brighten(stateOptions.brightness) - .get(); - } - return attr; - }, - /*= } =*/ - - /** - * Extending ColumnSeries drawPoints - */ - drawPoints: function () { - var series = this, - points = grep(series.points, function (n) { - return n.node.visible; - }); - - each(points, function (point) { - var groupKey = 'level-group-' + point.node.levelDynamic; - if (!series[groupKey]) { - series[groupKey] = series.chart.renderer.g(groupKey) - .attr({ - // @todo Set the zIndex based upon the number of levels, - // instead of using 1000 - zIndex: 1000 - point.node.levelDynamic - }) - .add(series.group); - } - point.group = series[groupKey]; - - }); - // Call standard drawPoints - seriesTypes.column.prototype.drawPoints.call(this); - - /*= if (!build.classic) { =*/ - // In styled mode apply point.color. Use CSS, otherwise the fill - // used in the style sheet will take precedence over the fill - // attribute. - if (this.colorAttribs) { // Heatmap is loaded - each(this.points, function (point) { - if (point.graphic) { - point.graphic.css(this.colorAttribs(point)); - } - }, this); - } - /*= } =*/ - - // If drillToNode is allowed, set a point cursor on clickables & add - // drillId to point - if (series.options.allowDrillToNode) { - each(points, function (point) { - if (point.graphic) { - point.drillId = series.options.interactByLeaf ? - series.drillToByLeaf(point) : - series.drillToByGroup(point); - } - }); - } - }, - /** - * Add drilling on the suitable points - */ - onClickDrillToNode: function (event) { - var series = this, - point = event.point, - drillId = point && point.drillId; - // If a drill id is returned, add click event and cursor. - if (isString(drillId)) { - point.setState(''); // Remove hover - series.drillToNode(drillId); - } - }, - /** - * Finds the drill id for a parent node. - * Returns false if point should not have a click event - * @param {Object} point - * @return {String|Boolean} Drill to id or false when point should not have a - * click event - */ - drillToByGroup: function (point) { - var series = this, - drillId = false; - if ( - (point.node.level - series.nodeMap[series.rootNode].level) === 1 && - !point.node.isLeaf - ) { - drillId = point.id; - } - return drillId; - }, - /** - * Finds the drill id for a leaf node. - * Returns false if point should not have a click event - * @param {Object} point - * @return {String|Boolean} Drill to id or false when point should not have a - * click event - */ - drillToByLeaf: function (point) { - var series = this, - drillId = false, - nodeParent; - if ((point.node.parent !== series.rootNode) && (point.node.isLeaf)) { - nodeParent = point.node; - while (!drillId) { - nodeParent = series.nodeMap[nodeParent.parent]; - if (nodeParent.parent === series.rootNode) { - drillId = nodeParent.id; - } - } - } - return drillId; - }, - drillUp: function () { - var series = this, - node = series.nodeMap[series.rootNode]; - if (node && isString(node.parent)) { - series.drillToNode(node.parent); - } - }, - drillToNode: function (id, redraw) { - var series = this, - nodeMap = series.nodeMap, - node = nodeMap[id]; - series.idPreviousRoot = series.rootNode; - series.rootNode = id; - if (id === '') { - series.drillUpButton = series.drillUpButton.destroy(); - } else { - series.showDrillUpButton((node && node.name || id)); - } - this.isDirty = true; // Force redraw - if (pick(redraw, true)) { - this.chart.redraw(); - } - }, - showDrillUpButton: function (name) { - var series = this, - backText = (name || '< Back'), - buttonOptions = series.options.drillUpButton, - attr, - states; - - if (buttonOptions.text) { - backText = buttonOptions.text; - } - if (!this.drillUpButton) { - attr = buttonOptions.theme; - states = attr && attr.states; - - this.drillUpButton = this.chart.renderer.button( - backText, - null, - null, - function () { - series.drillUp(); - }, - attr, - states && states.hover, - states && states.select - ) - .addClass('highcharts-drillup-button') - .attr({ - align: buttonOptions.position.align, - zIndex: 7 - }) - .add() - .align( - buttonOptions.position, - false, - buttonOptions.relativeTo || 'plotBox' - ); - } else { - this.drillUpButton.placed = false; - this.drillUpButton.attr({ - text: backText - }) - .align(); - } - }, - buildKDTree: noop, - drawLegendSymbol: H.LegendSymbolMixin.drawRectangle, - getExtremes: function () { - // Get the extremes from the value data - Series.prototype.getExtremes.call(this, this.colorValueData); - this.valueMin = this.dataMin; - this.valueMax = this.dataMax; - - // Get the extremes from the y data - Series.prototype.getExtremes.call(this); - }, - getExtremesFromAll: true, - bindAxes: function () { - var treeAxis = { - endOnTick: false, - gridLineWidth: 0, - lineWidth: 0, - min: 0, - dataMin: 0, - minPadding: 0, - max: 100, - dataMax: 100, - maxPadding: 0, - startOnTick: false, - title: null, - tickPositions: [] - }; - Series.prototype.bindAxes.call(this); - H.extend(this.yAxis.options, treeAxis); - H.extend(this.xAxis.options, treeAxis); - }, - utils: { - recursive: recursive, - reduce: reduce - } + pointArrayMap: ['value'], + axisTypes: seriesTypes.heatmap ? + ['xAxis', 'yAxis', 'colorAxis'] : + ['xAxis', 'yAxis'], + directTouch: true, + optionalAxis: 'colorAxis', + getSymbol: noop, + parallelArrays: ['x', 'y', 'value', 'colorValue'], + colorKey: 'colorValue', // Point color option key + translateColors: ( + seriesTypes.heatmap && + seriesTypes.heatmap.prototype.translateColors + ), + colorAttribs: ( + seriesTypes.heatmap && + seriesTypes.heatmap.prototype.colorAttribs + ), + trackerGroups: ['group', 'dataLabelsGroup'], + /** + * Creates an object map from parent id to childrens index. + * @param {Array} data List of points set in options. + * @param {string} data[].parent Parent id of point. + * @param {Array} ids List of all point ids. + * @return {Object} Map from parent id to children index in data. + */ + getListOfParents: function (data, ids) { + var listOfParents = reduce(data || [], function (prev, curr, i) { + var parent = pick(curr.parent, ''); + if (prev[parent] === undefined) { + prev[parent] = []; + } + prev[parent].push(i); + return prev; + }, {}); + + // If parent does not exist, hoist parent to root of tree. + eachObject(listOfParents, function (children, parent, list) { + if ((parent !== '') && (H.inArray(parent, ids) === -1)) { + each(children, function (child) { + list[''].push(child); + }); + delete list[parent]; + } + }); + return listOfParents; + }, + /** + * Creates a tree structured object from the series points + */ + getTree: function () { + var series = this, + allIds = map(this.data, function (d) { + return d.id; + }), + parentList = series.getListOfParents(this.data, allIds); + + series.nodeMap = []; + return series.buildNode('', -1, 0, parentList, null); + }, + init: function (chart, options) { + var series = this; + Series.prototype.init.call(series, chart, options); + if (series.options.allowDrillToNode) { + H.addEvent(series, 'click', series.onClickDrillToNode); + } + }, + buildNode: function (id, i, level, list, parent) { + var series = this, + children = [], + point = series.points[i], + height = 0, + node, + child; + + // Actions + each((list[id] || []), function (i) { + child = series.buildNode( + series.points[i].id, + i, + (level + 1), + list, + id + ); + height = Math.max(child.height + 1, height); + children.push(child); + }); + node = { + id: id, + i: i, + children: children, + height: height, + level: level, + parent: parent, + visible: false // @todo move this to better location + }; + series.nodeMap[node.id] = node; + if (point) { + point.node = node; + } + return node; + }, + setTreeValues: function (tree) { + var series = this, + options = series.options, + idRoot = series.rootNode, + mapIdToNode = series.nodeMap, + nodeRoot = mapIdToNode[idRoot], + levelIsConstant = ( + isBoolean(options.levelIsConstant) ? + options.levelIsConstant : + true + ), + childrenTotal = 0, + children = [], + val, + point = series.points[tree.i]; + + // First give the children some values + each(tree.children, function (child) { + child = series.setTreeValues(child); + children.push(child); + if (!child.ignore) { + childrenTotal += child.val; + } + }); + // Sort the children + stableSort(children, function (a, b) { + return a.sortIndex - b.sortIndex; + }); + // Set the values + val = pick(point && point.options.value, childrenTotal); + if (point) { + point.value = val; + } + extend(tree, { + children: children, + childrenTotal: childrenTotal, + // Ignore this node if point is not visible + ignore: !(pick(point && point.visible, true) && (val > 0)), + isLeaf: tree.visible && !childrenTotal, + levelDynamic: tree.level - (levelIsConstant ? 0 : nodeRoot.level), + name: pick(point && point.name, ''), + sortIndex: pick(point && point.sortIndex, -val), + val: val + }); + return tree; + }, + /** + * Recursive function which calculates the area for all children of a node. + * @param {Object} node The node which is parent to the children. + * @param {Object} area The rectangular area of the parent. + */ + calculateChildrenAreas: function (parent, area) { + var series = this, + options = series.options, + mapOptionsToLevel = series.mapOptionsToLevel, + level = mapOptionsToLevel[parent.level + 1], + algorithm = pick( + ( + series[level && + level.layoutAlgorithm] && + level.layoutAlgorithm + ), + options.layoutAlgorithm + ), + alternate = options.alternateStartingDirection, + childrenValues = [], + children; + + // Collect all children which should be included + children = grep(parent.children, function (n) { + return !n.ignore; + }); + + if (level && level.layoutStartingDirection) { + area.direction = level.layoutStartingDirection === 'vertical' ? + 0 : + 1; + } + childrenValues = series[algorithm](area, children); + each(children, function (child, index) { + var values = childrenValues[index]; + child.values = merge(values, { + val: child.childrenTotal, + direction: (alternate ? 1 - area.direction : area.direction) + }); + child.pointValues = merge(values, { + x: (values.x / series.axisRatio), + width: (values.width / series.axisRatio) + }); + // If node has children, then call method recursively + if (child.children.length) { + series.calculateChildrenAreas(child, child.values); + } + }); + }, + setPointValues: function () { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis; + each(series.points, function (point) { + var node = point.node, + values = node.pointValues, + x1, + x2, + y1, + y2, + crispCorr = 0; + + /*= if (build.classic) { =*/ + // Get the crisp correction in classic mode. For this to work in + // styled mode, we would need to first add the shape (without x, y, + // width and height), then read the rendered stroke width using + // point.graphic.strokeWidth(), then modify and apply the shapeArgs. + // This applies also to column series, but the downside is + // performance and code complexity. + crispCorr = ( + (series.pointAttribs(point)['stroke-width'] || 0) % 2 + ) / 2; + /*= } =*/ + + // Points which is ignored, have no values. + if (values && node.visible) { + x1 = Math.round( + xAxis.translate(values.x, 0, 0, 0, 1) + ) - crispCorr; + x2 = Math.round( + xAxis.translate(values.x + values.width, 0, 0, 0, 1) + ) - crispCorr; + y1 = Math.round( + yAxis.translate(values.y, 0, 0, 0, 1) + ) - crispCorr; + y2 = Math.round( + yAxis.translate(values.y + values.height, 0, 0, 0, 1) + ) - crispCorr; + // Set point values + point.shapeType = 'rect'; + point.shapeArgs = { + x: Math.min(x1, x2), + y: Math.min(y1, y2), + width: Math.abs(x2 - x1), + height: Math.abs(y2 - y1) + }; + point.plotX = point.shapeArgs.x + (point.shapeArgs.width / 2); + point.plotY = point.shapeArgs.y + (point.shapeArgs.height / 2); + } else { + // Reset visibility + delete point.plotX; + delete point.plotY; + } + }); + }, + + /** + * Set the node's color recursively, from the parent down. + */ + setColorRecursive: function ( + node, + parentColor, + colorIndex, + index, + siblings + ) { + var series = this, + chart = series && series.chart, + colors = chart && chart.options && chart.options.colors, + colorInfo, + point; + + if (node) { + colorInfo = getColor(node, { + colors: colors, + index: index, + mapOptionsToLevel: series.mapOptionsToLevel, + parentColor: parentColor, + parentColorIndex: colorIndex, + series: series, + siblings: siblings + }); + + point = series.points[node.i]; + if (point) { + point.color = colorInfo.color; + point.colorIndex = colorInfo.colorIndex; + } + + // Do it all again with the children + each(node.children || [], function (child, i) { + series.setColorRecursive( + child, + colorInfo.color, + colorInfo.colorIndex, + i, + node.children.length + ); + }); + } + }, + algorithmGroup: function (h, w, d, p) { + this.height = h; + this.width = w; + this.plot = p; + this.direction = d; + this.startDirection = d; + this.total = 0; + this.nW = 0; + this.lW = 0; + this.nH = 0; + this.lH = 0; + this.elArr = []; + this.lP = { + total: 0, + lH: 0, + nH: 0, + lW: 0, + nW: 0, + nR: 0, + lR: 0, + aspectRatio: function (w, h) { + return Math.max((w / h), (h / w)); + } + }; + this.addElement = function (el) { + this.lP.total = this.elArr[this.elArr.length - 1]; + this.total = this.total + el; + if (this.direction === 0) { + // Calculate last point old aspect ratio + this.lW = this.nW; + this.lP.lH = this.lP.total / this.lW; + this.lP.lR = this.lP.aspectRatio(this.lW, this.lP.lH); + // Calculate last point new aspect ratio + this.nW = this.total / this.height; + this.lP.nH = this.lP.total / this.nW; + this.lP.nR = this.lP.aspectRatio(this.nW, this.lP.nH); + } else { + // Calculate last point old aspect ratio + this.lH = this.nH; + this.lP.lW = this.lP.total / this.lH; + this.lP.lR = this.lP.aspectRatio(this.lP.lW, this.lH); + // Calculate last point new aspect ratio + this.nH = this.total / this.width; + this.lP.nW = this.lP.total / this.nH; + this.lP.nR = this.lP.aspectRatio(this.lP.nW, this.nH); + } + this.elArr.push(el); + }; + this.reset = function () { + this.nW = 0; + this.lW = 0; + this.elArr = []; + this.total = 0; + }; + }, + algorithmCalcPoints: function (directionChange, last, group, childrenArea) { + var pX, + pY, + pW, + pH, + gW = group.lW, + gH = group.lH, + plot = group.plot, + keep, + i = 0, + end = group.elArr.length - 1; + if (last) { + gW = group.nW; + gH = group.nH; + } else { + keep = group.elArr[group.elArr.length - 1]; + } + each(group.elArr, function (p) { + if (last || (i < end)) { + if (group.direction === 0) { + pX = plot.x; + pY = plot.y; + pW = gW; + pH = p / pW; + } else { + pX = plot.x; + pY = plot.y; + pH = gH; + pW = p / pH; + } + childrenArea.push({ + x: pX, + y: pY, + width: pW, + height: pH + }); + if (group.direction === 0) { + plot.y = plot.y + pH; + } else { + plot.x = plot.x + pW; + } + } + i = i + 1; + }); + // Reset variables + group.reset(); + if (group.direction === 0) { + group.width = group.width - gW; + } else { + group.height = group.height - gH; + } + plot.y = plot.parent.y + (plot.parent.height - group.height); + plot.x = plot.parent.x + (plot.parent.width - group.width); + if (directionChange) { + group.direction = 1 - group.direction; + } + // If not last, then add uncalculated element + if (!last) { + group.addElement(keep); + } + }, + algorithmLowAspectRatio: function (directionChange, parent, children) { + var childrenArea = [], + series = this, + pTot, + plot = { + x: parent.x, + y: parent.y, + parent: parent + }, + direction = parent.direction, + i = 0, + end = children.length - 1, + group = new this.algorithmGroup( // eslint-disable-line new-cap + parent.height, + parent.width, + direction, + plot + ); + // Loop through and calculate all areas + each(children, function (child) { + pTot = (parent.width * parent.height) * (child.val / parent.val); + group.addElement(pTot); + if (group.lP.nR > group.lP.lR) { + series.algorithmCalcPoints( + directionChange, + false, + group, + childrenArea, + plot + ); + } + // If last child, then calculate all remaining areas + if (i === end) { + series.algorithmCalcPoints( + directionChange, + true, + group, + childrenArea, + plot + ); + } + i = i + 1; + }); + return childrenArea; + }, + algorithmFill: function (directionChange, parent, children) { + var childrenArea = [], + pTot, + direction = parent.direction, + x = parent.x, + y = parent.y, + width = parent.width, + height = parent.height, + pX, + pY, + pW, + pH; + each(children, function (child) { + pTot = (parent.width * parent.height) * (child.val / parent.val); + pX = x; + pY = y; + if (direction === 0) { + pH = height; + pW = pTot / pH; + width = width - pW; + x = x + pW; + } else { + pW = width; + pH = pTot / pW; + height = height - pH; + y = y + pH; + } + childrenArea.push({ + x: pX, + y: pY, + width: pW, + height: pH + }); + if (directionChange) { + direction = 1 - direction; + } + }); + return childrenArea; + }, + strip: function (parent, children) { + return this.algorithmLowAspectRatio(false, parent, children); + }, + squarified: function (parent, children) { + return this.algorithmLowAspectRatio(true, parent, children); + }, + sliceAndDice: function (parent, children) { + return this.algorithmFill(true, parent, children); + }, + stripes: function (parent, children) { + return this.algorithmFill(false, parent, children); + }, + translate: function () { + var series = this, + options = series.options, + // NOTE: updateRootId modifies series. + rootId = updateRootId(series), + rootNode, + pointValues, + seriesArea, + tree, + val; + + // Call prototype function + Series.prototype.translate.call(series); + + // @todo Only if series.isDirtyData is true + tree = series.tree = series.getTree(); + rootNode = series.nodeMap[rootId]; + series.mapOptionsToLevel = getLevelOptions({ + from: rootNode.level + 1, + levels: options.levels, + to: tree.height, + defaults: { + levelIsConstant: series.options.levelIsConstant, + colorByPoint: options.colorByPoint + } + }); + if ( + rootId !== '' && + (!rootNode || !rootNode.children.length) + ) { + series.drillToNode('', false); + rootId = series.rootNode; + rootNode = series.nodeMap[rootId]; + } + // Parents of the root node is by default visible + recursive(series.nodeMap[series.rootNode], function (node) { + var next = false, + p = node.parent; + node.visible = true; + if (p || p === '') { + next = series.nodeMap[p]; + } + return next; + }); + // Children of the root node is by default visible + recursive( + series.nodeMap[series.rootNode].children, + function (children) { + var next = false; + each(children, function (child) { + child.visible = true; + if (child.children.length) { + next = (next || []).concat(child.children); + } + }); + return next; + } + ); + series.setTreeValues(tree); + + // Calculate plotting values. + series.axisRatio = (series.xAxis.len / series.yAxis.len); + series.nodeMap[''].pointValues = pointValues = + { x: 0, y: 0, width: 100, height: 100 }; + series.nodeMap[''].values = seriesArea = merge(pointValues, { + width: (pointValues.width * series.axisRatio), + direction: (options.layoutStartingDirection === 'vertical' ? 0 : 1), + val: tree.val + }); + series.calculateChildrenAreas(tree, seriesArea); + + // Logic for point colors + if (series.colorAxis) { + series.translateColors(); + } else if (!options.colorByPoint) { + series.setColorRecursive(series.tree); + } + + // Update axis extremes according to the root node. + if (options.allowDrillToNode) { + val = rootNode.pointValues; + series.xAxis.setExtremes(val.x, val.x + val.width, false); + series.yAxis.setExtremes(val.y, val.y + val.height, false); + series.xAxis.setScale(); + series.yAxis.setScale(); + } + + // Assign values to points. + series.setPointValues(); + }, + /** + * Extend drawDataLabels with logic to handle custom options related to the + * treemap series: + * - Points which is not a leaf node, has dataLabels disabled by default. + * - Options set on series.levels is merged in. + * - Width of the dataLabel is set to match the width of the point shape. + */ + drawDataLabels: function () { + var series = this, + mapOptionsToLevel = series.mapOptionsToLevel, + points = grep(series.points, function (n) { + return n.node.visible; + }), + options, + level; + each(points, function (point) { + level = mapOptionsToLevel[point.node.level]; + // Set options to new object to avoid problems with scope + options = { style: {} }; + + // If not a leaf, then label should be disabled as default + if (!point.node.isLeaf) { + options.enabled = false; + } + + // If options for level exists, include them as well + if (level && level.dataLabels) { + options = merge(options, level.dataLabels); + series._hasPointLabels = true; + } + + // Set dataLabel width to the width of the point shape. + if (point.shapeArgs) { + options.style.width = point.shapeArgs.width; + if (point.dataLabel) { + point.dataLabel.css({ + width: point.shapeArgs.width + 'px' + }); + } + } + + // Merge custom options with point options + point.dlOptions = merge(options, point.options.dataLabels); + }); + Series.prototype.drawDataLabels.call(this); + }, + + /** + * Over the alignment method by setting z index + */ + alignDataLabel: function (point) { + seriesTypes.column.prototype.alignDataLabel.apply(this, arguments); + if (point.dataLabel) { + // point.node.zIndex could be undefined (#6956) + point.dataLabel.attr({ zIndex: (point.node.zIndex || 0) + 1 }); + } + }, + + /*= if (build.classic) { =*/ + /** + * Get presentational attributes + */ + pointAttribs: function (point, state) { + var series = this, + mapOptionsToLevel = ( + isObject(series.mapOptionsToLevel) ? + series.mapOptionsToLevel : + {} + ), + level = point && mapOptionsToLevel[point.node.level] || {}, + options = this.options, + attr, + stateOptions = (state && options.states[state]) || {}, + className = (point && point.getClassName()) || '', + opacity; + + // Set attributes by precedence. Point trumps level trumps series. + // Stroke width uses pick because it can be 0. + attr = { + 'stroke': + (point && point.borderColor) || + level.borderColor || + stateOptions.borderColor || + options.borderColor, + 'stroke-width': pick( + point && point.borderWidth, + level.borderWidth, + stateOptions.borderWidth, + options.borderWidth + ), + 'dashstyle': + (point && point.borderDashStyle) || + level.borderDashStyle || + stateOptions.borderDashStyle || + options.borderDashStyle, + 'fill': (point && point.color) || this.color + }; + + // Hide levels above the current view + if (className.indexOf('highcharts-above-level') !== -1) { + attr.fill = 'none'; + attr['stroke-width'] = 0; + + // Nodes with children that accept interaction + } else if ( + className.indexOf('highcharts-internal-node-interactive') !== -1 + ) { + opacity = pick(stateOptions.opacity, options.opacity); + attr.fill = color(attr.fill).setOpacity(opacity).get(); + attr.cursor = 'pointer'; + // Hide nodes that have children + } else if (className.indexOf('highcharts-internal-node') !== -1) { + attr.fill = 'none'; + + } else if (state) { + // Brighten and hoist the hover nodes + attr.fill = color(attr.fill) + .brighten(stateOptions.brightness) + .get(); + } + return attr; + }, + /*= } =*/ + + /** + * Extending ColumnSeries drawPoints + */ + drawPoints: function () { + var series = this, + points = grep(series.points, function (n) { + return n.node.visible; + }); + + each(points, function (point) { + var groupKey = 'level-group-' + point.node.levelDynamic; + if (!series[groupKey]) { + series[groupKey] = series.chart.renderer.g(groupKey) + .attr({ + // @todo Set the zIndex based upon the number of levels, + // instead of using 1000 + zIndex: 1000 - point.node.levelDynamic + }) + .add(series.group); + } + point.group = series[groupKey]; + + }); + // Call standard drawPoints + seriesTypes.column.prototype.drawPoints.call(this); + + /*= if (!build.classic) { =*/ + // In styled mode apply point.color. Use CSS, otherwise the fill + // used in the style sheet will take precedence over the fill + // attribute. + if (this.colorAttribs) { // Heatmap is loaded + each(this.points, function (point) { + if (point.graphic) { + point.graphic.css(this.colorAttribs(point)); + } + }, this); + } + /*= } =*/ + + // If drillToNode is allowed, set a point cursor on clickables & add + // drillId to point + if (series.options.allowDrillToNode) { + each(points, function (point) { + if (point.graphic) { + point.drillId = series.options.interactByLeaf ? + series.drillToByLeaf(point) : + series.drillToByGroup(point); + } + }); + } + }, + /** + * Add drilling on the suitable points + */ + onClickDrillToNode: function (event) { + var series = this, + point = event.point, + drillId = point && point.drillId; + // If a drill id is returned, add click event and cursor. + if (isString(drillId)) { + point.setState(''); // Remove hover + series.drillToNode(drillId); + } + }, + /** + * Finds the drill id for a parent node. + * Returns false if point should not have a click event + * @param {Object} point + * @return {String|Boolean} Drill to id or false when point should not have a + * click event + */ + drillToByGroup: function (point) { + var series = this, + drillId = false; + if ( + (point.node.level - series.nodeMap[series.rootNode].level) === 1 && + !point.node.isLeaf + ) { + drillId = point.id; + } + return drillId; + }, + /** + * Finds the drill id for a leaf node. + * Returns false if point should not have a click event + * @param {Object} point + * @return {String|Boolean} Drill to id or false when point should not have a + * click event + */ + drillToByLeaf: function (point) { + var series = this, + drillId = false, + nodeParent; + if ((point.node.parent !== series.rootNode) && (point.node.isLeaf)) { + nodeParent = point.node; + while (!drillId) { + nodeParent = series.nodeMap[nodeParent.parent]; + if (nodeParent.parent === series.rootNode) { + drillId = nodeParent.id; + } + } + } + return drillId; + }, + drillUp: function () { + var series = this, + node = series.nodeMap[series.rootNode]; + if (node && isString(node.parent)) { + series.drillToNode(node.parent); + } + }, + drillToNode: function (id, redraw) { + var series = this, + nodeMap = series.nodeMap, + node = nodeMap[id]; + series.idPreviousRoot = series.rootNode; + series.rootNode = id; + if (id === '') { + series.drillUpButton = series.drillUpButton.destroy(); + } else { + series.showDrillUpButton((node && node.name || id)); + } + this.isDirty = true; // Force redraw + if (pick(redraw, true)) { + this.chart.redraw(); + } + }, + showDrillUpButton: function (name) { + var series = this, + backText = (name || '< Back'), + buttonOptions = series.options.drillUpButton, + attr, + states; + + if (buttonOptions.text) { + backText = buttonOptions.text; + } + if (!this.drillUpButton) { + attr = buttonOptions.theme; + states = attr && attr.states; + + this.drillUpButton = this.chart.renderer.button( + backText, + null, + null, + function () { + series.drillUp(); + }, + attr, + states && states.hover, + states && states.select + ) + .addClass('highcharts-drillup-button') + .attr({ + align: buttonOptions.position.align, + zIndex: 7 + }) + .add() + .align( + buttonOptions.position, + false, + buttonOptions.relativeTo || 'plotBox' + ); + } else { + this.drillUpButton.placed = false; + this.drillUpButton.attr({ + text: backText + }) + .align(); + } + }, + buildKDTree: noop, + drawLegendSymbol: H.LegendSymbolMixin.drawRectangle, + getExtremes: function () { + // Get the extremes from the value data + Series.prototype.getExtremes.call(this, this.colorValueData); + this.valueMin = this.dataMin; + this.valueMax = this.dataMax; + + // Get the extremes from the y data + Series.prototype.getExtremes.call(this); + }, + getExtremesFromAll: true, + bindAxes: function () { + var treeAxis = { + endOnTick: false, + gridLineWidth: 0, + lineWidth: 0, + min: 0, + dataMin: 0, + minPadding: 0, + max: 100, + dataMax: 100, + maxPadding: 0, + startOnTick: false, + title: null, + tickPositions: [] + }; + Series.prototype.bindAxes.call(this); + H.extend(this.yAxis.options, treeAxis); + H.extend(this.xAxis.options, treeAxis); + }, + utils: { + recursive: recursive, + reduce: reduce + } // Point class }, { - getClassName: function () { - var className = H.Point.prototype.getClassName.call(this), - series = this.series, - options = series.options; - - // Above the current level - if (this.node.level <= series.nodeMap[series.rootNode].level) { - className += ' highcharts-above-level'; - - } else if ( - !this.node.isLeaf && - !pick(options.interactByLeaf, !options.allowDrillToNode) - ) { - className += ' highcharts-internal-node-interactive'; - - } else if (!this.node.isLeaf) { - className += ' highcharts-internal-node'; - } - return className; - }, - - /** - * A tree point is valid if it has han id too, assume it may be a parent - * item. - */ - isValid: function () { - return this.id || isNumber(this.value); - }, - setState: function (state) { - H.Point.prototype.setState.call(this, state); - - // Graphic does not exist when point is not visible. - if (this.graphic) { - this.graphic.attr({ - zIndex: state === 'hover' ? 1 : 0 - }); - } - }, - setVisible: seriesTypes.pie.prototype.pointClass.prototype.setVisible + getClassName: function () { + var className = H.Point.prototype.getClassName.call(this), + series = this.series, + options = series.options; + + // Above the current level + if (this.node.level <= series.nodeMap[series.rootNode].level) { + className += ' highcharts-above-level'; + + } else if ( + !this.node.isLeaf && + !pick(options.interactByLeaf, !options.allowDrillToNode) + ) { + className += ' highcharts-internal-node-interactive'; + + } else if (!this.node.isLeaf) { + className += ' highcharts-internal-node'; + } + return className; + }, + + /** + * A tree point is valid if it has han id too, assume it may be a parent + * item. + */ + isValid: function () { + return this.id || isNumber(this.value); + }, + setState: function (state) { + H.Point.prototype.setState.call(this, state); + + // Graphic does not exist when point is not visible. + if (this.graphic) { + this.graphic.attr({ + zIndex: state === 'hover' ? 1 : 0 + }); + } + }, + setVisible: seriesTypes.pie.prototype.pointClass.prototype.setVisible }); @@ -1525,7 +1525,7 @@ seriesType('treemap', 'scatter', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts * @apioption series.treemap.data */ diff --git a/js/modules/variable-pie.src.js b/js/modules/variable-pie.src.js index 45e96267cf6..c28d61f4759 100644 --- a/js/modules/variable-pie.src.js +++ b/js/modules/variable-pie.src.js @@ -6,19 +6,19 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Options.js'; var pick = H.pick, - each = H.each, - grep = H.grep, - arrayMin = H.arrayMin, - arrayMax = H.arrayMax, - seriesType = H.seriesType, - pieProto = H.seriesTypes.pie.prototype; + each = H.each, + grep = H.grep, + arrayMin = H.arrayMin, + arrayMax = H.arrayMax, + seriesType = H.seriesType, + pieProto = H.seriesTypes.pie.prototype; /** * The variablepie series type. @@ -28,388 +28,388 @@ var pick = H.pick, */ seriesType('variablepie', 'pie', - /** - * A variable pie series is a two dimensional series type, where each point - * renders an Y and Z value. Each point is drawn as a pie slice where the - * size (arc) of the slice relates to the Y value and the radius of pie - * slice relates to the Z value. Requires `highcharts-more.js`. - * - * @extends {plotOptions.pie} - * @product highcharts - * @sample {highcharts} highcharts/demo/variable-radius-pie/ - * Variable-radius pie chart - * @since 6.0.0 - * @optionparent plotOptions.variablepie - */ - { - /** - * The minimum size of the points' radius related to chart's `plotArea`. - * If a number is set, it applies in pixels. - * - * @sample {highcharts} - * highcharts/variable-radius-pie/min-max-point-size/ - * Example of minPointSize and maxPointSize - * @sample {highcharts} - * highcharts/variable-radius-pie/min-point-size-100/ - * minPointSize set to 100 - * @type {String|Number} - * @since 6.0.0 - * @product highcharts - */ - minPointSize: '10%', - /** - * The maximum size of the points' radius related to chart's `plotArea`. - * If a number is set, it applies in pixels. - * - * @sample {highcharts} - * highcharts/variable-radius-pie/min-max-point-size/ - * Example of minPointSize and maxPointSize - * @type {String|Number} - * @since 6.0.0 - * @product highcharts - */ - maxPointSize: '100%', - /** - * The minimum possible z value for the point's radius calculation. - * If the point's Z value is smaller than zMin, the slice will be drawn - * according to the zMin value. - * - * @sample {highcharts} - * highcharts/variable-radius-pie/zmin-5/ - * zMin set to 5, smaller z values are treated as 5 - * @sample {highcharts} - * highcharts/variable-radius-pie/zmin-zmax/ - * Series limited by both zMin and zMax - * @type {Number} - * @since 6.0.0 - * @product highcharts - */ - zMin: undefined, - /** - * The maximum possible z value for the point's radius calculation. If - * the point's Z value is bigger than zMax, the slice will be drawn - * according to the zMax value - * - * @sample {highcharts} - * highcharts/variable-radius-pie/zmin-zmax/ - * Series limited by both zMin and zMax - * @type {Number} - * @since 6.0.0 - * @product highcharts - */ - zMax: undefined, - /** - * Whether the pie slice's value should be represented by the area - * or the radius of the slice. Can be either `area` or `radius`. The - * default, `area`, corresponds best to the human perception of the size - * of each pie slice. - * - * @sample {highcharts} - * highcharts/variable-radius-pie/sizeby/ - * Difference between area and radius sizeBy - * @type {String} - * @validvalue ["area", "radius"] - * @since 6.0.0 - * @product highcharts - */ - sizeBy: 'area', - - tooltip: { - pointFormat: '\u25CF {series.name}
Value: {point.y}
Size: {point.z}
' - } - }, { - pointArrayMap: ['y', 'z'], - parallelArrays: ['x', 'y', 'z'], - - /* - * It is needed to null series.center on chart redraw. Probably good - * idea will be to add this option in directly in pie series. - */ - redraw: function () { - this.center = null; - pieProto.redraw.call(this, arguments); - }, - - /* - * For arrayMin and arrayMax calculations array shouldn't have - * null/undefined/string values. - * In this case it is needed to check if points Z value is a Number. - */ - zValEval: function (zVal) { - if (typeof zVal === 'number' && !isNaN(zVal)) { - return true; - } - return null; - }, - - /* - * Before standard translate method for pie chart it is needed to - * calculate min/max radius of each pie slice based on its Z value. - */ - calculateExtremes: function () { - var series = this, - chart = series.chart, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - seriesOptions = series.options, - slicingRoom = 2 * (seriesOptions.slicedOffset || 0), - zMin, - zMax, - zData = series.zData, - smallestSize = Math.min(plotWidth, plotHeight) - slicingRoom, - extremes = {}, // Min and max size of pie slice. - // In pie charts size of a pie is changed to make space for - // dataLabels, then series.center is changing. - positions = series.center || series.getCenter(); - - each(['minPointSize', 'maxPointSize'], function (prop) { - var length = seriesOptions[prop], - isPercent = /%$/.test(length); - length = parseInt(length, 10); - extremes[prop] = isPercent ? - smallestSize * length / 100 : - length * 2; // Because it should be radius, not diameter. - }); - - series.minPxSize = positions[3] + extremes.minPointSize; - series.maxPxSize = Math.max( - Math.min(positions[2], extremes.maxPointSize), - positions[3] + extremes.minPointSize - ); - - if (zData.length) { - zMin = pick( - seriesOptions.zMin, - arrayMin(grep(zData, series.zValEval)) - ); - zMax = pick( - seriesOptions.zMax, - arrayMax(grep(zData, series.zValEval)) - ); - this.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize); - } - }, - - /* - * Finding radius of series points based on their Z value and min/max Z - * value for all series - * zMin - min threshold for Z value. If point's Z value is smaller that - * zMin, point will have the smallest possible radius. - * zMax - max threshold for Z value. If point's Z value is bigger that - * zMax, point will have the biggest possible radius. - * minSize - minimal pixel size possible for radius - * maxSize - minimal pixel size possible for radius - */ - getRadii: function (zMin, zMax, minSize, maxSize) { - var i = 0, - pos, - zData = this.zData, - len = zData.length, - radii = [], - options = this.options, - sizeByArea = options.sizeBy !== 'radius', - zRange = zMax - zMin, - value, - radius; - - - // Calculate radius for all pie slice's based on their Z values - for (i; i < len; i++) { - // if zData[i] is null/undefined/string we need to take zMin for - // smallest radius. - value = this.zValEval(zData[i]) ? zData[i] : zMin; - - if (value <= zMin) { - radius = minSize / 2; - } else if (value >= zMax) { - radius = maxSize / 2; - } else { - // Relative size, a number between 0 and 1 - pos = zRange > 0 ? (value - zMin) / zRange : 0.5; - - if (sizeByArea) { - pos = Math.sqrt(pos); - } - - radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2; - } - radii.push(radius); - } - this.radii = radii; - }, - - - /** - * Extend tranlate by updating radius for each pie slice instead of - * using one global radius. - */ - translate: function (positions) { - - this.generatePoints(); - - var series = this, - cumulative = 0, - precision = 1000, // issue #172 - options = series.options, - slicedOffset = options.slicedOffset, - connectorOffset = slicedOffset + (options.borderWidth || 0), - finalConnectorOffset, - start, - end, - angle, - startAngle = options.startAngle || 0, - startAngleRad = Math.PI / 180 * (startAngle - 90), - endAngleRad = Math.PI / 180 * (pick( - options.endAngle, - startAngle + 360) - 90), - circ = endAngleRad - startAngleRad, // 2 * Math.PI, - points = series.points, - // the x component of the radius vector for a given point - radiusX, - radiusY, - labelDistance = options.dataLabels.distance, - ignoreHiddenPoint = options.ignoreHiddenPoint, - i, - len = points.length, - point, - pointRadii, - pointRadiusX, - pointRadiusY; - - series.startAngleRad = startAngleRad; - series.endAngleRad = endAngleRad; - // Use calculateExtremes to get series.radii array. - series.calculateExtremes(); - - // Get positions - either an integer or a percentage string must be - // given. If positions are passed as a parameter, we're in a - // recursive loop for adjusting space for data labels. - if (!positions) { - series.center = positions = series.getCenter(); - } - - // Utility for getting the x value from a given y, used for - // anticollision logic in data labels. Added point for using - // specific points' label distance. - series.getX = function (y, left, point) { - var radii = point.series.radii[point.index]; - angle = Math.asin( - Math.max( // #7663 - Math.min( - (y - positions[1]) / - (radii + point.labelDistance), - 1 - ), - -1 - ) - ); - return positions[0] + - (left ? -1 : 1) * - (Math.cos(angle) * (radii + - point.labelDistance)); - }; - - // Calculate the geometry for each point - for (i = 0; i < len; i++) { - - point = points[i]; - pointRadii = series.radii[i]; - - // Used for distance calculation for specific point. - point.labelDistance = pick( - point.options.dataLabels && - point.options.dataLabels.distance, - labelDistance - ); - - // Saved for later dataLabels distance calculation. - series.maxLabelDistance = Math.max( - series.maxLabelDistance || 0, - point.labelDistance - ); - - // set start and end angle - start = startAngleRad + (cumulative * circ); - if (!ignoreHiddenPoint || point.visible) { - cumulative += point.percentage / 100; - } - end = startAngleRad + (cumulative * circ); - - // set the shape - point.shapeType = 'arc'; - point.shapeArgs = { - x: positions[0], - y: positions[1], - r: pointRadii, - innerR: positions[3] / 2, - start: Math.round(start * precision) / precision, - end: Math.round(end * precision) / precision - }; - - // The angle must stay within -90 and 270 (#2645) - angle = (end + start) / 2; - if (angle > 1.5 * Math.PI) { - angle -= 2 * Math.PI; - } else if (angle < -Math.PI / 2) { - angle += 2 * Math.PI; - } - - // Center for the sliced out slice - point.slicedTranslation = { - translateX: Math.round(Math.cos(angle) * slicedOffset), - translateY: Math.round(Math.sin(angle) * slicedOffset) - }; - - // set the anchor point for tooltips - radiusX = Math.cos(angle) * positions[2] / 2; - radiusY = Math.sin(angle) * positions[2] / 2; - pointRadiusX = Math.cos(angle) * pointRadii; - pointRadiusY = Math.sin(angle) * pointRadii; - point.tooltipPos = [ - positions[0] + radiusX * 0.7, - positions[1] + radiusY * 0.7 - ]; - - point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? - 1 : - 0; - point.angle = angle; - - // Set the anchor point for data labels. Use point.labelDistance - // instead of labelDistance // #1174 - // finalConnectorOffset - not override connectorOffset value. - finalConnectorOffset = Math.min( - connectorOffset, - point.labelDistance / 5 - ); // #1678 - - point.labelPos = [ - positions[0] + pointRadiusX + - // first break of connector - Math.cos(angle) * point.labelDistance, - positions[1] + pointRadiusY + - Math.sin(angle) * point.labelDistance, // a/a - positions[0] + pointRadiusX + - // second break, right outside pie - Math.cos(angle) * finalConnectorOffset, - positions[1] + pointRadiusY + - Math.sin(angle) * finalConnectorOffset, // a/a - positions[0] + pointRadiusX, // landing point for connector - positions[1] + pointRadiusY, // a/a - point.labelDistance < 0 ? // alignment - 'center' : - point.half ? 'right' : 'left', // alignment - angle // center angle - ]; - } - } - } + /** + * A variable pie series is a two dimensional series type, where each point + * renders an Y and Z value. Each point is drawn as a pie slice where the + * size (arc) of the slice relates to the Y value and the radius of pie + * slice relates to the Z value. Requires `highcharts-more.js`. + * + * @extends {plotOptions.pie} + * @product highcharts + * @sample {highcharts} highcharts/demo/variable-radius-pie/ + * Variable-radius pie chart + * @since 6.0.0 + * @optionparent plotOptions.variablepie + */ + { + /** + * The minimum size of the points' radius related to chart's `plotArea`. + * If a number is set, it applies in pixels. + * + * @sample {highcharts} + * highcharts/variable-radius-pie/min-max-point-size/ + * Example of minPointSize and maxPointSize + * @sample {highcharts} + * highcharts/variable-radius-pie/min-point-size-100/ + * minPointSize set to 100 + * @type {String|Number} + * @since 6.0.0 + * @product highcharts + */ + minPointSize: '10%', + /** + * The maximum size of the points' radius related to chart's `plotArea`. + * If a number is set, it applies in pixels. + * + * @sample {highcharts} + * highcharts/variable-radius-pie/min-max-point-size/ + * Example of minPointSize and maxPointSize + * @type {String|Number} + * @since 6.0.0 + * @product highcharts + */ + maxPointSize: '100%', + /** + * The minimum possible z value for the point's radius calculation. + * If the point's Z value is smaller than zMin, the slice will be drawn + * according to the zMin value. + * + * @sample {highcharts} + * highcharts/variable-radius-pie/zmin-5/ + * zMin set to 5, smaller z values are treated as 5 + * @sample {highcharts} + * highcharts/variable-radius-pie/zmin-zmax/ + * Series limited by both zMin and zMax + * @type {Number} + * @since 6.0.0 + * @product highcharts + */ + zMin: undefined, + /** + * The maximum possible z value for the point's radius calculation. If + * the point's Z value is bigger than zMax, the slice will be drawn + * according to the zMax value + * + * @sample {highcharts} + * highcharts/variable-radius-pie/zmin-zmax/ + * Series limited by both zMin and zMax + * @type {Number} + * @since 6.0.0 + * @product highcharts + */ + zMax: undefined, + /** + * Whether the pie slice's value should be represented by the area + * or the radius of the slice. Can be either `area` or `radius`. The + * default, `area`, corresponds best to the human perception of the size + * of each pie slice. + * + * @sample {highcharts} + * highcharts/variable-radius-pie/sizeby/ + * Difference between area and radius sizeBy + * @type {String} + * @validvalue ["area", "radius"] + * @since 6.0.0 + * @product highcharts + */ + sizeBy: 'area', + + tooltip: { + pointFormat: '\u25CF {series.name}
Value: {point.y}
Size: {point.z}
' + } + }, { + pointArrayMap: ['y', 'z'], + parallelArrays: ['x', 'y', 'z'], + + /* + * It is needed to null series.center on chart redraw. Probably good + * idea will be to add this option in directly in pie series. + */ + redraw: function () { + this.center = null; + pieProto.redraw.call(this, arguments); + }, + + /* + * For arrayMin and arrayMax calculations array shouldn't have + * null/undefined/string values. + * In this case it is needed to check if points Z value is a Number. + */ + zValEval: function (zVal) { + if (typeof zVal === 'number' && !isNaN(zVal)) { + return true; + } + return null; + }, + + /* + * Before standard translate method for pie chart it is needed to + * calculate min/max radius of each pie slice based on its Z value. + */ + calculateExtremes: function () { + var series = this, + chart = series.chart, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + seriesOptions = series.options, + slicingRoom = 2 * (seriesOptions.slicedOffset || 0), + zMin, + zMax, + zData = series.zData, + smallestSize = Math.min(plotWidth, plotHeight) - slicingRoom, + extremes = {}, // Min and max size of pie slice. + // In pie charts size of a pie is changed to make space for + // dataLabels, then series.center is changing. + positions = series.center || series.getCenter(); + + each(['minPointSize', 'maxPointSize'], function (prop) { + var length = seriesOptions[prop], + isPercent = /%$/.test(length); + length = parseInt(length, 10); + extremes[prop] = isPercent ? + smallestSize * length / 100 : + length * 2; // Because it should be radius, not diameter. + }); + + series.minPxSize = positions[3] + extremes.minPointSize; + series.maxPxSize = Math.max( + Math.min(positions[2], extremes.maxPointSize), + positions[3] + extremes.minPointSize + ); + + if (zData.length) { + zMin = pick( + seriesOptions.zMin, + arrayMin(grep(zData, series.zValEval)) + ); + zMax = pick( + seriesOptions.zMax, + arrayMax(grep(zData, series.zValEval)) + ); + this.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize); + } + }, + + /* + * Finding radius of series points based on their Z value and min/max Z + * value for all series + * zMin - min threshold for Z value. If point's Z value is smaller that + * zMin, point will have the smallest possible radius. + * zMax - max threshold for Z value. If point's Z value is bigger that + * zMax, point will have the biggest possible radius. + * minSize - minimal pixel size possible for radius + * maxSize - minimal pixel size possible for radius + */ + getRadii: function (zMin, zMax, minSize, maxSize) { + var i = 0, + pos, + zData = this.zData, + len = zData.length, + radii = [], + options = this.options, + sizeByArea = options.sizeBy !== 'radius', + zRange = zMax - zMin, + value, + radius; + + + // Calculate radius for all pie slice's based on their Z values + for (i; i < len; i++) { + // if zData[i] is null/undefined/string we need to take zMin for + // smallest radius. + value = this.zValEval(zData[i]) ? zData[i] : zMin; + + if (value <= zMin) { + radius = minSize / 2; + } else if (value >= zMax) { + radius = maxSize / 2; + } else { + // Relative size, a number between 0 and 1 + pos = zRange > 0 ? (value - zMin) / zRange : 0.5; + + if (sizeByArea) { + pos = Math.sqrt(pos); + } + + radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2; + } + radii.push(radius); + } + this.radii = radii; + }, + + + /** + * Extend tranlate by updating radius for each pie slice instead of + * using one global radius. + */ + translate: function (positions) { + + this.generatePoints(); + + var series = this, + cumulative = 0, + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + (options.borderWidth || 0), + finalConnectorOffset, + start, + end, + angle, + startAngle = options.startAngle || 0, + startAngleRad = Math.PI / 180 * (startAngle - 90), + endAngleRad = Math.PI / 180 * (pick( + options.endAngle, + startAngle + 360) - 90), + circ = endAngleRad - startAngleRad, // 2 * Math.PI, + points = series.points, + // the x component of the radius vector for a given point + radiusX, + radiusY, + labelDistance = options.dataLabels.distance, + ignoreHiddenPoint = options.ignoreHiddenPoint, + i, + len = points.length, + point, + pointRadii, + pointRadiusX, + pointRadiusY; + + series.startAngleRad = startAngleRad; + series.endAngleRad = endAngleRad; + // Use calculateExtremes to get series.radii array. + series.calculateExtremes(); + + // Get positions - either an integer or a percentage string must be + // given. If positions are passed as a parameter, we're in a + // recursive loop for adjusting space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } + + // Utility for getting the x value from a given y, used for + // anticollision logic in data labels. Added point for using + // specific points' label distance. + series.getX = function (y, left, point) { + var radii = point.series.radii[point.index]; + angle = Math.asin( + Math.max( // #7663 + Math.min( + (y - positions[1]) / + (radii + point.labelDistance), + 1 + ), + -1 + ) + ); + return positions[0] + + (left ? -1 : 1) * + (Math.cos(angle) * (radii + + point.labelDistance)); + }; + + // Calculate the geometry for each point + for (i = 0; i < len; i++) { + + point = points[i]; + pointRadii = series.radii[i]; + + // Used for distance calculation for specific point. + point.labelDistance = pick( + point.options.dataLabels && + point.options.dataLabels.distance, + labelDistance + ); + + // Saved for later dataLabels distance calculation. + series.maxLabelDistance = Math.max( + series.maxLabelDistance || 0, + point.labelDistance + ); + + // set start and end angle + start = startAngleRad + (cumulative * circ); + if (!ignoreHiddenPoint || point.visible) { + cumulative += point.percentage / 100; + } + end = startAngleRad + (cumulative * circ); + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: pointRadii, + innerR: positions[3] / 2, + start: Math.round(start * precision) / precision, + end: Math.round(end * precision) / precision + }; + + // The angle must stay within -90 and 270 (#2645) + angle = (end + start) / 2; + if (angle > 1.5 * Math.PI) { + angle -= 2 * Math.PI; + } else if (angle < -Math.PI / 2) { + angle += 2 * Math.PI; + } + + // Center for the sliced out slice + point.slicedTranslation = { + translateX: Math.round(Math.cos(angle) * slicedOffset), + translateY: Math.round(Math.sin(angle) * slicedOffset) + }; + + // set the anchor point for tooltips + radiusX = Math.cos(angle) * positions[2] / 2; + radiusY = Math.sin(angle) * positions[2] / 2; + pointRadiusX = Math.cos(angle) * pointRadii; + pointRadiusY = Math.sin(angle) * pointRadii; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; + + point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? + 1 : + 0; + point.angle = angle; + + // Set the anchor point for data labels. Use point.labelDistance + // instead of labelDistance // #1174 + // finalConnectorOffset - not override connectorOffset value. + finalConnectorOffset = Math.min( + connectorOffset, + point.labelDistance / 5 + ); // #1678 + + point.labelPos = [ + positions[0] + pointRadiusX + + // first break of connector + Math.cos(angle) * point.labelDistance, + positions[1] + pointRadiusY + + Math.sin(angle) * point.labelDistance, // a/a + positions[0] + pointRadiusX + + // second break, right outside pie + Math.cos(angle) * finalConnectorOffset, + positions[1] + pointRadiusY + + Math.sin(angle) * finalConnectorOffset, // a/a + positions[0] + pointRadiusX, // landing point for connector + positions[1] + pointRadiusY, // a/a + point.labelDistance < 0 ? // alignment + 'center' : + point.half ? 'right' : 'left', // alignment + angle // center angle + ]; + } + } + } ); /** * A `variablepie` series. If the [type](#series.variablepie.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.variablepie * @excluding dataParser,dataURL,stack,xAxis,yAxis @@ -420,24 +420,24 @@ seriesType('variablepie', 'pie', /** * An array of data points for the series. For the `variablepie` series type, * points can be given in the following ways: - * + * * 1. An array of arrays with 2 values. In this case, the numerical values * will be interpreted as `y, z` options. Example: - * + * * ```js * data: [ * [40, 75], * [50, 50], * [60, 40] - * ] + * ] * ``` - * + * * 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.variablepie.turboThreshold), this option is not * available. - * + * * ```js * data: [{ * y: 1, @@ -451,7 +451,7 @@ seriesType('variablepie', 'pie', * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.pie.data * @excluding marker,x diff --git a/js/modules/variwide.src.js b/js/modules/variwide.src.js index ce9dc2069c8..f7b4c87893b 100644 --- a/js/modules/variwide.src.js +++ b/js/modules/variwide.src.js @@ -17,15 +17,15 @@ import H from '../parts/Globals.js'; import '../parts/AreaSeries.js'; var addEvent = H.addEvent, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes, - each = H.each, - pick = H.pick; + seriesType = H.seriesType, + seriesTypes = H.seriesTypes, + each = H.each, + pick = H.pick; /** * A variwide chart (related to marimekko chart) is a column chart with a * variable width expressing a third dimension. - * + * * @extends {plotOptions.column} * @excluding boostThreshold,crisp,depth,edgeColor,edgeWidth,groupZPadding * @product highcharts @@ -35,154 +35,154 @@ var addEvent = H.addEvent, * @optionparent plotOptions.variwide */ seriesType('variwide', 'column', { - /** - * In a variwide chart, the point padding is 0 in order to express the - * horizontal stacking of items. - */ - pointPadding: 0, - /** - * In a variwide chart, the group padding is 0 in order to express the - * horizontal stacking of items. - */ - groupPadding: 0 + /** + * In a variwide chart, the point padding is 0 in order to express the + * horizontal stacking of items. + */ + pointPadding: 0, + /** + * In a variwide chart, the group padding is 0 in order to express the + * horizontal stacking of items. + */ + groupPadding: 0 }, { - pointArrayMap: ['y', 'z'], - parallelArrays: ['x', 'y', 'z'], - processData: function () { - var series = this; - this.totalZ = 0; - this.relZ = []; - seriesTypes.column.prototype.processData.call(this); + pointArrayMap: ['y', 'z'], + parallelArrays: ['x', 'y', 'z'], + processData: function () { + var series = this; + this.totalZ = 0; + this.relZ = []; + seriesTypes.column.prototype.processData.call(this); - each(this.zData, function (z, i) { - series.relZ[i] = series.totalZ; - series.totalZ += z; - }); + each(this.zData, function (z, i) { + series.relZ[i] = series.totalZ; + series.totalZ += z; + }); - if (this.xAxis.categories) { - this.xAxis.variwide = true; - } - }, + if (this.xAxis.categories) { + this.xAxis.variwide = true; + } + }, - /** - * Translate an x value inside a given category index into the distorted - * axis translation. - * @param {Number} index The category index - * @param {Number} x The X pixel position in undistorted axis pixels - * @return {Number} Distorted X position - */ - postTranslate: function (index, x) { + /** + * Translate an x value inside a given category index into the distorted + * axis translation. + * @param {Number} index The category index + * @param {Number} x The X pixel position in undistorted axis pixels + * @return {Number} Distorted X position + */ + postTranslate: function (index, x) { - var axis = this.xAxis, - relZ = this.relZ, - i = index, - len = axis.len, - totalZ = this.totalZ, - linearSlotLeft = i / relZ.length * len, - linearSlotRight = (i + 1) / relZ.length * len, - slotLeft = (pick(relZ[i], totalZ) / totalZ) * len, - slotRight = (pick(relZ[i + 1], totalZ) / totalZ) * len, - xInsideLinearSlot = x - linearSlotLeft, - ret; + var axis = this.xAxis, + relZ = this.relZ, + i = index, + len = axis.len, + totalZ = this.totalZ, + linearSlotLeft = i / relZ.length * len, + linearSlotRight = (i + 1) / relZ.length * len, + slotLeft = (pick(relZ[i], totalZ) / totalZ) * len, + slotRight = (pick(relZ[i + 1], totalZ) / totalZ) * len, + xInsideLinearSlot = x - linearSlotLeft, + ret; - ret = slotLeft + - xInsideLinearSlot * (slotRight - slotLeft) / - (linearSlotRight - linearSlotLeft); + ret = slotLeft + + xInsideLinearSlot * (slotRight - slotLeft) / + (linearSlotRight - linearSlotLeft); - return ret; - }, + return ret; + }, - /** - * Extend translation by distoring X position based on Z. - */ - translate: function () { + /** + * Extend translation by distoring X position based on Z. + */ + translate: function () { - // Temporarily disable crisping when computing original shapeArgs - var crispOption = this.options.crisp; - this.options.crisp = false; + // Temporarily disable crisping when computing original shapeArgs + var crispOption = this.options.crisp; + this.options.crisp = false; - seriesTypes.column.prototype.translate.call(this); + seriesTypes.column.prototype.translate.call(this); - // Reset option - this.options.crisp = crispOption; + // Reset option + this.options.crisp = crispOption; - var inverted = this.chart.inverted, - crisp = this.borderWidth % 2 / 2; + var inverted = this.chart.inverted, + crisp = this.borderWidth % 2 / 2; - // Distort the points to reflect z dimension - each(this.points, function (point, i) { - var left = this.postTranslate( - i, - point.shapeArgs.x - ), - right = this.postTranslate( - i, - point.shapeArgs.x + point.shapeArgs.width - ); + // Distort the points to reflect z dimension + each(this.points, function (point, i) { + var left = this.postTranslate( + i, + point.shapeArgs.x + ), + right = this.postTranslate( + i, + point.shapeArgs.x + point.shapeArgs.width + ); - if (this.options.crisp) { - left = Math.round(left) - crisp; - right = Math.round(right) - crisp; - } + if (this.options.crisp) { + left = Math.round(left) - crisp; + right = Math.round(right) - crisp; + } - point.shapeArgs.x = left; - point.shapeArgs.width = right - left; + point.shapeArgs.x = left; + point.shapeArgs.width = right - left; - point.tooltipPos[inverted ? 1 : 0] = this.postTranslate( - i, - point.tooltipPos[inverted ? 1 : 0] - ); - }, this); - } + point.tooltipPos[inverted ? 1 : 0] = this.postTranslate( + i, + point.tooltipPos[inverted ? 1 : 0] + ); + }, this); + } // Point functions }, { - isValid: function () { - return H.isNumber(this.y, true) && H.isNumber(this.z, true); - } + isValid: function () { + return H.isNumber(this.y, true) && H.isNumber(this.z, true); + } }); H.Tick.prototype.postTranslate = function (xy, xOrY, index) { - xy[xOrY] = this.axis.pos + - this.axis.series[0].postTranslate(index, xy[xOrY] - this.axis.pos); + xy[xOrY] = this.axis.pos + + this.axis.series[0].postTranslate(index, xy[xOrY] - this.axis.pos); }; addEvent(H.Tick, 'afterGetPosition', function (e) { - var axis = this.axis, - xOrY = axis.horiz ? 'x' : 'y'; + var axis = this.axis, + xOrY = axis.horiz ? 'x' : 'y'; - if (axis.categories && axis.variwide) { - this[xOrY + 'Orig'] = e.pos[xOrY]; - this.postTranslate(e.pos, xOrY, this.pos); - } + if (axis.categories && axis.variwide) { + this[xOrY + 'Orig'] = e.pos[xOrY]; + this.postTranslate(e.pos, xOrY, this.pos); + } }); H.wrap(H.Tick.prototype, 'getLabelPosition', function ( - proceed, - x, - y, - label, - horiz, - labelOptions, - tickmarkOffset, - index + proceed, + x, + y, + label, + horiz, + labelOptions, + tickmarkOffset, + index ) { - var args = Array.prototype.slice.call(arguments, 1), - xy, - xOrY = horiz ? 'x' : 'y'; + var args = Array.prototype.slice.call(arguments, 1), + xy, + xOrY = horiz ? 'x' : 'y'; - // Replace the x with the original x - if (this.axis.variwide && typeof this[xOrY + 'Orig'] === 'number') { - args[horiz ? 0 : 1] = this[xOrY + 'Orig']; - } + // Replace the x with the original x + if (this.axis.variwide && typeof this[xOrY + 'Orig'] === 'number') { + args[horiz ? 0 : 1] = this[xOrY + 'Orig']; + } - xy = proceed.apply(this, args); + xy = proceed.apply(this, args); - // Post-translate - if (this.axis.variwide && this.axis.categories) { - this.postTranslate(xy, xOrY, index); - } - return xy; + // Post-translate + if (this.axis.variwide && this.axis.categories) { + this.postTranslate(xy, xOrY, index); + } + return xy; }); @@ -190,7 +190,7 @@ H.wrap(H.Tick.prototype, 'getLabelPosition', function ( /** * A `variwide` series. If the [type](#series.variwide.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.variwide * @product highcharts @@ -200,7 +200,7 @@ H.wrap(H.Tick.prototype, 'getLabelPosition', function ( /** * An array of data points for the series. For the `variwide` series type, * points can be given in the following ways: - * + * * 1. An array of arrays with 3 or 2 values. In this case, the values * correspond to `x,y,z`. If the first value is a string, it is applied * as the name of the point, and the `x` value is inferred. The `x` @@ -208,7 +208,7 @@ H.wrap(H.Tick.prototype, 'getLabelPosition', function ( * be of length 2\. 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, 1, 2], @@ -216,12 +216,12 @@ H.wrap(H.Tick.prototype, 'getLabelPosition', function ( * [2, 0, 2] * ] * ``` - * + * * 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.variwide.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -237,7 +237,7 @@ H.wrap(H.Tick.prototype, 'getLabelPosition', function ( * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding marker @@ -250,15 +250,15 @@ H.wrap(H.Tick.prototype, 'getLabelPosition', function ( * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts * @apioption series.variwide.data */ /** - * The relative width for each column. The widths are distributed so they sum + * The relative width for each column. The widths are distributed so they sum * up to the X axis length. - * + * * @type {Number} * @product highcharts * @apioption series.variwide.data.z diff --git a/js/modules/vector.src.js b/js/modules/vector.src.js index d9816a2be3a..cf08438a3cc 100644 --- a/js/modules/vector.src.js +++ b/js/modules/vector.src.js @@ -10,12 +10,12 @@ import H from '../parts/Globals.js'; var each = H.each, - seriesType = H.seriesType; + seriesType = H.seriesType; /** * A vector plot is a type of cartesian chart where each point has an X and Y * position, a length and a direction. Vectors are drawn as arrows. - * + * * @extends {plotOptions.scatter} * @excluding boostThreshold,marker,connectEnds,connectNulls,cropThreshold, * dashStyle,gapSize,gapUnit,dataGrouping,linecap,shadow,stacking, @@ -28,189 +28,189 @@ var each = H.each, */ seriesType('vector', 'scatter', { - /** - * The line width for each vector arrow. - */ - lineWidth: 2, - - /** - * @ignore - */ - marker: null, - /** - * What part of the vector it should be rotated around. Can be one of - * `start`, `center` and `end`. When `start`, the vectors will start from - * the given [x, y] position, and when `end` the vectors will end in the - * [x, y] position. - * - * @sample highcharts/plotoptions/vector-rotationorigin-start/ - * Rotate from start - * @validvalue ["start", "center", "end"] - */ - rotationOrigin: 'center', - states: { - hover: { - /** - * Additonal line width for the vector errors when they are hovered. - */ - lineWidthPlus: 1 - } - }, - tooltip: { - pointFormat: '[{point.x}, {point.y}]
Length: {point.length}
Direction: {point.direction}\u00B0
' - }, - /** - * Maximum length of the arrows in the vector plot. The individual arrow - * length is computed between 0 and this value. - */ - vectorLength: 20 - + /** + * The line width for each vector arrow. + */ + lineWidth: 2, + + /** + * @ignore + */ + marker: null, + /** + * What part of the vector it should be rotated around. Can be one of + * `start`, `center` and `end`. When `start`, the vectors will start from + * the given [x, y] position, and when `end` the vectors will end in the + * [x, y] position. + * + * @sample highcharts/plotoptions/vector-rotationorigin-start/ + * Rotate from start + * @validvalue ["start", "center", "end"] + */ + rotationOrigin: 'center', + states: { + hover: { + /** + * Additonal line width for the vector errors when they are hovered. + */ + lineWidthPlus: 1 + } + }, + tooltip: { + pointFormat: '[{point.x}, {point.y}]
Length: {point.length}
Direction: {point.direction}\u00B0
' + }, + /** + * Maximum length of the arrows in the vector plot. The individual arrow + * length is computed between 0 and this value. + */ + vectorLength: 20 + }, { - pointArrayMap: ['y', 'length', 'direction'], - parallelArrays: ['x', 'y', 'length', 'direction'], - - /** - * Get presentational attributes. - */ - pointAttribs: function (point, state) { - var options = this.options, - stroke = point.color || this.color, - strokeWidth = this.options.lineWidth; - - if (state) { - stroke = options.states[state].color || stroke; - strokeWidth = - (options.states[state].lineWidth || strokeWidth) + - (options.states[state].lineWidthPlus || 0); - } - - return { - 'stroke': stroke, - 'stroke-width': strokeWidth - }; - }, - markerAttribs: H.noop, - getSymbol: H.noop, - - /** - * Create a single arrow. It is later rotated around the zero - * centerpoint. - */ - arrow: function (point) { - var path, - fraction = point.length / this.lengthMax, - o = { - start: 10, - center: 0, - end: -10 - }[this.options.rotationOrigin] || 0, - u = fraction * this.options.vectorLength / 20; - - // The stem and the arrow head. Draw the arrow first with rotation 0, - // which is the arrow pointing down (vector from north to south). - path = [ - 'M', 0, 7 * u + o, // base of arrow - 'L', -1.5 * u, 7 * u + o, - 0, 10 * u + o, - 1.5 * u, 7 * u + o, - 0, 7 * u + o, - 0, -10 * u + o// top - ]; - - return path; - }, - - translate: function () { - H.Series.prototype.translate.call(this); - - this.lengthMax = H.arrayMax(this.lengthData); - }, - - - drawPoints: function () { - - var chart = this.chart; - - each(this.points, function (point) { - var plotX = point.plotX, - plotY = point.plotY; - - if (chart.isInsidePlot(plotX, plotY, chart.inverted)) { - - if (!point.graphic) { - point.graphic = this.chart.renderer - .path() - .add(this.markerGroup); - } - point.graphic - .attr({ - d: this.arrow(point), - translateX: plotX, - translateY: plotY, - rotation: point.direction - }) - .attr(this.pointAttribs(point)); - - } else if (point.graphic) { - point.graphic = point.graphic.destroy(); - } - - }, this); - }, - - drawGraph: H.noop, - - /* - drawLegendSymbol: function (legend, item) { - var options = legend.options, - symbolHeight = legend.symbolHeight, - square = options.squareSymbol, - symbolWidth = square ? symbolHeight : legend.symbolWidth, - path = this.arrow.call({ - lengthMax: 1, - options: { - vectorLength: symbolWidth - } - }, { - length: 1 - }); - - item.legendLine = this.chart.renderer.path(path) - .addClass('highcharts-point') - .attr({ - zIndex: 3, - translateY: symbolWidth / 2, - rotation: 270, - 'stroke-width': 1, - 'stroke': 'black' - }).add(item.legendGroup); - - }, - */ - - /** - * Fade in the arrows on initiating series. - */ - animate: function (init) { - if (init) { - this.markerGroup.attr({ - opacity: 0.01 - }); - } else { - this.markerGroup.animate({ - opacity: 1 - }, H.animObject(this.options.animation)); - - this.animate = null; - } - } + pointArrayMap: ['y', 'length', 'direction'], + parallelArrays: ['x', 'y', 'length', 'direction'], + + /** + * Get presentational attributes. + */ + pointAttribs: function (point, state) { + var options = this.options, + stroke = point.color || this.color, + strokeWidth = this.options.lineWidth; + + if (state) { + stroke = options.states[state].color || stroke; + strokeWidth = + (options.states[state].lineWidth || strokeWidth) + + (options.states[state].lineWidthPlus || 0); + } + + return { + 'stroke': stroke, + 'stroke-width': strokeWidth + }; + }, + markerAttribs: H.noop, + getSymbol: H.noop, + + /** + * Create a single arrow. It is later rotated around the zero + * centerpoint. + */ + arrow: function (point) { + var path, + fraction = point.length / this.lengthMax, + o = { + start: 10, + center: 0, + end: -10 + }[this.options.rotationOrigin] || 0, + u = fraction * this.options.vectorLength / 20; + + // The stem and the arrow head. Draw the arrow first with rotation 0, + // which is the arrow pointing down (vector from north to south). + path = [ + 'M', 0, 7 * u + o, // base of arrow + 'L', -1.5 * u, 7 * u + o, + 0, 10 * u + o, + 1.5 * u, 7 * u + o, + 0, 7 * u + o, + 0, -10 * u + o// top + ]; + + return path; + }, + + translate: function () { + H.Series.prototype.translate.call(this); + + this.lengthMax = H.arrayMax(this.lengthData); + }, + + + drawPoints: function () { + + var chart = this.chart; + + each(this.points, function (point) { + var plotX = point.plotX, + plotY = point.plotY; + + if (chart.isInsidePlot(plotX, plotY, chart.inverted)) { + + if (!point.graphic) { + point.graphic = this.chart.renderer + .path() + .add(this.markerGroup); + } + point.graphic + .attr({ + d: this.arrow(point), + translateX: plotX, + translateY: plotY, + rotation: point.direction + }) + .attr(this.pointAttribs(point)); + + } else if (point.graphic) { + point.graphic = point.graphic.destroy(); + } + + }, this); + }, + + drawGraph: H.noop, + + /* + drawLegendSymbol: function (legend, item) { + var options = legend.options, + symbolHeight = legend.symbolHeight, + square = options.squareSymbol, + symbolWidth = square ? symbolHeight : legend.symbolWidth, + path = this.arrow.call({ + lengthMax: 1, + options: { + vectorLength: symbolWidth + } + }, { + length: 1 + }); + + item.legendLine = this.chart.renderer.path(path) + .addClass('highcharts-point') + .attr({ + zIndex: 3, + translateY: symbolWidth / 2, + rotation: 270, + 'stroke-width': 1, + 'stroke': 'black' + }).add(item.legendGroup); + + }, + */ + + /** + * Fade in the arrows on initiating series. + */ + animate: function (init) { + if (init) { + this.markerGroup.attr({ + opacity: 0.01 + }); + } else { + this.markerGroup.animate({ + opacity: 1 + }, H.animObject(this.options.animation)); + + this.animate = null; + } + } }); /** * A `vector` series. If the [type](#series.vector.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.vector * @excluding dataParser,dataURL @@ -221,11 +221,11 @@ seriesType('vector', 'scatter', { /** * An array of data points for the series. For the `vector` series type, * points can be given in the following ways: - * + * * 1. An array of arrays with 4 values. In this case, the values correspond * to `x,y,length,direction`. If the first value is a string, it is applied as * the name of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 0, 10, 90], @@ -233,12 +233,12 @@ seriesType('vector', 'scatter', { * [1, 1, 2, 270] * ] * ``` - * + * * 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.area.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 0, @@ -253,7 +253,7 @@ seriesType('vector', 'scatter', { * direction: 270 * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -265,7 +265,7 @@ seriesType('vector', 'scatter', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts highstock * @apioption series.vector.data */ @@ -273,7 +273,7 @@ seriesType('vector', 'scatter', { /** * The length of the vector. The rendered length will relate to the * `vectorLength` setting. - * + * * @type {Number} * @product highcharts highstock * @apioption series.vector.data.length @@ -281,7 +281,7 @@ seriesType('vector', 'scatter', { /** * The vector direction in degrees, where 0 is north (pointing towards south). - * + * * @type {Number} * @product highcharts highstock * @apioption series.vector.data.direction diff --git a/js/modules/windbarb.src.js b/js/modules/windbarb.src.js index 4d8d709ae1f..dc852b0b623 100644 --- a/js/modules/windbarb.src.js +++ b/js/modules/windbarb.src.js @@ -10,14 +10,14 @@ import H from '../parts/Globals.js'; import onSeriesMixin from '../mixins/on-series.js'; var each = H.each, - noop = H.noop, - seriesType = H.seriesType; + noop = H.noop, + seriesType = H.seriesType; /** * Wind barbs are a convenient way to represent wind speed and direction in one * graphical form. Wind direction is given by the stem direction, and wind speed - * by the number and shape of barbs. - * + * by the number and shape of barbs. + * * @extends {plotOptions.column} * @excluding boostThreshold,marker,connectEnds,connectNulls,cropThreshold, * dashStyle,gapSize,gapUnit,dataGrouping,linecap,shadow,stacking, @@ -29,272 +29,272 @@ var each = H.each, * @optionparent plotOptions.windbarb */ seriesType('windbarb', 'column', { - /** - * The line width of the wind barb symbols. - */ - lineWidth: 2, - /** - * The id of another series in the chart that the wind barbs are projected - * on. When `null`, the wind symbols are drawn on the X axis, but offset - * up or down by the `yOffset` setting. - * - * @sample {highcharts|highstock} highcharts/plotoptions/windbarb-onseries - * Projected on area series - * @type {String|null} - */ - onSeries: null, - states: { - hover: { - lineWidthPlus: 0 - } - }, - tooltip: { - /** - * The default point format for the wind barb tooltip. Note the - * `point.beaufort` property that refers to the Beaufort wind scale. The - * names can be internationalized by modifying - * `Highcharts.seriesTypes.windbarb.prototype.beaufortNames`. - */ - pointFormat: '\u25CF {series.name}: {point.value} ({point.beaufort})
' - }, - /** - * Pixel length of the stems. - */ - vectorLength: 20, - /** - * Vertical offset from the cartesian position, in pixels. The default value - * makes sure the symbols don't overlap the X axis when `onSeries` is - * `null`, and that they don't overlap the linked series when `onSeries` is - * given. - */ - yOffset: -20, - /** - * Horizontal offset from the cartesian position, in pixels. When the chart - * is inverted, this option allows translation like - * [yOffset](#plotOptions.windbarb.yOffset) in non inverted charts. - * - * @since 6.1.0 - */ - xOffset: 0 + /** + * The line width of the wind barb symbols. + */ + lineWidth: 2, + /** + * The id of another series in the chart that the wind barbs are projected + * on. When `null`, the wind symbols are drawn on the X axis, but offset + * up or down by the `yOffset` setting. + * + * @sample {highcharts|highstock} highcharts/plotoptions/windbarb-onseries + * Projected on area series + * @type {String|null} + */ + onSeries: null, + states: { + hover: { + lineWidthPlus: 0 + } + }, + tooltip: { + /** + * The default point format for the wind barb tooltip. Note the + * `point.beaufort` property that refers to the Beaufort wind scale. The + * names can be internationalized by modifying + * `Highcharts.seriesTypes.windbarb.prototype.beaufortNames`. + */ + pointFormat: '\u25CF {series.name}: {point.value} ({point.beaufort})
' + }, + /** + * Pixel length of the stems. + */ + vectorLength: 20, + /** + * Vertical offset from the cartesian position, in pixels. The default value + * makes sure the symbols don't overlap the X axis when `onSeries` is + * `null`, and that they don't overlap the linked series when `onSeries` is + * given. + */ + yOffset: -20, + /** + * Horizontal offset from the cartesian position, in pixels. When the chart + * is inverted, this option allows translation like + * [yOffset](#plotOptions.windbarb.yOffset) in non inverted charts. + * + * @since 6.1.0 + */ + xOffset: 0 }, { - pointArrayMap: ['value', 'direction'], - parallelArrays: ['x', 'value', 'direction'], - beaufortName: ['Calm', 'Light air', 'Light breeze', - 'Gentle breeze', 'Moderate breeze', 'Fresh breeze', - 'Strong breeze', 'Near gale', 'Gale', 'Strong gale', 'Storm', - 'Violent storm', 'Hurricane'], - beaufortFloor: [0, 0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, - 24.5, 28.5, 32.7], - trackerGroups: ['markerGroup'], - - /** - * Get presentational attributes. - */ - pointAttribs: function (point, state) { - var options = this.options, - stroke = point.color || this.color, - strokeWidth = this.options.lineWidth; - - if (state) { - stroke = options.states[state].color || stroke; - strokeWidth = - (options.states[state].lineWidth || strokeWidth) + - (options.states[state].lineWidthPlus || 0); - } - - return { - 'stroke': stroke, - 'stroke-width': strokeWidth - }; - }, - markerAttribs: function () { - return undefined; - }, - getPlotBox: onSeriesMixin.getPlotBox, - /** - * Create a single wind arrow. It is later rotated around the zero - * centerpoint. - */ - windArrow: function (point) { - var knots = point.value * 1.943844, - level = point.beaufortLevel, - path, - barbs, - u = this.options.vectorLength / 20, - pos = -10; - - if (point.isNull) { - return []; - } - - if (level === 0) { - return this.chart.renderer.symbols.circle( - -10 * u, - -10 * u, - 20 * u, - 20 * u - ); - } - - // The stem and the arrow head - path = [ - 'M', 0, 7 * u, // base of arrow - 'L', -1.5 * u, 7 * u, - 0, 10 * u, - 1.5 * u, 7 * u, - 0, 7 * u, - 0, -10 * u// top - ]; - - // For each full 50 knots, add a pennant - barbs = (knots - knots % 50) / 50; // pennants - if (barbs > 0) { - while (barbs--) { - path.push( - pos === -10 ? 'L' : 'M', - 0, - pos * u, - 'L', - 5 * u, - pos * u + 2, - 'L', - 0, - pos * u + 4 - - ); - - // Substract from the rest and move position for next - knots -= 50; - pos += 7; - } - } - - // For each full 10 knots, add a full barb - barbs = (knots - knots % 10) / 10; - if (barbs > 0) { - while (barbs--) { - path.push( - pos === -10 ? 'L' : 'M', - 0, - pos * u, - 'L', - 7 * u, - pos * u - ); - knots -= 10; - pos += 3; - } - } - - // For each full 5 knots, add a half barb - barbs = (knots - knots % 5) / 5; // half barbs - if (barbs > 0) { - while (barbs--) { - path.push( - pos === -10 ? 'L' : 'M', - 0, - pos * u, - 'L', - 4 * u, - pos * u - ); - knots -= 5; - pos += 3; - } - } - return path; - }, - - translate: function () { - var beaufortFloor = this.beaufortFloor, - beaufortName = this.beaufortName; - - onSeriesMixin.translate.call(this); - - each(this.points, function (point) { - var level = 0; - // Find the beaufort level (zero based) - for (; level < beaufortFloor.length; level++) { - if (beaufortFloor[level] > point.value) { - break; - } - } - point.beaufortLevel = level - 1; - point.beaufort = beaufortName[level - 1]; - - }); - - }, - - drawPoints: function () { - var chart = this.chart, - yAxis = this.yAxis, - inverted = chart.inverted, - shapeOffset = this.options.vectorLength / 2; - - each(this.points, function (point) { - var plotX = point.plotX, - plotY = point.plotY; - - // Check if it's inside the plot area, but only for the X dimension. - if (chart.isInsidePlot(plotX, 0, false)) { - - // Create the graphic the first time - if (!point.graphic) { - point.graphic = this.chart.renderer - .path() - .add(this.markerGroup); - } - - // Position the graphic - point.graphic - .attr({ - d: this.windArrow(point), - translateX: plotX + this.options.xOffset, - translateY: plotY + this.options.yOffset, - rotation: point.direction - }) - .attr(this.pointAttribs(point)); - - } else if (point.graphic) { - point.graphic = point.graphic.destroy(); - } - - // Set the tooltip anchor position - point.tooltipPos = [ - plotX + this.options.xOffset + (inverted && !this.onSeries ? - shapeOffset : 0), - plotY + this.options.yOffset - (inverted ? 0 : - shapeOffset + yAxis.pos - chart.plotTop) - ]; // #6327 - }, this); - }, - - /** - * Fade in the arrows on initiating series. - */ - animate: function (init) { - if (init) { - this.markerGroup.attr({ - opacity: 0.01 - }); - } else { - this.markerGroup.animate({ - opacity: 1 - }, H.animObject(this.options.animation)); - - this.animate = null; - } - }, - - /** - * Don't invert the marker group (#4960) - */ - invertGroups: noop + pointArrayMap: ['value', 'direction'], + parallelArrays: ['x', 'value', 'direction'], + beaufortName: ['Calm', 'Light air', 'Light breeze', + 'Gentle breeze', 'Moderate breeze', 'Fresh breeze', + 'Strong breeze', 'Near gale', 'Gale', 'Strong gale', 'Storm', + 'Violent storm', 'Hurricane'], + beaufortFloor: [0, 0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, + 24.5, 28.5, 32.7], + trackerGroups: ['markerGroup'], + + /** + * Get presentational attributes. + */ + pointAttribs: function (point, state) { + var options = this.options, + stroke = point.color || this.color, + strokeWidth = this.options.lineWidth; + + if (state) { + stroke = options.states[state].color || stroke; + strokeWidth = + (options.states[state].lineWidth || strokeWidth) + + (options.states[state].lineWidthPlus || 0); + } + + return { + 'stroke': stroke, + 'stroke-width': strokeWidth + }; + }, + markerAttribs: function () { + return undefined; + }, + getPlotBox: onSeriesMixin.getPlotBox, + /** + * Create a single wind arrow. It is later rotated around the zero + * centerpoint. + */ + windArrow: function (point) { + var knots = point.value * 1.943844, + level = point.beaufortLevel, + path, + barbs, + u = this.options.vectorLength / 20, + pos = -10; + + if (point.isNull) { + return []; + } + + if (level === 0) { + return this.chart.renderer.symbols.circle( + -10 * u, + -10 * u, + 20 * u, + 20 * u + ); + } + + // The stem and the arrow head + path = [ + 'M', 0, 7 * u, // base of arrow + 'L', -1.5 * u, 7 * u, + 0, 10 * u, + 1.5 * u, 7 * u, + 0, 7 * u, + 0, -10 * u// top + ]; + + // For each full 50 knots, add a pennant + barbs = (knots - knots % 50) / 50; // pennants + if (barbs > 0) { + while (barbs--) { + path.push( + pos === -10 ? 'L' : 'M', + 0, + pos * u, + 'L', + 5 * u, + pos * u + 2, + 'L', + 0, + pos * u + 4 + + ); + + // Substract from the rest and move position for next + knots -= 50; + pos += 7; + } + } + + // For each full 10 knots, add a full barb + barbs = (knots - knots % 10) / 10; + if (barbs > 0) { + while (barbs--) { + path.push( + pos === -10 ? 'L' : 'M', + 0, + pos * u, + 'L', + 7 * u, + pos * u + ); + knots -= 10; + pos += 3; + } + } + + // For each full 5 knots, add a half barb + barbs = (knots - knots % 5) / 5; // half barbs + if (barbs > 0) { + while (barbs--) { + path.push( + pos === -10 ? 'L' : 'M', + 0, + pos * u, + 'L', + 4 * u, + pos * u + ); + knots -= 5; + pos += 3; + } + } + return path; + }, + + translate: function () { + var beaufortFloor = this.beaufortFloor, + beaufortName = this.beaufortName; + + onSeriesMixin.translate.call(this); + + each(this.points, function (point) { + var level = 0; + // Find the beaufort level (zero based) + for (; level < beaufortFloor.length; level++) { + if (beaufortFloor[level] > point.value) { + break; + } + } + point.beaufortLevel = level - 1; + point.beaufort = beaufortName[level - 1]; + + }); + + }, + + drawPoints: function () { + var chart = this.chart, + yAxis = this.yAxis, + inverted = chart.inverted, + shapeOffset = this.options.vectorLength / 2; + + each(this.points, function (point) { + var plotX = point.plotX, + plotY = point.plotY; + + // Check if it's inside the plot area, but only for the X dimension. + if (chart.isInsidePlot(plotX, 0, false)) { + + // Create the graphic the first time + if (!point.graphic) { + point.graphic = this.chart.renderer + .path() + .add(this.markerGroup); + } + + // Position the graphic + point.graphic + .attr({ + d: this.windArrow(point), + translateX: plotX + this.options.xOffset, + translateY: plotY + this.options.yOffset, + rotation: point.direction + }) + .attr(this.pointAttribs(point)); + + } else if (point.graphic) { + point.graphic = point.graphic.destroy(); + } + + // Set the tooltip anchor position + point.tooltipPos = [ + plotX + this.options.xOffset + (inverted && !this.onSeries ? + shapeOffset : 0), + plotY + this.options.yOffset - (inverted ? 0 : + shapeOffset + yAxis.pos - chart.plotTop) + ]; // #6327 + }, this); + }, + + /** + * Fade in the arrows on initiating series. + */ + animate: function (init) { + if (init) { + this.markerGroup.attr({ + opacity: 0.01 + }); + } else { + this.markerGroup.animate({ + opacity: 1 + }, H.animObject(this.options.animation)); + + this.animate = null; + } + }, + + /** + * Don't invert the marker group (#4960) + */ + invertGroups: noop }, { - isValid: function () { - return H.isNumber(this.value) && this.value >= 0; - } + isValid: function () { + return H.isNumber(this.value) && this.value >= 0; + } }); @@ -302,7 +302,7 @@ seriesType('windbarb', 'column', { /** * A `windbarb` series. If the [type](#series.windbarb.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.windbarb * @excluding dataParser,dataURL @@ -313,11 +313,11 @@ seriesType('windbarb', 'column', { /** * An array of data points for the series. For the `windbarb` series type, * points can be given in the following ways: - * + * * 1. An array of arrays with 3 values. In this case, the values correspond * to `x,value,direction`. If the first value is a string, it is applied as * the name of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [Date.UTC(2017, 0, 1, 0), 3.3, 90], @@ -325,12 +325,12 @@ seriesType('windbarb', 'column', { * [Date.UTC(2017, 0, 1, 2), 11.1, 270] * ] * ``` - * + * * 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.area.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: Date.UTC(2017, 0, 1, 0), @@ -342,7 +342,7 @@ seriesType('windbarb', 'column', { * direction: 270 * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -354,14 +354,14 @@ seriesType('windbarb', 'column', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts highstock * @apioption series.windbarb.data */ /** * The wind speed in meters per second. - * + * * @type {Number} * @product highcharts highstock * @apioption series.windbarb.data.value @@ -369,7 +369,7 @@ seriesType('windbarb', 'column', { /** * The wind direction in degrees, where 0 is north (pointing towards south). - * + * * @type {Number} * @product highcharts highstock * @apioption series.windbarb.data.direction diff --git a/js/modules/wordcloud.src.js b/js/modules/wordcloud.src.js index 64cf2e2bd90..f9a6c306091 100644 --- a/js/modules/wordcloud.src.js +++ b/js/modules/wordcloud.src.js @@ -12,12 +12,12 @@ import H from '../parts/Globals.js'; import drawPoint from '../mixins/draw-point.js'; import '../parts/Series.js'; var each = H.each, - extend = H.extend, - isArray = H.isArray, - isNumber = H.isNumber, - isObject = H.isObject, - reduce = H.reduce, - Series = H.Series; + extend = H.extend, + isArray = H.isArray, + isNumber = H.isNumber, + isObject = H.isObject, + reduce = H.reduce, + Series = H.Series; /** * isRectanglesIntersecting - Detects if there is a collision between two @@ -28,12 +28,12 @@ var each = H.each, * @return {boolean} Returns true if the rectangles overlap. */ var isRectanglesIntersecting = function isRectanglesIntersecting(r1, r2) { - return !( - r2.left > r1.right || - r2.right < r1.left || - r2.top > r1.bottom || - r2.bottom < r1.top - ); + return !( + r2.left > r1.right || + r2.right < r1.left || + r2.top > r1.bottom || + r2.bottom < r1.top + ); }; /** @@ -45,29 +45,29 @@ var isRectanglesIntersecting = function isRectanglesIntersecting(r1, r2) { * @return {boolean} Returns true if there is collision. */ var intersectsAnyWord = function intersectsAnyWord(point, points) { - var intersects = false, - rect1 = point.rect, - rect2; - if (point.lastCollidedWith) { - rect2 = point.lastCollidedWith.rect; - intersects = isRectanglesIntersecting(rect1, rect2); - // If they no longer intersects, remove the cache from the point. - if (!intersects) { - delete point.lastCollidedWith; - } - } - if (!intersects) { - intersects = !!H.find(points, function (p) { - var result; - rect2 = p.rect; - result = isRectanglesIntersecting(rect1, rect2); - if (result) { - point.lastCollidedWith = p; - } - return result; - }); - } - return intersects; + var intersects = false, + rect1 = point.rect, + rect2; + if (point.lastCollidedWith) { + rect2 = point.lastCollidedWith.rect; + intersects = isRectanglesIntersecting(rect1, rect2); + // If they no longer intersects, remove the cache from the point. + if (!intersects) { + delete point.lastCollidedWith; + } + } + if (!intersects) { + intersects = !!H.find(points, function (p) { + var result; + rect2 = p.rect; + result = isRectanglesIntersecting(rect1, rect2); + if (result) { + point.lastCollidedWith = p; + } + return result; + }); + } + return intersects; }; /** @@ -80,21 +80,21 @@ var intersectsAnyWord = function intersectsAnyWord(point, points) { * should be dropped from the visualization. */ var archimedeanSpiral = function archimedeanSpiral(attempt, params) { - var field = params.field, - result = false, - maxDelta = (field.width * field.width) + (field.height * field.height), - t = attempt * 0.2; - // Emergency brake. TODO make spiralling logic more foolproof. - if (attempt <= 10000) { - result = { - x: t * Math.cos(t), - y: t * Math.sin(t) - }; - if (!(Math.min(Math.abs(result.x), Math.abs(result.y)) < maxDelta)) { - result = false; - } - } - return result; + var field = params.field, + result = false, + maxDelta = (field.width * field.width) + (field.height * field.height), + t = attempt * 0.2; + // Emergency brake. TODO make spiralling logic more foolproof. + if (attempt <= 10000) { + result = { + x: t * Math.cos(t), + y: t * Math.sin(t) + }; + if (!(Math.min(Math.abs(result.x), Math.abs(result.y)) < maxDelta)) { + result = false; + } + } + return result; }; /** @@ -106,47 +106,47 @@ var archimedeanSpiral = function archimedeanSpiral(attempt, params) { * should be dropped from the visualization. */ var squareSpiral = function squareSpiral(attempt) { - var k = Math.ceil((Math.sqrt(attempt) - 1) / 2), - t = 2 * k + 1, - m = Math.pow(t, 2), - isBoolean = function (x) { - return typeof x === 'boolean'; - }, - result = false; - t -= 1; - if (attempt <= 10000) { - if (isBoolean(result) && attempt >= m - t) { - result = { - x: k - (m - attempt), - y: -k - }; - } - m -= t; - if (isBoolean(result) && attempt >= m - t) { - result = { - x: -k, - y: -k + (m - attempt) - }; - } - - m -= t; - if (isBoolean(result)) { - if (attempt >= m - t) { - result = { - x: -k + (m - attempt), - y: k - }; - } else { - result = { - x: k, - y: k - (m - attempt - t) - }; - } - } - result.x *= 5; - result.y *= 5; - } - return result; + var k = Math.ceil((Math.sqrt(attempt) - 1) / 2), + t = 2 * k + 1, + m = Math.pow(t, 2), + isBoolean = function (x) { + return typeof x === 'boolean'; + }, + result = false; + t -= 1; + if (attempt <= 10000) { + if (isBoolean(result) && attempt >= m - t) { + result = { + x: k - (m - attempt), + y: -k + }; + } + m -= t; + if (isBoolean(result) && attempt >= m - t) { + result = { + x: -k, + y: -k + (m - attempt) + }; + } + + m -= t; + if (isBoolean(result)) { + if (attempt >= m - t) { + result = { + x: -k + (m - attempt), + y: k + }; + } else { + result = { + x: k, + y: k - (m - attempt - t) + }; + } + } + result.x *= 5; + result.y *= 5; + } + return result; }; /** @@ -158,12 +158,12 @@ var squareSpiral = function squareSpiral(attempt) { * should be dropped from the visualization. */ var rectangularSpiral = function rectangularSpiral(attempt, params) { - var result = squareSpiral(attempt, params), - field = params.field; - if (result) { - result.x *= field.ratio; - } - return result; + var result = squareSpiral(attempt, params), + field = params.field; + if (result) { + result.x *= field.ratio; + } + return result; }; /** @@ -173,7 +173,7 @@ var rectangularSpiral = function rectangularSpiral(attempt, params) { * @return {number} */ var getRandomPosition = function getRandomPosition(size) { - return Math.round((size * (Math.random() + 0.5)) / 2); + return Math.round((size * (Math.random() + 0.5)) / 2); }; /** @@ -188,11 +188,11 @@ var getRandomPosition = function getRandomPosition(size) { * of the target area. */ var getScale = function getScale(targetWidth, targetHeight, field) { - var height = Math.max(Math.abs(field.top), Math.abs(field.bottom)) * 2, - width = Math.max(Math.abs(field.left), Math.abs(field.right)) * 2, - scaleX = 1 / width * targetWidth, - scaleY = 1 / height * targetHeight; - return Math.min(scaleX, scaleY); + var height = Math.max(Math.abs(field.top), Math.abs(field.bottom)) * 2, + width = Math.max(Math.abs(field.left), Math.abs(field.right)) * 2, + scaleX = 1 / width * targetWidth, + scaleY = 1 / height * targetHeight; + return Math.min(scaleX, scaleY); }; /** @@ -207,36 +207,36 @@ var getScale = function getScale(targetWidth, targetHeight, field) { * @return {object} The width and height of the playing field. */ var getPlayingField = function getPlayingField( - targetWidth, - targetHeight, - data + targetWidth, + targetHeight, + data ) { - var ratio = targetWidth / targetHeight, - info = reduce(data, function (obj, point) { - var dimensions = point.dimensions; - // Find largest height. - obj.maxHeight = Math.max(obj.maxHeight, dimensions.height); - // Find largest width. - obj.maxWidth = Math.max(obj.maxWidth, dimensions.width); - // Sum up the total area of all the words. - obj.area += dimensions.width * dimensions.height; - return obj; - }, { - maxHeight: 0, - maxWidth: 0, - area: 0 - }), - /** - * Use largest width, largest height, or root of total area to give size - * to the playing field. - * Add extra 10 percentage to ensure enough space. - */ - x = 1.1 * Math.max(info.maxHeight, info.maxWidth, Math.sqrt(info.area)); - return { - width: x * ratio, - height: x, - ratio: ratio - }; + var ratio = targetWidth / targetHeight, + info = reduce(data, function (obj, point) { + var dimensions = point.dimensions; + // Find largest height. + obj.maxHeight = Math.max(obj.maxHeight, dimensions.height); + // Find largest width. + obj.maxWidth = Math.max(obj.maxWidth, dimensions.width); + // Sum up the total area of all the words. + obj.area += dimensions.width * dimensions.height; + return obj; + }, { + maxHeight: 0, + maxWidth: 0, + area: 0 + }), + /** + * Use largest width, largest height, or root of total area to give size + * to the playing field. + * Add extra 10 percentage to ensure enough space. + */ + x = 1.1 * Math.max(info.maxHeight, info.maxWidth, Math.sqrt(info.area)); + return { + width: x * ratio, + height: x, + ratio: ratio + }; }; @@ -252,27 +252,27 @@ var getPlayingField = function getPlayingField( * false if invalid input parameters. */ var getRotation = function getRotation(orientations, index, from, to) { - var result = false, // Default to false - range, - intervals, - orientation; + var result = false, // Default to false + range, + intervals, + orientation; - // Check if we have valid input parameters. - if ( - isNumber(orientations) && - isNumber(index) && - isNumber(from) && - isNumber(to) && - orientations > -1 && - index > -1 && - to > from - ) { - range = to - from; - intervals = range / (orientations - 1); - orientation = index % orientations; - result = from + (orientation * intervals); - } - return result; + // Check if we have valid input parameters. + if ( + isNumber(orientations) && + isNumber(index) && + isNumber(from) && + isNumber(to) && + orientations > -1 && + index > -1 && + to > from + ) { + range = to - from; + intervals = range / (orientations - 1); + orientation = index % orientations; + result = from + (orientation * intervals); + } + return result; }; /** @@ -283,19 +283,19 @@ var getRotation = function getRotation(orientations, index, from, to) { * @return {boolean} Returns true if the word is placed outside the field. */ var outsidePlayingField = function outsidePlayingField(wrapper, field) { - var rect = wrapper.getBBox(), - playingField = { - left: -(field.width / 2), - right: field.width / 2, - top: -(field.height / 2), - bottom: field.height / 2 - }; - return !( - playingField.left < (rect.x - rect.width / 2) && - playingField.right > (rect.x + rect.width / 2) && - playingField.top < (rect.y - rect.height / 2) && - playingField.bottom > (rect.y + rect.height / 2) - ); + var rect = wrapper.getBBox(), + playingField = { + left: -(field.width / 2), + right: field.width / 2, + top: -(field.height / 2), + bottom: field.height / 2 + }; + return !( + playingField.left < (rect.x - rect.width / 2) && + playingField.right > (rect.x + rect.width / 2) && + playingField.top < (rect.y - rect.height / 2) && + playingField.bottom > (rect.y + rect.height / 2) + ); }; /** @@ -304,48 +304,48 @@ var outsidePlayingField = function outsidePlayingField(wrapper, field) { * to adjusts the position. * * @param {object} point Point to test for intersections. - * @param {object} options Options object. + * @param {object} options Options object. * @return {boolean|object} Returns an object with how much to correct the * positions. Returns false if the word should not be placed at all. */ var intersectionTesting = function intersectionTesting(point, options) { - var placed = options.placed, - element = options.element, - field = options.field, - clientRect = options.clientRect, - spiral = options.spiral, - attempt = 1, - delta = { - x: 0, - y: 0 - }, - rect = point.rect = extend({}, clientRect); - /** - * while w intersects any previously placed words: - * do { - * move w a little bit along a spiral path - * } while any part of w is outside the playing field and - * the spiral radius is still smallish - */ - while ( - ( - intersectsAnyWord(point, placed) || - outsidePlayingField(element, field) - ) && delta !== false - ) { - delta = spiral(attempt, { - field: field - }); - if (isObject(delta)) { - // Update the DOMRect with new positions. - rect.left = clientRect.left + delta.x; - rect.right = rect.left + rect.width; - rect.top = clientRect.top + delta.y; - rect.bottom = rect.top + rect.height; - } - attempt++; - } - return delta; + var placed = options.placed, + element = options.element, + field = options.field, + clientRect = options.clientRect, + spiral = options.spiral, + attempt = 1, + delta = { + x: 0, + y: 0 + }, + rect = point.rect = extend({}, clientRect); + /** + * while w intersects any previously placed words: + * do { + * move w a little bit along a spiral path + * } while any part of w is outside the playing field and + * the spiral radius is still smallish + */ + while ( + ( + intersectsAnyWord(point, placed) || + outsidePlayingField(element, field) + ) && delta !== false + ) { + delta = spiral(attempt, { + field: field + }); + if (isObject(delta)) { + // Update the DOMRect with new positions. + rect.left = clientRect.left + delta.x; + rect.right = rect.left + rect.width; + rect.top = clientRect.top + delta.y; + rect.bottom = rect.top + rect.height; + } + attempt++; + } + return delta; }; /** @@ -358,20 +358,20 @@ var intersectionTesting = function intersectionTesting(point, options) { * @return {object} Returns a modified field object. */ var updateFieldBoundaries = function updateFieldBoundaries(field, rectangle) { - // TODO improve type checking. - if (!isNumber(field.left) || field.left > rectangle.left) { - field.left = rectangle.left; - } - if (!isNumber(field.right) || field.right < rectangle.right) { - field.right = rectangle.right; - } - if (!isNumber(field.top) || field.top > rectangle.top) { - field.top = rectangle.top; - } - if (!isNumber(field.bottom) || field.bottom < rectangle.bottom) { - field.bottom = rectangle.bottom; - } - return field; + // TODO improve type checking. + if (!isNumber(field.left) || field.left > rectangle.left) { + field.left = rectangle.left; + } + if (!isNumber(field.right) || field.right < rectangle.right) { + field.right = rectangle.right; + } + if (!isNumber(field.top) || field.top > rectangle.top) { + field.top = rectangle.top; + } + if (!isNumber(field.bottom) || field.bottom < rectangle.bottom) { + field.bottom = rectangle.bottom; + } + return field; }; /** @@ -393,334 +393,334 @@ var updateFieldBoundaries = function updateFieldBoundaries(field, rectangle) { * @optionparent plotOptions.wordcloud */ var wordCloudOptions = { - animation: { - duration: 500 - }, - borderWidth: 0, - clip: false, // Something goes wrong with clip. // TODO fix this - /** - * When using automatic point colors pulled from the `options.colors` - * collection, this option determines whether the chart should receive - * one color per series or one color per point. - * - * @see [series colors](#plotOptions.column.colors) - */ - colorByPoint: true, - /** - * This option decides which algorithm is used for placement, and rotation - * of a word. The choice of algorith is therefore a crucial part of the - * resulting layout of the wordcloud. - * It is possible for users to add their own custom placement strategies - * for use in word cloud. Read more about it in our - * [documentation](https://www.highcharts.com/docs/chart-and-series-types/word-cloud-series#custom-placement-strategies) - * - * @validvalue: ["center", "random"] - */ - placementStrategy: 'center', - /** - * Rotation options for the words in the wordcloud. - * @sample highcharts/plotoptions/wordcloud-rotation - * Word cloud with rotation - */ - rotation: { - /** - * The smallest degree of rotation for a word. - */ - from: 0, - /** - * The number of possible orientations for a word, within the range of - * `rotation.from` and `rotation.to`. - */ - orientations: 2, - /** - * The largest degree of rotation for a word. - */ - to: 90 - }, - showInLegend: false, - /** - * Spiral used for placing a word after the inital position experienced a - * collision with either another word or the borders. - * It is possible for users to add their own custom spiralling algorithms - * for use in word cloud. Read more about it in our - * [documentation](https://www.highcharts.com/docs/chart-and-series-types/word-cloud-series#custom-spiralling-algorithm) - * - * @validvalue: ["archimedean", "rectangular", "square"] - */ - spiral: 'rectangular', - /** - * CSS styles for the words. - * - * @type {CSSObject} - * @default {"fontFamily":"sans-serif", "fontWeight": "900"} - */ - style: { - fontFamily: 'sans-serif', - fontWeight: '900' - }, - tooltip: { - followPointer: true, - pointFormat: '\u25CF {series.name}: {point.weight}
' - } + animation: { + duration: 500 + }, + borderWidth: 0, + clip: false, // Something goes wrong with clip. // TODO fix this + /** + * When using automatic point colors pulled from the `options.colors` + * collection, this option determines whether the chart should receive + * one color per series or one color per point. + * + * @see [series colors](#plotOptions.column.colors) + */ + colorByPoint: true, + /** + * This option decides which algorithm is used for placement, and rotation + * of a word. The choice of algorith is therefore a crucial part of the + * resulting layout of the wordcloud. + * It is possible for users to add their own custom placement strategies + * for use in word cloud. Read more about it in our + * [documentation](https://www.highcharts.com/docs/chart-and-series-types/word-cloud-series#custom-placement-strategies) + * + * @validvalue: ["center", "random"] + */ + placementStrategy: 'center', + /** + * Rotation options for the words in the wordcloud. + * @sample highcharts/plotoptions/wordcloud-rotation + * Word cloud with rotation + */ + rotation: { + /** + * The smallest degree of rotation for a word. + */ + from: 0, + /** + * The number of possible orientations for a word, within the range of + * `rotation.from` and `rotation.to`. + */ + orientations: 2, + /** + * The largest degree of rotation for a word. + */ + to: 90 + }, + showInLegend: false, + /** + * Spiral used for placing a word after the inital position experienced a + * collision with either another word or the borders. + * It is possible for users to add their own custom spiralling algorithms + * for use in word cloud. Read more about it in our + * [documentation](https://www.highcharts.com/docs/chart-and-series-types/word-cloud-series#custom-spiralling-algorithm) + * + * @validvalue: ["archimedean", "rectangular", "square"] + */ + spiral: 'rectangular', + /** + * CSS styles for the words. + * + * @type {CSSObject} + * @default {"fontFamily":"sans-serif", "fontWeight": "900"} + */ + style: { + fontFamily: 'sans-serif', + fontWeight: '900' + }, + tooltip: { + followPointer: true, + pointFormat: '\u25CF {series.name}: {point.weight}
' + } }; /** * Properties of the WordCloud series. */ var wordCloudSeries = { - animate: Series.prototype.animate, - bindAxes: function () { - var wordcloudAxis = { - endOnTick: false, - gridLineWidth: 0, - lineWidth: 0, - maxPadding: 0, - startOnTick: false, - title: null, - tickPositions: [] - }; - Series.prototype.bindAxes.call(this); - extend(this.yAxis.options, wordcloudAxis); - extend(this.xAxis.options, wordcloudAxis); - }, - /** - * deriveFontSize - Calculates the fontSize of a word based on its weight. - * - * @param {number} relativeWeight The weight of the word, on a scale 0-1. - * @return {number} Returns the resulting fontSize of a word. - */ - deriveFontSize: function deriveFontSize(relativeWeight) { - var maxFontSize = 25; - return Math.floor(maxFontSize * relativeWeight); - }, - drawPoints: function () { - var series = this, - hasRendered = series.hasRendered, - xAxis = series.xAxis, - yAxis = series.yAxis, - chart = series.chart, - group = series.group, - options = series.options, - animation = options.animation, - renderer = chart.renderer, - testElement = renderer.text().add(group), - placed = [], - placementStrategy = series.placementStrategy[ - options.placementStrategy - ], - spiral = series.spirals[options.spiral], - rotation = options.rotation, - scale, - weights = series.points - .map(function (p) { - return p.weight; - }), - maxWeight = Math.max.apply(null, weights), - data = series.points - .sort(function (a, b) { - return b.weight - a.weight; // Sort descending - }), - field; + animate: Series.prototype.animate, + bindAxes: function () { + var wordcloudAxis = { + endOnTick: false, + gridLineWidth: 0, + lineWidth: 0, + maxPadding: 0, + startOnTick: false, + title: null, + tickPositions: [] + }; + Series.prototype.bindAxes.call(this); + extend(this.yAxis.options, wordcloudAxis); + extend(this.xAxis.options, wordcloudAxis); + }, + /** + * deriveFontSize - Calculates the fontSize of a word based on its weight. + * + * @param {number} relativeWeight The weight of the word, on a scale 0-1. + * @return {number} Returns the resulting fontSize of a word. + */ + deriveFontSize: function deriveFontSize(relativeWeight) { + var maxFontSize = 25; + return Math.floor(maxFontSize * relativeWeight); + }, + drawPoints: function () { + var series = this, + hasRendered = series.hasRendered, + xAxis = series.xAxis, + yAxis = series.yAxis, + chart = series.chart, + group = series.group, + options = series.options, + animation = options.animation, + renderer = chart.renderer, + testElement = renderer.text().add(group), + placed = [], + placementStrategy = series.placementStrategy[ + options.placementStrategy + ], + spiral = series.spirals[options.spiral], + rotation = options.rotation, + scale, + weights = series.points + .map(function (p) { + return p.weight; + }), + maxWeight = Math.max.apply(null, weights), + data = series.points + .sort(function (a, b) { + return b.weight - a.weight; // Sort descending + }), + field; - // Get the dimensions for each word. - // Used in calculating the playing field. - each(data, function (point) { - var relativeWeight = 1 / maxWeight * point.weight, - css = extend({ - fontSize: series.deriveFontSize(relativeWeight) + 'px' - }, options.style), - bBox; + // Get the dimensions for each word. + // Used in calculating the playing field. + each(data, function (point) { + var relativeWeight = 1 / maxWeight * point.weight, + css = extend({ + fontSize: series.deriveFontSize(relativeWeight) + 'px' + }, options.style), + bBox; - testElement.css(css).attr({ - x: 0, - y: 0, - text: point.name - }); + testElement.css(css).attr({ + x: 0, + y: 0, + text: point.name + }); - // TODO Replace all use of clientRect with bBox. - bBox = testElement.getBBox(true); - point.dimensions = { - height: bBox.height, - width: bBox.width - }; - }); + // TODO Replace all use of clientRect with bBox. + bBox = testElement.getBBox(true); + point.dimensions = { + height: bBox.height, + width: bBox.width + }; + }); - // Calculate the playing field. - field = getPlayingField(xAxis.len, yAxis.len, data); + // Calculate the playing field. + field = getPlayingField(xAxis.len, yAxis.len, data); - // Draw all the points. - each(data, function (point) { - var relativeWeight = 1 / maxWeight * point.weight, - css = extend({ - fontSize: series.deriveFontSize(relativeWeight) + 'px', - fill: point.color - }, options.style), - placement = placementStrategy(point, { - data: data, - field: field, - placed: placed, - rotation: rotation - }), - attr = { - align: 'center', - x: placement.x, - y: placement.y, - text: point.name, - rotation: placement.rotation - }, - animate, - delta, - clientRect; - testElement.css(css).attr(attr); - // Cache the original DOMRect values for later calculations. - point.clientRect = clientRect = extend( - {}, - testElement.element.getBoundingClientRect() - ); - delta = intersectionTesting(point, { - clientRect: clientRect, - element: testElement, - field: field, - placed: placed, - spiral: spiral - }); - /** - * Check if point was placed, if so delete it, - * otherwise place it on the correct positions. - */ - if (isObject(delta)) { - attr.x += delta.x; - attr.y += delta.y; - extend(placement, { - left: attr.x - (clientRect.width / 2), - right: attr.x + (clientRect.width / 2), - top: attr.y - (clientRect.height / 2), - bottom: attr.y + (clientRect.height / 2) - }); - field = updateFieldBoundaries(field, placement); - placed.push(point); - point.isNull = false; - } else { - point.isNull = true; - } + // Draw all the points. + each(data, function (point) { + var relativeWeight = 1 / maxWeight * point.weight, + css = extend({ + fontSize: series.deriveFontSize(relativeWeight) + 'px', + fill: point.color + }, options.style), + placement = placementStrategy(point, { + data: data, + field: field, + placed: placed, + rotation: rotation + }), + attr = { + align: 'center', + x: placement.x, + y: placement.y, + text: point.name, + rotation: placement.rotation + }, + animate, + delta, + clientRect; + testElement.css(css).attr(attr); + // Cache the original DOMRect values for later calculations. + point.clientRect = clientRect = extend( + {}, + testElement.element.getBoundingClientRect() + ); + delta = intersectionTesting(point, { + clientRect: clientRect, + element: testElement, + field: field, + placed: placed, + spiral: spiral + }); + /** + * Check if point was placed, if so delete it, + * otherwise place it on the correct positions. + */ + if (isObject(delta)) { + attr.x += delta.x; + attr.y += delta.y; + extend(placement, { + left: attr.x - (clientRect.width / 2), + right: attr.x + (clientRect.width / 2), + top: attr.y - (clientRect.height / 2), + bottom: attr.y + (clientRect.height / 2) + }); + field = updateFieldBoundaries(field, placement); + placed.push(point); + point.isNull = false; + } else { + point.isNull = true; + } - if (animation) { - // Animate to new positions - animate = { - x: attr.x, - y: attr.y - }; - // Animate from center of chart - if (!hasRendered) { - attr.x = 0; - attr.y = 0; - // or animate from previous position - } else { - delete attr.x; - delete attr.y; - } - } + if (animation) { + // Animate to new positions + animate = { + x: attr.x, + y: attr.y + }; + // Animate from center of chart + if (!hasRendered) { + attr.x = 0; + attr.y = 0; + // or animate from previous position + } else { + delete attr.x; + delete attr.y; + } + } - point.draw({ - animate: animate, - attr: attr, - css: css, - group: group, - renderer: renderer, - shapeArgs: undefined, - shapeType: 'text' - }); - }); + point.draw({ + animate: animate, + attr: attr, + css: css, + group: group, + renderer: renderer, + shapeArgs: undefined, + shapeType: 'text' + }); + }); - // Destroy the element after use. - testElement = testElement.destroy(); + // Destroy the element after use. + testElement = testElement.destroy(); - /** - * Scale the series group to fit within the plotArea. - */ - scale = getScale(xAxis.len, yAxis.len, field); - series.group.attr({ - scaleX: scale, - scaleY: scale - }); - }, - hasData: function () { - var series = this; - return ( - isObject(series) && - series.visible === true && - isArray(series.points) && - series.points.length > 0 - ); - }, - /** - * Strategies used for deciding rotation and initial position of a word. - * To implement a custom strategy, have a look at the function - * randomPlacement for example. - */ - placementStrategy: { - random: function randomPlacement(point, options) { - var field = options.field, - r = options.rotation; - return { - x: getRandomPosition(field.width) - (field.width / 2), - y: getRandomPosition(field.height) - (field.height / 2), - rotation: getRotation(r.orientations, point.index, r.from, r.to) - }; - }, - center: function centerPlacement(point, options) { - var r = options.rotation; - return { - x: 0, - y: 0, - rotation: getRotation(r.orientations, point.index, r.from, r.to) - }; - } - }, - pointArrayMap: ['weight'], - /** - * Spirals used for placing a word after the inital position experienced a - * collision with either another word or the borders. - * To implement a custom spiral, look at the function archimedeanSpiral for - * example. - */ - spirals: { - 'archimedean': archimedeanSpiral, - 'rectangular': rectangularSpiral, - 'square': squareSpiral - }, - utils: { - getRotation: getRotation - }, - getPlotBox: function () { - var series = this, - chart = series.chart, - inverted = chart.inverted, - // Swap axes for inverted (#2339) - xAxis = series[(inverted ? 'yAxis' : 'xAxis')], - yAxis = series[(inverted ? 'xAxis' : 'yAxis')], - width = xAxis ? xAxis.len : chart.plotWidth, - height = yAxis ? yAxis.len : chart.plotHeight, - x = xAxis ? xAxis.left : chart.plotLeft, - y = yAxis ? yAxis.top : chart.plotTop; - return { - translateX: x + (width / 2), - translateY: y + (height / 2), - scaleX: 1, // #1623 - scaleY: 1 - }; - } + /** + * Scale the series group to fit within the plotArea. + */ + scale = getScale(xAxis.len, yAxis.len, field); + series.group.attr({ + scaleX: scale, + scaleY: scale + }); + }, + hasData: function () { + var series = this; + return ( + isObject(series) && + series.visible === true && + isArray(series.points) && + series.points.length > 0 + ); + }, + /** + * Strategies used for deciding rotation and initial position of a word. + * To implement a custom strategy, have a look at the function + * randomPlacement for example. + */ + placementStrategy: { + random: function randomPlacement(point, options) { + var field = options.field, + r = options.rotation; + return { + x: getRandomPosition(field.width) - (field.width / 2), + y: getRandomPosition(field.height) - (field.height / 2), + rotation: getRotation(r.orientations, point.index, r.from, r.to) + }; + }, + center: function centerPlacement(point, options) { + var r = options.rotation; + return { + x: 0, + y: 0, + rotation: getRotation(r.orientations, point.index, r.from, r.to) + }; + } + }, + pointArrayMap: ['weight'], + /** + * Spirals used for placing a word after the inital position experienced a + * collision with either another word or the borders. + * To implement a custom spiral, look at the function archimedeanSpiral for + * example. + */ + spirals: { + 'archimedean': archimedeanSpiral, + 'rectangular': rectangularSpiral, + 'square': squareSpiral + }, + utils: { + getRotation: getRotation + }, + getPlotBox: function () { + var series = this, + chart = series.chart, + inverted = chart.inverted, + // Swap axes for inverted (#2339) + xAxis = series[(inverted ? 'yAxis' : 'xAxis')], + yAxis = series[(inverted ? 'xAxis' : 'yAxis')], + width = xAxis ? xAxis.len : chart.plotWidth, + height = yAxis ? yAxis.len : chart.plotHeight, + x = xAxis ? xAxis.left : chart.plotLeft, + y = yAxis ? yAxis.top : chart.plotTop; + return { + translateX: x + (width / 2), + translateY: y + (height / 2), + scaleX: 1, // #1623 + scaleY: 1 + }; + } }; /** * Properties of the Sunburst series. */ var wordCloudPoint = { - draw: drawPoint, - shouldDraw: function shouldDraw() { - var point = this; - return !point.isNull; - } + draw: drawPoint, + shouldDraw: function shouldDraw() { + var point = this; + return !point.isNull; + } }; /** @@ -736,23 +736,23 @@ var wordCloudPoint = { /** * An array of data points for the series. For the `wordcloud` series * type, points can be given in the following ways: - * + * * 1. An array of arrays with 2 values. In this case, the values - * correspond to `name,weight`. - * + * correspond to `name,weight`. + * * ```js * data: [ * ['Lorem', 4], * ['Ipsum', 1] * ] * ``` - * + * * 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.arearange.turboThreshold), this option is not * available. - * + * * ```js * data: [{ * name: "Lorem", @@ -762,7 +762,7 @@ var wordCloudPoint = { * weight: 1 * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding drilldown,marker,x,y @@ -791,9 +791,9 @@ var wordCloudPoint = { * @apioption series.sunburst.data.weight */ H.seriesType( - 'wordcloud', - 'column', - wordCloudOptions, - wordCloudSeries, - wordCloudPoint + 'wordcloud', + 'column', + wordCloudOptions, + wordCloudSeries, + wordCloudPoint ); diff --git a/js/modules/xrange.src.js b/js/modules/xrange.src.js index 200d6d70da9..36bf4a66caf 100644 --- a/js/modules/xrange.src.js +++ b/js/modules/xrange.src.js @@ -10,24 +10,24 @@ import H from '../parts/Globals.js'; var addEvent = H.addEvent, - defined = H.defined, - color = H.Color, - columnType = H.seriesTypes.column, - each = H.each, - isNumber = H.isNumber, - isObject = H.isObject, - merge = H.merge, - pick = H.pick, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes, - Axis = H.Axis, - Point = H.Point, - Series = H.Series; + defined = H.defined, + color = H.Color, + columnType = H.seriesTypes.column, + each = H.each, + isNumber = H.isNumber, + isObject = H.isObject, + merge = H.merge, + pick = H.pick, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes, + Axis = H.Axis, + Point = H.Point, + Series = H.Series; /** * The X-range series displays ranges on the X axis, typically time intervals * with a start and end date. - * + * * @extends {plotOptions.column} * @excluding boostThreshold,crisp,cropThreshold,depth,edgeColor,edgeWidth, * findNearestPointBy,getExtremesFromAll,grouping,groupPadding, @@ -44,422 +44,422 @@ var addEvent = H.addEvent, * @optionparent plotOptions.xrange */ seriesType('xrange', 'column', { - /** - * A partial fill for each point, typically used to visualize how much of - * a task is performed. The partial fill object can be set either on series - * or point level. - * - * @sample {highcharts} highcharts/demo/x-range - * X-range with partial fill - * @type {Object} - * @product highcharts highstock - * @apioption plotOptions.xrange.partialFill - */ - - /** - * The fill color to be used for partial fills. Defaults to a darker shade - * of the point color. - * - * @type {Color} - * @product highcharts highstock - * @apioption plotOptions.xrange.partialFill.fill - */ - - /** - * In an X-range series, this option makes all points of the same Y-axis - * category the same color. - */ - colorByPoint: true, - dataLabels: { - verticalAlign: 'middle', - inside: true, - /** - * The default formatter for X-range data labels displays the percentage - * of the partial fill amount. - */ - formatter: function () { - var point = this.point, - amount = point.partialFill; - if (isObject(amount)) { - amount = amount.amount; - } - if (!defined(amount)) { - amount = 0; - } - return (amount * 100) + '%'; - } - }, - tooltip: { - headerFormat: '{point.x} - {point.x2}
', - pointFormat: '\u25CF {series.name}: {point.yCategory}
' - }, - borderRadius: 3, - pointRange: 0 + /** + * A partial fill for each point, typically used to visualize how much of + * a task is performed. The partial fill object can be set either on series + * or point level. + * + * @sample {highcharts} highcharts/demo/x-range + * X-range with partial fill + * @type {Object} + * @product highcharts highstock + * @apioption plotOptions.xrange.partialFill + */ + + /** + * The fill color to be used for partial fills. Defaults to a darker shade + * of the point color. + * + * @type {Color} + * @product highcharts highstock + * @apioption plotOptions.xrange.partialFill.fill + */ + + /** + * In an X-range series, this option makes all points of the same Y-axis + * category the same color. + */ + colorByPoint: true, + dataLabels: { + verticalAlign: 'middle', + inside: true, + /** + * The default formatter for X-range data labels displays the percentage + * of the partial fill amount. + */ + formatter: function () { + var point = this.point, + amount = point.partialFill; + if (isObject(amount)) { + amount = amount.amount; + } + if (!defined(amount)) { + amount = 0; + } + return (amount * 100) + '%'; + } + }, + tooltip: { + headerFormat: '{point.x} - {point.x2}
', + pointFormat: '\u25CF {series.name}: {point.yCategory}
' + }, + borderRadius: 3, + pointRange: 0 }, { - type: 'xrange', - parallelArrays: ['x', 'x2', 'y'], - requireSorting: false, - animate: seriesTypes.line.prototype.animate, - cropShoulder: 1, - getExtremesFromAll: true, - - /** - * Borrow the column series metrics, but with swapped axes. This gives free - * access to features like groupPadding, grouping, pointWidth etc. - */ - getColumnMetrics: function () { - var metrics, - chart = this.chart; - - function swapAxes() { - each(chart.series, function (s) { - var xAxis = s.xAxis; - s.xAxis = s.yAxis; - s.yAxis = xAxis; - }); - } - - swapAxes(); - - metrics = columnType.prototype.getColumnMetrics.call(this); - - swapAxes(); - - return metrics; - }, - - /** - * Override cropData to show a point where x or x2 is outside visible range, - * but one of them is inside. - */ - cropData: function (xData, yData, min, max) { - - // Replace xData with x2Data to find the appropriate cropStart - var cropData = Series.prototype.cropData, - crop = cropData.call(this, this.x2Data, yData, min, max); - - // Re-insert the cropped xData - crop.xData = xData.slice(crop.start, crop.end); - - return crop; - }, - - translatePoint: function (point) { - var series = this, - xAxis = series.xAxis, - metrics = series.columnMetrics, - minPointLength = series.options.minPointLength || 0, - plotX = point.plotX, - posX = pick(point.x2, point.x + (point.len || 0)), - plotX2 = xAxis.translate(posX, 0, 0, 0, 1), - length = plotX2 - plotX, - widthDifference, - shapeArgs, - partialFill, - inverted = this.chart.inverted, - borderWidth = pick(series.options.borderWidth, 1), - crisper = borderWidth % 2 / 2, - dlLeft, - dlRight, - dlWidth; - - if (minPointLength) { - widthDifference = minPointLength - length; - if (widthDifference < 0) { - widthDifference = 0; - } - plotX -= widthDifference / 2; - plotX2 += widthDifference / 2; - } - - plotX = Math.max(plotX, -10); - plotX2 = Math.min(Math.max(plotX2, -10), xAxis.len + 10); - - point.shapeArgs = { - x: Math.floor(Math.min(plotX, plotX2)) + crisper, - y: Math.floor(point.plotY + metrics.offset) + crisper, - width: Math.round(Math.abs(plotX2 - plotX)), - height: Math.round(metrics.width), - r: series.options.borderRadius - }; - - // Align data labels inside the shape and inside the plot area - dlLeft = point.shapeArgs.x; - dlRight = dlLeft + point.shapeArgs.width; - if (dlLeft < 0 || dlRight > xAxis.len) { - dlLeft = Math.min(xAxis.len, Math.max(0, dlLeft)); - dlRight = Math.max(0, Math.min(dlRight, xAxis.len)); - dlWidth = dlRight - dlLeft; - point.dlBox = merge(point.shapeArgs, { - x: dlLeft, - width: dlRight - dlLeft, - centerX: dlWidth ? dlWidth / 2 : null - }); - - } else { - point.dlBox = null; - } - - // Tooltip position - point.tooltipPos[0] += inverted ? 0 : length / 2; - point.tooltipPos[1] -= inverted ? length / 2 : metrics.width / 2; - - // Add a partShapeArgs to the point, based on the shapeArgs property - partialFill = point.partialFill; - if (partialFill) { - // Get the partial fill amount - if (isObject(partialFill)) { - partialFill = partialFill.amount; - } - // If it was not a number, assume 0 - if (!isNumber(partialFill)) { - partialFill = 0; - } - shapeArgs = point.shapeArgs; - point.partShapeArgs = { - x: shapeArgs.x, - y: shapeArgs.y, - width: shapeArgs.width, - height: shapeArgs.height, - r: series.options.borderRadius - }; - point.clipRectArgs = { - x: shapeArgs.x, - y: shapeArgs.y, - width: Math.max( - Math.round( - length * partialFill + - (point.plotX - plotX) - ), - 0 - ), - height: shapeArgs.height - }; - } - }, - - translate: function () { - columnType.prototype.translate.apply(this, arguments); - each(this.points, function (point) { - this.translatePoint(point); - }, this); - }, - - /** - * Draws a single point in the series. Needed for partial fill. - * - * This override turns point.graphic into a group containing the original - * graphic and an overlay displaying the partial fill. - * - * @param {Object} point an instance of Point in the series - * @param {string} verb 'animate' (animates changes) or 'attr' (sets - * options) - * @returns {void} - */ - drawPoint: function (point, verb) { - var series = this, - seriesOpts = series.options, - renderer = series.chart.renderer, - graphic = point.graphic, - type = point.shapeType, - shapeArgs = point.shapeArgs, - partShapeArgs = point.partShapeArgs, - clipRectArgs = point.clipRectArgs, - pfOptions = point.partialFill, - fill, - state = point.selected && 'select', - cutOff = seriesOpts.stacking && !seriesOpts.borderRadius; - - if (!point.isNull) { - - // Original graphic - if (graphic) { // update - point.graphicOriginal[verb]( - merge(shapeArgs) - ); - - } else { - point.graphic = graphic = renderer.g('point') - .addClass(point.getClassName()) - .add(point.group || series.group); - - point.graphicOriginal = renderer[type](shapeArgs) - .addClass(point.getClassName()) - .addClass('highcharts-partfill-original') - .add(graphic); - } - - // Partial fill graphic - if (partShapeArgs) { - if (point.graphicOverlay) { - point.graphicOverlay[verb]( - merge(partShapeArgs) - ); - point.clipRect.animate( - merge(clipRectArgs) - ); - - } else { - - point.clipRect = renderer.clipRect( - clipRectArgs.x, - clipRectArgs.y, - clipRectArgs.width, - clipRectArgs.height - ); - - point.graphicOverlay = renderer[type](partShapeArgs) - .addClass('highcharts-partfill-overlay') - .add(graphic) - .clip(point.clipRect); - } - } - - - /*= if (build.classic) { =*/ - // Presentational - point.graphicOriginal - .attr(series.pointAttribs(point, state)) - .shadow(seriesOpts.shadow, null, cutOff); - if (partShapeArgs) { - // Ensure pfOptions is an object - if (!isObject(pfOptions)) { - pfOptions = {}; - } - if (isObject(seriesOpts.partialFill)) { - pfOptions = merge(pfOptions, seriesOpts.partialFill); - } - - fill = ( - pfOptions.fill || - color(point.color || series.color).brighten(-0.3).get() - ); - - point.graphicOverlay - .attr(series.pointAttribs(point, state)) - .attr({ - 'fill': fill - }) - .shadow(seriesOpts.shadow, null, cutOff); - } - /*= } =*/ - - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 - } - }, - - drawPoints: function () { - var series = this, - chart = this.chart, - options = series.options, - animationLimit = options.animationLimit || 250, - verb = chart.pointCount < animationLimit ? 'animate' : 'attr'; - - // draw the columns - each(series.points, function (point) { - series.drawPoint(point, verb); - }); - } - - /** - * Override to remove stroke from points. - * For partial fill. - * / - pointAttribs: function () { - var series = this, - retVal = columnType.prototype.pointAttribs.apply(series, arguments); - - //retVal['stroke-width'] = 0; - return retVal; - } - //*/ + type: 'xrange', + parallelArrays: ['x', 'x2', 'y'], + requireSorting: false, + animate: seriesTypes.line.prototype.animate, + cropShoulder: 1, + getExtremesFromAll: true, + + /** + * Borrow the column series metrics, but with swapped axes. This gives free + * access to features like groupPadding, grouping, pointWidth etc. + */ + getColumnMetrics: function () { + var metrics, + chart = this.chart; + + function swapAxes() { + each(chart.series, function (s) { + var xAxis = s.xAxis; + s.xAxis = s.yAxis; + s.yAxis = xAxis; + }); + } + + swapAxes(); + + metrics = columnType.prototype.getColumnMetrics.call(this); + + swapAxes(); + + return metrics; + }, + + /** + * Override cropData to show a point where x or x2 is outside visible range, + * but one of them is inside. + */ + cropData: function (xData, yData, min, max) { + + // Replace xData with x2Data to find the appropriate cropStart + var cropData = Series.prototype.cropData, + crop = cropData.call(this, this.x2Data, yData, min, max); + + // Re-insert the cropped xData + crop.xData = xData.slice(crop.start, crop.end); + + return crop; + }, + + translatePoint: function (point) { + var series = this, + xAxis = series.xAxis, + metrics = series.columnMetrics, + minPointLength = series.options.minPointLength || 0, + plotX = point.plotX, + posX = pick(point.x2, point.x + (point.len || 0)), + plotX2 = xAxis.translate(posX, 0, 0, 0, 1), + length = plotX2 - plotX, + widthDifference, + shapeArgs, + partialFill, + inverted = this.chart.inverted, + borderWidth = pick(series.options.borderWidth, 1), + crisper = borderWidth % 2 / 2, + dlLeft, + dlRight, + dlWidth; + + if (minPointLength) { + widthDifference = minPointLength - length; + if (widthDifference < 0) { + widthDifference = 0; + } + plotX -= widthDifference / 2; + plotX2 += widthDifference / 2; + } + + plotX = Math.max(plotX, -10); + plotX2 = Math.min(Math.max(plotX2, -10), xAxis.len + 10); + + point.shapeArgs = { + x: Math.floor(Math.min(plotX, plotX2)) + crisper, + y: Math.floor(point.plotY + metrics.offset) + crisper, + width: Math.round(Math.abs(plotX2 - plotX)), + height: Math.round(metrics.width), + r: series.options.borderRadius + }; + + // Align data labels inside the shape and inside the plot area + dlLeft = point.shapeArgs.x; + dlRight = dlLeft + point.shapeArgs.width; + if (dlLeft < 0 || dlRight > xAxis.len) { + dlLeft = Math.min(xAxis.len, Math.max(0, dlLeft)); + dlRight = Math.max(0, Math.min(dlRight, xAxis.len)); + dlWidth = dlRight - dlLeft; + point.dlBox = merge(point.shapeArgs, { + x: dlLeft, + width: dlRight - dlLeft, + centerX: dlWidth ? dlWidth / 2 : null + }); + + } else { + point.dlBox = null; + } + + // Tooltip position + point.tooltipPos[0] += inverted ? 0 : length / 2; + point.tooltipPos[1] -= inverted ? length / 2 : metrics.width / 2; + + // Add a partShapeArgs to the point, based on the shapeArgs property + partialFill = point.partialFill; + if (partialFill) { + // Get the partial fill amount + if (isObject(partialFill)) { + partialFill = partialFill.amount; + } + // If it was not a number, assume 0 + if (!isNumber(partialFill)) { + partialFill = 0; + } + shapeArgs = point.shapeArgs; + point.partShapeArgs = { + x: shapeArgs.x, + y: shapeArgs.y, + width: shapeArgs.width, + height: shapeArgs.height, + r: series.options.borderRadius + }; + point.clipRectArgs = { + x: shapeArgs.x, + y: shapeArgs.y, + width: Math.max( + Math.round( + length * partialFill + + (point.plotX - plotX) + ), + 0 + ), + height: shapeArgs.height + }; + } + }, + + translate: function () { + columnType.prototype.translate.apply(this, arguments); + each(this.points, function (point) { + this.translatePoint(point); + }, this); + }, + + /** + * Draws a single point in the series. Needed for partial fill. + * + * This override turns point.graphic into a group containing the original + * graphic and an overlay displaying the partial fill. + * + * @param {Object} point an instance of Point in the series + * @param {string} verb 'animate' (animates changes) or 'attr' (sets + * options) + * @returns {void} + */ + drawPoint: function (point, verb) { + var series = this, + seriesOpts = series.options, + renderer = series.chart.renderer, + graphic = point.graphic, + type = point.shapeType, + shapeArgs = point.shapeArgs, + partShapeArgs = point.partShapeArgs, + clipRectArgs = point.clipRectArgs, + pfOptions = point.partialFill, + fill, + state = point.selected && 'select', + cutOff = seriesOpts.stacking && !seriesOpts.borderRadius; + + if (!point.isNull) { + + // Original graphic + if (graphic) { // update + point.graphicOriginal[verb]( + merge(shapeArgs) + ); + + } else { + point.graphic = graphic = renderer.g('point') + .addClass(point.getClassName()) + .add(point.group || series.group); + + point.graphicOriginal = renderer[type](shapeArgs) + .addClass(point.getClassName()) + .addClass('highcharts-partfill-original') + .add(graphic); + } + + // Partial fill graphic + if (partShapeArgs) { + if (point.graphicOverlay) { + point.graphicOverlay[verb]( + merge(partShapeArgs) + ); + point.clipRect.animate( + merge(clipRectArgs) + ); + + } else { + + point.clipRect = renderer.clipRect( + clipRectArgs.x, + clipRectArgs.y, + clipRectArgs.width, + clipRectArgs.height + ); + + point.graphicOverlay = renderer[type](partShapeArgs) + .addClass('highcharts-partfill-overlay') + .add(graphic) + .clip(point.clipRect); + } + } + + + /*= if (build.classic) { =*/ + // Presentational + point.graphicOriginal + .attr(series.pointAttribs(point, state)) + .shadow(seriesOpts.shadow, null, cutOff); + if (partShapeArgs) { + // Ensure pfOptions is an object + if (!isObject(pfOptions)) { + pfOptions = {}; + } + if (isObject(seriesOpts.partialFill)) { + pfOptions = merge(pfOptions, seriesOpts.partialFill); + } + + fill = ( + pfOptions.fill || + color(point.color || series.color).brighten(-0.3).get() + ); + + point.graphicOverlay + .attr(series.pointAttribs(point, state)) + .attr({ + 'fill': fill + }) + .shadow(seriesOpts.shadow, null, cutOff); + } + /*= } =*/ + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + }, + + drawPoints: function () { + var series = this, + chart = this.chart, + options = series.options, + animationLimit = options.animationLimit || 250, + verb = chart.pointCount < animationLimit ? 'animate' : 'attr'; + + // draw the columns + each(series.points, function (point) { + series.drawPoint(point, verb); + }); + } + + /** + * Override to remove stroke from points. + * For partial fill. + * / + pointAttribs: function () { + var series = this, + retVal = columnType.prototype.pointAttribs.apply(series, arguments); + + //retVal['stroke-width'] = 0; + return retVal; + } + //*/ // Point class properties }, { - /** - * Extend init so that `colorByPoint` for x-range means that one color is - * applied per Y axis category. - */ - init: function () { - - Point.prototype.init.apply(this, arguments); - - var colors, - series = this.series, - colorCount = series.chart.options.chart.colorCount; - - if (!this.y) { - this.y = 0; - } - - /*= if (build.classic) { =*/ - if (series.options.colorByPoint) { - colors = series.options.colors || series.chart.options.colors; - colorCount = colors.length; - - if (!this.options.color && colors[this.y % colorCount]) { - this.color = colors[this.y % colorCount]; - } - } - /*= } =*/ - this.colorIndex = pick(this.options.colorIndex, this.y % colorCount); - - return this; - }, - - // Add x2 and yCategory to the available properties for tooltip formats - getLabelConfig: function () { - var point = this, - cfg = Point.prototype.getLabelConfig.call(point), - yCats = point.series.yAxis.categories; - - cfg.x2 = point.x2; - cfg.yCategory = point.yCategory = yCats && yCats[point.y]; - return cfg; - }, - tooltipDateKeys: ['x', 'x2'], - - isValid: function () { - return typeof this.x === 'number' && - typeof this.x2 === 'number'; - } + /** + * Extend init so that `colorByPoint` for x-range means that one color is + * applied per Y axis category. + */ + init: function () { + + Point.prototype.init.apply(this, arguments); + + var colors, + series = this.series, + colorCount = series.chart.options.chart.colorCount; + + if (!this.y) { + this.y = 0; + } + + /*= if (build.classic) { =*/ + if (series.options.colorByPoint) { + colors = series.options.colors || series.chart.options.colors; + colorCount = colors.length; + + if (!this.options.color && colors[this.y % colorCount]) { + this.color = colors[this.y % colorCount]; + } + } + /*= } =*/ + this.colorIndex = pick(this.options.colorIndex, this.y % colorCount); + + return this; + }, + + // Add x2 and yCategory to the available properties for tooltip formats + getLabelConfig: function () { + var point = this, + cfg = Point.prototype.getLabelConfig.call(point), + yCats = point.series.yAxis.categories; + + cfg.x2 = point.x2; + cfg.yCategory = point.yCategory = yCats && yCats[point.y]; + return cfg; + }, + tooltipDateKeys: ['x', 'x2'], + + isValid: function () { + return typeof this.x === 'number' && + typeof this.x2 === 'number'; + } }); /** * Max x2 should be considered in xAxis extremes */ addEvent(Axis, 'afterGetSeriesExtremes', function () { - var axis = this, - axisSeries = axis.series, - dataMax, - modMax; - - if (axis.isXAxis) { - dataMax = pick(axis.dataMax, -Number.MAX_VALUE); - each(axisSeries, function (series) { - if (series.x2Data) { - each(series.x2Data, function (val) { - if (val > dataMax) { - dataMax = val; - modMax = true; - } - }); - } - }); - if (modMax) { - axis.dataMax = dataMax; - } - } + var axis = this, + axisSeries = axis.series, + dataMax, + modMax; + + if (axis.isXAxis) { + dataMax = pick(axis.dataMax, -Number.MAX_VALUE); + each(axisSeries, function (series) { + if (series.x2Data) { + each(series.x2Data, function (val) { + if (val > dataMax) { + dataMax = val; + modMax = true; + } + }); + } + }); + if (modMax) { + axis.dataMax = dataMax; + } + } }); /** * An `xrange` series. If the [type](#series.xrange.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.xrange * @excluding boostThreshold,crisp,cropThreshold,depth,edgeColor,edgeWidth, @@ -473,10 +473,10 @@ addEvent(Axis, 'afterGetSeriesExtremes', function () { /** * An array of data points for the series. For the `xrange` 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. - * + * * ```js * data: [{ * x: Date.UTC(2017, 0, 1), @@ -492,7 +492,7 @@ addEvent(Axis, 'afterGetSeriesExtremes', function () { * color: "#FF0000" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -522,7 +522,7 @@ addEvent(Axis, 'afterGetSeriesExtremes', function () { /** * A partial fill for each point, typically used to visualize how much of * a task is performed. The partial fill object can be set either on series - * or point level. + * or point level. * * @sample {highcharts} highcharts/demo/x-range * X-range with partial fill @@ -532,7 +532,7 @@ addEvent(Axis, 'afterGetSeriesExtremes', function () { */ /** - * The amount of the X-range point to be filled. Values can be 0-1 and are + * The amount of the X-range point to be filled. Values can be 0-1 and are * converted to percentages in the default data label formatter. * * @type {Number} diff --git a/js/parts-3d/Axis.js b/js/parts-3d/Axis.js index 438e5843a57..bd3b02979fc 100644 --- a/js/parts-3d/Axis.js +++ b/js/parts-3d/Axis.js @@ -13,459 +13,460 @@ import '../parts/Chart.js'; import '../parts/Tick.js'; var ZAxis, - addEvent = H.addEvent, - Axis = H.Axis, - Chart = H.Chart, - deg2rad = H.deg2rad, - each = H.each, - extend = H.extend, - merge = H.merge, - perspective = H.perspective, - pick = H.pick, - shapeArea = H.shapeArea, - splat = H.splat, - Tick = H.Tick, - wrap = H.wrap; + addEvent = H.addEvent, + Axis = H.Axis, + Chart = H.Chart, + deg2rad = H.deg2rad, + each = H.each, + extend = H.extend, + merge = H.merge, + perspective = H.perspective, + pick = H.pick, + shapeArea = H.shapeArea, + splat = H.splat, + Tick = H.Tick, + wrap = H.wrap; /** * @optionparent xAxis */ var extendedOptions = { - labels: { - /** - * Defines how the labels are be repositioned according to the 3D chart - * orientation. - * - `'offset'`: Maintain a fixed horizontal/vertical distance from the - * tick marks, despite the chart orientation. This is the backwards - * compatible behavior, and causes skewing of X and Z axes. - * - `'chart'`: Preserve 3D position relative to the chart. - * This looks nice, but hard to read if the text isn't - * forward-facing. - * - `'flap'`: Rotated text along the axis to compensate for the chart - * orientation. This tries to maintain text as legible as possible on - * all orientations. - * - `'ortho'`: Rotated text along the axis direction so that the labels - * are orthogonal to the axis. This is very similar to `'flap'`, but - * prevents skewing the labels (X and Y scaling are still present). - * - * @validvalue ['offset', 'chart', 'flap', 'ortho'] - * @sample highcharts/3d/skewed-labels/ Skewed labels - * @since 5.0.15 - * @product highcharts - */ - position3d: 'offset', - - /** - * If enabled, the axis labels will skewed to follow the perspective. - * - * This will fix overlapping labels and titles, but texts become less - * legible due to the distortion. - * - * The final appearance depends heavily on `labels.position3d`. - * - * @since 5.0.15 - * @sample highcharts/3d/skewed-labels/ Skewed labels - * @product highcharts - */ - skew3d: false - }, - title: { - /** - * Defines how the title is repositioned according to the 3D chart - * orientation. - * - `'offset'`: Maintain a fixed horizontal/vertical distance from the - * tick marks, despite the chart orientation. This is the backwards - * compatible behavior, and causes skewing of X and Z axes. - * - `'chart'`: Preserve 3D position relative to the chart. - * This looks nice, but hard to read if the text isn't - * forward-facing. - * - `'flap'`: Rotated text along the axis to compensate for the chart - * orientation. This tries to maintain text as legible as possible on - * all orientations. - * - `'ortho'`: Rotated text along the axis direction so that the labels - * are orthogonal to the axis. This is very similar to `'flap'`, but - * prevents skewing the labels (X and Y scaling are still present). - * - `null`: Will use the config from `labels.position3d` - * - * @validvalue ['offset', 'chart', 'flap', 'ortho', null] - * @type {String} - * @since 5.0.15 - * @sample highcharts/3d/skewed-labels/ Skewed labels - * @product highcharts - */ - position3d: null, - - /** - * If enabled, the axis title will skewed to follow the perspective. - * - * This will fix overlapping labels and titles, but texts become less - * legible due to the distortion. - * - * The final appearance depends heavily on `title.position3d`. - * - * A `null` value will use the config from `labels.skew3d`. - * - * @validvalue [false, true, null] - * @type {Boolean} - * @sample highcharts/3d/skewed-labels/ Skewed labels - * @since 5.0.15 - * @product highcharts - */ - skew3d: null - } + labels: { + /** + * Defines how the labels are be repositioned according to the 3D chart + * orientation. + * - `'offset'`: Maintain a fixed horizontal/vertical distance from the + * tick marks, despite the chart orientation. This is the backwards + * compatible behavior, and causes skewing of X and Z axes. + * - `'chart'`: Preserve 3D position relative to the chart. + * This looks nice, but hard to read if the text isn't + * forward-facing. + * - `'flap'`: Rotated text along the axis to compensate for the chart + * orientation. This tries to maintain text as legible as possible + * on all orientations. + * - `'ortho'`: Rotated text along the axis direction so that the labels + * are orthogonal to the axis. This is very similar to `'flap'`, + * but prevents skewing the labels (X and Y scaling are still + * present). + * + * @validvalue ['offset', 'chart', 'flap', 'ortho'] + * @sample highcharts/3d/skewed-labels/ Skewed labels + * @since 5.0.15 + * @product highcharts + */ + position3d: 'offset', + + /** + * If enabled, the axis labels will skewed to follow the perspective. + * + * This will fix overlapping labels and titles, but texts become less + * legible due to the distortion. + * + * The final appearance depends heavily on `labels.position3d`. + * + * @since 5.0.15 + * @sample highcharts/3d/skewed-labels/ Skewed labels + * @product highcharts + */ + skew3d: false + }, + title: { + /** + * Defines how the title is repositioned according to the 3D chart + * orientation. + * - `'offset'`: Maintain a fixed horizontal/vertical distance from the + * tick marks, despite the chart orientation. This is the backwards + * compatible behavior, and causes skewing of X and Z axes. + * - `'chart'`: Preserve 3D position relative to the chart. + * This looks nice, but hard to read if the text isn't + * forward-facing. + * - `'flap'`: Rotated text along the axis to compensate for the chart + * orientation. This tries to maintain text as legible as possible on + * all orientations. + * - `'ortho'`: Rotated text along the axis direction so that the labels + * are orthogonal to the axis. This is very similar to `'flap'`, but + * prevents skewing the labels (X and Y scaling are still present). + * - `null`: Will use the config from `labels.position3d` + * + * @validvalue ['offset', 'chart', 'flap', 'ortho', null] + * @type {String} + * @since 5.0.15 + * @sample highcharts/3d/skewed-labels/ Skewed labels + * @product highcharts + */ + position3d: null, + + /** + * If enabled, the axis title will skewed to follow the perspective. + * + * This will fix overlapping labels and titles, but texts become less + * legible due to the distortion. + * + * The final appearance depends heavily on `title.position3d`. + * + * A `null` value will use the config from `labels.skew3d`. + * + * @validvalue [false, true, null] + * @type {Boolean} + * @sample highcharts/3d/skewed-labels/ Skewed labels + * @since 5.0.15 + * @product highcharts + */ + skew3d: null + } }; merge(true, Axis.prototype.defaultOptions, extendedOptions); addEvent(Axis, 'afterSetOptions', function () { - var options; - if (this.chart.is3d && this.chart.is3d() && this.coll !== 'colorAxis') { - options = this.options; - options.tickWidth = pick(options.tickWidth, 0); - options.gridLineWidth = pick(options.gridLineWidth, 1); - } + var options; + if (this.chart.is3d && this.chart.is3d() && this.coll !== 'colorAxis') { + options = this.options; + options.tickWidth = pick(options.tickWidth, 0); + options.gridLineWidth = pick(options.gridLineWidth, 1); + } }); wrap(Axis.prototype, 'getPlotLinePath', function (proceed) { - var path = proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.chart.is3d() || this.coll === 'colorAxis') { - return path; - } - - if (path === null) { - return path; - } - - var chart = this.chart, - options3d = chart.options.chart.options3d, - d = this.isZAxis ? chart.plotWidth : options3d.depth, - frame = chart.frame3d; - - var pArr = [ - this.swapZ({ x: path[1], y: path[2], z: 0 }), - this.swapZ({ x: path[1], y: path[2], z: d }), - this.swapZ({ x: path[4], y: path[5], z: 0 }), - this.swapZ({ x: path[4], y: path[5], z: d }) - ]; - - var pathSegments = []; - if (!this.horiz) { // Y-Axis - if (frame.front.visible) { - pathSegments.push(pArr[0], pArr[2]); - } - if (frame.back.visible) { - pathSegments.push(pArr[1], pArr[3]); - } - if (frame.left.visible) { - pathSegments.push(pArr[0], pArr[1]); - } - if (frame.right.visible) { - pathSegments.push(pArr[2], pArr[3]); - } - } else if (this.isZAxis) { // Z-Axis - if (frame.left.visible) { - pathSegments.push(pArr[0], pArr[2]); - } - if (frame.right.visible) { - pathSegments.push(pArr[1], pArr[3]); - } - if (frame.top.visible) { - pathSegments.push(pArr[0], pArr[1]); - } - if (frame.bottom.visible) { - pathSegments.push(pArr[2], pArr[3]); - } - } else { // X-Axis - if (frame.front.visible) { - pathSegments.push(pArr[0], pArr[2]); - } - if (frame.back.visible) { - pathSegments.push(pArr[1], pArr[3]); - } - if (frame.top.visible) { - pathSegments.push(pArr[0], pArr[1]); - } - if (frame.bottom.visible) { - pathSegments.push(pArr[2], pArr[3]); - } - } - - pathSegments = perspective(pathSegments, this.chart, false); - - return this.chart.renderer.toLineSegments(pathSegments); + var path = proceed.apply(this, [].slice.call(arguments, 1)); + + // Do not do this if the chart is not 3D + if (!this.chart.is3d() || this.coll === 'colorAxis') { + return path; + } + + if (path === null) { + return path; + } + + var chart = this.chart, + options3d = chart.options.chart.options3d, + d = this.isZAxis ? chart.plotWidth : options3d.depth, + frame = chart.frame3d; + + var pArr = [ + this.swapZ({ x: path[1], y: path[2], z: 0 }), + this.swapZ({ x: path[1], y: path[2], z: d }), + this.swapZ({ x: path[4], y: path[5], z: 0 }), + this.swapZ({ x: path[4], y: path[5], z: d }) + ]; + + var pathSegments = []; + if (!this.horiz) { // Y-Axis + if (frame.front.visible) { + pathSegments.push(pArr[0], pArr[2]); + } + if (frame.back.visible) { + pathSegments.push(pArr[1], pArr[3]); + } + if (frame.left.visible) { + pathSegments.push(pArr[0], pArr[1]); + } + if (frame.right.visible) { + pathSegments.push(pArr[2], pArr[3]); + } + } else if (this.isZAxis) { // Z-Axis + if (frame.left.visible) { + pathSegments.push(pArr[0], pArr[2]); + } + if (frame.right.visible) { + pathSegments.push(pArr[1], pArr[3]); + } + if (frame.top.visible) { + pathSegments.push(pArr[0], pArr[1]); + } + if (frame.bottom.visible) { + pathSegments.push(pArr[2], pArr[3]); + } + } else { // X-Axis + if (frame.front.visible) { + pathSegments.push(pArr[0], pArr[2]); + } + if (frame.back.visible) { + pathSegments.push(pArr[1], pArr[3]); + } + if (frame.top.visible) { + pathSegments.push(pArr[0], pArr[1]); + } + if (frame.bottom.visible) { + pathSegments.push(pArr[2], pArr[3]); + } + } + + pathSegments = perspective(pathSegments, this.chart, false); + + return this.chart.renderer.toLineSegments(pathSegments); }); // Do not draw axislines in 3D wrap(Axis.prototype, 'getLinePath', function (proceed) { - // Do not do this if the chart is not 3D - if (!this.chart.is3d() || this.coll === 'colorAxis') { - return proceed.apply(this, [].slice.call(arguments, 1)); - } + // Do not do this if the chart is not 3D + if (!this.chart.is3d() || this.coll === 'colorAxis') { + return proceed.apply(this, [].slice.call(arguments, 1)); + } - return []; + return []; }); wrap(Axis.prototype, 'getPlotBandPath', function (proceed) { - // Do not do this if the chart is not 3D - if (!this.chart.is3d() || this.coll === 'colorAxis') { - return proceed.apply(this, [].slice.call(arguments, 1)); - } - - var args = arguments, - from = args[1], - to = args[2], - path = [], - fromPath = this.getPlotLinePath(from), - toPath = this.getPlotLinePath(to); - - if (fromPath && toPath) { - for (var i = 0; i < fromPath.length; i += 6) { - path.push( - 'M', fromPath[i + 1], fromPath[i + 2], - 'L', fromPath[i + 4], fromPath[i + 5], - 'L', toPath[i + 4], toPath[i + 5], - 'L', toPath[i + 1], toPath[i + 2], - 'Z'); - } - } - - return path; + // Do not do this if the chart is not 3D + if (!this.chart.is3d() || this.coll === 'colorAxis') { + return proceed.apply(this, [].slice.call(arguments, 1)); + } + + var args = arguments, + from = args[1], + to = args[2], + path = [], + fromPath = this.getPlotLinePath(from), + toPath = this.getPlotLinePath(to); + + if (fromPath && toPath) { + for (var i = 0; i < fromPath.length; i += 6) { + path.push( + 'M', fromPath[i + 1], fromPath[i + 2], + 'L', fromPath[i + 4], fromPath[i + 5], + 'L', toPath[i + 4], toPath[i + 5], + 'L', toPath[i + 1], toPath[i + 2], + 'Z'); + } + } + + return path; }); function fix3dPosition(axis, pos, isTitle) { - // Do not do this if the chart is not 3D - if (!axis.chart.is3d() || axis.coll === 'colorAxis') { - return pos; - } - - var chart = axis.chart, - alpha = deg2rad * chart.options.chart.options3d.alpha, - beta = deg2rad * chart.options.chart.options3d.beta, - positionMode = pick( - isTitle && axis.options.title.position3d, - axis.options.labels.position3d - ), - skew = pick( - isTitle && axis.options.title.skew3d, - axis.options.labels.skew3d - ), - frame = chart.frame3d, - plotLeft = chart.plotLeft, - plotRight = chart.plotWidth + plotLeft, - plotTop = chart.plotTop, - plotBottom = chart.plotHeight + plotTop, - // Indicates we are labelling an X or Z axis on the "back" of the chart - reverseFlap = false, - offsetX = 0, - offsetY = 0, - vecX, - vecY = { x: 0, y: 1, z: 0 }; - - pos = axis.swapZ({ x: pos.x, y: pos.y, z: 0 }); - - - if (axis.isZAxis) { // Z Axis - if (axis.opposite) { - if (frame.axes.z.top === null) { - return {}; - } - offsetY = pos.y - plotTop; - pos.x = frame.axes.z.top.x; - pos.y = frame.axes.z.top.y; - vecX = frame.axes.z.top.xDir; - reverseFlap = !frame.top.frontFacing; - } else { - if (frame.axes.z.bottom === null) { - return {}; - } - offsetY = pos.y - plotBottom; - pos.x = frame.axes.z.bottom.x; - pos.y = frame.axes.z.bottom.y; - vecX = frame.axes.z.bottom.xDir; - reverseFlap = !frame.bottom.frontFacing; - } - } else if (axis.horiz) { // X Axis - if (axis.opposite) { - if (frame.axes.x.top === null) { - return {}; - } - offsetY = pos.y - plotTop; - pos.y = frame.axes.x.top.y; - pos.z = frame.axes.x.top.z; - vecX = frame.axes.x.top.xDir; - reverseFlap = !frame.top.frontFacing; - } else { - if (frame.axes.x.bottom === null) { - return {}; - } - offsetY = pos.y - plotBottom; - pos.y = frame.axes.x.bottom.y; - pos.z = frame.axes.x.bottom.z; - vecX = frame.axes.x.bottom.xDir; - reverseFlap = !frame.bottom.frontFacing; - } - } else { // Y Axis - if (axis.opposite) { - if (frame.axes.y.right === null) { - return {}; - } - offsetX = pos.x - plotRight; - pos.x = frame.axes.y.right.x; - pos.z = frame.axes.y.right.z; - vecX = frame.axes.y.right.xDir; - // Rotate 90º on opposite edge - vecX = { x: vecX.z, y: vecX.y, z: -vecX.x }; - } else { - if (frame.axes.y.left === null) { - return {}; - } - offsetX = pos.x - plotLeft; - pos.x = frame.axes.y.left.x; - pos.z = frame.axes.y.left.z; - vecX = frame.axes.y.left.xDir; - } - } - - if (positionMode === 'chart') { - // Labels preserve their direction relative to the chart - // nothing to do - - } else if (positionMode === 'flap') { - // Labels are be rotated around the axis direction to face the screen - if (!axis.horiz) { // Y Axis - vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) }; - } else { // X and Z Axis - var sin = Math.sin(alpha); - var cos = Math.cos(alpha); - if (axis.opposite) { - sin = -sin; - } - if (reverseFlap) { - sin = -sin; - } - vecY = { x: vecX.z * sin, y: cos, z: -vecX.x * sin }; - } - } else if (positionMode === 'ortho') { - // Labels will be rotated to be ortogonal to the axis - if (!axis.horiz) { // Y Axis - vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) }; - } else { // X and Z Axis - var sina = Math.sin(alpha); - var cosa = Math.cos(alpha); - var sinb = Math.sin(beta); - var cosb = Math.cos(beta); - var vecZ = { x: sinb * cosa, y: -sina, z: -cosa * cosb }; - vecY = { - x: vecX.y * vecZ.z - vecX.z * vecZ.y, - y: vecX.z * vecZ.x - vecX.x * vecZ.z, - z: vecX.x * vecZ.y - vecX.y * vecZ.x - }; - var scale = 1 / Math.sqrt( - vecY.x * vecY.x + vecY.y * vecY.y + vecY.z * vecY.z - ); - if (reverseFlap) { - scale = -scale; - } - vecY = { x: scale * vecY.x, y: scale * vecY.y, z: scale * vecY.z }; - } - } else { // positionMode == 'offset' - // Labels will be skewd to maintain vertical / horizontal offsets from - // axis - if (!axis.horiz) { // Y Axis - vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) }; - } else { // X and Z Axis - vecY = { - x: Math.sin(beta) * Math.sin(alpha), - y: Math.cos(alpha), - z: -Math.cos(beta) * Math.sin(alpha) - }; - } - } - pos.x += offsetX * vecX.x + offsetY * vecY.x; - pos.y += offsetX * vecX.y + offsetY * vecY.y; - pos.z += offsetX * vecX.z + offsetY * vecY.z; - - var projected = perspective([pos], axis.chart)[0]; - - if (skew) { - // Check if the label text would be mirrored - var isMirrored = shapeArea(perspective([ - pos, - { x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z }, - { x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z } - ], axis.chart)) < 0; - if (isMirrored) { - vecX = { x: -vecX.x, y: -vecX.y, z: -vecX.z }; - } - - var pointsProjected = perspective([ - { x: pos.x, y: pos.y, z: pos.z }, - { x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z }, - { x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z } - ], axis.chart); - - projected.matrix = [ - pointsProjected[1].x - pointsProjected[0].x, - pointsProjected[1].y - pointsProjected[0].y, - pointsProjected[2].x - pointsProjected[0].x, - pointsProjected[2].y - pointsProjected[0].y, - projected.x, - projected.y - ]; - projected.matrix[4] -= projected.x * projected.matrix[0] + - projected.y * projected.matrix[2]; - projected.matrix[5] -= projected.x * projected.matrix[1] + - projected.y * projected.matrix[3]; - } else { - projected.matrix = null; - } - - return projected; + // Do not do this if the chart is not 3D + if (!axis.chart.is3d() || axis.coll === 'colorAxis') { + return pos; + } + + var chart = axis.chart, + alpha = deg2rad * chart.options.chart.options3d.alpha, + beta = deg2rad * chart.options.chart.options3d.beta, + positionMode = pick( + isTitle && axis.options.title.position3d, + axis.options.labels.position3d + ), + skew = pick( + isTitle && axis.options.title.skew3d, + axis.options.labels.skew3d + ), + frame = chart.frame3d, + plotLeft = chart.plotLeft, + plotRight = chart.plotWidth + plotLeft, + plotTop = chart.plotTop, + plotBottom = chart.plotHeight + plotTop, + // Indicates we are labelling an X or Z axis on the "back" of the chart + reverseFlap = false, + offsetX = 0, + offsetY = 0, + vecX, + vecY = { x: 0, y: 1, z: 0 }; + + pos = axis.swapZ({ x: pos.x, y: pos.y, z: 0 }); + + + if (axis.isZAxis) { // Z Axis + if (axis.opposite) { + if (frame.axes.z.top === null) { + return {}; + } + offsetY = pos.y - plotTop; + pos.x = frame.axes.z.top.x; + pos.y = frame.axes.z.top.y; + vecX = frame.axes.z.top.xDir; + reverseFlap = !frame.top.frontFacing; + } else { + if (frame.axes.z.bottom === null) { + return {}; + } + offsetY = pos.y - plotBottom; + pos.x = frame.axes.z.bottom.x; + pos.y = frame.axes.z.bottom.y; + vecX = frame.axes.z.bottom.xDir; + reverseFlap = !frame.bottom.frontFacing; + } + } else if (axis.horiz) { // X Axis + if (axis.opposite) { + if (frame.axes.x.top === null) { + return {}; + } + offsetY = pos.y - plotTop; + pos.y = frame.axes.x.top.y; + pos.z = frame.axes.x.top.z; + vecX = frame.axes.x.top.xDir; + reverseFlap = !frame.top.frontFacing; + } else { + if (frame.axes.x.bottom === null) { + return {}; + } + offsetY = pos.y - plotBottom; + pos.y = frame.axes.x.bottom.y; + pos.z = frame.axes.x.bottom.z; + vecX = frame.axes.x.bottom.xDir; + reverseFlap = !frame.bottom.frontFacing; + } + } else { // Y Axis + if (axis.opposite) { + if (frame.axes.y.right === null) { + return {}; + } + offsetX = pos.x - plotRight; + pos.x = frame.axes.y.right.x; + pos.z = frame.axes.y.right.z; + vecX = frame.axes.y.right.xDir; + // Rotate 90º on opposite edge + vecX = { x: vecX.z, y: vecX.y, z: -vecX.x }; + } else { + if (frame.axes.y.left === null) { + return {}; + } + offsetX = pos.x - plotLeft; + pos.x = frame.axes.y.left.x; + pos.z = frame.axes.y.left.z; + vecX = frame.axes.y.left.xDir; + } + } + + if (positionMode === 'chart') { + // Labels preserve their direction relative to the chart + // nothing to do + + } else if (positionMode === 'flap') { + // Labels are be rotated around the axis direction to face the screen + if (!axis.horiz) { // Y Axis + vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) }; + } else { // X and Z Axis + var sin = Math.sin(alpha); + var cos = Math.cos(alpha); + if (axis.opposite) { + sin = -sin; + } + if (reverseFlap) { + sin = -sin; + } + vecY = { x: vecX.z * sin, y: cos, z: -vecX.x * sin }; + } + } else if (positionMode === 'ortho') { + // Labels will be rotated to be ortogonal to the axis + if (!axis.horiz) { // Y Axis + vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) }; + } else { // X and Z Axis + var sina = Math.sin(alpha); + var cosa = Math.cos(alpha); + var sinb = Math.sin(beta); + var cosb = Math.cos(beta); + var vecZ = { x: sinb * cosa, y: -sina, z: -cosa * cosb }; + vecY = { + x: vecX.y * vecZ.z - vecX.z * vecZ.y, + y: vecX.z * vecZ.x - vecX.x * vecZ.z, + z: vecX.x * vecZ.y - vecX.y * vecZ.x + }; + var scale = 1 / Math.sqrt( + vecY.x * vecY.x + vecY.y * vecY.y + vecY.z * vecY.z + ); + if (reverseFlap) { + scale = -scale; + } + vecY = { x: scale * vecY.x, y: scale * vecY.y, z: scale * vecY.z }; + } + } else { // positionMode == 'offset' + // Labels will be skewd to maintain vertical / horizontal offsets from + // axis + if (!axis.horiz) { // Y Axis + vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) }; + } else { // X and Z Axis + vecY = { + x: Math.sin(beta) * Math.sin(alpha), + y: Math.cos(alpha), + z: -Math.cos(beta) * Math.sin(alpha) + }; + } + } + pos.x += offsetX * vecX.x + offsetY * vecY.x; + pos.y += offsetX * vecX.y + offsetY * vecY.y; + pos.z += offsetX * vecX.z + offsetY * vecY.z; + + var projected = perspective([pos], axis.chart)[0]; + + if (skew) { + // Check if the label text would be mirrored + var isMirrored = shapeArea(perspective([ + pos, + { x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z }, + { x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z } + ], axis.chart)) < 0; + if (isMirrored) { + vecX = { x: -vecX.x, y: -vecX.y, z: -vecX.z }; + } + + var pointsProjected = perspective([ + { x: pos.x, y: pos.y, z: pos.z }, + { x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z }, + { x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z } + ], axis.chart); + + projected.matrix = [ + pointsProjected[1].x - pointsProjected[0].x, + pointsProjected[1].y - pointsProjected[0].y, + pointsProjected[2].x - pointsProjected[0].x, + pointsProjected[2].y - pointsProjected[0].y, + projected.x, + projected.y + ]; + projected.matrix[4] -= projected.x * projected.matrix[0] + + projected.y * projected.matrix[2]; + projected.matrix[5] -= projected.x * projected.matrix[1] + + projected.y * projected.matrix[3]; + } else { + projected.matrix = null; + } + + return projected; } /* Tick extensions */ wrap(Tick.prototype, 'getMarkPath', function (proceed) { - var path = proceed.apply(this, [].slice.call(arguments, 1)); + var path = proceed.apply(this, [].slice.call(arguments, 1)); - var pArr = [ - fix3dPosition(this.axis, { x: path[1], y: path[2], z: 0 }), - fix3dPosition(this.axis, { x: path[4], y: path[5], z: 0 }) - ]; + var pArr = [ + fix3dPosition(this.axis, { x: path[1], y: path[2], z: 0 }), + fix3dPosition(this.axis, { x: path[4], y: path[5], z: 0 }) + ]; - return this.axis.chart.renderer.toLineSegments(pArr); + return this.axis.chart.renderer.toLineSegments(pArr); }); addEvent(Tick, 'afterGetLabelPosition', function (e) { - extend(e.pos, fix3dPosition(this.axis, e.pos)); + extend(e.pos, fix3dPosition(this.axis, e.pos)); }); wrap(Axis.prototype, 'getTitlePosition', function (proceed) { - var pos = proceed.apply(this, [].slice.call(arguments, 1)); - return fix3dPosition(this, pos, true); + var pos = proceed.apply(this, [].slice.call(arguments, 1)); + return fix3dPosition(this, pos, true); }); addEvent(Axis, 'drawCrosshair', function (e) { - if (this.chart.is3d() && this.coll !== 'colorAxis') { - if (e.point) { - e.point.crosshairPos = this.isXAxis ? - e.point.plotXold || e.point.plotX : - this.len - (e.point.plotYold || e.point.plotY); - } - } + if (this.chart.is3d() && this.coll !== 'colorAxis') { + if (e.point) { + e.point.crosshairPos = this.isXAxis ? + e.point.plotXold || e.point.plotX : + this.len - (e.point.plotYold || e.point.plotY); + } + } }); addEvent(Axis, 'destroy', function () { - each(['backFrame', 'bottomFrame', 'sideFrame'], function (prop) { - if (this[prop]) { - this[prop] = this[prop].destroy(); - } - }, this); + each(['backFrame', 'bottomFrame', 'sideFrame'], function (prop) { + if (this[prop]) { + this[prop] = this[prop].destroy(); + } + }, this); }); /* @@ -473,82 +474,82 @@ Z-AXIS */ Axis.prototype.swapZ = function (p, insidePlotArea) { - if (this.isZAxis) { - var plotLeft = insidePlotArea ? 0 : this.chart.plotLeft; - return { - x: plotLeft + p.z, - y: p.y, - z: p.x - plotLeft - }; - } - return p; + if (this.isZAxis) { + var plotLeft = insidePlotArea ? 0 : this.chart.plotLeft; + return { + x: plotLeft + p.z, + y: p.y, + z: p.x - plotLeft + }; + } + return p; }; ZAxis = H.ZAxis = function () { - this.init.apply(this, arguments); + this.init.apply(this, arguments); }; extend(ZAxis.prototype, Axis.prototype); extend(ZAxis.prototype, { - isZAxis: true, - setOptions: function (userOptions) { - userOptions = merge({ - offset: 0, - lineWidth: 0 - }, userOptions); - Axis.prototype.setOptions.call(this, userOptions); - this.coll = 'zAxis'; - }, - setAxisSize: function () { - Axis.prototype.setAxisSize.call(this); - this.width = this.len = this.chart.options.chart.options3d.depth; - this.right = this.chart.chartWidth - this.width - this.left; - }, - getSeriesExtremes: function () { - var axis = this, - chart = axis.chart; - - axis.hasVisibleSeries = false; - - // Reset properties in case we're redrawing (#3353) - axis.dataMin = - axis.dataMax = - axis.ignoreMinPadding = - axis.ignoreMaxPadding = null; - - if (axis.buildStacks) { - axis.buildStacks(); - } - - // loop through this axis' series - each(axis.series, function (series) { - - if (series.visible || !chart.options.chart.ignoreHiddenSeries) { - - var seriesOptions = series.options, - zData, - threshold = seriesOptions.threshold; - - axis.hasVisibleSeries = true; - - // Validate threshold in logarithmic axes - if (axis.positiveValuesOnly && threshold <= 0) { - threshold = null; - } - - zData = series.zData; - if (zData.length) { - axis.dataMin = Math.min( - pick(axis.dataMin, zData[0]), - Math.min.apply(null, zData) - ); - axis.dataMax = Math.max( - pick(axis.dataMax, zData[0]), - Math.max.apply(null, zData) - ); - } - } - }); - } + isZAxis: true, + setOptions: function (userOptions) { + userOptions = merge({ + offset: 0, + lineWidth: 0 + }, userOptions); + Axis.prototype.setOptions.call(this, userOptions); + this.coll = 'zAxis'; + }, + setAxisSize: function () { + Axis.prototype.setAxisSize.call(this); + this.width = this.len = this.chart.options.chart.options3d.depth; + this.right = this.chart.chartWidth - this.width - this.left; + }, + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart; + + axis.hasVisibleSeries = false; + + // Reset properties in case we're redrawing (#3353) + axis.dataMin = + axis.dataMax = + axis.ignoreMinPadding = + axis.ignoreMaxPadding = null; + + if (axis.buildStacks) { + axis.buildStacks(); + } + + // loop through this axis' series + each(axis.series, function (series) { + + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + + var seriesOptions = series.options, + zData, + threshold = seriesOptions.threshold; + + axis.hasVisibleSeries = true; + + // Validate threshold in logarithmic axes + if (axis.positiveValuesOnly && threshold <= 0) { + threshold = null; + } + + zData = series.zData; + if (zData.length) { + axis.dataMin = Math.min( + pick(axis.dataMin, zData[0]), + Math.min.apply(null, zData) + ); + axis.dataMax = Math.max( + pick(axis.dataMax, zData[0]), + Math.max.apply(null, zData) + ); + } + } + }); + } }); @@ -556,19 +557,19 @@ extend(ZAxis.prototype, { * Get the Z axis in addition to the default X and Y. */ addEvent(Chart, 'afterGetAxes', function () { - var chart = this, - options = this.options, - zAxisOptions = options.zAxis = splat(options.zAxis || {}); - - if (!chart.is3d()) { - return; - } - this.zAxis = []; - each(zAxisOptions, function (axisOptions, i) { - axisOptions.index = i; - // Z-Axis is shown horizontally, so it's kind of a X-Axis - axisOptions.isX = true; - var zAxis = new ZAxis(chart, axisOptions); - zAxis.setScale(); - }); + var chart = this, + options = this.options, + zAxisOptions = options.zAxis = splat(options.zAxis || {}); + + if (!chart.is3d()) { + return; + } + this.zAxis = []; + each(zAxisOptions, function (axisOptions, i) { + axisOptions.index = i; + // Z-Axis is shown horizontally, so it's kind of a X-Axis + axisOptions.isX = true; + var zAxis = new ZAxis(chart, axisOptions); + zAxis.setScale(); + }); }); diff --git a/js/parts-3d/Chart.js b/js/parts-3d/Chart.js index 81c69a7220c..7509e59d1e9 100644 --- a/js/parts-3d/Chart.js +++ b/js/parts-3d/Chart.js @@ -10,48 +10,48 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Chart.js'; var addEvent = H.addEvent, - Chart = H.Chart, - each = H.each, - merge = H.merge, - perspective = H.perspective, - pick = H.pick, - wrap = H.wrap; + Chart = H.Chart, + each = H.each, + merge = H.merge, + perspective = H.perspective, + pick = H.pick, + wrap = H.wrap; // Shorthand to check the is3d flag Chart.prototype.is3d = function () { - return ( - this.options.chart.options3d && - this.options.chart.options3d.enabled - ); // #4280 + return ( + this.options.chart.options3d && + this.options.chart.options3d.enabled + ); // #4280 }; Chart.prototype.propsRequireDirtyBox.push('chart.options3d'); Chart.prototype.propsRequireUpdateSeries.push('chart.options3d'); // Legacy support for HC < 6 to make 'scatter' series in a 3D chart route to the -// real 'scatter3d' series type. +// real 'scatter3d' series type. addEvent(Chart, 'afterInit', function () { - var options = this.options; - - if (this.is3d()) { - each(options.series, function (s) { - var type = s.type || - options.chart.type || - options.chart.defaultSeriesType; - if (type === 'scatter') { - s.type = 'scatter3d'; - } - }); - } + var options = this.options; + + if (this.is3d()) { + each(options.series, function (s) { + var type = s.type || + options.chart.type || + options.chart.defaultSeriesType; + if (type === 'scatter') { + s.type = 'scatter3d'; + } + }); + } }); /** * Calculate scale of the 3D view. That is required to * fit chart's 3D projection into the actual plotting area. Reported as #4933. * @notice This function should ideally take the plot values instead of a chart - * object, but since the chart object is needed for perspective it is - * not practical. Possible to make both getScale and perspective more - * logical and also immutable. + * object, but since the chart object is needed for perspective it is + * not practical. Possible to make both getScale and perspective more + * logical and also immutable. * @param {Object} chart Chart object * @param {Number} chart.plotLeft * @param {Number} chart.plotWidth @@ -61,107 +61,107 @@ addEvent(Chart, 'afterInit', function () { * @return {Number} The scale to fit the 3D chart into the plotting area. */ function getScale(chart, depth) { - var plotLeft = chart.plotLeft, - plotRight = chart.plotWidth + plotLeft, - plotTop = chart.plotTop, - plotBottom = chart.plotHeight + plotTop, - originX = plotLeft + chart.plotWidth / 2, - originY = plotTop + chart.plotHeight / 2, - bbox3d = { - minX: Number.MAX_VALUE, - maxX: -Number.MAX_VALUE, - minY: Number.MAX_VALUE, - maxY: -Number.MAX_VALUE - }, - corners, - scale = 1; - - // Top left corners: - corners = [{ - x: plotLeft, - y: plotTop, - z: 0 - }, { - x: plotLeft, - y: plotTop, - z: depth - }]; - - // Top right corners: - each([0, 1], function (i) { - corners.push({ - x: plotRight, - y: corners[i].y, - z: corners[i].z - }); - }); - - // All bottom corners: - each([0, 1, 2, 3], function (i) { - corners.push({ - x: corners[i].x, - y: plotBottom, - z: corners[i].z - }); - }); - - // Calculate 3D corners: - corners = perspective(corners, chart, false); - - // Get bounding box of 3D element: - each(corners, function (corner) { - bbox3d.minX = Math.min(bbox3d.minX, corner.x); - bbox3d.maxX = Math.max(bbox3d.maxX, corner.x); - bbox3d.minY = Math.min(bbox3d.minY, corner.y); - bbox3d.maxY = Math.max(bbox3d.maxY, corner.y); - }); - - // Left edge: - if (plotLeft > bbox3d.minX) { - scale = Math.min( - scale, - 1 - Math.abs((plotLeft + originX) / (bbox3d.minX + originX)) % 1 - ); - } - - // Right edge: - if (plotRight < bbox3d.maxX) { - scale = Math.min( - scale, - (plotRight - originX) / (bbox3d.maxX - originX) - ); - } - - // Top edge: - if (plotTop > bbox3d.minY) { - if (bbox3d.minY < 0) { - scale = Math.min( - scale, - (plotTop + originY) / (-bbox3d.minY + plotTop + originY) - ); - } else { - scale = Math.min( - scale, - 1 - (plotTop + originY) / (bbox3d.minY + originY) % 1 - ); - } - } - - // Bottom edge: - if (plotBottom < bbox3d.maxY) { - scale = Math.min( - scale, - Math.abs((plotBottom - originY) / (bbox3d.maxY - originY)) - ); - } - - return scale; + var plotLeft = chart.plotLeft, + plotRight = chart.plotWidth + plotLeft, + plotTop = chart.plotTop, + plotBottom = chart.plotHeight + plotTop, + originX = plotLeft + chart.plotWidth / 2, + originY = plotTop + chart.plotHeight / 2, + bbox3d = { + minX: Number.MAX_VALUE, + maxX: -Number.MAX_VALUE, + minY: Number.MAX_VALUE, + maxY: -Number.MAX_VALUE + }, + corners, + scale = 1; + + // Top left corners: + corners = [{ + x: plotLeft, + y: plotTop, + z: 0 + }, { + x: plotLeft, + y: plotTop, + z: depth + }]; + + // Top right corners: + each([0, 1], function (i) { + corners.push({ + x: plotRight, + y: corners[i].y, + z: corners[i].z + }); + }); + + // All bottom corners: + each([0, 1, 2, 3], function (i) { + corners.push({ + x: corners[i].x, + y: plotBottom, + z: corners[i].z + }); + }); + + // Calculate 3D corners: + corners = perspective(corners, chart, false); + + // Get bounding box of 3D element: + each(corners, function (corner) { + bbox3d.minX = Math.min(bbox3d.minX, corner.x); + bbox3d.maxX = Math.max(bbox3d.maxX, corner.x); + bbox3d.minY = Math.min(bbox3d.minY, corner.y); + bbox3d.maxY = Math.max(bbox3d.maxY, corner.y); + }); + + // Left edge: + if (plotLeft > bbox3d.minX) { + scale = Math.min( + scale, + 1 - Math.abs((plotLeft + originX) / (bbox3d.minX + originX)) % 1 + ); + } + + // Right edge: + if (plotRight < bbox3d.maxX) { + scale = Math.min( + scale, + (plotRight - originX) / (bbox3d.maxX - originX) + ); + } + + // Top edge: + if (plotTop > bbox3d.minY) { + if (bbox3d.minY < 0) { + scale = Math.min( + scale, + (plotTop + originY) / (-bbox3d.minY + plotTop + originY) + ); + } else { + scale = Math.min( + scale, + 1 - (plotTop + originY) / (bbox3d.minY + originY) % 1 + ); + } + } + + // Bottom edge: + if (plotBottom < bbox3d.maxY) { + scale = Math.min( + scale, + Math.abs((plotBottom - originY) / (bbox3d.maxY - originY)) + ); + } + + return scale; } H.wrap(H.Chart.prototype, 'isInsidePlot', function (proceed) { - return this.is3d() || proceed.apply(this, [].slice.call(arguments, 1)); + return this.is3d() || proceed.apply(this, [].slice.call(arguments, 1)); }); var defaultOptions = H.getOptions(); @@ -169,200 +169,200 @@ var defaultOptions = H.getOptions(); /** * @optionparent */ -var extendedOptions = { - - chart: { - - /** - * Options to render charts in 3 dimensions. This feature requires - * `highcharts-3d.js`, found in the download package or online at - * [code.highcharts.com/highcharts-3d.js](http://code.highcharts.com/highcharts- - * 3d.js). - * - * @since 4.0 - * @product highcharts - */ - options3d: { - - /** - * Wether to render the chart using the 3D functionality. - * - * @type {Boolean} - * @default false - * @since 4.0 - * @product highcharts - */ - enabled: false, - - /** - * One of the two rotation angles for the chart. - * - * @type {Number} - * @default 0 - * @since 4.0 - * @product highcharts - */ - alpha: 0, - - /** - * One of the two rotation angles for the chart. - * - * @type {Number} - * @default 0 - * @since 4.0 - * @product highcharts - */ - beta: 0, - - /** - * The total depth of the chart. - * - * @type {Number} - * @default 100 - * @since 4.0 - * @product highcharts - */ - depth: 100, - - /** - * Whether the 3d box should automatically adjust to the chart plot - * area. - * - * @type {Boolean} - * @default true - * @since 4.2.4 - * @product highcharts - */ - fitToPlot: true, - - /** - * Defines the distance the viewer is standing in front of the - * chart, this setting is important to calculate the perspective - * effect in column and scatter charts. It is not used for 3D pie - * charts. - * - * @type {Number} - * @default 100 - * @since 4.0 - * @product highcharts - */ - viewDistance: 25, - - /** - * Set it to `"auto"` to automatically move the labels to the best - * edge. - * - * @validvalue [null, "auto"] - * @type {String} - * @default null - * @since 5.0.12 - * @product highcharts - */ - axisLabelPosition: 'default', - - /** - * Provides the option to draw a frame around the charts by defining - * a bottom, front and back panel. - * - * @since 4.0 - * @product highcharts - */ - frame: { - - /** - * Whether the frames are visible. - */ - visible: 'default', - - /** - * General pixel thickness for the frame faces. - */ - size: 1, - - /** - * The bottom of the frame around a 3D chart. - * - * @since 4.0 - * @product highcharts - */ - - /** - * The color of the panel. - * - * @type {Color} - * @default transparent - * @since 4.0 - * @product highcharts - * @apioption chart.options3d.frame.bottom.color - */ - - /** - * The thickness of the panel. - * - * @type {Number} - * @default 1 - * @since 4.0 - * @product highcharts - * @apioption chart.options3d.frame.bottom.size - */ - - /** - * Whether to display the frame. Possible values are `true`, - * `false`, `"auto"` to display only the frames behind the data, - * and `"default"` to display faces behind the data based on the - * axis layout, ignoring the point of view. - * - * @validvalue ["default", "auto", true, false] - * @type {Boolean|String} - * @sample {highcharts} highcharts/3d/scatter-frame/ Auto frames - * @default default - * @since 5.0.12 - * @product highcharts - * @apioption chart.options3d.frame.bottom.visible - */ - - /** - * The bottom of the frame around a 3D chart. - */ - bottom: {}, - - /** - * The top of the frame around a 3D chart. - * - * @extends {chart.options3d.frame.bottom} - */ - top: {}, - - /** - * The left side of the frame around a 3D chart. - * - * @extends {chart.options3d.frame.bottom} - */ - left: {}, - - /** - * The right of the frame around a 3D chart. - * - * @extends {chart.options3d.frame.bottom} - */ - right: {}, - - /** - * The back side of the frame around a 3D chart. - * - * @extends {chart.options3d.frame.bottom} - */ - back: {}, - - /** - * The front of the frame around a 3D chart. - * - * @extends {chart.options3d.frame.bottom} - */ - front: {} - } - } - } +var extendedOptions = { + + chart: { + + /** + * Options to render charts in 3 dimensions. This feature requires + * `highcharts-3d.js`, found in the download package or online at + * [code.highcharts.com/highcharts-3d.js](http://code.highcharts.com/highcharts- + * 3d.js). + * + * @since 4.0 + * @product highcharts + */ + options3d: { + + /** + * Wether to render the chart using the 3D functionality. + * + * @type {Boolean} + * @default false + * @since 4.0 + * @product highcharts + */ + enabled: false, + + /** + * One of the two rotation angles for the chart. + * + * @type {Number} + * @default 0 + * @since 4.0 + * @product highcharts + */ + alpha: 0, + + /** + * One of the two rotation angles for the chart. + * + * @type {Number} + * @default 0 + * @since 4.0 + * @product highcharts + */ + beta: 0, + + /** + * The total depth of the chart. + * + * @type {Number} + * @default 100 + * @since 4.0 + * @product highcharts + */ + depth: 100, + + /** + * Whether the 3d box should automatically adjust to the chart plot + * area. + * + * @type {Boolean} + * @default true + * @since 4.2.4 + * @product highcharts + */ + fitToPlot: true, + + /** + * Defines the distance the viewer is standing in front of the + * chart, this setting is important to calculate the perspective + * effect in column and scatter charts. It is not used for 3D pie + * charts. + * + * @type {Number} + * @default 100 + * @since 4.0 + * @product highcharts + */ + viewDistance: 25, + + /** + * Set it to `"auto"` to automatically move the labels to the best + * edge. + * + * @validvalue [null, "auto"] + * @type {String} + * @default null + * @since 5.0.12 + * @product highcharts + */ + axisLabelPosition: 'default', + + /** + * Provides the option to draw a frame around the charts by defining + * a bottom, front and back panel. + * + * @since 4.0 + * @product highcharts + */ + frame: { + + /** + * Whether the frames are visible. + */ + visible: 'default', + + /** + * General pixel thickness for the frame faces. + */ + size: 1, + + /** + * The bottom of the frame around a 3D chart. + * + * @since 4.0 + * @product highcharts + */ + + /** + * The color of the panel. + * + * @type {Color} + * @default transparent + * @since 4.0 + * @product highcharts + * @apioption chart.options3d.frame.bottom.color + */ + + /** + * The thickness of the panel. + * + * @type {Number} + * @default 1 + * @since 4.0 + * @product highcharts + * @apioption chart.options3d.frame.bottom.size + */ + + /** + * Whether to display the frame. Possible values are `true`, + * `false`, `"auto"` to display only the frames behind the data, + * and `"default"` to display faces behind the data based on the + * axis layout, ignoring the point of view. + * + * @validvalue ["default", "auto", true, false] + * @type {Boolean|String} + * @sample {highcharts} highcharts/3d/scatter-frame/ Auto frames + * @default default + * @since 5.0.12 + * @product highcharts + * @apioption chart.options3d.frame.bottom.visible + */ + + /** + * The bottom of the frame around a 3D chart. + */ + bottom: {}, + + /** + * The top of the frame around a 3D chart. + * + * @extends {chart.options3d.frame.bottom} + */ + top: {}, + + /** + * The left side of the frame around a 3D chart. + * + * @extends {chart.options3d.frame.bottom} + */ + left: {}, + + /** + * The right of the frame around a 3D chart. + * + * @extends {chart.options3d.frame.bottom} + */ + right: {}, + + /** + * The back side of the frame around a 3D chart. + * + * @extends {chart.options3d.frame.bottom} + */ + back: {}, + + /** + * The front of the frame around a 3D chart. + * + * @extends {chart.options3d.frame.bottom} + */ + front: {} + } + } + } }; merge(true, defaultOptions, extendedOptions); @@ -372,1317 +372,1317 @@ merge(true, defaultOptions, extendedOptions); * Add the required CSS classes for column sides (#6018) */ addEvent(Chart, 'afterGetContainer', function () { - this.renderer.definition({ - tagName: 'style', - textContent: - '.highcharts-3d-top{' + - 'filter: url(#highcharts-brighter)' + - '}\n' + - '.highcharts-3d-side{' + - 'filter: url(#highcharts-darker)' + - '}\n' - }); + this.renderer.definition({ + tagName: 'style', + textContent: + '.highcharts-3d-top{' + + 'filter: url(#highcharts-brighter)' + + '}\n' + + '.highcharts-3d-side{' + + 'filter: url(#highcharts-darker)' + + '}\n' + }); }); /*= } =*/ wrap(Chart.prototype, 'setClassName', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); + proceed.apply(this, [].slice.call(arguments, 1)); - if (this.is3d()) { - this.container.className += ' highcharts-3d-chart'; - } + if (this.is3d()) { + this.container.className += ' highcharts-3d-chart'; + } }); addEvent(H.Chart, 'afterSetChartSize', function () { - var chart = this, - options3d = chart.options.chart.options3d; - - if (chart.is3d()) { - var inverted = chart.inverted, - clipBox = chart.clipBox, - margin = chart.margin, - x = inverted ? 'y' : 'x', - y = inverted ? 'x' : 'y', - w = inverted ? 'height' : 'width', - h = inverted ? 'width' : 'height'; - - clipBox[x] = -(margin[3] || 0); - clipBox[y] = -(margin[0] || 0); - clipBox[w] = chart.chartWidth + (margin[3] || 0) + (margin[1] || 0); - clipBox[h] = chart.chartHeight + (margin[0] || 0) + (margin[2] || 0); - - // Set scale, used later in perspective method(): - // getScale uses perspective, so scale3d has to be reset. - chart.scale3d = 1; - if (options3d.fitToPlot === true) { - chart.scale3d = getScale(chart, options3d.depth); - } - // Recalculate the 3d frame with every call of setChartSize, - // instead of doing it after every redraw(). It avoids ticks - // and axis title outside of chart. - chart.frame3d = this.get3dFrame(); // #7942 - } + var chart = this, + options3d = chart.options.chart.options3d; + + if (chart.is3d()) { + var inverted = chart.inverted, + clipBox = chart.clipBox, + margin = chart.margin, + x = inverted ? 'y' : 'x', + y = inverted ? 'x' : 'y', + w = inverted ? 'height' : 'width', + h = inverted ? 'width' : 'height'; + + clipBox[x] = -(margin[3] || 0); + clipBox[y] = -(margin[0] || 0); + clipBox[w] = chart.chartWidth + (margin[3] || 0) + (margin[1] || 0); + clipBox[h] = chart.chartHeight + (margin[0] || 0) + (margin[2] || 0); + + // Set scale, used later in perspective method(): + // getScale uses perspective, so scale3d has to be reset. + chart.scale3d = 1; + if (options3d.fitToPlot === true) { + chart.scale3d = getScale(chart, options3d.depth); + } + // Recalculate the 3d frame with every call of setChartSize, + // instead of doing it after every redraw(). It avoids ticks + // and axis title outside of chart. + chart.frame3d = this.get3dFrame(); // #7942 + } }); addEvent(Chart, 'beforeRedraw', function () { - if (this.is3d()) { - // Set to force a redraw of all elements - this.isDirtyBox = true; - } + if (this.is3d()) { + // Set to force a redraw of all elements + this.isDirtyBox = true; + } }); addEvent(Chart, 'beforeRender', function () { - if (this.is3d()) { - this.frame3d = this.get3dFrame(); - } + if (this.is3d()) { + this.frame3d = this.get3dFrame(); + } }); // Draw the series in the reverse order (#3803, #3917) wrap(Chart.prototype, 'renderSeries', function (proceed) { - var series, - i = this.series.length; - - if (this.is3d()) { - while (i--) { - series = this.series[i]; - series.translate(); - series.render(); - } - } else { - proceed.call(this); - } + var series, + i = this.series.length; + + if (this.is3d()) { + while (i--) { + series = this.series[i]; + series.translate(); + series.render(); + } + } else { + proceed.call(this); + } }); addEvent(Chart, 'afterDrawChartBox', function () { - if (this.is3d()) { - var chart = this, - renderer = chart.renderer, - options3d = this.options.chart.options3d, - frame = chart.get3dFrame(), - xm = this.plotLeft, - xp = this.plotLeft + this.plotWidth, - ym = this.plotTop, - yp = this.plotTop + this.plotHeight, - zm = 0, - zp = options3d.depth, - xmm = xm - (frame.left.visible ? frame.left.size : 0), - xpp = xp + (frame.right.visible ? frame.right.size : 0), - ymm = ym - (frame.top.visible ? frame.top.size : 0), - ypp = yp + (frame.bottom.visible ? frame.bottom.size : 0), - zmm = zm - (frame.front.visible ? frame.front.size : 0), - zpp = zp + (frame.back.visible ? frame.back.size : 0), - verb = chart.hasRendered ? 'animate' : 'attr'; - - this.frame3d = frame; - - if (!this.frameShapes) { - this.frameShapes = { - bottom: renderer.polyhedron().add(), - top: renderer.polyhedron().add(), - left: renderer.polyhedron().add(), - right: renderer.polyhedron().add(), - back: renderer.polyhedron().add(), - front: renderer.polyhedron().add() - }; - } - this.frameShapes.bottom[verb]({ - 'class': 'highcharts-3d-frame highcharts-3d-frame-bottom', - zIndex: frame.bottom.frontFacing ? -1000 : 1000, - faces: [{ // bottom - fill: H.color(frame.bottom.color).brighten(0.1).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zmm - }, { - x: xpp, - y: ypp, - z: zmm - }, { - x: xpp, - y: ypp, - z: zpp - }, { - x: xmm, - y: ypp, - z: zpp - }], - enabled: frame.bottom.visible - }, - { // top - fill: H.color(frame.bottom.color).brighten(0.1).get(), - vertexes: [{ - x: xm, - y: yp, - z: zp - }, { - x: xp, - y: yp, - z: zp - }, { - x: xp, - y: yp, - z: zm - }, { - x: xm, - y: yp, - z: zm - }], - enabled: frame.bottom.visible - }, - { // left - fill: H.color(frame.bottom.color).brighten(-0.1).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zmm - }, { - x: xmm, - y: ypp, - z: zpp - }, { - x: xm, - y: yp, - z: zp - }, { - x: xm, - y: yp, - z: zm - }], - enabled: frame.bottom.visible && !frame.left.visible - }, - { // right - fill: H.color(frame.bottom.color).brighten(-0.1).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zpp - }, { - x: xpp, - y: ypp, - z: zmm - }, { - x: xp, - y: yp, - z: zm - }, { - x: xp, - y: yp, - z: zp - }], - enabled: frame.bottom.visible && !frame.right.visible - }, - { // front - fill: H.color(frame.bottom.color).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zmm - }, { - x: xmm, - y: ypp, - z: zmm - }, { - x: xm, - y: yp, - z: zm - }, { - x: xp, - y: yp, - z: zm - }], - enabled: frame.bottom.visible && !frame.front.visible - }, - { // back - fill: H.color(frame.bottom.color).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zpp - }, { - x: xpp, - y: ypp, - z: zpp - }, { - x: xp, - y: yp, - z: zp - }, { - x: xm, - y: yp, - z: zp - }], - enabled: frame.bottom.visible && !frame.back.visible - }] - }); - this.frameShapes.top[verb]({ - 'class': 'highcharts-3d-frame highcharts-3d-frame-top', - zIndex: frame.top.frontFacing ? -1000 : 1000, - faces: [{ // bottom - fill: H.color(frame.top.color).brighten(0.1).get(), - vertexes: [{ - x: xmm, - y: ymm, - z: zpp - }, { - x: xpp, - y: ymm, - z: zpp - }, { - x: xpp, - y: ymm, - z: zmm - }, { - x: xmm, - y: ymm, - z: zmm - }], - enabled: frame.top.visible - }, - { // top - fill: H.color(frame.top.color).brighten(0.1).get(), - vertexes: [{ - x: xm, - y: ym, - z: zm - }, { - x: xp, - y: ym, - z: zm - }, { - x: xp, - y: ym, - z: zp - }, { - x: xm, - y: ym, - z: zp - }], - enabled: frame.top.visible - }, - { // left - fill: H.color(frame.top.color).brighten(-0.1).get(), - vertexes: [{ - x: xmm, - y: ymm, - z: zpp - }, { - x: xmm, - y: ymm, - z: zmm - }, { - x: xm, - y: ym, - z: zm - }, { - x: xm, - y: ym, - z: zp - }], - enabled: frame.top.visible && !frame.left.visible - }, - { // right - fill: H.color(frame.top.color).brighten(-0.1).get(), - vertexes: [{ - x: xpp, - y: ymm, - z: zmm - }, { - x: xpp, - y: ymm, - z: zpp - }, { - x: xp, - y: ym, - z: zp - }, { - x: xp, - y: ym, - z: zm - }], - enabled: frame.top.visible && !frame.right.visible - }, - { // front - fill: H.color(frame.top.color).get(), - vertexes: [{ - x: xmm, - y: ymm, - z: zmm - }, { - x: xpp, - y: ymm, - z: zmm - }, { - x: xp, - y: ym, - z: zm - }, { - x: xm, - y: ym, - z: zm - }], - enabled: frame.top.visible && !frame.front.visible - }, - { // back - fill: H.color(frame.top.color).get(), - vertexes: [{ - x: xpp, - y: ymm, - z: zpp - }, { - x: xmm, - y: ymm, - z: zpp - }, { - x: xm, - y: ym, - z: zp - }, { - x: xp, - y: ym, - z: zp - }], - enabled: frame.top.visible && !frame.back.visible - }] - }); - this.frameShapes.left[verb]({ - 'class': 'highcharts-3d-frame highcharts-3d-frame-left', - zIndex: frame.left.frontFacing ? -1000 : 1000, - faces: [{ // bottom - fill: H.color(frame.left.color).brighten(0.1).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zmm - }, { - x: xm, - y: yp, - z: zm - }, { - x: xm, - y: yp, - z: zp - }, { - x: xmm, - y: ypp, - z: zpp - }], - enabled: frame.left.visible && !frame.bottom.visible - }, - { // top - fill: H.color(frame.left.color).brighten(0.1).get(), - vertexes: [{ - x: xmm, - y: ymm, - z: zpp - }, { - x: xm, - y: ym, - z: zp - }, { - x: xm, - y: ym, - z: zm - }, { - x: xmm, - y: ymm, - z: zmm - }], - enabled: frame.left.visible && !frame.top.visible - }, - { // left - fill: H.color(frame.left.color).brighten(-0.1).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zpp - }, { - x: xmm, - y: ymm, - z: zpp - }, { - x: xmm, - y: ymm, - z: zmm - }, { - x: xmm, - y: ypp, - z: zmm - }], - enabled: frame.left.visible - }, - { // right - fill: H.color(frame.left.color).brighten(-0.1).get(), - vertexes: [{ - x: xm, - y: ym, - z: zp - }, { - x: xm, - y: yp, - z: zp - }, { - x: xm, - y: yp, - z: zm - }, { - x: xm, - y: ym, - z: zm - }], - enabled: frame.left.visible - }, - { // front - fill: H.color(frame.left.color).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zmm - }, { - x: xmm, - y: ymm, - z: zmm - }, { - x: xm, - y: ym, - z: zm - }, { - x: xm, - y: yp, - z: zm - }], - enabled: frame.left.visible && !frame.front.visible - }, - { // back - fill: H.color(frame.left.color).get(), - vertexes: [{ - x: xmm, - y: ymm, - z: zpp - }, { - x: xmm, - y: ypp, - z: zpp - }, { - x: xm, - y: yp, - z: zp - }, { - x: xm, - y: ym, - z: zp - }], - enabled: frame.left.visible && !frame.back.visible - }] - }); - this.frameShapes.right[verb]({ - 'class': 'highcharts-3d-frame highcharts-3d-frame-right', - zIndex: frame.right.frontFacing ? -1000 : 1000, - faces: [{ // bottom - fill: H.color(frame.right.color).brighten(0.1).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zpp - }, { - x: xp, - y: yp, - z: zp - }, { - x: xp, - y: yp, - z: zm - }, { - x: xpp, - y: ypp, - z: zmm - }], - enabled: frame.right.visible && !frame.bottom.visible - }, - { // top - fill: H.color(frame.right.color).brighten(0.1).get(), - vertexes: [{ - x: xpp, - y: ymm, - z: zmm - }, { - x: xp, - y: ym, - z: zm - }, { - x: xp, - y: ym, - z: zp - }, { - x: xpp, - y: ymm, - z: zpp - }], - enabled: frame.right.visible && !frame.top.visible - }, - { // left - fill: H.color(frame.right.color).brighten(-0.1).get(), - vertexes: [{ - x: xp, - y: ym, - z: zm - }, { - x: xp, - y: yp, - z: zm - }, { - x: xp, - y: yp, - z: zp - }, { - x: xp, - y: ym, - z: zp - }], - enabled: frame.right.visible - }, - { // right - fill: H.color(frame.right.color).brighten(-0.1).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zmm - }, { - x: xpp, - y: ymm, - z: zmm - }, { - x: xpp, - y: ymm, - z: zpp - }, { - x: xpp, - y: ypp, - z: zpp - }], - enabled: frame.right.visible - }, - { // front - fill: H.color(frame.right.color).get(), - vertexes: [{ - x: xpp, - y: ymm, - z: zmm - }, { - x: xpp, - y: ypp, - z: zmm - }, { - x: xp, - y: yp, - z: zm - }, { - x: xp, - y: ym, - z: zm - }], - enabled: frame.right.visible && !frame.front.visible - }, - { // back - fill: H.color(frame.right.color).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zpp - }, { - x: xpp, - y: ymm, - z: zpp - }, { - x: xp, - y: ym, - z: zp - }, { - x: xp, - y: yp, - z: zp - }], - enabled: frame.right.visible && !frame.back.visible - }] - }); - this.frameShapes.back[verb]({ - 'class': 'highcharts-3d-frame highcharts-3d-frame-back', - zIndex: frame.back.frontFacing ? -1000 : 1000, - faces: [{ // bottom - fill: H.color(frame.back.color).brighten(0.1).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zpp - }, { - x: xmm, - y: ypp, - z: zpp - }, { - x: xm, - y: yp, - z: zp - }, { - x: xp, - y: yp, - z: zp - }], - enabled: frame.back.visible && !frame.bottom.visible - }, - { // top - fill: H.color(frame.back.color).brighten(0.1).get(), - vertexes: [{ - x: xmm, - y: ymm, - z: zpp - }, { - x: xpp, - y: ymm, - z: zpp - }, { - x: xp, - y: ym, - z: zp - }, { - x: xm, - y: ym, - z: zp - }], - enabled: frame.back.visible && !frame.top.visible - }, - { // left - fill: H.color(frame.back.color).brighten(-0.1).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zpp - }, { - x: xmm, - y: ymm, - z: zpp - }, { - x: xm, - y: ym, - z: zp - }, { - x: xm, - y: yp, - z: zp - }], - enabled: frame.back.visible && !frame.left.visible - }, - { // right - fill: H.color(frame.back.color).brighten(-0.1).get(), - vertexes: [{ - x: xpp, - y: ymm, - z: zpp - }, { - x: xpp, - y: ypp, - z: zpp - }, { - x: xp, - y: yp, - z: zp - }, { - x: xp, - y: ym, - z: zp - }], - enabled: frame.back.visible && !frame.right.visible - }, - { // front - fill: H.color(frame.back.color).get(), - vertexes: [{ - x: xm, - y: ym, - z: zp - }, { - x: xp, - y: ym, - z: zp - }, { - x: xp, - y: yp, - z: zp - }, { - x: xm, - y: yp, - z: zp - }], - enabled: frame.back.visible - }, - { // back - fill: H.color(frame.back.color).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zpp - }, { - x: xpp, - y: ypp, - z: zpp - }, { - x: xpp, - y: ymm, - z: zpp - }, { - x: xmm, - y: ymm, - z: zpp - }], - enabled: frame.back.visible - }] - }); - this.frameShapes.front[verb]({ - 'class': 'highcharts-3d-frame highcharts-3d-frame-front', - zIndex: frame.front.frontFacing ? -1000 : 1000, - faces: [{ // bottom - fill: H.color(frame.front.color).brighten(0.1).get(), - vertexes: [{ - x: xmm, - y: ypp, - z: zmm - }, { - x: xpp, - y: ypp, - z: zmm - }, { - x: xp, - y: yp, - z: zm - }, { - x: xm, - y: yp, - z: zm - }], - enabled: frame.front.visible && !frame.bottom.visible - }, - { // top - fill: H.color(frame.front.color).brighten(0.1).get(), - vertexes: [{ - x: xpp, - y: ymm, - z: zmm - }, { - x: xmm, - y: ymm, - z: zmm - }, { - x: xm, - y: ym, - z: zm - }, { - x: xp, - y: ym, - z: zm - }], - enabled: frame.front.visible && !frame.top.visible - }, - { // left - fill: H.color(frame.front.color).brighten(-0.1).get(), - vertexes: [{ - x: xmm, - y: ymm, - z: zmm - }, { - x: xmm, - y: ypp, - z: zmm - }, { - x: xm, - y: yp, - z: zm - }, { - x: xm, - y: ym, - z: zm - }], - enabled: frame.front.visible && !frame.left.visible - }, - { // right - fill: H.color(frame.front.color).brighten(-0.1).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zmm - }, { - x: xpp, - y: ymm, - z: zmm - }, { - x: xp, - y: ym, - z: zm - }, { - x: xp, - y: yp, - z: zm - }], - enabled: frame.front.visible && !frame.right.visible - }, - { // front - fill: H.color(frame.front.color).get(), - vertexes: [{ - x: xp, - y: ym, - z: zm - }, { - x: xm, - y: ym, - z: zm - }, { - x: xm, - y: yp, - z: zm - }, { - x: xp, - y: yp, - z: zm - }], - enabled: frame.front.visible - }, - { // back - fill: H.color(frame.front.color).get(), - vertexes: [{ - x: xpp, - y: ypp, - z: zmm - }, { - x: xmm, - y: ypp, - z: zmm - }, { - x: xmm, - y: ymm, - z: zmm - }, { - x: xpp, - y: ymm, - z: zmm - }], - enabled: frame.front.visible - }] - }); - } + if (this.is3d()) { + var chart = this, + renderer = chart.renderer, + options3d = this.options.chart.options3d, + frame = chart.get3dFrame(), + xm = this.plotLeft, + xp = this.plotLeft + this.plotWidth, + ym = this.plotTop, + yp = this.plotTop + this.plotHeight, + zm = 0, + zp = options3d.depth, + xmm = xm - (frame.left.visible ? frame.left.size : 0), + xpp = xp + (frame.right.visible ? frame.right.size : 0), + ymm = ym - (frame.top.visible ? frame.top.size : 0), + ypp = yp + (frame.bottom.visible ? frame.bottom.size : 0), + zmm = zm - (frame.front.visible ? frame.front.size : 0), + zpp = zp + (frame.back.visible ? frame.back.size : 0), + verb = chart.hasRendered ? 'animate' : 'attr'; + + this.frame3d = frame; + + if (!this.frameShapes) { + this.frameShapes = { + bottom: renderer.polyhedron().add(), + top: renderer.polyhedron().add(), + left: renderer.polyhedron().add(), + right: renderer.polyhedron().add(), + back: renderer.polyhedron().add(), + front: renderer.polyhedron().add() + }; + } + this.frameShapes.bottom[verb]({ + 'class': 'highcharts-3d-frame highcharts-3d-frame-bottom', + zIndex: frame.bottom.frontFacing ? -1000 : 1000, + faces: [{ // bottom + fill: H.color(frame.bottom.color).brighten(0.1).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zmm + }, { + x: xpp, + y: ypp, + z: zmm + }, { + x: xpp, + y: ypp, + z: zpp + }, { + x: xmm, + y: ypp, + z: zpp + }], + enabled: frame.bottom.visible + }, + { // top + fill: H.color(frame.bottom.color).brighten(0.1).get(), + vertexes: [{ + x: xm, + y: yp, + z: zp + }, { + x: xp, + y: yp, + z: zp + }, { + x: xp, + y: yp, + z: zm + }, { + x: xm, + y: yp, + z: zm + }], + enabled: frame.bottom.visible + }, + { // left + fill: H.color(frame.bottom.color).brighten(-0.1).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zmm + }, { + x: xmm, + y: ypp, + z: zpp + }, { + x: xm, + y: yp, + z: zp + }, { + x: xm, + y: yp, + z: zm + }], + enabled: frame.bottom.visible && !frame.left.visible + }, + { // right + fill: H.color(frame.bottom.color).brighten(-0.1).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zpp + }, { + x: xpp, + y: ypp, + z: zmm + }, { + x: xp, + y: yp, + z: zm + }, { + x: xp, + y: yp, + z: zp + }], + enabled: frame.bottom.visible && !frame.right.visible + }, + { // front + fill: H.color(frame.bottom.color).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zmm + }, { + x: xmm, + y: ypp, + z: zmm + }, { + x: xm, + y: yp, + z: zm + }, { + x: xp, + y: yp, + z: zm + }], + enabled: frame.bottom.visible && !frame.front.visible + }, + { // back + fill: H.color(frame.bottom.color).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zpp + }, { + x: xpp, + y: ypp, + z: zpp + }, { + x: xp, + y: yp, + z: zp + }, { + x: xm, + y: yp, + z: zp + }], + enabled: frame.bottom.visible && !frame.back.visible + }] + }); + this.frameShapes.top[verb]({ + 'class': 'highcharts-3d-frame highcharts-3d-frame-top', + zIndex: frame.top.frontFacing ? -1000 : 1000, + faces: [{ // bottom + fill: H.color(frame.top.color).brighten(0.1).get(), + vertexes: [{ + x: xmm, + y: ymm, + z: zpp + }, { + x: xpp, + y: ymm, + z: zpp + }, { + x: xpp, + y: ymm, + z: zmm + }, { + x: xmm, + y: ymm, + z: zmm + }], + enabled: frame.top.visible + }, + { // top + fill: H.color(frame.top.color).brighten(0.1).get(), + vertexes: [{ + x: xm, + y: ym, + z: zm + }, { + x: xp, + y: ym, + z: zm + }, { + x: xp, + y: ym, + z: zp + }, { + x: xm, + y: ym, + z: zp + }], + enabled: frame.top.visible + }, + { // left + fill: H.color(frame.top.color).brighten(-0.1).get(), + vertexes: [{ + x: xmm, + y: ymm, + z: zpp + }, { + x: xmm, + y: ymm, + z: zmm + }, { + x: xm, + y: ym, + z: zm + }, { + x: xm, + y: ym, + z: zp + }], + enabled: frame.top.visible && !frame.left.visible + }, + { // right + fill: H.color(frame.top.color).brighten(-0.1).get(), + vertexes: [{ + x: xpp, + y: ymm, + z: zmm + }, { + x: xpp, + y: ymm, + z: zpp + }, { + x: xp, + y: ym, + z: zp + }, { + x: xp, + y: ym, + z: zm + }], + enabled: frame.top.visible && !frame.right.visible + }, + { // front + fill: H.color(frame.top.color).get(), + vertexes: [{ + x: xmm, + y: ymm, + z: zmm + }, { + x: xpp, + y: ymm, + z: zmm + }, { + x: xp, + y: ym, + z: zm + }, { + x: xm, + y: ym, + z: zm + }], + enabled: frame.top.visible && !frame.front.visible + }, + { // back + fill: H.color(frame.top.color).get(), + vertexes: [{ + x: xpp, + y: ymm, + z: zpp + }, { + x: xmm, + y: ymm, + z: zpp + }, { + x: xm, + y: ym, + z: zp + }, { + x: xp, + y: ym, + z: zp + }], + enabled: frame.top.visible && !frame.back.visible + }] + }); + this.frameShapes.left[verb]({ + 'class': 'highcharts-3d-frame highcharts-3d-frame-left', + zIndex: frame.left.frontFacing ? -1000 : 1000, + faces: [{ // bottom + fill: H.color(frame.left.color).brighten(0.1).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zmm + }, { + x: xm, + y: yp, + z: zm + }, { + x: xm, + y: yp, + z: zp + }, { + x: xmm, + y: ypp, + z: zpp + }], + enabled: frame.left.visible && !frame.bottom.visible + }, + { // top + fill: H.color(frame.left.color).brighten(0.1).get(), + vertexes: [{ + x: xmm, + y: ymm, + z: zpp + }, { + x: xm, + y: ym, + z: zp + }, { + x: xm, + y: ym, + z: zm + }, { + x: xmm, + y: ymm, + z: zmm + }], + enabled: frame.left.visible && !frame.top.visible + }, + { // left + fill: H.color(frame.left.color).brighten(-0.1).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zpp + }, { + x: xmm, + y: ymm, + z: zpp + }, { + x: xmm, + y: ymm, + z: zmm + }, { + x: xmm, + y: ypp, + z: zmm + }], + enabled: frame.left.visible + }, + { // right + fill: H.color(frame.left.color).brighten(-0.1).get(), + vertexes: [{ + x: xm, + y: ym, + z: zp + }, { + x: xm, + y: yp, + z: zp + }, { + x: xm, + y: yp, + z: zm + }, { + x: xm, + y: ym, + z: zm + }], + enabled: frame.left.visible + }, + { // front + fill: H.color(frame.left.color).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zmm + }, { + x: xmm, + y: ymm, + z: zmm + }, { + x: xm, + y: ym, + z: zm + }, { + x: xm, + y: yp, + z: zm + }], + enabled: frame.left.visible && !frame.front.visible + }, + { // back + fill: H.color(frame.left.color).get(), + vertexes: [{ + x: xmm, + y: ymm, + z: zpp + }, { + x: xmm, + y: ypp, + z: zpp + }, { + x: xm, + y: yp, + z: zp + }, { + x: xm, + y: ym, + z: zp + }], + enabled: frame.left.visible && !frame.back.visible + }] + }); + this.frameShapes.right[verb]({ + 'class': 'highcharts-3d-frame highcharts-3d-frame-right', + zIndex: frame.right.frontFacing ? -1000 : 1000, + faces: [{ // bottom + fill: H.color(frame.right.color).brighten(0.1).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zpp + }, { + x: xp, + y: yp, + z: zp + }, { + x: xp, + y: yp, + z: zm + }, { + x: xpp, + y: ypp, + z: zmm + }], + enabled: frame.right.visible && !frame.bottom.visible + }, + { // top + fill: H.color(frame.right.color).brighten(0.1).get(), + vertexes: [{ + x: xpp, + y: ymm, + z: zmm + }, { + x: xp, + y: ym, + z: zm + }, { + x: xp, + y: ym, + z: zp + }, { + x: xpp, + y: ymm, + z: zpp + }], + enabled: frame.right.visible && !frame.top.visible + }, + { // left + fill: H.color(frame.right.color).brighten(-0.1).get(), + vertexes: [{ + x: xp, + y: ym, + z: zm + }, { + x: xp, + y: yp, + z: zm + }, { + x: xp, + y: yp, + z: zp + }, { + x: xp, + y: ym, + z: zp + }], + enabled: frame.right.visible + }, + { // right + fill: H.color(frame.right.color).brighten(-0.1).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zmm + }, { + x: xpp, + y: ymm, + z: zmm + }, { + x: xpp, + y: ymm, + z: zpp + }, { + x: xpp, + y: ypp, + z: zpp + }], + enabled: frame.right.visible + }, + { // front + fill: H.color(frame.right.color).get(), + vertexes: [{ + x: xpp, + y: ymm, + z: zmm + }, { + x: xpp, + y: ypp, + z: zmm + }, { + x: xp, + y: yp, + z: zm + }, { + x: xp, + y: ym, + z: zm + }], + enabled: frame.right.visible && !frame.front.visible + }, + { // back + fill: H.color(frame.right.color).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zpp + }, { + x: xpp, + y: ymm, + z: zpp + }, { + x: xp, + y: ym, + z: zp + }, { + x: xp, + y: yp, + z: zp + }], + enabled: frame.right.visible && !frame.back.visible + }] + }); + this.frameShapes.back[verb]({ + 'class': 'highcharts-3d-frame highcharts-3d-frame-back', + zIndex: frame.back.frontFacing ? -1000 : 1000, + faces: [{ // bottom + fill: H.color(frame.back.color).brighten(0.1).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zpp + }, { + x: xmm, + y: ypp, + z: zpp + }, { + x: xm, + y: yp, + z: zp + }, { + x: xp, + y: yp, + z: zp + }], + enabled: frame.back.visible && !frame.bottom.visible + }, + { // top + fill: H.color(frame.back.color).brighten(0.1).get(), + vertexes: [{ + x: xmm, + y: ymm, + z: zpp + }, { + x: xpp, + y: ymm, + z: zpp + }, { + x: xp, + y: ym, + z: zp + }, { + x: xm, + y: ym, + z: zp + }], + enabled: frame.back.visible && !frame.top.visible + }, + { // left + fill: H.color(frame.back.color).brighten(-0.1).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zpp + }, { + x: xmm, + y: ymm, + z: zpp + }, { + x: xm, + y: ym, + z: zp + }, { + x: xm, + y: yp, + z: zp + }], + enabled: frame.back.visible && !frame.left.visible + }, + { // right + fill: H.color(frame.back.color).brighten(-0.1).get(), + vertexes: [{ + x: xpp, + y: ymm, + z: zpp + }, { + x: xpp, + y: ypp, + z: zpp + }, { + x: xp, + y: yp, + z: zp + }, { + x: xp, + y: ym, + z: zp + }], + enabled: frame.back.visible && !frame.right.visible + }, + { // front + fill: H.color(frame.back.color).get(), + vertexes: [{ + x: xm, + y: ym, + z: zp + }, { + x: xp, + y: ym, + z: zp + }, { + x: xp, + y: yp, + z: zp + }, { + x: xm, + y: yp, + z: zp + }], + enabled: frame.back.visible + }, + { // back + fill: H.color(frame.back.color).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zpp + }, { + x: xpp, + y: ypp, + z: zpp + }, { + x: xpp, + y: ymm, + z: zpp + }, { + x: xmm, + y: ymm, + z: zpp + }], + enabled: frame.back.visible + }] + }); + this.frameShapes.front[verb]({ + 'class': 'highcharts-3d-frame highcharts-3d-frame-front', + zIndex: frame.front.frontFacing ? -1000 : 1000, + faces: [{ // bottom + fill: H.color(frame.front.color).brighten(0.1).get(), + vertexes: [{ + x: xmm, + y: ypp, + z: zmm + }, { + x: xpp, + y: ypp, + z: zmm + }, { + x: xp, + y: yp, + z: zm + }, { + x: xm, + y: yp, + z: zm + }], + enabled: frame.front.visible && !frame.bottom.visible + }, + { // top + fill: H.color(frame.front.color).brighten(0.1).get(), + vertexes: [{ + x: xpp, + y: ymm, + z: zmm + }, { + x: xmm, + y: ymm, + z: zmm + }, { + x: xm, + y: ym, + z: zm + }, { + x: xp, + y: ym, + z: zm + }], + enabled: frame.front.visible && !frame.top.visible + }, + { // left + fill: H.color(frame.front.color).brighten(-0.1).get(), + vertexes: [{ + x: xmm, + y: ymm, + z: zmm + }, { + x: xmm, + y: ypp, + z: zmm + }, { + x: xm, + y: yp, + z: zm + }, { + x: xm, + y: ym, + z: zm + }], + enabled: frame.front.visible && !frame.left.visible + }, + { // right + fill: H.color(frame.front.color).brighten(-0.1).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zmm + }, { + x: xpp, + y: ymm, + z: zmm + }, { + x: xp, + y: ym, + z: zm + }, { + x: xp, + y: yp, + z: zm + }], + enabled: frame.front.visible && !frame.right.visible + }, + { // front + fill: H.color(frame.front.color).get(), + vertexes: [{ + x: xp, + y: ym, + z: zm + }, { + x: xm, + y: ym, + z: zm + }, { + x: xm, + y: yp, + z: zm + }, { + x: xp, + y: yp, + z: zm + }], + enabled: frame.front.visible + }, + { // back + fill: H.color(frame.front.color).get(), + vertexes: [{ + x: xpp, + y: ypp, + z: zmm + }, { + x: xmm, + y: ypp, + z: zmm + }, { + x: xmm, + y: ymm, + z: zmm + }, { + x: xpp, + y: ymm, + z: zmm + }], + enabled: frame.front.visible + }] + }); + } }); Chart.prototype.retrieveStacks = function (stacking) { - var series = this.series, - stacks = {}, - stackNumber, - i = 1; - - each(this.series, function (s) { - stackNumber = pick( - s.options.stack, - (stacking ? 0 : series.length - 1 - s.index) - ); // #3841, #4532 - if (!stacks[stackNumber]) { - stacks[stackNumber] = { series: [s], position: i }; - i++; - } else { - stacks[stackNumber].series.push(s); - } - }); - - stacks.totalStacks = i + 1; - return stacks; + var series = this.series, + stacks = {}, + stackNumber, + i = 1; + + each(this.series, function (s) { + stackNumber = pick( + s.options.stack, + (stacking ? 0 : series.length - 1 - s.index) + ); // #3841, #4532 + if (!stacks[stackNumber]) { + stacks[stackNumber] = { series: [s], position: i }; + i++; + } else { + stacks[stackNumber].series.push(s); + } + }); + + stacks.totalStacks = i + 1; + return stacks; }; Chart.prototype.get3dFrame = function () { - var chart = this, - options3d = chart.options.chart.options3d, - frameOptions = options3d.frame, - xm = chart.plotLeft, - xp = chart.plotLeft + chart.plotWidth, - ym = chart.plotTop, - yp = chart.plotTop + chart.plotHeight, - zm = 0, - zp = options3d.depth, - faceOrientation = function (vertexes) { - var area = H.shapeArea3d(vertexes, chart); - // Give it 0.5 squared-pixel as a margin for rounding errors. - if (area > 0.5) { - return 1; - } - if (area < -0.5) { - return -1; - } - return 0; - }, - bottomOrientation = faceOrientation([ - { x: xm, y: yp, z: zp }, - { x: xp, y: yp, z: zp }, - { x: xp, y: yp, z: zm }, - { x: xm, y: yp, z: zm } - ]), - topOrientation = faceOrientation([ - { x: xm, y: ym, z: zm }, - { x: xp, y: ym, z: zm }, - { x: xp, y: ym, z: zp }, - { x: xm, y: ym, z: zp } - ]), - leftOrientation = faceOrientation([ - { x: xm, y: ym, z: zm }, - { x: xm, y: ym, z: zp }, - { x: xm, y: yp, z: zp }, - { x: xm, y: yp, z: zm } - ]), - rightOrientation = faceOrientation([ - { x: xp, y: ym, z: zp }, - { x: xp, y: ym, z: zm }, - { x: xp, y: yp, z: zm }, - { x: xp, y: yp, z: zp } - ]), - frontOrientation = faceOrientation([ - { x: xm, y: yp, z: zm }, - { x: xp, y: yp, z: zm }, - { x: xp, y: ym, z: zm }, - { x: xm, y: ym, z: zm } - ]), - backOrientation = faceOrientation([ - { x: xm, y: ym, z: zp }, - { x: xp, y: ym, z: zp }, - { x: xp, y: yp, z: zp }, - { x: xm, y: yp, z: zp } - ]), - defaultShowBottom = false, - defaultShowTop = false, - defaultShowLeft = false, - defaultShowRight = false, - defaultShowFront = false, - defaultShowBack = true; - - // The 'default' criteria to visible faces of the frame is looking up every - // axis to decide whenever the left/right//top/bottom sides of the frame - // will be shown - each([].concat(chart.xAxis, chart.yAxis, chart.zAxis), function (axis) { - if (axis) { - if (axis.horiz) { - if (axis.opposite) { - defaultShowTop = true; - } else { - defaultShowBottom = true; - } - } else { - if (axis.opposite) { - defaultShowRight = true; - } else { - defaultShowLeft = true; - } - } - } - }); - - var getFaceOptions = function (sources, faceOrientation, defaultVisible) { - var faceAttrs = ['size', 'color', 'visible']; - var options = {}; - for (var i = 0; i < faceAttrs.length; i++) { - var attr = faceAttrs[i]; - for (var j = 0; j < sources.length; j++) { - if (typeof sources[j] === 'object') { - var val = sources[j][attr]; - if (val !== undefined && val !== null) { - options[attr] = val; - break; - } - } - } - } - var isVisible = defaultVisible; - if (options.visible === true || options.visible === false) { - isVisible = options.visible; - } else if (options.visible === 'auto') { - isVisible = faceOrientation > 0; - } - - return { - size: pick(options.size, 1), - color: pick(options.color, 'none'), - frontFacing: faceOrientation > 0, - visible: isVisible - }; - }; - - // docs @TODO: Add all frame options (left, right, top, bottom, front, back) - // to apioptions JSDoc once the new system is up. - var ret = { - // FIXME: Previously, left/right, top/bottom and front/back pairs shared - // size and color. - // For compatibility and consistency sake, when one face have - // size/color/visibility set, the opposite face will default to the same - // values. Also, left/right used to be called 'side', so that's also - // added as a fallback - bottom: getFaceOptions( - [frameOptions.bottom, frameOptions.top, frameOptions], - bottomOrientation, - defaultShowBottom - ), - top: getFaceOptions( - [frameOptions.top, frameOptions.bottom, frameOptions], - topOrientation, - defaultShowTop - ), - left: getFaceOptions( - [ - frameOptions.left, - frameOptions.right, - frameOptions.side, - frameOptions - ], - leftOrientation, - defaultShowLeft - ), - right: getFaceOptions( - [ - frameOptions.right, - frameOptions.left, - frameOptions.side, - frameOptions - ], - rightOrientation, - defaultShowRight - ), - back: getFaceOptions( - [frameOptions.back, frameOptions.front, frameOptions], - backOrientation, - defaultShowBack - ), - front: getFaceOptions( - [frameOptions.front, frameOptions.back, frameOptions], - frontOrientation, - defaultShowFront - ) - }; - - - // Decide the bast place to put axis title/labels based on the visible - // faces. Ideally, The labels can only be on the edge between a visible face - // and an invisble one. Also, the Y label should be one the left-most edge - // (right-most if opposite), - if (options3d.axisLabelPosition === 'auto') { - var isValidEdge = function (face1, face2) { - return ( - (face1.visible !== face2.visible) || - ( - face1.visible && - face2.visible && - (face1.frontFacing !== face2.frontFacing) - ) - ); - }; - - var yEdges = []; - if (isValidEdge(ret.left, ret.front)) { - yEdges.push({ - y: (ym + yp) / 2, - x: xm, - z: zm, - xDir: { x: 1, y: 0, z: 0 } - }); - } - if (isValidEdge(ret.left, ret.back)) { - yEdges.push({ - y: (ym + yp) / 2, - x: xm, - z: zp, - xDir: { x: 0, y: 0, z: -1 } - }); - } - if (isValidEdge(ret.right, ret.front)) { - yEdges.push({ - y: (ym + yp) / 2, - x: xp, - z: zm, - xDir: { x: 0, y: 0, z: 1 } - }); - } - if (isValidEdge(ret.right, ret.back)) { - yEdges.push({ - y: (ym + yp) / 2, - x: xp, - z: zp, - xDir: { x: -1, y: 0, z: 0 } - }); - } - - var xBottomEdges = []; - if (isValidEdge(ret.bottom, ret.front)) { - xBottomEdges.push({ - x: (xm + xp) / 2, - y: yp, - z: zm, - xDir: { x: 1, y: 0, z: 0 } - }); - } - if (isValidEdge(ret.bottom, ret.back)) { - xBottomEdges.push({ - x: (xm + xp) / 2, - y: yp, - z: zp, - xDir: { x: -1, y: 0, z: 0 } - }); - } - - var xTopEdges = []; - if (isValidEdge(ret.top, ret.front)) { - xTopEdges.push({ - x: (xm + xp) / 2, - y: ym, - z: zm, - xDir: { x: 1, y: 0, z: 0 } - }); - } - if (isValidEdge(ret.top, ret.back)) { - xTopEdges.push({ - x: (xm + xp) / 2, - y: ym, - z: zp, - xDir: { x: -1, y: 0, z: 0 } - }); - } - - var zBottomEdges = []; - if (isValidEdge(ret.bottom, ret.left)) { - zBottomEdges.push({ - z: (zm + zp) / 2, - y: yp, - x: xm, - xDir: { x: 0, y: 0, z: -1 } - }); - } - if (isValidEdge(ret.bottom, ret.right)) { - zBottomEdges.push({ - z: (zm + zp) / 2, - y: yp, - x: xp, - xDir: { x: 0, y: 0, z: 1 } - }); - } - - var zTopEdges = []; - if (isValidEdge(ret.top, ret.left)) { - zTopEdges.push({ - z: (zm + zp) / 2, - y: ym, - x: xm, - xDir: { x: 0, y: 0, z: -1 } - }); - } - if (isValidEdge(ret.top, ret.right)) { - zTopEdges.push({ - z: (zm + zp) / 2, - y: ym, - x: xp, - xDir: { x: 0, y: 0, z: 1 } - }); - } - - var pickEdge = function (edges, axis, mult) { - if (edges.length === 0) { - return null; - } else if (edges.length === 1) { - return edges[0]; - } - var best = 0, - projections = perspective(edges, chart, false); - for (var i = 1; i < projections.length; i++) { - if ( - mult * projections[i][axis] > - mult * projections[best][axis] - ) { - best = i; - } else if ( - ( - mult * projections[i][axis] === - mult * projections[best][axis] - ) && - (projections[i].z < projections[best].z) - ) { - best = i; - } - } - return edges[best]; - }; - ret.axes = { - y: { - 'left': pickEdge(yEdges, 'x', -1), - 'right': pickEdge(yEdges, 'x', +1) - }, - x: { - 'top': pickEdge(xTopEdges, 'y', -1), - 'bottom': pickEdge(xBottomEdges, 'y', +1) - }, - z: { - 'top': pickEdge(zTopEdges, 'y', -1), - 'bottom': pickEdge(zBottomEdges, 'y', +1) - } - }; - } else { - ret.axes = { - y: { - 'left': { x: xm, z: zm, xDir: { x: 1, y: 0, z: 0 } }, - 'right': { x: xp, z: zm, xDir: { x: 0, y: 0, z: 1 } } - }, - x: { - 'top': { y: ym, z: zm, xDir: { x: 1, y: 0, z: 0 } }, - 'bottom': { y: yp, z: zm, xDir: { x: 1, y: 0, z: 0 } } - }, - z: { - 'top': { - x: defaultShowLeft ? xp : xm, - y: ym, - xDir: defaultShowLeft ? - { x: 0, y: 0, z: 1 } : - { x: 0, y: 0, z: -1 } - }, - 'bottom': { - x: defaultShowLeft ? xp : xm, - y: yp, - xDir: defaultShowLeft ? - { x: 0, y: 0, z: 1 } : - { x: 0, y: 0, z: -1 } - } - } - }; - } - - return ret; + var chart = this, + options3d = chart.options.chart.options3d, + frameOptions = options3d.frame, + xm = chart.plotLeft, + xp = chart.plotLeft + chart.plotWidth, + ym = chart.plotTop, + yp = chart.plotTop + chart.plotHeight, + zm = 0, + zp = options3d.depth, + faceOrientation = function (vertexes) { + var area = H.shapeArea3d(vertexes, chart); + // Give it 0.5 squared-pixel as a margin for rounding errors. + if (area > 0.5) { + return 1; + } + if (area < -0.5) { + return -1; + } + return 0; + }, + bottomOrientation = faceOrientation([ + { x: xm, y: yp, z: zp }, + { x: xp, y: yp, z: zp }, + { x: xp, y: yp, z: zm }, + { x: xm, y: yp, z: zm } + ]), + topOrientation = faceOrientation([ + { x: xm, y: ym, z: zm }, + { x: xp, y: ym, z: zm }, + { x: xp, y: ym, z: zp }, + { x: xm, y: ym, z: zp } + ]), + leftOrientation = faceOrientation([ + { x: xm, y: ym, z: zm }, + { x: xm, y: ym, z: zp }, + { x: xm, y: yp, z: zp }, + { x: xm, y: yp, z: zm } + ]), + rightOrientation = faceOrientation([ + { x: xp, y: ym, z: zp }, + { x: xp, y: ym, z: zm }, + { x: xp, y: yp, z: zm }, + { x: xp, y: yp, z: zp } + ]), + frontOrientation = faceOrientation([ + { x: xm, y: yp, z: zm }, + { x: xp, y: yp, z: zm }, + { x: xp, y: ym, z: zm }, + { x: xm, y: ym, z: zm } + ]), + backOrientation = faceOrientation([ + { x: xm, y: ym, z: zp }, + { x: xp, y: ym, z: zp }, + { x: xp, y: yp, z: zp }, + { x: xm, y: yp, z: zp } + ]), + defaultShowBottom = false, + defaultShowTop = false, + defaultShowLeft = false, + defaultShowRight = false, + defaultShowFront = false, + defaultShowBack = true; + + // The 'default' criteria to visible faces of the frame is looking up every + // axis to decide whenever the left/right//top/bottom sides of the frame + // will be shown + each([].concat(chart.xAxis, chart.yAxis, chart.zAxis), function (axis) { + if (axis) { + if (axis.horiz) { + if (axis.opposite) { + defaultShowTop = true; + } else { + defaultShowBottom = true; + } + } else { + if (axis.opposite) { + defaultShowRight = true; + } else { + defaultShowLeft = true; + } + } + } + }); + + var getFaceOptions = function (sources, faceOrientation, defaultVisible) { + var faceAttrs = ['size', 'color', 'visible']; + var options = {}; + for (var i = 0; i < faceAttrs.length; i++) { + var attr = faceAttrs[i]; + for (var j = 0; j < sources.length; j++) { + if (typeof sources[j] === 'object') { + var val = sources[j][attr]; + if (val !== undefined && val !== null) { + options[attr] = val; + break; + } + } + } + } + var isVisible = defaultVisible; + if (options.visible === true || options.visible === false) { + isVisible = options.visible; + } else if (options.visible === 'auto') { + isVisible = faceOrientation > 0; + } + + return { + size: pick(options.size, 1), + color: pick(options.color, 'none'), + frontFacing: faceOrientation > 0, + visible: isVisible + }; + }; + + // docs @TODO: Add all frame options (left, right, top, bottom, front, back) + // to apioptions JSDoc once the new system is up. + var ret = { + // FIXME: Previously, left/right, top/bottom and front/back pairs shared + // size and color. + // For compatibility and consistency sake, when one face have + // size/color/visibility set, the opposite face will default to the same + // values. Also, left/right used to be called 'side', so that's also + // added as a fallback + bottom: getFaceOptions( + [frameOptions.bottom, frameOptions.top, frameOptions], + bottomOrientation, + defaultShowBottom + ), + top: getFaceOptions( + [frameOptions.top, frameOptions.bottom, frameOptions], + topOrientation, + defaultShowTop + ), + left: getFaceOptions( + [ + frameOptions.left, + frameOptions.right, + frameOptions.side, + frameOptions + ], + leftOrientation, + defaultShowLeft + ), + right: getFaceOptions( + [ + frameOptions.right, + frameOptions.left, + frameOptions.side, + frameOptions + ], + rightOrientation, + defaultShowRight + ), + back: getFaceOptions( + [frameOptions.back, frameOptions.front, frameOptions], + backOrientation, + defaultShowBack + ), + front: getFaceOptions( + [frameOptions.front, frameOptions.back, frameOptions], + frontOrientation, + defaultShowFront + ) + }; + + + // Decide the bast place to put axis title/labels based on the visible + // faces. Ideally, The labels can only be on the edge between a visible face + // and an invisble one. Also, the Y label should be one the left-most edge + // (right-most if opposite), + if (options3d.axisLabelPosition === 'auto') { + var isValidEdge = function (face1, face2) { + return ( + (face1.visible !== face2.visible) || + ( + face1.visible && + face2.visible && + (face1.frontFacing !== face2.frontFacing) + ) + ); + }; + + var yEdges = []; + if (isValidEdge(ret.left, ret.front)) { + yEdges.push({ + y: (ym + yp) / 2, + x: xm, + z: zm, + xDir: { x: 1, y: 0, z: 0 } + }); + } + if (isValidEdge(ret.left, ret.back)) { + yEdges.push({ + y: (ym + yp) / 2, + x: xm, + z: zp, + xDir: { x: 0, y: 0, z: -1 } + }); + } + if (isValidEdge(ret.right, ret.front)) { + yEdges.push({ + y: (ym + yp) / 2, + x: xp, + z: zm, + xDir: { x: 0, y: 0, z: 1 } + }); + } + if (isValidEdge(ret.right, ret.back)) { + yEdges.push({ + y: (ym + yp) / 2, + x: xp, + z: zp, + xDir: { x: -1, y: 0, z: 0 } + }); + } + + var xBottomEdges = []; + if (isValidEdge(ret.bottom, ret.front)) { + xBottomEdges.push({ + x: (xm + xp) / 2, + y: yp, + z: zm, + xDir: { x: 1, y: 0, z: 0 } + }); + } + if (isValidEdge(ret.bottom, ret.back)) { + xBottomEdges.push({ + x: (xm + xp) / 2, + y: yp, + z: zp, + xDir: { x: -1, y: 0, z: 0 } + }); + } + + var xTopEdges = []; + if (isValidEdge(ret.top, ret.front)) { + xTopEdges.push({ + x: (xm + xp) / 2, + y: ym, + z: zm, + xDir: { x: 1, y: 0, z: 0 } + }); + } + if (isValidEdge(ret.top, ret.back)) { + xTopEdges.push({ + x: (xm + xp) / 2, + y: ym, + z: zp, + xDir: { x: -1, y: 0, z: 0 } + }); + } + + var zBottomEdges = []; + if (isValidEdge(ret.bottom, ret.left)) { + zBottomEdges.push({ + z: (zm + zp) / 2, + y: yp, + x: xm, + xDir: { x: 0, y: 0, z: -1 } + }); + } + if (isValidEdge(ret.bottom, ret.right)) { + zBottomEdges.push({ + z: (zm + zp) / 2, + y: yp, + x: xp, + xDir: { x: 0, y: 0, z: 1 } + }); + } + + var zTopEdges = []; + if (isValidEdge(ret.top, ret.left)) { + zTopEdges.push({ + z: (zm + zp) / 2, + y: ym, + x: xm, + xDir: { x: 0, y: 0, z: -1 } + }); + } + if (isValidEdge(ret.top, ret.right)) { + zTopEdges.push({ + z: (zm + zp) / 2, + y: ym, + x: xp, + xDir: { x: 0, y: 0, z: 1 } + }); + } + + var pickEdge = function (edges, axis, mult) { + if (edges.length === 0) { + return null; + } else if (edges.length === 1) { + return edges[0]; + } + var best = 0, + projections = perspective(edges, chart, false); + for (var i = 1; i < projections.length; i++) { + if ( + mult * projections[i][axis] > + mult * projections[best][axis] + ) { + best = i; + } else if ( + ( + mult * projections[i][axis] === + mult * projections[best][axis] + ) && + (projections[i].z < projections[best].z) + ) { + best = i; + } + } + return edges[best]; + }; + ret.axes = { + y: { + 'left': pickEdge(yEdges, 'x', -1), + 'right': pickEdge(yEdges, 'x', +1) + }, + x: { + 'top': pickEdge(xTopEdges, 'y', -1), + 'bottom': pickEdge(xBottomEdges, 'y', +1) + }, + z: { + 'top': pickEdge(zTopEdges, 'y', -1), + 'bottom': pickEdge(zBottomEdges, 'y', +1) + } + }; + } else { + ret.axes = { + y: { + 'left': { x: xm, z: zm, xDir: { x: 1, y: 0, z: 0 } }, + 'right': { x: xp, z: zm, xDir: { x: 0, y: 0, z: 1 } } + }, + x: { + 'top': { y: ym, z: zm, xDir: { x: 1, y: 0, z: 0 } }, + 'bottom': { y: yp, z: zm, xDir: { x: 1, y: 0, z: 0 } } + }, + z: { + 'top': { + x: defaultShowLeft ? xp : xm, + y: ym, + xDir: defaultShowLeft ? + { x: 0, y: 0, z: 1 } : + { x: 0, y: 0, z: -1 } + }, + 'bottom': { + x: defaultShowLeft ? xp : xm, + y: yp, + xDir: defaultShowLeft ? + { x: 0, y: 0, z: 1 } : + { x: 0, y: 0, z: -1 } + } + } + }; + } + + return ret; }; /** * Animation setter for matrix property. */ H.Fx.prototype.matrixSetter = function () { - var interpolated; - if (this.pos < 1 && - (H.isArray(this.start) || H.isArray(this.end))) { - var start = this.start || [ 1, 0, 0, 1, 0, 0]; - var end = this.end || [ 1, 0, 0, 1, 0, 0]; - interpolated = []; - for (var i = 0; i < 6; i++) { - interpolated.push(this.pos * end[i] + (1 - this.pos) * start[i]); - } - } else { - interpolated = this.end; - } - - this.elem.attr( - this.prop, - interpolated, - null, - true - ); + var interpolated; + if (this.pos < 1 && + (H.isArray(this.start) || H.isArray(this.end))) { + var start = this.start || [ 1, 0, 0, 1, 0, 0]; + var end = this.end || [ 1, 0, 0, 1, 0, 0]; + interpolated = []; + for (var i = 0; i < 6; i++) { + interpolated.push(this.pos * end[i] + (1 - this.pos) * start[i]); + } + } else { + interpolated = this.end; + } + + this.elem.attr( + this.prop, + interpolated, + null, + true + ); }; /** * Note: As of v5.0.12, `frame.left` or `frame.right` should be used * instead. - * + * * The side for the frame around a 3D chart. - * + * * @since 4.0 * @product highcharts * @apioption chart.options3d.frame.side @@ -1690,7 +1690,7 @@ H.Fx.prototype.matrixSetter = function () { /** * The color of the panel. - * + * * @type {Color} * @default transparent * @since 4.0 @@ -1700,7 +1700,7 @@ H.Fx.prototype.matrixSetter = function () { /** * The thickness of the panel. - * + * * @type {Number} * @default 1 * @since 4.0 diff --git a/js/parts-3d/Column.js b/js/parts-3d/Column.js index 15a7aea8963..ce704d7e9db 100644 --- a/js/parts-3d/Column.js +++ b/js/parts-3d/Column.js @@ -8,20 +8,20 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Series.js'; var addEvent = H.addEvent, - each = H.each, - perspective = H.perspective, - pick = H.pick, - Series = H.Series, - seriesTypes = H.seriesTypes, - inArray = H.inArray, - svg = H.svg, - wrap = H.wrap; + each = H.each, + perspective = H.perspective, + pick = H.pick, + Series = H.Series, + seriesTypes = H.seriesTypes, + inArray = H.inArray, + svg = H.svg, + wrap = H.wrap; /** * Depth of the columns in a 3D column chart. Requires `highcharts-3d.js`. - * + * * @type {Number} * @default 25 * @since 4.0 @@ -32,7 +32,7 @@ var addEvent = H.addEvent, /** * 3D columns only. The color of the edges. Similar to `borderColor`, * except it defaults to the same color as the column. - * + * * @type {Color} * @product highcharts * @apioption plotOptions.column.edgeColor @@ -40,7 +40,7 @@ var addEvent = H.addEvent, /** * 3D columns only. The width of the colored edges. - * + * * @type {Number} * @default 1 * @product highcharts @@ -50,7 +50,7 @@ var addEvent = H.addEvent, /** * The spacing between columns on the Z Axis in a 3D chart. Requires * `highcharts-3d.js`. - * + * * @type {Number} * @default 1 * @since 4.0 @@ -59,176 +59,176 @@ var addEvent = H.addEvent, */ wrap(seriesTypes.column.prototype, 'translate', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); + proceed.apply(this, [].slice.call(arguments, 1)); - // Do not do this if the chart is not 3D - if (this.chart.is3d()) { - this.translate3dShapes(); - } + // Do not do this if the chart is not 3D + if (this.chart.is3d()) { + this.translate3dShapes(); + } }); // In 3D we need to pass point.outsidePlot option to the justifyDataLabel // method for disabling justifying dataLabels in columns outside plot wrap(H.Series.prototype, 'alignDataLabel', function (proceed) { - arguments[3].outside3dPlot = arguments[1].outside3dPlot; - proceed.apply(this, [].slice.call(arguments, 1)); + arguments[3].outside3dPlot = arguments[1].outside3dPlot; + proceed.apply(this, [].slice.call(arguments, 1)); }); // Don't use justifyDataLabel when point is outsidePlot wrap(H.Series.prototype, 'justifyDataLabel', function (proceed) { - return !(arguments[2].outside3dPlot) ? - proceed.apply(this, [].slice.call(arguments, 1)) : - false; + return !(arguments[2].outside3dPlot) ? + proceed.apply(this, [].slice.call(arguments, 1)) : + false; }); seriesTypes.column.prototype.translate3dPoints = function () {}; seriesTypes.column.prototype.translate3dShapes = function () { - var series = this, - chart = series.chart, - seriesOptions = series.options, - depth = seriesOptions.depth || 25, - stack = seriesOptions.stacking ? - (seriesOptions.stack || 0) : - series.index, // #4743 - z = stack * (depth + (seriesOptions.groupZPadding || 1)), - borderCrisp = series.borderWidth % 2 ? 0.5 : 0; - - if (chart.inverted && !series.yAxis.reversed) { - borderCrisp *= -1; - } - - if (seriesOptions.grouping !== false) { - z = 0; - } - - z += (seriesOptions.groupZPadding || 1); - each(series.data, function (point) { - // #7103 Reset outside3dPlot flag - point.outside3dPlot = null; - if (point.y !== null) { - var shapeArgs = point.shapeArgs, - tooltipPos = point.tooltipPos, - // Array for final shapeArgs calculation. - // We are checking two dimensions (x and y). - dimensions = [['x', 'width'], ['y', 'height']], - borderlessBase; // Crisped rects can have +/- 0.5 pixels offset. - - // #3131 We need to check if column is inside plotArea. - each(dimensions, function (d) { - borderlessBase = shapeArgs[d[0]] - borderCrisp; - if (borderlessBase < 0) { - // If borderLessBase is smaller than 0, it is needed to set - // its value to 0 or 0.5 depending on borderWidth - // borderWidth may be even or odd. - shapeArgs[d[1]] += shapeArgs[d[0]] + borderCrisp; - shapeArgs[d[0]] = -borderCrisp; - borderlessBase = 0; - } - if ( - ( - borderlessBase + shapeArgs[d[1]] > - series[d[0] + 'Axis'].len - ) && - // Do not change height/width of column if 0 (#6708) - shapeArgs[d[1]] !== 0 - ) { - shapeArgs[d[1]] = - series[d[0] + 'Axis'].len - shapeArgs[d[0]]; - } - if ( - // Do not remove columns with zero height/width. - (shapeArgs[d[1]] !== 0) && - ( - shapeArgs[d[0]] >= series[d[0] + 'Axis'].len || - shapeArgs[d[0]] + shapeArgs[d[1]] <= borderCrisp - ) - ) { - // Set args to 0 if column is outside the chart. - for (var key in shapeArgs) { - shapeArgs[key] = 0; - } - // #7103 outside3dPlot flag is set on Points which are - // currently outside of plot. - point.outside3dPlot = true; - } - }); - - point.shapeType = 'cuboid'; - shapeArgs.z = z; - shapeArgs.depth = depth; - shapeArgs.insidePlotArea = true; - - // Translate the tooltip position in 3d space - tooltipPos = perspective( - [{ x: tooltipPos[0], y: tooltipPos[1], z: z }], - chart, - true - )[0]; - point.tooltipPos = [tooltipPos.x, tooltipPos.y]; - } - }); - // store for later use #4067 - series.z = z; + var series = this, + chart = series.chart, + seriesOptions = series.options, + depth = seriesOptions.depth || 25, + stack = seriesOptions.stacking ? + (seriesOptions.stack || 0) : + series.index, // #4743 + z = stack * (depth + (seriesOptions.groupZPadding || 1)), + borderCrisp = series.borderWidth % 2 ? 0.5 : 0; + + if (chart.inverted && !series.yAxis.reversed) { + borderCrisp *= -1; + } + + if (seriesOptions.grouping !== false) { + z = 0; + } + + z += (seriesOptions.groupZPadding || 1); + each(series.data, function (point) { + // #7103 Reset outside3dPlot flag + point.outside3dPlot = null; + if (point.y !== null) { + var shapeArgs = point.shapeArgs, + tooltipPos = point.tooltipPos, + // Array for final shapeArgs calculation. + // We are checking two dimensions (x and y). + dimensions = [['x', 'width'], ['y', 'height']], + borderlessBase; // Crisped rects can have +/- 0.5 pixels offset. + + // #3131 We need to check if column is inside plotArea. + each(dimensions, function (d) { + borderlessBase = shapeArgs[d[0]] - borderCrisp; + if (borderlessBase < 0) { + // If borderLessBase is smaller than 0, it is needed to set + // its value to 0 or 0.5 depending on borderWidth + // borderWidth may be even or odd. + shapeArgs[d[1]] += shapeArgs[d[0]] + borderCrisp; + shapeArgs[d[0]] = -borderCrisp; + borderlessBase = 0; + } + if ( + ( + borderlessBase + shapeArgs[d[1]] > + series[d[0] + 'Axis'].len + ) && + // Do not change height/width of column if 0 (#6708) + shapeArgs[d[1]] !== 0 + ) { + shapeArgs[d[1]] = + series[d[0] + 'Axis'].len - shapeArgs[d[0]]; + } + if ( + // Do not remove columns with zero height/width. + (shapeArgs[d[1]] !== 0) && + ( + shapeArgs[d[0]] >= series[d[0] + 'Axis'].len || + shapeArgs[d[0]] + shapeArgs[d[1]] <= borderCrisp + ) + ) { + // Set args to 0 if column is outside the chart. + for (var key in shapeArgs) { + shapeArgs[key] = 0; + } + // #7103 outside3dPlot flag is set on Points which are + // currently outside of plot. + point.outside3dPlot = true; + } + }); + + point.shapeType = 'cuboid'; + shapeArgs.z = z; + shapeArgs.depth = depth; + shapeArgs.insidePlotArea = true; + + // Translate the tooltip position in 3d space + tooltipPos = perspective( + [{ x: tooltipPos[0], y: tooltipPos[1], z: z }], + chart, + true + )[0]; + point.tooltipPos = [tooltipPos.x, tooltipPos.y]; + } + }); + // store for later use #4067 + series.z = z; }; wrap(seriesTypes.column.prototype, 'animate', function (proceed) { - if (!this.chart.is3d()) { - proceed.apply(this, [].slice.call(arguments, 1)); - } else { - var args = arguments, - init = args[1], - yAxis = this.yAxis, - series = this, - reversed = this.yAxis.reversed; - - if (svg) { // VML is too slow anyway - if (init) { - each(series.data, function (point) { - if (point.y !== null) { - point.height = point.shapeArgs.height; - point.shapey = point.shapeArgs.y; // #2968 - point.shapeArgs.height = 1; - if (!reversed) { - if (point.stackY) { - point.shapeArgs.y = - point.plotY + yAxis.translate(point.stackY); - } else { - point.shapeArgs.y = - point.plotY + - ( - point.negative ? - -point.height : - point.height - ); - } - } - } - }); - - } else { // run the animation - each(series.data, function (point) { - if (point.y !== null) { - point.shapeArgs.height = point.height; - point.shapeArgs.y = point.shapey; // #2968 - // null value do not have a graphic - if (point.graphic) { - point.graphic.animate( - point.shapeArgs, - series.options.animation - ); - } - } - }); - - // redraw datalabels to the correct position - this.drawDataLabels(); - - // delete this function to allow it only once - series.animate = null; - } - } - } + if (!this.chart.is3d()) { + proceed.apply(this, [].slice.call(arguments, 1)); + } else { + var args = arguments, + init = args[1], + yAxis = this.yAxis, + series = this, + reversed = this.yAxis.reversed; + + if (svg) { // VML is too slow anyway + if (init) { + each(series.data, function (point) { + if (point.y !== null) { + point.height = point.shapeArgs.height; + point.shapey = point.shapeArgs.y; // #2968 + point.shapeArgs.height = 1; + if (!reversed) { + if (point.stackY) { + point.shapeArgs.y = + point.plotY + yAxis.translate(point.stackY); + } else { + point.shapeArgs.y = + point.plotY + + ( + point.negative ? + -point.height : + point.height + ); + } + } + } + }); + + } else { // run the animation + each(series.data, function (point) { + if (point.y !== null) { + point.shapeArgs.height = point.height; + point.shapeArgs.y = point.shapey; // #2968 + // null value do not have a graphic + if (point.graphic) { + point.graphic.animate( + point.shapeArgs, + series.options.animation + ); + } + } + }); + + // redraw datalabels to the correct position + this.drawDataLabels(); + + // delete this function to allow it only once + series.animate = null; + } + } + } }); /* @@ -238,199 +238,199 @@ wrap(seriesTypes.column.prototype, 'animate', function (proceed) { */ wrap( - seriesTypes.column.prototype, - 'plotGroup', - function (proceed, prop, name, visibility, zIndex, parent) { - if (this.chart.is3d() && parent && !this[prop]) { - if (!this.chart.columnGroup) { - this.chart.columnGroup = - this.chart.renderer.g('columnGroup').add(parent); - } - this[prop] = this.chart.columnGroup; - this.chart.columnGroup.attr(this.getPlotBox()); - this[prop].survive = true; - } - return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + seriesTypes.column.prototype, + 'plotGroup', + function (proceed, prop, name, visibility, zIndex, parent) { + if (this.chart.is3d() && parent && !this[prop]) { + if (!this.chart.columnGroup) { + this.chart.columnGroup = + this.chart.renderer.g('columnGroup').add(parent); + } + this[prop] = this.chart.columnGroup; + this.chart.columnGroup.attr(this.getPlotBox()); + this[prop].survive = true; + } + return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } ); /* - * When series is not added to group it is needed to change + * When series is not added to group it is needed to change * setVisible method to allow correct Legend funcionality * This wrap is basing on pie chart series */ wrap( - seriesTypes.column.prototype, - 'setVisible', - function (proceed, vis) { - var series = this, - pointVis; - if (series.chart.is3d()) { - each(series.data, function (point) { - point.visible = point.options.visible = vis = - vis === undefined ? !point.visible : vis; - pointVis = vis ? 'visible' : 'hidden'; - series.options.data[inArray(point, series.data)] = - point.options; - if (point.graphic) { - point.graphic.attr({ - visibility: pointVis - }); - } - }); - } - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - } + seriesTypes.column.prototype, + 'setVisible', + function (proceed, vis) { + var series = this, + pointVis; + if (series.chart.is3d()) { + each(series.data, function (point) { + point.visible = point.options.visible = vis = + vis === undefined ? !point.visible : vis; + pointVis = vis ? 'visible' : 'hidden'; + series.options.data[inArray(point, series.data)] = + point.options; + if (point.graphic) { + point.graphic.attr({ + visibility: pointVis + }); + } + }); + } + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } ); seriesTypes.column.prototype.handle3dGrouping = true; addEvent(Series, 'afterInit', function () { - if (this.chart.is3d() && this.handle3dGrouping) { - var seriesOptions = this.options, - grouping = seriesOptions.grouping, - stacking = seriesOptions.stacking, - reversedStacks = pick(this.yAxis.options.reversedStacks, true), - z = 0; - - if (!(grouping !== undefined && !grouping)) { - var stacks = this.chart.retrieveStacks(stacking), - stack = seriesOptions.stack || 0, - i; // position within the stack - for (i = 0; i < stacks[stack].series.length; i++) { - if (stacks[stack].series[i] === this) { - break; - } - } - z = (10 * (stacks.totalStacks - stacks[stack].position)) + - (reversedStacks ? i : -i); // #4369 - - // In case when axis is reversed, columns are also reversed inside - // the group (#3737) - if (!this.xAxis.reversed) { - z = (stacks.totalStacks * 10) - z; - } - } - - seriesOptions.zIndex = z; - } + if (this.chart.is3d() && this.handle3dGrouping) { + var seriesOptions = this.options, + grouping = seriesOptions.grouping, + stacking = seriesOptions.stacking, + reversedStacks = pick(this.yAxis.options.reversedStacks, true), + z = 0; + + if (!(grouping !== undefined && !grouping)) { + var stacks = this.chart.retrieveStacks(stacking), + stack = seriesOptions.stack || 0, + i; // position within the stack + for (i = 0; i < stacks[stack].series.length; i++) { + if (stacks[stack].series[i] === this) { + break; + } + } + z = (10 * (stacks.totalStacks - stacks[stack].position)) + + (reversedStacks ? i : -i); // #4369 + + // In case when axis is reversed, columns are also reversed inside + // the group (#3737) + if (!this.xAxis.reversed) { + z = (stacks.totalStacks * 10) - z; + } + } + + seriesOptions.zIndex = z; + } }); /*= if (build.classic) { =*/ function pointAttribs(proceed) { - var attr = proceed.apply(this, [].slice.call(arguments, 1)); + var attr = proceed.apply(this, [].slice.call(arguments, 1)); - if (this.chart.is3d && this.chart.is3d()) { - // Set the fill color to the fill color to provide a smooth edge - attr.stroke = this.options.edgeColor || attr.fill; - attr['stroke-width'] = pick(this.options.edgeWidth, 1); // #4055 - } + if (this.chart.is3d && this.chart.is3d()) { + // Set the fill color to the fill color to provide a smooth edge + attr.stroke = this.options.edgeColor || attr.fill; + attr['stroke-width'] = pick(this.options.edgeWidth, 1); // #4055 + } - return attr; + return attr; } wrap(seriesTypes.column.prototype, 'pointAttribs', pointAttribs); if (seriesTypes.columnrange) { - wrap(seriesTypes.columnrange.prototype, 'pointAttribs', pointAttribs); - seriesTypes.columnrange.prototype.plotGroup = - seriesTypes.column.prototype.plotGroup; - seriesTypes.columnrange.prototype.setVisible = - seriesTypes.column.prototype.setVisible; + wrap(seriesTypes.columnrange.prototype, 'pointAttribs', pointAttribs); + seriesTypes.columnrange.prototype.plotGroup = + seriesTypes.column.prototype.plotGroup; + seriesTypes.columnrange.prototype.setVisible = + seriesTypes.column.prototype.setVisible; } /*= } =*/ wrap(Series.prototype, 'alignDataLabel', function (proceed) { - - // Only do this for 3D columns and columnranges - if ( - this.chart.is3d() && - (this.type === 'column' || this.type === 'columnrange') - ) { - var series = this, - chart = series.chart; - - var args = arguments, - alignTo = args[4], - point = args[1]; - - var pos = ({ x: alignTo.x, y: alignTo.y, z: series.z }); - pos = perspective([pos], chart, true)[0]; - alignTo.x = pos.x; - // #7103 If point is outside of plotArea, hide data label. - alignTo.y = point.outside3dPlot ? -9e9 : pos.y; - } - - proceed.apply(this, [].slice.call(arguments, 1)); + + // Only do this for 3D columns and columnranges + if ( + this.chart.is3d() && + (this.type === 'column' || this.type === 'columnrange') + ) { + var series = this, + chart = series.chart; + + var args = arguments, + alignTo = args[4], + point = args[1]; + + var pos = ({ x: alignTo.x, y: alignTo.y, z: series.z }); + pos = perspective([pos], chart, true)[0]; + alignTo.x = pos.x; + // #7103 If point is outside of plotArea, hide data label. + alignTo.y = point.outside3dPlot ? -9e9 : pos.y; + } + + proceed.apply(this, [].slice.call(arguments, 1)); }); // Added stackLabels position calculation for 3D charts. wrap(H.StackItem.prototype, 'getStackBox', function (proceed, chart) { // #3946 - var stackBox = proceed.apply(this, [].slice.call(arguments, 1)); - - // Only do this for 3D chart. - if (chart.is3d()) { - var pos = ({ - x: stackBox.x, - y: stackBox.y, - z: 0 - }); - pos = H.perspective([pos], chart, true)[0]; - stackBox.x = pos.x; - stackBox.y = pos.y; - } - - return stackBox; + var stackBox = proceed.apply(this, [].slice.call(arguments, 1)); + + // Only do this for 3D chart. + if (chart.is3d()) { + var pos = ({ + x: stackBox.x, + y: stackBox.y, + z: 0 + }); + pos = H.perspective([pos], chart, true)[0]; + stackBox.x = pos.x; + stackBox.y = pos.y; + } + + return stackBox; }); /* - EXTENSION FOR 3D CYLINDRICAL COLUMNS - Not supported + EXTENSION FOR 3D CYLINDRICAL COLUMNS + Not supported */ /* var defaultOptions = H.getOptions(); defaultOptions.plotOptions.cylinder = - H.merge(defaultOptions.plotOptions.column); + H.merge(defaultOptions.plotOptions.column); var CylinderSeries = H.extendClass(seriesTypes.column, { - type: 'cylinder' + type: 'cylinder' }); seriesTypes.cylinder = CylinderSeries; wrap(seriesTypes.cylinder.prototype, 'translate', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return; - } - - var series = this, - chart = series.chart, - options = chart.options, - cylOptions = options.plotOptions.cylinder, - options3d = options.chart.options3d, - depth = cylOptions.depth || 0, - alpha = chart.alpha3d; - - var z = cylOptions.stacking ? - (this.options.stack || 0) * depth : - series._i * depth; - z += depth / 2; - - if (cylOptions.grouping !== false) { z = 0; } - - each(series.data, function (point) { - var shapeArgs = point.shapeArgs, - deg2rad = H.deg2rad; - point.shapeType = 'arc3d'; - shapeArgs.x += depth / 2; - shapeArgs.z = z; - shapeArgs.start = 0; - shapeArgs.end = 2 * PI; - shapeArgs.r = depth * 0.95; - shapeArgs.innerR = 0; - shapeArgs.depth = - shapeArgs.height * (1 / sin((90 - alpha) * deg2rad)) - z; - shapeArgs.alpha = 90 - alpha; - shapeArgs.beta = 0; - }); + proceed.apply(this, [].slice.call(arguments, 1)); + + // Do not do this if the chart is not 3D + if (!this.chart.is3d()) { + return; + } + + var series = this, + chart = series.chart, + options = chart.options, + cylOptions = options.plotOptions.cylinder, + options3d = options.chart.options3d, + depth = cylOptions.depth || 0, + alpha = chart.alpha3d; + + var z = cylOptions.stacking ? + (this.options.stack || 0) * depth : + series._i * depth; + z += depth / 2; + + if (cylOptions.grouping !== false) { z = 0; } + + each(series.data, function (point) { + var shapeArgs = point.shapeArgs, + deg2rad = H.deg2rad; + point.shapeType = 'arc3d'; + shapeArgs.x += depth / 2; + shapeArgs.z = z; + shapeArgs.start = 0; + shapeArgs.end = 2 * PI; + shapeArgs.r = depth * 0.95; + shapeArgs.innerR = 0; + shapeArgs.depth = + shapeArgs.height * (1 / sin((90 - alpha) * deg2rad)) - z; + shapeArgs.alpha = 90 - alpha; + shapeArgs.beta = 0; + }); }); */ diff --git a/js/parts-3d/Math.js b/js/parts-3d/Math.js index e7d2222a0ea..280056c6e18 100644 --- a/js/parts-3d/Math.js +++ b/js/parts-3d/Math.js @@ -8,118 +8,118 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; /** - * Mathematical Functionility + * Mathematical Functionility */ var deg2rad = H.deg2rad, - pick = H.pick; + pick = H.pick; /** * Apply 3-D rotation - * Euler Angles (XYZ): cosA = cos(Alfa|Roll), cosB = cos(Beta|Pitch), cosG = cos(Gamma|Yaw) - * + * Euler Angles (XYZ): cosA = cos(Alfa|Roll), cosB = cos(Beta|Pitch), cosG = cos(Gamma|Yaw) + * * Composite rotation: * | cosB * cosG | cosB * sinG | -sinB | * | sinA * sinB * cosG - cosA * sinG | sinA * sinB * sinG + cosA * cosG | sinA * cosB | * | cosA * sinB * cosG + sinA * sinG | cosA * sinB * sinG - sinA * cosG | cosA * cosB | - * + * * Now, Gamma/Yaw is not used (angle=0), so we assume cosG = 1 and sinG = 0, so we get: * | cosB | 0 | - sinB | * | sinA * sinB | cosA | sinA * cosB | * | cosA * sinB | - sinA | cosA * cosB | - * + * * But in browsers, y is reversed, so we get sinA => -sinA. The general result is: * | cosB | 0 | - sinB | | x | | px | - * | - sinA * sinB | cosA | - sinA * cosB | x | y | = | py | + * | - sinA * sinB | cosA | - sinA * cosB | x | y | = | py | * | cosA * sinB | sinA | cosA * cosB | | z | | pz | */ function rotate3D(x, y, z, angles) { - return { - x: angles.cosB * x - angles.sinB * z, - y: -angles.sinA * angles.sinB * x + angles.cosA * y - angles.cosB * angles.sinA * z, - z: angles.cosA * angles.sinB * x + angles.sinA * y + angles.cosA * angles.cosB * z - }; + return { + x: angles.cosB * x - angles.sinB * z, + y: -angles.sinA * angles.sinB * x + angles.cosA * y - angles.cosB * angles.sinA * z, + z: angles.cosA * angles.sinB * x + angles.sinA * y + angles.cosA * angles.cosB * z + }; } function perspective3D(coordinate, origin, distance) { - var projection = ((distance > 0) && (distance < Number.POSITIVE_INFINITY)) ? distance / (coordinate.z + origin.z + distance) : 1; - return { - x: coordinate.x * projection, - y: coordinate.y * projection - }; + var projection = ((distance > 0) && (distance < Number.POSITIVE_INFINITY)) ? distance / (coordinate.z + origin.z + distance) : 1; + return { + x: coordinate.x * projection, + y: coordinate.y * projection + }; } /** * Transforms a given array of points according to the angles in chart.options. * Parameters: - * - points: the array of points - * - chart: the chart - * - insidePlotArea: wether to verifiy the points are inside the plotArea + * - points: the array of points + * - chart: the chart + * - insidePlotArea: wether to verifiy the points are inside the plotArea * Returns: - * - an array of transformed points + * - an array of transformed points */ H.perspective = function (points, chart, insidePlotArea) { - var options3d = chart.options.chart.options3d, - inverted = insidePlotArea ? chart.inverted : false, - origin = { - x: chart.plotWidth / 2, - y: chart.plotHeight / 2, - z: options3d.depth / 2, - vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0) - }, - scale = chart.scale3d || 1, - beta = deg2rad * options3d.beta * (inverted ? -1 : 1), - alpha = deg2rad * options3d.alpha * (inverted ? -1 : 1), - angles = { - cosA: Math.cos(alpha), - cosB: Math.cos(-beta), - sinA: Math.sin(alpha), - sinB: Math.sin(-beta) - }; + var options3d = chart.options.chart.options3d, + inverted = insidePlotArea ? chart.inverted : false, + origin = { + x: chart.plotWidth / 2, + y: chart.plotHeight / 2, + z: options3d.depth / 2, + vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0) + }, + scale = chart.scale3d || 1, + beta = deg2rad * options3d.beta * (inverted ? -1 : 1), + alpha = deg2rad * options3d.alpha * (inverted ? -1 : 1), + angles = { + cosA: Math.cos(alpha), + cosB: Math.cos(-beta), + sinA: Math.sin(alpha), + sinB: Math.sin(-beta) + }; - if (!insidePlotArea) { - origin.x += chart.plotLeft; - origin.y += chart.plotTop; - } + if (!insidePlotArea) { + origin.x += chart.plotLeft; + origin.y += chart.plotTop; + } - // Transform each point - return H.map(points, function (point) { - var rotated = rotate3D( - (inverted ? point.y : point.x) - origin.x, - (inverted ? point.x : point.y) - origin.y, - (point.z || 0) - origin.z, - angles - ), - coordinate = perspective3D(rotated, origin, origin.vd); // Apply perspective + // Transform each point + return H.map(points, function (point) { + var rotated = rotate3D( + (inverted ? point.y : point.x) - origin.x, + (inverted ? point.x : point.y) - origin.y, + (point.z || 0) - origin.z, + angles + ), + coordinate = perspective3D(rotated, origin, origin.vd); // Apply perspective - // Apply translation - coordinate.x = coordinate.x * scale + origin.x; - coordinate.y = coordinate.y * scale + origin.y; - coordinate.z = rotated.z * scale + origin.z; + // Apply translation + coordinate.x = coordinate.x * scale + origin.x; + coordinate.y = coordinate.y * scale + origin.y; + coordinate.z = rotated.z * scale + origin.z; - return { - x: (inverted ? coordinate.y : coordinate.x), - y: (inverted ? coordinate.x : coordinate.y), - z: coordinate.z - }; - }); + return { + x: (inverted ? coordinate.y : coordinate.x), + y: (inverted ? coordinate.x : coordinate.y), + z: coordinate.z + }; + }); }; /** * Calculate a distance from camera to points - made for calculating zIndex of scatter points. * Parameters: - * - coordinates: The coordinates of the specific point - * - chart: the chart + * - coordinates: The coordinates of the specific point + * - chart: the chart * Returns: - * - a distance from camera to point + * - a distance from camera to point */ H.pointCameraDistance = function (coordinates, chart) { - var options3d = chart.options.chart.options3d, - cameraPosition = { - x: chart.plotWidth / 2, - y: chart.plotHeight / 2, - z: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0) + options3d.depth - }, - distance = Math.sqrt(Math.pow(cameraPosition.x - coordinates.plotX, 2) + Math.pow(cameraPosition.y - coordinates.plotY, 2) + Math.pow(cameraPosition.z - coordinates.plotZ, 2)); - return distance; + var options3d = chart.options.chart.options3d, + cameraPosition = { + x: chart.plotWidth / 2, + y: chart.plotHeight / 2, + z: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0) + options3d.depth + }, + distance = Math.sqrt(Math.pow(cameraPosition.x - coordinates.plotX, 2) + Math.pow(cameraPosition.y - coordinates.plotY, 2) + Math.pow(cameraPosition.z - coordinates.plotZ, 2)); + return distance; }; /** @@ -127,20 +127,20 @@ H.pointCameraDistance = function (coordinates, chart) { * http://en.wikipedia.org/wiki/Shoelace_formula */ H.shapeArea = function (vertexes) { - var area = 0, - i, - j; - for (i = 0; i < vertexes.length; i++) { - j = (i + 1) % vertexes.length; - area += vertexes[i].x * vertexes[j].y - vertexes[j].x * vertexes[i].y; - } - return area / 2; + var area = 0, + i, + j; + for (i = 0; i < vertexes.length; i++) { + j = (i + 1) % vertexes.length; + area += vertexes[i].x * vertexes[j].y - vertexes[j].x * vertexes[i].y; + } + return area / 2; }; /** * Calculate area of a 3D polygon after perspective projection */ H.shapeArea3d = function (vertexes, chart, insidePlotArea) { - return H.shapeArea(H.perspective(vertexes, chart, insidePlotArea)); + return H.shapeArea(H.perspective(vertexes, chart, insidePlotArea)); }; diff --git a/js/parts-3d/Pie.js b/js/parts-3d/Pie.js index 42d6aaa18ed..3ff479cced6 100644 --- a/js/parts-3d/Pie.js +++ b/js/parts-3d/Pie.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * 3D pie series - * + * * License: www.highcharts.com/license */ /* eslint max-len: 0 */ @@ -10,16 +10,16 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var deg2rad = H.deg2rad, - each = H.each, - pick = H.pick, - seriesTypes = H.seriesTypes, - svg = H.svg, - wrap = H.wrap; + each = H.each, + pick = H.pick, + seriesTypes = H.seriesTypes, + svg = H.svg, + wrap = H.wrap; /** * The thickness of a 3D pie. Requires `highcharts-3d.js` - * + * * @type {Number} * @default 0 * @since 4.0 @@ -28,173 +28,173 @@ var deg2rad = H.deg2rad, */ wrap(seriesTypes.pie.prototype, 'translate', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); + proceed.apply(this, [].slice.call(arguments, 1)); - // Do not do this if the chart is not 3D - if (!this.chart.is3d()) { - return; - } + // Do not do this if the chart is not 3D + if (!this.chart.is3d()) { + return; + } - var series = this, - seriesOptions = series.options, - depth = seriesOptions.depth || 0, - options3d = series.chart.options.chart.options3d, - alpha = options3d.alpha, - beta = options3d.beta, - z = seriesOptions.stacking ? (seriesOptions.stack || 0) * depth : series._i * depth; + var series = this, + seriesOptions = series.options, + depth = seriesOptions.depth || 0, + options3d = series.chart.options.chart.options3d, + alpha = options3d.alpha, + beta = options3d.beta, + z = seriesOptions.stacking ? (seriesOptions.stack || 0) * depth : series._i * depth; - z += depth / 2; + z += depth / 2; - if (seriesOptions.grouping !== false) { - z = 0; - } + if (seriesOptions.grouping !== false) { + z = 0; + } - each(series.data, function (point) { + each(series.data, function (point) { - var shapeArgs = point.shapeArgs, - angle; + var shapeArgs = point.shapeArgs, + angle; - point.shapeType = 'arc3d'; + point.shapeType = 'arc3d'; - shapeArgs.z = z; - shapeArgs.depth = depth * 0.75; - shapeArgs.alpha = alpha; - shapeArgs.beta = beta; - shapeArgs.center = series.center; + shapeArgs.z = z; + shapeArgs.depth = depth * 0.75; + shapeArgs.alpha = alpha; + shapeArgs.beta = beta; + shapeArgs.center = series.center; - angle = (shapeArgs.end + shapeArgs.start) / 2; + angle = (shapeArgs.end + shapeArgs.start) / 2; - point.slicedTranslation = { - translateX: Math.round(Math.cos(angle) * seriesOptions.slicedOffset * Math.cos(alpha * deg2rad)), - translateY: Math.round(Math.sin(angle) * seriesOptions.slicedOffset * Math.cos(alpha * deg2rad)) - }; - }); + point.slicedTranslation = { + translateX: Math.round(Math.cos(angle) * seriesOptions.slicedOffset * Math.cos(alpha * deg2rad)), + translateY: Math.round(Math.sin(angle) * seriesOptions.slicedOffset * Math.cos(alpha * deg2rad)) + }; + }); }); wrap(seriesTypes.pie.prototype.pointClass.prototype, 'haloPath', function (proceed) { - var args = arguments; - return this.series.chart.is3d() ? [] : proceed.call(this, args[1]); + var args = arguments; + return this.series.chart.is3d() ? [] : proceed.call(this, args[1]); }); /*= if (build.classic) { =*/ wrap(seriesTypes.pie.prototype, 'pointAttribs', function (proceed, point, state) { - var attr = proceed.call(this, point, state), - options = this.options; + var attr = proceed.call(this, point, state), + options = this.options; - if (this.chart.is3d()) { - attr.stroke = options.edgeColor || point.color || this.color; - attr['stroke-width'] = pick(options.edgeWidth, 1); - } + if (this.chart.is3d()) { + attr.stroke = options.edgeColor || point.color || this.color; + attr['stroke-width'] = pick(options.edgeWidth, 1); + } - return attr; + return attr; }); /*= } =*/ wrap(seriesTypes.pie.prototype, 'drawPoints', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - if (this.chart.is3d()) { - each(this.points, function (point) { - var graphic = point.graphic; - - // #4584 Check if has graphic - null points don't have it - if (graphic) { - // Hide null or 0 points (#3006, 3650) - graphic[point.y && point.visible ? 'show' : 'hide'](); - } - }); - } + proceed.apply(this, [].slice.call(arguments, 1)); + + if (this.chart.is3d()) { + each(this.points, function (point) { + var graphic = point.graphic; + + // #4584 Check if has graphic - null points don't have it + if (graphic) { + // Hide null or 0 points (#3006, 3650) + graphic[point.y && point.visible ? 'show' : 'hide'](); + } + }); + } }); wrap(seriesTypes.pie.prototype, 'drawDataLabels', function (proceed) { - if (this.chart.is3d()) { - var series = this, - chart = series.chart, - options3d = chart.options.chart.options3d; - each(series.data, function (point) { - var shapeArgs = point.shapeArgs, - r = shapeArgs.r, - a1 = (shapeArgs.alpha || options3d.alpha) * deg2rad, // #3240 issue with datalabels for 0 and null values - b1 = (shapeArgs.beta || options3d.beta) * deg2rad, - a2 = (shapeArgs.start + shapeArgs.end) / 2, - labelPos = point.labelPos, - labelIndexes = [0, 2, 4], // [x1, y1, x2, y2, x3, y3] - yOffset = (-r * (1 - Math.cos(a1)) * Math.sin(a2)), // + (sin(a2) > 0 ? sin(a1) * d : 0) - xOffset = r * (Math.cos(b1) - 1) * Math.cos(a2); - - // Apply perspective on label positions - each(labelIndexes, function (index) { - labelPos[index] += xOffset; - labelPos[index + 1] += yOffset; - }); - }); - } - - proceed.apply(this, [].slice.call(arguments, 1)); + if (this.chart.is3d()) { + var series = this, + chart = series.chart, + options3d = chart.options.chart.options3d; + each(series.data, function (point) { + var shapeArgs = point.shapeArgs, + r = shapeArgs.r, + a1 = (shapeArgs.alpha || options3d.alpha) * deg2rad, // #3240 issue with datalabels for 0 and null values + b1 = (shapeArgs.beta || options3d.beta) * deg2rad, + a2 = (shapeArgs.start + shapeArgs.end) / 2, + labelPos = point.labelPos, + labelIndexes = [0, 2, 4], // [x1, y1, x2, y2, x3, y3] + yOffset = (-r * (1 - Math.cos(a1)) * Math.sin(a2)), // + (sin(a2) > 0 ? sin(a1) * d : 0) + xOffset = r * (Math.cos(b1) - 1) * Math.cos(a2); + + // Apply perspective on label positions + each(labelIndexes, function (index) { + labelPos[index] += xOffset; + labelPos[index + 1] += yOffset; + }); + }); + } + + proceed.apply(this, [].slice.call(arguments, 1)); }); wrap(seriesTypes.pie.prototype, 'addPoint', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - if (this.chart.is3d()) { - // destroy (and rebuild) everything!!! - this.update(this.userOptions, true); // #3845 pass the old options - } + proceed.apply(this, [].slice.call(arguments, 1)); + if (this.chart.is3d()) { + // destroy (and rebuild) everything!!! + this.update(this.userOptions, true); // #3845 pass the old options + } }); wrap(seriesTypes.pie.prototype, 'animate', function (proceed) { - if (!this.chart.is3d()) { - proceed.apply(this, [].slice.call(arguments, 1)); - } else { - var args = arguments, - init = args[1], - animation = this.options.animation, - attribs, - center = this.center, - group = this.group, - markerGroup = this.markerGroup; - - if (svg) { // VML is too slow anyway - - if (animation === true) { - animation = {}; - } - // Initialize the animation - if (init) { - - // Scale down the group and place it in the center - group.oldtranslateX = group.translateX; - group.oldtranslateY = group.translateY; - attribs = { - translateX: center[0], - translateY: center[1], - scaleX: 0.001, // #1499 - scaleY: 0.001 - }; - - group.attr(attribs); - if (markerGroup) { - markerGroup.attrSetters = group.attrSetters; - markerGroup.attr(attribs); - } - - // Run the animation - } else { - attribs = { - translateX: group.oldtranslateX, - translateY: group.oldtranslateY, - scaleX: 1, - scaleY: 1 - }; - group.animate(attribs, animation); - - if (markerGroup) { - markerGroup.animate(attribs, animation); - } - - // Delete this function to allow it only once - this.animate = null; - } - - } - } + if (!this.chart.is3d()) { + proceed.apply(this, [].slice.call(arguments, 1)); + } else { + var args = arguments, + init = args[1], + animation = this.options.animation, + attribs, + center = this.center, + group = this.group, + markerGroup = this.markerGroup; + + if (svg) { // VML is too slow anyway + + if (animation === true) { + animation = {}; + } + // Initialize the animation + if (init) { + + // Scale down the group and place it in the center + group.oldtranslateX = group.translateX; + group.oldtranslateY = group.translateY; + attribs = { + translateX: center[0], + translateY: center[1], + scaleX: 0.001, // #1499 + scaleY: 0.001 + }; + + group.attr(attribs); + if (markerGroup) { + markerGroup.attrSetters = group.attrSetters; + markerGroup.attr(attribs); + } + + // Run the animation + } else { + attribs = { + translateX: group.oldtranslateX, + translateY: group.oldtranslateY, + scaleX: 1, + scaleY: 1 + }; + group.animate(attribs, animation); + + if (markerGroup) { + markerGroup.animate(attribs, animation); + } + + // Delete this function to allow it only once + this.animate = null; + } + + } + } }); diff --git a/js/parts-3d/SVGRenderer.js b/js/parts-3d/SVGRenderer.js index 34dcf13e7a2..6f05bf69d06 100644 --- a/js/parts-3d/SVGRenderer.js +++ b/js/parts-3d/SVGRenderer.js @@ -10,27 +10,27 @@ import '../parts/Utilities.js'; import '../parts/Color.js'; import '../parts/SvgRenderer.js'; var cos = Math.cos, - PI = Math.PI, - sin = Math.sin; - + PI = Math.PI, + sin = Math.sin; + var animObject = H.animObject, - charts = H.charts, - color = H.color, - defined = H.defined, - deg2rad = H.deg2rad, - each = H.each, - extend = H.extend, - inArray = H.inArray, - map = H.map, - merge = H.merge, - perspective = H.perspective, - pick = H.pick, - SVGElement = H.SVGElement, - SVGRenderer = H.SVGRenderer, - wrap = H.wrap; + charts = H.charts, + color = H.color, + defined = H.defined, + deg2rad = H.deg2rad, + each = H.each, + extend = H.extend, + inArray = H.inArray, + map = H.map, + merge = H.merge, + perspective = H.perspective, + pick = H.pick, + SVGElement = H.SVGElement, + SVGRenderer = H.SVGRenderer, + wrap = H.wrap; /* - EXTENSION TO THE SVG-RENDERER TO ENABLE 3D SHAPES + EXTENSION TO THE SVG-RENDERER TO ENABLE 3D SHAPES */ // HELPER METHODS // @@ -40,28 +40,28 @@ var dFactor = (4 * (Math.sqrt(2) - 1) / 3) / (PI / 2); * Can 'wrap' around more then 180 degrees */ function curveTo(cx, cy, rx, ry, start, end, dx, dy) { - var result = [], - arcAngle = end - start; - if ((end > start) && (end - start > Math.PI / 2 + 0.0001)) { - result = result.concat(curveTo(cx, cy, rx, ry, start, start + (Math.PI / 2), dx, dy)); - result = result.concat(curveTo(cx, cy, rx, ry, start + (Math.PI / 2), end, dx, dy)); - return result; - } - if ((end < start) && (start - end > Math.PI / 2 + 0.0001)) { - result = result.concat(curveTo(cx, cy, rx, ry, start, start - (Math.PI / 2), dx, dy)); - result = result.concat(curveTo(cx, cy, rx, ry, start - (Math.PI / 2), end, dx, dy)); - return result; - } - return [ - 'C', - cx + (rx * Math.cos(start)) - ((rx * dFactor * arcAngle) * Math.sin(start)) + dx, - cy + (ry * Math.sin(start)) + ((ry * dFactor * arcAngle) * Math.cos(start)) + dy, - cx + (rx * Math.cos(end)) + ((rx * dFactor * arcAngle) * Math.sin(end)) + dx, - cy + (ry * Math.sin(end)) - ((ry * dFactor * arcAngle) * Math.cos(end)) + dy, - - cx + (rx * Math.cos(end)) + dx, - cy + (ry * Math.sin(end)) + dy - ]; + var result = [], + arcAngle = end - start; + if ((end > start) && (end - start > Math.PI / 2 + 0.0001)) { + result = result.concat(curveTo(cx, cy, rx, ry, start, start + (Math.PI / 2), dx, dy)); + result = result.concat(curveTo(cx, cy, rx, ry, start + (Math.PI / 2), end, dx, dy)); + return result; + } + if ((end < start) && (start - end > Math.PI / 2 + 0.0001)) { + result = result.concat(curveTo(cx, cy, rx, ry, start, start - (Math.PI / 2), dx, dy)); + result = result.concat(curveTo(cx, cy, rx, ry, start - (Math.PI / 2), end, dx, dy)); + return result; + } + return [ + 'C', + cx + (rx * Math.cos(start)) - ((rx * dFactor * arcAngle) * Math.sin(start)) + dx, + cy + (ry * Math.sin(start)) + ((ry * dFactor * arcAngle) * Math.cos(start)) + dy, + cx + (rx * Math.cos(end)) + ((rx * dFactor * arcAngle) * Math.sin(end)) + dx, + cy + (ry * Math.sin(end)) - ((ry * dFactor * arcAngle) * Math.cos(end)) + dy, + + cx + (rx * Math.cos(end)) + dx, + cy + (ry * Math.sin(end)) + dy + ]; } /*= if (!build.classic) { =*/ @@ -70,70 +70,70 @@ function curveTo(cx, cy, rx, ry, start, end, dx, dy) { * darker faces of the cuboids. */ wrap(SVGRenderer.prototype, 'init', function (proceed) { - proceed.apply(this, [].slice.call(arguments, 1)); - - each([{ - name: 'darker', - slope: 0.6 - }, { - name: 'brighter', - slope: 1.4 - }], function (cfg) { - this.definition({ - tagName: 'filter', - id: 'highcharts-' + cfg.name, - children: [{ - tagName: 'feComponentTransfer', - children: [{ - tagName: 'feFuncR', - type: 'linear', - slope: cfg.slope - }, { - tagName: 'feFuncG', - type: 'linear', - slope: cfg.slope - }, { - tagName: 'feFuncB', - type: 'linear', - slope: cfg.slope - }] - }] - }); - }, this); + proceed.apply(this, [].slice.call(arguments, 1)); + + each([{ + name: 'darker', + slope: 0.6 + }, { + name: 'brighter', + slope: 1.4 + }], function (cfg) { + this.definition({ + tagName: 'filter', + id: 'highcharts-' + cfg.name, + children: [{ + tagName: 'feComponentTransfer', + children: [{ + tagName: 'feFuncR', + type: 'linear', + slope: cfg.slope + }, { + tagName: 'feFuncG', + type: 'linear', + slope: cfg.slope + }, { + tagName: 'feFuncB', + type: 'linear', + slope: cfg.slope + }] + }] + }); + }, this); }); /*= } =*/ SVGRenderer.prototype.toLinePath = function (points, closed) { - var result = []; + var result = []; - // Put "L x y" for each point - each(points, function (point) { - result.push('L', point.x, point.y); - }); + // Put "L x y" for each point + each(points, function (point) { + result.push('L', point.x, point.y); + }); - if (points.length) { - // Set the first element to M - result[0] = 'M'; + if (points.length) { + // Set the first element to M + result[0] = 'M'; - // If it is a closed line, add Z - if (closed) { - result.push('Z'); - } - } + // If it is a closed line, add Z + if (closed) { + result.push('Z'); + } + } - return result; + return result; }; SVGRenderer.prototype.toLineSegments = function (points) { - var result = []; + var result = []; - var m = true; - each(points, function (point) { - result.push(m ? 'M' : 'L', point.x, point.y); - m = !m; - }); + var m = true; + each(points, function (point) { + result.push(m ? 'M' : 'L', point.x, point.y); + m = !m; + }); - return result; + return result; }; /** @@ -142,58 +142,58 @@ SVGRenderer.prototype.toLineSegments = function (points) { * It is used as a polyhedron Element */ SVGRenderer.prototype.face3d = function (args) { - var renderer = this, - ret = this.createElement('path'); - ret.vertexes = []; - ret.insidePlotArea = false; - ret.enabled = true; - - wrap(ret, 'attr', function (proceed, hash) { - if (typeof hash === 'object' && - (defined(hash.enabled) || defined(hash.vertexes) || defined(hash.insidePlotArea))) { - this.enabled = pick(hash.enabled, this.enabled); - this.vertexes = pick(hash.vertexes, this.vertexes); - this.insidePlotArea = pick(hash.insidePlotArea, this.insidePlotArea); - delete hash.enabled; - delete hash.vertexes; - delete hash.insidePlotArea; - - var chart = charts[renderer.chartIndex], - vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), - path = renderer.toLinePath(vertexes2d, true), - area = H.shapeArea(vertexes2d), - visibility = (this.enabled && area > 0) ? 'visible' : 'hidden'; - - hash.d = path; - hash.visibility = visibility; - } - return proceed.apply(this, [].slice.call(arguments, 1)); - }); - - wrap(ret, 'animate', function (proceed, params) { - if (typeof params === 'object' && - (defined(params.enabled) || defined(params.vertexes) || defined(params.insidePlotArea))) { - this.enabled = pick(params.enabled, this.enabled); - this.vertexes = pick(params.vertexes, this.vertexes); - this.insidePlotArea = pick(params.insidePlotArea, this.insidePlotArea); - delete params.enabled; - delete params.vertexes; - delete params.insidePlotArea; - - var chart = charts[renderer.chartIndex], - vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), - path = renderer.toLinePath(vertexes2d, true), - area = H.shapeArea(vertexes2d), - visibility = (this.enabled && area > 0) ? 'visible' : 'hidden'; - - params.d = path; - this.attr('visibility', visibility); - } - - return proceed.apply(this, [].slice.call(arguments, 1)); - }); - - return ret.attr(args); + var renderer = this, + ret = this.createElement('path'); + ret.vertexes = []; + ret.insidePlotArea = false; + ret.enabled = true; + + wrap(ret, 'attr', function (proceed, hash) { + if (typeof hash === 'object' && + (defined(hash.enabled) || defined(hash.vertexes) || defined(hash.insidePlotArea))) { + this.enabled = pick(hash.enabled, this.enabled); + this.vertexes = pick(hash.vertexes, this.vertexes); + this.insidePlotArea = pick(hash.insidePlotArea, this.insidePlotArea); + delete hash.enabled; + delete hash.vertexes; + delete hash.insidePlotArea; + + var chart = charts[renderer.chartIndex], + vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), + path = renderer.toLinePath(vertexes2d, true), + area = H.shapeArea(vertexes2d), + visibility = (this.enabled && area > 0) ? 'visible' : 'hidden'; + + hash.d = path; + hash.visibility = visibility; + } + return proceed.apply(this, [].slice.call(arguments, 1)); + }); + + wrap(ret, 'animate', function (proceed, params) { + if (typeof params === 'object' && + (defined(params.enabled) || defined(params.vertexes) || defined(params.insidePlotArea))) { + this.enabled = pick(params.enabled, this.enabled); + this.vertexes = pick(params.vertexes, this.vertexes); + this.insidePlotArea = pick(params.insidePlotArea, this.insidePlotArea); + delete params.enabled; + delete params.vertexes; + delete params.insidePlotArea; + + var chart = charts[renderer.chartIndex], + vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), + path = renderer.toLinePath(vertexes2d, true), + area = H.shapeArea(vertexes2d), + visibility = (this.enabled && area > 0) ? 'visible' : 'hidden'; + + params.d = path; + this.attr('visibility', visibility); + } + + return proceed.apply(this, [].slice.call(arguments, 1)); + }); + + return ret.attr(args); }; /** @@ -201,712 +201,712 @@ SVGRenderer.prototype.face3d = function (args) { * It's only attribute is `faces`, an array of attributes of each one of it's Face3D instances. */ SVGRenderer.prototype.polyhedron = function (args) { - var renderer = this, - result = this.g(), - destroy = result.destroy; - - /*= if (build.classic) { =*/ - result.attr({ - 'stroke-linejoin': 'round' - }); - /*= } =*/ - - result.faces = []; - - - // destroy all children - result.destroy = function () { - for (var i = 0; i < result.faces.length; i++) { - result.faces[i].destroy(); - } - return destroy.call(this); - }; - - wrap(result, 'attr', function (proceed, hash, val, complete, continueAnimation) { - if (typeof hash === 'object' && defined(hash.faces)) { - while (result.faces.length > hash.faces.length) { - result.faces.pop().destroy(); - } - while (result.faces.length < hash.faces.length) { - result.faces.push(renderer.face3d().add(result)); - } - for (var i = 0; i < hash.faces.length; i++) { - result.faces[i].attr(hash.faces[i], null, complete, continueAnimation); - } - delete hash.faces; - } - return proceed.apply(this, [].slice.call(arguments, 1)); - }); - - wrap(result, 'animate', function (proceed, params, duration, complete) { - if (params && params.faces) { - while (result.faces.length > params.faces.length) { - result.faces.pop().destroy(); - } - while (result.faces.length < params.faces.length) { - result.faces.push(renderer.face3d().add(result)); - } - for (var i = 0; i < params.faces.length; i++) { - result.faces[i].animate(params.faces[i], duration, complete); - } - delete params.faces; - } - return proceed.apply(this, [].slice.call(arguments, 1)); - }); - - return result.attr(args); + var renderer = this, + result = this.g(), + destroy = result.destroy; + + /*= if (build.classic) { =*/ + result.attr({ + 'stroke-linejoin': 'round' + }); + /*= } =*/ + + result.faces = []; + + + // destroy all children + result.destroy = function () { + for (var i = 0; i < result.faces.length; i++) { + result.faces[i].destroy(); + } + return destroy.call(this); + }; + + wrap(result, 'attr', function (proceed, hash, val, complete, continueAnimation) { + if (typeof hash === 'object' && defined(hash.faces)) { + while (result.faces.length > hash.faces.length) { + result.faces.pop().destroy(); + } + while (result.faces.length < hash.faces.length) { + result.faces.push(renderer.face3d().add(result)); + } + for (var i = 0; i < hash.faces.length; i++) { + result.faces[i].attr(hash.faces[i], null, complete, continueAnimation); + } + delete hash.faces; + } + return proceed.apply(this, [].slice.call(arguments, 1)); + }); + + wrap(result, 'animate', function (proceed, params, duration, complete) { + if (params && params.faces) { + while (result.faces.length > params.faces.length) { + result.faces.pop().destroy(); + } + while (result.faces.length < params.faces.length) { + result.faces.push(renderer.face3d().add(result)); + } + for (var i = 0; i < params.faces.length; i++) { + result.faces[i].animate(params.faces[i], duration, complete); + } + delete params.faces; + } + return proceed.apply(this, [].slice.call(arguments, 1)); + }); + + return result.attr(args); }; // CUBOIDS // SVGRenderer.prototype.cuboid = function (shapeArgs) { - var result = this.g(), - destroy = result.destroy, - paths = this.cuboidPath(shapeArgs); - - /*= if (build.classic) { =*/ - result.attr({ - 'stroke-linejoin': 'round' - }); - /*= } =*/ - - // create the 3 sides - result.front = this.path(paths[0]).attr({ - 'class': 'highcharts-3d-front' - }).add(result); // Front, top and side are never overlapping in our case so it is redundant to set zIndex of every element. - result.top = this.path(paths[1]).attr({ - 'class': 'highcharts-3d-top' - }).add(result); - result.side = this.path(paths[2]).attr({ - 'class': 'highcharts-3d-side' - }).add(result); - - // apply the fill everywhere, the top a bit brighter, the side a bit darker - result.fillSetter = function (fill) { - this.front.attr({ - fill: fill - }); - this.top.attr({ - fill: color(fill).brighten(0.1).get() - }); - this.side.attr({ - fill: color(fill).brighten(-0.1).get() - }); - this.color = fill; - - // for animation getter (#6776) - result.fill = fill; - - return this; - }; - - // apply opacaity everywhere - result.opacitySetter = function (opacity) { - this.front.attr({ opacity: opacity }); - this.top.attr({ opacity: opacity }); - this.side.attr({ opacity: opacity }); - return this; - }; - - result.attr = function (args, val, complete, continueAnimation) { - - // Resolve setting attributes by string name - if (typeof args === 'string' && typeof val !== 'undefined') { - var key = args; - args = {}; - args[key] = val; - } - - if (args.shapeArgs || defined(args.x)) { - var shapeArgs = args.shapeArgs || args; - var paths = this.renderer.cuboidPath(shapeArgs); - this.front.attr({ d: paths[0] }); - this.top.attr({ d: paths[1] }); - this.side.attr({ d: paths[2] }); - } else { - // getter returns value - return SVGElement.prototype.attr.call( - this, args, undefined, complete, continueAnimation - ); - } - - return this; - }; - - result.animate = function (args, duration, complete) { - if (defined(args.x) && defined(args.y)) { - var paths = this.renderer.cuboidPath(args); - this.front.animate({ d: paths[0] }, duration, complete); - this.top.animate({ d: paths[1] }, duration, complete); - this.side.animate({ d: paths[2] }, duration, complete); - this.attr({ - zIndex: -paths[3] // #4774 - }); - } else if (args.opacity) { - this.front.animate(args, duration, complete); - this.top.animate(args, duration, complete); - this.side.animate(args, duration, complete); - } else { - SVGElement.prototype.animate.call(this, args, duration, complete); - } - return this; - }; - - // destroy all children - result.destroy = function () { - this.front.destroy(); - this.top.destroy(); - this.side.destroy(); - - return destroy.call(this); - }; - - // Apply the Z index to the cuboid group - result.attr({ zIndex: -paths[3] }); - - return result; + var result = this.g(), + destroy = result.destroy, + paths = this.cuboidPath(shapeArgs); + + /*= if (build.classic) { =*/ + result.attr({ + 'stroke-linejoin': 'round' + }); + /*= } =*/ + + // create the 3 sides + result.front = this.path(paths[0]).attr({ + 'class': 'highcharts-3d-front' + }).add(result); // Front, top and side are never overlapping in our case so it is redundant to set zIndex of every element. + result.top = this.path(paths[1]).attr({ + 'class': 'highcharts-3d-top' + }).add(result); + result.side = this.path(paths[2]).attr({ + 'class': 'highcharts-3d-side' + }).add(result); + + // apply the fill everywhere, the top a bit brighter, the side a bit darker + result.fillSetter = function (fill) { + this.front.attr({ + fill: fill + }); + this.top.attr({ + fill: color(fill).brighten(0.1).get() + }); + this.side.attr({ + fill: color(fill).brighten(-0.1).get() + }); + this.color = fill; + + // for animation getter (#6776) + result.fill = fill; + + return this; + }; + + // apply opacaity everywhere + result.opacitySetter = function (opacity) { + this.front.attr({ opacity: opacity }); + this.top.attr({ opacity: opacity }); + this.side.attr({ opacity: opacity }); + return this; + }; + + result.attr = function (args, val, complete, continueAnimation) { + + // Resolve setting attributes by string name + if (typeof args === 'string' && typeof val !== 'undefined') { + var key = args; + args = {}; + args[key] = val; + } + + if (args.shapeArgs || defined(args.x)) { + var shapeArgs = args.shapeArgs || args; + var paths = this.renderer.cuboidPath(shapeArgs); + this.front.attr({ d: paths[0] }); + this.top.attr({ d: paths[1] }); + this.side.attr({ d: paths[2] }); + } else { + // getter returns value + return SVGElement.prototype.attr.call( + this, args, undefined, complete, continueAnimation + ); + } + + return this; + }; + + result.animate = function (args, duration, complete) { + if (defined(args.x) && defined(args.y)) { + var paths = this.renderer.cuboidPath(args); + this.front.animate({ d: paths[0] }, duration, complete); + this.top.animate({ d: paths[1] }, duration, complete); + this.side.animate({ d: paths[2] }, duration, complete); + this.attr({ + zIndex: -paths[3] // #4774 + }); + } else if (args.opacity) { + this.front.animate(args, duration, complete); + this.top.animate(args, duration, complete); + this.side.animate(args, duration, complete); + } else { + SVGElement.prototype.animate.call(this, args, duration, complete); + } + return this; + }; + + // destroy all children + result.destroy = function () { + this.front.destroy(); + this.top.destroy(); + this.side.destroy(); + + return destroy.call(this); + }; + + // Apply the Z index to the cuboid group + result.attr({ zIndex: -paths[3] }); + + return result; }; /** - * Generates a cuboid + * Generates a cuboid */ H.SVGRenderer.prototype.cuboidPath = function (shapeArgs) { - var x = shapeArgs.x, - y = shapeArgs.y, - z = shapeArgs.z, - h = shapeArgs.height, - w = shapeArgs.width, - d = shapeArgs.depth, - chart = charts[this.chartIndex], - front, - back, - top, - bottom, - left, - right, - shape, - path1, - path2, - path3, - isFront, - isTop, - isRight, - options3d = chart.options.chart.options3d, - alpha = options3d.alpha, - // Priority for x axis is the biggest, - // because of x direction has biggest influence on zIndex - incrementX = 10000, - // y axis has the smallest priority in case of our charts - // (needs to be set because of stacking) - incrementY = 10, - incrementZ = 100, - zIndex = 0; - - // The 8 corners of the cube - var pArr = [{ - x: x, - y: y, - z: z - }, { - x: x + w, - y: y, - z: z - }, { - x: x + w, - y: y + h, - z: z - }, { - x: x, - y: y + h, - z: z - }, { - x: x, - y: y + h, - z: z + d - }, { - x: x + w, - y: y + h, - z: z + d - }, { - x: x + w, - y: y, - z: z + d - }, { - x: x, - y: y, - z: z + d - }]; - - // apply perspective - pArr = perspective(pArr, chart, shapeArgs.insidePlotArea); - - // helper method to decide which side is visible - function mapPath(i) { - return pArr[i]; - } - - /* - * First value - path with specific side - * Second value - added information about side for later calculations. - * Possible second values are 0 for path1, 1 for path2 and -1 for no path choosed. - */ - var pickShape = function (path1, path2) { - var ret = [ - [], -1 - ]; - path1 = map(path1, mapPath); - path2 = map(path2, mapPath); - if (H.shapeArea(path1) < 0) { - ret = [path1, 0]; - } else if (H.shapeArea(path2) < 0) { - ret = [path2, 1]; - } - return ret; - }; - - // front or back - front = [3, 2, 1, 0]; - back = [7, 6, 5, 4]; - shape = pickShape(front, back); - path1 = shape[0]; - isFront = shape[1]; - - - // top or bottom - top = [1, 6, 7, 0]; - bottom = [4, 5, 2, 3]; - shape = pickShape(top, bottom); - path2 = shape[0]; - isTop = shape[1]; - - // side - right = [1, 2, 5, 6]; - left = [0, 7, 4, 3]; - shape = pickShape(right, left); - path3 = shape[0]; - isRight = shape[1]; - - /* - * New block used for calculating zIndex. It is basing on X, Y and Z position of specific columns. - * All zIndexes (for X, Y and Z values) are added to the final zIndex, where every value has different priority. - * The biggest priority is in X and Z directions, the lowest index is for stacked columns (Y direction and the same X and Z positions). - * Big differents between priorities is made because we need to ensure that even for big changes in Y and Z parameters - * all columns will be drawn correctly. - */ - - if (isRight === 1) { - zIndex += incrementX * (1000 - x); - } else if (!isRight) { - zIndex += incrementX * x; - } - - zIndex += incrementY * ( - !isTop || - (alpha >= 0 && alpha <= 180 || alpha < 360 && alpha > 357.5) ? // Numbers checked empirically - chart.plotHeight - y : 10 + y - ); - - if (isFront === 1) { - zIndex += incrementZ * (z); - } else if (!isFront) { - zIndex += incrementZ * (1000 - z); - } - - zIndex = -Math.round(zIndex); - - return [ - this.toLinePath(path1, true), - this.toLinePath(path2, true), - this.toLinePath(path3, true), - zIndex - ]; // #4774 + var x = shapeArgs.x, + y = shapeArgs.y, + z = shapeArgs.z, + h = shapeArgs.height, + w = shapeArgs.width, + d = shapeArgs.depth, + chart = charts[this.chartIndex], + front, + back, + top, + bottom, + left, + right, + shape, + path1, + path2, + path3, + isFront, + isTop, + isRight, + options3d = chart.options.chart.options3d, + alpha = options3d.alpha, + // Priority for x axis is the biggest, + // because of x direction has biggest influence on zIndex + incrementX = 10000, + // y axis has the smallest priority in case of our charts + // (needs to be set because of stacking) + incrementY = 10, + incrementZ = 100, + zIndex = 0; + + // The 8 corners of the cube + var pArr = [{ + x: x, + y: y, + z: z + }, { + x: x + w, + y: y, + z: z + }, { + x: x + w, + y: y + h, + z: z + }, { + x: x, + y: y + h, + z: z + }, { + x: x, + y: y + h, + z: z + d + }, { + x: x + w, + y: y + h, + z: z + d + }, { + x: x + w, + y: y, + z: z + d + }, { + x: x, + y: y, + z: z + d + }]; + + // apply perspective + pArr = perspective(pArr, chart, shapeArgs.insidePlotArea); + + // helper method to decide which side is visible + function mapPath(i) { + return pArr[i]; + } + + /* + * First value - path with specific side + * Second value - added information about side for later calculations. + * Possible second values are 0 for path1, 1 for path2 and -1 for no path choosed. + */ + var pickShape = function (path1, path2) { + var ret = [ + [], -1 + ]; + path1 = map(path1, mapPath); + path2 = map(path2, mapPath); + if (H.shapeArea(path1) < 0) { + ret = [path1, 0]; + } else if (H.shapeArea(path2) < 0) { + ret = [path2, 1]; + } + return ret; + }; + + // front or back + front = [3, 2, 1, 0]; + back = [7, 6, 5, 4]; + shape = pickShape(front, back); + path1 = shape[0]; + isFront = shape[1]; + + + // top or bottom + top = [1, 6, 7, 0]; + bottom = [4, 5, 2, 3]; + shape = pickShape(top, bottom); + path2 = shape[0]; + isTop = shape[1]; + + // side + right = [1, 2, 5, 6]; + left = [0, 7, 4, 3]; + shape = pickShape(right, left); + path3 = shape[0]; + isRight = shape[1]; + + /* + * New block used for calculating zIndex. It is basing on X, Y and Z position of specific columns. + * All zIndexes (for X, Y and Z values) are added to the final zIndex, where every value has different priority. + * The biggest priority is in X and Z directions, the lowest index is for stacked columns (Y direction and the same X and Z positions). + * Big differents between priorities is made because we need to ensure that even for big changes in Y and Z parameters + * all columns will be drawn correctly. + */ + + if (isRight === 1) { + zIndex += incrementX * (1000 - x); + } else if (!isRight) { + zIndex += incrementX * x; + } + + zIndex += incrementY * ( + !isTop || + (alpha >= 0 && alpha <= 180 || alpha < 360 && alpha > 357.5) ? // Numbers checked empirically + chart.plotHeight - y : 10 + y + ); + + if (isFront === 1) { + zIndex += incrementZ * (z); + } else if (!isFront) { + zIndex += incrementZ * (1000 - z); + } + + zIndex = -Math.round(zIndex); + + return [ + this.toLinePath(path1, true), + this.toLinePath(path2, true), + this.toLinePath(path3, true), + zIndex + ]; // #4774 }; // SECTORS // H.SVGRenderer.prototype.arc3d = function (attribs) { - var wrapper = this.g(), - renderer = wrapper.renderer, - customAttribs = ['x', 'y', 'r', 'innerR', 'start', 'end']; - - /** - * Get custom attributes. Don't mutate the original object and return an object with only custom attr. - */ - function suckOutCustom(params) { - var hasCA = false, - ca = {}; - - params = merge(params); // Don't mutate the original object - - for (var key in params) { - if (inArray(key, customAttribs) !== -1) { - ca[key] = params[key]; - delete params[key]; - hasCA = true; - } - } - return hasCA ? ca : false; - } - - attribs = merge(attribs); - - attribs.alpha *= deg2rad; - attribs.beta *= deg2rad; - - // Create the different sub sections of the shape - wrapper.top = renderer.path(); - wrapper.side1 = renderer.path(); - wrapper.side2 = renderer.path(); - wrapper.inn = renderer.path(); - wrapper.out = renderer.path(); - - /** - * Add all faces - */ - wrapper.onAdd = function () { - var parent = wrapper.parentGroup, - className = wrapper.attr('class'); - wrapper.top.add(wrapper); - - // These faces are added outside the wrapper group because the z index - // relates to neighbour elements as well - each(['out', 'inn', 'side1', 'side2'], function (face) { - wrapper[face] - .attr({ - 'class': className + ' highcharts-3d-side' - }) - .add(parent); - }); - }; - - // Cascade to faces - each(['addClass', 'removeClass'], function (fn) { - wrapper[fn] = function () { - var args = arguments; - each(['top', 'out', 'inn', 'side1', 'side2'], function (face) { - wrapper[face][fn].apply(wrapper[face], args); - }); - }; - }); - - /** - * Compute the transformed paths and set them to the composite shapes - */ - wrapper.setPaths = function (attribs) { - - var paths = wrapper.renderer.arc3dPath(attribs), - zIndex = paths.zTop * 100; - - wrapper.attribs = attribs; - - wrapper.top.attr({ d: paths.top, zIndex: paths.zTop }); - wrapper.inn.attr({ d: paths.inn, zIndex: paths.zInn }); - wrapper.out.attr({ d: paths.out, zIndex: paths.zOut }); - wrapper.side1.attr({ d: paths.side1, zIndex: paths.zSide1 }); - wrapper.side2.attr({ d: paths.side2, zIndex: paths.zSide2 }); - - - // show all children - wrapper.zIndex = zIndex; - wrapper.attr({ zIndex: zIndex }); - - // Set the radial gradient center the first time - if (attribs.center) { - wrapper.top.setRadialReference(attribs.center); - delete attribs.center; - } - }; - wrapper.setPaths(attribs); - - // Apply the fill to the top and a darker shade to the sides - wrapper.fillSetter = function (value) { - var darker = color(value).brighten(-0.1).get(); - - this.fill = value; - - this.side1.attr({ fill: darker }); - this.side2.attr({ fill: darker }); - this.inn.attr({ fill: darker }); - this.out.attr({ fill: darker }); - this.top.attr({ fill: value }); - return this; - }; - - // Apply the same value to all. These properties cascade down to the children - // when set to the composite arc3d. - each(['opacity', 'translateX', 'translateY', 'visibility'], function (setter) { - wrapper[setter + 'Setter'] = function (value, key) { - wrapper[key] = value; - each(['out', 'inn', 'side1', 'side2', 'top'], function (el) { - wrapper[el].attr(key, value); - }); - }; - }); - - /** - * Override attr to remove shape attributes and use those to set child paths - */ - wrap(wrapper, 'attr', function (proceed, params) { - var ca; - if (typeof params === 'object') { - ca = suckOutCustom(params); - if (ca) { - extend(wrapper.attribs, ca); - wrapper.setPaths(wrapper.attribs); - } - } - return proceed.apply(this, [].slice.call(arguments, 1)); - }); - - /** - * Override the animate function by sucking out custom parameters related to the shapes directly, - * and update the shapes from the animation step. - */ - wrap(wrapper, 'animate', function (proceed, params, animation, complete) { - var ca, - from = this.attribs, - to, - anim; - - // Attribute-line properties connected to 3D. These shouldn't have been in the - // attribs collection in the first place. - delete params.center; - delete params.z; - delete params.depth; - delete params.alpha; - delete params.beta; - - anim = animObject(pick(animation, this.renderer.globalAnimation)); - - if (anim.duration) { - ca = suckOutCustom(params); - // Params need to have a property in order for the step to run - // (#5765, #7437) - params.dummy = wrapper.dummy++; - - if (ca) { - to = ca; - anim.step = function (a, fx) { - function interpolate(key) { - return from[key] + (pick(to[key], from[key]) - from[key]) * fx.pos; - } - - if (fx.prop === 'dummy') { - fx.elem.setPaths(merge(from, { - x: interpolate('x'), - y: interpolate('y'), - r: interpolate('r'), - innerR: interpolate('innerR'), - start: interpolate('start'), - end: interpolate('end') - })); - } - }; - } - animation = anim; // Only when duration (#5572) - } - return proceed.call(this, params, animation, complete); - }); - wrapper.dummy = 0; - - // destroy all children - wrapper.destroy = function () { - this.top.destroy(); - this.out.destroy(); - this.inn.destroy(); - this.side1.destroy(); - this.side2.destroy(); - - SVGElement.prototype.destroy.call(this); - }; - // hide all children - wrapper.hide = function () { - this.top.hide(); - this.out.hide(); - this.inn.hide(); - this.side1.hide(); - this.side2.hide(); - }; - wrapper.show = function () { - this.top.show(); - this.out.show(); - this.inn.show(); - this.side1.show(); - this.side2.show(); - }; - return wrapper; + var wrapper = this.g(), + renderer = wrapper.renderer, + customAttribs = ['x', 'y', 'r', 'innerR', 'start', 'end']; + + /** + * Get custom attributes. Don't mutate the original object and return an object with only custom attr. + */ + function suckOutCustom(params) { + var hasCA = false, + ca = {}; + + params = merge(params); // Don't mutate the original object + + for (var key in params) { + if (inArray(key, customAttribs) !== -1) { + ca[key] = params[key]; + delete params[key]; + hasCA = true; + } + } + return hasCA ? ca : false; + } + + attribs = merge(attribs); + + attribs.alpha *= deg2rad; + attribs.beta *= deg2rad; + + // Create the different sub sections of the shape + wrapper.top = renderer.path(); + wrapper.side1 = renderer.path(); + wrapper.side2 = renderer.path(); + wrapper.inn = renderer.path(); + wrapper.out = renderer.path(); + + /** + * Add all faces + */ + wrapper.onAdd = function () { + var parent = wrapper.parentGroup, + className = wrapper.attr('class'); + wrapper.top.add(wrapper); + + // These faces are added outside the wrapper group because the z index + // relates to neighbour elements as well + each(['out', 'inn', 'side1', 'side2'], function (face) { + wrapper[face] + .attr({ + 'class': className + ' highcharts-3d-side' + }) + .add(parent); + }); + }; + + // Cascade to faces + each(['addClass', 'removeClass'], function (fn) { + wrapper[fn] = function () { + var args = arguments; + each(['top', 'out', 'inn', 'side1', 'side2'], function (face) { + wrapper[face][fn].apply(wrapper[face], args); + }); + }; + }); + + /** + * Compute the transformed paths and set them to the composite shapes + */ + wrapper.setPaths = function (attribs) { + + var paths = wrapper.renderer.arc3dPath(attribs), + zIndex = paths.zTop * 100; + + wrapper.attribs = attribs; + + wrapper.top.attr({ d: paths.top, zIndex: paths.zTop }); + wrapper.inn.attr({ d: paths.inn, zIndex: paths.zInn }); + wrapper.out.attr({ d: paths.out, zIndex: paths.zOut }); + wrapper.side1.attr({ d: paths.side1, zIndex: paths.zSide1 }); + wrapper.side2.attr({ d: paths.side2, zIndex: paths.zSide2 }); + + + // show all children + wrapper.zIndex = zIndex; + wrapper.attr({ zIndex: zIndex }); + + // Set the radial gradient center the first time + if (attribs.center) { + wrapper.top.setRadialReference(attribs.center); + delete attribs.center; + } + }; + wrapper.setPaths(attribs); + + // Apply the fill to the top and a darker shade to the sides + wrapper.fillSetter = function (value) { + var darker = color(value).brighten(-0.1).get(); + + this.fill = value; + + this.side1.attr({ fill: darker }); + this.side2.attr({ fill: darker }); + this.inn.attr({ fill: darker }); + this.out.attr({ fill: darker }); + this.top.attr({ fill: value }); + return this; + }; + + // Apply the same value to all. These properties cascade down to the children + // when set to the composite arc3d. + each(['opacity', 'translateX', 'translateY', 'visibility'], function (setter) { + wrapper[setter + 'Setter'] = function (value, key) { + wrapper[key] = value; + each(['out', 'inn', 'side1', 'side2', 'top'], function (el) { + wrapper[el].attr(key, value); + }); + }; + }); + + /** + * Override attr to remove shape attributes and use those to set child paths + */ + wrap(wrapper, 'attr', function (proceed, params) { + var ca; + if (typeof params === 'object') { + ca = suckOutCustom(params); + if (ca) { + extend(wrapper.attribs, ca); + wrapper.setPaths(wrapper.attribs); + } + } + return proceed.apply(this, [].slice.call(arguments, 1)); + }); + + /** + * Override the animate function by sucking out custom parameters related to the shapes directly, + * and update the shapes from the animation step. + */ + wrap(wrapper, 'animate', function (proceed, params, animation, complete) { + var ca, + from = this.attribs, + to, + anim; + + // Attribute-line properties connected to 3D. These shouldn't have been in the + // attribs collection in the first place. + delete params.center; + delete params.z; + delete params.depth; + delete params.alpha; + delete params.beta; + + anim = animObject(pick(animation, this.renderer.globalAnimation)); + + if (anim.duration) { + ca = suckOutCustom(params); + // Params need to have a property in order for the step to run + // (#5765, #7437) + params.dummy = wrapper.dummy++; + + if (ca) { + to = ca; + anim.step = function (a, fx) { + function interpolate(key) { + return from[key] + (pick(to[key], from[key]) - from[key]) * fx.pos; + } + + if (fx.prop === 'dummy') { + fx.elem.setPaths(merge(from, { + x: interpolate('x'), + y: interpolate('y'), + r: interpolate('r'), + innerR: interpolate('innerR'), + start: interpolate('start'), + end: interpolate('end') + })); + } + }; + } + animation = anim; // Only when duration (#5572) + } + return proceed.call(this, params, animation, complete); + }); + wrapper.dummy = 0; + + // destroy all children + wrapper.destroy = function () { + this.top.destroy(); + this.out.destroy(); + this.inn.destroy(); + this.side1.destroy(); + this.side2.destroy(); + + SVGElement.prototype.destroy.call(this); + }; + // hide all children + wrapper.hide = function () { + this.top.hide(); + this.out.hide(); + this.inn.hide(); + this.side1.hide(); + this.side2.hide(); + }; + wrapper.show = function () { + this.top.show(); + this.out.show(); + this.inn.show(); + this.side1.show(); + this.side2.show(); + }; + return wrapper; }; /** * Generate the paths required to draw a 3D arc */ SVGRenderer.prototype.arc3dPath = function (shapeArgs) { - var cx = shapeArgs.x, // x coordinate of the center - cy = shapeArgs.y, // y coordinate of the center - start = shapeArgs.start, // start angle - end = shapeArgs.end - 0.00001, // end angle - r = shapeArgs.r, // radius - ir = shapeArgs.innerR, // inner radius - d = shapeArgs.depth, // depth - alpha = shapeArgs.alpha, // alpha rotation of the chart - beta = shapeArgs.beta; // beta rotation of the chart - - // Derived Variables - var cs = Math.cos(start), // cosinus of the start angle - ss = Math.sin(start), // sinus of the start angle - ce = Math.cos(end), // cosinus of the end angle - se = Math.sin(end), // sinus of the end angle - rx = r * Math.cos(beta), // x-radius - ry = r * Math.cos(alpha), // y-radius - irx = ir * Math.cos(beta), // x-radius (inner) - iry = ir * Math.cos(alpha), // y-radius (inner) - dx = d * Math.sin(beta), // distance between top and bottom in x - dy = d * Math.sin(alpha); // distance between top and bottom in y - - // TOP - var top = ['M', cx + (rx * cs), cy + (ry * ss)]; - top = top.concat(curveTo(cx, cy, rx, ry, start, end, 0, 0)); - top = top.concat([ - 'L', cx + (irx * ce), cy + (iry * se) - ]); - top = top.concat(curveTo(cx, cy, irx, iry, end, start, 0, 0)); - top = top.concat(['Z']); - // OUTSIDE - var b = (beta > 0 ? Math.PI / 2 : 0), - a = (alpha > 0 ? 0 : Math.PI / 2); - - var start2 = start > -b ? start : (end > -b ? -b : start), - end2 = end < PI - a ? end : (start < PI - a ? PI - a : end), - midEnd = 2 * PI - a; - - // When slice goes over bottom middle, need to add both, left and right outer side. - // Additionally, when we cross right hand edge, create sharp edge. Outer shape/wall: - // - // ------- - // / ^ \ - // 4) / / \ \ 1) - // / / \ \ - // / / \ \ - // (c)=> ==== ==== <=(d) - // \ \ / / - // \ \<=(a)/ / - // \ \ / / <=(b) - // 3) \ v / 2) - // ------- - // - // (a) - inner side - // (b) - outer side - // (c) - left edge (sharp) - // (d) - right edge (sharp) - // 1..n - rendering order for startAngle = 0, when set to e.g 90, order changes clockwise (1->2, 2->3, n->1) and counterclockwise for negative startAngle - - var out = ['M', cx + (rx * cos(start2)), cy + (ry * sin(start2))]; - out = out.concat(curveTo(cx, cy, rx, ry, start2, end2, 0, 0)); - - if (end > midEnd && start < midEnd) { // When shape is wide, it can cross both, (c) and (d) edges, when using startAngle - // Go to outer side - out = out.concat([ - 'L', cx + (rx * cos(end2)) + dx, cy + (ry * sin(end2)) + dy - ]); - // Curve to the right edge of the slice (d) - out = out.concat(curveTo(cx, cy, rx, ry, end2, midEnd, dx, dy)); - // Go to the inner side - out = out.concat([ - 'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd)) - ]); - // Curve to the true end of the slice - out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end, 0, 0)); - // Go to the outer side - out = out.concat([ - 'L', cx + (rx * cos(end)) + dx, cy + (ry * sin(end)) + dy - ]); - // Go back to middle (d) - out = out.concat(curveTo(cx, cy, rx, ry, end, midEnd, dx, dy)); - out = out.concat([ - 'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd)) - ]); - // Go back to the left edge - out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end2, 0, 0)); - } else if (end > PI - a && start < PI - a) { // But shape can cross also only (c) edge: - // Go to outer side - out = out.concat([ - 'L', cx + (rx * Math.cos(end2)) + dx, cy + (ry * Math.sin(end2)) + dy - ]); - // Curve to the true end of the slice - out = out.concat(curveTo(cx, cy, rx, ry, end2, end, dx, dy)); - // Go to the inner side - out = out.concat([ - 'L', cx + (rx * Math.cos(end)), cy + (ry * Math.sin(end)) - ]); - // Go back to the artifical end2 - out = out.concat(curveTo(cx, cy, rx, ry, end, end2, 0, 0)); - } - - out = out.concat([ - 'L', cx + (rx * Math.cos(end2)) + dx, cy + (ry * Math.sin(end2)) + dy - ]); - out = out.concat(curveTo(cx, cy, rx, ry, end2, start2, dx, dy)); - out = out.concat(['Z']); - - // INSIDE - var inn = ['M', cx + (irx * cs), cy + (iry * ss)]; - inn = inn.concat(curveTo(cx, cy, irx, iry, start, end, 0, 0)); - inn = inn.concat([ - 'L', cx + (irx * Math.cos(end)) + dx, cy + (iry * Math.sin(end)) + dy - ]); - inn = inn.concat(curveTo(cx, cy, irx, iry, end, start, dx, dy)); - inn = inn.concat(['Z']); - - // SIDES - var side1 = [ - 'M', cx + (rx * cs), cy + (ry * ss), - 'L', cx + (rx * cs) + dx, cy + (ry * ss) + dy, - 'L', cx + (irx * cs) + dx, cy + (iry * ss) + dy, - 'L', cx + (irx * cs), cy + (iry * ss), - 'Z' - ]; - var side2 = [ - 'M', cx + (rx * ce), cy + (ry * se), - 'L', cx + (rx * ce) + dx, cy + (ry * se) + dy, - 'L', cx + (irx * ce) + dx, cy + (iry * se) + dy, - 'L', cx + (irx * ce), cy + (iry * se), - 'Z' - ]; - - // correction for changed position of vanishing point caused by alpha and beta rotations - var angleCorr = Math.atan2(dy, -dx), - angleEnd = Math.abs(end + angleCorr), - angleStart = Math.abs(start + angleCorr), - angleMid = Math.abs((start + end) / 2 + angleCorr); - - // set to 0-PI range - function toZeroPIRange(angle) { - angle = angle % (2 * Math.PI); - if (angle > Math.PI) { - angle = 2 * Math.PI - angle; - } - return angle; - } - angleEnd = toZeroPIRange(angleEnd); - angleStart = toZeroPIRange(angleStart); - angleMid = toZeroPIRange(angleMid); - - // *1e5 is to compensate pInt in zIndexSetter - var incPrecision = 1e5, - a1 = angleMid * incPrecision, - a2 = angleStart * incPrecision, - a3 = angleEnd * incPrecision; - - return { - top: top, - zTop: Math.PI * incPrecision + 1, // max angle is PI, so this is allways higher - out: out, - zOut: Math.max(a1, a2, a3), - inn: inn, - zInn: Math.max(a1, a2, a3), - side1: side1, - zSide1: a3 * 0.99, // to keep below zOut and zInn in case of same values - side2: side2, - zSide2: a2 * 0.99 - }; + var cx = shapeArgs.x, // x coordinate of the center + cy = shapeArgs.y, // y coordinate of the center + start = shapeArgs.start, // start angle + end = shapeArgs.end - 0.00001, // end angle + r = shapeArgs.r, // radius + ir = shapeArgs.innerR, // inner radius + d = shapeArgs.depth, // depth + alpha = shapeArgs.alpha, // alpha rotation of the chart + beta = shapeArgs.beta; // beta rotation of the chart + + // Derived Variables + var cs = Math.cos(start), // cosinus of the start angle + ss = Math.sin(start), // sinus of the start angle + ce = Math.cos(end), // cosinus of the end angle + se = Math.sin(end), // sinus of the end angle + rx = r * Math.cos(beta), // x-radius + ry = r * Math.cos(alpha), // y-radius + irx = ir * Math.cos(beta), // x-radius (inner) + iry = ir * Math.cos(alpha), // y-radius (inner) + dx = d * Math.sin(beta), // distance between top and bottom in x + dy = d * Math.sin(alpha); // distance between top and bottom in y + + // TOP + var top = ['M', cx + (rx * cs), cy + (ry * ss)]; + top = top.concat(curveTo(cx, cy, rx, ry, start, end, 0, 0)); + top = top.concat([ + 'L', cx + (irx * ce), cy + (iry * se) + ]); + top = top.concat(curveTo(cx, cy, irx, iry, end, start, 0, 0)); + top = top.concat(['Z']); + // OUTSIDE + var b = (beta > 0 ? Math.PI / 2 : 0), + a = (alpha > 0 ? 0 : Math.PI / 2); + + var start2 = start > -b ? start : (end > -b ? -b : start), + end2 = end < PI - a ? end : (start < PI - a ? PI - a : end), + midEnd = 2 * PI - a; + + // When slice goes over bottom middle, need to add both, left and right outer side. + // Additionally, when we cross right hand edge, create sharp edge. Outer shape/wall: + // + // ------- + // / ^ \ + // 4) / / \ \ 1) + // / / \ \ + // / / \ \ + // (c)=> ==== ==== <=(d) + // \ \ / / + // \ \<=(a)/ / + // \ \ / / <=(b) + // 3) \ v / 2) + // ------- + // + // (a) - inner side + // (b) - outer side + // (c) - left edge (sharp) + // (d) - right edge (sharp) + // 1..n - rendering order for startAngle = 0, when set to e.g 90, order changes clockwise (1->2, 2->3, n->1) and counterclockwise for negative startAngle + + var out = ['M', cx + (rx * cos(start2)), cy + (ry * sin(start2))]; + out = out.concat(curveTo(cx, cy, rx, ry, start2, end2, 0, 0)); + + if (end > midEnd && start < midEnd) { // When shape is wide, it can cross both, (c) and (d) edges, when using startAngle + // Go to outer side + out = out.concat([ + 'L', cx + (rx * cos(end2)) + dx, cy + (ry * sin(end2)) + dy + ]); + // Curve to the right edge of the slice (d) + out = out.concat(curveTo(cx, cy, rx, ry, end2, midEnd, dx, dy)); + // Go to the inner side + out = out.concat([ + 'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd)) + ]); + // Curve to the true end of the slice + out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end, 0, 0)); + // Go to the outer side + out = out.concat([ + 'L', cx + (rx * cos(end)) + dx, cy + (ry * sin(end)) + dy + ]); + // Go back to middle (d) + out = out.concat(curveTo(cx, cy, rx, ry, end, midEnd, dx, dy)); + out = out.concat([ + 'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd)) + ]); + // Go back to the left edge + out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end2, 0, 0)); + } else if (end > PI - a && start < PI - a) { // But shape can cross also only (c) edge: + // Go to outer side + out = out.concat([ + 'L', cx + (rx * Math.cos(end2)) + dx, cy + (ry * Math.sin(end2)) + dy + ]); + // Curve to the true end of the slice + out = out.concat(curveTo(cx, cy, rx, ry, end2, end, dx, dy)); + // Go to the inner side + out = out.concat([ + 'L', cx + (rx * Math.cos(end)), cy + (ry * Math.sin(end)) + ]); + // Go back to the artifical end2 + out = out.concat(curveTo(cx, cy, rx, ry, end, end2, 0, 0)); + } + + out = out.concat([ + 'L', cx + (rx * Math.cos(end2)) + dx, cy + (ry * Math.sin(end2)) + dy + ]); + out = out.concat(curveTo(cx, cy, rx, ry, end2, start2, dx, dy)); + out = out.concat(['Z']); + + // INSIDE + var inn = ['M', cx + (irx * cs), cy + (iry * ss)]; + inn = inn.concat(curveTo(cx, cy, irx, iry, start, end, 0, 0)); + inn = inn.concat([ + 'L', cx + (irx * Math.cos(end)) + dx, cy + (iry * Math.sin(end)) + dy + ]); + inn = inn.concat(curveTo(cx, cy, irx, iry, end, start, dx, dy)); + inn = inn.concat(['Z']); + + // SIDES + var side1 = [ + 'M', cx + (rx * cs), cy + (ry * ss), + 'L', cx + (rx * cs) + dx, cy + (ry * ss) + dy, + 'L', cx + (irx * cs) + dx, cy + (iry * ss) + dy, + 'L', cx + (irx * cs), cy + (iry * ss), + 'Z' + ]; + var side2 = [ + 'M', cx + (rx * ce), cy + (ry * se), + 'L', cx + (rx * ce) + dx, cy + (ry * se) + dy, + 'L', cx + (irx * ce) + dx, cy + (iry * se) + dy, + 'L', cx + (irx * ce), cy + (iry * se), + 'Z' + ]; + + // correction for changed position of vanishing point caused by alpha and beta rotations + var angleCorr = Math.atan2(dy, -dx), + angleEnd = Math.abs(end + angleCorr), + angleStart = Math.abs(start + angleCorr), + angleMid = Math.abs((start + end) / 2 + angleCorr); + + // set to 0-PI range + function toZeroPIRange(angle) { + angle = angle % (2 * Math.PI); + if (angle > Math.PI) { + angle = 2 * Math.PI - angle; + } + return angle; + } + angleEnd = toZeroPIRange(angleEnd); + angleStart = toZeroPIRange(angleStart); + angleMid = toZeroPIRange(angleMid); + + // *1e5 is to compensate pInt in zIndexSetter + var incPrecision = 1e5, + a1 = angleMid * incPrecision, + a2 = angleStart * incPrecision, + a3 = angleEnd * incPrecision; + + return { + top: top, + zTop: Math.PI * incPrecision + 1, // max angle is PI, so this is allways higher + out: out, + zOut: Math.max(a1, a2, a3), + inn: inn, + zInn: Math.max(a1, a2, a3), + side1: side1, + zSide1: a3 * 0.99, // to keep below zOut and zInn in case of same values + side2: side2, + zSide2: a2 * 0.99 + }; }; diff --git a/js/parts-3d/Scatter.js b/js/parts-3d/Scatter.js index caffac16b1b..8c235884ffd 100644 --- a/js/parts-3d/Scatter.js +++ b/js/parts-3d/Scatter.js @@ -9,8 +9,8 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var Point = H.Point, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; /** * A 3D scatter plot uses x, y and z coordinates to display values for three @@ -20,46 +20,46 @@ var Point = H.Point, * Simple 3D scatter * @sample {highcharts} highcharts/demo/3d-scatter-draggable * Draggable 3d scatter - * + * * @extends {plotOptions.scatter} * @product highcharts * @optionparent plotOptions.scatter3d */ seriesType('scatter3d', 'scatter', { - tooltip: { - pointFormat: 'x: {point.x}
y: {point.y}
z: {point.z}
' - } + tooltip: { + pointFormat: 'x: {point.x}
y: {point.y}
z: {point.z}
' + } // Series class }, { - pointAttribs: function (point) { - var attribs = seriesTypes.scatter.prototype.pointAttribs - .apply(this, arguments); + pointAttribs: function (point) { + var attribs = seriesTypes.scatter.prototype.pointAttribs + .apply(this, arguments); - if (this.chart.is3d() && point) { - attribs.zIndex = H.pointCameraDistance(point, this.chart); - } + if (this.chart.is3d() && point) { + attribs.zIndex = H.pointCameraDistance(point, this.chart); + } - return attribs; - }, - axisTypes: ['xAxis', 'yAxis', 'zAxis'], - pointArrayMap: ['x', 'y', 'z'], - parallelArrays: ['x', 'y', 'z'], + return attribs; + }, + axisTypes: ['xAxis', 'yAxis', 'zAxis'], + pointArrayMap: ['x', 'y', 'z'], + parallelArrays: ['x', 'y', 'z'], - // Require direct touch rather than using the k-d-tree, because the k-d-tree - // currently doesn't take the xyz coordinate system into account (#4552) - directTouch: true + // Require direct touch rather than using the k-d-tree, because the k-d-tree + // currently doesn't take the xyz coordinate system into account (#4552) + directTouch: true // Point class }, { - applyOptions: function () { - Point.prototype.applyOptions.apply(this, arguments); - if (this.z === undefined) { - this.z = 0; - } + applyOptions: function () { + Point.prototype.applyOptions.apply(this, arguments); + if (this.z === undefined) { + this.z = 0; + } - return this; - } + return this; + } }); @@ -67,9 +67,9 @@ seriesType('scatter3d', 'scatter', { /** * A `scatter3d` series. If the [type](#series.scatter3d.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * scatter3d](#plotOptions.scatter3d). - * + * * @type {Object} * @extends series,plotOptions.scatter3d * @product highcharts @@ -79,11 +79,11 @@ seriesType('scatter3d', 'scatter', { /** * An array of data points for the series. For the `scatter3d` series * type, points can be given in the following ways: - * + * * 1. An array of arrays with 3 values. In this case, the values correspond * to `x,y,z`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 0, 1], @@ -91,13 +91,13 @@ seriesType('scatter3d', 'scatter', { * [2, 9, 2] * ] * ``` - * + * * 3. 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.scatter3d.turboThreshold), this option is not * available. - * + * * ```js * data: [{ * x: 1, @@ -113,7 +113,7 @@ seriesType('scatter3d', 'scatter', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.scatter.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -125,14 +125,14 @@ seriesType('scatter3d', 'scatter', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts * @apioption series.scatter3d.data */ /** * The z value for each data point. - * + * * @type {Number} * @product highcharts * @apioption series.scatter3d.data.z diff --git a/js/parts-3d/Series.js b/js/parts-3d/Series.js index c7fa0248af8..919fc8015ec 100644 --- a/js/parts-3d/Series.js +++ b/js/parts-3d/Series.js @@ -9,65 +9,65 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var addEvent = H.addEvent, - perspective = H.perspective, - pick = H.pick; + perspective = H.perspective, + pick = H.pick; // Wrap the translate method to post-translate points into 3D perspective addEvent(H.Series, 'afterTranslate', function () { - if (this.chart.is3d()) { - this.translate3dPoints(); - } + if (this.chart.is3d()) { + this.translate3dPoints(); + } }); /** * Translate the plotX, plotY properties and add plotZ. */ H.Series.prototype.translate3dPoints = function () { - var series = this, - chart = series.chart, - zAxis = pick(series.zAxis, chart.options.zAxis[0]), - rawPoints = [], - rawPoint, - projectedPoints, - projectedPoint, - zValue, - i; + var series = this, + chart = series.chart, + zAxis = pick(series.zAxis, chart.options.zAxis[0]), + rawPoints = [], + rawPoint, + projectedPoints, + projectedPoint, + zValue, + i; - for (i = 0; i < series.data.length; i++) { - rawPoint = series.data[i]; - - if (zAxis && zAxis.translate) { - zValue = zAxis.isLog && zAxis.val2lin ? - zAxis.val2lin(rawPoint.z) : - rawPoint.z; // #4562 - rawPoint.plotZ = zAxis.translate(zValue); - rawPoint.isInside = rawPoint.isInside ? - (zValue >= zAxis.min && zValue <= zAxis.max) : - false; - } else { - rawPoint.plotZ = 0; - } + for (i = 0; i < series.data.length; i++) { + rawPoint = series.data[i]; - rawPoints.push({ - x: pick(rawPoint.plotXold, rawPoint.plotX), - y: pick(rawPoint.plotYold, rawPoint.plotY), - z: pick(rawPoint.plotZold, rawPoint.plotZ) - }); - } + if (zAxis && zAxis.translate) { + zValue = zAxis.isLog && zAxis.val2lin ? + zAxis.val2lin(rawPoint.z) : + rawPoint.z; // #4562 + rawPoint.plotZ = zAxis.translate(zValue); + rawPoint.isInside = rawPoint.isInside ? + (zValue >= zAxis.min && zValue <= zAxis.max) : + false; + } else { + rawPoint.plotZ = 0; + } - projectedPoints = perspective(rawPoints, chart, true); + rawPoints.push({ + x: pick(rawPoint.plotXold, rawPoint.plotX), + y: pick(rawPoint.plotYold, rawPoint.plotY), + z: pick(rawPoint.plotZold, rawPoint.plotZ) + }); + } - for (i = 0; i < series.data.length; i++) { - rawPoint = series.data[i]; - projectedPoint = projectedPoints[i]; + projectedPoints = perspective(rawPoints, chart, true); - rawPoint.plotXold = rawPoint.plotX; - rawPoint.plotYold = rawPoint.plotY; - rawPoint.plotZold = rawPoint.plotZ; + for (i = 0; i < series.data.length; i++) { + rawPoint = series.data[i]; + projectedPoint = projectedPoints[i]; - rawPoint.plotX = projectedPoint.x; - rawPoint.plotY = projectedPoint.y; - rawPoint.plotZ = projectedPoint.z; - } + rawPoint.plotXold = rawPoint.plotX; + rawPoint.plotYold = rawPoint.plotY; + rawPoint.plotZold = rawPoint.plotZ; + + rawPoint.plotX = projectedPoint.x; + rawPoint.plotY = projectedPoint.y; + rawPoint.plotZ = projectedPoint.z; + } }; diff --git a/js/parts-3d/VMLRenderer.js b/js/parts-3d/VMLRenderer.js index b303459114f..95c2fa90dae 100644 --- a/js/parts-3d/VMLRenderer.js +++ b/js/parts-3d/VMLRenderer.js @@ -10,52 +10,52 @@ import '../parts/Axis.js'; import '../parts/SvgRenderer.js'; /*= if (build.classic) { =*/ var addEvent = H.addEvent, - Axis = H.Axis, - SVGRenderer = H.SVGRenderer, - VMLRenderer = H.VMLRenderer; + Axis = H.Axis, + SVGRenderer = H.SVGRenderer, + VMLRenderer = H.VMLRenderer; /** - * Extension to the VML Renderer + * Extension to the VML Renderer */ if (VMLRenderer) { - H.setOptions({ animate: false }); - - VMLRenderer.prototype.face3d = SVGRenderer.prototype.face3d; - VMLRenderer.prototype.polyhedron = SVGRenderer.prototype.polyhedron; - VMLRenderer.prototype.cuboid = SVGRenderer.prototype.cuboid; - VMLRenderer.prototype.cuboidPath = SVGRenderer.prototype.cuboidPath; - - VMLRenderer.prototype.toLinePath = SVGRenderer.prototype.toLinePath; - VMLRenderer.prototype.toLineSegments = SVGRenderer.prototype.toLineSegments; - - VMLRenderer.prototype.createElement3D = - SVGRenderer.prototype.createElement3D; - - VMLRenderer.prototype.arc3d = function (shapeArgs) { - var result = SVGRenderer.prototype.arc3d.call(this, shapeArgs); - result.css({ zIndex: result.zIndex }); - return result; - }; - - H.VMLRenderer.prototype.arc3dPath = H.SVGRenderer.prototype.arc3dPath; - - addEvent(Axis, 'render', function () { - - // VML doesn't support a negative z-index - if (this.sideFrame) { - this.sideFrame.css({ zIndex: 0 }); - this.sideFrame.front.attr({ fill: this.sideFrame.color }); - } - if (this.bottomFrame) { - this.bottomFrame.css({ zIndex: 1 }); - this.bottomFrame.front.attr({ fill: this.bottomFrame.color }); - } - if (this.backFrame) { - this.backFrame.css({ zIndex: 0 }); - this.backFrame.front.attr({ fill: this.backFrame.color }); - } - }); + H.setOptions({ animate: false }); + + VMLRenderer.prototype.face3d = SVGRenderer.prototype.face3d; + VMLRenderer.prototype.polyhedron = SVGRenderer.prototype.polyhedron; + VMLRenderer.prototype.cuboid = SVGRenderer.prototype.cuboid; + VMLRenderer.prototype.cuboidPath = SVGRenderer.prototype.cuboidPath; + + VMLRenderer.prototype.toLinePath = SVGRenderer.prototype.toLinePath; + VMLRenderer.prototype.toLineSegments = SVGRenderer.prototype.toLineSegments; + + VMLRenderer.prototype.createElement3D = + SVGRenderer.prototype.createElement3D; + + VMLRenderer.prototype.arc3d = function (shapeArgs) { + var result = SVGRenderer.prototype.arc3d.call(this, shapeArgs); + result.css({ zIndex: result.zIndex }); + return result; + }; + + H.VMLRenderer.prototype.arc3dPath = H.SVGRenderer.prototype.arc3dPath; + + addEvent(Axis, 'render', function () { + + // VML doesn't support a negative z-index + if (this.sideFrame) { + this.sideFrame.css({ zIndex: 0 }); + this.sideFrame.front.attr({ fill: this.sideFrame.color }); + } + if (this.bottomFrame) { + this.bottomFrame.css({ zIndex: 1 }); + this.bottomFrame.front.attr({ fill: this.bottomFrame.color }); + } + if (this.backFrame) { + this.backFrame.css({ zIndex: 0 }); + this.backFrame.front.attr({ fill: this.backFrame.color }); + } + }); } /*= } =*/ diff --git a/js/parts-gantt/grid-axis.js b/js/parts-gantt/grid-axis.js index d9ca8db5e9c..613c84789e5 100644 --- a/js/parts-gantt/grid-axis.js +++ b/js/parts-gantt/grid-axis.js @@ -8,25 +8,25 @@ import H from '../parts/Globals.js'; var each = H.each, - isObject = H.isObject, - pick = H.pick, - wrap = H.wrap, - Axis = H.Axis, - Chart = H.Chart, - Tick = H.Tick; + isObject = H.isObject, + pick = H.pick, + wrap = H.wrap, + Axis = H.Axis, + Chart = H.Chart, + Tick = H.Tick; // Enum for which side the axis is on. // Maps to axis.side var axisSide = { - top: 0, - right: 1, - bottom: 2, - left: 3, - 0: 'top', - 1: 'right', - 2: 'bottom', - 3: 'left' + top: 0, + right: 1, + bottom: 2, + left: 3, + 0: 'top', + 1: 'right', + 2: 'bottom', + 3: 'left' }; /** @@ -39,32 +39,32 @@ var axisSide = { * of the x-axes. * * @return true if the axis is the outermost axis in its dimension; - * false if not + * false if not */ Axis.prototype.isOuterAxis = function () { - var axis = this, - thisIndex = -1, - isOuter = true; - - each(this.chart.axes, function (otherAxis, index) { - if (otherAxis.side === axis.side) { - if (otherAxis === axis) { - // Get the index of the axis in question - thisIndex = index; - - // Check thisIndex >= 0 in case thisIndex has - // not been found yet - } else if (thisIndex >= 0 && index > thisIndex) { - // There was an axis on the same side with a - // higher index. Exit the loop. - isOuter = false; - return; - } - } - }); - // There were either no other axes on the same side, - // or the other axes were not farther from the chart - return isOuter; + var axis = this, + thisIndex = -1, + isOuter = true; + + each(this.chart.axes, function (otherAxis, index) { + if (otherAxis.side === axis.side) { + if (otherAxis === axis) { + // Get the index of the axis in question + thisIndex = index; + + // Check thisIndex >= 0 in case thisIndex has + // not been found yet + } else if (thisIndex >= 0 && index > thisIndex) { + // There was an axis on the same side with a + // higher index. Exit the loop. + isOuter = false; + return; + } + } + }); + // There were either no other axes on the same side, + // or the other axes were not farther from the chart + return isOuter; }; /** @@ -73,7 +73,7 @@ Axis.prototype.isOuterAxis = function () { * @return {number} width - the width of the tick label */ Tick.prototype.getLabelWidth = function () { - return this.label.getBBox().width; + return this.label.getBBox().width; }; /** @@ -86,102 +86,102 @@ Tick.prototype.getLabelWidth = function () { * @return {number} maxLabelLength - the maximum label length of the axis */ Axis.prototype.getMaxLabelLength = function (force) { - var tickPositions = this.tickPositions, - ticks = this.ticks, - maxLabelLength = 0; - - if (!this.maxLabelLength || force) { - each(tickPositions, function (tick) { - tick = ticks[tick]; - if (tick && tick.labelLength > maxLabelLength) { - maxLabelLength = tick.labelLength; - } - }); - this.maxLabelLength = maxLabelLength; - } - return this.maxLabelLength; + var tickPositions = this.tickPositions, + ticks = this.ticks, + maxLabelLength = 0; + + if (!this.maxLabelLength || force) { + each(tickPositions, function (tick) { + tick = ticks[tick]; + if (tick && tick.labelLength > maxLabelLength) { + maxLabelLength = tick.labelLength; + } + }); + this.maxLabelLength = maxLabelLength; + } + return this.maxLabelLength; }; /** * Adds the axis defined in axis.options.title */ Axis.prototype.addTitle = function () { - var axis = this, - renderer = axis.chart.renderer, - axisParent = axis.axisParent, - horiz = axis.horiz, - opposite = axis.opposite, - options = axis.options, - axisTitleOptions = options.title, - hasData, - showAxis, - textAlign; - - // For reuse in Axis.render - hasData = axis.hasData(); - axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); - - // Disregard title generation in original Axis.getOffset() - options.title = ''; - - if (!axis.axisTitle) { - textAlign = axisTitleOptions.textAlign; - if (!textAlign) { - textAlign = (horiz ? { - low: 'left', - middle: 'center', - high: 'right' - } : { - low: opposite ? 'right' : 'left', - middle: 'center', - high: opposite ? 'left' : 'right' - })[axisTitleOptions.align]; - } - axis.axisTitle = renderer.text( - axisTitleOptions.text, - 0, - 0, - axisTitleOptions.useHTML - ) - .attr({ - zIndex: 7, - rotation: axisTitleOptions.rotation || 0, - align: textAlign - }) - .addClass('highcharts-axis-title') - /*= if (build.classic) { =*/ - .css(axisTitleOptions.style) - /*= } =*/ - // Add to axisParent instead of axisGroup, to ignore the space - // it takes - .add(axisParent); - axis.axisTitle.isNew = true; - } - - - // hide or show the title depending on whether showEmpty is set - axis.axisTitle[showAxis ? 'show' : 'hide'](true); + var axis = this, + renderer = axis.chart.renderer, + axisParent = axis.axisParent, + horiz = axis.horiz, + opposite = axis.opposite, + options = axis.options, + axisTitleOptions = options.title, + hasData, + showAxis, + textAlign; + + // For reuse in Axis.render + hasData = axis.hasData(); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + + // Disregard title generation in original Axis.getOffset() + options.title = ''; + + if (!axis.axisTitle) { + textAlign = axisTitleOptions.textAlign; + if (!textAlign) { + textAlign = (horiz ? { + low: 'left', + middle: 'center', + high: 'right' + } : { + low: opposite ? 'right' : 'left', + middle: 'center', + high: opposite ? 'left' : 'right' + })[axisTitleOptions.align]; + } + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: textAlign + }) + .addClass('highcharts-axis-title') + /*= if (build.classic) { =*/ + .css(axisTitleOptions.style) + /*= } =*/ + // Add to axisParent instead of axisGroup, to ignore the space + // it takes + .add(axisParent); + axis.axisTitle.isNew = true; + } + + + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[showAxis ? 'show' : 'hide'](true); }; /** * Add custom date formats */ H.dateFormats = { - // Week number - W: function (timestamp) { - var date = new this.Date(timestamp), - day = this.get('Day', date) === 0 ? 7 : this.get('Day', date), - time = date.getTime(), - startOfYear = new Date(this.get('FullYear', date), 0, 1, -6), - dayNumber; - this.set('Date', date, this.get('Date', date) + 4 - day); - dayNumber = Math.floor((time - startOfYear) / 86400000); - return 1 + Math.floor(dayNumber / 7); - }, - // First letter of the day of the week, e.g. 'M' for 'Monday'. - E: function (timestamp) { - return this.dateFormat('%a', timestamp, true).charAt(0); - } + // Week number + W: function (timestamp) { + var date = new this.Date(timestamp), + day = this.get('Day', date) === 0 ? 7 : this.get('Day', date), + time = date.getTime(), + startOfYear = new Date(this.get('FullYear', date), 0, 1, -6), + dayNumber; + this.set('Date', date, this.get('Date', date) + 4 - day); + dayNumber = Math.floor((time - startOfYear) / 86400000); + return 1 + Math.floor(dayNumber / 7); + }, + // First letter of the day of the week, e.g. 'M' for 'Monday'. + E: function (timestamp) { + return this.dateFormat('%a', timestamp, true).charAt(0); + } }; /** @@ -194,15 +194,15 @@ H.dateFormats = { * @param {function} proceed - the original function */ wrap(Tick.prototype, 'addLabel', function (proceed) { - var axis = this.axis, - isCategoryAxis = axis.options.categories !== undefined, - tickPositions = axis.tickPositions, - lastTick = tickPositions[tickPositions.length - 1], - isLastTick = this.pos !== lastTick; - - if (!axis.options.grid || isCategoryAxis || isLastTick) { - proceed.apply(this); - } + var axis = this.axis, + isCategoryAxis = axis.options.categories !== undefined, + tickPositions = axis.tickPositions, + lastTick = tickPositions[tickPositions.length - 1], + isLastTick = this.pos !== lastTick; + + if (!axis.options.grid || isCategoryAxis || isLastTick) { + proceed.apply(this); + } }); /** @@ -211,59 +211,59 @@ wrap(Tick.prototype, 'addLabel', function (proceed) { * @param {function} proceed - the original function * * @return {object} object - an object containing x and y positions - * for the tick + * for the tick */ wrap(Tick.prototype, 'getLabelPosition', function (proceed, x, y, label) { - var retVal = proceed.apply(this, Array.prototype.slice.call(arguments, 1)), - axis = this.axis, - options = axis.options, - tickInterval = options.tickInterval || 1, - newX, - newPos, - axisHeight, - fontSize, - labelMetrics, - lblB, - lblH, - labelCenter; - - // Only center tick labels if axis has option grid: true - if (options.grid) { - fontSize = options.labels.style.fontSize; - labelMetrics = axis.chart.renderer.fontMetrics(fontSize, label); - lblB = labelMetrics.b; - lblH = labelMetrics.h; - - if (axis.horiz && options.categories === undefined) { - // Center x position - axisHeight = axis.axisGroup.getBBox().height; - newPos = this.pos + tickInterval / 2; - retVal.x = axis.translate(newPos) + axis.left; - labelCenter = (axisHeight / 2) + (lblH / 2) - Math.abs(lblH - lblB); - - // Center y position - if (axis.side === axisSide.top) { - retVal.y = y - labelCenter; - } else { - retVal.y = y + labelCenter; - } - } else { - // Center y position - if (options.categories === undefined) { - newPos = this.pos + (tickInterval / 2); - retVal.y = axis.translate(newPos) + axis.top + (lblB / 2); - } - - // Center x position - newX = (this.getLabelWidth() / 2) - (axis.maxLabelLength / 2); - if (axis.side === axisSide.left) { - retVal.x += newX; - } else { - retVal.x -= newX; - } - } - } - return retVal; + var retVal = proceed.apply(this, Array.prototype.slice.call(arguments, 1)), + axis = this.axis, + options = axis.options, + tickInterval = options.tickInterval || 1, + newX, + newPos, + axisHeight, + fontSize, + labelMetrics, + lblB, + lblH, + labelCenter; + + // Only center tick labels if axis has option grid: true + if (options.grid) { + fontSize = options.labels.style.fontSize; + labelMetrics = axis.chart.renderer.fontMetrics(fontSize, label); + lblB = labelMetrics.b; + lblH = labelMetrics.h; + + if (axis.horiz && options.categories === undefined) { + // Center x position + axisHeight = axis.axisGroup.getBBox().height; + newPos = this.pos + tickInterval / 2; + retVal.x = axis.translate(newPos) + axis.left; + labelCenter = (axisHeight / 2) + (lblH / 2) - Math.abs(lblH - lblB); + + // Center y position + if (axis.side === axisSide.top) { + retVal.y = y - labelCenter; + } else { + retVal.y = y + labelCenter; + } + } else { + // Center y position + if (options.categories === undefined) { + newPos = this.pos + (tickInterval / 2); + retVal.y = axis.translate(newPos) + axis.top + (lblB / 2); + } + + // Center x position + newX = (this.getLabelWidth() / 2) - (axis.maxLabelLength / 2); + if (axis.side === axisSide.left) { + retVal.x += newX; + } else { + retVal.x -= newX; + } + } + } + return retVal; }); @@ -275,21 +275,21 @@ wrap(Tick.prototype, 'getLabelPosition', function (proceed, x, y, label) { * @returns {array} retVal - */ wrap(Axis.prototype, 'tickSize', function (proceed) { - var axis = this, - retVal = proceed.apply(axis, Array.prototype.slice.call(arguments, 1)), - labelPadding, - distance; - - if (axis.options.grid && !axis.horiz) { - labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2); - if (!axis.maxLabelLength) { - axis.maxLabelLength = axis.getMaxLabelLength(); - } - distance = axis.maxLabelLength + labelPadding; - - retVal[0] = distance; - } - return retVal; + var axis = this, + retVal = proceed.apply(axis, Array.prototype.slice.call(arguments, 1)), + labelPadding, + distance; + + if (axis.options.grid && !axis.horiz) { + labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2); + if (!axis.maxLabelLength) { + axis.maxLabelLength = axis.getMaxLabelLength(); + } + distance = axis.maxLabelLength + labelPadding; + + retVal[0] = distance; + } + return retVal; }); /** @@ -300,41 +300,41 @@ wrap(Axis.prototype, 'tickSize', function (proceed) { * @param {function} proceed - the original function */ wrap(Axis.prototype, 'getOffset', function (proceed) { - var axis = this, - axisOffset = axis.chart.axisOffset, - side = axis.side, - axisHeight, - tickSize, - options = axis.options, - axisTitleOptions = options.title, - addTitle = axisTitleOptions && - axisTitleOptions.text && - axisTitleOptions.enabled !== false; + var axis = this, + axisOffset = axis.chart.axisOffset, + side = axis.side, + axisHeight, + tickSize, + options = axis.options, + axisTitleOptions = options.title, + addTitle = axisTitleOptions && + axisTitleOptions.text && + axisTitleOptions.enabled !== false; - if (axis.options.grid && isObject(axis.options.title)) { + if (axis.options.grid && isObject(axis.options.title)) { - tickSize = axis.tickSize('tick')[0]; - if (axisOffset[side] && tickSize) { - axisHeight = axisOffset[side] + tickSize; - } + tickSize = axis.tickSize('tick')[0]; + if (axisOffset[side] && tickSize) { + axisHeight = axisOffset[side] + tickSize; + } - if (addTitle) { - // Use the custom addTitle() to add it, while preventing making room - // for it - axis.addTitle(); - } + if (addTitle) { + // Use the custom addTitle() to add it, while preventing making room + // for it + axis.addTitle(); + } - proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); - axisOffset[side] = pick(axisHeight, axisOffset[side]); + axisOffset[side] = pick(axisHeight, axisOffset[side]); - // Put axis options back after original Axis.getOffset() has been called - options.title = axisTitleOptions; + // Put axis options back after original Axis.getOffset() has been called + options.title = axisTitleOptions; - } else { - proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); - } + } else { + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + } }); /** @@ -344,11 +344,11 @@ wrap(Axis.prototype, 'getOffset', function (proceed) { * @param {function} proceed - the original function */ wrap(Axis.prototype, 'renderUnsquish', function (proceed) { - if (this.options.grid) { - this.labelRotation = 0; - this.options.labels.rotation = 0; - } - proceed.apply(this); + if (this.options.grid) { + this.labelRotation = 0; + this.options.labels.rotation = 0; + } + proceed.apply(this); }); /** @@ -357,13 +357,13 @@ wrap(Axis.prototype, 'renderUnsquish', function (proceed) { * @param {function} proceed - the original function */ wrap(Axis.prototype, 'setOptions', function (proceed, userOptions) { - var axis = this; - if (userOptions.grid && axis.horiz) { - userOptions.startOnTick = true; - userOptions.minPadding = 0; - userOptions.endOnTick = true; - } - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + var axis = this; + if (userOptions.grid && axis.horiz) { + userOptions.startOnTick = true; + userOptions.minPadding = 0; + userOptions.endOnTick = true; + } + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); }); /** @@ -373,103 +373,103 @@ wrap(Axis.prototype, 'setOptions', function (proceed, userOptions) { * @param {function} proceed - the original function */ wrap(Axis.prototype, 'render', function (proceed) { - var axis = this, - options = axis.options, - labelPadding, - distance, - lineWidth, - linePath, - yStartIndex, - yEndIndex, - xStartIndex, - xEndIndex, - renderer = axis.chart.renderer, - axisGroupBox; - - if (options.grid) { - labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2); - distance = axis.maxLabelLength + labelPadding; - lineWidth = options.lineWidth; - - // Remove right wall before rendering - if (axis.rightWall) { - axis.rightWall.destroy(); - } - - // Call original Axis.render() to obtain axis.axisLine and - // axis.axisGroup - proceed.apply(axis); - - axisGroupBox = axis.axisGroup.getBBox(); - - // Add right wall on horizontal axes - if (axis.horiz) { - axis.rightWall = renderer.path([ - 'M', - axisGroupBox.x + axis.width + 1, // account for left wall - axisGroupBox.y, - 'L', - axisGroupBox.x + axis.width + 1, // account for left wall - axisGroupBox.y + axisGroupBox.height - ]) - .attr({ - stroke: options.tickColor || '#ccd6eb', - 'stroke-width': options.tickWidth || 1, - zIndex: 7, - class: 'grid-wall' - }) - .add(axis.axisGroup); - } - - if (axis.isOuterAxis() && axis.axisLine) { - if (axis.horiz) { - // -1 to avoid adding distance each time the chart updates - distance = axisGroupBox.height - 1; - } - - if (lineWidth) { - linePath = axis.getLinePath(lineWidth); - xStartIndex = linePath.indexOf('M') + 1; - xEndIndex = linePath.indexOf('L') + 1; - yStartIndex = linePath.indexOf('M') + 2; - yEndIndex = linePath.indexOf('L') + 2; - - // Negate distance if top or left axis - if (axis.side === axisSide.top || axis.side === axisSide.left) { - distance = -distance; - } - - // If axis is horizontal, reposition line path vertically - if (axis.horiz) { - linePath[yStartIndex] = linePath[yStartIndex] + distance; - linePath[yEndIndex] = linePath[yEndIndex] + distance; - } else { - // If axis is vertical, reposition line path horizontally - linePath[xStartIndex] = linePath[xStartIndex] + distance; - linePath[xEndIndex] = linePath[xEndIndex] + distance; - } - - if (!axis.axisLineExtra) { - axis.axisLineExtra = renderer.path(linePath) - .attr({ - stroke: options.lineColor, - 'stroke-width': lineWidth, - zIndex: 7 - }) - .add(axis.axisGroup); - } else { - axis.axisLineExtra.animate({ - d: linePath - }); - } - - // show or hide the line depending on options.showEmpty - axis.axisLine[axis.showAxis ? 'show' : 'hide'](true); - } - } - } else { - proceed.apply(axis); - } + var axis = this, + options = axis.options, + labelPadding, + distance, + lineWidth, + linePath, + yStartIndex, + yEndIndex, + xStartIndex, + xEndIndex, + renderer = axis.chart.renderer, + axisGroupBox; + + if (options.grid) { + labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2); + distance = axis.maxLabelLength + labelPadding; + lineWidth = options.lineWidth; + + // Remove right wall before rendering + if (axis.rightWall) { + axis.rightWall.destroy(); + } + + // Call original Axis.render() to obtain axis.axisLine and + // axis.axisGroup + proceed.apply(axis); + + axisGroupBox = axis.axisGroup.getBBox(); + + // Add right wall on horizontal axes + if (axis.horiz) { + axis.rightWall = renderer.path([ + 'M', + axisGroupBox.x + axis.width + 1, // account for left wall + axisGroupBox.y, + 'L', + axisGroupBox.x + axis.width + 1, // account for left wall + axisGroupBox.y + axisGroupBox.height + ]) + .attr({ + stroke: options.tickColor || '#ccd6eb', + 'stroke-width': options.tickWidth || 1, + zIndex: 7, + class: 'grid-wall' + }) + .add(axis.axisGroup); + } + + if (axis.isOuterAxis() && axis.axisLine) { + if (axis.horiz) { + // -1 to avoid adding distance each time the chart updates + distance = axisGroupBox.height - 1; + } + + if (lineWidth) { + linePath = axis.getLinePath(lineWidth); + xStartIndex = linePath.indexOf('M') + 1; + xEndIndex = linePath.indexOf('L') + 1; + yStartIndex = linePath.indexOf('M') + 2; + yEndIndex = linePath.indexOf('L') + 2; + + // Negate distance if top or left axis + if (axis.side === axisSide.top || axis.side === axisSide.left) { + distance = -distance; + } + + // If axis is horizontal, reposition line path vertically + if (axis.horiz) { + linePath[yStartIndex] = linePath[yStartIndex] + distance; + linePath[yEndIndex] = linePath[yEndIndex] + distance; + } else { + // If axis is vertical, reposition line path horizontally + linePath[xStartIndex] = linePath[xStartIndex] + distance; + linePath[xEndIndex] = linePath[xEndIndex] + distance; + } + + if (!axis.axisLineExtra) { + axis.axisLineExtra = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(axis.axisGroup); + } else { + axis.axisLineExtra.animate({ + d: linePath + }); + } + + // show or hide the line depending on options.showEmpty + axis.axisLine[axis.showAxis ? 'show' : 'hide'](true); + } + } + } else { + proceed.apply(axis); + } }); /** @@ -480,47 +480,47 @@ wrap(Axis.prototype, 'render', function (proceed) { * @param {function} proceed - the original function */ wrap(Chart.prototype, 'render', function (proceed) { - // 25 is optimal height for default fontSize (11px) - // 25 / 11 ≈ 2.28 - var fontSizeToCellHeightRatio = 25 / 11, - fontMetrics, - fontSize; - - each(this.axes, function (axis) { - var options = axis.options; - if (options.grid) { - fontSize = options.labels.style.fontSize; - fontMetrics = axis.chart.renderer.fontMetrics(fontSize); - - // Prohibit timespans of multitudes of a time unit, - // e.g. two days, three weeks, etc. - if (options.type === 'datetime') { - options.units = [ - ['millisecond', [1]], - ['second', [1]], - ['minute', [1]], - ['hour', [1]], - ['day', [1]], - ['week', [1]], - ['month', [1]], - ['year', null] - ]; - } - - // Make tick marks taller, creating cell walls of a grid. - // Use cellHeight axis option if set - if (axis.horiz) { - options.tickLength = options.cellHeight || - fontMetrics.h * fontSizeToCellHeightRatio; - } else { - options.tickWidth = 1; - if (!options.lineWidth) { - options.lineWidth = 1; - } - } - } - }); - - // Call original Chart.render() - proceed.apply(this); + // 25 is optimal height for default fontSize (11px) + // 25 / 11 ≈ 2.28 + var fontSizeToCellHeightRatio = 25 / 11, + fontMetrics, + fontSize; + + each(this.axes, function (axis) { + var options = axis.options; + if (options.grid) { + fontSize = options.labels.style.fontSize; + fontMetrics = axis.chart.renderer.fontMetrics(fontSize); + + // Prohibit timespans of multitudes of a time unit, + // e.g. two days, three weeks, etc. + if (options.type === 'datetime') { + options.units = [ + ['millisecond', [1]], + ['second', [1]], + ['minute', [1]], + ['hour', [1]], + ['day', [1]], + ['week', [1]], + ['month', [1]], + ['year', null] + ]; + } + + // Make tick marks taller, creating cell walls of a grid. + // Use cellHeight axis option if set + if (axis.horiz) { + options.tickLength = options.cellHeight || + fontMetrics.h * fontSizeToCellHeightRatio; + } else { + options.tickWidth = 1; + if (!options.lineWidth) { + options.lineWidth = 1; + } + } + } + }); + + // Call original Chart.render() + proceed.apply(this); }); diff --git a/js/parts-map/ColorAxis.js b/js/parts-map/ColorAxis.js index a4a47f85ffa..ebd33e98cfe 100644 --- a/js/parts-map/ColorAxis.js +++ b/js/parts-map/ColorAxis.js @@ -12,991 +12,991 @@ import '../parts/Chart.js'; import '../parts/Color.js'; import '../parts/Legend.js'; var addEvent = H.addEvent, - Axis = H.Axis, - Chart = H.Chart, - color = H.color, - ColorAxis, - each = H.each, - extend = H.extend, - isNumber = H.isNumber, - Legend = H.Legend, - LegendSymbolMixin = H.LegendSymbolMixin, - noop = H.noop, - merge = H.merge, - pick = H.pick; + Axis = H.Axis, + Chart = H.Chart, + color = H.color, + ColorAxis, + each = H.each, + extend = H.extend, + isNumber = H.isNumber, + Legend = H.Legend, + LegendSymbolMixin = H.LegendSymbolMixin, + noop = H.noop, + merge = H.merge, + pick = H.pick; // If ColorAxis already exists, we may be loading the heatmap module on top of // Highmaps. -if (!H.ColorAxis) { - - /** - * The ColorAxis object for inclusion in gradient legends - */ - ColorAxis = H.ColorAxis = function () { - this.init.apply(this, arguments); - }; - extend(ColorAxis.prototype, Axis.prototype); - extend(ColorAxis.prototype, { - /** - * A color axis for choropleth maps and heat maps. Visually, the color axis - * will appear as a gradient or as separate items inside the legend, - * depending on whether the axis is scalar or based on data classes. - * - * For supported color formats, see the - * [docs article about colors](http://www.highcharts.com/docs/chart-design-and-style/colors). - * - * A scalar color axis is represented by a gradient. The colors either range - * between the [minColor](#colorAxis.minColor) and the [maxColor](#colorAxis.maxColor), - * or for more fine grained control the colors can be - * defined in [stops](#colorAxis.stops). Often times, the color axis needs - * to be adjusted to get the right color spread for the data. In addition to - * stops, consider using a logarithmic [axis type](#colorAxis.type), or - * setting [min](#colorAxis.min) and [max](#colorAxis.max) to avoid the - * colors being determined by outliers. - * - * When [dataClasses](#colorAxis.dataClasses) are used, the ranges are - * subdivided into separate classes like categories based on their values. - * This can be used for ranges between two values, but also for a true - * category. However, when your data is categorized, it may be as convenient - * to add each category to a separate series. - * - * See [the Axis object](#Axis) for programmatic access to the axis. - * @extends {xAxis} - * @excluding allowDecimals,alternateGridColor,breaks,categories,crosshair, - * dateTimeLabelFormats,lineWidth,linkedTo,maxZoom,minRange, - * minTickInterval,offset,opposite,plotBands,plotLines,showEmpty, - * title - * @product highcharts highmaps - * @optionparent colorAxis - */ - defaultColorAxisOptions: { - - /** - * Whether to allow decimals on the color axis. - * @type {Boolean} - * @default true - * @product highcharts highmaps - * @apioption colorAxis.allowDecimals - */ - - /** - * Determines how to set each data class' color if no individual color - * is set. The default value, `tween`, computes intermediate colors - * between `minColor` and `maxColor`. The other possible value, `category`, - * pulls colors from the global or chart specific [colors](#colors) - * array. - * - * @validvalue ["tween", "category"] - * @type {String} - * @sample {highmaps} maps/coloraxis/dataclasscolor/ Category colors - * @default tween - * @product highcharts highmaps - * @apioption colorAxis.dataClassColor - */ - - /** - * An array of data classes or ranges for the choropleth map. If none - * given, the color axis is scalar and values are distributed as a gradient - * between the minimum and maximum colors. - * - * @type {Array} - * @sample {highmaps} maps/demo/data-class-ranges/ Multiple ranges - * @sample {highmaps} maps/demo/data-class-two-ranges/ Two ranges - * @product highcharts highmaps - * @apioption colorAxis.dataClasses - */ - - /** - * The color of each data class. If not set, the color is pulled from - * the global or chart-specific [colors](#colors) array. In - * styled mode, this option is ignored. Instead, use colors defined in - * CSS. - * - * @type {Color} - * @sample {highmaps} maps/demo/data-class-two-ranges/ Explicit colors - * @product highcharts highmaps - * @apioption colorAxis.dataClasses.color - */ - - /** - * The start of the value range that the data class represents, - * relating to the point value. - * - * The range of each `dataClass` is closed in both ends, but can be - * overridden by the next `dataClass`. - * - * @type {Number} - * @product highcharts highmaps - * @apioption colorAxis.dataClasses.from - */ - - /** - * The name of the data class as it appears in the legend. - * If no name is given, it is automatically created based on the - * `from` and `to` values. For full programmatic control, - * [legend.labelFormatter](#legend.labelFormatter) can be used. - * In the formatter, `this.from` and `this.to` can be accessed. - * - * @type {String} - * @sample {highmaps} maps/coloraxis/dataclasses-name/ - * Named data classes - * @sample {highmaps} maps/coloraxis/dataclasses-labelformatter/ - * Formatted data classes - * @product highcharts highmaps - * @apioption colorAxis.dataClasses.name - */ - - /** - * The end of the value range that the data class represents, - * relating to the point value. - * - * The range of each `dataClass` is closed in both ends, but can be - * overridden by the next `dataClass`. - * - * @type {Number} - * @product highcharts highmaps - * @apioption colorAxis.dataClasses.to - */ - - /** - * @ignore - */ - lineWidth: 0, - - /** - * Padding of the min value relative to the length of the axis. A - * padding of 0.05 will make a 100px axis 5px longer. - * - * @type {Number} - * @product highcharts highmaps - */ - minPadding: 0, - - /** - * The maximum value of the axis in terms of map point values. If `null`, - * the max value is automatically calculated. If the `endOnTick` option - * is true, the max value might be rounded up. - * - * @type {Number} - * @sample {highmaps} maps/coloraxis/gridlines/ - * Explicit min and max to reduce the effect of outliers - * @product highcharts highmaps - * @apioption colorAxis.max - */ - - /** - * The minimum value of the axis in terms of map point values. If `null`, - * the min value is automatically calculated. If the `startOnTick` - * option is true, the min value might be rounded down. - * - * @type {Number} - * @sample {highmaps} maps/coloraxis/gridlines/ - * Explicit min and max to reduce the effect of outliers - * @product highcharts highmaps - * @apioption colorAxis.min - */ - - /** - * Padding of the max value relative to the length of the axis. A - * padding of 0.05 will make a 100px axis 5px longer. - * - * @type {Number} - * @product highcharts highmaps - */ - maxPadding: 0, - - /** - * Color of the grid lines extending from the axis across the gradient. - * - * @type {Color} - * @sample {highmaps} maps/coloraxis/gridlines/ Grid lines demonstrated - * @default #e6e6e6 - * @product highcharts highmaps - * @apioption colorAxis.gridLineColor - */ - - /** - * The width of the grid lines extending from the axis across the - * gradient of a scalar color axis. - * - * @type {Number} - * @sample {highmaps} maps/coloraxis/gridlines/ Grid lines demonstrated - * @default 1 - * @product highcharts highmaps - */ - gridLineWidth: 1, - - /** - * The interval of the tick marks in axis units. When `null`, the tick - * interval is computed to approximately follow the `tickPixelInterval`. - * - * @type {Number} - * @product highcharts highmaps - * @apioption colorAxis.tickInterval - */ - - /** - * If [tickInterval](#colorAxis.tickInterval) is `null` this option - * sets the approximate pixel interval of the tick marks. - * - * @type {Number} - * @default 72 - * @product highcharts highmaps - */ - tickPixelInterval: 72, - - /** - * Whether to force the axis to start on a tick. Use this option with - * the `maxPadding` option to control the axis start. - * - * @type {Boolean} - * @default true - * @product highcharts highmaps - */ - startOnTick: true, - - /** - * Whether to force the axis to end on a tick. Use this option with - * the [maxPadding](#colorAxis.maxPadding) option to control the axis - * end. - * - * @type {Boolean} - * @default true - * @product highcharts highmaps - */ - endOnTick: true, - - /** @ignore */ - offset: 0, - - /** - * The triangular marker on a scalar color axis that points to the - * value of the hovered area. To disable the marker, set - * `marker: null`. - * - * @type {Object} - * @sample {highmaps} maps/coloraxis/marker/ Black marker - * @product highcharts highmaps - */ - marker: { - - /** - * Animation for the marker as it moves between values. Set to `false` - * to disable animation. Defaults to `{ duration: 50 }`. - * - * @type {Object|Boolean} - * @product highcharts highmaps - */ - animation: { - duration: 50 - }, - - /** - * @ignore - */ - width: 0.01, - /*= if (build.classic) { =*/ - - /** - * The color of the marker. - * - * @type {Color} - * @default #999999 - * @product highcharts highmaps - */ - color: '${palette.neutralColor40}' - /*= } =*/ - }, - - /** - * The axis labels show the number for each tick. - * - * For more live examples on label options, see [xAxis.labels in the - * Highcharts API.](/highcharts#xAxis.labels) - * - * @type {Object} - * @extends xAxis.labels - * @product highcharts highmaps - */ - labels: { - - /** - * How to handle overflowing labels on horizontal color axis. Can be - * undefined or "justify". If "justify", labels will not render - * outside the legend area. If there is room to move it, it will be - * aligned to the edge, else it will be removed. - * - * @validvalue [null, "justify"] - * @type {String} - * @default justify - * @product highcharts highmaps - */ - overflow: 'justify', - - rotation: 0 - }, - - /** - * The color to represent the minimum of the color axis. Unless - * [dataClasses](#colorAxis.dataClasses) or - * [stops](#colorAxis.stops) are set, the gradient starts at this - * value. - * - * If dataClasses are set, the color is based on minColor and - * maxColor unless a color is set for each data class, or the - * [dataClassColor](#colorAxis.dataClassColor) is set. - * - * @type {Color} - * @sample {highmaps} maps/coloraxis/mincolor-maxcolor/ Min and max colors on scalar (gradient) axis - * @sample {highmaps} maps/coloraxis/mincolor-maxcolor-dataclasses/ On data classes - * @default #e6ebf5 - * @product highcharts highmaps - */ - minColor: '${palette.highlightColor10}', - - /** - * The color to represent the maximum of the color axis. Unless - * [dataClasses](#colorAxis.dataClasses) or - * [stops](#colorAxis.stops) are set, the gradient ends at this - * value. - * - * If dataClasses are set, the color is based on minColor and - * maxColor unless a color is set for each data class, or the - * [dataClassColor](#colorAxis.dataClassColor) is set. - * - * @type {Color} - * @sample {highmaps} maps/coloraxis/mincolor-maxcolor/ Min and max colors on scalar (gradient) axis - * @sample {highmaps} maps/coloraxis/mincolor-maxcolor-dataclasses/ On data classes - * @default #003399 - * @product highcharts highmaps - */ - maxColor: '${palette.highlightColor100}', - - /** - * Color stops for the gradient of a scalar color axis. Use this in - * cases where a linear gradient between a `minColor` and `maxColor` - * is not sufficient. The stops is an array of tuples, where the first - * item is a float between 0 and 1 assigning the relative position in - * the gradient, and the second item is the color. - * - * @type {Array} - * @sample {highmaps} maps/demo/heatmap/ Heatmap with three color stops - * @product highcharts highmaps - * @apioption colorAxis.stops - */ - - /** - * The pixel length of the main tick marks on the color axis. - */ - tickLength: 5, - - /** - * The type of interpolation to use for the color axis. Can be `linear` - * or `logarithmic`. - * - * @validvalue ["linear", "logarithmic"] - * @type {String} - * @default linear - * @product highcharts highmaps - * @apioption colorAxis.type - */ - - /** - * Whether to reverse the axis so that the highest number is closest - * to the origin. Defaults to `false` in a horizontal legend and `true` - * in a vertical legend, where the smallest value starts on top. - * - * @type {Boolean} - * @product highcharts highmaps - * @apioption colorAxis.reversed - */ - - /** - * Fires when the legend item belonging to the colorAxis is clicked. - * One parameter, `event`, is passed to the function. - * - * @type {Function} - * @product highcharts highmaps - * @apioption colorAxis.events.legendItemClick - */ - - /** - * Whether to display the colorAxis in the legend. - * - * @type {Boolean} - * @see [heatmap.showInLegend](#series.heatmap.showInLegend) - * @default true - * @since 4.2.7 - * @product highcharts highmaps - */ - showInLegend: true - }, - - // Properties to preserve after destroy, for Axis.update (#5881, #6025) - keepProps: [ - 'legendGroup', - 'legendItemHeight', - 'legendItemWidth', - 'legendItem', - 'legendSymbol' - ].concat(Axis.prototype.keepProps), - - /** - * Initialize the color axis - */ - init: function (chart, userOptions) { - var horiz = chart.options.legend.layout !== 'vertical', - options; - - this.coll = 'colorAxis'; - - // Build the options - options = merge(this.defaultColorAxisOptions, { - side: horiz ? 2 : 1, - reversed: !horiz - }, userOptions, { - opposite: !horiz, - showEmpty: false, - title: null, - visible: chart.options.legend.enabled - }); - - Axis.prototype.init.call(this, chart, options); - - // Base init() pushes it to the xAxis array, now pop it again - // chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop(); - - // Prepare data classes - if (userOptions.dataClasses) { - this.initDataClasses(userOptions); - } - this.initStops(); - - // Override original axis properties - this.horiz = horiz; - this.zoomEnabled = false; - - // Add default values - this.defaultLegendLength = 200; - }, - - initDataClasses: function (userOptions) { - var chart = this.chart, - dataClasses, - colorCounter = 0, - colorCount = chart.options.chart.colorCount, - options = this.options, - len = userOptions.dataClasses.length; - this.dataClasses = dataClasses = []; - this.legendItems = []; - - each(userOptions.dataClasses, function (dataClass, i) { - var colors; - - dataClass = merge(dataClass); - dataClasses.push(dataClass); - - /*= if (build.classic) { =*/ - if (dataClass.color) { - return; - } - /*= } =*/ - if (options.dataClassColor === 'category') { - /*= if (build.classic) { =*/ - colors = chart.options.colors; - colorCount = colors.length; - dataClass.color = colors[colorCounter]; - /*= } =*/ - dataClass.colorIndex = colorCounter; - - // increase and loop back to zero - colorCounter++; - if (colorCounter === colorCount) { - colorCounter = 0; - } - } else { - dataClass.color = color(options.minColor).tweenTo( - color(options.maxColor), - len < 2 ? 0.5 : i / (len - 1) // #3219 - ); - } - }); - }, - - /** - * Override so that ticks are not added in data class axes (#6914) - */ - setTickPositions: function () { - if (!this.dataClasses) { - return Axis.prototype.setTickPositions.call(this); - } - }, - - - initStops: function () { - this.stops = this.options.stops || [ - [0, this.options.minColor], - [1, this.options.maxColor] - ]; - each(this.stops, function (stop) { - stop.color = color(stop[1]); - }); - }, - - /** - * Extend the setOptions method to process extreme colors and color - * stops. - */ - setOptions: function (userOptions) { - Axis.prototype.setOptions.call(this, userOptions); - - this.options.crosshair = this.options.marker; - }, - - setAxisSize: function () { - var symbol = this.legendSymbol, - chart = this.chart, - legendOptions = chart.options.legend || {}, - x, - y, - width, - height; - - if (symbol) { - this.left = x = symbol.attr('x'); - this.top = y = symbol.attr('y'); - this.width = width = symbol.attr('width'); - this.height = height = symbol.attr('height'); - this.right = chart.chartWidth - x - width; - this.bottom = chart.chartHeight - y - height; - - this.len = this.horiz ? width : height; - this.pos = this.horiz ? x : y; - } else { - // Fake length for disabled legend to avoid tick issues - // and such (#5205) - this.len = ( - this.horiz ? - legendOptions.symbolWidth : - legendOptions.symbolHeight - ) || this.defaultLegendLength; - } - }, - - normalizedValue: function (value) { - if (this.isLog) { - value = this.val2lin(value); - } - return 1 - ((this.max - value) / ((this.max - this.min) || 1)); - }, - - /** - * Translate from a value to a color - */ - toColor: function (value, point) { - var pos, - stops = this.stops, - from, - to, - color, - dataClasses = this.dataClasses, - dataClass, - i; - - if (dataClasses) { - i = dataClasses.length; - while (i--) { - dataClass = dataClasses[i]; - from = dataClass.from; - to = dataClass.to; - if ( - (from === undefined || value >= from) && - (to === undefined || value <= to) - ) { - /*= if (build.classic) { =*/ - color = dataClass.color; - /*= } =*/ - if (point) { - point.dataClass = i; - point.colorIndex = dataClass.colorIndex; - } - break; - } - } - - } else { - - pos = this.normalizedValue(value); - i = stops.length; - while (i--) { - if (pos > stops[i][0]) { - break; - } - } - from = stops[i] || stops[i + 1]; - to = stops[i + 1] || from; - - // The position within the gradient - pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1); - - color = from.color.tweenTo( - to.color, - pos - ); - } - return color; - }, - - /** - * Override the getOffset method to add the whole axis groups inside - * the legend. - */ - getOffset: function () { - var group = this.legendGroup, - sideOffset = this.chart.axisOffset[this.side]; - - if (group) { - - // Hook for the getOffset method to add groups to this parent group - this.axisParent = group; - - // Call the base - Axis.prototype.getOffset.call(this); - - // First time only - if (!this.added) { - - this.added = true; - - this.labelLeft = 0; - this.labelRight = this.width; - } - // Reset it to avoid color axis reserving space - this.chart.axisOffset[this.side] = sideOffset; - } - }, - - /** - * Create the color gradient - */ - setLegendColor: function () { - var grad, - horiz = this.horiz, - reversed = this.reversed, - one = reversed ? 1 : 0, - zero = reversed ? 0 : 1; - - grad = horiz ? [one, 0, zero, 0] : [0, zero, 0, one]; // #3190 - this.legendColor = { - linearGradient: { - x1: grad[0], y1: grad[1], - x2: grad[2], y2: grad[3] - }, - stops: this.stops - }; - }, - - /** - * The color axis appears inside the legend and has its own legend symbol - */ - drawLegendSymbol: function (legend, item) { - var padding = legend.padding, - legendOptions = legend.options, - horiz = this.horiz, - width = pick( - legendOptions.symbolWidth, - horiz ? this.defaultLegendLength : 12 - ), - height = pick( - legendOptions.symbolHeight, - horiz ? 12 : this.defaultLegendLength - ), - labelPadding = pick(legendOptions.labelPadding, horiz ? 16 : 30), - itemDistance = pick(legendOptions.itemDistance, 10); - - this.setLegendColor(); - - // Create the gradient - item.legendSymbol = this.chart.renderer.rect( - 0, - legend.baseline - 11, - width, - height - ).attr({ - zIndex: 1 - }).add(item.legendGroup); - - // Set how much space this legend item takes up - this.legendItemWidth = width + padding + - (horiz ? itemDistance : labelPadding); - this.legendItemHeight = height + padding + (horiz ? labelPadding : 0); - }, - /** - * Fool the legend - */ - setState: function (state) { - each(this.series, function (series) { - series.setState(state); - }); - }, - visible: true, - setVisible: noop, - getSeriesExtremes: function () { - var series = this.series, - i = series.length; - this.dataMin = Infinity; - this.dataMax = -Infinity; - while (i--) { - if (series[i].valueMin !== undefined) { - this.dataMin = Math.min(this.dataMin, series[i].valueMin); - this.dataMax = Math.max(this.dataMax, series[i].valueMax); - } - } - }, - drawCrosshair: function (e, point) { - var plotX = point && point.plotX, - plotY = point && point.plotY, - crossPos, - axisPos = this.pos, - axisLen = this.len; - - if (point) { - crossPos = this.toPixels(point[point.series.colorKey]); - if (crossPos < axisPos) { - crossPos = axisPos - 2; - } else if (crossPos > axisPos + axisLen) { - crossPos = axisPos + axisLen + 2; - } - - point.plotX = crossPos; - point.plotY = this.len - crossPos; - Axis.prototype.drawCrosshair.call(this, e, point); - point.plotX = plotX; - point.plotY = plotY; - - if ( - this.cross && - !this.cross.addedToColorAxis && - this.legendGroup - ) { - this.cross - .addClass('highcharts-coloraxis-marker') - .add(this.legendGroup); - - this.cross.addedToColorAxis = true; - - /*= if (build.classic) { =*/ - this.cross.attr({ - fill: this.crosshair.color - }); - /*= } =*/ - - } - } - }, - getPlotLinePath: function (a, b, c, d, pos) { - // crosshairs only - return isNumber(pos) ? // pos can be 0 (#3969) - ( - this.horiz ? [ - 'M', - pos - 4, this.top - 6, - 'L', - pos + 4, this.top - 6, - pos, this.top, - 'Z' - ] : [ - 'M', - this.left, pos, - 'L', - this.left - 6, pos + 6, - this.left - 6, pos - 6, - 'Z' - ] - ) : - Axis.prototype.getPlotLinePath.call(this, a, b, c, d); - }, - - update: function (newOptions, redraw) { - var chart = this.chart, - legend = chart.legend; - - each(this.series, function (series) { - // Needed for Axis.update when choropleth colors change - series.isDirtyData = true; - }); - - // When updating data classes, destroy old items and make sure new ones - // are created (#3207) - if (newOptions.dataClasses && legend.allItems) { - each(legend.allItems, function (item) { - if (item.isDataClass && item.legendGroup) { - item.legendGroup.destroy(); - } - }); - chart.isDirtyLegend = true; - } - - // Keep the options structure updated for export. Unlike xAxis and - // yAxis, the colorAxis is not an array. (#3207) - chart.options[this.coll] = merge(this.userOptions, newOptions); - - Axis.prototype.update.call(this, newOptions, redraw); - if (this.legendItem) { - this.setLegendColor(); - legend.colorizeItem(this, true); - } - }, - - /** - * Extend basic axis remove by also removing the legend item. - */ - remove: function () { - if (this.legendItem) { - this.chart.legend.destroyItem(this); - } - Axis.prototype.remove.call(this); - }, - - /** - * Get the legend item symbols for data classes - */ - getDataClassLegendSymbols: function () { - var axis = this, - chart = this.chart, - legendItems = this.legendItems, - legendOptions = chart.options.legend, - valueDecimals = legendOptions.valueDecimals, - valueSuffix = legendOptions.valueSuffix || '', - name; - - if (!legendItems.length) { - each(this.dataClasses, function (dataClass, i) { - var vis = true, - from = dataClass.from, - to = dataClass.to; - - // Assemble the default name. This can be overridden - // by legend.options.labelFormatter - name = ''; - if (from === undefined) { - name = '< '; - } else if (to === undefined) { - name = '> '; - } - if (from !== undefined) { - name += H.numberFormat(from, valueDecimals) + valueSuffix; - } - if (from !== undefined && to !== undefined) { - name += ' - '; - } - if (to !== undefined) { - name += H.numberFormat(to, valueDecimals) + valueSuffix; - } - // Add a mock object to the legend items - legendItems.push(extend({ - chart: chart, - name: name, - options: {}, - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - visible: true, - setState: noop, - isDataClass: true, - setVisible: function () { - vis = this.visible = !vis; - each(axis.series, function (series) { - each(series.points, function (point) { - if (point.dataClass === i) { - point.setVisible(vis); - } - }); - }); - - chart.legend.colorizeItem(this, vis); - } - }, dataClass)); - }); - } - return legendItems; - }, - name: '' // Prevents 'undefined' in legend in IE8 - }); - - /** - * Handle animation of the color attributes directly - */ - each(['fill', 'stroke'], function (prop) { - H.Fx.prototype[prop + 'Setter'] = function () { - this.elem.attr( - prop, - color(this.start).tweenTo( - color(this.end), - this.pos - ), - null, - true - ); - }; - }); - - /** - * Extend the chart getAxes method to also get the color axis - */ - addEvent(Chart, 'afterGetAxes', function () { - - var options = this.options, - colorAxisOptions = options.colorAxis; - - this.colorAxis = []; - if (colorAxisOptions) { - new ColorAxis(this, colorAxisOptions); // eslint-disable-line no-new - } - }); - - - /** - * Add the color axis. This also removes the axis' own series to prevent - * them from showing up individually. - */ - addEvent(Legend, 'afterGetAllItems', function (e) { - var colorAxisItems = [], - colorAxis = this.chart.colorAxis[0]; - - if (colorAxis && colorAxis.options) { - if (colorAxis.options.showInLegend) { - // Data classes - if (colorAxis.options.dataClasses) { - colorAxisItems = colorAxis.getDataClassLegendSymbols(); - // Gradient legend - } else { - // Add this axis on top - colorAxisItems.push(colorAxis); - } - } - - // Don't add the color axis' series - each(colorAxis.series, function (series) { - H.erase(e.allItems, series); - }); - } - - while (colorAxisItems.length) { - e.allItems.unshift(colorAxisItems.pop()); - } - }); - - addEvent(Legend, 'afterColorizeItem', function (e) { - if (e.visible && e.item.legendColor) { - e.item.legendSymbol.attr({ - fill: e.item.legendColor - }); - } - }); - - // Updates in the legend need to be reflected in the color axis (6888) - addEvent(Legend, 'afterUpdate', function () { - if (this.chart.colorAxis[0]) { - this.chart.colorAxis[0].update({}, arguments[2]); - } - }); +if (!H.ColorAxis) { + + /** + * The ColorAxis object for inclusion in gradient legends + */ + ColorAxis = H.ColorAxis = function () { + this.init.apply(this, arguments); + }; + extend(ColorAxis.prototype, Axis.prototype); + extend(ColorAxis.prototype, { + /** + * A color axis for choropleth maps and heat maps. Visually, the color axis + * will appear as a gradient or as separate items inside the legend, + * depending on whether the axis is scalar or based on data classes. + * + * For supported color formats, see the + * [docs article about colors](http://www.highcharts.com/docs/chart-design-and-style/colors). + * + * A scalar color axis is represented by a gradient. The colors either range + * between the [minColor](#colorAxis.minColor) and the [maxColor](#colorAxis.maxColor), + * or for more fine grained control the colors can be + * defined in [stops](#colorAxis.stops). Often times, the color axis needs + * to be adjusted to get the right color spread for the data. In addition to + * stops, consider using a logarithmic [axis type](#colorAxis.type), or + * setting [min](#colorAxis.min) and [max](#colorAxis.max) to avoid the + * colors being determined by outliers. + * + * When [dataClasses](#colorAxis.dataClasses) are used, the ranges are + * subdivided into separate classes like categories based on their values. + * This can be used for ranges between two values, but also for a true + * category. However, when your data is categorized, it may be as convenient + * to add each category to a separate series. + * + * See [the Axis object](#Axis) for programmatic access to the axis. + * @extends {xAxis} + * @excluding allowDecimals,alternateGridColor,breaks,categories,crosshair, + * dateTimeLabelFormats,lineWidth,linkedTo,maxZoom,minRange, + * minTickInterval,offset,opposite,plotBands,plotLines,showEmpty, + * title + * @product highcharts highmaps + * @optionparent colorAxis + */ + defaultColorAxisOptions: { + + /** + * Whether to allow decimals on the color axis. + * @type {Boolean} + * @default true + * @product highcharts highmaps + * @apioption colorAxis.allowDecimals + */ + + /** + * Determines how to set each data class' color if no individual color + * is set. The default value, `tween`, computes intermediate colors + * between `minColor` and `maxColor`. The other possible value, `category`, + * pulls colors from the global or chart specific [colors](#colors) + * array. + * + * @validvalue ["tween", "category"] + * @type {String} + * @sample {highmaps} maps/coloraxis/dataclasscolor/ Category colors + * @default tween + * @product highcharts highmaps + * @apioption colorAxis.dataClassColor + */ + + /** + * An array of data classes or ranges for the choropleth map. If none + * given, the color axis is scalar and values are distributed as a gradient + * between the minimum and maximum colors. + * + * @type {Array} + * @sample {highmaps} maps/demo/data-class-ranges/ Multiple ranges + * @sample {highmaps} maps/demo/data-class-two-ranges/ Two ranges + * @product highcharts highmaps + * @apioption colorAxis.dataClasses + */ + + /** + * The color of each data class. If not set, the color is pulled from + * the global or chart-specific [colors](#colors) array. In + * styled mode, this option is ignored. Instead, use colors defined in + * CSS. + * + * @type {Color} + * @sample {highmaps} maps/demo/data-class-two-ranges/ Explicit colors + * @product highcharts highmaps + * @apioption colorAxis.dataClasses.color + */ + + /** + * The start of the value range that the data class represents, + * relating to the point value. + * + * The range of each `dataClass` is closed in both ends, but can be + * overridden by the next `dataClass`. + * + * @type {Number} + * @product highcharts highmaps + * @apioption colorAxis.dataClasses.from + */ + + /** + * The name of the data class as it appears in the legend. + * If no name is given, it is automatically created based on the + * `from` and `to` values. For full programmatic control, + * [legend.labelFormatter](#legend.labelFormatter) can be used. + * In the formatter, `this.from` and `this.to` can be accessed. + * + * @type {String} + * @sample {highmaps} maps/coloraxis/dataclasses-name/ + * Named data classes + * @sample {highmaps} maps/coloraxis/dataclasses-labelformatter/ + * Formatted data classes + * @product highcharts highmaps + * @apioption colorAxis.dataClasses.name + */ + + /** + * The end of the value range that the data class represents, + * relating to the point value. + * + * The range of each `dataClass` is closed in both ends, but can be + * overridden by the next `dataClass`. + * + * @type {Number} + * @product highcharts highmaps + * @apioption colorAxis.dataClasses.to + */ + + /** + * @ignore + */ + lineWidth: 0, + + /** + * Padding of the min value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. + * + * @type {Number} + * @product highcharts highmaps + */ + minPadding: 0, + + /** + * The maximum value of the axis in terms of map point values. If `null`, + * the max value is automatically calculated. If the `endOnTick` option + * is true, the max value might be rounded up. + * + * @type {Number} + * @sample {highmaps} maps/coloraxis/gridlines/ + * Explicit min and max to reduce the effect of outliers + * @product highcharts highmaps + * @apioption colorAxis.max + */ + + /** + * The minimum value of the axis in terms of map point values. If `null`, + * the min value is automatically calculated. If the `startOnTick` + * option is true, the min value might be rounded down. + * + * @type {Number} + * @sample {highmaps} maps/coloraxis/gridlines/ + * Explicit min and max to reduce the effect of outliers + * @product highcharts highmaps + * @apioption colorAxis.min + */ + + /** + * Padding of the max value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. + * + * @type {Number} + * @product highcharts highmaps + */ + maxPadding: 0, + + /** + * Color of the grid lines extending from the axis across the gradient. + * + * @type {Color} + * @sample {highmaps} maps/coloraxis/gridlines/ Grid lines demonstrated + * @default #e6e6e6 + * @product highcharts highmaps + * @apioption colorAxis.gridLineColor + */ + + /** + * The width of the grid lines extending from the axis across the + * gradient of a scalar color axis. + * + * @type {Number} + * @sample {highmaps} maps/coloraxis/gridlines/ Grid lines demonstrated + * @default 1 + * @product highcharts highmaps + */ + gridLineWidth: 1, + + /** + * The interval of the tick marks in axis units. When `null`, the tick + * interval is computed to approximately follow the `tickPixelInterval`. + * + * @type {Number} + * @product highcharts highmaps + * @apioption colorAxis.tickInterval + */ + + /** + * If [tickInterval](#colorAxis.tickInterval) is `null` this option + * sets the approximate pixel interval of the tick marks. + * + * @type {Number} + * @default 72 + * @product highcharts highmaps + */ + tickPixelInterval: 72, + + /** + * Whether to force the axis to start on a tick. Use this option with + * the `maxPadding` option to control the axis start. + * + * @type {Boolean} + * @default true + * @product highcharts highmaps + */ + startOnTick: true, + + /** + * Whether to force the axis to end on a tick. Use this option with + * the [maxPadding](#colorAxis.maxPadding) option to control the axis + * end. + * + * @type {Boolean} + * @default true + * @product highcharts highmaps + */ + endOnTick: true, + + /** @ignore */ + offset: 0, + + /** + * The triangular marker on a scalar color axis that points to the + * value of the hovered area. To disable the marker, set + * `marker: null`. + * + * @type {Object} + * @sample {highmaps} maps/coloraxis/marker/ Black marker + * @product highcharts highmaps + */ + marker: { + + /** + * Animation for the marker as it moves between values. Set to `false` + * to disable animation. Defaults to `{ duration: 50 }`. + * + * @type {Object|Boolean} + * @product highcharts highmaps + */ + animation: { + duration: 50 + }, + + /** + * @ignore + */ + width: 0.01, + /*= if (build.classic) { =*/ + + /** + * The color of the marker. + * + * @type {Color} + * @default #999999 + * @product highcharts highmaps + */ + color: '${palette.neutralColor40}' + /*= } =*/ + }, + + /** + * The axis labels show the number for each tick. + * + * For more live examples on label options, see [xAxis.labels in the + * Highcharts API.](/highcharts#xAxis.labels) + * + * @type {Object} + * @extends xAxis.labels + * @product highcharts highmaps + */ + labels: { + + /** + * How to handle overflowing labels on horizontal color axis. Can be + * undefined or "justify". If "justify", labels will not render + * outside the legend area. If there is room to move it, it will be + * aligned to the edge, else it will be removed. + * + * @validvalue [null, "justify"] + * @type {String} + * @default justify + * @product highcharts highmaps + */ + overflow: 'justify', + + rotation: 0 + }, + + /** + * The color to represent the minimum of the color axis. Unless + * [dataClasses](#colorAxis.dataClasses) or + * [stops](#colorAxis.stops) are set, the gradient starts at this + * value. + * + * If dataClasses are set, the color is based on minColor and + * maxColor unless a color is set for each data class, or the + * [dataClassColor](#colorAxis.dataClassColor) is set. + * + * @type {Color} + * @sample {highmaps} maps/coloraxis/mincolor-maxcolor/ Min and max colors on scalar (gradient) axis + * @sample {highmaps} maps/coloraxis/mincolor-maxcolor-dataclasses/ On data classes + * @default #e6ebf5 + * @product highcharts highmaps + */ + minColor: '${palette.highlightColor10}', + + /** + * The color to represent the maximum of the color axis. Unless + * [dataClasses](#colorAxis.dataClasses) or + * [stops](#colorAxis.stops) are set, the gradient ends at this + * value. + * + * If dataClasses are set, the color is based on minColor and + * maxColor unless a color is set for each data class, or the + * [dataClassColor](#colorAxis.dataClassColor) is set. + * + * @type {Color} + * @sample {highmaps} maps/coloraxis/mincolor-maxcolor/ Min and max colors on scalar (gradient) axis + * @sample {highmaps} maps/coloraxis/mincolor-maxcolor-dataclasses/ On data classes + * @default #003399 + * @product highcharts highmaps + */ + maxColor: '${palette.highlightColor100}', + + /** + * Color stops for the gradient of a scalar color axis. Use this in + * cases where a linear gradient between a `minColor` and `maxColor` + * is not sufficient. The stops is an array of tuples, where the first + * item is a float between 0 and 1 assigning the relative position in + * the gradient, and the second item is the color. + * + * @type {Array} + * @sample {highmaps} maps/demo/heatmap/ Heatmap with three color stops + * @product highcharts highmaps + * @apioption colorAxis.stops + */ + + /** + * The pixel length of the main tick marks on the color axis. + */ + tickLength: 5, + + /** + * The type of interpolation to use for the color axis. Can be `linear` + * or `logarithmic`. + * + * @validvalue ["linear", "logarithmic"] + * @type {String} + * @default linear + * @product highcharts highmaps + * @apioption colorAxis.type + */ + + /** + * Whether to reverse the axis so that the highest number is closest + * to the origin. Defaults to `false` in a horizontal legend and `true` + * in a vertical legend, where the smallest value starts on top. + * + * @type {Boolean} + * @product highcharts highmaps + * @apioption colorAxis.reversed + */ + + /** + * Fires when the legend item belonging to the colorAxis is clicked. + * One parameter, `event`, is passed to the function. + * + * @type {Function} + * @product highcharts highmaps + * @apioption colorAxis.events.legendItemClick + */ + + /** + * Whether to display the colorAxis in the legend. + * + * @type {Boolean} + * @see [heatmap.showInLegend](#series.heatmap.showInLegend) + * @default true + * @since 4.2.7 + * @product highcharts highmaps + */ + showInLegend: true + }, + + // Properties to preserve after destroy, for Axis.update (#5881, #6025) + keepProps: [ + 'legendGroup', + 'legendItemHeight', + 'legendItemWidth', + 'legendItem', + 'legendSymbol' + ].concat(Axis.prototype.keepProps), + + /** + * Initialize the color axis + */ + init: function (chart, userOptions) { + var horiz = chart.options.legend.layout !== 'vertical', + options; + + this.coll = 'colorAxis'; + + // Build the options + options = merge(this.defaultColorAxisOptions, { + side: horiz ? 2 : 1, + reversed: !horiz + }, userOptions, { + opposite: !horiz, + showEmpty: false, + title: null, + visible: chart.options.legend.enabled + }); + + Axis.prototype.init.call(this, chart, options); + + // Base init() pushes it to the xAxis array, now pop it again + // chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop(); + + // Prepare data classes + if (userOptions.dataClasses) { + this.initDataClasses(userOptions); + } + this.initStops(); + + // Override original axis properties + this.horiz = horiz; + this.zoomEnabled = false; + + // Add default values + this.defaultLegendLength = 200; + }, + + initDataClasses: function (userOptions) { + var chart = this.chart, + dataClasses, + colorCounter = 0, + colorCount = chart.options.chart.colorCount, + options = this.options, + len = userOptions.dataClasses.length; + this.dataClasses = dataClasses = []; + this.legendItems = []; + + each(userOptions.dataClasses, function (dataClass, i) { + var colors; + + dataClass = merge(dataClass); + dataClasses.push(dataClass); + + /*= if (build.classic) { =*/ + if (dataClass.color) { + return; + } + /*= } =*/ + if (options.dataClassColor === 'category') { + /*= if (build.classic) { =*/ + colors = chart.options.colors; + colorCount = colors.length; + dataClass.color = colors[colorCounter]; + /*= } =*/ + dataClass.colorIndex = colorCounter; + + // increase and loop back to zero + colorCounter++; + if (colorCounter === colorCount) { + colorCounter = 0; + } + } else { + dataClass.color = color(options.minColor).tweenTo( + color(options.maxColor), + len < 2 ? 0.5 : i / (len - 1) // #3219 + ); + } + }); + }, + + /** + * Override so that ticks are not added in data class axes (#6914) + */ + setTickPositions: function () { + if (!this.dataClasses) { + return Axis.prototype.setTickPositions.call(this); + } + }, + + + initStops: function () { + this.stops = this.options.stops || [ + [0, this.options.minColor], + [1, this.options.maxColor] + ]; + each(this.stops, function (stop) { + stop.color = color(stop[1]); + }); + }, + + /** + * Extend the setOptions method to process extreme colors and color + * stops. + */ + setOptions: function (userOptions) { + Axis.prototype.setOptions.call(this, userOptions); + + this.options.crosshair = this.options.marker; + }, + + setAxisSize: function () { + var symbol = this.legendSymbol, + chart = this.chart, + legendOptions = chart.options.legend || {}, + x, + y, + width, + height; + + if (symbol) { + this.left = x = symbol.attr('x'); + this.top = y = symbol.attr('y'); + this.width = width = symbol.attr('width'); + this.height = height = symbol.attr('height'); + this.right = chart.chartWidth - x - width; + this.bottom = chart.chartHeight - y - height; + + this.len = this.horiz ? width : height; + this.pos = this.horiz ? x : y; + } else { + // Fake length for disabled legend to avoid tick issues + // and such (#5205) + this.len = ( + this.horiz ? + legendOptions.symbolWidth : + legendOptions.symbolHeight + ) || this.defaultLegendLength; + } + }, + + normalizedValue: function (value) { + if (this.isLog) { + value = this.val2lin(value); + } + return 1 - ((this.max - value) / ((this.max - this.min) || 1)); + }, + + /** + * Translate from a value to a color + */ + toColor: function (value, point) { + var pos, + stops = this.stops, + from, + to, + color, + dataClasses = this.dataClasses, + dataClass, + i; + + if (dataClasses) { + i = dataClasses.length; + while (i--) { + dataClass = dataClasses[i]; + from = dataClass.from; + to = dataClass.to; + if ( + (from === undefined || value >= from) && + (to === undefined || value <= to) + ) { + /*= if (build.classic) { =*/ + color = dataClass.color; + /*= } =*/ + if (point) { + point.dataClass = i; + point.colorIndex = dataClass.colorIndex; + } + break; + } + } + + } else { + + pos = this.normalizedValue(value); + i = stops.length; + while (i--) { + if (pos > stops[i][0]) { + break; + } + } + from = stops[i] || stops[i + 1]; + to = stops[i + 1] || from; + + // The position within the gradient + pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1); + + color = from.color.tweenTo( + to.color, + pos + ); + } + return color; + }, + + /** + * Override the getOffset method to add the whole axis groups inside + * the legend. + */ + getOffset: function () { + var group = this.legendGroup, + sideOffset = this.chart.axisOffset[this.side]; + + if (group) { + + // Hook for the getOffset method to add groups to this parent group + this.axisParent = group; + + // Call the base + Axis.prototype.getOffset.call(this); + + // First time only + if (!this.added) { + + this.added = true; + + this.labelLeft = 0; + this.labelRight = this.width; + } + // Reset it to avoid color axis reserving space + this.chart.axisOffset[this.side] = sideOffset; + } + }, + + /** + * Create the color gradient + */ + setLegendColor: function () { + var grad, + horiz = this.horiz, + reversed = this.reversed, + one = reversed ? 1 : 0, + zero = reversed ? 0 : 1; + + grad = horiz ? [one, 0, zero, 0] : [0, zero, 0, one]; // #3190 + this.legendColor = { + linearGradient: { + x1: grad[0], y1: grad[1], + x2: grad[2], y2: grad[3] + }, + stops: this.stops + }; + }, + + /** + * The color axis appears inside the legend and has its own legend symbol + */ + drawLegendSymbol: function (legend, item) { + var padding = legend.padding, + legendOptions = legend.options, + horiz = this.horiz, + width = pick( + legendOptions.symbolWidth, + horiz ? this.defaultLegendLength : 12 + ), + height = pick( + legendOptions.symbolHeight, + horiz ? 12 : this.defaultLegendLength + ), + labelPadding = pick(legendOptions.labelPadding, horiz ? 16 : 30), + itemDistance = pick(legendOptions.itemDistance, 10); + + this.setLegendColor(); + + // Create the gradient + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - 11, + width, + height + ).attr({ + zIndex: 1 + }).add(item.legendGroup); + + // Set how much space this legend item takes up + this.legendItemWidth = width + padding + + (horiz ? itemDistance : labelPadding); + this.legendItemHeight = height + padding + (horiz ? labelPadding : 0); + }, + /** + * Fool the legend + */ + setState: function (state) { + each(this.series, function (series) { + series.setState(state); + }); + }, + visible: true, + setVisible: noop, + getSeriesExtremes: function () { + var series = this.series, + i = series.length; + this.dataMin = Infinity; + this.dataMax = -Infinity; + while (i--) { + if (series[i].valueMin !== undefined) { + this.dataMin = Math.min(this.dataMin, series[i].valueMin); + this.dataMax = Math.max(this.dataMax, series[i].valueMax); + } + } + }, + drawCrosshair: function (e, point) { + var plotX = point && point.plotX, + plotY = point && point.plotY, + crossPos, + axisPos = this.pos, + axisLen = this.len; + + if (point) { + crossPos = this.toPixels(point[point.series.colorKey]); + if (crossPos < axisPos) { + crossPos = axisPos - 2; + } else if (crossPos > axisPos + axisLen) { + crossPos = axisPos + axisLen + 2; + } + + point.plotX = crossPos; + point.plotY = this.len - crossPos; + Axis.prototype.drawCrosshair.call(this, e, point); + point.plotX = plotX; + point.plotY = plotY; + + if ( + this.cross && + !this.cross.addedToColorAxis && + this.legendGroup + ) { + this.cross + .addClass('highcharts-coloraxis-marker') + .add(this.legendGroup); + + this.cross.addedToColorAxis = true; + + /*= if (build.classic) { =*/ + this.cross.attr({ + fill: this.crosshair.color + }); + /*= } =*/ + + } + } + }, + getPlotLinePath: function (a, b, c, d, pos) { + // crosshairs only + return isNumber(pos) ? // pos can be 0 (#3969) + ( + this.horiz ? [ + 'M', + pos - 4, this.top - 6, + 'L', + pos + 4, this.top - 6, + pos, this.top, + 'Z' + ] : [ + 'M', + this.left, pos, + 'L', + this.left - 6, pos + 6, + this.left - 6, pos - 6, + 'Z' + ] + ) : + Axis.prototype.getPlotLinePath.call(this, a, b, c, d); + }, + + update: function (newOptions, redraw) { + var chart = this.chart, + legend = chart.legend; + + each(this.series, function (series) { + // Needed for Axis.update when choropleth colors change + series.isDirtyData = true; + }); + + // When updating data classes, destroy old items and make sure new ones + // are created (#3207) + if (newOptions.dataClasses && legend.allItems) { + each(legend.allItems, function (item) { + if (item.isDataClass && item.legendGroup) { + item.legendGroup.destroy(); + } + }); + chart.isDirtyLegend = true; + } + + // Keep the options structure updated for export. Unlike xAxis and + // yAxis, the colorAxis is not an array. (#3207) + chart.options[this.coll] = merge(this.userOptions, newOptions); + + Axis.prototype.update.call(this, newOptions, redraw); + if (this.legendItem) { + this.setLegendColor(); + legend.colorizeItem(this, true); + } + }, + + /** + * Extend basic axis remove by also removing the legend item. + */ + remove: function () { + if (this.legendItem) { + this.chart.legend.destroyItem(this); + } + Axis.prototype.remove.call(this); + }, + + /** + * Get the legend item symbols for data classes + */ + getDataClassLegendSymbols: function () { + var axis = this, + chart = this.chart, + legendItems = this.legendItems, + legendOptions = chart.options.legend, + valueDecimals = legendOptions.valueDecimals, + valueSuffix = legendOptions.valueSuffix || '', + name; + + if (!legendItems.length) { + each(this.dataClasses, function (dataClass, i) { + var vis = true, + from = dataClass.from, + to = dataClass.to; + + // Assemble the default name. This can be overridden + // by legend.options.labelFormatter + name = ''; + if (from === undefined) { + name = '< '; + } else if (to === undefined) { + name = '> '; + } + if (from !== undefined) { + name += H.numberFormat(from, valueDecimals) + valueSuffix; + } + if (from !== undefined && to !== undefined) { + name += ' - '; + } + if (to !== undefined) { + name += H.numberFormat(to, valueDecimals) + valueSuffix; + } + // Add a mock object to the legend items + legendItems.push(extend({ + chart: chart, + name: name, + options: {}, + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + visible: true, + setState: noop, + isDataClass: true, + setVisible: function () { + vis = this.visible = !vis; + each(axis.series, function (series) { + each(series.points, function (point) { + if (point.dataClass === i) { + point.setVisible(vis); + } + }); + }); + + chart.legend.colorizeItem(this, vis); + } + }, dataClass)); + }); + } + return legendItems; + }, + name: '' // Prevents 'undefined' in legend in IE8 + }); + + /** + * Handle animation of the color attributes directly + */ + each(['fill', 'stroke'], function (prop) { + H.Fx.prototype[prop + 'Setter'] = function () { + this.elem.attr( + prop, + color(this.start).tweenTo( + color(this.end), + this.pos + ), + null, + true + ); + }; + }); + + /** + * Extend the chart getAxes method to also get the color axis + */ + addEvent(Chart, 'afterGetAxes', function () { + + var options = this.options, + colorAxisOptions = options.colorAxis; + + this.colorAxis = []; + if (colorAxisOptions) { + new ColorAxis(this, colorAxisOptions); // eslint-disable-line no-new + } + }); + + + /** + * Add the color axis. This also removes the axis' own series to prevent + * them from showing up individually. + */ + addEvent(Legend, 'afterGetAllItems', function (e) { + var colorAxisItems = [], + colorAxis = this.chart.colorAxis[0]; + + if (colorAxis && colorAxis.options) { + if (colorAxis.options.showInLegend) { + // Data classes + if (colorAxis.options.dataClasses) { + colorAxisItems = colorAxis.getDataClassLegendSymbols(); + // Gradient legend + } else { + // Add this axis on top + colorAxisItems.push(colorAxis); + } + } + + // Don't add the color axis' series + each(colorAxis.series, function (series) { + H.erase(e.allItems, series); + }); + } + + while (colorAxisItems.length) { + e.allItems.unshift(colorAxisItems.pop()); + } + }); + + addEvent(Legend, 'afterColorizeItem', function (e) { + if (e.visible && e.item.legendColor) { + e.item.legendSymbol.attr({ + fill: e.item.legendColor + }); + } + }); + + // Updates in the legend need to be reflected in the color axis (6888) + addEvent(Legend, 'afterUpdate', function () { + if (this.chart.colorAxis[0]) { + this.chart.colorAxis[0].update({}, arguments[2]); + } + }); } diff --git a/js/parts-map/ColorSeriesMixin.js b/js/parts-map/ColorSeriesMixin.js index 9a21595e2a9..728e6a0cb06 100644 --- a/js/parts-map/ColorSeriesMixin.js +++ b/js/parts-map/ColorSeriesMixin.js @@ -7,101 +7,101 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var defined = H.defined, - each = H.each, - noop = H.noop, - seriesTypes = H.seriesTypes; + each = H.each, + noop = H.noop, + seriesTypes = H.seriesTypes; /** * Mixin for maps and heatmaps */ H.colorPointMixin = { - /** - * Color points have a value option that determines whether or not it is - * a null point - */ - isValid: function () { - // undefined is allowed - return ( - this.value !== null && - this.value !== Infinity && - this.value !== -Infinity - ); - }, + /** + * Color points have a value option that determines whether or not it is + * a null point + */ + isValid: function () { + // undefined is allowed + return ( + this.value !== null && + this.value !== Infinity && + this.value !== -Infinity + ); + }, - /** - * Set the visibility of a single point - */ - setVisible: function (vis) { - var point = this, - method = vis ? 'show' : 'hide'; + /** + * Set the visibility of a single point + */ + setVisible: function (vis) { + var point = this, + method = vis ? 'show' : 'hide'; - // Show and hide associated elements - each(['graphic', 'dataLabel'], function (key) { - if (point[key]) { - point[key][method](); - } - }); - }, - setState: function (state) { - H.Point.prototype.setState.call(this, state); - if (this.graphic) { - this.graphic.attr({ - zIndex: state === 'hover' ? 1 : 0 - }); - } - } + // Show and hide associated elements + each(['graphic', 'dataLabel'], function (key) { + if (point[key]) { + point[key][method](); + } + }); + }, + setState: function (state) { + H.Point.prototype.setState.call(this, state); + if (this.graphic) { + this.graphic.attr({ + zIndex: state === 'hover' ? 1 : 0 + }); + } + } }; H.colorSeriesMixin = { - pointArrayMap: ['value'], - axisTypes: ['xAxis', 'yAxis', 'colorAxis'], - optionalAxis: 'colorAxis', - trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], - getSymbol: noop, - parallelArrays: ['x', 'y', 'value'], - colorKey: 'value', + pointArrayMap: ['value'], + axisTypes: ['xAxis', 'yAxis', 'colorAxis'], + optionalAxis: 'colorAxis', + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], + getSymbol: noop, + parallelArrays: ['x', 'y', 'value'], + colorKey: 'value', - /*= if (build.classic) { =*/ - pointAttribs: seriesTypes.column.prototype.pointAttribs, - /*= } =*/ - - /** - * In choropleth maps, the color is a result of the value, so this needs - * translation too - */ - translateColors: function () { - var series = this, - nullColor = this.options.nullColor, - colorAxis = this.colorAxis, - colorKey = this.colorKey; + /*= if (build.classic) { =*/ + pointAttribs: seriesTypes.column.prototype.pointAttribs, + /*= } =*/ - each(this.data, function (point) { - var value = point[colorKey], - color; + /** + * In choropleth maps, the color is a result of the value, so this needs + * translation too + */ + translateColors: function () { + var series = this, + nullColor = this.options.nullColor, + colorAxis = this.colorAxis, + colorKey = this.colorKey; - color = point.options.color || - ( - point.isNull ? - nullColor : - (colorAxis && value !== undefined) ? - colorAxis.toColor(value, point) : - point.color || series.color - ); + each(this.data, function (point) { + var value = point[colorKey], + color; - if (color) { - point.color = color; - } - }); - }, + color = point.options.color || + ( + point.isNull ? + nullColor : + (colorAxis && value !== undefined) ? + colorAxis.toColor(value, point) : + point.color || series.color + ); - /** - * Get the color attibutes to apply on the graphic - */ - colorAttribs: function (point) { - var ret = {}; - if (defined(point.color)) { - ret[this.colorProp || 'fill'] = point.color; - } - return ret; - } + if (color) { + point.color = color; + } + }); + }, + + /** + * Get the color attibutes to apply on the graphic + */ + colorAttribs: function (point) { + var ret = {}; + if (defined(point.color)) { + ret[this.colorProp || 'fill'] = point.color; + } + return ret; + } }; diff --git a/js/parts-map/GeoJSON.js b/js/parts-map/GeoJSON.js index 6a1af2dfbdb..b3cb3e35436 100644 --- a/js/parts-map/GeoJSON.js +++ b/js/parts-map/GeoJSON.js @@ -10,33 +10,33 @@ import '../parts/Utilities.js'; import '../parts/Options.js'; import '../parts/Chart.js'; var Chart = H.Chart, - each = H.each, - extend = H.extend, - format = H.format, - merge = H.merge, - win = H.win, - wrap = H.wrap; -/** + each = H.each, + extend = H.extend, + format = H.format, + merge = H.merge, + win = H.win, + wrap = H.wrap; +/** * Test for point in polygon. Polygon defined as array of [x,y] points. */ function pointInPolygon(point, polygon) { - var i, - j, - rel1, - rel2, - c = false, - x = point.x, - y = point.y; - - for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - rel1 = polygon[i][1] > y; - rel2 = polygon[j][1] > y; - if (rel1 !== rel2 && (x < (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) { - c = !c; - } - } - - return c; + var i, + j, + rel1, + rel2, + c = false, + x = point.x, + y = point.y; + + for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + rel1 = polygon[i][1] > y; + rel2 = polygon[j][1] > y; + if (rel1 !== rel2 && (x < (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) { + c = !c; + } + } + + return c; } /** @@ -63,23 +63,23 @@ function pointInPolygon(point, polygon) { * Use specific transformation for lat/lon */ Chart.prototype.transformFromLatLon = function (latLon, transform) { - if (win.proj4 === undefined) { - H.error(21); - return { - x: 0, - y: null - }; - } - - var projected = win.proj4(transform.crs, [latLon.lon, latLon.lat]), - cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)), - sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)), - rotated = transform.rotation ? [projected[0] * cosAngle + projected[1] * sinAngle, -projected[0] * sinAngle + projected[1] * cosAngle] : projected; - - return { - x: ((rotated[0] - (transform.xoffset || 0)) * (transform.scale || 1) + (transform.xpan || 0)) * (transform.jsonres || 1) + (transform.jsonmarginX || 0), - y: (((transform.yoffset || 0) - rotated[1]) * (transform.scale || 1) + (transform.ypan || 0)) * (transform.jsonres || 1) - (transform.jsonmarginY || 0) - }; + if (win.proj4 === undefined) { + H.error(21); + return { + x: 0, + y: null + }; + } + + var projected = win.proj4(transform.crs, [latLon.lon, latLon.lat]), + cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)), + sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)), + rotated = transform.rotation ? [projected[0] * cosAngle + projected[1] * sinAngle, -projected[0] * sinAngle + projected[1] * cosAngle] : projected; + + return { + x: ((rotated[0] - (transform.xoffset || 0)) * (transform.scale || 1) + (transform.xpan || 0)) * (transform.jsonres || 1) + (transform.jsonmarginX || 0), + y: (((transform.yoffset || 0) - rotated[1]) * (transform.scale || 1) + (transform.ypan || 0)) * (transform.jsonres || 1) - (transform.jsonmarginY || 0) + }; }; /** @@ -101,27 +101,27 @@ Chart.prototype.transformFromLatLon = function (latLon, transform) { * * @sample maps/series/latlon-transform/ * Use specific transformation for lat/lon - * + * */ Chart.prototype.transformToLatLon = function (point, transform) { - if (win.proj4 === undefined) { - H.error(21); - return; - } - - var normalized = { - x: ((point.x - (transform.jsonmarginX || 0)) / (transform.jsonres || 1) - (transform.xpan || 0)) / (transform.scale || 1) + (transform.xoffset || 0), - y: ((-point.y - (transform.jsonmarginY || 0)) / (transform.jsonres || 1) + (transform.ypan || 0)) / (transform.scale || 1) + (transform.yoffset || 0) - }, - cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)), - sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)), - // Note: Inverted sinAngle to reverse rotation direction - projected = win.proj4(transform.crs, 'WGS84', transform.rotation ? { - x: normalized.x * cosAngle + normalized.y * -sinAngle, - y: normalized.x * sinAngle + normalized.y * cosAngle - } : normalized); - - return { lat: projected.y, lon: projected.x }; + if (win.proj4 === undefined) { + H.error(21); + return; + } + + var normalized = { + x: ((point.x - (transform.jsonmarginX || 0)) / (transform.jsonres || 1) - (transform.xpan || 0)) / (transform.scale || 1) + (transform.xoffset || 0), + y: ((-point.y - (transform.jsonmarginY || 0)) / (transform.jsonres || 1) + (transform.ypan || 0)) / (transform.scale || 1) + (transform.yoffset || 0) + }, + cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)), + sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)), + // Note: Inverted sinAngle to reverse rotation direction + projected = win.proj4(transform.crs, 'WGS84', transform.rotation ? { + x: normalized.x * cosAngle + normalized.y * -sinAngle, + y: normalized.x * sinAngle + normalized.y * cosAngle + } : normalized); + + return { lat: projected.y, lon: projected.x }; }; /** @@ -130,7 +130,7 @@ Chart.prototype.transformToLatLon = function (point, transform) { * * @function fromPointToLatLon * @memberOf Chart.prototype - * + * * @param {Point|Object} point * A `Point` instance or anything containing `x` and `y` properties * with numeric values @@ -141,22 +141,22 @@ Chart.prototype.transformToLatLon = function (point, transform) { * Advanced lat/lon demo */ Chart.prototype.fromPointToLatLon = function (point) { - var transforms = this.mapTransforms, - transform; - - if (!transforms) { - H.error(22); - return; - } - - for (transform in transforms) { - if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone && - pointInPolygon({ x: point.x, y: -point.y }, transforms[transform].hitZone.coordinates[0])) { - return this.transformToLatLon(point, transforms[transform]); - } - } - - return this.transformToLatLon(point, transforms['default']); // eslint-disable-line dot-notation + var transforms = this.mapTransforms, + transform; + + if (!transforms) { + H.error(22); + return; + } + + for (transform in transforms) { + if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone && + pointInPolygon({ x: point.x, y: -point.y }, transforms[transform].hitZone.coordinates[0])) { + return this.transformToLatLon(point, transforms[transform]); + } + } + + return this.transformToLatLon(point, transforms['default']); // eslint-disable-line dot-notation }; /** @@ -165,7 +165,7 @@ Chart.prototype.fromPointToLatLon = function (point) { * * @function fromLatLonToPoint * @memberOf Chart.prototype - * + * * @param {Object} latLon * Coordinates. * @param {Number} latLon.lat @@ -175,33 +175,33 @@ Chart.prototype.fromPointToLatLon = function (point) { * * @sample maps/series/latlon-to-point/ * Find a point from lat/lon - * + * * @return {Object} * X and Y coordinates in terms of chart axis values. */ Chart.prototype.fromLatLonToPoint = function (latLon) { - var transforms = this.mapTransforms, - transform, - coords; - - if (!transforms) { - H.error(22); - return { - x: 0, - y: null - }; - } - - for (transform in transforms) { - if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone) { - coords = this.transformFromLatLon(latLon, transforms[transform]); - if (pointInPolygon({ x: coords.x, y: -coords.y }, transforms[transform].hitZone.coordinates[0])) { - return coords; - } - } - } - - return this.transformFromLatLon(latLon, transforms['default']); // eslint-disable-line dot-notation + var transforms = this.mapTransforms, + transform, + coords; + + if (!transforms) { + H.error(22); + return { + x: 0, + y: null + }; + } + + for (transform in transforms) { + if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone) { + coords = this.transformFromLatLon(latLon, transforms[transform]); + if (pointInPolygon({ x: coords.x, y: -coords.y }, transforms[transform].hitZone.coordinates[0])) { + return coords; + } + } + } + + return this.transformFromLatLon(latLon, transforms['default']); // eslint-disable-line dot-notation }; /** @@ -210,7 +210,7 @@ Chart.prototype.fromLatLonToPoint = function (latLon) { * https://api.highcharts.com/highmaps/plotOptions.series.mapData| * series.mapData} option. The GeoJSON will be broken down to fit a specific * Highcharts type, either `map`, `mapline` or `mappoint`. Meta data in - * GeoJSON's properties object will be copied directly over to + * GeoJSON's properties object will be copied directly over to * {@link Point.properties} in Highmaps. * * @function #geojson @@ -232,95 +232,95 @@ Chart.prototype.fromLatLonToPoint = function (latLon) { * Simple areas * @sample maps/demo/geojson-multiple-types/ * Multiple types - * + * */ H.geojson = function (geojson, hType, series) { - var mapData = [], - path = [], - polygonToPath = function (polygon) { - var i, - len = polygon.length; - path.push('M'); - for (i = 0; i < len; i++) { - if (i === 1) { - path.push('L'); - } - path.push(polygon[i][0], -polygon[i][1]); - } - }; - - hType = hType || 'map'; - - each(geojson.features, function (feature) { - - var geometry = feature.geometry, - type = geometry.type, - coordinates = geometry.coordinates, - properties = feature.properties, - point; - - path = []; - - if (hType === 'map' || hType === 'mapbubble') { - if (type === 'Polygon') { - each(coordinates, polygonToPath); - path.push('Z'); - - } else if (type === 'MultiPolygon') { - each(coordinates, function (items) { - each(items, polygonToPath); - }); - path.push('Z'); - } - - if (path.length) { - point = { path: path }; - } - - } else if (hType === 'mapline') { - if (type === 'LineString') { - polygonToPath(coordinates); - } else if (type === 'MultiLineString') { - each(coordinates, polygonToPath); - } - - if (path.length) { - point = { path: path }; - } - - } else if (hType === 'mappoint') { - if (type === 'Point') { - point = { - x: coordinates[0], - y: -coordinates[1] - }; - } - } - if (point) { - mapData.push(extend(point, { - name: properties.name || properties.NAME, - - /** - * In Highmaps, when data is loaded from GeoJSON, the GeoJSON - * item's properies are copied over here. - * - * @name #properties - * @memberOf Point - * @type {Object} - */ - properties: properties - })); - } - - }); - - // Create a credits text that includes map source, to be picked up in Chart.addCredits - if (series && geojson.copyrightShort) { - series.chart.mapCredits = format(series.chart.options.credits.mapText, { geojson: geojson }); - series.chart.mapCreditsFull = format(series.chart.options.credits.mapTextFull, { geojson: geojson }); - } - - return mapData; + var mapData = [], + path = [], + polygonToPath = function (polygon) { + var i, + len = polygon.length; + path.push('M'); + for (i = 0; i < len; i++) { + if (i === 1) { + path.push('L'); + } + path.push(polygon[i][0], -polygon[i][1]); + } + }; + + hType = hType || 'map'; + + each(geojson.features, function (feature) { + + var geometry = feature.geometry, + type = geometry.type, + coordinates = geometry.coordinates, + properties = feature.properties, + point; + + path = []; + + if (hType === 'map' || hType === 'mapbubble') { + if (type === 'Polygon') { + each(coordinates, polygonToPath); + path.push('Z'); + + } else if (type === 'MultiPolygon') { + each(coordinates, function (items) { + each(items, polygonToPath); + }); + path.push('Z'); + } + + if (path.length) { + point = { path: path }; + } + + } else if (hType === 'mapline') { + if (type === 'LineString') { + polygonToPath(coordinates); + } else if (type === 'MultiLineString') { + each(coordinates, polygonToPath); + } + + if (path.length) { + point = { path: path }; + } + + } else if (hType === 'mappoint') { + if (type === 'Point') { + point = { + x: coordinates[0], + y: -coordinates[1] + }; + } + } + if (point) { + mapData.push(extend(point, { + name: properties.name || properties.NAME, + + /** + * In Highmaps, when data is loaded from GeoJSON, the GeoJSON + * item's properies are copied over here. + * + * @name #properties + * @memberOf Point + * @type {Object} + */ + properties: properties + })); + } + + }); + + // Create a credits text that includes map source, to be picked up in Chart.addCredits + if (series && geojson.copyrightShort) { + series.chart.mapCredits = format(series.chart.options.credits.mapText, { geojson: geojson }); + series.chart.mapCreditsFull = format(series.chart.options.credits.mapTextFull, { geojson: geojson }); + } + + return mapData; }; /** @@ -328,19 +328,19 @@ H.geojson = function (geojson, hType, series) { */ wrap(Chart.prototype, 'addCredits', function (proceed, credits) { - credits = merge(true, this.options.credits, credits); + credits = merge(true, this.options.credits, credits); - // Disable credits link if map credits enabled. This to allow for in-text anchors. - if (this.mapCredits) { - credits.href = null; - } + // Disable credits link if map credits enabled. This to allow for in-text anchors. + if (this.mapCredits) { + credits.href = null; + } - proceed.call(this, credits); + proceed.call(this, credits); - // Add full map credits to hover - if (this.credits && this.mapCreditsFull) { - this.credits.attr({ - title: this.mapCreditsFull - }); - } + // Add full map credits to hover + if (this.credits && this.mapCreditsFull) { + this.credits.attr({ + title: this.mapCreditsFull + }); + } }); diff --git a/js/parts-map/HeatmapSeries.js b/js/parts-map/HeatmapSeries.js index 7b7458ba486..608f3bc393b 100644 --- a/js/parts-map/HeatmapSeries.js +++ b/js/parts-map/HeatmapSeries.js @@ -12,15 +12,15 @@ import '../parts/Series.js'; import '../parts/Legend.js'; import './ColorSeriesMixin.js'; var colorPointMixin = H.colorPointMixin, - colorSeriesMixin = H.colorSeriesMixin, - each = H.each, - LegendSymbolMixin = H.LegendSymbolMixin, - merge = H.merge, - noop = H.noop, - pick = H.pick, - Series = H.Series, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + colorSeriesMixin = H.colorSeriesMixin, + each = H.each, + LegendSymbolMixin = H.LegendSymbolMixin, + merge = H.merge, + noop = H.noop, + pick = H.pick, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; /** @@ -38,240 +38,240 @@ var colorPointMixin = H.colorPointMixin, */ seriesType('heatmap', 'scatter', { - /** - * Animation is disabled by default on the heatmap series. - * - * @type {Boolean|Object} - */ - animation: false, - - /** - * The border width for each heat map item. - */ - borderWidth: 0, - - /** - * Padding between the points in the heatmap. - * - * @type {Number} - * @default 0 - * @since 6.0 - * @apioption plotOptions.heatmap.pointPadding - */ - - /** - * The main color of the series. In heat maps this color is rarely used, - * as we mostly use the color to denote the value of each point. Unless - * options are set in the [colorAxis](#colorAxis), the default value - * is pulled from the [options.colors](#colors) array. - * - * @type {Color} - * @default null - * @since 4.0 - * @product highcharts - * @apioption plotOptions.heatmap.color - */ - - /** - * The column size - how many X axis units each column in the heatmap - * should span. - * - * @type {Number} - * @sample {highcharts} maps/demo/heatmap/ One day - * @sample {highmaps} maps/demo/heatmap/ One day - * @default 1 - * @since 4.0 - * @product highcharts highmaps - * @apioption plotOptions.heatmap.colsize - */ - - /** - * The row size - how many Y axis units each heatmap row should span. - * - * @type {Number} - * @sample {highcharts} maps/demo/heatmap/ 1 by default - * @sample {highmaps} maps/demo/heatmap/ 1 by default - * @default 1 - * @since 4.0 - * @product highcharts highmaps - * @apioption plotOptions.heatmap.rowsize - */ - - /*= if (build.classic) { =*/ - - /** - * The color applied to null points. In styled mode, a general CSS class is - * applied instead. - * - * @type {Color} - */ - nullColor: '${palette.neutralColor3}', - /*= } =*/ - - dataLabels: { - - formatter: function () { // #2945 - return this.point.value; - }, - inside: true, - verticalAlign: 'middle', - crop: false, - overflow: false, - padding: 0 // #3837 - }, - - /** - * @ignore - */ - marker: null, - - /** @ignore */ - pointRange: null, // dynamically set to colsize by default - - tooltip: { - pointFormat: '{point.x}, {point.y}: {point.value}
' - }, - - states: { - - hover: { - /** - * @ignore - */ - halo: false, // #3406, halo is disabled on heatmaps by default - - /** - * How much to brighten the point on interaction. Requires the main - * color to be defined in hex or rgb(a) format. - * - * In styled mode, the hover brightening is by default replaced - * with a fill-opacity set in the `.highcharts-point:hover` rule. - * - * @type {Number} - * @product highcharts highmaps - */ - brightness: 0.2 - } - } + /** + * Animation is disabled by default on the heatmap series. + * + * @type {Boolean|Object} + */ + animation: false, + + /** + * The border width for each heat map item. + */ + borderWidth: 0, + + /** + * Padding between the points in the heatmap. + * + * @type {Number} + * @default 0 + * @since 6.0 + * @apioption plotOptions.heatmap.pointPadding + */ + + /** + * The main color of the series. In heat maps this color is rarely used, + * as we mostly use the color to denote the value of each point. Unless + * options are set in the [colorAxis](#colorAxis), the default value + * is pulled from the [options.colors](#colors) array. + * + * @type {Color} + * @default null + * @since 4.0 + * @product highcharts + * @apioption plotOptions.heatmap.color + */ + + /** + * The column size - how many X axis units each column in the heatmap + * should span. + * + * @type {Number} + * @sample {highcharts} maps/demo/heatmap/ One day + * @sample {highmaps} maps/demo/heatmap/ One day + * @default 1 + * @since 4.0 + * @product highcharts highmaps + * @apioption plotOptions.heatmap.colsize + */ + + /** + * The row size - how many Y axis units each heatmap row should span. + * + * @type {Number} + * @sample {highcharts} maps/demo/heatmap/ 1 by default + * @sample {highmaps} maps/demo/heatmap/ 1 by default + * @default 1 + * @since 4.0 + * @product highcharts highmaps + * @apioption plotOptions.heatmap.rowsize + */ + + /*= if (build.classic) { =*/ + + /** + * The color applied to null points. In styled mode, a general CSS class is + * applied instead. + * + * @type {Color} + */ + nullColor: '${palette.neutralColor3}', + /*= } =*/ + + dataLabels: { + + formatter: function () { // #2945 + return this.point.value; + }, + inside: true, + verticalAlign: 'middle', + crop: false, + overflow: false, + padding: 0 // #3837 + }, + + /** + * @ignore + */ + marker: null, + + /** @ignore */ + pointRange: null, // dynamically set to colsize by default + + tooltip: { + pointFormat: '{point.x}, {point.y}: {point.value}
' + }, + + states: { + + hover: { + /** + * @ignore + */ + halo: false, // #3406, halo is disabled on heatmaps by default + + /** + * How much to brighten the point on interaction. Requires the main + * color to be defined in hex or rgb(a) format. + * + * In styled mode, the hover brightening is by default replaced + * with a fill-opacity set in the `.highcharts-point:hover` rule. + * + * @type {Number} + * @product highcharts highmaps + */ + brightness: 0.2 + } + } }, merge(colorSeriesMixin, { - pointArrayMap: ['y', 'value'], - hasPointSpecificOptions: true, - getExtremesFromAll: true, - directTouch: true, - - /** - * Override the init method to add point ranges on both axes. - */ - init: function () { - var options; - seriesTypes.scatter.prototype.init.apply(this, arguments); - - options = this.options; - // #3758, prevent resetting in setData - options.pointRange = pick(options.pointRange, options.colsize || 1); - this.yAxis.axisPointRange = options.rowsize || 1; // general point range - }, - translate: function () { - var series = this, - options = series.options, - xAxis = series.xAxis, - yAxis = series.yAxis, - seriesPointPadding = options.pointPadding || 0, - between = function (x, a, b) { - return Math.min(Math.max(a, x), b); - }; - - series.generatePoints(); - - each(series.points, function (point) { - var xPad = (options.colsize || 1) / 2, - yPad = (options.rowsize || 1) / 2, - x1 = between( - Math.round( - xAxis.len - - xAxis.translate(point.x - xPad, 0, 1, 0, 1) - ), - -xAxis.len, 2 * xAxis.len - ), - x2 = between( - Math.round( - xAxis.len - - xAxis.translate(point.x + xPad, 0, 1, 0, 1) - ), - -xAxis.len, 2 * xAxis.len - ), - y1 = between( - Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), - -yAxis.len, 2 * yAxis.len - ), - y2 = between( - Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), - -yAxis.len, 2 * yAxis.len - ), - pointPadding = pick(point.pointPadding, seriesPointPadding); - - // Set plotX and plotY for use in K-D-Tree and more - point.plotX = point.clientX = (x1 + x2) / 2; - point.plotY = (y1 + y2) / 2; - - point.shapeType = 'rect'; - point.shapeArgs = { - x: Math.min(x1, x2) + pointPadding, - y: Math.min(y1, y2) + pointPadding, - width: Math.abs(x2 - x1) - pointPadding * 2, - height: Math.abs(y2 - y1) - pointPadding * 2 - }; - }); - - series.translateColors(); - }, - drawPoints: function () { - seriesTypes.column.prototype.drawPoints.call(this); - - each(this.points, function (point) { - /*= if (build.classic) { =*/ - point.graphic.attr(this.colorAttribs(point)); - /*= } else { =*/ - // In styled mode, use CSS, otherwise the fill used in the style - // sheet will take precedence over the fill attribute. - point.graphic.css(this.colorAttribs(point)); - /*= } =*/ - }, this); - }, - animate: noop, - getBox: noop, - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - alignDataLabel: seriesTypes.column.prototype.alignDataLabel, - getExtremes: function () { - // Get the extremes from the value data - Series.prototype.getExtremes.call(this, this.valueData); - this.valueMin = this.dataMin; - this.valueMax = this.dataMax; - - // Get the extremes from the y data - Series.prototype.getExtremes.call(this); - } + pointArrayMap: ['y', 'value'], + hasPointSpecificOptions: true, + getExtremesFromAll: true, + directTouch: true, + + /** + * Override the init method to add point ranges on both axes. + */ + init: function () { + var options; + seriesTypes.scatter.prototype.init.apply(this, arguments); + + options = this.options; + // #3758, prevent resetting in setData + options.pointRange = pick(options.pointRange, options.colsize || 1); + this.yAxis.axisPointRange = options.rowsize || 1; // general point range + }, + translate: function () { + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + seriesPointPadding = options.pointPadding || 0, + between = function (x, a, b) { + return Math.min(Math.max(a, x), b); + }; + + series.generatePoints(); + + each(series.points, function (point) { + var xPad = (options.colsize || 1) / 2, + yPad = (options.rowsize || 1) / 2, + x1 = between( + Math.round( + xAxis.len - + xAxis.translate(point.x - xPad, 0, 1, 0, 1) + ), + -xAxis.len, 2 * xAxis.len + ), + x2 = between( + Math.round( + xAxis.len - + xAxis.translate(point.x + xPad, 0, 1, 0, 1) + ), + -xAxis.len, 2 * xAxis.len + ), + y1 = between( + Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), + -yAxis.len, 2 * yAxis.len + ), + y2 = between( + Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), + -yAxis.len, 2 * yAxis.len + ), + pointPadding = pick(point.pointPadding, seriesPointPadding); + + // Set plotX and plotY for use in K-D-Tree and more + point.plotX = point.clientX = (x1 + x2) / 2; + point.plotY = (y1 + y2) / 2; + + point.shapeType = 'rect'; + point.shapeArgs = { + x: Math.min(x1, x2) + pointPadding, + y: Math.min(y1, y2) + pointPadding, + width: Math.abs(x2 - x1) - pointPadding * 2, + height: Math.abs(y2 - y1) - pointPadding * 2 + }; + }); + + series.translateColors(); + }, + drawPoints: function () { + seriesTypes.column.prototype.drawPoints.call(this); + + each(this.points, function (point) { + /*= if (build.classic) { =*/ + point.graphic.attr(this.colorAttribs(point)); + /*= } else { =*/ + // In styled mode, use CSS, otherwise the fill used in the style + // sheet will take precedence over the fill attribute. + point.graphic.css(this.colorAttribs(point)); + /*= } =*/ + }, this); + }, + animate: noop, + getBox: noop, + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + alignDataLabel: seriesTypes.column.prototype.alignDataLabel, + getExtremes: function () { + // Get the extremes from the value data + Series.prototype.getExtremes.call(this, this.valueData); + this.valueMin = this.dataMin; + this.valueMax = this.dataMax; + + // Get the extremes from the y data + Series.prototype.getExtremes.call(this); + } }), H.extend({ - haloPath: function (size) { - if (!size) { - return []; - } - var rect = this.shapeArgs; - return [ - 'M', rect.x - size, rect.y - size, - 'L', rect.x - size, rect.y + rect.height + size, - rect.x + rect.width + size, rect.y + rect.height + size, - rect.x + rect.width + size, rect.y - size, - 'Z' - ]; - } + haloPath: function (size) { + if (!size) { + return []; + } + var rect = this.shapeArgs; + return [ + 'M', rect.x - size, rect.y - size, + 'L', rect.x - size, rect.y + rect.height + size, + rect.x + rect.width + size, rect.y + rect.height + size, + rect.x + rect.width + size, rect.y - size, + 'Z' + ]; + } }, colorPointMixin)); /** * A `heatmap` series. If the [type](#series.heatmap.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.heatmap * @excluding dataParser,dataURL,marker,pointRange,stack @@ -282,7 +282,7 @@ seriesType('heatmap', 'scatter', { /** * An array of data points for the series. For the `heatmap` series * type, points can be given in the following ways: - * + * * 1. An array of arrays with 3 or 2 values. In this case, the values * correspond to `x,y,value`. If the first value is a string, it is * applied as the name of the point, and the `x` value is inferred. @@ -290,7 +290,7 @@ seriesType('heatmap', 'scatter', { * should be of length 2\. 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, 9, 7], @@ -298,12 +298,12 @@ seriesType('heatmap', 'scatter', { * [2, 6, 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.heatmap.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -319,7 +319,7 @@ seriesType('heatmap', 'scatter', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding marker @@ -341,7 +341,7 @@ seriesType('heatmap', 'scatter', { * The color of the point. In heat maps the point color is rarely set * explicitly, as we use the color to denote the `value`. Options for * this are set in the [colorAxis](#colorAxis) configuration. - * + * * @type {Color} * @product highcharts highmaps * @apioption series.heatmap.data.color @@ -350,7 +350,7 @@ seriesType('heatmap', 'scatter', { /** * The value of the point, resulting in a color controled by options * as set in the [colorAxis](#colorAxis) configuration. - * + * * @type {Number} * @product highcharts highmaps * @apioption series.heatmap.data.value @@ -359,7 +359,7 @@ seriesType('heatmap', 'scatter', { /** * The x value of the point. For datetime axes, * the X value is the timestamp in milliseconds since 1970. - * + * * @type {Number} * @product highcharts highmaps * @apioption series.heatmap.data.x @@ -367,7 +367,7 @@ seriesType('heatmap', 'scatter', { /** * The y value of the point. - * + * * @type {Number} * @product highcharts highmaps * @apioption series.heatmap.data.y diff --git a/js/parts-map/Map.js b/js/parts-map/Map.js index 31f0ba75304..b061e8b160e 100644 --- a/js/parts-map/Map.js +++ b/js/parts-map/Map.js @@ -10,297 +10,297 @@ import '../parts/Options.js'; import '../parts/Chart.js'; import '../parts/SvgRenderer.js'; var Chart = H.Chart, - defaultOptions = H.defaultOptions, - each = H.each, - extend = H.extend, - merge = H.merge, - pick = H.pick, - Renderer = H.Renderer, - SVGRenderer = H.SVGRenderer, - VMLRenderer = H.VMLRenderer; + defaultOptions = H.defaultOptions, + each = H.each, + extend = H.extend, + merge = H.merge, + pick = H.pick, + Renderer = H.Renderer, + SVGRenderer = H.SVGRenderer, + VMLRenderer = H.VMLRenderer; // Add language extend(defaultOptions.lang, { - zoomIn: 'Zoom in', - zoomOut: 'Zoom out' + zoomIn: 'Zoom in', + zoomOut: 'Zoom out' }); // Set the default map navigation options -/** +/** * @product highmaps - * @optionparent mapNavigation + * @optionparent mapNavigation */ defaultOptions.mapNavigation = { - /** - * General options for the map navigation buttons. Individual options - * can be given from the [mapNavigation.buttons](#mapNavigation.buttons) - * option set. - * - * @type {Object} - * @sample {highmaps} maps/mapnavigation/button-theme/ - * Theming the navigation buttons - * @product highmaps - */ - buttonOptions: { - - /** - * What box to align the buttons to. Possible values are `plotBox` - * and `spacingBox`. - * - * @validvalue ["plotBox", "spacingBox"] - * @type {String} - * @default plotBox - * @product highmaps - */ - alignTo: 'plotBox', - - /** - * The alignment of the navigation buttons. - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @default left - * @product highmaps - */ - align: 'left', - - /** - * The vertical alignment of the buttons. Individual alignment can - * be adjusted by each button's `y` offset. - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @default bottom - * @product highmaps - */ - verticalAlign: 'top', - - /** - * The X offset of the buttons relative to its `align` setting. - * - * @type {Number} - * @default 0 - * @product highmaps - */ - x: 0, - - /** - * The width of the map navigation buttons. - * - * @type {Number} - * @default 18 - * @product highmaps - */ - width: 18, - - /** - * The pixel height of the map navigation buttons. - * - * @type {Number} - * @default 18 - * @product highmaps - */ - height: 18, - - /** - * Padding for the navigation buttons. - * - * @type {Number} - * @default 5 - * @since 5.0.0 - * @product highmaps - */ - padding: 5, - /*= if (build.classic) { =*/ - - /** - * Text styles for the map navigation buttons. Defaults to - * - *
{
-		 *     fontSize: '15px',
-		 *     fontWeight: 'bold',
-		 *     textAlign: 'center'
-		 * }
- * - * @type {CSSObject} - * @product highmaps - */ - style: { - fontSize: '15px', - fontWeight: 'bold' - }, - - /** - * A configuration object for the button theme. The object accepts - * SVG properties like `stroke-width`, `stroke` and `fill`. Tri-state - * button styles are supported by the `states.hover` and `states.select` - * objects. - * - * @type {Object} - * @sample {highmaps} maps/mapnavigation/button-theme/ - * Themed navigation buttons - * @product highmaps - */ - theme: { - 'stroke-width': 1, - 'text-align': 'center' - } - /*= } =*/ - }, - - /** - * The individual buttons for the map navigation. This usually includes - * the zoom in and zoom out buttons. Properties for each button is - * inherited from - * [mapNavigation.buttonOptions](#mapNavigation.buttonOptions), while - * individual options can be overridden. But default, the `onclick`, `text` - * and `y` options are individual. - * - * @type {Object} - * @product highmaps - */ - buttons: { - - /** - * Options for the zoom in button. Properties for the zoom in and zoom - * out buttons are inherited from - * [mapNavigation.buttonOptions](#mapNavigation.buttonOptions), while - * individual options can be overridden. By default, the `onclick`, - * `text` and `y` options are individual. - * - * @type {Object} - * @extends mapNavigation.buttonOptions - * @product highmaps - */ - zoomIn: { - - /** - * Click handler for the button. Defaults to: - * - *
function () {
-			 * this.mapZoom(0.5);
-			 * }
- * - * @type {Function} - * @product highmaps - */ - onclick: function () { - this.mapZoom(0.5); - }, - - /** - * The text for the button. The tooltip (title) is a language option - * given by [lang.zoomIn](#lang.zoomIn). - * - * @type {String} - * @default + - * @product highmaps - */ - text: '+', - - /** - * The position of the zoomIn button relative to the vertical - * alignment. - * - * @type {Number} - * @default 0 - * @product highmaps - */ - y: 0 - }, - - /** - * Options for the zoom out button. Properties for the zoom in and - * zoom out buttons are inherited from - * [mapNavigation.buttonOptions](#mapNavigation.buttonOptions), while - * individual options can be overridden. By default, the `onclick`, - * `text` and `y` options are individual. - * - * @type {Object} - * @extends mapNavigation.buttonOptions - * @product highmaps - */ - zoomOut: { - - /** - * Click handler for the button. Defaults to: - * - *
function () {
-			 *     this.mapZoom(2);
-			 * }
- * - * @type {Function} - * @product highmaps - */ - onclick: function () { - this.mapZoom(2); - }, - - /** - * The text for the button. The tooltip (title) is a language option - * given by [lang.zoomOut](#lang.zoomIn). - * - * @type {String} - * @default - - * @product highmaps - */ - text: '-', - - /** - * The position of the zoomOut button relative to the vertical - * alignment. - * - * @type {Number} - * @default 28 - * @product highmaps - */ - y: 28 - } - }, - - /** - * Sensitivity of mouse wheel or trackpad scrolling. 1 is no sensitivity, - * while with 2, one mousewheel delta will zoom in 50%. - * - * @type {Number} - * @default 1.1 - * @since 4.2.4 - * @product highmaps - */ - mouseWheelSensitivity: 1.1 - // enabled: false, - // enableButtons: null, // inherit from enabled - // enableTouchZoom: null, // inherit from enabled - // enableDoubleClickZoom: null, // inherit from enabled - // enableDoubleClickZoomTo: false - // enableMouseWheelZoom: null, // inherit from enabled + /** + * General options for the map navigation buttons. Individual options + * can be given from the [mapNavigation.buttons](#mapNavigation.buttons) + * option set. + * + * @type {Object} + * @sample {highmaps} maps/mapnavigation/button-theme/ + * Theming the navigation buttons + * @product highmaps + */ + buttonOptions: { + + /** + * What box to align the buttons to. Possible values are `plotBox` + * and `spacingBox`. + * + * @validvalue ["plotBox", "spacingBox"] + * @type {String} + * @default plotBox + * @product highmaps + */ + alignTo: 'plotBox', + + /** + * The alignment of the navigation buttons. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @default left + * @product highmaps + */ + align: 'left', + + /** + * The vertical alignment of the buttons. Individual alignment can + * be adjusted by each button's `y` offset. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @default bottom + * @product highmaps + */ + verticalAlign: 'top', + + /** + * The X offset of the buttons relative to its `align` setting. + * + * @type {Number} + * @default 0 + * @product highmaps + */ + x: 0, + + /** + * The width of the map navigation buttons. + * + * @type {Number} + * @default 18 + * @product highmaps + */ + width: 18, + + /** + * The pixel height of the map navigation buttons. + * + * @type {Number} + * @default 18 + * @product highmaps + */ + height: 18, + + /** + * Padding for the navigation buttons. + * + * @type {Number} + * @default 5 + * @since 5.0.0 + * @product highmaps + */ + padding: 5, + /*= if (build.classic) { =*/ + + /** + * Text styles for the map navigation buttons. Defaults to + * + *
{
+         *     fontSize: '15px',
+         *     fontWeight: 'bold',
+         *     textAlign: 'center'
+         * }
+ * + * @type {CSSObject} + * @product highmaps + */ + style: { + fontSize: '15px', + fontWeight: 'bold' + }, + + /** + * A configuration object for the button theme. The object accepts + * SVG properties like `stroke-width`, `stroke` and `fill`. Tri-state + * button styles are supported by the `states.hover` and `states.select` + * objects. + * + * @type {Object} + * @sample {highmaps} maps/mapnavigation/button-theme/ + * Themed navigation buttons + * @product highmaps + */ + theme: { + 'stroke-width': 1, + 'text-align': 'center' + } + /*= } =*/ + }, + + /** + * The individual buttons for the map navigation. This usually includes + * the zoom in and zoom out buttons. Properties for each button is + * inherited from + * [mapNavigation.buttonOptions](#mapNavigation.buttonOptions), while + * individual options can be overridden. But default, the `onclick`, `text` + * and `y` options are individual. + * + * @type {Object} + * @product highmaps + */ + buttons: { + + /** + * Options for the zoom in button. Properties for the zoom in and zoom + * out buttons are inherited from + * [mapNavigation.buttonOptions](#mapNavigation.buttonOptions), while + * individual options can be overridden. By default, the `onclick`, + * `text` and `y` options are individual. + * + * @type {Object} + * @extends mapNavigation.buttonOptions + * @product highmaps + */ + zoomIn: { + + /** + * Click handler for the button. Defaults to: + * + *
function () {
+             * this.mapZoom(0.5);
+             * }
+ * + * @type {Function} + * @product highmaps + */ + onclick: function () { + this.mapZoom(0.5); + }, + + /** + * The text for the button. The tooltip (title) is a language option + * given by [lang.zoomIn](#lang.zoomIn). + * + * @type {String} + * @default + + * @product highmaps + */ + text: '+', + + /** + * The position of the zoomIn button relative to the vertical + * alignment. + * + * @type {Number} + * @default 0 + * @product highmaps + */ + y: 0 + }, + + /** + * Options for the zoom out button. Properties for the zoom in and + * zoom out buttons are inherited from + * [mapNavigation.buttonOptions](#mapNavigation.buttonOptions), while + * individual options can be overridden. By default, the `onclick`, + * `text` and `y` options are individual. + * + * @type {Object} + * @extends mapNavigation.buttonOptions + * @product highmaps + */ + zoomOut: { + + /** + * Click handler for the button. Defaults to: + * + *
function () {
+             *     this.mapZoom(2);
+             * }
+ * + * @type {Function} + * @product highmaps + */ + onclick: function () { + this.mapZoom(2); + }, + + /** + * The text for the button. The tooltip (title) is a language option + * given by [lang.zoomOut](#lang.zoomIn). + * + * @type {String} + * @default - + * @product highmaps + */ + text: '-', + + /** + * The position of the zoomOut button relative to the vertical + * alignment. + * + * @type {Number} + * @default 28 + * @product highmaps + */ + y: 28 + } + }, + + /** + * Sensitivity of mouse wheel or trackpad scrolling. 1 is no sensitivity, + * while with 2, one mousewheel delta will zoom in 50%. + * + * @type {Number} + * @default 1.1 + * @since 4.2.4 + * @product highmaps + */ + mouseWheelSensitivity: 1.1 + // enabled: false, + // enableButtons: null, // inherit from enabled + // enableTouchZoom: null, // inherit from enabled + // enableDoubleClickZoom: null, // inherit from enabled + // enableDoubleClickZoomTo: false + // enableMouseWheelZoom: null, // inherit from enabled }; /** * Utility for reading SVG paths directly. */ H.splitPath = function (path) { - var i; - - // Move letters apart - path = path.replace(/([A-Za-z])/g, ' $1 '); - // Trim - path = path.replace(/^\s*/, '').replace(/\s*$/, ''); - - // Split on spaces and commas - path = path.split(/[ ,,]+/); // Extra comma to escape gulp.scripts task - - // Parse numbers - for (i = 0; i < path.length; i++) { - if (!/[a-zA-Z]/.test(path[i])) { - path[i] = parseFloat(path[i]); - } - } - return path; + var i; + + // Move letters apart + path = path.replace(/([A-Za-z])/g, ' $1 '); + // Trim + path = path.replace(/^\s*/, '').replace(/\s*$/, ''); + + // Split on spaces and commas + path = path.split(/[ ,,]+/); // Extra comma to escape gulp.scripts task + + // Parse numbers + for (i = 0; i < path.length; i++) { + if (!/[a-zA-Z]/.test(path[i])) { + path[i] = parseFloat(path[i]); + } + } + return path; }; // A placeholder for map definitions @@ -312,65 +312,65 @@ H.maps = {}; // Create symbols for the zoom buttons function selectiveRoundedRect( - x, - y, - w, - h, - rTopLeft, - rTopRight, - rBottomRight, - rBottomLeft + x, + y, + w, + h, + rTopLeft, + rTopRight, + rBottomRight, + rBottomLeft ) { - return [ - 'M', x + rTopLeft, y, - // top side - 'L', x + w - rTopRight, y, - // top right corner - 'C', x + w - rTopRight / 2, - y, x + w, - y + rTopRight / 2, x + w, y + rTopRight, - // right side - 'L', x + w, y + h - rBottomRight, - // bottom right corner - 'C', x + w, y + h - rBottomRight / 2, - x + w - rBottomRight / 2, y + h, - x + w - rBottomRight, y + h, - // bottom side - 'L', x + rBottomLeft, y + h, - // bottom left corner - 'C', x + rBottomLeft / 2, y + h, - x, y + h - rBottomLeft / 2, - x, y + h - rBottomLeft, - // left side - 'L', x, y + rTopLeft, - // top left corner - 'C', x, y + rTopLeft / 2, - x + rTopLeft / 2, y, - x + rTopLeft, y, - 'Z' - ]; + return [ + 'M', x + rTopLeft, y, + // top side + 'L', x + w - rTopRight, y, + // top right corner + 'C', x + w - rTopRight / 2, + y, x + w, + y + rTopRight / 2, x + w, y + rTopRight, + // right side + 'L', x + w, y + h - rBottomRight, + // bottom right corner + 'C', x + w, y + h - rBottomRight / 2, + x + w - rBottomRight / 2, y + h, + x + w - rBottomRight, y + h, + // bottom side + 'L', x + rBottomLeft, y + h, + // bottom left corner + 'C', x + rBottomLeft / 2, y + h, + x, y + h - rBottomLeft / 2, + x, y + h - rBottomLeft, + // left side + 'L', x, y + rTopLeft, + // top left corner + 'C', x, y + rTopLeft / 2, + x + rTopLeft / 2, y, + x + rTopLeft, y, + 'Z' + ]; } SVGRenderer.prototype.symbols.topbutton = function (x, y, w, h, attr) { - return selectiveRoundedRect(x - 1, y - 1, w, h, attr.r, attr.r, 0, 0); + return selectiveRoundedRect(x - 1, y - 1, w, h, attr.r, attr.r, 0, 0); }; SVGRenderer.prototype.symbols.bottombutton = function (x, y, w, h, attr) { - return selectiveRoundedRect(x - 1, y - 1, w, h, 0, 0, attr.r, attr.r); + return selectiveRoundedRect(x - 1, y - 1, w, h, 0, 0, attr.r, attr.r); }; // The symbol callbacks are generated on the SVGRenderer object in all browsers. // Even VML browsers need this in order to generate shapes in export. Now share // them with the VMLRenderer. if (Renderer === VMLRenderer) { - each(['topbutton', 'bottombutton'], function (shape) { - VMLRenderer.prototype.symbols[shape] = - SVGRenderer.prototype.symbols[shape]; - }); + each(['topbutton', 'bottombutton'], function (shape) { + VMLRenderer.prototype.symbols[shape] = + SVGRenderer.prototype.symbols[shape]; + }); } /** * The factory function for creating new map charts. Creates a new {@link * Chart|Chart} object with different default options than the basic Chart. - * + * * @function #mapChart * @memberOf Highcharts * @@ -395,65 +395,65 @@ if (Renderer === VMLRenderer) { */ H.Map = H.mapChart = function (a, b, c) { - var hasRenderToArg = typeof a === 'string' || a.nodeName, - options = arguments[hasRenderToArg ? 1 : 0], - hiddenAxis = { - endOnTick: false, - visible: false, - minPadding: 0, - maxPadding: 0, - startOnTick: false - }, - seriesOptions, - defaultCreditsOptions = H.getOptions().credits; - - /* For visual testing - hiddenAxis.gridLineWidth = 1; - hiddenAxis.gridZIndex = 10; - hiddenAxis.tickPositions = undefined; - // */ - - // Don't merge the data - seriesOptions = options.series; - options.series = null; - - options = merge( - { - chart: { - panning: 'xy', - type: 'map' - }, - credits: { - mapText: pick( - defaultCreditsOptions.mapText, - ' \u00a9 ' + - '{geojson.copyrightShort}' - ), - mapTextFull: pick( - defaultCreditsOptions.mapTextFull, - '{geojson.copyright}' - ) - }, - tooltip: { - followTouchMove: false - }, - xAxis: hiddenAxis, - yAxis: merge(hiddenAxis, { reversed: true }) - }, - options, // user's options - - { // forced options - chart: { - inverted: false, - alignTicks: false - } - } - ); - - options.series = seriesOptions; - - - return hasRenderToArg ? - new Chart(a, options, c) : - new Chart(options, b); + var hasRenderToArg = typeof a === 'string' || a.nodeName, + options = arguments[hasRenderToArg ? 1 : 0], + hiddenAxis = { + endOnTick: false, + visible: false, + minPadding: 0, + maxPadding: 0, + startOnTick: false + }, + seriesOptions, + defaultCreditsOptions = H.getOptions().credits; + + /* For visual testing + hiddenAxis.gridLineWidth = 1; + hiddenAxis.gridZIndex = 10; + hiddenAxis.tickPositions = undefined; + // */ + + // Don't merge the data + seriesOptions = options.series; + options.series = null; + + options = merge( + { + chart: { + panning: 'xy', + type: 'map' + }, + credits: { + mapText: pick( + defaultCreditsOptions.mapText, + ' \u00a9 ' + + '{geojson.copyrightShort}' + ), + mapTextFull: pick( + defaultCreditsOptions.mapTextFull, + '{geojson.copyright}' + ) + }, + tooltip: { + followTouchMove: false + }, + xAxis: hiddenAxis, + yAxis: merge(hiddenAxis, { reversed: true }) + }, + options, // user's options + + { // forced options + chart: { + inverted: false, + alignTicks: false + } + } + ); + + options.series = seriesOptions; + + + return hasRenderToArg ? + new Chart(a, options, c) : + new Chart(options, b); }; diff --git a/js/parts-map/MapAxis.js b/js/parts-map/MapAxis.js index abde1809ef2..c63aed0919f 100644 --- a/js/parts-map/MapAxis.js +++ b/js/parts-map/MapAxis.js @@ -8,57 +8,57 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Axis.js'; var addEvent = H.addEvent, - Axis = H.Axis, - each = H.each, - pick = H.pick; + Axis = H.Axis, + each = H.each, + pick = H.pick; /** * Override to use the extreme coordinates from the SVG shape, not the * data values */ addEvent(Axis, 'getSeriesExtremes', function () { - var xData = []; - - // Remove the xData array and cache it locally so that the proceed method - // doesn't use it - if (this.isXAxis) { - each(this.series, function (series, i) { - if (series.useMapGeometry) { - xData[i] = series.xData; - series.xData = []; - } - }); - this.seriesXData = xData; - } + var xData = []; + + // Remove the xData array and cache it locally so that the proceed method + // doesn't use it + if (this.isXAxis) { + each(this.series, function (series, i) { + if (series.useMapGeometry) { + xData[i] = series.xData; + series.xData = []; + } + }); + this.seriesXData = xData; + } }); addEvent(Axis, 'afterGetSeriesExtremes', function () { - var xData = this.seriesXData, - dataMin, - dataMax, - useMapGeometry; - - // Run extremes logic for map and mapline - if (this.isXAxis) { - dataMin = pick(this.dataMin, Number.MAX_VALUE); - dataMax = pick(this.dataMax, -Number.MAX_VALUE); - each(this.series, function (series, i) { - if (series.useMapGeometry) { - dataMin = Math.min(dataMin, pick(series.minX, dataMin)); - dataMax = Math.max(dataMax, pick(series.maxX, dataMax)); - series.xData = xData[i]; // Reset xData array - useMapGeometry = true; - } - }); - if (useMapGeometry) { - this.dataMin = dataMin; - this.dataMax = dataMax; - } - - delete this.seriesXData; - } + var xData = this.seriesXData, + dataMin, + dataMax, + useMapGeometry; + + // Run extremes logic for map and mapline + if (this.isXAxis) { + dataMin = pick(this.dataMin, Number.MAX_VALUE); + dataMax = pick(this.dataMax, -Number.MAX_VALUE); + each(this.series, function (series, i) { + if (series.useMapGeometry) { + dataMin = Math.min(dataMin, pick(series.minX, dataMin)); + dataMax = Math.max(dataMax, pick(series.maxX, dataMax)); + series.xData = xData[i]; // Reset xData array + useMapGeometry = true; + } + }); + if (useMapGeometry) { + this.dataMin = dataMin; + this.dataMax = dataMax; + } + + delete this.seriesXData; + } }); @@ -66,63 +66,63 @@ addEvent(Axis, 'afterGetSeriesExtremes', function () { * Override axis translation to make sure the aspect ratio is always kept */ addEvent(Axis, 'afterSetAxisTranslation', function () { - var chart = this.chart, - mapRatio, - plotRatio = chart.plotWidth / chart.plotHeight, - adjustedAxisLength, - xAxis = chart.xAxis[0], - padAxis, - fixTo, - fixDiff, - preserveAspectRatio; - - // Check for map-like series - if (this.coll === 'yAxis' && xAxis.transA !== undefined) { - each(this.series, function (series) { - if (series.preserveAspectRatio) { - preserveAspectRatio = true; - } - }); - } - - // On Y axis, handle both - if (preserveAspectRatio) { - - // Use the same translation for both axes - this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA); - - mapRatio = plotRatio / - ((xAxis.max - xAxis.min) / (this.max - this.min)); - - // What axis to pad to put the map in the middle - padAxis = mapRatio < 1 ? this : xAxis; - - // Pad it - adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA; - padAxis.pixelPadding = padAxis.len - adjustedAxisLength; - padAxis.minPixelPadding = padAxis.pixelPadding / 2; - - fixTo = padAxis.fixTo; - if (fixTo) { - fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true); - fixDiff *= padAxis.transA; - if ( - Math.abs(fixDiff) > padAxis.minPixelPadding || - ( - padAxis.min === padAxis.dataMin && - padAxis.max === padAxis.dataMax - ) - ) { // zooming out again, keep within restricted area - fixDiff = 0; - } - padAxis.minPixelPadding -= fixDiff; - } - } + var chart = this.chart, + mapRatio, + plotRatio = chart.plotWidth / chart.plotHeight, + adjustedAxisLength, + xAxis = chart.xAxis[0], + padAxis, + fixTo, + fixDiff, + preserveAspectRatio; + + // Check for map-like series + if (this.coll === 'yAxis' && xAxis.transA !== undefined) { + each(this.series, function (series) { + if (series.preserveAspectRatio) { + preserveAspectRatio = true; + } + }); + } + + // On Y axis, handle both + if (preserveAspectRatio) { + + // Use the same translation for both axes + this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA); + + mapRatio = plotRatio / + ((xAxis.max - xAxis.min) / (this.max - this.min)); + + // What axis to pad to put the map in the middle + padAxis = mapRatio < 1 ? this : xAxis; + + // Pad it + adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA; + padAxis.pixelPadding = padAxis.len - adjustedAxisLength; + padAxis.minPixelPadding = padAxis.pixelPadding / 2; + + fixTo = padAxis.fixTo; + if (fixTo) { + fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true); + fixDiff *= padAxis.transA; + if ( + Math.abs(fixDiff) > padAxis.minPixelPadding || + ( + padAxis.min === padAxis.dataMin && + padAxis.max === padAxis.dataMax + ) + ) { // zooming out again, keep within restricted area + fixDiff = 0; + } + padAxis.minPixelPadding -= fixDiff; + } + } }); /** * Override Axis.render in order to delete the fixTo prop */ addEvent(Axis, 'render', function () { - this.fixTo = null; + this.fixTo = null; }); diff --git a/js/parts-map/MapBubbleSeries.js b/js/parts-map/MapBubbleSeries.js index e6e7e65685a..f8f32a9a430 100644 --- a/js/parts-map/MapBubbleSeries.js +++ b/js/parts-map/MapBubbleSeries.js @@ -11,189 +11,189 @@ import '../parts/Options.js'; import '../parts/Point.js'; import '../parts-more/BubbleSeries.js'; var merge = H.merge, - Point = H.Point, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + Point = H.Point, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; // The mapbubble series type if (seriesTypes.bubble) { - /** - * A map bubble series is a bubble series laid out on top of a map series, - * where each bubble is tied to a specific map area. - * - * @sample maps/demo/map-bubble/ Map bubble chart - * - * @extends {plotOptions.bubble} - * @product highmaps - * @optionparent plotOptions.mapbubble - */ - seriesType('mapbubble', 'bubble', { - - - - /** - * The main color of the series. This color affects both the fill and - * the stroke of the bubble. For enhanced control, use `marker` options. - * - * @type {Color} - * @sample {highmaps} maps/plotoptions/mapbubble-color/ Pink bubbles - * @product highmaps - * @apioption plotOptions.mapbubble.color - */ - - /** - * Whether to display negative sized bubbles. The threshold is given - * by the [zThreshold](#plotOptions.mapbubble.zThreshold) option, and negative - * bubbles can be visualized by setting [negativeColor]( - * #plotOptions.bubble.negativeColor). - * - * @type {Boolean} - * @default true - * @product highmaps - * @apioption plotOptions.mapbubble.displayNegative - */ - - /** - * @sample {highmaps} maps/demo/map-bubble/ Bubble size - * @product highmaps - * @apioption plotOptions.mapbubble.maxSize - */ - - /** - * @sample {highmaps} maps/demo/map-bubble/ Bubble size - * @product highmaps - * @apioption plotOptions.mapbubble.minSize - */ - - /** - * When a point's Z value is below the [zThreshold]( - * #plotOptions.mapbubble.zThreshold) setting, this color is used. - * - * @type {Color} - * @sample {highmaps} maps/plotoptions/mapbubble-negativecolor/ - * Negative color below a threshold - * @default null - * @product highmaps - * @apioption plotOptions.mapbubble.negativeColor - */ - - /** - * Whether the bubble's value should be represented by the area or the - * width of the bubble. The default, `area`, corresponds best to the - * human perception of the size of each bubble. - * - * @validvalue ["area", "width"] - * @type {String} - * @default area - * @product highmaps - * @apioption plotOptions.mapbubble.sizeBy - */ - - /** - * When this is true, the absolute value of z determines the size of - * the bubble. This means that with the default `zThreshold` of 0, a - * bubble of value -1 will have the same size as a bubble of value 1, - * while a bubble of value 0 will have a smaller size according to - * `minSize`. - * - * @type {Boolean} - * @sample {highmaps} highcharts/plotoptions/bubble-sizebyabsolutevalue/ - * Size by absolute value, various thresholds - * @default false - * @since 1.1.9 - * @product highmaps - * @apioption plotOptions.mapbubble.sizeByAbsoluteValue - */ - - /** - * The minimum for the Z value range. Defaults to the highest Z value - * in the data. - * - * @type {Number} - * @see [zMax](#plotOptions.mapbubble.zMin) - * @sample {highmaps} highcharts/plotoptions/bubble-zmin-zmax/ - * Z has a possible range of 0-100 - * @default null - * @since 1.0.3 - * @product highmaps - * @apioption plotOptions.mapbubble.zMax - */ - - /** - * The minimum for the Z value range. Defaults to the lowest Z value - * in the data. - * - * @type {Number} - * @see [zMax](#plotOptions.mapbubble.zMax) - * @sample {highmaps} highcharts/plotoptions/bubble-zmin-zmax/ - * Z has a possible range of 0-100 - * @default null - * @since 1.0.3 - * @product highmaps - * @apioption plotOptions.mapbubble.zMin - */ - - /** - * When [displayNegative](#plotOptions.mapbubble.displayNegative) is `false`, - * bubbles with lower Z values are skipped. When `displayNegative` - * is `true` and a [negativeColor](#plotOptions.mapbubble.negativeColor) - * is given, points with lower Z is colored. - * - * @type {Number} - * @sample {highmaps} maps/plotoptions/mapbubble-negativecolor/ - * Negative color below a threshold - * @default 0 - * @product highmaps - * @apioption plotOptions.mapbubble.zThreshold - */ - - animationLimit: 500, - - tooltip: { - pointFormat: '{point.name}: {point.z}' - } - - // Prototype members - }, { - xyFromShape: true, - type: 'mapbubble', - pointArrayMap: ['z'], // If one single value is passed, it is interpreted as z - /** - * Return the map area identified by the dataJoinBy option - */ - getMapData: seriesTypes.map.prototype.getMapData, - getBox: seriesTypes.map.prototype.getBox, - setData: seriesTypes.map.prototype.setData - - // Point class - }, { - applyOptions: function (options, x) { - var point; - if (options && options.lat !== undefined && options.lon !== undefined) { - point = Point.prototype.applyOptions.call( - this, - merge(options, this.series.chart.fromLatLonToPoint(options)), - x - ); - } else { - point = seriesTypes.map.prototype.pointClass.prototype.applyOptions.call(this, options, x); - } - return point; - }, - isValid: function () { - return typeof this.z === 'number'; - }, - ttBelow: false - }); + /** + * A map bubble series is a bubble series laid out on top of a map series, + * where each bubble is tied to a specific map area. + * + * @sample maps/demo/map-bubble/ Map bubble chart + * + * @extends {plotOptions.bubble} + * @product highmaps + * @optionparent plotOptions.mapbubble + */ + seriesType('mapbubble', 'bubble', { + + + + /** + * The main color of the series. This color affects both the fill and + * the stroke of the bubble. For enhanced control, use `marker` options. + * + * @type {Color} + * @sample {highmaps} maps/plotoptions/mapbubble-color/ Pink bubbles + * @product highmaps + * @apioption plotOptions.mapbubble.color + */ + + /** + * Whether to display negative sized bubbles. The threshold is given + * by the [zThreshold](#plotOptions.mapbubble.zThreshold) option, and negative + * bubbles can be visualized by setting [negativeColor]( + * #plotOptions.bubble.negativeColor). + * + * @type {Boolean} + * @default true + * @product highmaps + * @apioption plotOptions.mapbubble.displayNegative + */ + + /** + * @sample {highmaps} maps/demo/map-bubble/ Bubble size + * @product highmaps + * @apioption plotOptions.mapbubble.maxSize + */ + + /** + * @sample {highmaps} maps/demo/map-bubble/ Bubble size + * @product highmaps + * @apioption plotOptions.mapbubble.minSize + */ + + /** + * When a point's Z value is below the [zThreshold]( + * #plotOptions.mapbubble.zThreshold) setting, this color is used. + * + * @type {Color} + * @sample {highmaps} maps/plotoptions/mapbubble-negativecolor/ + * Negative color below a threshold + * @default null + * @product highmaps + * @apioption plotOptions.mapbubble.negativeColor + */ + + /** + * Whether the bubble's value should be represented by the area or the + * width of the bubble. The default, `area`, corresponds best to the + * human perception of the size of each bubble. + * + * @validvalue ["area", "width"] + * @type {String} + * @default area + * @product highmaps + * @apioption plotOptions.mapbubble.sizeBy + */ + + /** + * When this is true, the absolute value of z determines the size of + * the bubble. This means that with the default `zThreshold` of 0, a + * bubble of value -1 will have the same size as a bubble of value 1, + * while a bubble of value 0 will have a smaller size according to + * `minSize`. + * + * @type {Boolean} + * @sample {highmaps} highcharts/plotoptions/bubble-sizebyabsolutevalue/ + * Size by absolute value, various thresholds + * @default false + * @since 1.1.9 + * @product highmaps + * @apioption plotOptions.mapbubble.sizeByAbsoluteValue + */ + + /** + * The minimum for the Z value range. Defaults to the highest Z value + * in the data. + * + * @type {Number} + * @see [zMax](#plotOptions.mapbubble.zMin) + * @sample {highmaps} highcharts/plotoptions/bubble-zmin-zmax/ + * Z has a possible range of 0-100 + * @default null + * @since 1.0.3 + * @product highmaps + * @apioption plotOptions.mapbubble.zMax + */ + + /** + * The minimum for the Z value range. Defaults to the lowest Z value + * in the data. + * + * @type {Number} + * @see [zMax](#plotOptions.mapbubble.zMax) + * @sample {highmaps} highcharts/plotoptions/bubble-zmin-zmax/ + * Z has a possible range of 0-100 + * @default null + * @since 1.0.3 + * @product highmaps + * @apioption plotOptions.mapbubble.zMin + */ + + /** + * When [displayNegative](#plotOptions.mapbubble.displayNegative) is `false`, + * bubbles with lower Z values are skipped. When `displayNegative` + * is `true` and a [negativeColor](#plotOptions.mapbubble.negativeColor) + * is given, points with lower Z is colored. + * + * @type {Number} + * @sample {highmaps} maps/plotoptions/mapbubble-negativecolor/ + * Negative color below a threshold + * @default 0 + * @product highmaps + * @apioption plotOptions.mapbubble.zThreshold + */ + + animationLimit: 500, + + tooltip: { + pointFormat: '{point.name}: {point.z}' + } + + // Prototype members + }, { + xyFromShape: true, + type: 'mapbubble', + pointArrayMap: ['z'], // If one single value is passed, it is interpreted as z + /** + * Return the map area identified by the dataJoinBy option + */ + getMapData: seriesTypes.map.prototype.getMapData, + getBox: seriesTypes.map.prototype.getBox, + setData: seriesTypes.map.prototype.setData + + // Point class + }, { + applyOptions: function (options, x) { + var point; + if (options && options.lat !== undefined && options.lon !== undefined) { + point = Point.prototype.applyOptions.call( + this, + merge(options, this.series.chart.fromLatLonToPoint(options)), + x + ); + } else { + point = seriesTypes.map.prototype.pointClass.prototype.applyOptions.call(this, options, x); + } + return point; + }, + isValid: function () { + return typeof this.z === 'number'; + }, + ttBelow: false + }); } /** * A `mapbubble` series. If the [type](#series.mapbubble.type) option * is not specified, it is inherited from [chart.type](#chart.type). - * - * + * + * * @type {Object} * @extends series,plotOptions.mapbubble * @excluding dataParser,dataURL @@ -204,19 +204,19 @@ if (seriesTypes.bubble) { /** * An array of data points for the series. For the `mapbubble` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `z` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 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.mapbubble.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * z: 9, @@ -228,7 +228,7 @@ if (seriesTypes.bubble) { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.mappoint.data * @excluding labelrank,middleX,middleY,path,value,x,y,lat,lon @@ -240,7 +240,7 @@ if (seriesTypes.bubble) { * While the `x` and `y` values of the bubble are determined by the * underlying map, the `z` indicates the actual value that gives the * size of the bubble. - * + * * @type {Number} * @sample {highmaps} maps/demo/map-bubble/ Bubble * @product highmaps diff --git a/js/parts-map/MapLineSeries.js b/js/parts-map/MapLineSeries.js index 0631c11674f..a04ac624159 100644 --- a/js/parts-map/MapLineSeries.js +++ b/js/parts-map/MapLineSeries.js @@ -8,7 +8,7 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Options.js'; var seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + seriesTypes = H.seriesTypes; /** * A mapline series is a special case of the map series where the value colors @@ -21,58 +21,58 @@ var seriesType = H.seriesType, * @optionparent plotOptions.mapline */ seriesType('mapline', 'map', { - /*= if (build.classic) { =*/ + /*= if (build.classic) { =*/ - /** - * The width of the map line. - * - * @type {Number} - * @default 1 - * @product highmaps - */ - lineWidth: 1, + /** + * The width of the map line. + * + * @type {Number} + * @default 1 + * @product highmaps + */ + lineWidth: 1, - /** - * Fill color for the map line shapes - * - * @type {Color} - * @default none - * @product highmaps - */ - fillColor: 'none' - /*= } =*/ + /** + * Fill color for the map line shapes + * + * @type {Color} + * @default none + * @product highmaps + */ + fillColor: 'none' + /*= } =*/ }, { - type: 'mapline', - colorProp: 'stroke', - /*= if (build.classic) { =*/ - pointAttrToOptions: { - 'stroke': 'color', - 'stroke-width': 'lineWidth' - }, - /** - * Get presentational attributes - */ - pointAttribs: function (point, state) { - var attr = seriesTypes.map.prototype.pointAttribs.call( - this, - point, - state - ); + type: 'mapline', + colorProp: 'stroke', + /*= if (build.classic) { =*/ + pointAttrToOptions: { + 'stroke': 'color', + 'stroke-width': 'lineWidth' + }, + /** + * Get presentational attributes + */ + pointAttribs: function (point, state) { + var attr = seriesTypes.map.prototype.pointAttribs.call( + this, + point, + state + ); - // The difference from a map series is that the stroke takes the point - // color - attr.fill = this.options.fillColor; + // The difference from a map series is that the stroke takes the point + // color + attr.fill = this.options.fillColor; - return attr; - }, - /*= } =*/ - drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol + return attr; + }, + /*= } =*/ + drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol }); /** * A `mapline` series. If the [type](#series.mapline.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.mapline * @excluding dataParser,dataURL,marker @@ -83,17 +83,17 @@ seriesType('mapline', 'map', { /** * An array of data points for the series. For the `mapline` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `value` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `[hc-key, value]`. Example: - * + * * ```js * data: [ * ['us-ny', 0], @@ -102,12 +102,12 @@ seriesType('mapline', 'map', { * ['us-ak', 5] * ] * ``` - * + * * 3. 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.map.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * value: 6, @@ -119,7 +119,7 @@ seriesType('mapline', 'map', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @product highmaps * @apioption series.mapline.data diff --git a/js/parts-map/MapNavigation.js b/js/parts-map/MapNavigation.js index c00d451f7eb..7ef07ed6fdb 100644 --- a/js/parts-map/MapNavigation.js +++ b/js/parts-map/MapNavigation.js @@ -8,23 +8,23 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Chart.js'; var addEvent = H.addEvent, - Chart = H.Chart, - doc = H.doc, - each = H.each, - extend = H.extend, - merge = H.merge, - pick = H.pick; + Chart = H.Chart, + doc = H.doc, + each = H.each, + extend = H.extend, + merge = H.merge, + pick = H.pick; function stopEvent(e) { - if (e) { - if (e.preventDefault) { - e.preventDefault(); - } - if (e.stopPropagation) { - e.stopPropagation(); - } - e.cancelBubble = true; - } + if (e) { + if (e.preventDefault) { + e.preventDefault(); + } + if (e.stopPropagation) { + e.stopPropagation(); + } + e.cancelBubble = true; + } } /** @@ -33,7 +33,7 @@ function stopEvent(e) { * @param {Chart} chart The Chart instance. */ function MapNavigation(chart) { - this.init(chart); + this.init(chart); } /** @@ -41,96 +41,96 @@ function MapNavigation(chart) { * @param {Chart} chart The Chart instance. */ MapNavigation.prototype.init = function (chart) { - this.chart = chart; - chart.mapNavButtons = []; + this.chart = chart; + chart.mapNavButtons = []; }; /** - * Update the map navigation with new options. Calling this is the same as - * calling `chart.update({ mapNavigation: {} })`. + * Update the map navigation with new options. Calling this is the same as + * calling `chart.update({ mapNavigation: {} })`. * @param {Object} options New options for the map navigation. */ MapNavigation.prototype.update = function (options) { - var chart = this.chart, - o = chart.options.mapNavigation, - buttonOptions, - attr, - states, - hoverStates, - selectStates, - outerHandler = function (e) { - this.handler.call(chart, e); - stopEvent(e); // Stop default click event (#4444) - }, - mapNavButtons = chart.mapNavButtons; + var chart = this.chart, + o = chart.options.mapNavigation, + buttonOptions, + attr, + states, + hoverStates, + selectStates, + outerHandler = function (e) { + this.handler.call(chart, e); + stopEvent(e); // Stop default click event (#4444) + }, + mapNavButtons = chart.mapNavButtons; - // Merge in new options in case of update, and register back to chart - // options. - if (options) { - o = chart.options.mapNavigation = - merge(chart.options.mapNavigation, options); - } + // Merge in new options in case of update, and register back to chart + // options. + if (options) { + o = chart.options.mapNavigation = + merge(chart.options.mapNavigation, options); + } - // Destroy buttons in case of dynamic update - while (mapNavButtons.length) { - mapNavButtons.pop().destroy(); - } - - if (pick(o.enableButtons, o.enabled) && !chart.renderer.forExport) { + // Destroy buttons in case of dynamic update + while (mapNavButtons.length) { + mapNavButtons.pop().destroy(); + } - H.objectEach(o.buttons, function (button, n) { - buttonOptions = merge(o.buttonOptions, button); - - /*= if (build.classic) { =*/ - // Presentational - attr = buttonOptions.theme; - attr.style = merge( - buttonOptions.theme.style, - buttonOptions.style // #3203 - ); - states = attr.states; - hoverStates = states && states.hover; - selectStates = states && states.select; - /*= } =*/ - - button = chart.renderer.button( - buttonOptions.text, - 0, - 0, - outerHandler, - attr, - hoverStates, - selectStates, - 0, - n === 'zoomIn' ? 'topbutton' : 'bottombutton' - ) - .addClass('highcharts-map-navigation') - .attr({ - width: buttonOptions.width, - height: buttonOptions.height, - title: chart.options.lang[n], - padding: buttonOptions.padding, - zIndex: 5 - }) - .add(); - button.handler = buttonOptions.onclick; - button.align( - extend(buttonOptions, { - width: button.width, - height: 2 * button.height - }), - null, - buttonOptions.alignTo - ); - // Stop double click event (#4444) - addEvent(button.element, 'dblclick', stopEvent); - - mapNavButtons.push(button); - - }); - } + if (pick(o.enableButtons, o.enabled) && !chart.renderer.forExport) { - this.updateEvents(o); + H.objectEach(o.buttons, function (button, n) { + buttonOptions = merge(o.buttonOptions, button); + + /*= if (build.classic) { =*/ + // Presentational + attr = buttonOptions.theme; + attr.style = merge( + buttonOptions.theme.style, + buttonOptions.style // #3203 + ); + states = attr.states; + hoverStates = states && states.hover; + selectStates = states && states.select; + /*= } =*/ + + button = chart.renderer.button( + buttonOptions.text, + 0, + 0, + outerHandler, + attr, + hoverStates, + selectStates, + 0, + n === 'zoomIn' ? 'topbutton' : 'bottombutton' + ) + .addClass('highcharts-map-navigation') + .attr({ + width: buttonOptions.width, + height: buttonOptions.height, + title: chart.options.lang[n], + padding: buttonOptions.padding, + zIndex: 5 + }) + .add(); + button.handler = buttonOptions.onclick; + button.align( + extend(buttonOptions, { + width: button.width, + height: 2 * button.height + }), + null, + buttonOptions.alignTo + ); + // Stop double click event (#4444) + addEvent(button.element, 'dblclick', stopEvent); + + mapNavButtons.push(button); + + }); + } + + this.updateEvents(o); }; /** @@ -139,182 +139,182 @@ MapNavigation.prototype.update = function (options) { * @param {Object} options Options for map navigation. */ MapNavigation.prototype.updateEvents = function (options) { - var chart = this.chart; + var chart = this.chart; - // Add the double click event - if ( - pick(options.enableDoubleClickZoom, options.enabled) || - options.enableDoubleClickZoomTo - ) { - this.unbindDblClick = this.unbindDblClick || addEvent( - chart.container, - 'dblclick', - function (e) { - chart.pointer.onContainerDblClick(e); - } - ); - } else if (this.unbindDblClick) { - // Unbind and set unbinder to undefined - this.unbindDblClick = this.unbindDblClick(); - } + // Add the double click event + if ( + pick(options.enableDoubleClickZoom, options.enabled) || + options.enableDoubleClickZoomTo + ) { + this.unbindDblClick = this.unbindDblClick || addEvent( + chart.container, + 'dblclick', + function (e) { + chart.pointer.onContainerDblClick(e); + } + ); + } else if (this.unbindDblClick) { + // Unbind and set unbinder to undefined + this.unbindDblClick = this.unbindDblClick(); + } - // Add the mousewheel event - if (pick(options.enableMouseWheelZoom, options.enabled)) { - this.unbindMouseWheel = this.unbindMouseWheel || addEvent( - chart.container, - doc.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', - function (e) { - chart.pointer.onContainerMouseWheel(e); - // Issue #5011, returning false from non-jQuery event does - // not prevent default - stopEvent(e); - return false; - } - ); - } else if (this.unbindMouseWheel) { - // Unbind and set unbinder to undefined - this.unbindMouseWheel = this.unbindMouseWheel(); - } + // Add the mousewheel event + if (pick(options.enableMouseWheelZoom, options.enabled)) { + this.unbindMouseWheel = this.unbindMouseWheel || addEvent( + chart.container, + doc.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', + function (e) { + chart.pointer.onContainerMouseWheel(e); + // Issue #5011, returning false from non-jQuery event does + // not prevent default + stopEvent(e); + return false; + } + ); + } else if (this.unbindMouseWheel) { + // Unbind and set unbinder to undefined + this.unbindMouseWheel = this.unbindMouseWheel(); + } }; // Add events to the Chart object itself extend(Chart.prototype, /** @lends Chart.prototype */ { - /** - * Fit an inner box to an outer. If the inner box overflows left or right, - * align it to the sides of the outer. If it overflows both sides, fit it - * within the outer. This is a pattern that occurs more places in - * Highcharts, perhaps it should be elevated to a common utility function. - * - * @private - */ - fitToBox: function (inner, outer) { - each([['x', 'width'], ['y', 'height']], function (dim) { - var pos = dim[0], - size = dim[1]; + /** + * Fit an inner box to an outer. If the inner box overflows left or right, + * align it to the sides of the outer. If it overflows both sides, fit it + * within the outer. This is a pattern that occurs more places in + * Highcharts, perhaps it should be elevated to a common utility function. + * + * @private + */ + fitToBox: function (inner, outer) { + each([['x', 'width'], ['y', 'height']], function (dim) { + var pos = dim[0], + size = dim[1]; - if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right - // the general size is greater, fit fully to outer - if (inner[size] > outer[size]) { - inner[size] = outer[size]; - inner[pos] = outer[pos]; - } else { // align right - inner[pos] = outer[pos] + outer[size] - inner[size]; - } - } - if (inner[size] > outer[size]) { - inner[size] = outer[size]; - } - if (inner[pos] < outer[pos]) { - inner[pos] = outer[pos]; - } - }); + if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right + // the general size is greater, fit fully to outer + if (inner[size] > outer[size]) { + inner[size] = outer[size]; + inner[pos] = outer[pos]; + } else { // align right + inner[pos] = outer[pos] + outer[size] - inner[size]; + } + } + if (inner[size] > outer[size]) { + inner[size] = outer[size]; + } + if (inner[pos] < outer[pos]) { + inner[pos] = outer[pos]; + } + }); - return inner; - }, + return inner; + }, - /** - * Highmaps only. Zoom in or out of the map. See also {@link Point#zoomTo}. - * See {@link Chart#fromLatLonToPoint} for how to get the `centerX` and - * `centerY` parameters for a geographic location. - * - * @param {Number} [howMuch] - * How much to zoom the map. Values less than 1 zooms in. 0.5 zooms - * in to half the current view. 2 zooms to twice the current view. - * If omitted, the zoom is reset. - * @param {Number} [centerX] - * The X axis position to center around if available space. - * @param {Number} [centerY] - * The Y axis position to center around if available space. - * @param {Number} [mouseX] - * Fix the zoom to this position if possible. This is used for - * example in mousewheel events, where the area under the mouse - * should be fixed as we zoom in. - * @param {Number} [mouseY] - * Fix the zoom to this position if possible. - */ - mapZoom: function (howMuch, centerXArg, centerYArg, mouseX, mouseY) { - var chart = this, - xAxis = chart.xAxis[0], - xRange = xAxis.max - xAxis.min, - centerX = pick(centerXArg, xAxis.min + xRange / 2), - newXRange = xRange * howMuch, - yAxis = chart.yAxis[0], - yRange = yAxis.max - yAxis.min, - centerY = pick(centerYArg, yAxis.min + yRange / 2), - newYRange = yRange * howMuch, - fixToX = mouseX ? ((mouseX - xAxis.pos) / xAxis.len) : 0.5, - fixToY = mouseY ? ((mouseY - yAxis.pos) / yAxis.len) : 0.5, - newXMin = centerX - newXRange * fixToX, - newYMin = centerY - newYRange * fixToY, - newExt = chart.fitToBox({ - x: newXMin, - y: newYMin, - width: newXRange, - height: newYRange - }, { - x: xAxis.dataMin, - y: yAxis.dataMin, - width: xAxis.dataMax - xAxis.dataMin, - height: yAxis.dataMax - yAxis.dataMin - }), - zoomOut = newExt.x <= xAxis.dataMin && - newExt.width >= xAxis.dataMax - xAxis.dataMin && - newExt.y <= yAxis.dataMin && - newExt.height >= yAxis.dataMax - yAxis.dataMin; + /** + * Highmaps only. Zoom in or out of the map. See also {@link Point#zoomTo}. + * See {@link Chart#fromLatLonToPoint} for how to get the `centerX` and + * `centerY` parameters for a geographic location. + * + * @param {Number} [howMuch] + * How much to zoom the map. Values less than 1 zooms in. 0.5 zooms + * in to half the current view. 2 zooms to twice the current view. + * If omitted, the zoom is reset. + * @param {Number} [centerX] + * The X axis position to center around if available space. + * @param {Number} [centerY] + * The Y axis position to center around if available space. + * @param {Number} [mouseX] + * Fix the zoom to this position if possible. This is used for + * example in mousewheel events, where the area under the mouse + * should be fixed as we zoom in. + * @param {Number} [mouseY] + * Fix the zoom to this position if possible. + */ + mapZoom: function (howMuch, centerXArg, centerYArg, mouseX, mouseY) { + var chart = this, + xAxis = chart.xAxis[0], + xRange = xAxis.max - xAxis.min, + centerX = pick(centerXArg, xAxis.min + xRange / 2), + newXRange = xRange * howMuch, + yAxis = chart.yAxis[0], + yRange = yAxis.max - yAxis.min, + centerY = pick(centerYArg, yAxis.min + yRange / 2), + newYRange = yRange * howMuch, + fixToX = mouseX ? ((mouseX - xAxis.pos) / xAxis.len) : 0.5, + fixToY = mouseY ? ((mouseY - yAxis.pos) / yAxis.len) : 0.5, + newXMin = centerX - newXRange * fixToX, + newYMin = centerY - newYRange * fixToY, + newExt = chart.fitToBox({ + x: newXMin, + y: newYMin, + width: newXRange, + height: newYRange + }, { + x: xAxis.dataMin, + y: yAxis.dataMin, + width: xAxis.dataMax - xAxis.dataMin, + height: yAxis.dataMax - yAxis.dataMin + }), + zoomOut = newExt.x <= xAxis.dataMin && + newExt.width >= xAxis.dataMax - xAxis.dataMin && + newExt.y <= yAxis.dataMin && + newExt.height >= yAxis.dataMax - yAxis.dataMin; - // When mousewheel zooming, fix the point under the mouse - if (mouseX) { - xAxis.fixTo = [mouseX - xAxis.pos, centerXArg]; - } - if (mouseY) { - yAxis.fixTo = [mouseY - yAxis.pos, centerYArg]; - } + // When mousewheel zooming, fix the point under the mouse + if (mouseX) { + xAxis.fixTo = [mouseX - xAxis.pos, centerXArg]; + } + if (mouseY) { + yAxis.fixTo = [mouseY - yAxis.pos, centerYArg]; + } - // Zoom - if (howMuch !== undefined && !zoomOut) { - xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false); - yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false); + // Zoom + if (howMuch !== undefined && !zoomOut) { + xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false); + yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false); - // Reset zoom - } else { - xAxis.setExtremes(undefined, undefined, false); - yAxis.setExtremes(undefined, undefined, false); - } + // Reset zoom + } else { + xAxis.setExtremes(undefined, undefined, false); + yAxis.setExtremes(undefined, undefined, false); + } - // Prevent zooming until this one is finished animating - /* - chart.holdMapZoom = true; - setTimeout(function () { - chart.holdMapZoom = false; - }, 200); - */ - /* - delay = animation ? animation.duration || 500 : 0; - if (delay) { - chart.isMapZooming = true; - setTimeout(function () { - chart.isMapZooming = false; - if (chart.mapZoomQueue) { - chart.mapZoom.apply(chart, chart.mapZoomQueue); - } - chart.mapZoomQueue = null; - }, delay); - } - */ + // Prevent zooming until this one is finished animating + /* + chart.holdMapZoom = true; + setTimeout(function () { + chart.holdMapZoom = false; + }, 200); + */ + /* + delay = animation ? animation.duration || 500 : 0; + if (delay) { + chart.isMapZooming = true; + setTimeout(function () { + chart.isMapZooming = false; + if (chart.mapZoomQueue) { + chart.mapZoom.apply(chart, chart.mapZoomQueue); + } + chart.mapZoomQueue = null; + }, delay); + } + */ - chart.redraw(); - } + chart.redraw(); + } }); /** * Extend the Chart.render method to add zooming and panning */ addEvent(Chart, 'beforeRender', function () { - // Render the plus and minus buttons. Doing this before the shapes makes - // getBBox much quicker, at least in Chrome. - this.mapNavigation = new MapNavigation(this); - this.mapNavigation.update(); + // Render the plus and minus buttons. Doing this before the shapes makes + // getBBox much quicker, at least in Chrome. + this.mapNavigation = new MapNavigation(this); + this.mapNavigation.update(); }); diff --git a/js/parts-map/MapPointSeries.js b/js/parts-map/MapPointSeries.js index e7b0e6d6601..1cca85d6132 100644 --- a/js/parts-map/MapPointSeries.js +++ b/js/parts-map/MapPointSeries.js @@ -10,8 +10,8 @@ import '../parts/Options.js'; import '../parts/Point.js'; import '../parts/ScatterSeries.js'; var merge = H.merge, - Point = H.Point, - seriesType = H.seriesType; + Point = H.Point, + seriesType = H.seriesType; /** * A mappoint series is a special form of scatter series where the points can @@ -24,46 +24,46 @@ var merge = H.merge, */ seriesType('mappoint', 'scatter', { - dataLabels: { - /** - * @default {point.name} - * @apioption plotOptions.mappoint.dataLabels.format - */ - enabled: true, - formatter: function () { // #2945 - return this.point.name; - }, - crop: false, - defer: false, - overflow: false, - style: { - color: '${palette.neutralColor100}' - } - } + dataLabels: { + /** + * @default {point.name} + * @apioption plotOptions.mappoint.dataLabels.format + */ + enabled: true, + formatter: function () { // #2945 + return this.point.name; + }, + crop: false, + defer: false, + overflow: false, + style: { + color: '${palette.neutralColor100}' + } + } // Prototype members }, { - type: 'mappoint', - forceDL: true + type: 'mappoint', + forceDL: true // Point class }, { - applyOptions: function (options, x) { - var mergedOptions = ( - options.lat !== undefined && - options.lon !== undefined ? - merge(options, this.series.chart.fromLatLonToPoint(options)) : - options - ); - return Point.prototype.applyOptions.call(this, mergedOptions, x); - } + applyOptions: function (options, x) { + var mergedOptions = ( + options.lat !== undefined && + options.lon !== undefined ? + merge(options, this.series.chart.fromLatLonToPoint(options)) : + options + ); + return Point.prototype.applyOptions.call(this, mergedOptions, x); + } }); /** * A `mappoint` series. If the [type](#series.mappoint.type) option * is not specified, it is inherited from [chart.type](#chart.type). - * - * + * + * * @type {Object} * @extends series,plotOptions.mappoint * @excluding dataParser,dataURL @@ -74,21 +74,21 @@ seriesType('mappoint', 'scatter', { /** * An array of data points for the series. For the `mappoint` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 1], @@ -96,12 +96,12 @@ seriesType('mappoint', 'scatter', { * [2, 7] * ] * ``` - * + * * 3. 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.mappoint.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -115,7 +115,7 @@ seriesType('mappoint', 'scatter', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.map.data * @excluding labelrank,middleX,middleY,path,value @@ -126,7 +126,7 @@ seriesType('mappoint', 'scatter', { /** * The latitude of the point. Must be combined with the `lon` option * to work. Overrides `x` and `y` values. - * + * * @type {Number} * @sample {highmaps} maps/demo/mappoint-latlon/ Point position by lat/lon * @since 1.1.0 @@ -137,7 +137,7 @@ seriesType('mappoint', 'scatter', { /** * The longitude of the point. Must be combined with the `lon` option * to work. Overrides `x` and `y` values. - * + * * @type {Number} * @sample {highmaps} maps/demo/mappoint-latlon/ Point position by lat/lon * @since 1.1.0 @@ -147,7 +147,7 @@ seriesType('mappoint', 'scatter', { /** * The x coordinate of the point in terms of the map path coordinates. - * + * * @type {Number} * @sample {highmaps} maps/demo/mapline-mappoint/ Map point demo * @product highmaps @@ -156,7 +156,7 @@ seriesType('mappoint', 'scatter', { /** * The x coordinate of the point in terms of the map path coordinates. - * + * * @type {Number} * @sample {highmaps} maps/demo/mapline-mappoint/ Map point demo * @product highmaps diff --git a/js/parts-map/MapPointer.js b/js/parts-map/MapPointer.js index e4527378996..83e1f62e2d5 100644 --- a/js/parts-map/MapPointer.js +++ b/js/parts-map/MapPointer.js @@ -9,91 +9,91 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Pointer.js'; var extend = H.extend, - pick = H.pick, - Pointer = H.Pointer, - wrap = H.wrap; - + pick = H.pick, + Pointer = H.Pointer, + wrap = H.wrap; + // Extend the Pointer extend(Pointer.prototype, { - /** - * The event handler for the doubleclick event - */ - onContainerDblClick: function (e) { - var chart = this.chart; + /** + * The event handler for the doubleclick event + */ + onContainerDblClick: function (e) { + var chart = this.chart; - e = this.normalize(e); + e = this.normalize(e); - if (chart.options.mapNavigation.enableDoubleClickZoomTo) { - if (chart.pointer.inClass(e.target, 'highcharts-tracker') && chart.hoverPoint) { - chart.hoverPoint.zoomTo(); - } - } else if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { - chart.mapZoom( - 0.5, - chart.xAxis[0].toValue(e.chartX), - chart.yAxis[0].toValue(e.chartY), - e.chartX, - e.chartY - ); - } - }, + if (chart.options.mapNavigation.enableDoubleClickZoomTo) { + if (chart.pointer.inClass(e.target, 'highcharts-tracker') && chart.hoverPoint) { + chart.hoverPoint.zoomTo(); + } + } else if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + chart.mapZoom( + 0.5, + chart.xAxis[0].toValue(e.chartX), + chart.yAxis[0].toValue(e.chartY), + e.chartX, + e.chartY + ); + } + }, - /** - * The event handler for the mouse scroll event - */ - onContainerMouseWheel: function (e) { - var chart = this.chart, - delta; + /** + * The event handler for the mouse scroll event + */ + onContainerMouseWheel: function (e) { + var chart = this.chart, + delta; - e = this.normalize(e); + e = this.normalize(e); - // Firefox uses e.detail, WebKit and IE uses wheelDelta - delta = e.detail || -(e.wheelDelta / 120); - if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { - chart.mapZoom( - Math.pow(chart.options.mapNavigation.mouseWheelSensitivity, delta), - chart.xAxis[0].toValue(e.chartX), - chart.yAxis[0].toValue(e.chartY), - e.chartX, - e.chartY - ); - } - } + // Firefox uses e.detail, WebKit and IE uses wheelDelta + delta = e.detail || -(e.wheelDelta / 120); + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + chart.mapZoom( + Math.pow(chart.options.mapNavigation.mouseWheelSensitivity, delta), + chart.xAxis[0].toValue(e.chartX), + chart.yAxis[0].toValue(e.chartY), + e.chartX, + e.chartY + ); + } + } }); // The pinchType is inferred from mapNavigation options. wrap(Pointer.prototype, 'zoomOption', function (proceed) { - var mapNavigation = this.chart.options.mapNavigation; - - // Pinch status - if (pick(mapNavigation.enableTouchZoom, mapNavigation.enabled)) { - this.chart.options.chart.pinchType = 'xy'; - } - - proceed.apply(this, [].slice.call(arguments, 1)); + var mapNavigation = this.chart.options.mapNavigation; + + // Pinch status + if (pick(mapNavigation.enableTouchZoom, mapNavigation.enabled)) { + this.chart.options.chart.pinchType = 'xy'; + } + + proceed.apply(this, [].slice.call(arguments, 1)); }); // Extend the pinchTranslate method to preserve fixed ratio when zooming wrap(Pointer.prototype, 'pinchTranslate', function (proceed, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { - var xBigger; - proceed.call(this, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + var xBigger; + proceed.call(this, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); - // Keep ratio - if (this.chart.options.chart.type === 'map' && this.hasZoom) { - xBigger = transform.scaleX > transform.scaleY; - this.pinchTranslateDirection( - !xBigger, - pinchDown, - touches, - transform, - selectionMarker, - clip, - lastValidTouch, - xBigger ? transform.scaleX : transform.scaleY - ); - } + // Keep ratio + if (this.chart.options.chart.type === 'map' && this.hasZoom) { + xBigger = transform.scaleX > transform.scaleY; + this.pinchTranslateDirection( + !xBigger, + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch, + xBigger ? transform.scaleX : transform.scaleY + ); + } }); diff --git a/js/parts-map/MapSeries.js b/js/parts-map/MapSeries.js index 9d509709cea..aa02e5bfb5d 100644 --- a/js/parts-map/MapSeries.js +++ b/js/parts-map/MapSeries.js @@ -14,22 +14,22 @@ import '../parts/Point.js'; import '../parts/Series.js'; import '../parts/ScatterSeries.js'; var colorPointMixin = H.colorPointMixin, - colorSeriesMixin = H.colorSeriesMixin, - doc = H.doc, - each = H.each, - extend = H.extend, - isNumber = H.isNumber, - LegendSymbolMixin = H.LegendSymbolMixin, - map = H.map, - merge = H.merge, - noop = H.noop, - pick = H.pick, - isArray = H.isArray, - Point = H.Point, - Series = H.Series, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes, - splat = H.splat; + colorSeriesMixin = H.colorSeriesMixin, + doc = H.doc, + each = H.each, + extend = H.extend, + isNumber = H.isNumber, + LegendSymbolMixin = H.LegendSymbolMixin, + map = H.map, + merge = H.merge, + noop = H.noop, + pick = H.pick, + isArray = H.isArray, + Point = H.Point, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes, + splat = H.splat; // The vector-effect attribute is not supported in IE <= 11 (at least), so we need // diffent logic (#3218) @@ -48,971 +48,971 @@ var supportsVectorEffect = doc.documentElement.style.vectorEffect !== undefined; */ seriesType('map', 'scatter', { - /** - * Define the z index of the series. - * - * @type {Number} - * @product highmaps - * @apioption plotOptions.series.zIndex - */ - - /** - * Whether all areas of the map defined in `mapData` should be rendered. - * If `true`, areas which don't correspond to a data point, are rendered - * as `null` points. If `false`, those areas are skipped. - * - * @type {Boolean} - * @sample {highmaps} maps/plotoptions/series-allareas-false/ All areas set to false - * @default true - * @product highmaps - * @apioption plotOptions.series.allAreas - */ - allAreas: true, - - animation: false, // makes the complex shapes slow - - /*= if (build.classic) { =*/ - /** - * The color to apply to null points. - * - * In styled mode, the null point fill is set in the - * `.highcharts-null-point` class. - * - * @type {Color} - * @sample {highmaps} maps/demo/all-areas-as-null/ Null color - * @default #f7f7f7 - * @product highmaps - */ - nullColor: '${palette.neutralColor3}', - - /** - * The border color of the map areas. - * - * In styled mode, the border stroke is given in the `.highcharts-point` class. - * - * @type {Color} - * @sample {highmaps} maps/plotoptions/series-border/ Borders demo - * @default #cccccc - * @product highmaps - * @apioption plotOptions.series.borderColor - */ - borderColor: '${palette.neutralColor20}', - - /** - * The border width of each map area. - * - * In styled mode, the border stroke width is given in the - * `.highcharts-point` class. - * - * @sample {highmaps} maps/plotoptions/series-border/ Borders demo - * @product highmaps - * @apioption plotOptions.series.borderWidth - */ - borderWidth: 1, - /*= } =*/ - - /** - * Whether to allow pointer interaction like tooltips and mouse events - * on null points. - * - * @type {Boolean} - * @default false - * @since 4.2.7 - * @product highmaps - * @apioption plotOptions.map.nullInteraction - */ - - /** - * Set this option to `false` to prevent a series from connecting to - * the global color axis. This will cause the series to have its own - * legend item. - * - * @type {Boolean} - * @default undefined - * @product highmaps - * @apioption plotOptions.series.colorAxis - */ - - /** - * @ignore - */ - marker: null, - - stickyTracking: false, - - /** - * What property to join the `mapData` to the value data. For example, - * if joinBy is "code", the mapData items with a specific code is merged - * into the data with the same code. For maps loaded from GeoJSON, the - * keys may be held in each point's `properties` object. - * - * The joinBy option can also be an array of two values, where the first - * points to a key in the `mapData`, and the second points to another - * key in the `data`. - * - * When joinBy is `null`, the map items are joined by their position - * in the array, which performs much better in maps with many data points. - * This is the recommended option if you are printing more than a thousand - * data points and have a backend that can preprocess the data into - * a parallel array of the mapData. - * - * @type {String|Array} - * @sample {highmaps} maps/plotoptions/series-border/ Joined by "code" - * @sample {highmaps} maps/demo/geojson/ GeoJSON joined by an array - * @sample {highmaps} maps/series/joinby-null/ Simple data joined by null - * @product highmaps - * @apioption plotOptions.series.joinBy - */ - joinBy: 'hc-key', - - dataLabels: { - formatter: function () { // #2945 - return this.point.value; - }, - inside: true, // for the color - verticalAlign: 'middle', - crop: false, - overflow: false, - padding: 0 - }, - - /** - * @ignore - */ - turboThreshold: 0, - - tooltip: { - followPointer: true, - pointFormat: '{point.name}: {point.value}
' - }, - - states: { - - /** - * Overrides for the normal state. - * - * @type {Object} - * @product highmaps - * @apioption plotOptions.series.states.normal - */ - normal: { - - /** - * Animation options for the fill color when returning from hover state - * to normal state. The animation adds some latency in order to reduce - * the effect of flickering when hovering in and out of for example - * an uneven coastline. - * - * @type {Object|Boolean} - * @sample {highmaps} maps/plotoptions/series-states-animation-false/ - * No animation of fill color - * @default true - * @product highmaps - * @apioption plotOptions.series.states.normal.animation - */ - animation: true - }, - - hover: { - - halo: null, - - /** - * The color of the shape in this state - * - * @type {Color} - * @sample {highmaps} maps/plotoptions/series-states-hover/ Hover options - * @product highmaps - * @apioption plotOptions.series.states.hover.color - */ - - /** - * The border color of the point in this state. - * - * @type {Color} - * @product highmaps - * @apioption plotOptions.series.states.hover.borderColor - */ - - /** - * The border width of the point in this state - * - * @type {Number} - * @product highmaps - * @apioption plotOptions.series.states.hover.borderWidth - */ - - /** - * The relative brightness of the point when hovered, relative to the - * normal point color. - * - * @type {Number} - * @default 0.2 - * @product highmaps - * @apioption plotOptions.series.states.hover.brightness - */ - brightness: 0.2 - - }, - - /*= if (build.classic) { =*/ - select: { - color: '${palette.neutralColor20}' - } - /*= } =*/ - } + /** + * Define the z index of the series. + * + * @type {Number} + * @product highmaps + * @apioption plotOptions.series.zIndex + */ + + /** + * Whether all areas of the map defined in `mapData` should be rendered. + * If `true`, areas which don't correspond to a data point, are rendered + * as `null` points. If `false`, those areas are skipped. + * + * @type {Boolean} + * @sample {highmaps} maps/plotoptions/series-allareas-false/ All areas set to false + * @default true + * @product highmaps + * @apioption plotOptions.series.allAreas + */ + allAreas: true, + + animation: false, // makes the complex shapes slow + + /*= if (build.classic) { =*/ + /** + * The color to apply to null points. + * + * In styled mode, the null point fill is set in the + * `.highcharts-null-point` class. + * + * @type {Color} + * @sample {highmaps} maps/demo/all-areas-as-null/ Null color + * @default #f7f7f7 + * @product highmaps + */ + nullColor: '${palette.neutralColor3}', + + /** + * The border color of the map areas. + * + * In styled mode, the border stroke is given in the `.highcharts-point` class. + * + * @type {Color} + * @sample {highmaps} maps/plotoptions/series-border/ Borders demo + * @default #cccccc + * @product highmaps + * @apioption plotOptions.series.borderColor + */ + borderColor: '${palette.neutralColor20}', + + /** + * The border width of each map area. + * + * In styled mode, the border stroke width is given in the + * `.highcharts-point` class. + * + * @sample {highmaps} maps/plotoptions/series-border/ Borders demo + * @product highmaps + * @apioption plotOptions.series.borderWidth + */ + borderWidth: 1, + /*= } =*/ + + /** + * Whether to allow pointer interaction like tooltips and mouse events + * on null points. + * + * @type {Boolean} + * @default false + * @since 4.2.7 + * @product highmaps + * @apioption plotOptions.map.nullInteraction + */ + + /** + * Set this option to `false` to prevent a series from connecting to + * the global color axis. This will cause the series to have its own + * legend item. + * + * @type {Boolean} + * @default undefined + * @product highmaps + * @apioption plotOptions.series.colorAxis + */ + + /** + * @ignore + */ + marker: null, + + stickyTracking: false, + + /** + * What property to join the `mapData` to the value data. For example, + * if joinBy is "code", the mapData items with a specific code is merged + * into the data with the same code. For maps loaded from GeoJSON, the + * keys may be held in each point's `properties` object. + * + * The joinBy option can also be an array of two values, where the first + * points to a key in the `mapData`, and the second points to another + * key in the `data`. + * + * When joinBy is `null`, the map items are joined by their position + * in the array, which performs much better in maps with many data points. + * This is the recommended option if you are printing more than a thousand + * data points and have a backend that can preprocess the data into + * a parallel array of the mapData. + * + * @type {String|Array} + * @sample {highmaps} maps/plotoptions/series-border/ Joined by "code" + * @sample {highmaps} maps/demo/geojson/ GeoJSON joined by an array + * @sample {highmaps} maps/series/joinby-null/ Simple data joined by null + * @product highmaps + * @apioption plotOptions.series.joinBy + */ + joinBy: 'hc-key', + + dataLabels: { + formatter: function () { // #2945 + return this.point.value; + }, + inside: true, // for the color + verticalAlign: 'middle', + crop: false, + overflow: false, + padding: 0 + }, + + /** + * @ignore + */ + turboThreshold: 0, + + tooltip: { + followPointer: true, + pointFormat: '{point.name}: {point.value}
' + }, + + states: { + + /** + * Overrides for the normal state. + * + * @type {Object} + * @product highmaps + * @apioption plotOptions.series.states.normal + */ + normal: { + + /** + * Animation options for the fill color when returning from hover state + * to normal state. The animation adds some latency in order to reduce + * the effect of flickering when hovering in and out of for example + * an uneven coastline. + * + * @type {Object|Boolean} + * @sample {highmaps} maps/plotoptions/series-states-animation-false/ + * No animation of fill color + * @default true + * @product highmaps + * @apioption plotOptions.series.states.normal.animation + */ + animation: true + }, + + hover: { + + halo: null, + + /** + * The color of the shape in this state + * + * @type {Color} + * @sample {highmaps} maps/plotoptions/series-states-hover/ Hover options + * @product highmaps + * @apioption plotOptions.series.states.hover.color + */ + + /** + * The border color of the point in this state. + * + * @type {Color} + * @product highmaps + * @apioption plotOptions.series.states.hover.borderColor + */ + + /** + * The border width of the point in this state + * + * @type {Number} + * @product highmaps + * @apioption plotOptions.series.states.hover.borderWidth + */ + + /** + * The relative brightness of the point when hovered, relative to the + * normal point color. + * + * @type {Number} + * @default 0.2 + * @product highmaps + * @apioption plotOptions.series.states.hover.brightness + */ + brightness: 0.2 + + }, + + /*= if (build.classic) { =*/ + select: { + color: '${palette.neutralColor20}' + } + /*= } =*/ + } // Prototype members }, merge(colorSeriesMixin, { - type: 'map', - getExtremesFromAll: true, - useMapGeometry: true, // get axis extremes from paths, not values - forceDL: true, - searchPoint: noop, - directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply. - preserveAspectRatio: true, // X axis and Y axis must have same translation slope - pointArrayMap: ['value'], - /** - * Get the bounding box of all paths in the map combined. - */ - getBox: function (paths) { - var MAX_VALUE = Number.MAX_VALUE, - maxX = -MAX_VALUE, - minX = MAX_VALUE, - maxY = -MAX_VALUE, - minY = MAX_VALUE, - minRange = MAX_VALUE, - xAxis = this.xAxis, - yAxis = this.yAxis, - hasBox; - - // Find the bounding box - each(paths || [], function (point) { - - if (point.path) { - if (typeof point.path === 'string') { - point.path = H.splitPath(point.path); - } - - var path = point.path || [], - i = path.length, - even = false, // while loop reads from the end - pointMaxX = -MAX_VALUE, - pointMinX = MAX_VALUE, - pointMaxY = -MAX_VALUE, - pointMinY = MAX_VALUE, - properties = point.properties; - - // The first time a map point is used, analyze its box - if (!point._foundBox) { - while (i--) { - if (isNumber(path[i])) { - if (even) { // even = x - pointMaxX = Math.max(pointMaxX, path[i]); - pointMinX = Math.min(pointMinX, path[i]); - } else { // odd = Y - pointMaxY = Math.max(pointMaxY, path[i]); - pointMinY = Math.min(pointMinY, path[i]); - } - even = !even; - } - } - // Cache point bounding box for use to position data labels, - // bubbles etc - point._midX = pointMinX + (pointMaxX - pointMinX) * pick( - point.middleX, - properties && properties['hc-middle-x'], - 0.5 - ); - point._midY = pointMinY + (pointMaxY - pointMinY) * pick( - point.middleY, - properties && properties['hc-middle-y'], - 0.5 - ); - point._maxX = pointMaxX; - point._minX = pointMinX; - point._maxY = pointMaxY; - point._minY = pointMinY; - point.labelrank = pick(point.labelrank, (pointMaxX - pointMinX) * (pointMaxY - pointMinY)); - point._foundBox = true; - } - - maxX = Math.max(maxX, point._maxX); - minX = Math.min(minX, point._minX); - maxY = Math.max(maxY, point._maxY); - minY = Math.min(minY, point._minY); - minRange = Math.min(point._maxX - point._minX, point._maxY - point._minY, minRange); - hasBox = true; - } - }); - - // Set the box for the whole series - if (hasBox) { - this.minY = Math.min(minY, pick(this.minY, MAX_VALUE)); - this.maxY = Math.max(maxY, pick(this.maxY, -MAX_VALUE)); - this.minX = Math.min(minX, pick(this.minX, MAX_VALUE)); - this.maxX = Math.max(maxX, pick(this.maxX, -MAX_VALUE)); - - // If no minRange option is set, set the default minimum zooming range to 5 times the - // size of the smallest element - if (xAxis && xAxis.options.minRange === undefined) { - xAxis.minRange = Math.min(5 * minRange, (this.maxX - this.minX) / 5, xAxis.minRange || MAX_VALUE); - } - if (yAxis && yAxis.options.minRange === undefined) { - yAxis.minRange = Math.min(5 * minRange, (this.maxY - this.minY) / 5, yAxis.minRange || MAX_VALUE); - } - } - }, - - getExtremes: function () { - // Get the actual value extremes for colors - Series.prototype.getExtremes.call(this, this.valueData); - - // Recalculate box on updated data - if (this.chart.hasRendered && this.isDirtyData) { - this.getBox(this.options.data); - } - - this.valueMin = this.dataMin; - this.valueMax = this.dataMax; - - // Extremes for the mock Y axis - this.dataMin = this.minY; - this.dataMax = this.maxY; - }, - - /** - * Translate the path so that it automatically fits into the plot area box - * @param {Object} path - */ - translatePath: function (path) { - - var series = this, - even = false, // while loop reads from the end - xAxis = series.xAxis, - yAxis = series.yAxis, - xMin = xAxis.min, - xTransA = xAxis.transA, - xMinPixelPadding = xAxis.minPixelPadding, - yMin = yAxis.min, - yTransA = yAxis.transA, - yMinPixelPadding = yAxis.minPixelPadding, - i, - ret = []; // Preserve the original - - // Do the translation - if (path) { - i = path.length; - while (i--) { - if (isNumber(path[i])) { - ret[i] = even ? - (path[i] - xMin) * xTransA + xMinPixelPadding : - (path[i] - yMin) * yTransA + yMinPixelPadding; - even = !even; - } else { - ret[i] = path[i]; - } - } - } - - return ret; - }, - - /** - * Extend setData to join in mapData. If the allAreas option is true, all areas - * from the mapData are used, and those that don't correspond to a data value - * are given null values. - */ - setData: function (data, redraw, animation, updatePoints) { - var options = this.options, - chartOptions = this.chart.options.chart, - globalMapData = chartOptions && chartOptions.map, - mapData = options.mapData, - joinBy = options.joinBy, - joinByNull = joinBy === null, - pointArrayMap = options.keys || this.pointArrayMap, - dataUsed = [], - mapMap = {}, - mapPoint, - mapTransforms = this.chart.mapTransforms, - props, - i; - - // Collect mapData from chart options if not defined on series - if (!mapData && globalMapData) { - mapData = typeof globalMapData === 'string' ? H.maps[globalMapData] : globalMapData; - } - - if (joinByNull) { - joinBy = '_i'; - } - joinBy = this.joinBy = splat(joinBy); - if (!joinBy[1]) { - joinBy[1] = joinBy[0]; - } - - // Pick up numeric values, add index - // Convert Array point definitions to objects using pointArrayMap - if (data) { - each(data, function (val, i) { - var ix = 0; - if (isNumber(val)) { - data[i] = { - value: val - }; - } else if (isArray(val)) { - data[i] = {}; - // Automatically copy first item to hc-key if there is an extra leading string - if (!options.keys && val.length > pointArrayMap.length && typeof val[0] === 'string') { - data[i]['hc-key'] = val[0]; - ++ix; - } - // Run through pointArrayMap and what's left of the point data array in parallel, copying over the values - for (var j = 0; j < pointArrayMap.length; ++j, ++ix) { - if (pointArrayMap[j] && val[ix] !== undefined) { - if (pointArrayMap[j].indexOf('.') > 0) { - H.Point.prototype.setNestedProperty( - data[i], val[ix], pointArrayMap[j] - ); - } else { - data[i][pointArrayMap[j]] = val[ix]; - } - } - } - } - if (joinByNull) { - data[i]._i = i; - } - }); - } - - this.getBox(data); - - // Pick up transform definitions for chart - this.chart.mapTransforms = mapTransforms = chartOptions && chartOptions.mapTransforms || mapData && mapData['hc-transform'] || mapTransforms; - - // Cache cos/sin of transform rotation angle - if (mapTransforms) { - H.objectEach(mapTransforms, function (transform) { - if (transform.rotation) { - transform.cosAngle = Math.cos(transform.rotation); - transform.sinAngle = Math.sin(transform.rotation); - } - }); - } - - if (mapData) { - if (mapData.type === 'FeatureCollection') { - this.mapTitle = mapData.title; - mapData = H.geojson(mapData, this.type, this); - } - - this.mapData = mapData; - this.mapMap = {}; - - for (i = 0; i < mapData.length; i++) { - mapPoint = mapData[i]; - props = mapPoint.properties; - - mapPoint._i = i; - // Copy the property over to root for faster access - if (joinBy[0] && props && props[joinBy[0]]) { - mapPoint[joinBy[0]] = props[joinBy[0]]; - } - mapMap[mapPoint[joinBy[0]]] = mapPoint; - } - this.mapMap = mapMap; - - // Registered the point codes that actually hold data - if (data && joinBy[1]) { - each(data, function (point) { - if (mapMap[point[joinBy[1]]]) { - dataUsed.push(mapMap[point[joinBy[1]]]); - } - }); - } - - if (options.allAreas) { - this.getBox(mapData); - data = data || []; - - // Registered the point codes that actually hold data - if (joinBy[1]) { - each(data, function (point) { - dataUsed.push(point[joinBy[1]]); - }); - } - - // Add those map points that don't correspond to data, which will be drawn as null points - dataUsed = '|' + map(dataUsed, function (point) { - return point && point[joinBy[0]]; - }).join('|') + '|'; // String search is faster than array.indexOf - - each(mapData, function (mapPoint) { - if (!joinBy[0] || dataUsed.indexOf('|' + mapPoint[joinBy[0]] + '|') === -1) { - data.push(merge(mapPoint, { value: null })); - updatePoints = false; // #5050 - adding all areas causes the update optimization of setData to kick in, even though the point order has changed - } - }); - } else { - this.getBox(dataUsed); // Issue #4784 - } - } - Series.prototype.setData.call(this, data, redraw, animation, updatePoints); - }, - - - /** - * No graph for the map series - */ - drawGraph: noop, - - /** - * We need the points' bounding boxes in order to draw the data labels, so - * we skip it now and call it from drawPoints instead. - */ - drawDataLabels: noop, - - /** - * Allow a quick redraw by just translating the area group. Used for zooming and panning - * in capable browsers. - */ - doFullTranslate: function () { - return this.isDirtyData || this.chart.isResizing || this.chart.renderer.isVML || !this.baseTrans; - }, - - /** - * Add the path option for data points. Find the max value for color calculation. - */ - translate: function () { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis, - doFullTranslate = series.doFullTranslate(); - - series.generatePoints(); - - each(series.data, function (point) { - - // Record the middle point (loosely based on centroid), determined - // by the middleX and middleY options. - point.plotX = xAxis.toPixels(point._midX, true); - point.plotY = yAxis.toPixels(point._midY, true); - - if (doFullTranslate) { - - point.shapeType = 'path'; - point.shapeArgs = { - d: series.translatePath(point.path) - }; - } - }); - - series.translateColors(); - }, - - /** - * Get presentational attributes. In the maps series this runs in both - * styled and non-styled mode, because colors hold data when a colorAxis - * is used. - */ - pointAttribs: function (point, state) { - var attr; - /*= if (build.classic) { =*/ - attr = seriesTypes.column.prototype.pointAttribs.call( - this, point, state - ); - /*= } else { =*/ - attr = this.colorAttribs(point); - /*= } =*/ - - // If vector-effect is not supported, we set the stroke-width on the group element - // and let all point graphics inherit. That way we don't have to iterate over all - // points to update the stroke-width on zooming. TODO: Check unstyled - if (supportsVectorEffect) { - attr['vector-effect'] = 'non-scaling-stroke'; - } else { - attr['stroke-width'] = 'inherit'; - } - - return attr; - }, - - /** - * Use the drawPoints method of column, that is able to handle simple shapeArgs. - * Extend it by assigning the tooltip position. - */ - drawPoints: function () { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis, - group = series.group, - chart = series.chart, - renderer = chart.renderer, - scaleX, - scaleY, - translateX, - translateY, - baseTrans = this.baseTrans, - transformGroup, - startTranslateX, - startTranslateY, - startScaleX, - startScaleY; - - // Set a group that handles transform during zooming and panning in order to preserve clipping - // on series.group - if (!series.transformGroup) { - series.transformGroup = renderer.g() - .attr({ - scaleX: 1, - scaleY: 1 - }) - .add(group); - series.transformGroup.survive = true; - } - - // Draw the shapes again - if (series.doFullTranslate()) { - - // Individual point actions. TODO: Check unstyled. - /*= if (build.classic) { =*/ - if (chart.hasRendered) { - each(series.points, function (point) { - - // Restore state color on update/redraw (#3529) - if (point.shapeArgs) { - point.shapeArgs.fill = series.pointAttribs(point, point.state).fill; - } - }); - } - /*= } =*/ - - // Draw them in transformGroup - series.group = series.transformGroup; - seriesTypes.column.prototype.drawPoints.apply(series); - series.group = group; // Reset - - // Add class names - each(series.points, function (point) { - if (point.graphic) { - if (point.name) { - point.graphic.addClass('highcharts-name-' + point.name.replace(/ /g, '-').toLowerCase()); - } - if (point.properties && point.properties['hc-key']) { - point.graphic.addClass('highcharts-key-' + point.properties['hc-key'].toLowerCase()); - } - - /*= if (!build.classic) { =*/ - point.graphic.css( - series.pointAttribs(point, point.selected && 'select') - ); - /*= } =*/ - } - }); - - // Set the base for later scale-zooming. The originX and originY properties are the - // axis values in the plot area's upper left corner. - this.baseTrans = { - originX: xAxis.min - xAxis.minPixelPadding / xAxis.transA, - originY: yAxis.min - yAxis.minPixelPadding / yAxis.transA + (yAxis.reversed ? 0 : yAxis.len / yAxis.transA), - transAX: xAxis.transA, - transAY: yAxis.transA - }; - - // Reset transformation in case we're doing a full translate (#3789) - this.transformGroup.animate({ - translateX: 0, - translateY: 0, - scaleX: 1, - scaleY: 1 - }); - - // Just update the scale and transform for better performance - } else { - scaleX = xAxis.transA / baseTrans.transAX; - scaleY = yAxis.transA / baseTrans.transAY; - translateX = xAxis.toPixels(baseTrans.originX, true); - translateY = yAxis.toPixels(baseTrans.originY, true); - - // Handle rounding errors in normal view (#3789) - if (scaleX > 0.99 && scaleX < 1.01 && scaleY > 0.99 && scaleY < 1.01) { - scaleX = 1; - scaleY = 1; - translateX = Math.round(translateX); - translateY = Math.round(translateY); - } - - // Animate or move to the new zoom level. In order to prevent - // flickering as the different transform components are set out of - // sync (#5991), we run a fake animator attribute and set scale and - // translation synchronously in the same step. - // A possible improvement to the API would be to handle this in the - // renderer or animation engine itself, to ensure that when we are - // animating multiple properties, we make sure that each step for - // each property is performed in the same step. Also, for symbols - // and for transform properties, it should induce a single - // updateTransform and symbolAttr call. - transformGroup = this.transformGroup; - if (chart.renderer.globalAnimation) { - startTranslateX = transformGroup.attr('translateX'); - startTranslateY = transformGroup.attr('translateY'); - startScaleX = transformGroup.attr('scaleX'); - startScaleY = transformGroup.attr('scaleY'); - transformGroup - .attr({ animator: 0 }) - .animate({ - animator: 1 - }, { - step: function (now, fx) { - transformGroup.attr({ - translateX: startTranslateX + - (translateX - startTranslateX) * fx.pos, - translateY: startTranslateY + - (translateY - startTranslateY) * fx.pos, - scaleX: startScaleX + - (scaleX - startScaleX) * fx.pos, - scaleY: startScaleY + - (scaleY - startScaleY) * fx.pos - }); - - } - }); - - // When dragging, animation is off. - } else { - transformGroup.attr({ - translateX: translateX, - translateY: translateY, - scaleX: scaleX, - scaleY: scaleY - }); - } - - } - - // Set the stroke-width directly on the group element so the children inherit it. We need to use - // setAttribute directly, because the stroke-widthSetter method expects a stroke color also to be - // set. - if (!supportsVectorEffect) { - series.group.element.setAttribute( - 'stroke-width', - series.options[ - (series.pointAttrToOptions && series.pointAttrToOptions['stroke-width']) || 'borderWidth' - ] / (scaleX || 1) - ); - } - - this.drawMapDataLabels(); - - - }, - - /** - * Draw the data labels. Special for maps is the time that the data labels are drawn (after points), - * and the clipping of the dataLabelsGroup. - */ - drawMapDataLabels: function () { - - Series.prototype.drawDataLabels.call(this); - if (this.dataLabelsGroup) { - this.dataLabelsGroup.clip(this.chart.clipRect); - } - }, - - /** - * Override render to throw in an async call in IE8. Otherwise it chokes on the US counties demo. - */ - render: function () { - var series = this, - render = Series.prototype.render; - - // Give IE8 some time to breathe. - if (series.chart.renderer.isVML && series.data.length > 3000) { - setTimeout(function () { - render.call(series); - }); - } else { - render.call(series); - } - }, - - /** - * The initial animation for the map series. By default, animation is disabled. - * Animation of map shapes is not at all supported in VML browsers. - */ - animate: function (init) { - var chart = this.chart, - animation = this.options.animation, - group = this.group, - xAxis = this.xAxis, - yAxis = this.yAxis, - left = xAxis.pos, - top = yAxis.pos; - - if (chart.renderer.isSVG) { - - if (animation === true) { - animation = { - duration: 1000 - }; - } - - // Initialize the animation - if (init) { - - // Scale down the group and place it in the center - group.attr({ - translateX: left + xAxis.len / 2, - translateY: top + yAxis.len / 2, - scaleX: 0.001, // #1499 - scaleY: 0.001 - }); - - // Run the animation - } else { - group.animate({ - translateX: left, - translateY: top, - scaleX: 1, - scaleY: 1 - }, animation); - - // Delete this function to allow it only once - this.animate = null; - } - } - }, - - /** - * Animate in the new series from the clicked point in the old series. - * Depends on the drilldown.js module - */ - animateDrilldown: function (init) { - var toBox = this.chart.plotBox, - level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1], - fromBox = level.bBox, - animationOptions = this.chart.options.drilldown.animation, - scale; - - if (!init) { - - scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height); - level.shapeArgs = { - scaleX: scale, - scaleY: scale, - translateX: fromBox.x, - translateY: fromBox.y - }; - - each(this.points, function (point) { - if (point.graphic) { - point.graphic - .attr(level.shapeArgs) - .animate({ - scaleX: 1, - scaleY: 1, - translateX: 0, - translateY: 0 - }, animationOptions); - } - }); - - this.animate = null; - } - - }, - - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - - /** - * When drilling up, pull out the individual point graphics from the lower series - * and animate them into the origin point in the upper series. - */ - animateDrillupFrom: function (level) { - seriesTypes.column.prototype.animateDrillupFrom.call(this, level); - }, - - - /** - * When drilling up, keep the upper series invisible until the lower series has - * moved into place - */ - animateDrillupTo: function (init) { - seriesTypes.column.prototype.animateDrillupTo.call(this, init); - } + type: 'map', + getExtremesFromAll: true, + useMapGeometry: true, // get axis extremes from paths, not values + forceDL: true, + searchPoint: noop, + directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply. + preserveAspectRatio: true, // X axis and Y axis must have same translation slope + pointArrayMap: ['value'], + /** + * Get the bounding box of all paths in the map combined. + */ + getBox: function (paths) { + var MAX_VALUE = Number.MAX_VALUE, + maxX = -MAX_VALUE, + minX = MAX_VALUE, + maxY = -MAX_VALUE, + minY = MAX_VALUE, + minRange = MAX_VALUE, + xAxis = this.xAxis, + yAxis = this.yAxis, + hasBox; + + // Find the bounding box + each(paths || [], function (point) { + + if (point.path) { + if (typeof point.path === 'string') { + point.path = H.splitPath(point.path); + } + + var path = point.path || [], + i = path.length, + even = false, // while loop reads from the end + pointMaxX = -MAX_VALUE, + pointMinX = MAX_VALUE, + pointMaxY = -MAX_VALUE, + pointMinY = MAX_VALUE, + properties = point.properties; + + // The first time a map point is used, analyze its box + if (!point._foundBox) { + while (i--) { + if (isNumber(path[i])) { + if (even) { // even = x + pointMaxX = Math.max(pointMaxX, path[i]); + pointMinX = Math.min(pointMinX, path[i]); + } else { // odd = Y + pointMaxY = Math.max(pointMaxY, path[i]); + pointMinY = Math.min(pointMinY, path[i]); + } + even = !even; + } + } + // Cache point bounding box for use to position data labels, + // bubbles etc + point._midX = pointMinX + (pointMaxX - pointMinX) * pick( + point.middleX, + properties && properties['hc-middle-x'], + 0.5 + ); + point._midY = pointMinY + (pointMaxY - pointMinY) * pick( + point.middleY, + properties && properties['hc-middle-y'], + 0.5 + ); + point._maxX = pointMaxX; + point._minX = pointMinX; + point._maxY = pointMaxY; + point._minY = pointMinY; + point.labelrank = pick(point.labelrank, (pointMaxX - pointMinX) * (pointMaxY - pointMinY)); + point._foundBox = true; + } + + maxX = Math.max(maxX, point._maxX); + minX = Math.min(minX, point._minX); + maxY = Math.max(maxY, point._maxY); + minY = Math.min(minY, point._minY); + minRange = Math.min(point._maxX - point._minX, point._maxY - point._minY, minRange); + hasBox = true; + } + }); + + // Set the box for the whole series + if (hasBox) { + this.minY = Math.min(minY, pick(this.minY, MAX_VALUE)); + this.maxY = Math.max(maxY, pick(this.maxY, -MAX_VALUE)); + this.minX = Math.min(minX, pick(this.minX, MAX_VALUE)); + this.maxX = Math.max(maxX, pick(this.maxX, -MAX_VALUE)); + + // If no minRange option is set, set the default minimum zooming range to 5 times the + // size of the smallest element + if (xAxis && xAxis.options.minRange === undefined) { + xAxis.minRange = Math.min(5 * minRange, (this.maxX - this.minX) / 5, xAxis.minRange || MAX_VALUE); + } + if (yAxis && yAxis.options.minRange === undefined) { + yAxis.minRange = Math.min(5 * minRange, (this.maxY - this.minY) / 5, yAxis.minRange || MAX_VALUE); + } + } + }, + + getExtremes: function () { + // Get the actual value extremes for colors + Series.prototype.getExtremes.call(this, this.valueData); + + // Recalculate box on updated data + if (this.chart.hasRendered && this.isDirtyData) { + this.getBox(this.options.data); + } + + this.valueMin = this.dataMin; + this.valueMax = this.dataMax; + + // Extremes for the mock Y axis + this.dataMin = this.minY; + this.dataMax = this.maxY; + }, + + /** + * Translate the path so that it automatically fits into the plot area box + * @param {Object} path + */ + translatePath: function (path) { + + var series = this, + even = false, // while loop reads from the end + xAxis = series.xAxis, + yAxis = series.yAxis, + xMin = xAxis.min, + xTransA = xAxis.transA, + xMinPixelPadding = xAxis.minPixelPadding, + yMin = yAxis.min, + yTransA = yAxis.transA, + yMinPixelPadding = yAxis.minPixelPadding, + i, + ret = []; // Preserve the original + + // Do the translation + if (path) { + i = path.length; + while (i--) { + if (isNumber(path[i])) { + ret[i] = even ? + (path[i] - xMin) * xTransA + xMinPixelPadding : + (path[i] - yMin) * yTransA + yMinPixelPadding; + even = !even; + } else { + ret[i] = path[i]; + } + } + } + + return ret; + }, + + /** + * Extend setData to join in mapData. If the allAreas option is true, all areas + * from the mapData are used, and those that don't correspond to a data value + * are given null values. + */ + setData: function (data, redraw, animation, updatePoints) { + var options = this.options, + chartOptions = this.chart.options.chart, + globalMapData = chartOptions && chartOptions.map, + mapData = options.mapData, + joinBy = options.joinBy, + joinByNull = joinBy === null, + pointArrayMap = options.keys || this.pointArrayMap, + dataUsed = [], + mapMap = {}, + mapPoint, + mapTransforms = this.chart.mapTransforms, + props, + i; + + // Collect mapData from chart options if not defined on series + if (!mapData && globalMapData) { + mapData = typeof globalMapData === 'string' ? H.maps[globalMapData] : globalMapData; + } + + if (joinByNull) { + joinBy = '_i'; + } + joinBy = this.joinBy = splat(joinBy); + if (!joinBy[1]) { + joinBy[1] = joinBy[0]; + } + + // Pick up numeric values, add index + // Convert Array point definitions to objects using pointArrayMap + if (data) { + each(data, function (val, i) { + var ix = 0; + if (isNumber(val)) { + data[i] = { + value: val + }; + } else if (isArray(val)) { + data[i] = {}; + // Automatically copy first item to hc-key if there is an extra leading string + if (!options.keys && val.length > pointArrayMap.length && typeof val[0] === 'string') { + data[i]['hc-key'] = val[0]; + ++ix; + } + // Run through pointArrayMap and what's left of the point data array in parallel, copying over the values + for (var j = 0; j < pointArrayMap.length; ++j, ++ix) { + if (pointArrayMap[j] && val[ix] !== undefined) { + if (pointArrayMap[j].indexOf('.') > 0) { + H.Point.prototype.setNestedProperty( + data[i], val[ix], pointArrayMap[j] + ); + } else { + data[i][pointArrayMap[j]] = val[ix]; + } + } + } + } + if (joinByNull) { + data[i]._i = i; + } + }); + } + + this.getBox(data); + + // Pick up transform definitions for chart + this.chart.mapTransforms = mapTransforms = chartOptions && chartOptions.mapTransforms || mapData && mapData['hc-transform'] || mapTransforms; + + // Cache cos/sin of transform rotation angle + if (mapTransforms) { + H.objectEach(mapTransforms, function (transform) { + if (transform.rotation) { + transform.cosAngle = Math.cos(transform.rotation); + transform.sinAngle = Math.sin(transform.rotation); + } + }); + } + + if (mapData) { + if (mapData.type === 'FeatureCollection') { + this.mapTitle = mapData.title; + mapData = H.geojson(mapData, this.type, this); + } + + this.mapData = mapData; + this.mapMap = {}; + + for (i = 0; i < mapData.length; i++) { + mapPoint = mapData[i]; + props = mapPoint.properties; + + mapPoint._i = i; + // Copy the property over to root for faster access + if (joinBy[0] && props && props[joinBy[0]]) { + mapPoint[joinBy[0]] = props[joinBy[0]]; + } + mapMap[mapPoint[joinBy[0]]] = mapPoint; + } + this.mapMap = mapMap; + + // Registered the point codes that actually hold data + if (data && joinBy[1]) { + each(data, function (point) { + if (mapMap[point[joinBy[1]]]) { + dataUsed.push(mapMap[point[joinBy[1]]]); + } + }); + } + + if (options.allAreas) { + this.getBox(mapData); + data = data || []; + + // Registered the point codes that actually hold data + if (joinBy[1]) { + each(data, function (point) { + dataUsed.push(point[joinBy[1]]); + }); + } + + // Add those map points that don't correspond to data, which will be drawn as null points + dataUsed = '|' + map(dataUsed, function (point) { + return point && point[joinBy[0]]; + }).join('|') + '|'; // String search is faster than array.indexOf + + each(mapData, function (mapPoint) { + if (!joinBy[0] || dataUsed.indexOf('|' + mapPoint[joinBy[0]] + '|') === -1) { + data.push(merge(mapPoint, { value: null })); + updatePoints = false; // #5050 - adding all areas causes the update optimization of setData to kick in, even though the point order has changed + } + }); + } else { + this.getBox(dataUsed); // Issue #4784 + } + } + Series.prototype.setData.call(this, data, redraw, animation, updatePoints); + }, + + + /** + * No graph for the map series + */ + drawGraph: noop, + + /** + * We need the points' bounding boxes in order to draw the data labels, so + * we skip it now and call it from drawPoints instead. + */ + drawDataLabels: noop, + + /** + * Allow a quick redraw by just translating the area group. Used for zooming and panning + * in capable browsers. + */ + doFullTranslate: function () { + return this.isDirtyData || this.chart.isResizing || this.chart.renderer.isVML || !this.baseTrans; + }, + + /** + * Add the path option for data points. Find the max value for color calculation. + */ + translate: function () { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + doFullTranslate = series.doFullTranslate(); + + series.generatePoints(); + + each(series.data, function (point) { + + // Record the middle point (loosely based on centroid), determined + // by the middleX and middleY options. + point.plotX = xAxis.toPixels(point._midX, true); + point.plotY = yAxis.toPixels(point._midY, true); + + if (doFullTranslate) { + + point.shapeType = 'path'; + point.shapeArgs = { + d: series.translatePath(point.path) + }; + } + }); + + series.translateColors(); + }, + + /** + * Get presentational attributes. In the maps series this runs in both + * styled and non-styled mode, because colors hold data when a colorAxis + * is used. + */ + pointAttribs: function (point, state) { + var attr; + /*= if (build.classic) { =*/ + attr = seriesTypes.column.prototype.pointAttribs.call( + this, point, state + ); + /*= } else { =*/ + attr = this.colorAttribs(point); + /*= } =*/ + + // If vector-effect is not supported, we set the stroke-width on the group element + // and let all point graphics inherit. That way we don't have to iterate over all + // points to update the stroke-width on zooming. TODO: Check unstyled + if (supportsVectorEffect) { + attr['vector-effect'] = 'non-scaling-stroke'; + } else { + attr['stroke-width'] = 'inherit'; + } + + return attr; + }, + + /** + * Use the drawPoints method of column, that is able to handle simple shapeArgs. + * Extend it by assigning the tooltip position. + */ + drawPoints: function () { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + group = series.group, + chart = series.chart, + renderer = chart.renderer, + scaleX, + scaleY, + translateX, + translateY, + baseTrans = this.baseTrans, + transformGroup, + startTranslateX, + startTranslateY, + startScaleX, + startScaleY; + + // Set a group that handles transform during zooming and panning in order to preserve clipping + // on series.group + if (!series.transformGroup) { + series.transformGroup = renderer.g() + .attr({ + scaleX: 1, + scaleY: 1 + }) + .add(group); + series.transformGroup.survive = true; + } + + // Draw the shapes again + if (series.doFullTranslate()) { + + // Individual point actions. TODO: Check unstyled. + /*= if (build.classic) { =*/ + if (chart.hasRendered) { + each(series.points, function (point) { + + // Restore state color on update/redraw (#3529) + if (point.shapeArgs) { + point.shapeArgs.fill = series.pointAttribs(point, point.state).fill; + } + }); + } + /*= } =*/ + + // Draw them in transformGroup + series.group = series.transformGroup; + seriesTypes.column.prototype.drawPoints.apply(series); + series.group = group; // Reset + + // Add class names + each(series.points, function (point) { + if (point.graphic) { + if (point.name) { + point.graphic.addClass('highcharts-name-' + point.name.replace(/ /g, '-').toLowerCase()); + } + if (point.properties && point.properties['hc-key']) { + point.graphic.addClass('highcharts-key-' + point.properties['hc-key'].toLowerCase()); + } + + /*= if (!build.classic) { =*/ + point.graphic.css( + series.pointAttribs(point, point.selected && 'select') + ); + /*= } =*/ + } + }); + + // Set the base for later scale-zooming. The originX and originY properties are the + // axis values in the plot area's upper left corner. + this.baseTrans = { + originX: xAxis.min - xAxis.minPixelPadding / xAxis.transA, + originY: yAxis.min - yAxis.minPixelPadding / yAxis.transA + (yAxis.reversed ? 0 : yAxis.len / yAxis.transA), + transAX: xAxis.transA, + transAY: yAxis.transA + }; + + // Reset transformation in case we're doing a full translate (#3789) + this.transformGroup.animate({ + translateX: 0, + translateY: 0, + scaleX: 1, + scaleY: 1 + }); + + // Just update the scale and transform for better performance + } else { + scaleX = xAxis.transA / baseTrans.transAX; + scaleY = yAxis.transA / baseTrans.transAY; + translateX = xAxis.toPixels(baseTrans.originX, true); + translateY = yAxis.toPixels(baseTrans.originY, true); + + // Handle rounding errors in normal view (#3789) + if (scaleX > 0.99 && scaleX < 1.01 && scaleY > 0.99 && scaleY < 1.01) { + scaleX = 1; + scaleY = 1; + translateX = Math.round(translateX); + translateY = Math.round(translateY); + } + + // Animate or move to the new zoom level. In order to prevent + // flickering as the different transform components are set out of + // sync (#5991), we run a fake animator attribute and set scale and + // translation synchronously in the same step. + // A possible improvement to the API would be to handle this in the + // renderer or animation engine itself, to ensure that when we are + // animating multiple properties, we make sure that each step for + // each property is performed in the same step. Also, for symbols + // and for transform properties, it should induce a single + // updateTransform and symbolAttr call. + transformGroup = this.transformGroup; + if (chart.renderer.globalAnimation) { + startTranslateX = transformGroup.attr('translateX'); + startTranslateY = transformGroup.attr('translateY'); + startScaleX = transformGroup.attr('scaleX'); + startScaleY = transformGroup.attr('scaleY'); + transformGroup + .attr({ animator: 0 }) + .animate({ + animator: 1 + }, { + step: function (now, fx) { + transformGroup.attr({ + translateX: startTranslateX + + (translateX - startTranslateX) * fx.pos, + translateY: startTranslateY + + (translateY - startTranslateY) * fx.pos, + scaleX: startScaleX + + (scaleX - startScaleX) * fx.pos, + scaleY: startScaleY + + (scaleY - startScaleY) * fx.pos + }); + + } + }); + + // When dragging, animation is off. + } else { + transformGroup.attr({ + translateX: translateX, + translateY: translateY, + scaleX: scaleX, + scaleY: scaleY + }); + } + + } + + // Set the stroke-width directly on the group element so the children inherit it. We need to use + // setAttribute directly, because the stroke-widthSetter method expects a stroke color also to be + // set. + if (!supportsVectorEffect) { + series.group.element.setAttribute( + 'stroke-width', + series.options[ + (series.pointAttrToOptions && series.pointAttrToOptions['stroke-width']) || 'borderWidth' + ] / (scaleX || 1) + ); + } + + this.drawMapDataLabels(); + + + }, + + /** + * Draw the data labels. Special for maps is the time that the data labels are drawn (after points), + * and the clipping of the dataLabelsGroup. + */ + drawMapDataLabels: function () { + + Series.prototype.drawDataLabels.call(this); + if (this.dataLabelsGroup) { + this.dataLabelsGroup.clip(this.chart.clipRect); + } + }, + + /** + * Override render to throw in an async call in IE8. Otherwise it chokes on the US counties demo. + */ + render: function () { + var series = this, + render = Series.prototype.render; + + // Give IE8 some time to breathe. + if (series.chart.renderer.isVML && series.data.length > 3000) { + setTimeout(function () { + render.call(series); + }); + } else { + render.call(series); + } + }, + + /** + * The initial animation for the map series. By default, animation is disabled. + * Animation of map shapes is not at all supported in VML browsers. + */ + animate: function (init) { + var chart = this.chart, + animation = this.options.animation, + group = this.group, + xAxis = this.xAxis, + yAxis = this.yAxis, + left = xAxis.pos, + top = yAxis.pos; + + if (chart.renderer.isSVG) { + + if (animation === true) { + animation = { + duration: 1000 + }; + } + + // Initialize the animation + if (init) { + + // Scale down the group and place it in the center + group.attr({ + translateX: left + xAxis.len / 2, + translateY: top + yAxis.len / 2, + scaleX: 0.001, // #1499 + scaleY: 0.001 + }); + + // Run the animation + } else { + group.animate({ + translateX: left, + translateY: top, + scaleX: 1, + scaleY: 1 + }, animation); + + // Delete this function to allow it only once + this.animate = null; + } + } + }, + + /** + * Animate in the new series from the clicked point in the old series. + * Depends on the drilldown.js module + */ + animateDrilldown: function (init) { + var toBox = this.chart.plotBox, + level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1], + fromBox = level.bBox, + animationOptions = this.chart.options.drilldown.animation, + scale; + + if (!init) { + + scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height); + level.shapeArgs = { + scaleX: scale, + scaleY: scale, + translateX: fromBox.x, + translateY: fromBox.y + }; + + each(this.points, function (point) { + if (point.graphic) { + point.graphic + .attr(level.shapeArgs) + .animate({ + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0 + }, animationOptions); + } + }); + + this.animate = null; + } + + }, + + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + /** + * When drilling up, pull out the individual point graphics from the lower series + * and animate them into the origin point in the upper series. + */ + animateDrillupFrom: function (level) { + seriesTypes.column.prototype.animateDrillupFrom.call(this, level); + }, + + + /** + * When drilling up, keep the upper series invisible until the lower series has + * moved into place + */ + animateDrillupTo: function (init) { + seriesTypes.column.prototype.animateDrillupTo.call(this, init); + } // Point class }), extend({ - /** - * Extend the Point object to split paths - */ - applyOptions: function (options, x) { - - var point = Point.prototype.applyOptions.call(this, options, x), - series = this.series, - joinBy = series.joinBy, - mapPoint; - - if (series.mapData) { - mapPoint = point[joinBy[1]] !== undefined && series.mapMap[point[joinBy[1]]]; - if (mapPoint) { - // This applies only to bubbles - if (series.xyFromShape) { - point.x = mapPoint._midX; - point.y = mapPoint._midY; - } - extend(point, mapPoint); // copy over properties - } else { - point.value = point.value || null; - } - } - - return point; - }, - - /** - * Stop the fade-out - */ - onMouseOver: function (e) { - H.clearTimeout(this.colorInterval); - if (this.value !== null || this.series.options.nullInteraction) { - Point.prototype.onMouseOver.call(this, e); - } else { // #3401 Tooltip doesn't hide when hovering over null points - this.series.onMouseOut(e); - } - }, - - /** - * Highmaps only. Zoom in on the point using the global animation. - * - * @function #zoomTo - * @memberOf Point - * @sample maps/members/point-zoomto/ - * Zoom to points from butons - */ - zoomTo: function () { - var point = this, - series = point.series; - - series.xAxis.setExtremes( - point._minX, - point._maxX, - false - ); - series.yAxis.setExtremes( - point._minY, - point._maxY, - false - ); - series.chart.redraw(); - } + /** + * Extend the Point object to split paths + */ + applyOptions: function (options, x) { + + var point = Point.prototype.applyOptions.call(this, options, x), + series = this.series, + joinBy = series.joinBy, + mapPoint; + + if (series.mapData) { + mapPoint = point[joinBy[1]] !== undefined && series.mapMap[point[joinBy[1]]]; + if (mapPoint) { + // This applies only to bubbles + if (series.xyFromShape) { + point.x = mapPoint._midX; + point.y = mapPoint._midY; + } + extend(point, mapPoint); // copy over properties + } else { + point.value = point.value || null; + } + } + + return point; + }, + + /** + * Stop the fade-out + */ + onMouseOver: function (e) { + H.clearTimeout(this.colorInterval); + if (this.value !== null || this.series.options.nullInteraction) { + Point.prototype.onMouseOver.call(this, e); + } else { // #3401 Tooltip doesn't hide when hovering over null points + this.series.onMouseOut(e); + } + }, + + /** + * Highmaps only. Zoom in on the point using the global animation. + * + * @function #zoomTo + * @memberOf Point + * @sample maps/members/point-zoomto/ + * Zoom to points from butons + */ + zoomTo: function () { + var point = this, + series = point.series; + + series.xAxis.setExtremes( + point._minX, + point._maxX, + false + ); + series.yAxis.setExtremes( + point._minY, + point._maxY, + false + ); + series.chart.redraw(); + } }, colorPointMixin)); /** * An array of objects containing a `path` definition and optionally * a code or property to join in the data as per the `joinBy` option. - * + * * @type {Array} * @sample {highmaps} maps/demo/category-map/ Map data and joinBy * @product highmaps @@ -1022,7 +1022,7 @@ seriesType('map', 'scatter', { /** * A `map` series. If the [type](#series.map.type) option is not specified, * it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.map * @excluding dataParser,dataURL,marker @@ -1033,17 +1033,17 @@ seriesType('map', 'scatter', { /** * An array of data points for the series. For the `map` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `value` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `[hc-key, value]`. Example: - * + * * ```js * data: [ * ['us-ny', 0], @@ -1052,12 +1052,12 @@ seriesType('map', 'scatter', { * ['us-ak', 5] * ] * ``` - * + * * 3. 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.map.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * value: 6, @@ -1069,7 +1069,7 @@ seriesType('map', 'scatter', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @product highmaps * @apioption series.map.data @@ -1078,7 +1078,7 @@ seriesType('map', 'scatter', { /** * Individual color for the point. By default the color is either used * to denote the value, or pulled from the global `colors` array. - * + * * @type {Color} * @default undefined * @product highmaps @@ -1089,7 +1089,7 @@ seriesType('map', 'scatter', { * Individual data label for each point. The options are the same as * the ones for [plotOptions.series.dataLabels]( * #plotOptions.series.dataLabels). - * + * * @type {Object} * @sample {highmaps} maps/series/data-datalabels/ Disable data labels for individual areas * @product highmaps @@ -1099,7 +1099,7 @@ seriesType('map', 'scatter', { /** * The `id` of a series in the [drilldown.series](#drilldown.series) * array to use for a drilldown for this point. - * + * * @type {String} * @sample {highmaps} maps/demo/map-drilldown/ Basic drilldown * @product highmaps @@ -1109,7 +1109,7 @@ seriesType('map', 'scatter', { /** * An id for the point. This can be used after render time to get a * pointer to the point object through `chart.get()`. - * + * * @type {String} * @sample {highmaps} maps/series/data-id/ Highlight a point by id * @product highmaps @@ -1121,7 +1121,7 @@ seriesType('map', 'scatter', { * algorithm to detect collision. When two labels collide, the one with * the lowest rank is hidden. By default the rank is computed from the * area. - * + * * @type {Number} * @product highmaps * @apioption series.map.data.labelrank @@ -1131,7 +1131,7 @@ seriesType('map', 'scatter', { * The relative mid point of an area, used to place the data label. * Ranges from 0 to 1\. When `mapData` is used, middleX can be defined * there. - * + * * @type {Number} * @default 0.5 * @product highmaps @@ -1142,7 +1142,7 @@ seriesType('map', 'scatter', { * The relative mid point of an area, used to place the data label. * Ranges from 0 to 1\. When `mapData` is used, middleY can be defined * there. - * + * * @type {Number} * @default 0.5 * @product highmaps @@ -1152,7 +1152,7 @@ seriesType('map', 'scatter', { /** * The name of the point as shown in the legend, tooltip, dataLabel * etc. - * + * * @type {String} * @sample {highmaps} maps/series/data-datalabels/ Point names * @product highmaps @@ -1163,11 +1163,11 @@ seriesType('map', 'scatter', { * For map and mapline series types, the SVG path for the shape. For * compatibily with old IE, not all SVG path definitions are supported, * but M, L and C operators are safe. - * + * * To achieve a better separation between the structure and the data, * it is recommended to use `mapData` to define that paths instead * of defining them on the data points themselves. - * + * * @type {String} * @sample {highmaps} maps/series/data-path/ Paths defined in data * @product highmaps @@ -1176,7 +1176,7 @@ seriesType('map', 'scatter', { /** * The numeric value of the data point. - * + * * @type {Number} * @product highmaps * @apioption series.map.data.value @@ -1185,7 +1185,7 @@ seriesType('map', 'scatter', { /** * Individual point events - * + * * @extends plotOptions.series.point.events * @product highmaps * @apioption series.map.data.events diff --git a/js/parts-more/AreaRangeSeries.js b/js/parts-more/AreaRangeSeries.js index 56d62db1342..f51c7aa258f 100644 --- a/js/parts-more/AreaRangeSeries.js +++ b/js/parts-more/AreaRangeSeries.js @@ -10,20 +10,20 @@ import '../parts/Utilities.js'; import '../parts/Options.js'; import '../parts/Series.js'; var each = H.each, - noop = H.noop, - pick = H.pick, - defined = H.defined, - Series = H.Series, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes, - seriesProto = Series.prototype, - pointProto = H.Point.prototype; + noop = H.noop, + pick = H.pick, + defined = H.defined, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes, + seriesProto = Series.prototype, + pointProto = H.Point.prototype; /** * The area range series is a carteseian series with higher and lower values * for each point along an X axis, where the area between the values is shaded. * Requires `highcharts-more.js`. - * + * * @extends plotOptions.area * @product highcharts highstock * @sample {highcharts} highcharts/demo/arearange/ @@ -34,562 +34,562 @@ var each = H.each, * @optionparent plotOptions.arearange */ seriesType('arearange', 'area', { - /*= if (build.classic) { =*/ - - /** - * Whether to apply a drop shadow to the graph line. Since 2.3 the shadow - * can be an object configuration containing `color`, `offsetX`, `offsetY`, - * `opacity` and `width`. - * - * @type {Boolean|Object} - * @product highcharts - * @apioption plotOptions.arearange.shadow - */ - - /** - * Pixel width of the arearange graph line. - * - * @since 2.3.0 - * @product highcharts highstock - */ - lineWidth: 1, - /*= } =*/ - - threshold: null, - - tooltip: { - /*= if (!build.classic) { =*/ - pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
', - /*= } else { =*/ - - pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
' // eslint-disable-line no-dupe-keys - /*= } =*/ - }, - - /** - * Whether the whole area or just the line should respond to mouseover - * tooltips and other mouse or touch events. - * - * @since 2.3.0 - * @product highcharts highstock - */ - trackByArea: true, - - /** - * Extended data labels for range series types. Range series data labels - * have no `x` and `y` options. Instead, they have `xLow`, `xHigh`, - * `yLow` and `yHigh` options to allow the higher and lower data label - * sets individually. - * - * @type {Object} - * @extends plotOptions.series.dataLabels - * @excluding x,y - * @since 2.3.0 - * @product highcharts highstock - */ - dataLabels: { - - align: null, - verticalAlign: null, - - /** - * X offset of the lower data labels relative to the point value. - * - * @sample {highcharts} highcharts/plotoptions/arearange-datalabels/ - * Data labels on range series - * @sample {highstock} highcharts/plotoptions/arearange-datalabels/ - * Data labels on range series - * @since 2.3.0 - * @product highcharts highstock - */ - xLow: 0, - - /** - * X offset of the higher data labels relative to the point value. - * - * @sample {highcharts|highstock} - * highcharts/plotoptions/arearange-datalabels/ - * Data labels on range series - * @since 2.3.0 - * @product highcharts highstock - */ - xHigh: 0, - - /** - * Y offset of the lower data labels relative to the point value. - * - * @sample {highcharts|highstock} - * highcharts/plotoptions/arearange-datalabels/ - * Data labels on range series - * @default 16 - * @since 2.3.0 - * @product highcharts highstock - */ - yLow: 0, - - /** - * Y offset of the higher data labels relative to the point value. - * - * @sample {highcharts|highstock} - * highcharts/plotoptions/arearange-datalabels/ - * Data labels on range series - * @default -6 - * @since 2.3.0 - * @product highcharts highstock - */ - yHigh: 0 - } + /*= if (build.classic) { =*/ + + /** + * Whether to apply a drop shadow to the graph line. Since 2.3 the shadow + * can be an object configuration containing `color`, `offsetX`, `offsetY`, + * `opacity` and `width`. + * + * @type {Boolean|Object} + * @product highcharts + * @apioption plotOptions.arearange.shadow + */ + + /** + * Pixel width of the arearange graph line. + * + * @since 2.3.0 + * @product highcharts highstock + */ + lineWidth: 1, + /*= } =*/ + + threshold: null, + + tooltip: { + /*= if (!build.classic) { =*/ + pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
', + /*= } else { =*/ + + pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
' // eslint-disable-line no-dupe-keys + /*= } =*/ + }, + + /** + * Whether the whole area or just the line should respond to mouseover + * tooltips and other mouse or touch events. + * + * @since 2.3.0 + * @product highcharts highstock + */ + trackByArea: true, + + /** + * Extended data labels for range series types. Range series data labels + * have no `x` and `y` options. Instead, they have `xLow`, `xHigh`, + * `yLow` and `yHigh` options to allow the higher and lower data label + * sets individually. + * + * @type {Object} + * @extends plotOptions.series.dataLabels + * @excluding x,y + * @since 2.3.0 + * @product highcharts highstock + */ + dataLabels: { + + align: null, + verticalAlign: null, + + /** + * X offset of the lower data labels relative to the point value. + * + * @sample {highcharts} highcharts/plotoptions/arearange-datalabels/ + * Data labels on range series + * @sample {highstock} highcharts/plotoptions/arearange-datalabels/ + * Data labels on range series + * @since 2.3.0 + * @product highcharts highstock + */ + xLow: 0, + + /** + * X offset of the higher data labels relative to the point value. + * + * @sample {highcharts|highstock} + * highcharts/plotoptions/arearange-datalabels/ + * Data labels on range series + * @since 2.3.0 + * @product highcharts highstock + */ + xHigh: 0, + + /** + * Y offset of the lower data labels relative to the point value. + * + * @sample {highcharts|highstock} + * highcharts/plotoptions/arearange-datalabels/ + * Data labels on range series + * @default 16 + * @since 2.3.0 + * @product highcharts highstock + */ + yLow: 0, + + /** + * Y offset of the higher data labels relative to the point value. + * + * @sample {highcharts|highstock} + * highcharts/plotoptions/arearange-datalabels/ + * Data labels on range series + * @default -6 + * @since 2.3.0 + * @product highcharts highstock + */ + yHigh: 0 + } // Prototype members }, { - pointArrayMap: ['low', 'high'], - dataLabelCollections: ['dataLabel', 'dataLabelUpper'], - toYData: function (point) { - return [point.low, point.high]; - }, - pointValKey: 'low', - deferTranslatePolar: true, - - /** - * Translate a point's plotHigh from the internal angle and radius - * measures to true plotHigh coordinates. This is an addition of the - * toXY method found in Polar.js, because it runs too early for - * arearanges to be considered (#3419). - */ - highToXY: function (point) { - // Find the polar plotX and plotY - var chart = this.chart, - xy = this.xAxis.postTranslate( - point.rectPlotX, - this.yAxis.len - point.plotHigh - ); - point.plotHighX = xy.x - chart.plotLeft; - point.plotHigh = xy.y - chart.plotTop; - point.plotLowX = point.plotX; - }, - - /** - * Translate data points from raw values x and y to plotX and plotY - */ - translate: function () { - var series = this, - yAxis = series.yAxis, - hasModifyValue = !!series.modifyValue; - - seriesTypes.area.prototype.translate.apply(series); - - // Set plotLow and plotHigh - each(series.points, function (point) { - - var low = point.low, - high = point.high, - plotY = point.plotY; - - if (high === null || low === null) { - point.isNull = true; - point.plotY = null; - } else { - point.plotLow = plotY; - point.plotHigh = yAxis.translate( - hasModifyValue ? series.modifyValue(high, point) : high, - 0, - 1, - 0, - 1 - ); - if (hasModifyValue) { - point.yBottom = point.plotHigh; - } - } - }); - - // Postprocess plotHigh - if (this.chart.polar) { - each(this.points, function (point) { - series.highToXY(point); - point.tooltipPos = [ - (point.plotHighX + point.plotLowX) / 2, - (point.plotHigh + point.plotLow) / 2 - ]; - }); - } - }, - - /** - * Extend the line series' getSegmentPath method by applying the segment - * path to both lower and higher values of the range - */ - getGraphPath: function (points) { - - var highPoints = [], - highAreaPoints = [], - i, - getGraphPath = seriesTypes.area.prototype.getGraphPath, - point, - pointShim, - linePath, - lowerPath, - options = this.options, - connectEnds = this.chart.polar && options.connectEnds !== false, - connectNulls = options.connectNulls, - step = options.step, - higherPath, - higherAreaPath; - - points = points || this.points; - i = points.length; - - /** - * Create the top line and the top part of the area fill. The area - * fill compensates for null points by drawing down to the lower graph, - * moving across the null gap and starting again at the lower graph. - */ - i = points.length; - while (i--) { - point = points[i]; - - if ( - !point.isNull && - !connectEnds && - !connectNulls && - (!points[i + 1] || points[i + 1].isNull) - ) { - highAreaPoints.push({ - plotX: point.plotX, - plotY: point.plotY, - doCurve: false // #5186, gaps in areasplinerange fill - }); - } - - pointShim = { - polarPlotY: point.polarPlotY, - rectPlotX: point.rectPlotX, - yBottom: point.yBottom, - // plotHighX is for polar charts - plotX: pick(point.plotHighX, point.plotX), - plotY: point.plotHigh, - isNull: point.isNull - }; - - highAreaPoints.push(pointShim); - - highPoints.push(pointShim); - - if ( - !point.isNull && - !connectEnds && - !connectNulls && - (!points[i - 1] || points[i - 1].isNull) - ) { - highAreaPoints.push({ - plotX: point.plotX, - plotY: point.plotY, - doCurve: false // #5186, gaps in areasplinerange fill - }); - } - } - - // Get the paths - lowerPath = getGraphPath.call(this, points); - if (step) { - if (step === true) { - step = 'left'; - } - options.step = { - left: 'right', - center: 'center', - right: 'left' - }[step]; // swap for reading in getGraphPath - } - higherPath = getGraphPath.call(this, highPoints); - higherAreaPath = getGraphPath.call(this, highAreaPoints); - options.step = step; - - // Create a line on both top and bottom of the range - linePath = [].concat(lowerPath, higherPath); - - // For the area path, we need to change the 'move' statement - // into 'lineTo' or 'curveTo' - if (!this.chart.polar && higherAreaPath[0] === 'M') { - higherAreaPath[0] = 'L'; // this probably doesn't work for spline - } - - this.graphPath = linePath; - this.areaPath = lowerPath.concat(higherAreaPath); - - // Prepare for sideways animation - linePath.isArea = true; - linePath.xMap = lowerPath.xMap; - this.areaPath.xMap = lowerPath.xMap; - - return linePath; - }, - - /** - * Extend the basic drawDataLabels method by running it for both lower - * and higher values. - */ - drawDataLabels: function () { - - var data = this.data, - length = data.length, - i, - originalDataLabels = [], - dataLabelOptions = this.options.dataLabels, - align = dataLabelOptions.align, - verticalAlign = dataLabelOptions.verticalAlign, - inside = dataLabelOptions.inside, - point, - up, - inverted = this.chart.inverted; - - if (dataLabelOptions.enabled || this._hasPointLabels) { - - // Step 1: set preliminary values for plotY and dataLabel - // and draw the upper labels - i = length; - while (i--) { - point = data[i]; - if (point) { - up = inside ? - point.plotHigh < point.plotLow : - point.plotHigh > point.plotLow; - - // Set preliminary values - point.y = point.high; - point._plotY = point.plotY; - point.plotY = point.plotHigh; - - // Store original data labels and set preliminary label - // objects to be picked up in the uber method - originalDataLabels[i] = point.dataLabel; - point.dataLabel = point.dataLabelUpper; - - // Set the default offset - point.below = up; - if (inverted) { - if (!align) { - dataLabelOptions.align = up ? 'right' : 'left'; - } - } else { - if (!verticalAlign) { - dataLabelOptions.verticalAlign = up ? - 'top' : - 'bottom'; - } - } - - dataLabelOptions.x = dataLabelOptions.xHigh; - dataLabelOptions.y = dataLabelOptions.yHigh; - } - } - - if (seriesProto.drawDataLabels) { - seriesProto.drawDataLabels.apply(this, arguments); // #1209 - } - - // Step 2: reorganize and handle data labels for the lower values - i = length; - while (i--) { - point = data[i]; - if (point) { - up = inside ? - point.plotHigh < point.plotLow : - point.plotHigh > point.plotLow; - - // Move the generated labels from step 1, and reassign - // the original data labels - point.dataLabelUpper = point.dataLabel; - point.dataLabel = originalDataLabels[i]; - - // Reset values - point.y = point.low; - point.plotY = point._plotY; - - // Set the default offset - point.below = !up; - if (inverted) { - if (!align) { - dataLabelOptions.align = up ? 'left' : 'right'; - } - } else { - if (!verticalAlign) { - dataLabelOptions.verticalAlign = up ? - 'bottom' : - 'top'; - } - - } - - dataLabelOptions.x = dataLabelOptions.xLow; - dataLabelOptions.y = dataLabelOptions.yLow; - } - } - if (seriesProto.drawDataLabels) { - seriesProto.drawDataLabels.apply(this, arguments); - } - } - - dataLabelOptions.align = align; - dataLabelOptions.verticalAlign = verticalAlign; - }, - - alignDataLabel: function () { - seriesTypes.column.prototype.alignDataLabel.apply(this, arguments); - }, - - drawPoints: function () { - var series = this, - pointLength = series.points.length, - point, - i; - - // Draw bottom points - seriesProto.drawPoints.apply(series, arguments); - - i = 0; - while (i < pointLength) { - point = series.points[i]; - point.lowerGraphic = point.graphic; - point.graphic = point.upperGraphic; - point._plotY = point.plotY; - point._plotX = point.plotX; - point.plotY = point.plotHigh; - if (defined(point.plotHighX)) { - point.plotX = point.plotHighX; - } - point._isInside = point.isInside; - if (!series.chart.polar) { - point.isInside = point.isTopInside = ( - point.plotY !== undefined && - point.plotY >= 0 && - point.plotY <= series.yAxis.len && // #3519 - point.plotX >= 0 && - point.plotX <= series.xAxis.len - ); - } - i++; - } - - // Draw top points - seriesProto.drawPoints.apply(series, arguments); - - i = 0; - while (i < pointLength) { - point = series.points[i]; - point.upperGraphic = point.graphic; - point.graphic = point.lowerGraphic; - point.isInside = point._isInside; - point.plotY = point._plotY; - point.plotX = point._plotX; - i++; - } - }, - - setStackedPoints: noop + pointArrayMap: ['low', 'high'], + dataLabelCollections: ['dataLabel', 'dataLabelUpper'], + toYData: function (point) { + return [point.low, point.high]; + }, + pointValKey: 'low', + deferTranslatePolar: true, + + /** + * Translate a point's plotHigh from the internal angle and radius + * measures to true plotHigh coordinates. This is an addition of the + * toXY method found in Polar.js, because it runs too early for + * arearanges to be considered (#3419). + */ + highToXY: function (point) { + // Find the polar plotX and plotY + var chart = this.chart, + xy = this.xAxis.postTranslate( + point.rectPlotX, + this.yAxis.len - point.plotHigh + ); + point.plotHighX = xy.x - chart.plotLeft; + point.plotHigh = xy.y - chart.plotTop; + point.plotLowX = point.plotX; + }, + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + hasModifyValue = !!series.modifyValue; + + seriesTypes.area.prototype.translate.apply(series); + + // Set plotLow and plotHigh + each(series.points, function (point) { + + var low = point.low, + high = point.high, + plotY = point.plotY; + + if (high === null || low === null) { + point.isNull = true; + point.plotY = null; + } else { + point.plotLow = plotY; + point.plotHigh = yAxis.translate( + hasModifyValue ? series.modifyValue(high, point) : high, + 0, + 1, + 0, + 1 + ); + if (hasModifyValue) { + point.yBottom = point.plotHigh; + } + } + }); + + // Postprocess plotHigh + if (this.chart.polar) { + each(this.points, function (point) { + series.highToXY(point); + point.tooltipPos = [ + (point.plotHighX + point.plotLowX) / 2, + (point.plotHigh + point.plotLow) / 2 + ]; + }); + } + }, + + /** + * Extend the line series' getSegmentPath method by applying the segment + * path to both lower and higher values of the range + */ + getGraphPath: function (points) { + + var highPoints = [], + highAreaPoints = [], + i, + getGraphPath = seriesTypes.area.prototype.getGraphPath, + point, + pointShim, + linePath, + lowerPath, + options = this.options, + connectEnds = this.chart.polar && options.connectEnds !== false, + connectNulls = options.connectNulls, + step = options.step, + higherPath, + higherAreaPath; + + points = points || this.points; + i = points.length; + + /** + * Create the top line and the top part of the area fill. The area + * fill compensates for null points by drawing down to the lower graph, + * moving across the null gap and starting again at the lower graph. + */ + i = points.length; + while (i--) { + point = points[i]; + + if ( + !point.isNull && + !connectEnds && + !connectNulls && + (!points[i + 1] || points[i + 1].isNull) + ) { + highAreaPoints.push({ + plotX: point.plotX, + plotY: point.plotY, + doCurve: false // #5186, gaps in areasplinerange fill + }); + } + + pointShim = { + polarPlotY: point.polarPlotY, + rectPlotX: point.rectPlotX, + yBottom: point.yBottom, + // plotHighX is for polar charts + plotX: pick(point.plotHighX, point.plotX), + plotY: point.plotHigh, + isNull: point.isNull + }; + + highAreaPoints.push(pointShim); + + highPoints.push(pointShim); + + if ( + !point.isNull && + !connectEnds && + !connectNulls && + (!points[i - 1] || points[i - 1].isNull) + ) { + highAreaPoints.push({ + plotX: point.plotX, + plotY: point.plotY, + doCurve: false // #5186, gaps in areasplinerange fill + }); + } + } + + // Get the paths + lowerPath = getGraphPath.call(this, points); + if (step) { + if (step === true) { + step = 'left'; + } + options.step = { + left: 'right', + center: 'center', + right: 'left' + }[step]; // swap for reading in getGraphPath + } + higherPath = getGraphPath.call(this, highPoints); + higherAreaPath = getGraphPath.call(this, highAreaPoints); + options.step = step; + + // Create a line on both top and bottom of the range + linePath = [].concat(lowerPath, higherPath); + + // For the area path, we need to change the 'move' statement + // into 'lineTo' or 'curveTo' + if (!this.chart.polar && higherAreaPath[0] === 'M') { + higherAreaPath[0] = 'L'; // this probably doesn't work for spline + } + + this.graphPath = linePath; + this.areaPath = lowerPath.concat(higherAreaPath); + + // Prepare for sideways animation + linePath.isArea = true; + linePath.xMap = lowerPath.xMap; + this.areaPath.xMap = lowerPath.xMap; + + return linePath; + }, + + /** + * Extend the basic drawDataLabels method by running it for both lower + * and higher values. + */ + drawDataLabels: function () { + + var data = this.data, + length = data.length, + i, + originalDataLabels = [], + dataLabelOptions = this.options.dataLabels, + align = dataLabelOptions.align, + verticalAlign = dataLabelOptions.verticalAlign, + inside = dataLabelOptions.inside, + point, + up, + inverted = this.chart.inverted; + + if (dataLabelOptions.enabled || this._hasPointLabels) { + + // Step 1: set preliminary values for plotY and dataLabel + // and draw the upper labels + i = length; + while (i--) { + point = data[i]; + if (point) { + up = inside ? + point.plotHigh < point.plotLow : + point.plotHigh > point.plotLow; + + // Set preliminary values + point.y = point.high; + point._plotY = point.plotY; + point.plotY = point.plotHigh; + + // Store original data labels and set preliminary label + // objects to be picked up in the uber method + originalDataLabels[i] = point.dataLabel; + point.dataLabel = point.dataLabelUpper; + + // Set the default offset + point.below = up; + if (inverted) { + if (!align) { + dataLabelOptions.align = up ? 'right' : 'left'; + } + } else { + if (!verticalAlign) { + dataLabelOptions.verticalAlign = up ? + 'top' : + 'bottom'; + } + } + + dataLabelOptions.x = dataLabelOptions.xHigh; + dataLabelOptions.y = dataLabelOptions.yHigh; + } + } + + if (seriesProto.drawDataLabels) { + seriesProto.drawDataLabels.apply(this, arguments); // #1209 + } + + // Step 2: reorganize and handle data labels for the lower values + i = length; + while (i--) { + point = data[i]; + if (point) { + up = inside ? + point.plotHigh < point.plotLow : + point.plotHigh > point.plotLow; + + // Move the generated labels from step 1, and reassign + // the original data labels + point.dataLabelUpper = point.dataLabel; + point.dataLabel = originalDataLabels[i]; + + // Reset values + point.y = point.low; + point.plotY = point._plotY; + + // Set the default offset + point.below = !up; + if (inverted) { + if (!align) { + dataLabelOptions.align = up ? 'left' : 'right'; + } + } else { + if (!verticalAlign) { + dataLabelOptions.verticalAlign = up ? + 'bottom' : + 'top'; + } + + } + + dataLabelOptions.x = dataLabelOptions.xLow; + dataLabelOptions.y = dataLabelOptions.yLow; + } + } + if (seriesProto.drawDataLabels) { + seriesProto.drawDataLabels.apply(this, arguments); + } + } + + dataLabelOptions.align = align; + dataLabelOptions.verticalAlign = verticalAlign; + }, + + alignDataLabel: function () { + seriesTypes.column.prototype.alignDataLabel.apply(this, arguments); + }, + + drawPoints: function () { + var series = this, + pointLength = series.points.length, + point, + i; + + // Draw bottom points + seriesProto.drawPoints.apply(series, arguments); + + i = 0; + while (i < pointLength) { + point = series.points[i]; + point.lowerGraphic = point.graphic; + point.graphic = point.upperGraphic; + point._plotY = point.plotY; + point._plotX = point.plotX; + point.plotY = point.plotHigh; + if (defined(point.plotHighX)) { + point.plotX = point.plotHighX; + } + point._isInside = point.isInside; + if (!series.chart.polar) { + point.isInside = point.isTopInside = ( + point.plotY !== undefined && + point.plotY >= 0 && + point.plotY <= series.yAxis.len && // #3519 + point.plotX >= 0 && + point.plotX <= series.xAxis.len + ); + } + i++; + } + + // Draw top points + seriesProto.drawPoints.apply(series, arguments); + + i = 0; + while (i < pointLength) { + point = series.points[i]; + point.upperGraphic = point.graphic; + point.graphic = point.lowerGraphic; + point.isInside = point._isInside; + point.plotY = point._plotY; + point.plotX = point._plotX; + i++; + } + }, + + setStackedPoints: noop }, { - setState: function () { - var prevState = this.state, - series = this.series, - isPolar = series.chart.polar; - - - if (!defined(this.plotHigh)) { - // Boost doesn't calculate plotHigh - this.plotHigh = series.yAxis.toPixels(this.high, true); - } - - if (!defined(this.plotLow)) { - // Boost doesn't calculate plotLow - this.plotLow = this.plotY = series.yAxis.toPixels(this.low, true); - } - - if (series.stateMarkerGraphic) { - series.lowerStateMarkerGraphic = series.stateMarkerGraphic; - series.stateMarkerGraphic = series.upperStateMarkerGraphic; - } - - // Change state also for the top marker - this.graphic = this.upperGraphic; - this.plotY = this.plotHigh; - - if (isPolar) { - this.plotX = this.plotHighX; - } - - // Top state: - pointProto.setState.apply(this, arguments); - - this.state = prevState; - - // Now restore defaults - this.plotY = this.plotLow; - this.graphic = this.lowerGraphic; - - if (isPolar) { - this.plotX = this.plotLowX; - } - - if (series.stateMarkerGraphic) { - series.upperStateMarkerGraphic = series.stateMarkerGraphic; - series.stateMarkerGraphic = series.lowerStateMarkerGraphic; - // Lower marker is stored at stateMarkerGraphic - // to avoid reference duplication (#7021) - series.lowerStateMarkerGraphic = undefined; - } - - pointProto.setState.apply(this, arguments); - - }, - haloPath: function () { - var isPolar = this.series.chart.polar, - path = []; - - // Bottom halo - this.plotY = this.plotLow; - if (isPolar) { - this.plotX = this.plotLowX; - } - - if (this.isInside) { - path = pointProto.haloPath.apply(this, arguments); - } - - // Top halo - this.plotY = this.plotHigh; - if (isPolar) { - this.plotX = this.plotHighX; - } - if (this.isTopInside) { - path = path.concat( - pointProto.haloPath.apply(this, arguments) - ); - } - - return path; - }, - destroyElements: function () { - var graphics = ['lowerGraphic', 'upperGraphic']; - - each(graphics, function (graphicName) { - if (this[graphicName]) { - this[graphicName] = this[graphicName].destroy(); - } - }, this); - - // Clear graphic for states, removed in the above each: - this.graphic = null; - - return pointProto.destroyElements.apply(this, arguments); - } + setState: function () { + var prevState = this.state, + series = this.series, + isPolar = series.chart.polar; + + + if (!defined(this.plotHigh)) { + // Boost doesn't calculate plotHigh + this.plotHigh = series.yAxis.toPixels(this.high, true); + } + + if (!defined(this.plotLow)) { + // Boost doesn't calculate plotLow + this.plotLow = this.plotY = series.yAxis.toPixels(this.low, true); + } + + if (series.stateMarkerGraphic) { + series.lowerStateMarkerGraphic = series.stateMarkerGraphic; + series.stateMarkerGraphic = series.upperStateMarkerGraphic; + } + + // Change state also for the top marker + this.graphic = this.upperGraphic; + this.plotY = this.plotHigh; + + if (isPolar) { + this.plotX = this.plotHighX; + } + + // Top state: + pointProto.setState.apply(this, arguments); + + this.state = prevState; + + // Now restore defaults + this.plotY = this.plotLow; + this.graphic = this.lowerGraphic; + + if (isPolar) { + this.plotX = this.plotLowX; + } + + if (series.stateMarkerGraphic) { + series.upperStateMarkerGraphic = series.stateMarkerGraphic; + series.stateMarkerGraphic = series.lowerStateMarkerGraphic; + // Lower marker is stored at stateMarkerGraphic + // to avoid reference duplication (#7021) + series.lowerStateMarkerGraphic = undefined; + } + + pointProto.setState.apply(this, arguments); + + }, + haloPath: function () { + var isPolar = this.series.chart.polar, + path = []; + + // Bottom halo + this.plotY = this.plotLow; + if (isPolar) { + this.plotX = this.plotLowX; + } + + if (this.isInside) { + path = pointProto.haloPath.apply(this, arguments); + } + + // Top halo + this.plotY = this.plotHigh; + if (isPolar) { + this.plotX = this.plotHighX; + } + if (this.isTopInside) { + path = path.concat( + pointProto.haloPath.apply(this, arguments) + ); + } + + return path; + }, + destroyElements: function () { + var graphics = ['lowerGraphic', 'upperGraphic']; + + each(graphics, function (graphicName) { + if (this[graphicName]) { + this[graphicName] = this[graphicName].destroy(); + } + }, this); + + // Clear graphic for states, removed in the above each: + this.graphic = null; + + return pointProto.destroyElements.apply(this, arguments); + } }); /** * A `arearange` series. If the [type](#series.arearange.type) option * is not specified, it is inherited from [chart.type](#chart.type). - * - * + * + * * @type {Object} * @extends series,plotOptions.arearange * @excluding dataParser,dataURL,stack,stacking @@ -600,7 +600,7 @@ seriesType('arearange', 'area', { /** * An array of data points for the series. For the `arearange` series * type, points can be given in the following ways: - * + * * 1. An array of arrays with 3 or 2 values. In this case, the values * correspond to `x,low,high`. If the first value is a string, it is * applied as the name of the point, and the `x` value is inferred. @@ -608,7 +608,7 @@ seriesType('arearange', 'area', { * should be of length 2\. 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, 8, 3], @@ -616,13 +616,13 @@ seriesType('arearange', 'area', { * [2, 6, 8] * ] * ``` - * + * * 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.arearange.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -638,7 +638,7 @@ seriesType('arearange', 'area', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding marker,y @@ -658,7 +658,7 @@ seriesType('arearange', 'area', { /** * The high or maximum value for each data point. - * + * * @type {Number} * @product highcharts highstock * @apioption series.arearange.data.high @@ -666,7 +666,7 @@ seriesType('arearange', 'area', { /** * The low or minimum value for each data point. - * + * * @type {Number} * @product highcharts highstock * @apioption series.arearange.data.low diff --git a/js/parts-more/AreaSplineRangeSeries.js b/js/parts-more/AreaSplineRangeSeries.js index d123c4236a8..583e708d8d5 100644 --- a/js/parts-more/AreaSplineRangeSeries.js +++ b/js/parts-more/AreaSplineRangeSeries.js @@ -9,14 +9,14 @@ import '../parts/Utilities.js'; import '../parts/Options.js'; var seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + seriesTypes = H.seriesTypes; /** * The area spline range is a cartesian series type with higher and * lower Y values along an X axis. The area inside the range is colored, and * the graph outlining the area is a smoothed spline. Requires * `highcharts-more.js`. - * + * * @extends plotOptions.arearange * @excluding step * @since 2.3.0 @@ -26,13 +26,13 @@ var seriesType = H.seriesType, * @apioption plotOptions.areasplinerange */ seriesType('areasplinerange', 'arearange', null, { - getPointSpline: seriesTypes.spline.prototype.getPointSpline + getPointSpline: seriesTypes.spline.prototype.getPointSpline }); /** * A `areasplinerange` series. If the [type](#series.areasplinerange.type) * option is not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.areasplinerange * @excluding dataParser,dataURL,stack @@ -43,7 +43,7 @@ seriesType('areasplinerange', 'arearange', null, { /** * An array of data points for the series. For the `areasplinerange` * series type, points can be given in the following ways: - * + * * 1. An array of arrays with 3 or 2 values. In this case, the values * correspond to `x,low,high`. If the first value is a string, it is * applied as the name of the point, and the `x` value is inferred. @@ -51,7 +51,7 @@ seriesType('areasplinerange', 'arearange', null, { * should be of length 2\. 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, 0, 5], @@ -59,12 +59,12 @@ seriesType('areasplinerange', 'arearange', null, { * [2, 5, 2] * ] * ``` - * + * * 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.areasplinerange.turboThreshold), this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -80,7 +80,7 @@ seriesType('areasplinerange', 'arearange', null, { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.arearange.data * @sample {highcharts} highcharts/chart/reflow-true/ diff --git a/js/parts-more/BoxPlotSeries.js b/js/parts-more/BoxPlotSeries.js index 9f8680a9467..fb375cddf21 100644 --- a/js/parts-more/BoxPlotSeries.js +++ b/js/parts-more/BoxPlotSeries.js @@ -3,16 +3,16 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Options.js'; var each = H.each, - noop = H.noop, - pick = H.pick, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + noop = H.noop, + pick = H.pick, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; /** * The boxplot series type. @@ -26,7 +26,7 @@ var each = H.each, * five-number summaries: the smallest observation (sample minimum), lower * quartile (Q1), median (Q2), upper quartile (Q3), and largest observation * (sample maximum). - * + * * @sample highcharts/demo/box-plot/ Box plot * @extends {plotOptions.column} * @product highcharts @@ -35,484 +35,484 @@ var each = H.each, */ seriesType('boxplot', 'column', { - threshold: null, - - tooltip: { - /*= if (!build.classic) { =*/ - pointFormat: '' + - '\u25CF {series.name}
' + - 'Maximum: {point.high}
' + - 'Upper quartile: {point.q3}
' + - 'Median: {point.median}
' + - 'Lower quartile: {point.q1}
' + - 'Minimum: {point.low}
', - /*= } else { =*/ - - pointFormat: // eslint-disable-line no-dupe-keys - '\u25CF ' + - '{series.name}
' + - 'Maximum: {point.high}
' + - 'Upper quartile: {point.q3}
' + - 'Median: {point.median}
' + - 'Lower quartile: {point.q1}
' + - 'Minimum: {point.low}
' - /*= } =*/ - }, - - /** - * The length of the whiskers, the horizontal lines marking low and - * high values. It can be a numerical pixel value, or a percentage - * value of the box width. Set `0` to disable whiskers. - * - * @type {Number|String} - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * True by default - * @since 3.0 - * @product highcharts - */ - whiskerLength: '50%', - /*= if (build.classic) { =*/ - - /** - * The fill color of the box. - * - * In styled mode, the fill color can be set with the - * `.highcharts-boxplot-box` class. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @default #ffffff - * @since 3.0 - * @product highcharts - */ - fillColor: '${palette.backgroundColor}', - - /** - * The width of the line surrounding the box. If any of - * [stemWidth](#plotOptions.boxplot.stemWidth), - * [medianWidth](#plotOptions.boxplot.medianWidth) - * or [whiskerWidth](#plotOptions.boxplot.whiskerWidth) are `null`, - * the lineWidth also applies to these lines. - * - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ - * Error bar styling - * @since 3.0 - * @product highcharts - */ - lineWidth: 1, - - /** - * The color of the median line. If `null`, the general series color - * applies. - * - * In styled mode, the median stroke width can be set with the - * `.highcharts-boxplot-median` class. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/css/boxplot/ - * Box plot in styled mode - * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ - * Error bar styling - * @default null - * @since 3.0 - * @product highcharts - * @apioption plotOptions.boxplot.medianColor - */ - - /** - * The pixel width of the median line. If `null`, the - * [lineWidth](#plotOptions.boxplot.lineWidth) is used. - * - * In styled mode, the median stroke width can be set with the - * `.highcharts-boxplot-median` class. - * - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/css/boxplot/ - * Box plot in styled mode - * @since 3.0 - * @product highcharts - */ - medianWidth: 2, - - /* - // States are not working and are removed from docs. - // Refer to: #2340 - states: { - hover: { - brightness: -0.3 - } - }, - */ - - /** - * The color of the stem, the vertical line extending from the box to - * the whiskers. If `null`, the series color is used. - * - * In styled mode, the stem stroke can be set with the - * `.highcharts-boxplot-stem` class. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/css/boxplot/ - * Box plot in styled mode - * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ - * Error bar styling - * @default null - * @since 3.0 - * @product highcharts - * @apioption plotOptions.boxplot.stemColor - */ - - /** - * The dash style of the stem, the vertical line extending from the - * box to the whiskers. - * - * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", - * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", "DashDot", - * "LongDashDot", "LongDashDotDot"] - * @type {String} - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/css/boxplot/ - * Box plot in styled mode - * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ - * Error bar styling - * @default Solid - * @since 3.0 - * @product highcharts - * @apioption plotOptions.boxplot.stemDashStyle - */ - - /** - * The width of the stem, the vertical line extending from the box to - * the whiskers. If `null`, the width is inherited from the - * [lineWidth](#plotOptions.boxplot.lineWidth) option. - * - * In styled mode, the stem stroke width can be set with the - * `.highcharts-boxplot-stem` class. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/css/boxplot/ - * Box plot in styled mode - * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ - * Error bar styling - * @default null - * @since 3.0 - * @product highcharts - * @apioption plotOptions.boxplot.stemWidth - */ - - /** - * The color of the whiskers, the horizontal lines marking low and high - * values. When `null`, the general series color is used. - * - * In styled mode, the whisker stroke can be set with the - * `.highcharts-boxplot-whisker` class . - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/css/boxplot/ - * Box plot in styled mode - * @default null - * @since 3.0 - * @product highcharts - * @apioption plotOptions.boxplot.whiskerColor - */ - - /** - * The line width of the whiskers, the horizontal lines marking low and - * high values. When `null`, the general - * [lineWidth](#plotOptions.boxplot.lineWidth) applies. - * - * In styled mode, the whisker stroke width can be set with the - * `.highcharts-boxplot-whisker` class. - * - * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ - * Box plot styling - * @sample {highcharts} highcharts/css/boxplot/ - * Box plot in styled mode - * @since 3.0 - * @product highcharts - */ - whiskerWidth: 2 - /*= } =*/ + threshold: null, + + tooltip: { + /*= if (!build.classic) { =*/ + pointFormat: '' + + '\u25CF {series.name}
' + + 'Maximum: {point.high}
' + + 'Upper quartile: {point.q3}
' + + 'Median: {point.median}
' + + 'Lower quartile: {point.q1}
' + + 'Minimum: {point.low}
', + /*= } else { =*/ + + pointFormat: // eslint-disable-line no-dupe-keys + '\u25CF ' + + '{series.name}
' + + 'Maximum: {point.high}
' + + 'Upper quartile: {point.q3}
' + + 'Median: {point.median}
' + + 'Lower quartile: {point.q1}
' + + 'Minimum: {point.low}
' + /*= } =*/ + }, + + /** + * The length of the whiskers, the horizontal lines marking low and + * high values. It can be a numerical pixel value, or a percentage + * value of the box width. Set `0` to disable whiskers. + * + * @type {Number|String} + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * True by default + * @since 3.0 + * @product highcharts + */ + whiskerLength: '50%', + /*= if (build.classic) { =*/ + + /** + * The fill color of the box. + * + * In styled mode, the fill color can be set with the + * `.highcharts-boxplot-box` class. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @default #ffffff + * @since 3.0 + * @product highcharts + */ + fillColor: '${palette.backgroundColor}', + + /** + * The width of the line surrounding the box. If any of + * [stemWidth](#plotOptions.boxplot.stemWidth), + * [medianWidth](#plotOptions.boxplot.medianWidth) + * or [whiskerWidth](#plotOptions.boxplot.whiskerWidth) are `null`, + * the lineWidth also applies to these lines. + * + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ + * Error bar styling + * @since 3.0 + * @product highcharts + */ + lineWidth: 1, + + /** + * The color of the median line. If `null`, the general series color + * applies. + * + * In styled mode, the median stroke width can be set with the + * `.highcharts-boxplot-median` class. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/css/boxplot/ + * Box plot in styled mode + * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ + * Error bar styling + * @default null + * @since 3.0 + * @product highcharts + * @apioption plotOptions.boxplot.medianColor + */ + + /** + * The pixel width of the median line. If `null`, the + * [lineWidth](#plotOptions.boxplot.lineWidth) is used. + * + * In styled mode, the median stroke width can be set with the + * `.highcharts-boxplot-median` class. + * + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/css/boxplot/ + * Box plot in styled mode + * @since 3.0 + * @product highcharts + */ + medianWidth: 2, + + /* + // States are not working and are removed from docs. + // Refer to: #2340 + states: { + hover: { + brightness: -0.3 + } + }, + */ + + /** + * The color of the stem, the vertical line extending from the box to + * the whiskers. If `null`, the series color is used. + * + * In styled mode, the stem stroke can be set with the + * `.highcharts-boxplot-stem` class. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/css/boxplot/ + * Box plot in styled mode + * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ + * Error bar styling + * @default null + * @since 3.0 + * @product highcharts + * @apioption plotOptions.boxplot.stemColor + */ + + /** + * The dash style of the stem, the vertical line extending from the + * box to the whiskers. + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", "DashDot", + * "LongDashDot", "LongDashDotDot"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/css/boxplot/ + * Box plot in styled mode + * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ + * Error bar styling + * @default Solid + * @since 3.0 + * @product highcharts + * @apioption plotOptions.boxplot.stemDashStyle + */ + + /** + * The width of the stem, the vertical line extending from the box to + * the whiskers. If `null`, the width is inherited from the + * [lineWidth](#plotOptions.boxplot.lineWidth) option. + * + * In styled mode, the stem stroke width can be set with the + * `.highcharts-boxplot-stem` class. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/css/boxplot/ + * Box plot in styled mode + * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ + * Error bar styling + * @default null + * @since 3.0 + * @product highcharts + * @apioption plotOptions.boxplot.stemWidth + */ + + /** + * The color of the whiskers, the horizontal lines marking low and high + * values. When `null`, the general series color is used. + * + * In styled mode, the whisker stroke can be set with the + * `.highcharts-boxplot-whisker` class . + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/css/boxplot/ + * Box plot in styled mode + * @default null + * @since 3.0 + * @product highcharts + * @apioption plotOptions.boxplot.whiskerColor + */ + + /** + * The line width of the whiskers, the horizontal lines marking low and + * high values. When `null`, the general + * [lineWidth](#plotOptions.boxplot.lineWidth) applies. + * + * In styled mode, the whisker stroke width can be set with the + * `.highcharts-boxplot-whisker` class. + * + * @sample {highcharts} highcharts/plotoptions/box-plot-styling/ + * Box plot styling + * @sample {highcharts} highcharts/css/boxplot/ + * Box plot in styled mode + * @since 3.0 + * @product highcharts + */ + whiskerWidth: 2 + /*= } =*/ }, /** @lends seriesTypes.boxplot */ { - // array point configs are mapped to this - pointArrayMap: ['low', 'q1', 'median', 'q3', 'high'], - toYData: function (point) { // return a plain array for speedy calculation - return [point.low, point.q1, point.median, point.q3, point.high]; - }, - - // defines the top of the tracker - pointValKey: 'high', - - /*= if (build.classic) { =*/ - /** - * Get presentational attributes - */ - pointAttribs: function () { - // No attributes should be set on point.graphic which is the group - return {}; - }, - /*= } =*/ - - /** - * Disable data labels for box plot - */ - drawDataLabels: noop, - - /** - * Translate data points from raw values x and y to plotX and plotY - */ - translate: function () { - var series = this, - yAxis = series.yAxis, - pointArrayMap = series.pointArrayMap; - - seriesTypes.column.prototype.translate.apply(series); - - // do the translation on each point dimension - each(series.points, function (point) { - each(pointArrayMap, function (key) { - if (point[key] !== null) { - point[key + 'Plot'] = yAxis.translate( - point[key], 0, 1, 0, 1 - ); - } - }); - }); - }, - - /** - * Draw the data points - */ - drawPoints: function () { - var series = this, - points = series.points, - options = series.options, - chart = series.chart, - renderer = chart.renderer, - q1Plot, - q3Plot, - highPlot, - lowPlot, - medianPlot, - medianPath, - crispCorr, - crispX = 0, - boxPath, - width, - left, - right, - halfWidth, - // error bar inherits this series type but doesn't do quartiles - doQuartiles = series.doQuartiles !== false, - pointWiskerLength, - whiskerLength = series.options.whiskerLength; - - - each(points, function (point) { - - var graphic = point.graphic, - verb = graphic ? 'animate' : 'attr', - shapeArgs = point.shapeArgs; // the box - - /*= if (build.classic) { =*/ - var boxAttr = {}, - stemAttr = {}, - whiskersAttr = {}, - medianAttr = {}, - color = point.color || series.color; - /*= } =*/ - - if (point.plotY !== undefined) { - - // crisp vector coordinates - width = shapeArgs.width; - left = Math.floor(shapeArgs.x); - right = left + width; - halfWidth = Math.round(width / 2); - q1Plot = Math.floor(doQuartiles ? point.q1Plot : point.lowPlot); - q3Plot = Math.floor(doQuartiles ? point.q3Plot : point.lowPlot); - highPlot = Math.floor(point.highPlot); - lowPlot = Math.floor(point.lowPlot); - - if (!graphic) { - point.graphic = graphic = renderer.g('point') - .add(series.group); - - point.stem = renderer.path() - .addClass('highcharts-boxplot-stem') - .add(graphic); - - if (whiskerLength) { - point.whiskers = renderer.path() - .addClass('highcharts-boxplot-whisker') - .add(graphic); - } - if (doQuartiles) { - point.box = renderer.path(boxPath) - .addClass('highcharts-boxplot-box') - .add(graphic); - } - point.medianShape = renderer.path(medianPath) - .addClass('highcharts-boxplot-median') - .add(graphic); - } - - /*= if (build.classic) { =*/ - - // Stem attributes - stemAttr.stroke = point.stemColor || options.stemColor || color; - stemAttr['stroke-width'] = pick( - point.stemWidth, - options.stemWidth, - options.lineWidth - ); - stemAttr.dashstyle = - point.stemDashStyle || options.stemDashStyle; - point.stem.attr(stemAttr); - - // Whiskers attributes - if (whiskerLength) { - whiskersAttr.stroke = - point.whiskerColor || options.whiskerColor || color; - whiskersAttr['stroke-width'] = pick( - point.whiskerWidth, - options.whiskerWidth, - options.lineWidth - ); - point.whiskers.attr(whiskersAttr); - } - - if (doQuartiles) { - boxAttr.fill = ( - point.fillColor || - options.fillColor || - color - ); - boxAttr.stroke = options.lineColor || color; - boxAttr['stroke-width'] = options.lineWidth || 0; - point.box.attr(boxAttr); - } - - - // Median attributes - medianAttr.stroke = - point.medianColor || options.medianColor || color; - medianAttr['stroke-width'] = pick( - point.medianWidth, - options.medianWidth, - options.lineWidth - ); - point.medianShape.attr(medianAttr); - - /*= } =*/ - - - // The stem - crispCorr = (point.stem.strokeWidth() % 2) / 2; - crispX = left + halfWidth + crispCorr; - point.stem[verb]({ d: [ - // stem up - 'M', - crispX, q3Plot, - 'L', - crispX, highPlot, - - // stem down - 'M', - crispX, q1Plot, - 'L', - crispX, lowPlot - ] }); - - // The box - if (doQuartiles) { - crispCorr = (point.box.strokeWidth() % 2) / 2; - q1Plot = Math.floor(q1Plot) + crispCorr; - q3Plot = Math.floor(q3Plot) + crispCorr; - left += crispCorr; - right += crispCorr; - point.box[verb]({ d: [ - 'M', - left, q3Plot, - 'L', - left, q1Plot, - 'L', - right, q1Plot, - 'L', - right, q3Plot, - 'L', - left, q3Plot, - 'z' - ] }); - } - - // The whiskers - if (whiskerLength) { - crispCorr = (point.whiskers.strokeWidth() % 2) / 2; - highPlot = highPlot + crispCorr; - lowPlot = lowPlot + crispCorr; - pointWiskerLength = (/%$/).test(whiskerLength) ? - halfWidth * parseFloat(whiskerLength) / 100 : - whiskerLength / 2; - point.whiskers[verb]({ d: [ - // High whisker - 'M', - crispX - pointWiskerLength, - highPlot, - 'L', - crispX + pointWiskerLength, - highPlot, - - // Low whisker - 'M', - crispX - pointWiskerLength, - lowPlot, - 'L', - crispX + pointWiskerLength, - lowPlot - ] }); - } - - // The median - medianPlot = Math.round(point.medianPlot); - crispCorr = (point.medianShape.strokeWidth() % 2) / 2; - medianPlot = medianPlot + crispCorr; - - point.medianShape[verb]({ d: [ - 'M', - left, - medianPlot, - 'L', - right, - medianPlot - ] }); - } - }); - - }, - setStackedPoints: noop // #3890 + // array point configs are mapped to this + pointArrayMap: ['low', 'q1', 'median', 'q3', 'high'], + toYData: function (point) { // return a plain array for speedy calculation + return [point.low, point.q1, point.median, point.q3, point.high]; + }, + + // defines the top of the tracker + pointValKey: 'high', + + /*= if (build.classic) { =*/ + /** + * Get presentational attributes + */ + pointAttribs: function () { + // No attributes should be set on point.graphic which is the group + return {}; + }, + /*= } =*/ + + /** + * Disable data labels for box plot + */ + drawDataLabels: noop, + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + pointArrayMap = series.pointArrayMap; + + seriesTypes.column.prototype.translate.apply(series); + + // do the translation on each point dimension + each(series.points, function (point) { + each(pointArrayMap, function (key) { + if (point[key] !== null) { + point[key + 'Plot'] = yAxis.translate( + point[key], 0, 1, 0, 1 + ); + } + }); + }); + }, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + points = series.points, + options = series.options, + chart = series.chart, + renderer = chart.renderer, + q1Plot, + q3Plot, + highPlot, + lowPlot, + medianPlot, + medianPath, + crispCorr, + crispX = 0, + boxPath, + width, + left, + right, + halfWidth, + // error bar inherits this series type but doesn't do quartiles + doQuartiles = series.doQuartiles !== false, + pointWiskerLength, + whiskerLength = series.options.whiskerLength; + + + each(points, function (point) { + + var graphic = point.graphic, + verb = graphic ? 'animate' : 'attr', + shapeArgs = point.shapeArgs; // the box + + /*= if (build.classic) { =*/ + var boxAttr = {}, + stemAttr = {}, + whiskersAttr = {}, + medianAttr = {}, + color = point.color || series.color; + /*= } =*/ + + if (point.plotY !== undefined) { + + // crisp vector coordinates + width = shapeArgs.width; + left = Math.floor(shapeArgs.x); + right = left + width; + halfWidth = Math.round(width / 2); + q1Plot = Math.floor(doQuartiles ? point.q1Plot : point.lowPlot); + q3Plot = Math.floor(doQuartiles ? point.q3Plot : point.lowPlot); + highPlot = Math.floor(point.highPlot); + lowPlot = Math.floor(point.lowPlot); + + if (!graphic) { + point.graphic = graphic = renderer.g('point') + .add(series.group); + + point.stem = renderer.path() + .addClass('highcharts-boxplot-stem') + .add(graphic); + + if (whiskerLength) { + point.whiskers = renderer.path() + .addClass('highcharts-boxplot-whisker') + .add(graphic); + } + if (doQuartiles) { + point.box = renderer.path(boxPath) + .addClass('highcharts-boxplot-box') + .add(graphic); + } + point.medianShape = renderer.path(medianPath) + .addClass('highcharts-boxplot-median') + .add(graphic); + } + + /*= if (build.classic) { =*/ + + // Stem attributes + stemAttr.stroke = point.stemColor || options.stemColor || color; + stemAttr['stroke-width'] = pick( + point.stemWidth, + options.stemWidth, + options.lineWidth + ); + stemAttr.dashstyle = + point.stemDashStyle || options.stemDashStyle; + point.stem.attr(stemAttr); + + // Whiskers attributes + if (whiskerLength) { + whiskersAttr.stroke = + point.whiskerColor || options.whiskerColor || color; + whiskersAttr['stroke-width'] = pick( + point.whiskerWidth, + options.whiskerWidth, + options.lineWidth + ); + point.whiskers.attr(whiskersAttr); + } + + if (doQuartiles) { + boxAttr.fill = ( + point.fillColor || + options.fillColor || + color + ); + boxAttr.stroke = options.lineColor || color; + boxAttr['stroke-width'] = options.lineWidth || 0; + point.box.attr(boxAttr); + } + + + // Median attributes + medianAttr.stroke = + point.medianColor || options.medianColor || color; + medianAttr['stroke-width'] = pick( + point.medianWidth, + options.medianWidth, + options.lineWidth + ); + point.medianShape.attr(medianAttr); + + /*= } =*/ + + + // The stem + crispCorr = (point.stem.strokeWidth() % 2) / 2; + crispX = left + halfWidth + crispCorr; + point.stem[verb]({ d: [ + // stem up + 'M', + crispX, q3Plot, + 'L', + crispX, highPlot, + + // stem down + 'M', + crispX, q1Plot, + 'L', + crispX, lowPlot + ] }); + + // The box + if (doQuartiles) { + crispCorr = (point.box.strokeWidth() % 2) / 2; + q1Plot = Math.floor(q1Plot) + crispCorr; + q3Plot = Math.floor(q3Plot) + crispCorr; + left += crispCorr; + right += crispCorr; + point.box[verb]({ d: [ + 'M', + left, q3Plot, + 'L', + left, q1Plot, + 'L', + right, q1Plot, + 'L', + right, q3Plot, + 'L', + left, q3Plot, + 'z' + ] }); + } + + // The whiskers + if (whiskerLength) { + crispCorr = (point.whiskers.strokeWidth() % 2) / 2; + highPlot = highPlot + crispCorr; + lowPlot = lowPlot + crispCorr; + pointWiskerLength = (/%$/).test(whiskerLength) ? + halfWidth * parseFloat(whiskerLength) / 100 : + whiskerLength / 2; + point.whiskers[verb]({ d: [ + // High whisker + 'M', + crispX - pointWiskerLength, + highPlot, + 'L', + crispX + pointWiskerLength, + highPlot, + + // Low whisker + 'M', + crispX - pointWiskerLength, + lowPlot, + 'L', + crispX + pointWiskerLength, + lowPlot + ] }); + } + + // The median + medianPlot = Math.round(point.medianPlot); + crispCorr = (point.medianShape.strokeWidth() % 2) / 2; + medianPlot = medianPlot + crispCorr; + + point.medianShape[verb]({ d: [ + 'M', + left, + medianPlot, + 'L', + right, + medianPlot + ] }); + } + }); + + }, + setStackedPoints: noop // #3890 }); /** * A `boxplot` series. If the [type](#series.boxplot.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.boxplot * @excluding dataParser,dataURL,marker,stack,stacking,states @@ -523,7 +523,7 @@ seriesType('boxplot', 'column', { /** * An array of data points for the series. For the `boxplot` series * type, points can be given in the following ways: - * + * * 1. An array of arrays with 6 or 5 values. In this case, the values * correspond to `x,low,q1,median,q3,high`. If the first value is a * string, it is applied as the name of the point, and the `x` value @@ -531,7 +531,7 @@ seriesType('boxplot', 'column', { * inner arrays should be of length 5\. 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, 3, 0, 10, 3, 5], @@ -539,12 +539,12 @@ seriesType('boxplot', 'column', { * [2, 6, 9, 5, 1, 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.boxplot.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -566,7 +566,7 @@ seriesType('boxplot', 'column', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding marker @@ -587,7 +587,7 @@ seriesType('boxplot', 'column', { /** * The `high` value for each data point, signifying the highest value * in the sample set. The top whisker is drawn here. - * + * * @type {Number} * @product highcharts * @apioption series.boxplot.data.high @@ -596,7 +596,7 @@ seriesType('boxplot', 'column', { /** * The `low` value for each data point, signifying the lowest value * in the sample set. The bottom whisker is drawn here. - * + * * @type {Number} * @product highcharts * @apioption series.boxplot.data.low @@ -605,7 +605,7 @@ seriesType('boxplot', 'column', { /** * The median for each data point. This is drawn as a line through the * middle area of the box. - * + * * @type {Number} * @product highcharts * @apioption series.boxplot.data.median @@ -614,7 +614,7 @@ seriesType('boxplot', 'column', { /** * The lower quartile for each data point. This is the bottom of the * box. - * + * * @type {Number} * @product highcharts * @apioption series.boxplot.data.q1 @@ -622,7 +622,7 @@ seriesType('boxplot', 'column', { /** * The higher quartile for each data point. This is the top of the box. - * + * * @type {Number} * @product highcharts * @apioption series.boxplot.data.q3 diff --git a/js/parts-more/BubbleSeries.js b/js/parts-more/BubbleSeries.js index 5cccabbe44c..06c65fc9133 100644 --- a/js/parts-more/BubbleSeries.js +++ b/js/parts-more/BubbleSeries.js @@ -13,18 +13,18 @@ import '../parts/Point.js'; import '../parts/Series.js'; import '../parts/ScatterSeries.js'; var arrayMax = H.arrayMax, - arrayMin = H.arrayMin, - Axis = H.Axis, - color = H.color, - each = H.each, - isNumber = H.isNumber, - noop = H.noop, - pick = H.pick, - pInt = H.pInt, - Point = H.Point, - Series = H.Series, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + arrayMin = H.arrayMin, + Axis = H.Axis, + color = H.color, + each = H.each, + isNumber = H.isNumber, + noop = H.noop, + pick = H.pick, + pInt = H.pInt, + Point = H.Point, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; /** @@ -40,393 +40,393 @@ var arrayMax = H.arrayMax, */ seriesType('bubble', 'scatter', { - dataLabels: { - formatter: function () { // #2945 - return this.point.z; - }, - inside: true, - verticalAlign: 'middle' - }, - - /** - * Whether to display negative sized bubbles. The threshold is given - * by the [zThreshold](#plotOptions.bubble.zThreshold) option, and negative - * bubbles can be visualized by setting - * [negativeColor](#plotOptions.bubble.negativeColor). - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/bubble-negative/ - * Negative bubbles - * @default true - * @since 3.0 - * @apioption plotOptions.bubble.displayNegative - */ - - /** - * @extends plotOptions.series.marker - * @excluding enabled,enabledThreshold,height,radius,width - */ - marker: { - /*= if (build.classic) { =*/ - lineColor: null, // inherit from series.color - lineWidth: 1, - - /** - * The fill opacity of the bubble markers. - */ - fillOpacity: 0.5, - /*= } =*/ - /** - * In bubble charts, the radius is overridden and determined based on - * the point's data value. - */ - /** - * @ignore - */ - radius: null, - - states: { - hover: { - radiusPlus: 0 - } - }, - - /** - * A predefined shape or symbol for the marker. Possible values are - * "circle", "square", "diamond", "triangle" and "triangle-down". - * - * Additionally, the URL to a graphic can be given on the 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. - * - * @validvalue ["circle", "square", "diamond", "triangle", - * "triangle-down"] - * @sample {highcharts} highcharts/plotoptions/bubble-symbol/ - * Bubble chart with various symbols - * @sample {highcharts} highcharts/plotoptions/series-marker-symbol/ - * General chart with predefined, graphic and custom markers - * @since 5.0.11 - */ - symbol: 'circle' - }, - - /** - * Minimum bubble size. Bubbles will automatically size between the - * `minSize` and `maxSize` to reflect the `z` value of each bubble. - * Can be either pixels (when no unit is given), or a percentage of - * the smallest one of the plot width and height. - * - * @type {Number|String} - * @sample {highcharts} highcharts/plotoptions/bubble-size/ Bubble size - * @since 3.0 - * @product highcharts highstock - */ - minSize: 8, - - /** - * Maximum bubble size. Bubbles will automatically size between the - * `minSize` and `maxSize` to reflect the `z` value of each bubble. - * Can be either pixels (when no unit is given), or a percentage of - * the smallest one of the plot width and height. - * - * @type {Number|String} - * @sample {highcharts} highcharts/plotoptions/bubble-size/ - * Bubble size - * @since 3.0 - * @product highcharts highstock - */ - maxSize: '20%', - - /** - * When a point's Z value is below the - * [zThreshold](#plotOptions.bubble.zThreshold) setting, this color is used. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/bubble-negative/ - * Negative bubbles - * @default null - * @since 3.0 - * @product highcharts - * @apioption plotOptions.bubble.negativeColor - */ - - /** - * Whether the bubble's value should be represented by the area or the - * width of the bubble. The default, `area`, corresponds best to the - * human perception of the size of each bubble. - * - * @validvalue ["area", "width"] - * @type {String} - * @sample {highcharts} highcharts/plotoptions/bubble-sizeby/ - * Comparison of area and size - * @default area - * @since 3.0.7 - * @apioption plotOptions.bubble.sizeBy - */ - - /** - * When this is true, the absolute value of z determines the size of - * the bubble. This means that with the default `zThreshold` of 0, a - * bubble of value -1 will have the same size as a bubble of value 1, - * while a bubble of value 0 will have a smaller size according to - * `minSize`. - * - * @type {Boolean} - * @sample {highcharts} - * highcharts/plotoptions/bubble-sizebyabsolutevalue/ - * Size by absolute value, various thresholds - * @default false - * @since 4.1.9 - * @product highcharts - * @apioption plotOptions.bubble.sizeByAbsoluteValue - */ - - /** - * When this is true, the series will not cause the Y axis to cross - * the zero plane (or [threshold](#plotOptions.series.threshold) option) - * unless the data actually crosses the plane. - * - * For example, if `softThreshold` is `false`, a series of 0, 1, 2, - * 3 will make the Y axis show negative values according to the `minPadding` - * option. If `softThreshold` is `true`, the Y axis starts at 0. - * - * @since 4.1.9 - * @product highcharts - */ - softThreshold: false, - - states: { - hover: { - halo: { - size: 5 - } - } - }, - - tooltip: { - pointFormat: '({point.x}, {point.y}), Size: {point.z}' - }, - - turboThreshold: 0, - - /** - * The minimum for the Z value range. Defaults to the highest Z value - * in the data. - * - * @type {Number} - * @see [zMin](#plotOptions.bubble.zMin) - * @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/ - * Z has a possible range of 0-100 - * @default null - * @since 4.0.3 - * @product highcharts - * @apioption plotOptions.bubble.zMax - */ - - /** - * The minimum for the Z value range. Defaults to the lowest Z value - * in the data. - * - * @type {Number} - * @see [zMax](#plotOptions.bubble.zMax) - * @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/ - * Z has a possible range of 0-100 - * @default null - * @since 4.0.3 - * @product highcharts - * @apioption plotOptions.bubble.zMin - */ - - /** - * When [displayNegative](#plotOptions.bubble.displayNegative) is `false`, - * bubbles with lower Z values are skipped. When `displayNegative` - * is `true` and a [negativeColor](#plotOptions.bubble.negativeColor) - * is given, points with lower Z is colored. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/bubble-negative/ - * Negative bubbles - * @default 0 - * @since 3.0 - * @product highcharts - */ - zThreshold: 0, - - zoneAxis: 'z' + dataLabels: { + formatter: function () { // #2945 + return this.point.z; + }, + inside: true, + verticalAlign: 'middle' + }, + + /** + * Whether to display negative sized bubbles. The threshold is given + * by the [zThreshold](#plotOptions.bubble.zThreshold) option, and negative + * bubbles can be visualized by setting + * [negativeColor](#plotOptions.bubble.negativeColor). + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/bubble-negative/ + * Negative bubbles + * @default true + * @since 3.0 + * @apioption plotOptions.bubble.displayNegative + */ + + /** + * @extends plotOptions.series.marker + * @excluding enabled,enabledThreshold,height,radius,width + */ + marker: { + /*= if (build.classic) { =*/ + lineColor: null, // inherit from series.color + lineWidth: 1, + + /** + * The fill opacity of the bubble markers. + */ + fillOpacity: 0.5, + /*= } =*/ + /** + * In bubble charts, the radius is overridden and determined based on + * the point's data value. + */ + /** + * @ignore + */ + radius: null, + + states: { + hover: { + radiusPlus: 0 + } + }, + + /** + * A predefined shape or symbol for the marker. Possible values are + * "circle", "square", "diamond", "triangle" and "triangle-down". + * + * Additionally, the URL to a graphic can be given on the 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. + * + * @validvalue ["circle", "square", "diamond", "triangle", + * "triangle-down"] + * @sample {highcharts} highcharts/plotoptions/bubble-symbol/ + * Bubble chart with various symbols + * @sample {highcharts} highcharts/plotoptions/series-marker-symbol/ + * General chart with predefined, graphic and custom markers + * @since 5.0.11 + */ + symbol: 'circle' + }, + + /** + * Minimum bubble size. Bubbles will automatically size between the + * `minSize` and `maxSize` to reflect the `z` value of each bubble. + * Can be either pixels (when no unit is given), or a percentage of + * the smallest one of the plot width and height. + * + * @type {Number|String} + * @sample {highcharts} highcharts/plotoptions/bubble-size/ Bubble size + * @since 3.0 + * @product highcharts highstock + */ + minSize: 8, + + /** + * Maximum bubble size. Bubbles will automatically size between the + * `minSize` and `maxSize` to reflect the `z` value of each bubble. + * Can be either pixels (when no unit is given), or a percentage of + * the smallest one of the plot width and height. + * + * @type {Number|String} + * @sample {highcharts} highcharts/plotoptions/bubble-size/ + * Bubble size + * @since 3.0 + * @product highcharts highstock + */ + maxSize: '20%', + + /** + * When a point's Z value is below the + * [zThreshold](#plotOptions.bubble.zThreshold) setting, this color is used. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/bubble-negative/ + * Negative bubbles + * @default null + * @since 3.0 + * @product highcharts + * @apioption plotOptions.bubble.negativeColor + */ + + /** + * Whether the bubble's value should be represented by the area or the + * width of the bubble. The default, `area`, corresponds best to the + * human perception of the size of each bubble. + * + * @validvalue ["area", "width"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/bubble-sizeby/ + * Comparison of area and size + * @default area + * @since 3.0.7 + * @apioption plotOptions.bubble.sizeBy + */ + + /** + * When this is true, the absolute value of z determines the size of + * the bubble. This means that with the default `zThreshold` of 0, a + * bubble of value -1 will have the same size as a bubble of value 1, + * while a bubble of value 0 will have a smaller size according to + * `minSize`. + * + * @type {Boolean} + * @sample {highcharts} + * highcharts/plotoptions/bubble-sizebyabsolutevalue/ + * Size by absolute value, various thresholds + * @default false + * @since 4.1.9 + * @product highcharts + * @apioption plotOptions.bubble.sizeByAbsoluteValue + */ + + /** + * When this is true, the series will not cause the Y axis to cross + * the zero plane (or [threshold](#plotOptions.series.threshold) option) + * unless the data actually crosses the plane. + * + * For example, if `softThreshold` is `false`, a series of 0, 1, 2, + * 3 will make the Y axis show negative values according to the `minPadding` + * option. If `softThreshold` is `true`, the Y axis starts at 0. + * + * @since 4.1.9 + * @product highcharts + */ + softThreshold: false, + + states: { + hover: { + halo: { + size: 5 + } + } + }, + + tooltip: { + pointFormat: '({point.x}, {point.y}), Size: {point.z}' + }, + + turboThreshold: 0, + + /** + * The minimum for the Z value range. Defaults to the highest Z value + * in the data. + * + * @type {Number} + * @see [zMin](#plotOptions.bubble.zMin) + * @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/ + * Z has a possible range of 0-100 + * @default null + * @since 4.0.3 + * @product highcharts + * @apioption plotOptions.bubble.zMax + */ + + /** + * The minimum for the Z value range. Defaults to the lowest Z value + * in the data. + * + * @type {Number} + * @see [zMax](#plotOptions.bubble.zMax) + * @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/ + * Z has a possible range of 0-100 + * @default null + * @since 4.0.3 + * @product highcharts + * @apioption plotOptions.bubble.zMin + */ + + /** + * When [displayNegative](#plotOptions.bubble.displayNegative) is `false`, + * bubbles with lower Z values are skipped. When `displayNegative` + * is `true` and a [negativeColor](#plotOptions.bubble.negativeColor) + * is given, points with lower Z is colored. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/bubble-negative/ + * Negative bubbles + * @default 0 + * @since 3.0 + * @product highcharts + */ + zThreshold: 0, + + zoneAxis: 'z' // Prototype members }, { - pointArrayMap: ['y', 'z'], - parallelArrays: ['x', 'y', 'z'], - trackerGroups: ['group', 'dataLabelsGroup'], - specialGroup: 'group', // To allow clipping (#6296) - bubblePadding: true, - zoneAxis: 'z', - directTouch: true, - - /*= if (build.classic) { =*/ - pointAttribs: function (point, state) { - var markerOptions = this.options.marker, - fillOpacity = markerOptions.fillOpacity, - attr = Series.prototype.pointAttribs.call(this, point, state); - - if (fillOpacity !== 1) { - attr.fill = color(attr.fill).setOpacity(fillOpacity).get('rgba'); - } - - return attr; - }, - /*= } =*/ - - /** - * Get the radius for each point based on the minSize, maxSize and each - * point's Z value. This must be done prior to Series.translate because - * the axis needs to add padding in accordance with the point sizes. - */ - getRadii: function (zMin, zMax, minSize, maxSize) { - var len, - i, - pos, - zData = this.zData, - radii = [], - options = this.options, - sizeByArea = options.sizeBy !== 'width', - zThreshold = options.zThreshold, - zRange = zMax - zMin, - value, - radius; - - // Set the shape type and arguments to be picked up in drawPoints - for (i = 0, len = zData.length; i < len; i++) { - - value = zData[i]; - - // When sizing by threshold, the absolute value of z determines - // the size of the bubble. - if (options.sizeByAbsoluteValue && value !== null) { - value = Math.abs(value - zThreshold); - zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold)); - zMin = 0; - } - - if (value === null) { - radius = null; - // Issue #4419 - if value is less than zMin, push a radius that's - // always smaller than the minimum size - } else if (value < zMin) { - radius = minSize / 2 - 1; - } else { - // Relative size, a number between 0 and 1 - pos = zRange > 0 ? (value - zMin) / zRange : 0.5; - - if (sizeByArea && pos >= 0) { - pos = Math.sqrt(pos); - } - radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2; - } - radii.push(radius); - } - this.radii = radii; - }, - - /** - * Perform animation on the bubbles - */ - animate: function (init) { - var animation = this.options.animation; - - if (!init) { // run the animation - each(this.points, function (point) { - var graphic = point.graphic, - animationTarget; - - if (graphic && graphic.width) { // URL symbols don't have width - animationTarget = { - x: graphic.x, - y: graphic.y, - width: graphic.width, - height: graphic.height - }; - - // Start values - graphic.attr({ - x: point.plotX, - y: point.plotY, - width: 1, - height: 1 - }); - - // Run animation - graphic.animate(animationTarget, animation); - } - }); - - // delete this function to allow it only once - this.animate = null; - } - }, - - /** - * Extend the base translate method to handle bubble size - */ - translate: function () { - - var i, - data = this.data, - point, - radius, - radii = this.radii; - - // Run the parent method - seriesTypes.scatter.prototype.translate.call(this); - - // Set the shape type and arguments to be picked up in drawPoints - i = data.length; - - while (i--) { - point = data[i]; - radius = radii ? radii[i] : 0; // #1737 - - if (isNumber(radius) && radius >= this.minPxSize / 2) { - // Shape arguments - point.marker = H.extend(point.marker, { - radius: radius, - width: 2 * radius, - height: 2 * radius - }); - - // Alignment box for the data label - point.dlBox = { - x: point.plotX - radius, - y: point.plotY - radius, - width: 2 * radius, - height: 2 * radius - }; - } else { // below zThreshold - // #1691 - point.shapeArgs = point.plotY = point.dlBox = undefined; - } - } - }, - - alignDataLabel: seriesTypes.column.prototype.alignDataLabel, - buildKDTree: noop, - applyZones: noop + pointArrayMap: ['y', 'z'], + parallelArrays: ['x', 'y', 'z'], + trackerGroups: ['group', 'dataLabelsGroup'], + specialGroup: 'group', // To allow clipping (#6296) + bubblePadding: true, + zoneAxis: 'z', + directTouch: true, + + /*= if (build.classic) { =*/ + pointAttribs: function (point, state) { + var markerOptions = this.options.marker, + fillOpacity = markerOptions.fillOpacity, + attr = Series.prototype.pointAttribs.call(this, point, state); + + if (fillOpacity !== 1) { + attr.fill = color(attr.fill).setOpacity(fillOpacity).get('rgba'); + } + + return attr; + }, + /*= } =*/ + + /** + * Get the radius for each point based on the minSize, maxSize and each + * point's Z value. This must be done prior to Series.translate because + * the axis needs to add padding in accordance with the point sizes. + */ + getRadii: function (zMin, zMax, minSize, maxSize) { + var len, + i, + pos, + zData = this.zData, + radii = [], + options = this.options, + sizeByArea = options.sizeBy !== 'width', + zThreshold = options.zThreshold, + zRange = zMax - zMin, + value, + radius; + + // Set the shape type and arguments to be picked up in drawPoints + for (i = 0, len = zData.length; i < len; i++) { + + value = zData[i]; + + // When sizing by threshold, the absolute value of z determines + // the size of the bubble. + if (options.sizeByAbsoluteValue && value !== null) { + value = Math.abs(value - zThreshold); + zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold)); + zMin = 0; + } + + if (value === null) { + radius = null; + // Issue #4419 - if value is less than zMin, push a radius that's + // always smaller than the minimum size + } else if (value < zMin) { + radius = minSize / 2 - 1; + } else { + // Relative size, a number between 0 and 1 + pos = zRange > 0 ? (value - zMin) / zRange : 0.5; + + if (sizeByArea && pos >= 0) { + pos = Math.sqrt(pos); + } + radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2; + } + radii.push(radius); + } + this.radii = radii; + }, + + /** + * Perform animation on the bubbles + */ + animate: function (init) { + var animation = this.options.animation; + + if (!init) { // run the animation + each(this.points, function (point) { + var graphic = point.graphic, + animationTarget; + + if (graphic && graphic.width) { // URL symbols don't have width + animationTarget = { + x: graphic.x, + y: graphic.y, + width: graphic.width, + height: graphic.height + }; + + // Start values + graphic.attr({ + x: point.plotX, + y: point.plotY, + width: 1, + height: 1 + }); + + // Run animation + graphic.animate(animationTarget, animation); + } + }); + + // delete this function to allow it only once + this.animate = null; + } + }, + + /** + * Extend the base translate method to handle bubble size + */ + translate: function () { + + var i, + data = this.data, + point, + radius, + radii = this.radii; + + // Run the parent method + seriesTypes.scatter.prototype.translate.call(this); + + // Set the shape type and arguments to be picked up in drawPoints + i = data.length; + + while (i--) { + point = data[i]; + radius = radii ? radii[i] : 0; // #1737 + + if (isNumber(radius) && radius >= this.minPxSize / 2) { + // Shape arguments + point.marker = H.extend(point.marker, { + radius: radius, + width: 2 * radius, + height: 2 * radius + }); + + // Alignment box for the data label + point.dlBox = { + x: point.plotX - radius, + y: point.plotY - radius, + width: 2 * radius, + height: 2 * radius + }; + } else { // below zThreshold + // #1691 + point.shapeArgs = point.plotY = point.dlBox = undefined; + } + } + }, + + alignDataLabel: seriesTypes.column.prototype.alignDataLabel, + buildKDTree: noop, + applyZones: noop // Point class }, { - haloPath: function (size) { - return Point.prototype.haloPath.call( - this, - // #6067 - size === 0 ? 0 : (this.marker ? this.marker.radius || 0 : 0) + size - ); - }, - ttBelow: false + haloPath: function (size) { + return Point.prototype.haloPath.call( + this, + // #6067 + size === 0 ? 0 : (this.marker ? this.marker.radius || 0 : 0) + size + ); + }, + ttBelow: false }); /** @@ -434,128 +434,128 @@ seriesType('bubble', 'scatter', { * necessary to avoid the bubbles to overflow. */ Axis.prototype.beforePadding = function () { - var axis = this, - axisLength = this.len, - chart = this.chart, - pxMin = 0, - pxMax = axisLength, - isXAxis = this.isXAxis, - dataKey = isXAxis ? 'xData' : 'yData', - min = this.min, - extremes = {}, - smallestSize = Math.min(chart.plotWidth, chart.plotHeight), - zMin = Number.MAX_VALUE, - zMax = -Number.MAX_VALUE, - range = this.max - min, - transA = axisLength / range, - activeSeries = []; - - // Handle padding on the second pass, or on redraw - each(this.series, function (series) { - - var seriesOptions = series.options, - zData; - - if ( - series.bubblePadding && - (series.visible || !chart.options.chart.ignoreHiddenSeries) - ) { - - // Correction for #1673 - axis.allowZoomOutside = true; - - // Cache it - activeSeries.push(series); - - if (isXAxis) { // because X axis is evaluated first - - // For each series, translate the size extremes to pixel values - each(['minSize', 'maxSize'], function (prop) { - var length = seriesOptions[prop], - isPercent = /%$/.test(length); - - length = pInt(length); - extremes[prop] = isPercent ? - smallestSize * length / 100 : - length; - - }); - series.minPxSize = extremes.minSize; - // Prioritize min size if conflict to make sure bubbles are - // always visible. #5873 - series.maxPxSize = Math.max(extremes.maxSize, extremes.minSize); - - // Find the min and max Z - zData = series.zData; - if (zData.length) { // #1735 - zMin = pick(seriesOptions.zMin, Math.min( - zMin, - Math.max( - arrayMin(zData), - seriesOptions.displayNegative === false ? - seriesOptions.zThreshold : - -Number.MAX_VALUE - ) - )); - zMax = pick( - seriesOptions.zMax, - Math.max(zMax, arrayMax(zData)) - ); - } - } - } - }); - - each(activeSeries, function (series) { - - var data = series[dataKey], - i = data.length, - radius; - - if (isXAxis) { - series.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize); - } - - if (range > 0) { - while (i--) { - if ( - isNumber(data[i]) && - axis.dataMin <= data[i] && - data[i] <= axis.dataMax - ) { - radius = series.radii[i]; - pxMin = Math.min( - ((data[i] - min) * transA) - radius, - pxMin - ); - pxMax = Math.max( - ((data[i] - min) * transA) + radius, - pxMax - ); - } - } - } - }); - - if (activeSeries.length && range > 0 && !this.isLog) { - pxMax -= axisLength; - transA *= (axisLength + pxMin - pxMax) / axisLength; - each( - [['min', 'userMin', pxMin], ['max', 'userMax', pxMax]], - function (keys) { - if (pick(axis.options[keys[0]], axis[keys[1]]) === undefined) { - axis[keys[0]] += keys[2] / transA; - } - } - ); - } + var axis = this, + axisLength = this.len, + chart = this.chart, + pxMin = 0, + pxMax = axisLength, + isXAxis = this.isXAxis, + dataKey = isXAxis ? 'xData' : 'yData', + min = this.min, + extremes = {}, + smallestSize = Math.min(chart.plotWidth, chart.plotHeight), + zMin = Number.MAX_VALUE, + zMax = -Number.MAX_VALUE, + range = this.max - min, + transA = axisLength / range, + activeSeries = []; + + // Handle padding on the second pass, or on redraw + each(this.series, function (series) { + + var seriesOptions = series.options, + zData; + + if ( + series.bubblePadding && + (series.visible || !chart.options.chart.ignoreHiddenSeries) + ) { + + // Correction for #1673 + axis.allowZoomOutside = true; + + // Cache it + activeSeries.push(series); + + if (isXAxis) { // because X axis is evaluated first + + // For each series, translate the size extremes to pixel values + each(['minSize', 'maxSize'], function (prop) { + var length = seriesOptions[prop], + isPercent = /%$/.test(length); + + length = pInt(length); + extremes[prop] = isPercent ? + smallestSize * length / 100 : + length; + + }); + series.minPxSize = extremes.minSize; + // Prioritize min size if conflict to make sure bubbles are + // always visible. #5873 + series.maxPxSize = Math.max(extremes.maxSize, extremes.minSize); + + // Find the min and max Z + zData = series.zData; + if (zData.length) { // #1735 + zMin = pick(seriesOptions.zMin, Math.min( + zMin, + Math.max( + arrayMin(zData), + seriesOptions.displayNegative === false ? + seriesOptions.zThreshold : + -Number.MAX_VALUE + ) + )); + zMax = pick( + seriesOptions.zMax, + Math.max(zMax, arrayMax(zData)) + ); + } + } + } + }); + + each(activeSeries, function (series) { + + var data = series[dataKey], + i = data.length, + radius; + + if (isXAxis) { + series.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize); + } + + if (range > 0) { + while (i--) { + if ( + isNumber(data[i]) && + axis.dataMin <= data[i] && + data[i] <= axis.dataMax + ) { + radius = series.radii[i]; + pxMin = Math.min( + ((data[i] - min) * transA) - radius, + pxMin + ); + pxMax = Math.max( + ((data[i] - min) * transA) + radius, + pxMax + ); + } + } + } + }); + + if (activeSeries.length && range > 0 && !this.isLog) { + pxMax -= axisLength; + transA *= (axisLength + pxMin - pxMax) / axisLength; + each( + [['min', 'userMin', pxMin], ['max', 'userMax', pxMax]], + function (keys) { + if (pick(axis.options[keys[0]], axis[keys[1]]) === undefined) { + axis[keys[0]] += keys[2] / transA; + } + } + ); + } }; /** * A `bubble` series. If the [type](#series.bubble.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.bubble * @excluding dataParser,dataURL,stack @@ -566,7 +566,7 @@ Axis.prototype.beforePadding = function () { /** * An array of data points for the series. For the `bubble` series type, * points can be given in the following ways: - * + * * 1. An array of arrays with 3 or 2 values. In this case, the values * correspond to `x,y,z`. If the first value is a string, it is applied * as the name of the point, and the `x` value is inferred. The `x` @@ -574,7 +574,7 @@ Axis.prototype.beforePadding = function () { * be of length 2\. 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, 1, 2], @@ -582,12 +582,12 @@ Axis.prototype.beforePadding = function () { * [2, 0, 2] * ] * ``` - * + * * 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.bubble.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -603,7 +603,7 @@ Axis.prototype.beforePadding = function () { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding marker @@ -625,7 +625,7 @@ Axis.prototype.beforePadding = function () { * The size value for each bubble. The bubbles' diameters are computed * based on the `z`, and controlled by series options like `minSize`, * `maxSize`, `sizeBy`, `zMin` and `zMax`. - * + * * @type {Number} * @product highcharts * @apioption series.bubble.data.z diff --git a/js/parts-more/ColumnRangeSeries.js b/js/parts-more/ColumnRangeSeries.js index c36886dd361..a8d8895a690 100644 --- a/js/parts-more/ColumnRangeSeries.js +++ b/js/parts-more/ColumnRangeSeries.js @@ -8,12 +8,12 @@ import H from '../parts/Globals.js'; import '../parts/Utilities.js'; var defaultPlotOptions = H.defaultPlotOptions, - each = H.each, - merge = H.merge, - noop = H.noop, - pick = H.pick, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + each = H.each, + merge = H.merge, + noop = H.noop, + pick = H.pick, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; var colProto = seriesTypes.column.prototype; /** @@ -32,152 +32,152 @@ var colProto = seriesTypes.column.prototype; */ var columnRangeOptions = { - /** - * Extended data labels for range series types. Range series data labels - * have no `x` and `y` options. Instead, they have `xLow`, `xHigh`, - * `yLow` and `yHigh` options to allow the higher and lower data label - * sets individually. - * - * @type {Object} - * @extends plotOptions.arearange.dataLabels - * @excluding x,y - * @since 2.3.0 - * @product highcharts highstock - * @apioption plotOptions.columnrange.dataLabels - */ + /** + * Extended data labels for range series types. Range series data labels + * have no `x` and `y` options. Instead, they have `xLow`, `xHigh`, + * `yLow` and `yHigh` options to allow the higher and lower data label + * sets individually. + * + * @type {Object} + * @extends plotOptions.arearange.dataLabels + * @excluding x,y + * @since 2.3.0 + * @product highcharts highstock + * @apioption plotOptions.columnrange.dataLabels + */ - pointRange: null, - - /** @ignore-option */ - marker: null, + pointRange: null, - states: { - hover: { - /** @ignore-option */ - halo: false - } - } + /** @ignore-option */ + marker: null, + + states: { + hover: { + /** @ignore-option */ + halo: false + } + } }; /** * The ColumnRangeSeries class */ seriesType('columnrange', 'arearange', merge( - defaultPlotOptions.column, - defaultPlotOptions.arearange, - columnRangeOptions + defaultPlotOptions.column, + defaultPlotOptions.arearange, + columnRangeOptions ), { - /** - * Translate data points from raw values x and y to plotX and plotY - */ - translate: function () { - var series = this, - yAxis = series.yAxis, - xAxis = series.xAxis, - startAngleRad = xAxis.startAngleRad, - start, - chart = series.chart, - isRadial = series.xAxis.isRadial, - safeDistance = Math.max(chart.chartWidth, chart.chartHeight) + 999, - plotHigh; + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + xAxis = series.xAxis, + startAngleRad = xAxis.startAngleRad, + start, + chart = series.chart, + isRadial = series.xAxis.isRadial, + safeDistance = Math.max(chart.chartWidth, chart.chartHeight) + 999, + plotHigh; - // Don't draw too far outside plot area (#6835) - function safeBounds(pixelPos) { - return Math.min(Math.max( - -safeDistance, - pixelPos - ), safeDistance); - } + // Don't draw too far outside plot area (#6835) + function safeBounds(pixelPos) { + return Math.min(Math.max( + -safeDistance, + pixelPos + ), safeDistance); + } - colProto.translate.apply(series); + colProto.translate.apply(series); - // Set plotLow and plotHigh - each(series.points, function (point) { - var shapeArgs = point.shapeArgs, - minPointLength = series.options.minPointLength, - heightDifference, - height, - y; + // Set plotLow and plotHigh + each(series.points, function (point) { + var shapeArgs = point.shapeArgs, + minPointLength = series.options.minPointLength, + heightDifference, + height, + y; - point.plotHigh = plotHigh = safeBounds( - yAxis.translate(point.high, 0, 1, 0, 1) - ); - point.plotLow = safeBounds(point.plotY); + point.plotHigh = plotHigh = safeBounds( + yAxis.translate(point.high, 0, 1, 0, 1) + ); + point.plotLow = safeBounds(point.plotY); - // adjust shape - y = plotHigh; - height = pick(point.rectPlotY, point.plotY) - plotHigh; + // adjust shape + y = plotHigh; + height = pick(point.rectPlotY, point.plotY) - plotHigh; - // Adjust for minPointLength - if (Math.abs(height) < minPointLength) { - heightDifference = (minPointLength - height); - height += heightDifference; - y -= heightDifference / 2; + // Adjust for minPointLength + if (Math.abs(height) < minPointLength) { + heightDifference = (minPointLength - height); + height += heightDifference; + y -= heightDifference / 2; - // Adjust for negative ranges or reversed Y axis (#1457) - } else if (height < 0) { - height *= -1; - y -= height; - } + // Adjust for negative ranges or reversed Y axis (#1457) + } else if (height < 0) { + height *= -1; + y -= height; + } - if (isRadial) { + if (isRadial) { - start = point.barX + startAngleRad; - point.shapeType = 'path'; - point.shapeArgs = { - d: series.polarArc( - y + height, - y, - start, - start + point.pointWidth - ) - }; - } else { + start = point.barX + startAngleRad; + point.shapeType = 'path'; + point.shapeArgs = { + d: series.polarArc( + y + height, + y, + start, + start + point.pointWidth + ) + }; + } else { - shapeArgs.height = height; - shapeArgs.y = y; + shapeArgs.height = height; + shapeArgs.y = y; - point.tooltipPos = chart.inverted ? - [ - yAxis.len + yAxis.pos - chart.plotLeft - y - height / 2, - xAxis.len + xAxis.pos - chart.plotTop - shapeArgs.x - - shapeArgs.width / 2, - height - ] : [ - xAxis.left - chart.plotLeft + shapeArgs.x + - shapeArgs.width / 2, - yAxis.pos - chart.plotTop + y + height / 2, - height - ]; // don't inherit from column tooltip position - #3372 - } - }); - }, - directTouch: true, - trackerGroups: ['group', 'dataLabelsGroup'], - drawGraph: noop, - getSymbol: noop, - crispCol: colProto.crispCol, - drawPoints: colProto.drawPoints, - drawTracker: colProto.drawTracker, - getColumnMetrics: colProto.getColumnMetrics, - pointAttribs: colProto.pointAttribs, + point.tooltipPos = chart.inverted ? + [ + yAxis.len + yAxis.pos - chart.plotLeft - y - height / 2, + xAxis.len + xAxis.pos - chart.plotTop - shapeArgs.x - + shapeArgs.width / 2, + height + ] : [ + xAxis.left - chart.plotLeft + shapeArgs.x + + shapeArgs.width / 2, + yAxis.pos - chart.plotTop + y + height / 2, + height + ]; // don't inherit from column tooltip position - #3372 + } + }); + }, + directTouch: true, + trackerGroups: ['group', 'dataLabelsGroup'], + drawGraph: noop, + getSymbol: noop, + crispCol: colProto.crispCol, + drawPoints: colProto.drawPoints, + drawTracker: colProto.drawTracker, + getColumnMetrics: colProto.getColumnMetrics, + pointAttribs: colProto.pointAttribs, - // Overrides from modules that may be loaded after this module - animate: function () { - return colProto.animate.apply(this, arguments); - }, - polarArc: function () { - return colProto.polarArc.apply(this, arguments); - }, - translate3dPoints: function () { - return colProto.translate3dPoints.apply(this, arguments); - }, - translate3dShapes: function () { - return colProto.translate3dShapes.apply(this, arguments); - } + // Overrides from modules that may be loaded after this module + animate: function () { + return colProto.animate.apply(this, arguments); + }, + polarArc: function () { + return colProto.polarArc.apply(this, arguments); + }, + translate3dPoints: function () { + return colProto.translate3dPoints.apply(this, arguments); + }, + translate3dShapes: function () { + return colProto.translate3dShapes.apply(this, arguments); + } }, { - setState: colProto.pointClass.prototype.setState + setState: colProto.pointClass.prototype.setState }); @@ -246,7 +246,7 @@ seriesType('columnrange', 'arearange', merge( * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts highstock * @apioption series.columnrange.data */ diff --git a/js/parts-more/ErrorBarSeries.js b/js/parts-more/ErrorBarSeries.js index 79001c20dd8..d7126826340 100644 --- a/js/parts-more/ErrorBarSeries.js +++ b/js/parts-more/ErrorBarSeries.js @@ -10,11 +10,11 @@ import '../parts/Utilities.js'; import '../parts/Options.js'; import './BoxPlotSeries.js'; var each = H.each, - noop = H.noop, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + noop = H.noop, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; -/** +/** * Error bars are a graphical representation of the variability of data and are * used on graphs to indicate the error, or uncertainty in a reported * measurement. @@ -26,86 +26,86 @@ var each = H.each, * @optionparent plotOptions.errorbar */ seriesType('errorbar', 'boxplot', { - /*= if (build.classic) { =*/ + /*= if (build.classic) { =*/ - /** - * The main color of the bars. This can be overridden by - * [stemColor](#plotOptions.errorbar.stemColor) and - * [whiskerColor](#plotOptions.errorbar.whiskerColor) individually. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ - * Error bar styling - * @default #000000 - * @since 3.0 - * @product highcharts - */ - color: '${palette.neutralColor100}', - /*= } =*/ + /** + * The main color of the bars. This can be overridden by + * [stemColor](#plotOptions.errorbar.stemColor) and + * [whiskerColor](#plotOptions.errorbar.whiskerColor) individually. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ + * Error bar styling + * @default #000000 + * @since 3.0 + * @product highcharts + */ + color: '${palette.neutralColor100}', + /*= } =*/ - grouping: false, + grouping: false, - /** - * The parent series of the error bar. The default value links it to - * the previous series. Otherwise, use the id of the parent series. - * - * @since 3.0 - * @product highcharts - */ - linkedTo: ':previous', + /** + * The parent series of the error bar. The default value links it to + * the previous series. Otherwise, use the id of the parent series. + * + * @since 3.0 + * @product highcharts + */ + linkedTo: ':previous', - tooltip: { - pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
' - }, + tooltip: { + pointFormat: '\u25CF {series.name}: {point.low} - {point.high}
' + }, - /** - * The line width of the whiskers, the horizontal lines marking low - * and high values. When `null`, the general - * [lineWidth](#plotOptions.errorbar.lineWidth) applies. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ - * Error bar styling - * @since 3.0 - * @product highcharts - */ - whiskerWidth: null + /** + * The line width of the whiskers, the horizontal lines marking low + * and high values. When `null`, the general + * [lineWidth](#plotOptions.errorbar.lineWidth) applies. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/error-bar-styling/ + * Error bar styling + * @since 3.0 + * @product highcharts + */ + whiskerWidth: null // Prototype members }, { - type: 'errorbar', - pointArrayMap: ['low', 'high'], // array point configs are mapped to this - toYData: function (point) { // return a plain array for speedy calculation - return [point.low, point.high]; - }, - pointValKey: 'high', // defines the top of the tracker - doQuartiles: false, - drawDataLabels: seriesTypes.arearange ? - function () { - var valKey = this.pointValKey; - seriesTypes.arearange.prototype.drawDataLabels.call(this); - // Arearange drawDataLabels does not reset point.y to high, - // but to low after drawing (#4133) - each(this.data, function (point) { - point.y = point[valKey]; - }); - } : - noop, + type: 'errorbar', + pointArrayMap: ['low', 'high'], // array point configs are mapped to this + toYData: function (point) { // return a plain array for speedy calculation + return [point.low, point.high]; + }, + pointValKey: 'high', // defines the top of the tracker + doQuartiles: false, + drawDataLabels: seriesTypes.arearange ? + function () { + var valKey = this.pointValKey; + seriesTypes.arearange.prototype.drawDataLabels.call(this); + // Arearange drawDataLabels does not reset point.y to high, + // but to low after drawing (#4133) + each(this.data, function (point) { + point.y = point[valKey]; + }); + } : + noop, - /** - * Get the width and X offset, either on top of the linked series column - * or standalone - */ - getColumnMetrics: function () { - return (this.linkedParent && this.linkedParent.columnMetrics) || - seriesTypes.column.prototype.getColumnMetrics.call(this); - } + /** + * Get the width and X offset, either on top of the linked series column + * or standalone + */ + getColumnMetrics: function () { + return (this.linkedParent && this.linkedParent.columnMetrics) || + seriesTypes.column.prototype.getColumnMetrics.call(this); + } }); /** * A `errorbar` series. If the [type](#series.errorbar.type) option * is not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.errorbar * @excluding dataParser,dataURL,stack,stacking @@ -116,7 +116,7 @@ seriesType('errorbar', 'boxplot', { /** * An array of data points for the series. For the `errorbar` series * type, points can be given in the following ways: - * + * * 1. An array of arrays with 3 or 2 values. In this case, the values * correspond to `x,low,high`. If the first value is a string, it is * applied as the name of the point, and the `x` value is inferred. @@ -124,7 +124,7 @@ seriesType('errorbar', 'boxplot', { * should be of length 2\. 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, 10, 2], @@ -132,12 +132,12 @@ seriesType('errorbar', 'boxplot', { * [2, 4, 5] * ] * ``` - * + * * 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.errorbar.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -153,7 +153,7 @@ seriesType('errorbar', 'boxplot', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.arearange.data * @excluding dataLabels,drilldown,marker,states diff --git a/js/parts-more/GaugeSeries.js b/js/parts-more/GaugeSeries.js index a4cbbc480a8..a1df0d73ffc 100644 --- a/js/parts-more/GaugeSeries.js +++ b/js/parts-more/GaugeSeries.js @@ -12,17 +12,17 @@ import '../parts/Point.js'; import '../parts/Series.js'; import '../parts/Interaction.js'; var each = H.each, - isNumber = H.isNumber, - merge = H.merge, - noop = H.noop, - pick = H.pick, - pInt = H.pInt, - Series = H.Series, - seriesType = H.seriesType, - TrackerMixin = H.TrackerMixin; + isNumber = H.isNumber, + merge = H.merge, + noop = H.noop, + pick = H.pick, + pInt = H.pInt, + Series = H.Series, + seriesType = H.seriesType, + TrackerMixin = H.TrackerMixin; -/** +/** * Gauges are circular plots displaying one or more values with a dial pointing * to values along the perimeter. * @@ -37,538 +37,538 @@ var each = H.each, */ seriesType('gauge', 'line', { - /** - * When this option is `true`, the dial will wrap around the axes. For - * instance, in a full-range gauge going from 0 to 360, a value of 400 - * will point to 40\. When `wrap` is `false`, the dial stops at 360. - * - * @type {Boolean} - * @see [overshoot](#plotOptions.gauge.overshoot) - * @default true - * @since 3.0 - * @product highcharts - * @apioption plotOptions.gauge.wrap - */ - - /** - * Data labels for the gauge. For gauges, the data labels are enabled - * by default and shown in a bordered box below the point. - * - * @type {Object} - * @extends plotOptions.series.dataLabels - * @since 2.3.0 - * @product highcharts - */ - dataLabels: { - - /** - * Enable or disable the data labels. - * - * @since 2.3.0 - * @product highcharts highmaps - */ - enabled: true, - - defer: false, - - /** - * The y position offset of the label relative to the center of the - * gauge. - * - * @since 2.3.0 - * @product highcharts highmaps - */ - y: 15, - - /** - * The border radius in pixels for the gauge's data label. - * - * @since 2.3.0 - * @product highcharts highmaps - */ - borderRadius: 3, - - crop: false, - - /** - * The vertical alignment of the data label. - * - * @product highcharts highmaps - */ - verticalAlign: 'top', - - /** - * The Z index of the data labels. A value of 2 display them behind - * the dial. - * - * @since 2.1.5 - * @product highcharts highmaps - */ - zIndex: 2, - /*= if (build.classic) { =*/ - // Presentational - - /** - * The border width in pixels for the gauge data label. - * - * @since 2.3.0 - * @product highcharts highmaps - */ - borderWidth: 1, - - /** - * The border color for the data label. - * - * @type {Color} - * @default #cccccc - * @since 2.3.0 - * @product highcharts highmaps - */ - borderColor: '${palette.neutralColor20}' - /*= } =*/ - }, - - /** - * Options for the dial or arrow pointer of the gauge. - * - * In styled mode, the dial is styled with the - * `.highcharts-gauge-series .highcharts-dial` rule. - * - * @type {Object} - * @sample {highcharts} highcharts/css/gauge/ Styled mode - * @since 2.3.0 - * @product highcharts - */ - dial: {}, - - /** - * The length of the dial's base part, relative to the total radius - * or length of the dial. - * - * @type {String} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default 70% - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.baseLength - */ - - /** - * The pixel width of the base of the gauge dial. The base is the part - * closest to the pivot, defined by baseLength. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default 3 - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.baseWidth - */ - - /** - * The radius or length of the dial, in percentages relative to the - * radius of the gauge itself. - * - * @type {String} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default 80% - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.radius - */ - - /** - * The length of the dial's rear end, the part that extends out on the - * other side of the pivot. Relative to the dial's length. - * - * @type {String} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default 10% - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.rearLength - */ - - /** - * The width of the top of the dial, closest to the perimeter. The pivot - * narrows in from the base to the top. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default 1 - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.topWidth - */ - - /*= if (build.classic) { =*/ - - /** - * The background or fill color of the gauge's dial. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default #000000 - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.backgroundColor - */ - - /** - * The border color or stroke of the gauge's dial. By default, the borderWidth - * is 0, so this must be set in addition to a custom border color. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default #cccccc - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.borderColor - */ - - /** - * The width of the gauge dial border in pixels. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/gauge-dial/ - * Dial options demonstrated - * @default 0 - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.dial.borderWidth - */ - - /*= } =*/ - - /** - * Allow the dial to overshoot the end of the perimeter axis by this - * many degrees. Say if the gauge axis goes from 0 to 60, a value of - * 100, or 1000, will show 5 degrees beyond the end of the axis when this - * option is set to 5. - * - * @type {Number} - * @see [wrap](#plotOptions.gauge.wrap) - * @sample {highcharts} highcharts/plotoptions/gauge-overshoot/ - * Allow 5 degrees overshoot - * @default 0 - * @since 3.0.10 - * @product highcharts - * @apioption plotOptions.gauge.overshoot - */ - - /** - * Options for the pivot or the center point of the gauge. - * - * In styled mode, the pivot is styled with the - * `.highcharts-gauge-series .highcharts-pivot` rule. - * - * @type {Object} - * @sample {highcharts} highcharts/css/gauge/ Styled mode - * @since 2.3.0 - * @product highcharts - */ - pivot: {}, - - /** - * The pixel radius of the pivot. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ - * Pivot options demonstrated - * @default 5 - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.pivot.radius - */ - - /*= if (build.classic) { =*/ - - /** - * The border or stroke width of the pivot. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ - * Pivot options demonstrated - * @default 0 - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.pivot.borderWidth - */ - - /** - * The border or stroke color of the pivot. In able to change this, - * the borderWidth must also be set to something other than the default - * 0. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ - * Pivot options demonstrated - * @default #cccccc - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.pivot.borderColor - */ - - /** - * The background color or fill of the pivot. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ - * Pivot options demonstrated - * @default #000000 - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.gauge.pivot.backgroundColor - */ - /*= } =*/ - - - tooltip: { - headerFormat: '' - }, - - /** - * Whether to display this particular series or series type in the - * legend. Defaults to false for gauge series. - * - * @since 2.3.0 - * @product highcharts - */ - showInLegend: false + /** + * When this option is `true`, the dial will wrap around the axes. For + * instance, in a full-range gauge going from 0 to 360, a value of 400 + * will point to 40\. When `wrap` is `false`, the dial stops at 360. + * + * @type {Boolean} + * @see [overshoot](#plotOptions.gauge.overshoot) + * @default true + * @since 3.0 + * @product highcharts + * @apioption plotOptions.gauge.wrap + */ + + /** + * Data labels for the gauge. For gauges, the data labels are enabled + * by default and shown in a bordered box below the point. + * + * @type {Object} + * @extends plotOptions.series.dataLabels + * @since 2.3.0 + * @product highcharts + */ + dataLabels: { + + /** + * Enable or disable the data labels. + * + * @since 2.3.0 + * @product highcharts highmaps + */ + enabled: true, + + defer: false, + + /** + * The y position offset of the label relative to the center of the + * gauge. + * + * @since 2.3.0 + * @product highcharts highmaps + */ + y: 15, + + /** + * The border radius in pixels for the gauge's data label. + * + * @since 2.3.0 + * @product highcharts highmaps + */ + borderRadius: 3, + + crop: false, + + /** + * The vertical alignment of the data label. + * + * @product highcharts highmaps + */ + verticalAlign: 'top', + + /** + * The Z index of the data labels. A value of 2 display them behind + * the dial. + * + * @since 2.1.5 + * @product highcharts highmaps + */ + zIndex: 2, + /*= if (build.classic) { =*/ + // Presentational + + /** + * The border width in pixels for the gauge data label. + * + * @since 2.3.0 + * @product highcharts highmaps + */ + borderWidth: 1, + + /** + * The border color for the data label. + * + * @type {Color} + * @default #cccccc + * @since 2.3.0 + * @product highcharts highmaps + */ + borderColor: '${palette.neutralColor20}' + /*= } =*/ + }, + + /** + * Options for the dial or arrow pointer of the gauge. + * + * In styled mode, the dial is styled with the + * `.highcharts-gauge-series .highcharts-dial` rule. + * + * @type {Object} + * @sample {highcharts} highcharts/css/gauge/ Styled mode + * @since 2.3.0 + * @product highcharts + */ + dial: {}, + + /** + * The length of the dial's base part, relative to the total radius + * or length of the dial. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default 70% + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.baseLength + */ + + /** + * The pixel width of the base of the gauge dial. The base is the part + * closest to the pivot, defined by baseLength. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default 3 + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.baseWidth + */ + + /** + * The radius or length of the dial, in percentages relative to the + * radius of the gauge itself. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default 80% + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.radius + */ + + /** + * The length of the dial's rear end, the part that extends out on the + * other side of the pivot. Relative to the dial's length. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default 10% + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.rearLength + */ + + /** + * The width of the top of the dial, closest to the perimeter. The pivot + * narrows in from the base to the top. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default 1 + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.topWidth + */ + + /*= if (build.classic) { =*/ + + /** + * The background or fill color of the gauge's dial. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default #000000 + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.backgroundColor + */ + + /** + * The border color or stroke of the gauge's dial. By default, the borderWidth + * is 0, so this must be set in addition to a custom border color. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default #cccccc + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.borderColor + */ + + /** + * The width of the gauge dial border in pixels. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/gauge-dial/ + * Dial options demonstrated + * @default 0 + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.dial.borderWidth + */ + + /*= } =*/ + + /** + * Allow the dial to overshoot the end of the perimeter axis by this + * many degrees. Say if the gauge axis goes from 0 to 60, a value of + * 100, or 1000, will show 5 degrees beyond the end of the axis when this + * option is set to 5. + * + * @type {Number} + * @see [wrap](#plotOptions.gauge.wrap) + * @sample {highcharts} highcharts/plotoptions/gauge-overshoot/ + * Allow 5 degrees overshoot + * @default 0 + * @since 3.0.10 + * @product highcharts + * @apioption plotOptions.gauge.overshoot + */ + + /** + * Options for the pivot or the center point of the gauge. + * + * In styled mode, the pivot is styled with the + * `.highcharts-gauge-series .highcharts-pivot` rule. + * + * @type {Object} + * @sample {highcharts} highcharts/css/gauge/ Styled mode + * @since 2.3.0 + * @product highcharts + */ + pivot: {}, + + /** + * The pixel radius of the pivot. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ + * Pivot options demonstrated + * @default 5 + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.pivot.radius + */ + + /*= if (build.classic) { =*/ + + /** + * The border or stroke width of the pivot. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ + * Pivot options demonstrated + * @default 0 + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.pivot.borderWidth + */ + + /** + * The border or stroke color of the pivot. In able to change this, + * the borderWidth must also be set to something other than the default + * 0. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ + * Pivot options demonstrated + * @default #cccccc + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.pivot.borderColor + */ + + /** + * The background color or fill of the pivot. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/gauge-pivot/ + * Pivot options demonstrated + * @default #000000 + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.gauge.pivot.backgroundColor + */ + /*= } =*/ + + + tooltip: { + headerFormat: '' + }, + + /** + * Whether to display this particular series or series type in the + * legend. Defaults to false for gauge series. + * + * @since 2.3.0 + * @product highcharts + */ + showInLegend: false // Prototype members }, { - // chart.angular will be set to true when a gauge series is present, - // and this will be used on the axes - angular: true, - directTouch: true, // #5063 - drawGraph: noop, - fixedBox: true, - forceDL: true, - noSharedTooltip: true, - trackerGroups: ['group', 'dataLabelsGroup'], - - /** - * Calculate paths etc - */ - translate: function () { - - var series = this, - yAxis = series.yAxis, - options = series.options, - center = yAxis.center; - - series.generatePoints(); - - each(series.points, function (point) { - - var dialOptions = merge(options.dial, point.dial), - radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) / - 200, - baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) / - 100, - rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) / - 100, - baseWidth = dialOptions.baseWidth || 3, - topWidth = dialOptions.topWidth || 1, - overshoot = options.overshoot, - rotation = yAxis.startAngleRad + - yAxis.translate(point.y, null, null, null, true); - - // Handle the wrap and overshoot options - if (isNumber(overshoot)) { - overshoot = overshoot / 180 * Math.PI; - rotation = Math.max( - yAxis.startAngleRad - overshoot, - Math.min(yAxis.endAngleRad + overshoot, rotation) - ); - - } else if (options.wrap === false) { - rotation = Math.max( - yAxis.startAngleRad, - Math.min(yAxis.endAngleRad, rotation) - ); - } - - rotation = rotation * 180 / Math.PI; - - point.shapeType = 'path'; - point.shapeArgs = { - d: dialOptions.path || [ - 'M', - -rearLength, -baseWidth / 2, - 'L', - baseLength, -baseWidth / 2, - radius, -topWidth / 2, - radius, topWidth / 2, - baseLength, baseWidth / 2, - -rearLength, baseWidth / 2, - 'z' - ], - translateX: center[0], - translateY: center[1], - rotation: rotation - }; - - // Positions for data label - point.plotX = center[0]; - point.plotY = center[1]; - }); - }, - - /** - * Draw the points where each point is one needle - */ - drawPoints: function () { - - var series = this, - center = series.yAxis.center, - pivot = series.pivot, - options = series.options, - pivotOptions = options.pivot, - renderer = series.chart.renderer; - - each(series.points, function (point) { - - var graphic = point.graphic, - shapeArgs = point.shapeArgs, - d = shapeArgs.d, - dialOptions = merge(options.dial, point.dial); // #1233 - - if (graphic) { - graphic.animate(shapeArgs); - shapeArgs.d = d; // animate alters it - } else { - point.graphic = renderer[point.shapeType](shapeArgs) - .attr({ - // required by VML when animation is false - rotation: shapeArgs.rotation, - zIndex: 1 - }) - .addClass('highcharts-dial') - .add(series.group); - - /*= if (build.classic) { =*/ - // Presentational attributes - point.graphic.attr({ - stroke: dialOptions.borderColor || 'none', - 'stroke-width': dialOptions.borderWidth || 0, - fill: dialOptions.backgroundColor || - '${palette.neutralColor100}' - }); - /*= } =*/ - } - }); - - // Add or move the pivot - if (pivot) { - pivot.animate({ // #1235 - translateX: center[0], - translateY: center[1] - }); - } else { - series.pivot = renderer.circle(0, 0, pick(pivotOptions.radius, 5)) - .attr({ - zIndex: 2 - }) - .addClass('highcharts-pivot') - .translate(center[0], center[1]) - .add(series.group); - - /*= if (build.classic) { =*/ - // Presentational attributes - series.pivot.attr({ - 'stroke-width': pivotOptions.borderWidth || 0, - stroke: pivotOptions.borderColor || - '${palette.neutralColor20}', - fill: pivotOptions.backgroundColor || - '${palette.neutralColor100}' - }); - /*= } =*/ - } - }, - - /** - * Animate the arrow up from startAngle - */ - animate: function (init) { - var series = this; - - if (!init) { - each(series.points, function (point) { - var graphic = point.graphic; - - if (graphic) { - // start value - graphic.attr({ - rotation: series.yAxis.startAngleRad * 180 / Math.PI - }); - - // animate - graphic.animate({ - rotation: point.shapeArgs.rotation - }, series.options.animation); - } - }); - - // delete this function to allow it only once - series.animate = null; - } - }, - - render: function () { - this.group = this.plotGroup( - 'group', - 'series', - this.visible ? 'visible' : 'hidden', - this.options.zIndex, - this.chart.seriesGroup - ); - Series.prototype.render.call(this); - this.group.clip(this.chart.clipRect); - }, - - /** - * Extend the basic setData method by running processData and generatePoints - * immediately, in order to access the points from the legend. - */ - setData: function (data, redraw) { - Series.prototype.setData.call(this, data, false); - this.processData(); - this.generatePoints(); - if (pick(redraw, true)) { - this.chart.redraw(); - } - }, - - /** - * If the tracking module is loaded, add the point tracker - */ - drawTracker: TrackerMixin && TrackerMixin.drawTrackerPoint + // chart.angular will be set to true when a gauge series is present, + // and this will be used on the axes + angular: true, + directTouch: true, // #5063 + drawGraph: noop, + fixedBox: true, + forceDL: true, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], + + /** + * Calculate paths etc + */ + translate: function () { + + var series = this, + yAxis = series.yAxis, + options = series.options, + center = yAxis.center; + + series.generatePoints(); + + each(series.points, function (point) { + + var dialOptions = merge(options.dial, point.dial), + radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) / + 200, + baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) / + 100, + rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) / + 100, + baseWidth = dialOptions.baseWidth || 3, + topWidth = dialOptions.topWidth || 1, + overshoot = options.overshoot, + rotation = yAxis.startAngleRad + + yAxis.translate(point.y, null, null, null, true); + + // Handle the wrap and overshoot options + if (isNumber(overshoot)) { + overshoot = overshoot / 180 * Math.PI; + rotation = Math.max( + yAxis.startAngleRad - overshoot, + Math.min(yAxis.endAngleRad + overshoot, rotation) + ); + + } else if (options.wrap === false) { + rotation = Math.max( + yAxis.startAngleRad, + Math.min(yAxis.endAngleRad, rotation) + ); + } + + rotation = rotation * 180 / Math.PI; + + point.shapeType = 'path'; + point.shapeArgs = { + d: dialOptions.path || [ + 'M', + -rearLength, -baseWidth / 2, + 'L', + baseLength, -baseWidth / 2, + radius, -topWidth / 2, + radius, topWidth / 2, + baseLength, baseWidth / 2, + -rearLength, baseWidth / 2, + 'z' + ], + translateX: center[0], + translateY: center[1], + rotation: rotation + }; + + // Positions for data label + point.plotX = center[0]; + point.plotY = center[1]; + }); + }, + + /** + * Draw the points where each point is one needle + */ + drawPoints: function () { + + var series = this, + center = series.yAxis.center, + pivot = series.pivot, + options = series.options, + pivotOptions = options.pivot, + renderer = series.chart.renderer; + + each(series.points, function (point) { + + var graphic = point.graphic, + shapeArgs = point.shapeArgs, + d = shapeArgs.d, + dialOptions = merge(options.dial, point.dial); // #1233 + + if (graphic) { + graphic.animate(shapeArgs); + shapeArgs.d = d; // animate alters it + } else { + point.graphic = renderer[point.shapeType](shapeArgs) + .attr({ + // required by VML when animation is false + rotation: shapeArgs.rotation, + zIndex: 1 + }) + .addClass('highcharts-dial') + .add(series.group); + + /*= if (build.classic) { =*/ + // Presentational attributes + point.graphic.attr({ + stroke: dialOptions.borderColor || 'none', + 'stroke-width': dialOptions.borderWidth || 0, + fill: dialOptions.backgroundColor || + '${palette.neutralColor100}' + }); + /*= } =*/ + } + }); + + // Add or move the pivot + if (pivot) { + pivot.animate({ // #1235 + translateX: center[0], + translateY: center[1] + }); + } else { + series.pivot = renderer.circle(0, 0, pick(pivotOptions.radius, 5)) + .attr({ + zIndex: 2 + }) + .addClass('highcharts-pivot') + .translate(center[0], center[1]) + .add(series.group); + + /*= if (build.classic) { =*/ + // Presentational attributes + series.pivot.attr({ + 'stroke-width': pivotOptions.borderWidth || 0, + stroke: pivotOptions.borderColor || + '${palette.neutralColor20}', + fill: pivotOptions.backgroundColor || + '${palette.neutralColor100}' + }); + /*= } =*/ + } + }, + + /** + * Animate the arrow up from startAngle + */ + animate: function (init) { + var series = this; + + if (!init) { + each(series.points, function (point) { + var graphic = point.graphic; + + if (graphic) { + // start value + graphic.attr({ + rotation: series.yAxis.startAngleRad * 180 / Math.PI + }); + + // animate + graphic.animate({ + rotation: point.shapeArgs.rotation + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + } + }, + + render: function () { + this.group = this.plotGroup( + 'group', + 'series', + this.visible ? 'visible' : 'hidden', + this.options.zIndex, + this.chart.seriesGroup + ); + Series.prototype.render.call(this); + this.group.clip(this.chart.clipRect); + }, + + /** + * Extend the basic setData method by running processData and generatePoints + * immediately, in order to access the points from the legend. + */ + setData: function (data, redraw) { + Series.prototype.setData.call(this, data, false); + this.processData(); + this.generatePoints(); + if (pick(redraw, true)) { + this.chart.redraw(); + } + }, + + /** + * If the tracking module is loaded, add the point tracker + */ + drawTracker: TrackerMixin && TrackerMixin.drawTrackerPoint // Point members }, { - /** - * Don't do any hover colors or anything - */ - setState: function (state) { - this.state = state; - } + /** + * Don't do any hover colors or anything + */ + setState: function (state) { + this.state = state; + } }); /** * A `gauge` series. If the [type](#series.gauge.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.gauge * @excluding animationLimit,boostThreshold,connectEnds,connectNulls, @@ -583,19 +583,19 @@ seriesType('gauge', 'line', { /** * An array of data points for the series. For the `gauge` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 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.gauge.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * y: 6, @@ -606,9 +606,9 @@ seriesType('gauge', 'line', { * name: "Point1", * color: "#FF00FF" * }] - * + * * The typical gauge only contains a single data value. - * + * * @type {Array} * @extends series.line.data * @excluding drilldown,marker,x diff --git a/js/parts-more/Pane.js b/js/parts-more/Pane.js index a0da8d2a7a3..856fd4fbc2e 100644 --- a/js/parts-more/Pane.js +++ b/js/parts-more/Pane.js @@ -9,10 +9,10 @@ import H from '../parts/Globals.js'; import '../mixins/centered-series.js'; import '../parts/Utilities.js'; var CenteredSeriesMixin = H.CenteredSeriesMixin, - each = H.each, - extend = H.extend, - merge = H.merge, - splat = H.splat; + each = H.each, + extend = H.extend, + merge = H.merge, + splat = H.splat; /** * The Pane object allows options that are common to a set of X and Y axes. * @@ -20,323 +20,323 @@ var CenteredSeriesMixin = H.CenteredSeriesMixin, * */ function Pane(options, chart) { - this.init(options, chart); + this.init(options, chart); } // Extend the Pane prototype extend(Pane.prototype, { - coll: 'pane', // Member of chart.pane - - /** - * Initiate the Pane object - */ - init: function (options, chart) { - this.chart = chart; - this.background = []; - - chart.pane.push(this); - - this.setOptions(options); - }, - - setOptions: function (options) { - - // Set options. Angular charts have a default background (#3318) - this.options = options = merge( - this.defaultOptions, - this.chart.angular ? { background: {} } : undefined, - options - ); - }, - - /** - * Render the pane with its backgrounds. - */ - render: function () { - - var options = this.options, - backgroundOption = this.options.background, - renderer = this.chart.renderer, - len, - i; - - if (!this.group) { - this.group = renderer.g('pane-group') - .attr({ zIndex: options.zIndex || 0 }) - .add(); - } - - this.updateCenter(); - - // Render the backgrounds - if (backgroundOption) { - backgroundOption = splat(backgroundOption); - - len = Math.max( - backgroundOption.length, - this.background.length || 0 - ); - - for (i = 0; i < len; i++) { - if (backgroundOption[i] && this.axis) { // #6641 - if axis exists, chart is circular and apply background - this.renderBackground( - merge( - this.defaultBackgroundOptions, - backgroundOption[i] - ), - i - ); - } else if (this.background[i]) { - this.background[i] = this.background[i].destroy(); - this.background.splice(i, 1); - } - } - } - }, - - /** - * Render an individual pane background. - * @param {Object} backgroundOptions Background options - * @param {number} i The index of the background in this.backgrounds - */ - renderBackground: function (backgroundOptions, i) { - var method = 'animate'; - - if (!this.background[i]) { - this.background[i] = this.chart.renderer.path() - .add(this.group); - method = 'attr'; - } - - this.background[i][method]({ - 'd': this.axis.getPlotBandPath( - backgroundOptions.from, - backgroundOptions.to, - backgroundOptions - ) - }).attr({ - /*= if (build.classic) { =*/ - 'fill': backgroundOptions.backgroundColor, - 'stroke': backgroundOptions.borderColor, - 'stroke-width': backgroundOptions.borderWidth, - /*= } =*/ - 'class': 'highcharts-pane ' + (backgroundOptions.className || '') - }); - - }, - - /** - * The pane serves as a container for axes and backgrounds for circular - * gauges and polar charts. - * @since 2.3.0 - * @optionparent pane - */ - defaultOptions: { - - /** - * The end angle of the polar X axis or gauge value axis, given in degrees - * where 0 is north. Defaults to [startAngle](#pane.startAngle) + 360. - * - * @type {Number} - * @sample {highcharts} highcharts/demo/gauge-vu-meter/ - * VU-meter with custom start and end angle - * @since 2.3.0 - * @product highcharts - * @apioption pane.endAngle - */ - - /** - * The center of a polar chart or angular gauge, given as an array - * of [x, y] positions. Positions can be given as integers that transform - * to pixels, or as percentages of the plot area size. - * - * @type {Array} - * @sample {highcharts} highcharts/demo/gauge-vu-meter/ - * Two gauges with different center - * @default ["50%", "50%"] - * @since 2.3.0 - * @product highcharts - */ - center: ['50%', '50%'], - - /** - * The size of the pane, either as a number defining pixels, or a - * percentage defining a percentage of the plot are. - * - * @type {Number|String} - * @sample {highcharts} highcharts/demo/gauge-vu-meter/ Smaller size - * @default 85% - * @product highcharts - */ - size: '85%', - - /** - * The start angle of the polar X axis or gauge axis, given in degrees - * where 0 is north. Defaults to 0. - * - * @type {Number} - * @sample {highcharts} highcharts/demo/gauge-vu-meter/ - * VU-meter with custom start and end angle - * @since 2.3.0 - * @product highcharts - */ - startAngle: 0 - }, - - /** - * An array of background items for the pane. - * @type Array. - * @sample {highcharts} highcharts/demo/gauge-speedometer/ - * Speedometer gauge with multiple backgrounds - * @optionparent pane.background - */ - defaultBackgroundOptions: { - /** - * The class name for this background. - * - * @type {String} - * @sample {highcharts} highcharts/css/pane/ Panes styled by CSS - * @sample {highstock} highcharts/css/pane/ Panes styled by CSS - * @sample {highmaps} highcharts/css/pane/ Panes styled by CSS - * @default highcharts-pane - * @since 5.0.0 - * @apioption pane.background.className - */ - - /** - * Tha shape of the pane background. When `solid`, the background - * is circular. When `arc`, the background extends only from the min - * to the max of the value axis. - * - * @validvalue ["solid", "arc"] - * @type {String} - * @default solid - * @since 2.3.0 - * @product highcharts - */ - shape: 'circle', - /*= if (build.classic) { =*/ - - /** - * The pixel border width of the pane background. - * - * @type {Number} - * @default 1 - * @since 2.3.0 - * @product highcharts - */ - borderWidth: 1, - - /** - * The pane background border color. - * - * @type {Color} - * @default #cccccc - * @since 2.3.0 - * @product highcharts - */ - borderColor: '${palette.neutralColor20}', - - /** - * The background color or gradient for the pane. - * - * @type {Color} - * @since 2.3.0 - * @product highcharts - */ - backgroundColor: { - /** - * Definition of the gradient, similar to SVG: object literal holds - * start position (x1, y1) and the end position (x2, y2) relative - * to the shape, where 0 means top/left and 1 is bottom/right. - * All positions are floats between 0 and 1. - * - * @type {Object} - */ - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - /** - * The stops is an array of tuples, where the first - * item is a float between 0 and 1 assigning the relative position in - * the gradient, and the second item is the color. - * - * @default [[0, #ffffff], [1, #e6e6e6]] - * @type {Array} - */ - stops: [ - [0, '${palette.backgroundColor}'], - [1, '${palette.neutralColor10}'] - ] - }, - /*= } =*/ - - /** @ignore-option */ - from: -Number.MAX_VALUE, // corrected to axis min - - /** - * The inner radius of the pane background. Can be either numeric - * (pixels) or a percentage string. - * - * @type {Number|String} - * @default 0 - * @since 2.3.0 - * @product highcharts - */ - innerRadius: 0, - - /** @ignore-option */ - to: Number.MAX_VALUE, // corrected to axis max - - /** - * The outer radius of the circular pane background. Can be either - * numeric (pixels) or a percentage string. - * - * @type {Number|String} - * @default 105% - * @since 2.3.0 - * @product highcharts - */ - outerRadius: '105%' - }, - - /** - * Gets the center for the pane and its axis. - */ - updateCenter: function (axis) { - this.center = (axis || this.axis || {}).center = - CenteredSeriesMixin.getCenter.call(this); - }, - - /** - * Destroy the pane item - * / - destroy: function () { - H.erase(this.chart.pane, this); - each(this.background, function (background) { - background.destroy(); - }); - this.background.length = 0; - this.group = this.group.destroy(); - }, - */ - - /** - * Update the pane item with new options - * @param {Object} options New pane options - */ - update: function (options, redraw) { - - merge(true, this.options, options); - this.setOptions(this.options); - this.render(); - each(this.chart.axes, function (axis) { - if (axis.pane === this) { - axis.pane = null; - axis.update({}, redraw); - } - }, this); - } - + coll: 'pane', // Member of chart.pane + + /** + * Initiate the Pane object + */ + init: function (options, chart) { + this.chart = chart; + this.background = []; + + chart.pane.push(this); + + this.setOptions(options); + }, + + setOptions: function (options) { + + // Set options. Angular charts have a default background (#3318) + this.options = options = merge( + this.defaultOptions, + this.chart.angular ? { background: {} } : undefined, + options + ); + }, + + /** + * Render the pane with its backgrounds. + */ + render: function () { + + var options = this.options, + backgroundOption = this.options.background, + renderer = this.chart.renderer, + len, + i; + + if (!this.group) { + this.group = renderer.g('pane-group') + .attr({ zIndex: options.zIndex || 0 }) + .add(); + } + + this.updateCenter(); + + // Render the backgrounds + if (backgroundOption) { + backgroundOption = splat(backgroundOption); + + len = Math.max( + backgroundOption.length, + this.background.length || 0 + ); + + for (i = 0; i < len; i++) { + if (backgroundOption[i] && this.axis) { // #6641 - if axis exists, chart is circular and apply background + this.renderBackground( + merge( + this.defaultBackgroundOptions, + backgroundOption[i] + ), + i + ); + } else if (this.background[i]) { + this.background[i] = this.background[i].destroy(); + this.background.splice(i, 1); + } + } + } + }, + + /** + * Render an individual pane background. + * @param {Object} backgroundOptions Background options + * @param {number} i The index of the background in this.backgrounds + */ + renderBackground: function (backgroundOptions, i) { + var method = 'animate'; + + if (!this.background[i]) { + this.background[i] = this.chart.renderer.path() + .add(this.group); + method = 'attr'; + } + + this.background[i][method]({ + 'd': this.axis.getPlotBandPath( + backgroundOptions.from, + backgroundOptions.to, + backgroundOptions + ) + }).attr({ + /*= if (build.classic) { =*/ + 'fill': backgroundOptions.backgroundColor, + 'stroke': backgroundOptions.borderColor, + 'stroke-width': backgroundOptions.borderWidth, + /*= } =*/ + 'class': 'highcharts-pane ' + (backgroundOptions.className || '') + }); + + }, + + /** + * The pane serves as a container for axes and backgrounds for circular + * gauges and polar charts. + * @since 2.3.0 + * @optionparent pane + */ + defaultOptions: { + + /** + * The end angle of the polar X axis or gauge value axis, given in degrees + * where 0 is north. Defaults to [startAngle](#pane.startAngle) + 360. + * + * @type {Number} + * @sample {highcharts} highcharts/demo/gauge-vu-meter/ + * VU-meter with custom start and end angle + * @since 2.3.0 + * @product highcharts + * @apioption pane.endAngle + */ + + /** + * The center of a polar chart or angular gauge, given as an array + * of [x, y] positions. Positions can be given as integers that transform + * to pixels, or as percentages of the plot area size. + * + * @type {Array} + * @sample {highcharts} highcharts/demo/gauge-vu-meter/ + * Two gauges with different center + * @default ["50%", "50%"] + * @since 2.3.0 + * @product highcharts + */ + center: ['50%', '50%'], + + /** + * The size of the pane, either as a number defining pixels, or a + * percentage defining a percentage of the plot are. + * + * @type {Number|String} + * @sample {highcharts} highcharts/demo/gauge-vu-meter/ Smaller size + * @default 85% + * @product highcharts + */ + size: '85%', + + /** + * The start angle of the polar X axis or gauge axis, given in degrees + * where 0 is north. Defaults to 0. + * + * @type {Number} + * @sample {highcharts} highcharts/demo/gauge-vu-meter/ + * VU-meter with custom start and end angle + * @since 2.3.0 + * @product highcharts + */ + startAngle: 0 + }, + + /** + * An array of background items for the pane. + * @type Array. + * @sample {highcharts} highcharts/demo/gauge-speedometer/ + * Speedometer gauge with multiple backgrounds + * @optionparent pane.background + */ + defaultBackgroundOptions: { + /** + * The class name for this background. + * + * @type {String} + * @sample {highcharts} highcharts/css/pane/ Panes styled by CSS + * @sample {highstock} highcharts/css/pane/ Panes styled by CSS + * @sample {highmaps} highcharts/css/pane/ Panes styled by CSS + * @default highcharts-pane + * @since 5.0.0 + * @apioption pane.background.className + */ + + /** + * Tha shape of the pane background. When `solid`, the background + * is circular. When `arc`, the background extends only from the min + * to the max of the value axis. + * + * @validvalue ["solid", "arc"] + * @type {String} + * @default solid + * @since 2.3.0 + * @product highcharts + */ + shape: 'circle', + /*= if (build.classic) { =*/ + + /** + * The pixel border width of the pane background. + * + * @type {Number} + * @default 1 + * @since 2.3.0 + * @product highcharts + */ + borderWidth: 1, + + /** + * The pane background border color. + * + * @type {Color} + * @default #cccccc + * @since 2.3.0 + * @product highcharts + */ + borderColor: '${palette.neutralColor20}', + + /** + * The background color or gradient for the pane. + * + * @type {Color} + * @since 2.3.0 + * @product highcharts + */ + backgroundColor: { + /** + * Definition of the gradient, similar to SVG: object literal holds + * start position (x1, y1) and the end position (x2, y2) relative + * to the shape, where 0 means top/left and 1 is bottom/right. + * All positions are floats between 0 and 1. + * + * @type {Object} + */ + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + /** + * The stops is an array of tuples, where the first + * item is a float between 0 and 1 assigning the relative position in + * the gradient, and the second item is the color. + * + * @default [[0, #ffffff], [1, #e6e6e6]] + * @type {Array} + */ + stops: [ + [0, '${palette.backgroundColor}'], + [1, '${palette.neutralColor10}'] + ] + }, + /*= } =*/ + + /** @ignore-option */ + from: -Number.MAX_VALUE, // corrected to axis min + + /** + * The inner radius of the pane background. Can be either numeric + * (pixels) or a percentage string. + * + * @type {Number|String} + * @default 0 + * @since 2.3.0 + * @product highcharts + */ + innerRadius: 0, + + /** @ignore-option */ + to: Number.MAX_VALUE, // corrected to axis max + + /** + * The outer radius of the circular pane background. Can be either + * numeric (pixels) or a percentage string. + * + * @type {Number|String} + * @default 105% + * @since 2.3.0 + * @product highcharts + */ + outerRadius: '105%' + }, + + /** + * Gets the center for the pane and its axis. + */ + updateCenter: function (axis) { + this.center = (axis || this.axis || {}).center = + CenteredSeriesMixin.getCenter.call(this); + }, + + /** + * Destroy the pane item + * / + destroy: function () { + H.erase(this.chart.pane, this); + each(this.background, function (background) { + background.destroy(); + }); + this.background.length = 0; + this.group = this.group.destroy(); + }, + */ + + /** + * Update the pane item with new options + * @param {Object} options New pane options + */ + update: function (options, redraw) { + + merge(true, this.options, options); + this.setOptions(this.options); + this.render(); + each(this.chart.axes, function (axis) { + if (axis.pane === this) { + axis.pane = null; + axis.update({}, redraw); + } + }, this); + } + }); H.Pane = Pane; diff --git a/js/parts-more/Polar.js b/js/parts-more/Polar.js index ab31b225b0b..c2b2f593e62 100644 --- a/js/parts-more/Polar.js +++ b/js/parts-more/Polar.js @@ -16,626 +16,626 @@ import '../parts/Pointer.js'; */ var each = H.each, - pick = H.pick, - Pointer = H.Pointer, - Series = H.Series, - seriesTypes = H.seriesTypes, - wrap = H.wrap, + pick = H.pick, + Pointer = H.Pointer, + Series = H.Series, + seriesTypes = H.seriesTypes, + wrap = H.wrap, - seriesProto = Series.prototype, - pointerProto = Pointer.prototype, - colProto; + seriesProto = Series.prototype, + pointerProto = Pointer.prototype, + colProto; if (!H.polarExtended) { - H.polarExtended = true; - - - /** - * Search a k-d tree by the point angle, used for shared tooltips in polar - * charts - */ - seriesProto.searchPointByAngle = function (e) { - var series = this, - chart = series.chart, - xAxis = series.xAxis, - center = xAxis.pane.center, - plotX = e.chartX - center[0] - chart.plotLeft, - plotY = e.chartY - center[1] - chart.plotTop; - - return this.searchKDTree({ - clientX: 180 + (Math.atan2(plotX, plotY) * (-180 / Math.PI)) - }); - - }; - - /** - * #6212 Calculate connectors for spline series in polar chart. - * @param {Boolean} calculateNeighbours - * Check if connectors should be calculated for neighbour points as - * well allows short recurence - */ - seriesProto.getConnectors = function ( - segment, - index, - calculateNeighbours, - connectEnds - ) { - - var i, - prevPointInd, - nextPointInd, - previousPoint, - nextPoint, - previousX, - previousY, - nextX, - nextY, - plotX, - plotY, - ret, - // 1 means control points midway between points, 2 means 1/3 from - // the point, 3 is 1/4 etc; - smoothing = 1.5, - denom = smoothing + 1, - leftContX, - leftContY, - rightContX, - rightContY, - dLControlPoint, // distance left control point - dRControlPoint, - leftContAngle, - rightContAngle, - jointAngle, - addedNumber = connectEnds ? 1 : 0; - - // Calculate final index of points depending on the initial index value. - // Because of calculating neighbours, index may be outisde segment - // array. - if (index >= 0 && index <= segment.length - 1) { - i = index; - } else if (index < 0) { - i = segment.length - 1 + index; - } else { - i = 0; - } - - prevPointInd = (i - 1 < 0) ? segment.length - (1 + addedNumber) : i - 1; - nextPointInd = (i + 1 > segment.length - 1) ? addedNumber : i + 1; - previousPoint = segment[prevPointInd]; - nextPoint = segment[nextPointInd]; - previousX = previousPoint.plotX; - previousY = previousPoint.plotY; - nextX = nextPoint.plotX; - nextY = nextPoint.plotY; - plotX = segment[i].plotX; // actual point - plotY = segment[i].plotY; - leftContX = (smoothing * plotX + previousX) / denom; - leftContY = (smoothing * plotY + previousY) / denom; - rightContX = (smoothing * plotX + nextX) / denom; - rightContY = (smoothing * plotY + nextY) / denom; - dLControlPoint = Math.sqrt( - Math.pow(leftContX - plotX, 2) + Math.pow(leftContY - plotY, 2) - ); - dRControlPoint = Math.sqrt( - Math.pow(rightContX - plotX, 2) + Math.pow(rightContY - plotY, 2) - ); - leftContAngle = Math.atan2(leftContY - plotY, leftContX - plotX); - rightContAngle = Math.atan2(rightContY - plotY, rightContX - plotX); - jointAngle = (Math.PI / 2) + ((leftContAngle + rightContAngle) / 2); - // Ensure the right direction, jointAngle should be in the same quadrant - // as leftContAngle - if (Math.abs(leftContAngle - jointAngle) > Math.PI / 2) { - jointAngle -= Math.PI; - } - // Find the corrected control points for a spline straight through the - // point - leftContX = plotX + Math.cos(jointAngle) * dLControlPoint; - leftContY = plotY + Math.sin(jointAngle) * dLControlPoint; - rightContX = plotX + Math.cos(Math.PI + jointAngle) * dRControlPoint; - rightContY = plotY + Math.sin(Math.PI + jointAngle) * dRControlPoint; - - // push current point's connectors into returned object - - ret = { - rightContX: rightContX, - rightContY: rightContY, - leftContX: leftContX, - leftContY: leftContY, - plotX: plotX, - plotY: plotY - }; - - // calculate connectors for previous and next point and push them inside - // returned object - if (calculateNeighbours) { - ret.prevPointCont = this.getConnectors( - segment, - prevPointInd, - false, - connectEnds - ); - } - return ret; - }; - - /** - * Wrap the buildKDTree function so that it searches by angle (clientX) in - * case of shared tooltip, and by two dimensional distance in case of - * non-shared. - */ - wrap(seriesProto, 'buildKDTree', function (proceed) { - if (this.chart.polar) { - if (this.kdByAngle) { - this.searchPoint = this.searchPointByAngle; - } else { - this.options.findNearestPointBy = 'xy'; - } - } - proceed.apply(this); - }); - - /** - * Translate a point's plotX and plotY from the internal angle and radius - * measures to true plotX, plotY coordinates - */ - seriesProto.toXY = function (point) { - var xy, - chart = this.chart, - plotX = point.plotX, - plotY = point.plotY, - clientX; - - // Save rectangular plotX, plotY for later computation - point.rectPlotX = plotX; - point.rectPlotY = plotY; - - // Find the polar plotX and plotY - xy = this.xAxis.postTranslate(point.plotX, this.yAxis.len - plotY); - point.plotX = point.polarPlotX = xy.x - chart.plotLeft; - point.plotY = point.polarPlotY = xy.y - chart.plotTop; - - // If shared tooltip, record the angle in degrees in order to align X - // points. Otherwise, use a standard k-d tree to get the nearest point - // in two dimensions. - if (this.kdByAngle) { - clientX = ( - (plotX / Math.PI * 180) + this.xAxis.pane.options.startAngle - ) % 360; - if (clientX < 0) { // #2665 - clientX += 360; - } - point.clientX = clientX; - } else { - point.clientX = point.plotX; - } - }; - - if (seriesTypes.spline) { - /** - * Overridden method for calculating a spline from one point to the next - */ - wrap( - seriesTypes.spline.prototype, - 'getPointSpline', - function (proceed, segment, point, i) { - var ret, - connectors; - - if (this.chart.polar) { - // moveTo or lineTo - if (!i) { - ret = ['M', point.plotX, point.plotY]; - } else { // curve from last point to this - connectors = this.getConnectors( - segment, - i, - true, - this.connectEnds - ); - ret = [ - 'C', - connectors.prevPointCont.rightContX, - connectors.prevPointCont.rightContY, - connectors.leftContX, - connectors.leftContY, - connectors.plotX, - connectors.plotY - ]; - } - } else { - ret = proceed.call(this, segment, point, i); - } - return ret; - } - ); - - // #6430 Areasplinerange series use unwrapped getPointSpline method, so - // we need to set this method again. - if (seriesTypes.areasplinerange) { - seriesTypes.areasplinerange.prototype.getPointSpline = - seriesTypes.spline.prototype.getPointSpline; - } - } - - /** - * Extend translate. The plotX and plotY values are computed as if the polar - * chart were a cartesian plane, where plotX denotes the angle in radians - * and (yAxis.len - plotY) is the pixel distance from center. - */ - H.addEvent(Series, 'afterTranslate', function () { - var chart = this.chart, - points, - i; - - if (chart.polar) { - // Postprocess plot coordinates - this.kdByAngle = chart.tooltip && chart.tooltip.shared; - - if (!this.preventPostTranslate) { - points = this.points; - i = points.length; - - while (i--) { - // Translate plotX, plotY from angle and radius to true plot - // coordinates - this.toXY(points[i]); - } - } - - // Perform clip after render - if (!this.hasClipCircleSetter) { - this.hasClipCircleSetter = Boolean( - H.addEvent(this, 'afterRender', function () { - var circ; - if (chart.polar) { - circ = this.yAxis.center; - this.group.clip( - chart.renderer.clipCircle( - circ[0], - circ[1], - circ[2] / 2 - ) - ); - this.setClip = H.noop; - } - }) - ); - } - } - }); - - /** - * Extend getSegmentPath to allow connecting ends across 0 to provide a - * closed circle in line-like series. - */ - wrap(seriesProto, 'getGraphPath', function (proceed, points) { - var series = this, - i, - firstValid, - popLastPoint; - - // Connect the path - if (this.chart.polar) { - points = points || this.points; - - // Append first valid point in order to connect the ends - for (i = 0; i < points.length; i++) { - if (!points[i].isNull) { - firstValid = i; - break; - } - } - - - /** - * Polar charts only. Whether to connect the ends of a line series - * plot across the extremes. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/line-connectends-false/ - * Do not connect - * @since 2.3.0 - * @product highcharts - * @apioption plotOptions.series.connectEnds - */ - if ( - this.options.connectEnds !== false && - firstValid !== undefined - ) { - this.connectEnds = true; // re-used in splines - points.splice(points.length, 0, points[firstValid]); - popLastPoint = true; - } - - // For area charts, pseudo points are added to the graph, now we - // need to translate these - each(points, function (point) { - if (point.polarPlotY === undefined) { - series.toXY(point); - } - }); - } - - // Run uber method - var ret = proceed.apply(this, [].slice.call(arguments, 1)); - - // #6212 points.splice method is adding points to an array. In case of - // areaspline getGraphPath method is used two times and in both times - // points are added to an array. That is why points.pop is used, to get - // unmodified points. - if (popLastPoint) { - points.pop(); - } - return ret; - }); - - - var polarAnimate = function (proceed, init) { - var chart = this.chart, - animation = this.options.animation, - group = this.group, - markerGroup = this.markerGroup, - center = this.xAxis.center, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - attribs; - - // Specific animation for polar charts - if (chart.polar) { - - // Enable animation on polar charts only in SVG. In VML, the scaling - // is different, plus animation would be so slow it would't matter. - if (chart.renderer.isSVG) { - - if (animation === true) { - animation = {}; - } - - // Initialize the animation - if (init) { - - // Scale down the group and place it in the center - attribs = { - translateX: center[0] + plotLeft, - translateY: center[1] + plotTop, - scaleX: 0.001, // #1499 - scaleY: 0.001 - }; - - group.attr(attribs); - if (markerGroup) { - markerGroup.attr(attribs); - } - - // Run the animation - } else { - attribs = { - translateX: plotLeft, - translateY: plotTop, - scaleX: 1, - scaleY: 1 - }; - group.animate(attribs, animation); - if (markerGroup) { - markerGroup.animate(attribs, animation); - } - - // Delete this function to allow it only once - this.animate = null; - } - } - - // For non-polar charts, revert to the basic animation - } else { - proceed.call(this, init); - } - }; - - // Define the animate method for regular series - wrap(seriesProto, 'animate', polarAnimate); - - - if (seriesTypes.column) { - - colProto = seriesTypes.column.prototype; - - colProto.polarArc = function (low, high, start, end) { - var center = this.xAxis.center, - len = this.yAxis.len; - - return this.chart.renderer.symbols.arc( - center[0], - center[1], - len - high, - null, - { - start: start, - end: end, - innerR: len - pick(low, len) - } - ); - }; - - /** - * Define the animate method for columnseries - */ - wrap(colProto, 'animate', polarAnimate); - - - /** - * Extend the column prototype's translate method - */ - wrap(colProto, 'translate', function (proceed) { - - var xAxis = this.xAxis, - startAngleRad = xAxis.startAngleRad, - start, - points, - point, - i; - - this.preventPostTranslate = true; - - // Run uber method - proceed.call(this); - - // Postprocess plot coordinates - if (xAxis.isRadial) { - points = this.points; - i = points.length; - while (i--) { - point = points[i]; - start = point.barX + startAngleRad; - point.shapeType = 'path'; - point.shapeArgs = { - d: this.polarArc( - point.yBottom, - point.plotY, - start, - start + point.pointWidth - ) - }; - // Provide correct plotX, plotY for tooltip - this.toXY(point); - point.tooltipPos = [point.plotX, point.plotY]; - point.ttBelow = point.plotY > xAxis.center[1]; - } - } - }); - - - /** - * Align column data labels outside the columns. #1199. - */ - wrap(colProto, 'alignDataLabel', function ( - proceed, - point, - dataLabel, - options, - alignTo, - isNew - ) { - - if (this.chart.polar) { - var angle = point.rectPlotX / Math.PI * 180, - align, - verticalAlign; - - // Align nicely outside the perimeter of the columns - if (options.align === null) { - if (angle > 20 && angle < 160) { - align = 'left'; // right hemisphere - } else if (angle > 200 && angle < 340) { - align = 'right'; // left hemisphere - } else { - align = 'center'; // top or bottom - } - options.align = align; - } - if (options.verticalAlign === null) { - if (angle < 45 || angle > 315) { - verticalAlign = 'bottom'; // top part - } else if (angle > 135 && angle < 225) { - verticalAlign = 'top'; // bottom part - } else { - verticalAlign = 'middle'; // left or right - } - options.verticalAlign = verticalAlign; - } - - seriesProto.alignDataLabel.call( - this, - point, - dataLabel, - options, - alignTo, - isNew - ); - } else { - proceed.call(this, point, dataLabel, options, alignTo, isNew); - } - - }); - } - - /** - * Extend getCoordinates to prepare for polar axis values - */ - wrap(pointerProto, 'getCoordinates', function (proceed, e) { - var chart = this.chart, - ret = { - xAxis: [], - yAxis: [] - }; - - if (chart.polar) { - - each(chart.axes, function (axis) { - var isXAxis = axis.isXAxis, - center = axis.center, - x = e.chartX - center[0] - chart.plotLeft, - y = e.chartY - center[1] - chart.plotTop; - - ret[isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - value: axis.translate( - isXAxis ? - Math.PI - Math.atan2(x, y) : // angle - // distance from center - Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), - true - ) - }); - }); - - } else { - ret = proceed.call(this, e); - } - - return ret; - }); - - H.SVGRenderer.prototype.clipCircle = function (x, y, r) { - var wrapper, - id = H.uniqueKey(), - - clipPath = this.createElement('clipPath').attr({ - id: id - }).add(this.defs); - - wrapper = this.circle(x, y, r).add(clipPath); - wrapper.id = id; - wrapper.clipPath = clipPath; - - return wrapper; - }; - - H.addEvent(H.Chart, 'getAxes', function () { - - if (!this.pane) { - this.pane = []; - } - each(H.splat(this.options.pane), function (paneOptions) { - new H.Pane( // eslint-disable-line no-new - paneOptions, - this - ); - }, this); - }); - - H.addEvent(H.Chart, 'afterDrawChartBox', function () { - each(this.pane, function (pane) { - pane.render(); - }); - }); - - /** - * Extend chart.get to also search in panes. Used internally in - * responsiveness and chart.update. - */ - wrap(H.Chart.prototype, 'get', function (proceed, id) { - return H.find(this.pane, function (pane) { - return pane.options.id === id; - }) || proceed.call(this, id); - }); + H.polarExtended = true; + + + /** + * Search a k-d tree by the point angle, used for shared tooltips in polar + * charts + */ + seriesProto.searchPointByAngle = function (e) { + var series = this, + chart = series.chart, + xAxis = series.xAxis, + center = xAxis.pane.center, + plotX = e.chartX - center[0] - chart.plotLeft, + plotY = e.chartY - center[1] - chart.plotTop; + + return this.searchKDTree({ + clientX: 180 + (Math.atan2(plotX, plotY) * (-180 / Math.PI)) + }); + + }; + + /** + * #6212 Calculate connectors for spline series in polar chart. + * @param {Boolean} calculateNeighbours + * Check if connectors should be calculated for neighbour points as + * well allows short recurence + */ + seriesProto.getConnectors = function ( + segment, + index, + calculateNeighbours, + connectEnds + ) { + + var i, + prevPointInd, + nextPointInd, + previousPoint, + nextPoint, + previousX, + previousY, + nextX, + nextY, + plotX, + plotY, + ret, + // 1 means control points midway between points, 2 means 1/3 from + // the point, 3 is 1/4 etc; + smoothing = 1.5, + denom = smoothing + 1, + leftContX, + leftContY, + rightContX, + rightContY, + dLControlPoint, // distance left control point + dRControlPoint, + leftContAngle, + rightContAngle, + jointAngle, + addedNumber = connectEnds ? 1 : 0; + + // Calculate final index of points depending on the initial index value. + // Because of calculating neighbours, index may be outisde segment + // array. + if (index >= 0 && index <= segment.length - 1) { + i = index; + } else if (index < 0) { + i = segment.length - 1 + index; + } else { + i = 0; + } + + prevPointInd = (i - 1 < 0) ? segment.length - (1 + addedNumber) : i - 1; + nextPointInd = (i + 1 > segment.length - 1) ? addedNumber : i + 1; + previousPoint = segment[prevPointInd]; + nextPoint = segment[nextPointInd]; + previousX = previousPoint.plotX; + previousY = previousPoint.plotY; + nextX = nextPoint.plotX; + nextY = nextPoint.plotY; + plotX = segment[i].plotX; // actual point + plotY = segment[i].plotY; + leftContX = (smoothing * plotX + previousX) / denom; + leftContY = (smoothing * plotY + previousY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + dLControlPoint = Math.sqrt( + Math.pow(leftContX - plotX, 2) + Math.pow(leftContY - plotY, 2) + ); + dRControlPoint = Math.sqrt( + Math.pow(rightContX - plotX, 2) + Math.pow(rightContY - plotY, 2) + ); + leftContAngle = Math.atan2(leftContY - plotY, leftContX - plotX); + rightContAngle = Math.atan2(rightContY - plotY, rightContX - plotX); + jointAngle = (Math.PI / 2) + ((leftContAngle + rightContAngle) / 2); + // Ensure the right direction, jointAngle should be in the same quadrant + // as leftContAngle + if (Math.abs(leftContAngle - jointAngle) > Math.PI / 2) { + jointAngle -= Math.PI; + } + // Find the corrected control points for a spline straight through the + // point + leftContX = plotX + Math.cos(jointAngle) * dLControlPoint; + leftContY = plotY + Math.sin(jointAngle) * dLControlPoint; + rightContX = plotX + Math.cos(Math.PI + jointAngle) * dRControlPoint; + rightContY = plotY + Math.sin(Math.PI + jointAngle) * dRControlPoint; + + // push current point's connectors into returned object + + ret = { + rightContX: rightContX, + rightContY: rightContY, + leftContX: leftContX, + leftContY: leftContY, + plotX: plotX, + plotY: plotY + }; + + // calculate connectors for previous and next point and push them inside + // returned object + if (calculateNeighbours) { + ret.prevPointCont = this.getConnectors( + segment, + prevPointInd, + false, + connectEnds + ); + } + return ret; + }; + + /** + * Wrap the buildKDTree function so that it searches by angle (clientX) in + * case of shared tooltip, and by two dimensional distance in case of + * non-shared. + */ + wrap(seriesProto, 'buildKDTree', function (proceed) { + if (this.chart.polar) { + if (this.kdByAngle) { + this.searchPoint = this.searchPointByAngle; + } else { + this.options.findNearestPointBy = 'xy'; + } + } + proceed.apply(this); + }); + + /** + * Translate a point's plotX and plotY from the internal angle and radius + * measures to true plotX, plotY coordinates + */ + seriesProto.toXY = function (point) { + var xy, + chart = this.chart, + plotX = point.plotX, + plotY = point.plotY, + clientX; + + // Save rectangular plotX, plotY for later computation + point.rectPlotX = plotX; + point.rectPlotY = plotY; + + // Find the polar plotX and plotY + xy = this.xAxis.postTranslate(point.plotX, this.yAxis.len - plotY); + point.plotX = point.polarPlotX = xy.x - chart.plotLeft; + point.plotY = point.polarPlotY = xy.y - chart.plotTop; + + // If shared tooltip, record the angle in degrees in order to align X + // points. Otherwise, use a standard k-d tree to get the nearest point + // in two dimensions. + if (this.kdByAngle) { + clientX = ( + (plotX / Math.PI * 180) + this.xAxis.pane.options.startAngle + ) % 360; + if (clientX < 0) { // #2665 + clientX += 360; + } + point.clientX = clientX; + } else { + point.clientX = point.plotX; + } + }; + + if (seriesTypes.spline) { + /** + * Overridden method for calculating a spline from one point to the next + */ + wrap( + seriesTypes.spline.prototype, + 'getPointSpline', + function (proceed, segment, point, i) { + var ret, + connectors; + + if (this.chart.polar) { + // moveTo or lineTo + if (!i) { + ret = ['M', point.plotX, point.plotY]; + } else { // curve from last point to this + connectors = this.getConnectors( + segment, + i, + true, + this.connectEnds + ); + ret = [ + 'C', + connectors.prevPointCont.rightContX, + connectors.prevPointCont.rightContY, + connectors.leftContX, + connectors.leftContY, + connectors.plotX, + connectors.plotY + ]; + } + } else { + ret = proceed.call(this, segment, point, i); + } + return ret; + } + ); + + // #6430 Areasplinerange series use unwrapped getPointSpline method, so + // we need to set this method again. + if (seriesTypes.areasplinerange) { + seriesTypes.areasplinerange.prototype.getPointSpline = + seriesTypes.spline.prototype.getPointSpline; + } + } + + /** + * Extend translate. The plotX and plotY values are computed as if the polar + * chart were a cartesian plane, where plotX denotes the angle in radians + * and (yAxis.len - plotY) is the pixel distance from center. + */ + H.addEvent(Series, 'afterTranslate', function () { + var chart = this.chart, + points, + i; + + if (chart.polar) { + // Postprocess plot coordinates + this.kdByAngle = chart.tooltip && chart.tooltip.shared; + + if (!this.preventPostTranslate) { + points = this.points; + i = points.length; + + while (i--) { + // Translate plotX, plotY from angle and radius to true plot + // coordinates + this.toXY(points[i]); + } + } + + // Perform clip after render + if (!this.hasClipCircleSetter) { + this.hasClipCircleSetter = Boolean( + H.addEvent(this, 'afterRender', function () { + var circ; + if (chart.polar) { + circ = this.yAxis.center; + this.group.clip( + chart.renderer.clipCircle( + circ[0], + circ[1], + circ[2] / 2 + ) + ); + this.setClip = H.noop; + } + }) + ); + } + } + }); + + /** + * Extend getSegmentPath to allow connecting ends across 0 to provide a + * closed circle in line-like series. + */ + wrap(seriesProto, 'getGraphPath', function (proceed, points) { + var series = this, + i, + firstValid, + popLastPoint; + + // Connect the path + if (this.chart.polar) { + points = points || this.points; + + // Append first valid point in order to connect the ends + for (i = 0; i < points.length; i++) { + if (!points[i].isNull) { + firstValid = i; + break; + } + } + + + /** + * Polar charts only. Whether to connect the ends of a line series + * plot across the extremes. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/line-connectends-false/ + * Do not connect + * @since 2.3.0 + * @product highcharts + * @apioption plotOptions.series.connectEnds + */ + if ( + this.options.connectEnds !== false && + firstValid !== undefined + ) { + this.connectEnds = true; // re-used in splines + points.splice(points.length, 0, points[firstValid]); + popLastPoint = true; + } + + // For area charts, pseudo points are added to the graph, now we + // need to translate these + each(points, function (point) { + if (point.polarPlotY === undefined) { + series.toXY(point); + } + }); + } + + // Run uber method + var ret = proceed.apply(this, [].slice.call(arguments, 1)); + + // #6212 points.splice method is adding points to an array. In case of + // areaspline getGraphPath method is used two times and in both times + // points are added to an array. That is why points.pop is used, to get + // unmodified points. + if (popLastPoint) { + points.pop(); + } + return ret; + }); + + + var polarAnimate = function (proceed, init) { + var chart = this.chart, + animation = this.options.animation, + group = this.group, + markerGroup = this.markerGroup, + center = this.xAxis.center, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + attribs; + + // Specific animation for polar charts + if (chart.polar) { + + // Enable animation on polar charts only in SVG. In VML, the scaling + // is different, plus animation would be so slow it would't matter. + if (chart.renderer.isSVG) { + + if (animation === true) { + animation = {}; + } + + // Initialize the animation + if (init) { + + // Scale down the group and place it in the center + attribs = { + translateX: center[0] + plotLeft, + translateY: center[1] + plotTop, + scaleX: 0.001, // #1499 + scaleY: 0.001 + }; + + group.attr(attribs); + if (markerGroup) { + markerGroup.attr(attribs); + } + + // Run the animation + } else { + attribs = { + translateX: plotLeft, + translateY: plotTop, + scaleX: 1, + scaleY: 1 + }; + group.animate(attribs, animation); + if (markerGroup) { + markerGroup.animate(attribs, animation); + } + + // Delete this function to allow it only once + this.animate = null; + } + } + + // For non-polar charts, revert to the basic animation + } else { + proceed.call(this, init); + } + }; + + // Define the animate method for regular series + wrap(seriesProto, 'animate', polarAnimate); + + + if (seriesTypes.column) { + + colProto = seriesTypes.column.prototype; + + colProto.polarArc = function (low, high, start, end) { + var center = this.xAxis.center, + len = this.yAxis.len; + + return this.chart.renderer.symbols.arc( + center[0], + center[1], + len - high, + null, + { + start: start, + end: end, + innerR: len - pick(low, len) + } + ); + }; + + /** + * Define the animate method for columnseries + */ + wrap(colProto, 'animate', polarAnimate); + + + /** + * Extend the column prototype's translate method + */ + wrap(colProto, 'translate', function (proceed) { + + var xAxis = this.xAxis, + startAngleRad = xAxis.startAngleRad, + start, + points, + point, + i; + + this.preventPostTranslate = true; + + // Run uber method + proceed.call(this); + + // Postprocess plot coordinates + if (xAxis.isRadial) { + points = this.points; + i = points.length; + while (i--) { + point = points[i]; + start = point.barX + startAngleRad; + point.shapeType = 'path'; + point.shapeArgs = { + d: this.polarArc( + point.yBottom, + point.plotY, + start, + start + point.pointWidth + ) + }; + // Provide correct plotX, plotY for tooltip + this.toXY(point); + point.tooltipPos = [point.plotX, point.plotY]; + point.ttBelow = point.plotY > xAxis.center[1]; + } + } + }); + + + /** + * Align column data labels outside the columns. #1199. + */ + wrap(colProto, 'alignDataLabel', function ( + proceed, + point, + dataLabel, + options, + alignTo, + isNew + ) { + + if (this.chart.polar) { + var angle = point.rectPlotX / Math.PI * 180, + align, + verticalAlign; + + // Align nicely outside the perimeter of the columns + if (options.align === null) { + if (angle > 20 && angle < 160) { + align = 'left'; // right hemisphere + } else if (angle > 200 && angle < 340) { + align = 'right'; // left hemisphere + } else { + align = 'center'; // top or bottom + } + options.align = align; + } + if (options.verticalAlign === null) { + if (angle < 45 || angle > 315) { + verticalAlign = 'bottom'; // top part + } else if (angle > 135 && angle < 225) { + verticalAlign = 'top'; // bottom part + } else { + verticalAlign = 'middle'; // left or right + } + options.verticalAlign = verticalAlign; + } + + seriesProto.alignDataLabel.call( + this, + point, + dataLabel, + options, + alignTo, + isNew + ); + } else { + proceed.call(this, point, dataLabel, options, alignTo, isNew); + } + + }); + } + + /** + * Extend getCoordinates to prepare for polar axis values + */ + wrap(pointerProto, 'getCoordinates', function (proceed, e) { + var chart = this.chart, + ret = { + xAxis: [], + yAxis: [] + }; + + if (chart.polar) { + + each(chart.axes, function (axis) { + var isXAxis = axis.isXAxis, + center = axis.center, + x = e.chartX - center[0] - chart.plotLeft, + y = e.chartY - center[1] - chart.plotTop; + + ret[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.translate( + isXAxis ? + Math.PI - Math.atan2(x, y) : // angle + // distance from center + Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), + true + ) + }); + }); + + } else { + ret = proceed.call(this, e); + } + + return ret; + }); + + H.SVGRenderer.prototype.clipCircle = function (x, y, r) { + var wrapper, + id = H.uniqueKey(), + + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); + + wrapper = this.circle(x, y, r).add(clipPath); + wrapper.id = id; + wrapper.clipPath = clipPath; + + return wrapper; + }; + + H.addEvent(H.Chart, 'getAxes', function () { + + if (!this.pane) { + this.pane = []; + } + each(H.splat(this.options.pane), function (paneOptions) { + new H.Pane( // eslint-disable-line no-new + paneOptions, + this + ); + }, this); + }); + + H.addEvent(H.Chart, 'afterDrawChartBox', function () { + each(this.pane, function (pane) { + pane.render(); + }); + }); + + /** + * Extend chart.get to also search in panes. Used internally in + * responsiveness and chart.update. + */ + wrap(H.Chart.prototype, 'get', function (proceed, id) { + return H.find(this.pane, function (pane) { + return pane.options.id === id; + }) || proceed.call(this, id); + }); } diff --git a/js/parts-more/PolygonSeries.js b/js/parts-more/PolygonSeries.js index a268c9bfc29..1078ec20d7c 100644 --- a/js/parts-more/PolygonSeries.js +++ b/js/parts-more/PolygonSeries.js @@ -11,17 +11,17 @@ import '../parts/Series.js'; import '../parts/Legend.js'; import '../parts/ScatterSeries.js'; var LegendSymbolMixin = H.LegendSymbolMixin, - noop = H.noop, - Series = H.Series, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + noop = H.noop, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; /** * A polygon series can be used to draw any freeform shape in the cartesian * coordinate system. A fill is applied with the `color` option, and * stroke is applied through `lineWidth` and `lineColor` options. Requires * the `highcharts-more.js` file. - * + * * @type {Object} * @extends plotOptions.scatter * @excluding softThreshold,threshold @@ -32,48 +32,48 @@ var LegendSymbolMixin = H.LegendSymbolMixin, * @optionparent plotOptions.polygon */ seriesType('polygon', 'scatter', { - marker: { - enabled: false, - states: { - hover: { - enabled: false - } - } - }, - stickyTracking: false, - tooltip: { - followPointer: true, - pointFormat: '' - }, - trackByArea: true + marker: { + enabled: false, + states: { + hover: { + enabled: false + } + } + }, + stickyTracking: false, + tooltip: { + followPointer: true, + pointFormat: '' + }, + trackByArea: true // Prototype members }, { - type: 'polygon', - getGraphPath: function () { + type: 'polygon', + getGraphPath: function () { - var graphPath = Series.prototype.getGraphPath.call(this), - i = graphPath.length + 1; + var graphPath = Series.prototype.getGraphPath.call(this), + i = graphPath.length + 1; - // Close all segments - while (i--) { - if ((i === graphPath.length || graphPath[i] === 'M') && i > 0) { - graphPath.splice(i, 0, 'z'); - } - } - this.areaPath = graphPath; - return graphPath; - }, - drawGraph: function () { - /*= if (build.classic) { =*/ - // Hack into the fill logic in area.drawGraph - this.options.fillColor = this.color; - /*= } =*/ - seriesTypes.area.prototype.drawGraph.call(this); - }, - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - drawTracker: Series.prototype.drawTracker, - setStackedPoints: noop // No stacking points on polygons (#5310) + // Close all segments + while (i--) { + if ((i === graphPath.length || graphPath[i] === 'M') && i > 0) { + graphPath.splice(i, 0, 'z'); + } + } + this.areaPath = graphPath; + return graphPath; + }, + drawGraph: function () { + /*= if (build.classic) { =*/ + // Hack into the fill logic in area.drawGraph + this.options.fillColor = this.color; + /*= } =*/ + seriesTypes.area.prototype.drawGraph.call(this); + }, + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + drawTracker: Series.prototype.drawTracker, + setStackedPoints: noop // No stacking points on polygons (#5310) }); @@ -81,7 +81,7 @@ seriesType('polygon', 'scatter', { /** * A `polygon` series. If the [type](#series.polygon.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.polygon * @excluding dataParser,dataURL,stack @@ -92,21 +92,21 @@ seriesType('polygon', 'scatter', { /** * An array of data points for the series. For the `polygon` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 10], @@ -114,12 +114,12 @@ seriesType('polygon', 'scatter', { * [2, 1] * ] * ``` - * + * * 3. 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.polygon.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -133,7 +133,7 @@ seriesType('polygon', 'scatter', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ diff --git a/js/parts-more/RadialAxis.js b/js/parts-more/RadialAxis.js index bcc761b667b..a918926eaa8 100644 --- a/js/parts-more/RadialAxis.js +++ b/js/parts-more/RadialAxis.js @@ -10,676 +10,676 @@ import '../parts/Axis.js'; import '../parts/Tick.js'; import './Pane.js'; var addEvent = H.addEvent, - Axis = H.Axis, - each = H.each, - extend = H.extend, - map = H.map, - merge = H.merge, - noop = H.noop, - pick = H.pick, - pInt = H.pInt, - Tick = H.Tick, - wrap = H.wrap, - correctFloat = H.correctFloat, - - - hiddenAxisMixin, // @todo Extract this to a new file - radialAxisMixin, // @todo Extract this to a new file - axisProto = Axis.prototype, - tickProto = Tick.prototype; + Axis = H.Axis, + each = H.each, + extend = H.extend, + map = H.map, + merge = H.merge, + noop = H.noop, + pick = H.pick, + pInt = H.pInt, + Tick = H.Tick, + wrap = H.wrap, + correctFloat = H.correctFloat, + + + hiddenAxisMixin, // @todo Extract this to a new file + radialAxisMixin, // @todo Extract this to a new file + axisProto = Axis.prototype, + tickProto = Tick.prototype; if (!H.radialAxisExtended) { - H.radialAxisExtended = true; - - /** - * Augmented methods for the x axis in order to hide it completely, used for - * the X axis in gauges - */ - hiddenAxisMixin = { - getOffset: noop, - redraw: function () { - this.isDirty = false; // prevent setting Y axis dirty - }, - render: function () { - this.isDirty = false; // prevent setting Y axis dirty - }, - setScale: noop, - setCategories: noop, - setTitle: noop - }; - - /** - * Augmented methods for the value axis - */ - radialAxisMixin = { - - /** - * The default options extend defaultYAxisOptions - */ - defaultRadialGaugeOptions: { - labels: { - align: 'center', - x: 0, - y: null // auto - }, - minorGridLineWidth: 0, - minorTickInterval: 'auto', - minorTickLength: 10, - minorTickPosition: 'inside', - minorTickWidth: 1, - tickLength: 10, - tickPosition: 'inside', - tickWidth: 2, - title: { - rotation: 0 - }, - zIndex: 2 // behind dials, points in the series group - }, - - // Circular axis around the perimeter of a polar chart - defaultRadialXOptions: { - gridLineWidth: 1, // spokes - labels: { - align: null, // auto - distance: 15, - x: 0, - y: null, // auto - style: { - textOverflow: 'none' // wrap lines by default (#7248) - } - }, - maxPadding: 0, - minPadding: 0, - showLastLabel: false, - tickLength: 0 - }, - - // Radial axis, like a spoke in a polar chart - defaultRadialYOptions: { - gridLineInterpolation: 'circle', - labels: { - align: 'right', - x: -3, - y: -2 - }, - showLastLabel: false, - title: { - x: 4, - text: null, - rotation: 90 - } - }, - - /** - * Merge and set options - */ - setOptions: function (userOptions) { - - var options = this.options = merge( - this.defaultOptions, - this.defaultRadialOptions, - userOptions - ); - - // Make sure the plotBands array is instanciated for each Axis - // (#2649) - if (!options.plotBands) { - options.plotBands = []; - } - - }, - - /** - * Wrap the getOffset method to return zero offset for title or labels - * in a radial axis - */ - getOffset: function () { - // Call the Axis prototype method (the method we're in now is on the - // instance) - axisProto.getOffset.call(this); - - // Title or label offsets are not counted - this.chart.axisOffset[this.side] = 0; - - }, - - - /** - * Get the path for the axis line. This method is also referenced in the - * getPlotLinePath method. - */ - getLinePath: function (lineWidth, radius) { - var center = this.center, - end, - chart = this.chart, - r = pick(radius, center[2] / 2 - this.offset), - path; - - if (this.isCircular || radius !== undefined) { - path = this.chart.renderer.symbols.arc( - this.left + center[0], - this.top + center[1], - r, - r, - { - start: this.startAngleRad, - end: this.endAngleRad, - open: true, - innerR: 0 - } - ); - - // Bounds used to position the plotLine label next to the line - // (#7117) - path.xBounds = [this.left + center[0]]; - path.yBounds = [this.top + center[1] - r]; - - } else { - end = this.postTranslate(this.angleRad, r); - path = [ - 'M', - center[0] + chart.plotLeft, - center[1] + chart.plotTop, - 'L', - end.x, - end.y - ]; - } - return path; - }, - - /** - * Override setAxisTranslation by setting the translation to the - * difference in rotation. This allows the translate method to return - * angle for any given value. - */ - setAxisTranslation: function () { - - // Call uber method - axisProto.setAxisTranslation.call(this); - - // Set transA and minPixelPadding - if (this.center) { // it's not defined the first time - if (this.isCircular) { - - this.transA = (this.endAngleRad - this.startAngleRad) / - ((this.max - this.min) || 1); - - - } else { - this.transA = ( - (this.center[2] / 2) / - ((this.max - this.min) || 1) - ); - } - - if (this.isXAxis) { - this.minPixelPadding = this.transA * this.minPointOffset; - } else { - // This is a workaround for regression #2593, but categories - // still don't position correctly. - this.minPixelPadding = 0; - } - } - }, - - /** - * In case of auto connect, add one closestPointRange to the max value - * right before tickPositions are computed, so that ticks will extend - * passed the real max. - */ - beforeSetTickPositions: function () { - // If autoConnect is true, polygonal grid lines are connected, and - // one closestPointRange is added to the X axis to prevent the last - // point from overlapping the first. - this.autoConnect = ( - this.isCircular && - pick(this.userMax, this.options.max) === undefined && - correctFloat(this.endAngleRad - this.startAngleRad) === - correctFloat(2 * Math.PI) - ); - - if (this.autoConnect) { - this.max += ( - (this.categories && 1) || - this.pointRange || - this.closestPointRange || - 0 - ); // #1197, #2260 - } - }, - - /** - * Override the setAxisSize method to use the arc's circumference as - * length. This allows tickPixelInterval to apply to pixel lengths along - * the perimeter - */ - setAxisSize: function () { - - axisProto.setAxisSize.call(this); - - if (this.isRadial) { - - // Set the center array - this.pane.updateCenter(this); - - // The sector is used in Axis.translate to compute the - // translation of reversed axis points (#2570) - if (this.isCircular) { - this.sector = this.endAngleRad - this.startAngleRad; - } - - // Axis len is used to lay out the ticks - this.len = this.width = this.height = - this.center[2] * pick(this.sector, 1) / 2; - - } - }, - - /** - * Returns the x, y coordinate of a point given by a value and a pixel - * distance from center - */ - getPosition: function (value, length) { - return this.postTranslate( - this.isCircular ? - this.translate(value) : - this.angleRad, // #2848 - pick( - this.isCircular ? length : this.translate(value), - this.center[2] / 2 - ) - this.offset - ); - }, - - /** - * Translate from intermediate plotX (angle), plotY (axis.len - radius) - * to final chart coordinates. - */ - postTranslate: function (angle, radius) { - - var chart = this.chart, - center = this.center; - - angle = this.startAngleRad + angle; - - return { - x: chart.plotLeft + center[0] + Math.cos(angle) * radius, - y: chart.plotTop + center[1] + Math.sin(angle) * radius - }; - - }, - - /** - * Find the path for plot bands along the radial axis - */ - getPlotBandPath: function (from, to, options) { - var center = this.center, - startAngleRad = this.startAngleRad, - fullRadius = center[2] / 2, - radii = [ - pick(options.outerRadius, '100%'), - options.innerRadius, - pick(options.thickness, 10) - ], - offset = Math.min(this.offset, 0), - percentRegex = /%$/, - start, - end, - open, - isCircular = this.isCircular, // X axis in a polar chart - ret; - - // Polygonal plot bands - if (this.options.gridLineInterpolation === 'polygon') { - ret = this.getPlotLinePath(from).concat( - this.getPlotLinePath(to, true) - ); - - // Circular grid bands - } else { - - // Keep within bounds - from = Math.max(from, this.min); - to = Math.min(to, this.max); - - // Plot bands on Y axis (radial axis) - inner and outer radius - // depend on to and from - if (!isCircular) { - radii[0] = this.translate(from); - radii[1] = this.translate(to); - } - - // Convert percentages to pixel values - radii = map(radii, function (radius) { - if (percentRegex.test(radius)) { - radius = (pInt(radius, 10) * fullRadius) / 100; - } - return radius; - }); - - // Handle full circle - if (options.shape === 'circle' || !isCircular) { - start = -Math.PI / 2; - end = Math.PI * 1.5; - open = true; - } else { - start = startAngleRad + this.translate(from); - end = startAngleRad + this.translate(to); - } - - radii[0] -= offset; // #5283 - radii[2] -= offset; // #5283 - - ret = this.chart.renderer.symbols.arc( - this.left + center[0], - this.top + center[1], - radii[0], - radii[0], - { - // Math is for reversed yAxis (#3606) - start: Math.min(start, end), - end: Math.max(start, end), - innerR: pick(radii[1], radii[0] - radii[2]), - open: open - } - ); - } - - return ret; - }, - - /** - * Find the path for plot lines perpendicular to the radial axis. - */ - getPlotLinePath: function (value, reverse) { - var axis = this, - center = axis.center, - chart = axis.chart, - end = axis.getPosition(value), - xAxis, - xy, - tickPositions, - ret; - - // Spokes - if (axis.isCircular) { - ret = [ - 'M', - center[0] + chart.plotLeft, - center[1] + chart.plotTop, - 'L', - end.x, - end.y - ]; - - // Concentric circles - } else if (axis.options.gridLineInterpolation === 'circle') { - value = axis.translate(value); - if (value) { // a value of 0 is in the center - ret = axis.getLinePath(0, value); - } - // Concentric polygons - } else { - // Find the X axis in the same pane - each(chart.xAxis, function (a) { - if (a.pane === axis.pane) { - xAxis = a; - } - }); - ret = []; - value = axis.translate(value); - tickPositions = xAxis.tickPositions; - if (xAxis.autoConnect) { - tickPositions = tickPositions.concat([tickPositions[0]]); - } - // Reverse the positions for concatenation of polygonal plot - // bands - if (reverse) { - tickPositions = [].concat(tickPositions).reverse(); - } - - each(tickPositions, function (pos, i) { - xy = xAxis.getPosition(pos, value); - ret.push(i ? 'L' : 'M', xy.x, xy.y); - }); - - } - return ret; - }, - - /** - * Find the position for the axis title, by default inside the gauge - */ - getTitlePosition: function () { - var center = this.center, - chart = this.chart, - titleOptions = this.options.title; - - return { - x: chart.plotLeft + center[0] + (titleOptions.x || 0), - y: ( - chart.plotTop + - center[1] - - ( - { - high: 0.5, - middle: 0.25, - low: 0 - }[titleOptions.align] * center[2] - ) + - (titleOptions.y || 0) - ) - }; - } - - }; - - /** - * Actions before axis init. - */ - addEvent(Axis, 'init', function (e) { - var chart = this.chart, - angular = chart.angular, - polar = chart.polar, - isX = this.isXAxis, - isHidden = angular && isX, - isCircular, - chartOptions = chart.options, - paneIndex = e.userOptions.pane || 0, - pane = this.pane = chart.pane && chart.pane[paneIndex]; - - // Before prototype.init - if (angular) { - extend(this, isHidden ? hiddenAxisMixin : radialAxisMixin); - isCircular = !isX; - if (isCircular) { - this.defaultRadialOptions = this.defaultRadialGaugeOptions; - } - - } else if (polar) { - extend(this, radialAxisMixin); - isCircular = isX; - this.defaultRadialOptions = isX ? - this.defaultRadialXOptions : - merge(this.defaultYAxisOptions, this.defaultRadialYOptions); - - } - - // Disable certain features on angular and polar axes - if (angular || polar) { - this.isRadial = true; - chart.inverted = false; - chartOptions.chart.zoomType = null; - } else { - this.isRadial = false; - } - - // A pointer back to this axis to borrow geometry - if (pane && isCircular) { - pane.axis = this; - } - - this.isCircular = isCircular; - - }); - - addEvent(Axis, 'afterInit', function () { - - var chart = this.chart, - options = this.options, - isHidden = chart.angular && this.isXAxis, - pane = this.pane, - paneOptions = pane && pane.options; - - if (!isHidden && pane && (chart.angular || chart.polar)) { - - // Start and end angle options are - // given in degrees relative to top, while internal computations are - // in radians relative to right (like SVG). - - // Y axis in polar charts - this.angleRad = (options.angle || 0) * Math.PI / 180; - // Gauges - this.startAngleRad = (paneOptions.startAngle - 90) * Math.PI / 180; - this.endAngleRad = ( - pick(paneOptions.endAngle, paneOptions.startAngle + 360) - 90 - ) * Math.PI / 180; // Gauges - this.offset = options.offset || 0; - - } - - }); - - /** - * Wrap auto label align to avoid setting axis-wide rotation on radial axes - * (#4920) - * @param {Function} proceed - * @returns {String} Alignment - */ - wrap(axisProto, 'autoLabelAlign', function (proceed) { - if (!this.isRadial) { - return proceed.apply(this, [].slice.call(arguments, 1)); - } // else return undefined - }); - - /** - * Add special cases within the Tick class' methods for radial axes. - */ - addEvent(Tick, 'afterGetPosition', function (e) { - if (this.axis.getPosition) { - extend(e.pos, this.axis.getPosition(this.pos)); - } - }); - - /** - * Find the center position of the label based on the distance option. - */ - addEvent(Tick, 'afterGetLabelPosition', function (e) { - var axis = this.axis, - label = this.label, - labelOptions = axis.options.labels, - optionsY = labelOptions.y, - ret, - centerSlot = 20, // 20 degrees to each side at the top and bottom - align = labelOptions.align, - angle = ( - (axis.translate(this.pos) + axis.startAngleRad + Math.PI / 2) / - Math.PI * 180 - ) % 360; - - if (axis.isRadial) { // Both X and Y axes in a polar chart - ret = axis.getPosition(this.pos, (axis.center[2] / 2) + - pick(labelOptions.distance, -25)); - - // Automatically rotated - if (labelOptions.rotation === 'auto') { - label.attr({ - rotation: angle - }); - - // Vertically centered - } else if (optionsY === null) { - optionsY = ( - axis.chart.renderer - .fontMetrics(label.styles && label.styles.fontSize).b - - label.getBBox().height / 2 - ); - } - - // Automatic alignment - if (align === null) { - if (axis.isCircular) { // Y axis - if ( - this.label.getBBox().width > - axis.len * axis.tickInterval / (axis.max - axis.min) - ) { // #3506 - centerSlot = 0; - } - if (angle > centerSlot && angle < 180 - centerSlot) { - align = 'left'; // right hemisphere - } else if ( - angle > 180 + centerSlot && - angle < 360 - centerSlot - ) { - align = 'right'; // left hemisphere - } else { - align = 'center'; // top or bottom - } - } else { - align = 'center'; - } - label.attr({ - align: align - }); - } - - e.pos.x = ret.x + labelOptions.x; - e.pos.y = ret.y + optionsY; - - } - }); - - /** - * Wrap the getMarkPath function to return the path of the radial marker - */ - wrap(tickProto, 'getMarkPath', function ( - proceed, - x, - y, - tickLength, - tickWidth, - horiz, - renderer - ) { - var axis = this.axis, - endPoint, - ret; - - if (axis.isRadial) { - endPoint = axis.getPosition( - this.pos, - axis.center[2] / 2 + tickLength - ); - ret = [ - 'M', - x, - y, - 'L', - endPoint.x, - endPoint.y - ]; - } else { - ret = proceed.call( - this, - x, - y, - tickLength, - tickWidth, - horiz, - renderer - ); - } - return ret; - }); + H.radialAxisExtended = true; + + /** + * Augmented methods for the x axis in order to hide it completely, used for + * the X axis in gauges + */ + hiddenAxisMixin = { + getOffset: noop, + redraw: function () { + this.isDirty = false; // prevent setting Y axis dirty + }, + render: function () { + this.isDirty = false; // prevent setting Y axis dirty + }, + setScale: noop, + setCategories: noop, + setTitle: noop + }; + + /** + * Augmented methods for the value axis + */ + radialAxisMixin = { + + /** + * The default options extend defaultYAxisOptions + */ + defaultRadialGaugeOptions: { + labels: { + align: 'center', + x: 0, + y: null // auto + }, + minorGridLineWidth: 0, + minorTickInterval: 'auto', + minorTickLength: 10, + minorTickPosition: 'inside', + minorTickWidth: 1, + tickLength: 10, + tickPosition: 'inside', + tickWidth: 2, + title: { + rotation: 0 + }, + zIndex: 2 // behind dials, points in the series group + }, + + // Circular axis around the perimeter of a polar chart + defaultRadialXOptions: { + gridLineWidth: 1, // spokes + labels: { + align: null, // auto + distance: 15, + x: 0, + y: null, // auto + style: { + textOverflow: 'none' // wrap lines by default (#7248) + } + }, + maxPadding: 0, + minPadding: 0, + showLastLabel: false, + tickLength: 0 + }, + + // Radial axis, like a spoke in a polar chart + defaultRadialYOptions: { + gridLineInterpolation: 'circle', + labels: { + align: 'right', + x: -3, + y: -2 + }, + showLastLabel: false, + title: { + x: 4, + text: null, + rotation: 90 + } + }, + + /** + * Merge and set options + */ + setOptions: function (userOptions) { + + var options = this.options = merge( + this.defaultOptions, + this.defaultRadialOptions, + userOptions + ); + + // Make sure the plotBands array is instanciated for each Axis + // (#2649) + if (!options.plotBands) { + options.plotBands = []; + } + + }, + + /** + * Wrap the getOffset method to return zero offset for title or labels + * in a radial axis + */ + getOffset: function () { + // Call the Axis prototype method (the method we're in now is on the + // instance) + axisProto.getOffset.call(this); + + // Title or label offsets are not counted + this.chart.axisOffset[this.side] = 0; + + }, + + + /** + * Get the path for the axis line. This method is also referenced in the + * getPlotLinePath method. + */ + getLinePath: function (lineWidth, radius) { + var center = this.center, + end, + chart = this.chart, + r = pick(radius, center[2] / 2 - this.offset), + path; + + if (this.isCircular || radius !== undefined) { + path = this.chart.renderer.symbols.arc( + this.left + center[0], + this.top + center[1], + r, + r, + { + start: this.startAngleRad, + end: this.endAngleRad, + open: true, + innerR: 0 + } + ); + + // Bounds used to position the plotLine label next to the line + // (#7117) + path.xBounds = [this.left + center[0]]; + path.yBounds = [this.top + center[1] - r]; + + } else { + end = this.postTranslate(this.angleRad, r); + path = [ + 'M', + center[0] + chart.plotLeft, + center[1] + chart.plotTop, + 'L', + end.x, + end.y + ]; + } + return path; + }, + + /** + * Override setAxisTranslation by setting the translation to the + * difference in rotation. This allows the translate method to return + * angle for any given value. + */ + setAxisTranslation: function () { + + // Call uber method + axisProto.setAxisTranslation.call(this); + + // Set transA and minPixelPadding + if (this.center) { // it's not defined the first time + if (this.isCircular) { + + this.transA = (this.endAngleRad - this.startAngleRad) / + ((this.max - this.min) || 1); + + + } else { + this.transA = ( + (this.center[2] / 2) / + ((this.max - this.min) || 1) + ); + } + + if (this.isXAxis) { + this.minPixelPadding = this.transA * this.minPointOffset; + } else { + // This is a workaround for regression #2593, but categories + // still don't position correctly. + this.minPixelPadding = 0; + } + } + }, + + /** + * In case of auto connect, add one closestPointRange to the max value + * right before tickPositions are computed, so that ticks will extend + * passed the real max. + */ + beforeSetTickPositions: function () { + // If autoConnect is true, polygonal grid lines are connected, and + // one closestPointRange is added to the X axis to prevent the last + // point from overlapping the first. + this.autoConnect = ( + this.isCircular && + pick(this.userMax, this.options.max) === undefined && + correctFloat(this.endAngleRad - this.startAngleRad) === + correctFloat(2 * Math.PI) + ); + + if (this.autoConnect) { + this.max += ( + (this.categories && 1) || + this.pointRange || + this.closestPointRange || + 0 + ); // #1197, #2260 + } + }, + + /** + * Override the setAxisSize method to use the arc's circumference as + * length. This allows tickPixelInterval to apply to pixel lengths along + * the perimeter + */ + setAxisSize: function () { + + axisProto.setAxisSize.call(this); + + if (this.isRadial) { + + // Set the center array + this.pane.updateCenter(this); + + // The sector is used in Axis.translate to compute the + // translation of reversed axis points (#2570) + if (this.isCircular) { + this.sector = this.endAngleRad - this.startAngleRad; + } + + // Axis len is used to lay out the ticks + this.len = this.width = this.height = + this.center[2] * pick(this.sector, 1) / 2; + + } + }, + + /** + * Returns the x, y coordinate of a point given by a value and a pixel + * distance from center + */ + getPosition: function (value, length) { + return this.postTranslate( + this.isCircular ? + this.translate(value) : + this.angleRad, // #2848 + pick( + this.isCircular ? length : this.translate(value), + this.center[2] / 2 + ) - this.offset + ); + }, + + /** + * Translate from intermediate plotX (angle), plotY (axis.len - radius) + * to final chart coordinates. + */ + postTranslate: function (angle, radius) { + + var chart = this.chart, + center = this.center; + + angle = this.startAngleRad + angle; + + return { + x: chart.plotLeft + center[0] + Math.cos(angle) * radius, + y: chart.plotTop + center[1] + Math.sin(angle) * radius + }; + + }, + + /** + * Find the path for plot bands along the radial axis + */ + getPlotBandPath: function (from, to, options) { + var center = this.center, + startAngleRad = this.startAngleRad, + fullRadius = center[2] / 2, + radii = [ + pick(options.outerRadius, '100%'), + options.innerRadius, + pick(options.thickness, 10) + ], + offset = Math.min(this.offset, 0), + percentRegex = /%$/, + start, + end, + open, + isCircular = this.isCircular, // X axis in a polar chart + ret; + + // Polygonal plot bands + if (this.options.gridLineInterpolation === 'polygon') { + ret = this.getPlotLinePath(from).concat( + this.getPlotLinePath(to, true) + ); + + // Circular grid bands + } else { + + // Keep within bounds + from = Math.max(from, this.min); + to = Math.min(to, this.max); + + // Plot bands on Y axis (radial axis) - inner and outer radius + // depend on to and from + if (!isCircular) { + radii[0] = this.translate(from); + radii[1] = this.translate(to); + } + + // Convert percentages to pixel values + radii = map(radii, function (radius) { + if (percentRegex.test(radius)) { + radius = (pInt(radius, 10) * fullRadius) / 100; + } + return radius; + }); + + // Handle full circle + if (options.shape === 'circle' || !isCircular) { + start = -Math.PI / 2; + end = Math.PI * 1.5; + open = true; + } else { + start = startAngleRad + this.translate(from); + end = startAngleRad + this.translate(to); + } + + radii[0] -= offset; // #5283 + radii[2] -= offset; // #5283 + + ret = this.chart.renderer.symbols.arc( + this.left + center[0], + this.top + center[1], + radii[0], + radii[0], + { + // Math is for reversed yAxis (#3606) + start: Math.min(start, end), + end: Math.max(start, end), + innerR: pick(radii[1], radii[0] - radii[2]), + open: open + } + ); + } + + return ret; + }, + + /** + * Find the path for plot lines perpendicular to the radial axis. + */ + getPlotLinePath: function (value, reverse) { + var axis = this, + center = axis.center, + chart = axis.chart, + end = axis.getPosition(value), + xAxis, + xy, + tickPositions, + ret; + + // Spokes + if (axis.isCircular) { + ret = [ + 'M', + center[0] + chart.plotLeft, + center[1] + chart.plotTop, + 'L', + end.x, + end.y + ]; + + // Concentric circles + } else if (axis.options.gridLineInterpolation === 'circle') { + value = axis.translate(value); + if (value) { // a value of 0 is in the center + ret = axis.getLinePath(0, value); + } + // Concentric polygons + } else { + // Find the X axis in the same pane + each(chart.xAxis, function (a) { + if (a.pane === axis.pane) { + xAxis = a; + } + }); + ret = []; + value = axis.translate(value); + tickPositions = xAxis.tickPositions; + if (xAxis.autoConnect) { + tickPositions = tickPositions.concat([tickPositions[0]]); + } + // Reverse the positions for concatenation of polygonal plot + // bands + if (reverse) { + tickPositions = [].concat(tickPositions).reverse(); + } + + each(tickPositions, function (pos, i) { + xy = xAxis.getPosition(pos, value); + ret.push(i ? 'L' : 'M', xy.x, xy.y); + }); + + } + return ret; + }, + + /** + * Find the position for the axis title, by default inside the gauge + */ + getTitlePosition: function () { + var center = this.center, + chart = this.chart, + titleOptions = this.options.title; + + return { + x: chart.plotLeft + center[0] + (titleOptions.x || 0), + y: ( + chart.plotTop + + center[1] - + ( + { + high: 0.5, + middle: 0.25, + low: 0 + }[titleOptions.align] * center[2] + ) + + (titleOptions.y || 0) + ) + }; + } + + }; + + /** + * Actions before axis init. + */ + addEvent(Axis, 'init', function (e) { + var chart = this.chart, + angular = chart.angular, + polar = chart.polar, + isX = this.isXAxis, + isHidden = angular && isX, + isCircular, + chartOptions = chart.options, + paneIndex = e.userOptions.pane || 0, + pane = this.pane = chart.pane && chart.pane[paneIndex]; + + // Before prototype.init + if (angular) { + extend(this, isHidden ? hiddenAxisMixin : radialAxisMixin); + isCircular = !isX; + if (isCircular) { + this.defaultRadialOptions = this.defaultRadialGaugeOptions; + } + + } else if (polar) { + extend(this, radialAxisMixin); + isCircular = isX; + this.defaultRadialOptions = isX ? + this.defaultRadialXOptions : + merge(this.defaultYAxisOptions, this.defaultRadialYOptions); + + } + + // Disable certain features on angular and polar axes + if (angular || polar) { + this.isRadial = true; + chart.inverted = false; + chartOptions.chart.zoomType = null; + } else { + this.isRadial = false; + } + + // A pointer back to this axis to borrow geometry + if (pane && isCircular) { + pane.axis = this; + } + + this.isCircular = isCircular; + + }); + + addEvent(Axis, 'afterInit', function () { + + var chart = this.chart, + options = this.options, + isHidden = chart.angular && this.isXAxis, + pane = this.pane, + paneOptions = pane && pane.options; + + if (!isHidden && pane && (chart.angular || chart.polar)) { + + // Start and end angle options are + // given in degrees relative to top, while internal computations are + // in radians relative to right (like SVG). + + // Y axis in polar charts + this.angleRad = (options.angle || 0) * Math.PI / 180; + // Gauges + this.startAngleRad = (paneOptions.startAngle - 90) * Math.PI / 180; + this.endAngleRad = ( + pick(paneOptions.endAngle, paneOptions.startAngle + 360) - 90 + ) * Math.PI / 180; // Gauges + this.offset = options.offset || 0; + + } + + }); + + /** + * Wrap auto label align to avoid setting axis-wide rotation on radial axes + * (#4920) + * @param {Function} proceed + * @returns {String} Alignment + */ + wrap(axisProto, 'autoLabelAlign', function (proceed) { + if (!this.isRadial) { + return proceed.apply(this, [].slice.call(arguments, 1)); + } // else return undefined + }); + + /** + * Add special cases within the Tick class' methods for radial axes. + */ + addEvent(Tick, 'afterGetPosition', function (e) { + if (this.axis.getPosition) { + extend(e.pos, this.axis.getPosition(this.pos)); + } + }); + + /** + * Find the center position of the label based on the distance option. + */ + addEvent(Tick, 'afterGetLabelPosition', function (e) { + var axis = this.axis, + label = this.label, + labelOptions = axis.options.labels, + optionsY = labelOptions.y, + ret, + centerSlot = 20, // 20 degrees to each side at the top and bottom + align = labelOptions.align, + angle = ( + (axis.translate(this.pos) + axis.startAngleRad + Math.PI / 2) / + Math.PI * 180 + ) % 360; + + if (axis.isRadial) { // Both X and Y axes in a polar chart + ret = axis.getPosition(this.pos, (axis.center[2] / 2) + + pick(labelOptions.distance, -25)); + + // Automatically rotated + if (labelOptions.rotation === 'auto') { + label.attr({ + rotation: angle + }); + + // Vertically centered + } else if (optionsY === null) { + optionsY = ( + axis.chart.renderer + .fontMetrics(label.styles && label.styles.fontSize).b - + label.getBBox().height / 2 + ); + } + + // Automatic alignment + if (align === null) { + if (axis.isCircular) { // Y axis + if ( + this.label.getBBox().width > + axis.len * axis.tickInterval / (axis.max - axis.min) + ) { // #3506 + centerSlot = 0; + } + if (angle > centerSlot && angle < 180 - centerSlot) { + align = 'left'; // right hemisphere + } else if ( + angle > 180 + centerSlot && + angle < 360 - centerSlot + ) { + align = 'right'; // left hemisphere + } else { + align = 'center'; // top or bottom + } + } else { + align = 'center'; + } + label.attr({ + align: align + }); + } + + e.pos.x = ret.x + labelOptions.x; + e.pos.y = ret.y + optionsY; + + } + }); + + /** + * Wrap the getMarkPath function to return the path of the radial marker + */ + wrap(tickProto, 'getMarkPath', function ( + proceed, + x, + y, + tickLength, + tickWidth, + horiz, + renderer + ) { + var axis = this.axis, + endPoint, + ret; + + if (axis.isRadial) { + endPoint = axis.getPosition( + this.pos, + axis.center[2] / 2 + tickLength + ); + ret = [ + 'M', + x, + y, + 'L', + endPoint.x, + endPoint.y + ]; + } else { + ret = proceed.call( + this, + x, + y, + tickLength, + tickWidth, + horiz, + renderer + ); + } + return ret; + }); } diff --git a/js/parts-more/WaterfallSeries.js b/js/parts-more/WaterfallSeries.js index c8e01b903f6..aa48de7a22a 100644 --- a/js/parts-more/WaterfallSeries.js +++ b/js/parts-more/WaterfallSeries.js @@ -11,12 +11,12 @@ import '../parts/Options.js'; import '../parts/Series.js'; import '../parts/Point.js'; var correctFloat = H.correctFloat, - isNumber = H.isNumber, - pick = H.pick, - Point = H.Point, - Series = H.Series, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + isNumber = H.isNumber, + pick = H.pick, + Point = H.Point, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; /** * A waterfall chart displays sequentially introduced positive or negative @@ -32,442 +32,442 @@ var correctFloat = H.correctFloat, */ seriesType('waterfall', 'column', { - /** - * The color used specifically for positive point columns. When not - * specified, the general series color is used. - * - * In styled mode, the waterfall colors can be set with the - * `.highcharts-point-negative`, `.highcharts-sum` and - * `.highcharts-intermediate-sum` classes. - * - * @type {Color} - * @sample {highcharts} highcharts/demo/waterfall/ Waterfall - * @product highcharts - * @apioption plotOptions.waterfall.upColor - */ - - dataLabels: { - inside: true - }, - /*= if (build.classic) { =*/ - - /** - * The width of the line connecting waterfall columns. - * - * @product highcharts - */ - lineWidth: 1, - - /** - * The color of the line that connects columns in a waterfall series. - * - * In styled mode, the stroke can be set with the `.highcharts-graph` class. - * - * @type {Color} - * @default #333333 - * @since 3.0 - * @product highcharts - */ - lineColor: '${palette.neutralColor80}', - - /** - * A name for the dash style to use for the line connecting the columns - * of the waterfall series. Possible values: - * - * * Solid - * * ShortDash - * * ShortDot - * * ShortDashDot - * * ShortDashDotDot - * * Dot - * * Dash - * * LongDash - * * DashDot - * * LongDashDot - * * LongDashDotDot - * - * In styled mode, the stroke dash-array can be set with the - * `.highcharts-graph` class. - * - * @type {String} - * @default Dot - * @since 3.0 - * @product highcharts - */ - dashStyle: 'dot', - - /** - * The color of the border of each waterfall column. - * - * In styled mode, the border stroke can be set with the - * `.highcharts-point` class. - * - * @type {Color} - * @default #333333 - * @since 3.0 - * @product highcharts - */ - borderColor: '${palette.neutralColor80}', - - states: { - hover: { - lineWidthPlus: 0 // #3126 - } - } - /*= } =*/ + /** + * The color used specifically for positive point columns. When not + * specified, the general series color is used. + * + * In styled mode, the waterfall colors can be set with the + * `.highcharts-point-negative`, `.highcharts-sum` and + * `.highcharts-intermediate-sum` classes. + * + * @type {Color} + * @sample {highcharts} highcharts/demo/waterfall/ Waterfall + * @product highcharts + * @apioption plotOptions.waterfall.upColor + */ + + dataLabels: { + inside: true + }, + /*= if (build.classic) { =*/ + + /** + * The width of the line connecting waterfall columns. + * + * @product highcharts + */ + lineWidth: 1, + + /** + * The color of the line that connects columns in a waterfall series. + * + * In styled mode, the stroke can be set with the `.highcharts-graph` class. + * + * @type {Color} + * @default #333333 + * @since 3.0 + * @product highcharts + */ + lineColor: '${palette.neutralColor80}', + + /** + * A name for the dash style to use for the line connecting the columns + * of the waterfall series. Possible values: + * + * * Solid + * * ShortDash + * * ShortDot + * * ShortDashDot + * * ShortDashDotDot + * * Dot + * * Dash + * * LongDash + * * DashDot + * * LongDashDot + * * LongDashDotDot + * + * In styled mode, the stroke dash-array can be set with the + * `.highcharts-graph` class. + * + * @type {String} + * @default Dot + * @since 3.0 + * @product highcharts + */ + dashStyle: 'dot', + + /** + * The color of the border of each waterfall column. + * + * In styled mode, the border stroke can be set with the + * `.highcharts-point` class. + * + * @type {Color} + * @default #333333 + * @since 3.0 + * @product highcharts + */ + borderColor: '${palette.neutralColor80}', + + states: { + hover: { + lineWidthPlus: 0 // #3126 + } + } + /*= } =*/ // Prototype members }, { - pointValKey: 'y', - - /** - * Property needed to prevent lines between the columns from disappearing - * when negativeColor is used. - */ - showLine: true, - - /** - * Translate data points from raw values - */ - translate: function () { - var series = this, - options = series.options, - yAxis = series.yAxis, - len, - i, - points, - point, - shapeArgs, - stack, - y, - yValue, - previousY, - previousIntermediate, - range, - minPointLength = pick(options.minPointLength, 5), - halfMinPointLength = minPointLength / 2, - threshold = options.threshold, - stacking = options.stacking, - stackIndicator, - tooltipY; - - // run column series translate - seriesTypes.column.prototype.translate.apply(series); - - previousY = previousIntermediate = threshold; - points = series.points; - - for (i = 0, len = points.length; i < len; i++) { - // cache current point object - point = points[i]; - yValue = series.processedYData[i]; - shapeArgs = point.shapeArgs; - - // get current stack - stack = stacking && - yAxis.stacks[ - (series.negStacks && yValue < threshold ? '-' : '') + - series.stackKey - ]; - stackIndicator = series.getStackIndicator( - stackIndicator, - point.x, - series.index - ); - range = pick( - stack && stack[point.x].points[stackIndicator.key], - [0, yValue] - ); - - // override point value for sums - // #3710 Update point does not propagate to sum - if (point.isSum) { - point.y = correctFloat(yValue); - } else if (point.isIntermediateSum) { - point.y = correctFloat(yValue - previousIntermediate); // #3840 - } - // up points - y = Math.max(previousY, previousY + point.y) + range[0]; - shapeArgs.y = yAxis.translate(y, 0, 1, 0, 1); - - // sum points - if (point.isSum) { - shapeArgs.y = yAxis.translate(range[1], 0, 1, 0, 1); - shapeArgs.height = Math.min( - yAxis.translate(range[0], 0, 1, 0, 1), - yAxis.len - ) - shapeArgs.y; // #4256 - - } else if (point.isIntermediateSum) { - shapeArgs.y = yAxis.translate(range[1], 0, 1, 0, 1); - shapeArgs.height = Math.min( - yAxis.translate(previousIntermediate, 0, 1, 0, 1), - yAxis.len - ) - shapeArgs.y; - previousIntermediate = range[1]; - - // If it's not the sum point, update previous stack end position - // and get shape height (#3886) - } else { - shapeArgs.height = yValue > 0 ? - yAxis.translate(previousY, 0, 1, 0, 1) - shapeArgs.y : - yAxis.translate(previousY, 0, 1, 0, 1) - - yAxis.translate(previousY - yValue, 0, 1, 0, 1); - - previousY += stack && stack[point.x] ? - stack[point.x].total : - yValue; - } - - // #3952 Negative sum or intermediate sum not rendered correctly - if (shapeArgs.height < 0) { - shapeArgs.y += shapeArgs.height; - shapeArgs.height *= -1; - } - - point.plotY = shapeArgs.y = Math.round(shapeArgs.y) - - (series.borderWidth % 2) / 2; - // #3151 - shapeArgs.height = Math.max(Math.round(shapeArgs.height), 0.001); - point.yBottom = shapeArgs.y + shapeArgs.height; - - if (shapeArgs.height <= minPointLength && !point.isNull) { - shapeArgs.height = minPointLength; - shapeArgs.y -= halfMinPointLength; - point.plotY = shapeArgs.y; - if (point.y < 0) { - point.minPointLengthOffset = -halfMinPointLength; - } else { - point.minPointLengthOffset = halfMinPointLength; - } - } else { - point.minPointLengthOffset = 0; - } - - // Correct tooltip placement (#3014) - tooltipY = point.plotY + (point.negative ? shapeArgs.height : 0); - - if (series.chart.inverted) { - point.tooltipPos[0] = yAxis.len - tooltipY; - } else { - point.tooltipPos[1] = tooltipY; - } - } - }, - - /** - * Call default processData then override yData to reflect - * waterfall's extremes on yAxis - */ - processData: function (force) { - var series = this, - options = series.options, - yData = series.yData, - // #3710 Update point does not propagate to sum - points = series.options.data, - point, - dataLength = yData.length, - threshold = options.threshold || 0, - subSum, - sum, - dataMin, - dataMax, - y, - i; - - sum = subSum = dataMin = dataMax = threshold; - - for (i = 0; i < dataLength; i++) { - y = yData[i]; - point = points && points[i] ? points[i] : {}; - - if (y === 'sum' || point.isSum) { - yData[i] = correctFloat(sum); - } else if (y === 'intermediateSum' || point.isIntermediateSum) { - yData[i] = correctFloat(subSum); - } else { - sum += y; - subSum += y; - } - dataMin = Math.min(sum, dataMin); - dataMax = Math.max(sum, dataMax); - } - - Series.prototype.processData.call(this, force); - - // Record extremes only if stacking was not set: - if (!series.options.stacking) { - series.dataMin = dataMin; - series.dataMax = dataMax; - } - }, - - /** - * Return y value or string if point is sum - */ - toYData: function (pt) { - if (pt.isSum) { - // #3245 Error when first element is Sum or Intermediate Sum - return (pt.x === 0 ? null : 'sum'); - } - if (pt.isIntermediateSum) { - return (pt.x === 0 ? null : 'intermediateSum'); // #3245 - } - return pt.y; - }, - - /*= if (build.classic) { =*/ - /** - * Postprocess mapping between options and SVG attributes - */ - pointAttribs: function (point, state) { - - var upColor = this.options.upColor, - attr; - - // Set or reset up color (#3710, update to negative) - if (upColor && !point.options.color) { - point.color = point.y > 0 ? upColor : null; - } - - attr = seriesTypes.column.prototype.pointAttribs.call( - this, - point, - state - ); - - // The dashStyle option in waterfall applies to the graph, not - // the points - delete attr.dashstyle; - - return attr; - }, - /*= } =*/ - - /** - * Return an empty path initially, because we need to know the - * stroke-width in order to set the final path. - */ - getGraphPath: function () { - return ['M', 0, 0]; - }, - - /** - * Draw columns' connector lines - */ - getCrispPath: function () { - - var data = this.data, - length = data.length, - lineWidth = this.graph.strokeWidth() + this.borderWidth, - normalizer = Math.round(lineWidth) % 2 / 2, - reversedXAxis = this.xAxis.reversed, - reversedYAxis = this.yAxis.reversed, - path = [], - prevArgs, - pointArgs, - i, - d; - - for (i = 1; i < length; i++) { - pointArgs = data[i].shapeArgs; - prevArgs = data[i - 1].shapeArgs; - - d = [ - 'M', - prevArgs.x + (reversedXAxis ? 0 : prevArgs.width), - prevArgs.y + data[i - 1].minPointLengthOffset + normalizer, - 'L', - pointArgs.x + (reversedXAxis ? prevArgs.width : 0), - prevArgs.y + data[i - 1].minPointLengthOffset + normalizer - ]; - - if ( - (data[i - 1].y < 0 && !reversedYAxis) || - (data[i - 1].y > 0 && reversedYAxis) - ) { - d[2] += prevArgs.height; - d[5] += prevArgs.height; - } - - path = path.concat(d); - } - - return path; - }, - - /** - * The graph is initally drawn with an empty definition, then updated with - * crisp rendering. - */ - drawGraph: function () { - Series.prototype.drawGraph.call(this); - this.graph.attr({ - d: this.getCrispPath() - }); - }, - - /** - * Waterfall has stacking along the x-values too. - */ - setStackedPoints: function () { - var series = this, - options = series.options, - stackedYLength, - i; - - Series.prototype.setStackedPoints.apply(series, arguments); - - stackedYLength = series.stackedYData ? series.stackedYData.length : 0; - - // Start from the second point: - for (i = 1; i < stackedYLength; i++) { - if ( - !options.data[i].isSum && - !options.data[i].isIntermediateSum - ) { - // Sum previous stacked data as waterfall can grow up/down: - series.stackedYData[i] += series.stackedYData[i - 1]; - } - } - }, - - /** - * Extremes for a non-stacked series are recorded in processData. - * In case of stacking, use Series.stackedYData to calculate extremes. - */ - getExtremes: function () { - if (this.options.stacking) { - return Series.prototype.getExtremes.apply(this, arguments); - } - } + pointValKey: 'y', + + /** + * Property needed to prevent lines between the columns from disappearing + * when negativeColor is used. + */ + showLine: true, + + /** + * Translate data points from raw values + */ + translate: function () { + var series = this, + options = series.options, + yAxis = series.yAxis, + len, + i, + points, + point, + shapeArgs, + stack, + y, + yValue, + previousY, + previousIntermediate, + range, + minPointLength = pick(options.minPointLength, 5), + halfMinPointLength = minPointLength / 2, + threshold = options.threshold, + stacking = options.stacking, + stackIndicator, + tooltipY; + + // run column series translate + seriesTypes.column.prototype.translate.apply(series); + + previousY = previousIntermediate = threshold; + points = series.points; + + for (i = 0, len = points.length; i < len; i++) { + // cache current point object + point = points[i]; + yValue = series.processedYData[i]; + shapeArgs = point.shapeArgs; + + // get current stack + stack = stacking && + yAxis.stacks[ + (series.negStacks && yValue < threshold ? '-' : '') + + series.stackKey + ]; + stackIndicator = series.getStackIndicator( + stackIndicator, + point.x, + series.index + ); + range = pick( + stack && stack[point.x].points[stackIndicator.key], + [0, yValue] + ); + + // override point value for sums + // #3710 Update point does not propagate to sum + if (point.isSum) { + point.y = correctFloat(yValue); + } else if (point.isIntermediateSum) { + point.y = correctFloat(yValue - previousIntermediate); // #3840 + } + // up points + y = Math.max(previousY, previousY + point.y) + range[0]; + shapeArgs.y = yAxis.translate(y, 0, 1, 0, 1); + + // sum points + if (point.isSum) { + shapeArgs.y = yAxis.translate(range[1], 0, 1, 0, 1); + shapeArgs.height = Math.min( + yAxis.translate(range[0], 0, 1, 0, 1), + yAxis.len + ) - shapeArgs.y; // #4256 + + } else if (point.isIntermediateSum) { + shapeArgs.y = yAxis.translate(range[1], 0, 1, 0, 1); + shapeArgs.height = Math.min( + yAxis.translate(previousIntermediate, 0, 1, 0, 1), + yAxis.len + ) - shapeArgs.y; + previousIntermediate = range[1]; + + // If it's not the sum point, update previous stack end position + // and get shape height (#3886) + } else { + shapeArgs.height = yValue > 0 ? + yAxis.translate(previousY, 0, 1, 0, 1) - shapeArgs.y : + yAxis.translate(previousY, 0, 1, 0, 1) - + yAxis.translate(previousY - yValue, 0, 1, 0, 1); + + previousY += stack && stack[point.x] ? + stack[point.x].total : + yValue; + } + + // #3952 Negative sum or intermediate sum not rendered correctly + if (shapeArgs.height < 0) { + shapeArgs.y += shapeArgs.height; + shapeArgs.height *= -1; + } + + point.plotY = shapeArgs.y = Math.round(shapeArgs.y) - + (series.borderWidth % 2) / 2; + // #3151 + shapeArgs.height = Math.max(Math.round(shapeArgs.height), 0.001); + point.yBottom = shapeArgs.y + shapeArgs.height; + + if (shapeArgs.height <= minPointLength && !point.isNull) { + shapeArgs.height = minPointLength; + shapeArgs.y -= halfMinPointLength; + point.plotY = shapeArgs.y; + if (point.y < 0) { + point.minPointLengthOffset = -halfMinPointLength; + } else { + point.minPointLengthOffset = halfMinPointLength; + } + } else { + point.minPointLengthOffset = 0; + } + + // Correct tooltip placement (#3014) + tooltipY = point.plotY + (point.negative ? shapeArgs.height : 0); + + if (series.chart.inverted) { + point.tooltipPos[0] = yAxis.len - tooltipY; + } else { + point.tooltipPos[1] = tooltipY; + } + } + }, + + /** + * Call default processData then override yData to reflect + * waterfall's extremes on yAxis + */ + processData: function (force) { + var series = this, + options = series.options, + yData = series.yData, + // #3710 Update point does not propagate to sum + points = series.options.data, + point, + dataLength = yData.length, + threshold = options.threshold || 0, + subSum, + sum, + dataMin, + dataMax, + y, + i; + + sum = subSum = dataMin = dataMax = threshold; + + for (i = 0; i < dataLength; i++) { + y = yData[i]; + point = points && points[i] ? points[i] : {}; + + if (y === 'sum' || point.isSum) { + yData[i] = correctFloat(sum); + } else if (y === 'intermediateSum' || point.isIntermediateSum) { + yData[i] = correctFloat(subSum); + } else { + sum += y; + subSum += y; + } + dataMin = Math.min(sum, dataMin); + dataMax = Math.max(sum, dataMax); + } + + Series.prototype.processData.call(this, force); + + // Record extremes only if stacking was not set: + if (!series.options.stacking) { + series.dataMin = dataMin; + series.dataMax = dataMax; + } + }, + + /** + * Return y value or string if point is sum + */ + toYData: function (pt) { + if (pt.isSum) { + // #3245 Error when first element is Sum or Intermediate Sum + return (pt.x === 0 ? null : 'sum'); + } + if (pt.isIntermediateSum) { + return (pt.x === 0 ? null : 'intermediateSum'); // #3245 + } + return pt.y; + }, + + /*= if (build.classic) { =*/ + /** + * Postprocess mapping between options and SVG attributes + */ + pointAttribs: function (point, state) { + + var upColor = this.options.upColor, + attr; + + // Set or reset up color (#3710, update to negative) + if (upColor && !point.options.color) { + point.color = point.y > 0 ? upColor : null; + } + + attr = seriesTypes.column.prototype.pointAttribs.call( + this, + point, + state + ); + + // The dashStyle option in waterfall applies to the graph, not + // the points + delete attr.dashstyle; + + return attr; + }, + /*= } =*/ + + /** + * Return an empty path initially, because we need to know the + * stroke-width in order to set the final path. + */ + getGraphPath: function () { + return ['M', 0, 0]; + }, + + /** + * Draw columns' connector lines + */ + getCrispPath: function () { + + var data = this.data, + length = data.length, + lineWidth = this.graph.strokeWidth() + this.borderWidth, + normalizer = Math.round(lineWidth) % 2 / 2, + reversedXAxis = this.xAxis.reversed, + reversedYAxis = this.yAxis.reversed, + path = [], + prevArgs, + pointArgs, + i, + d; + + for (i = 1; i < length; i++) { + pointArgs = data[i].shapeArgs; + prevArgs = data[i - 1].shapeArgs; + + d = [ + 'M', + prevArgs.x + (reversedXAxis ? 0 : prevArgs.width), + prevArgs.y + data[i - 1].minPointLengthOffset + normalizer, + 'L', + pointArgs.x + (reversedXAxis ? prevArgs.width : 0), + prevArgs.y + data[i - 1].minPointLengthOffset + normalizer + ]; + + if ( + (data[i - 1].y < 0 && !reversedYAxis) || + (data[i - 1].y > 0 && reversedYAxis) + ) { + d[2] += prevArgs.height; + d[5] += prevArgs.height; + } + + path = path.concat(d); + } + + return path; + }, + + /** + * The graph is initally drawn with an empty definition, then updated with + * crisp rendering. + */ + drawGraph: function () { + Series.prototype.drawGraph.call(this); + this.graph.attr({ + d: this.getCrispPath() + }); + }, + + /** + * Waterfall has stacking along the x-values too. + */ + setStackedPoints: function () { + var series = this, + options = series.options, + stackedYLength, + i; + + Series.prototype.setStackedPoints.apply(series, arguments); + + stackedYLength = series.stackedYData ? series.stackedYData.length : 0; + + // Start from the second point: + for (i = 1; i < stackedYLength; i++) { + if ( + !options.data[i].isSum && + !options.data[i].isIntermediateSum + ) { + // Sum previous stacked data as waterfall can grow up/down: + series.stackedYData[i] += series.stackedYData[i - 1]; + } + } + }, + + /** + * Extremes for a non-stacked series are recorded in processData. + * In case of stacking, use Series.stackedYData to calculate extremes. + */ + getExtremes: function () { + if (this.options.stacking) { + return Series.prototype.getExtremes.apply(this, arguments); + } + } // Point members }, { - getClassName: function () { - var className = Point.prototype.getClassName.call(this); - - if (this.isSum) { - className += ' highcharts-sum'; - } else if (this.isIntermediateSum) { - className += ' highcharts-intermediate-sum'; - } - return className; - }, - /** - * Pass the null test in ColumnSeries.translate. - */ - isValid: function () { - return isNumber(this.y, true) || this.isSum || this.isIntermediateSum; - } - + getClassName: function () { + var className = Point.prototype.getClassName.call(this); + + if (this.isSum) { + className += ' highcharts-sum'; + } else if (this.isIntermediateSum) { + className += ' highcharts-intermediate-sum'; + } + return className; + }, + /** + * Pass the null test in ColumnSeries.translate. + */ + isValid: function () { + return isNumber(this.y, true) || this.isSum || this.isIntermediateSum; + } + }); /** * A `waterfall` series. If the [type](#series.waterfall.type) option * is not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.waterfall * @excluding dataParser,dataURL @@ -478,21 +478,21 @@ seriesType('waterfall', 'column', { /** * An array of data points for the series. For the `waterfall` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 7], @@ -500,13 +500,13 @@ seriesType('waterfall', 'column', { * [2, 3] * ] * ``` - * + * * 3. 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.waterfall.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -520,7 +520,7 @@ seriesType('waterfall', 'column', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @excluding marker @@ -533,7 +533,7 @@ seriesType('waterfall', 'column', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts * @apioption series.waterfall.data */ @@ -543,7 +543,7 @@ seriesType('waterfall', 'column', { * When this property is true, the points acts as a summary column for * the values added or substracted since the last intermediate sum, * or since the start of the series. The `y` value is ignored. - * + * * @type {Boolean} * @sample {highcharts} highcharts/demo/waterfall/ Waterfall * @default false @@ -554,7 +554,7 @@ seriesType('waterfall', 'column', { /** * When this property is true, the point display the total sum across * the entire series. The `y` value is ignored. - * + * * @type {Boolean} * @sample {highcharts} highcharts/demo/waterfall/ Waterfall * @default false diff --git a/js/parts.js b/js/parts.js index 2d216b1a32a..ee55a2687d7 100644 --- a/js/parts.js +++ b/js/parts.js @@ -1,662 +1,662 @@ 'use strict'; /* eslint-disable max-len, no-unused-vars */ var HighchartsConfig = { - 'version': [{ - 'highcharts': '4.0.1-modified' - }, { - 'Highstock': '2.0.1-modified' - }], - 'parts': [{ - 'name': 'Intro', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Globals', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Utilities', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Options', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Color', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'SvgRenderer', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Html', - 'component': 'Html', - 'group': 'Features', - 'baseUrl': 'parts', - 'depends': { - 'component': ['Core'] - } - }, { - 'name': 'VmlRenderer', - 'component': 'VML renderer', - 'group': 'Renderers', - 'depends': { - 'component': ['Html'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Tick', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'PlotLineOrBand', - 'component': 'Plotlines or bands', - 'group': 'Features', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Axis', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'DateTimeAxis', - 'component': 'Datetime axis', - 'group': 'Features', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'LogarithmicAxis', - 'component': 'Logarithmic axis', - 'group': 'Features', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Tooltip', - 'component': 'Tooltip', - 'group': 'Dynamics and Interaction', - 'depends': { - 'component': ['Interaction'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Pointer', - 'component': 'Interaction', - 'group': 'Dynamics and Interaction', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'TouchPointer', - 'component': 'Touch', - 'group': 'Dynamics and Interaction', - 'depends': { - 'component': ['Interaction', 'Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'MSPointer', - 'component': 'MS Touch', - 'group': 'Dynamics and Interaction', - 'depends': { - 'component': ['Touch'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Legend', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Chart', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'CenteredSeriesMixin', - 'component': 'CenteredSeriesMixin', - 'baseUrl': 'parts' - }, { - 'name': 'Point', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Series', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Stacking', - 'component': 'Stacking', - 'group': 'Features', - 'baseUrl': 'parts' - }, { - 'name': 'Dynamics', - 'component': 'Dynamics', - 'group': 'Dynamics and Interaction', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'LineSeries', - 'component': 'Line', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'AreaSeries', - 'component': 'Area', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'SplineSeries', - 'component': 'Spline', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'AreaSplineSeries', - 'component': 'AreaSpline', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core', 'Area', 'Spline'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'ColumnSeries', - 'component': 'Column', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'BarSeries', - 'component': 'Bar', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core', 'Column'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'ScatterSeries', - 'component': 'Scatter', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core', 'Column'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'PieSeries', - 'component': 'Pie', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core'], - 'name': ['CenteredSeriesMixin'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'DataLabels', - 'component': 'Datalabels', - 'group': 'Features', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Interaction', - 'component': 'Interaction', - 'group': 'Dynamics and Interaction', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'OrdinalAxis', - 'component': 'Stock', - 'group': 'Stock', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'DataGrouping', - 'component': 'Stock', - 'group': 'Stock', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'OHLCSeries', - 'component': 'OHLC', - 'group': 'Stock', - 'depends': { - 'component': ['Stock', 'Column'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'CandlestickSeries', - 'component': 'Candlestick', - 'group': 'Stock', - 'depends': { - 'component': ['Stock', 'OHLC', 'Column'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'FlagsSeries', - 'component': 'Flags', - 'group': 'Stock', - 'depends': { - 'component': ['Stock', 'Column'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Scroller', - 'component': 'Stock', - 'group': 'Stock', - 'depends': { - 'component': ['Core', 'Line'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'RangeSelector', - 'component': 'Stock', - 'group': 'Stock', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'StockNavigation', - 'component': 'Stock', - 'group': 'Stock', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'StockChart', - 'component': 'Stock', - 'group': 'Stock', - 'depends': { - 'component': ['Core', 'Interaction', 'Tooltip'] - }, - 'baseUrl': 'parts' - }, { - 'name': 'Pane', - 'baseUrl': 'parts-more' - }, { - 'name': 'RadialAxis', - 'depends': { - 'name': ['CenteredSeriesMixin'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'AreaRangeSeries', - 'component': 'Arearange', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Column', 'Area'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'AreaSplineRangeSeries', - 'component': 'Areasplinerange', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Arearange', 'Spline'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'ColumnRangeSeries', - 'component': 'Columnrange', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core', 'Column', 'Arearange'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'GaugeSeries', - 'component': 'Gauge', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core', 'Line'], - 'name': ['RadialAxis', 'Pane', 'PlotLineOrBand'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'BoxPlotSeries', - 'component': 'Boxplot', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Column'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'ErrorBarSeries', - 'component': 'Errorbar', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Boxplot'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'WaterfallSeries', - 'component': 'Waterfall', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Column', 'Stacking'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'BubbleSeries', - 'component': 'Bubble', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core', 'Scatter'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'Polar', - 'component': 'Polar', - 'group': 'Features', - 'depends': { - 'component': ['Core'], - 'name': ['RadialAxis', 'Pane', 'Column', 'Area'] - }, - 'baseUrl': 'parts-more' - }, { - 'name': 'Facade', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'Outro', - 'component': 'Core', - 'group': 'Core', - 'baseUrl': 'parts' - }, { - 'name': 'funnel.src', - 'component': 'Funnel', - 'group': 'Chart and Serie types', - 'depends': { - 'component': ['Core', 'Datalabels', 'Pie'] - }, - 'baseUrl': 'modules' - }, { - 'name': 'exporting.src', - 'component': 'Exporting', - 'group': 'Modules', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'modules' - }, { - 'name': 'offline-exporting.src', - 'component': 'Offline exporting', - 'group': 'Modules', - 'depends': { - 'component': ['Core', 'Exporting'] - }, - 'baseUrl': 'modules' - }, { - 'name': 'data.src', - 'component': 'Data', - 'group': 'Modules', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'modules' - }, { - 'name': 'no-data-to-display.src', - 'component': 'No data to display', - 'group': 'Modules', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'modules' - }, { - 'name': 'drilldown.src', - 'component': 'Drilldown', - 'group': 'Modules', - 'depends': { - 'component': ['Core'] - }, - 'baseUrl': 'modules' - }, { - 'name': 'solid-gauge.src', - 'component': 'Solid gauge', - 'group': 'Modules', - 'depends': { - 'component': ['Gauge'] - }, - 'baseUrl': 'modules' - }, { - 'name': 'HeatmapIntro', - 'component': 'Heatmap', - 'group': 'Modules', - 'depends': { - 'component': ['Core', 'Column', 'Scatter'] - }, - 'baseUrl': 'parts-map' - }, { - 'name': 'HeatmapGlobals', - 'component': 'Heatmap', - 'group': 'Modules', - 'depends': { - 'component': [] - }, - 'baseUrl': 'parts-map' - }, { - 'name': 'ColorAxis', - 'component': 'Heatmap', - 'group': 'Modules', - 'depends': { - 'component': [] - }, - 'baseUrl': 'parts-map' - }, { - 'name': 'ColorSeriesMixin', - 'component': 'Heatmap', - 'group': 'Modules', - 'depends': { - 'component': [] - }, - 'baseUrl': 'parts-map' - }, { - 'name': 'HeatmapSeries', - 'component': 'Heatmap', - 'group': 'Modules', - 'depends': { - 'component': [] - }, - 'baseUrl': 'parts-map' - }, { - 'name': 'Outro', - 'component': 'Heatmap', - 'group': 'Modules', - 'depends': { - 'component': [] - }, - 'baseUrl': 'parts-map' - }], - 'groups': { - 'Core': { - 'description': 'The Core of Highcharts', - 'depends': { - 'component': ['Line'] - } - }, - 'Stock': { - 'description': 'Highstock lets you create stock or general timeline charts' - }, - 'Chart and Serie types': { - 'description': 'All the serie types available with Highcharts. Note: Line series is the base serie, required by the Core module' - }, - 'Features': { - 'description': 'Enable behaviours to the chart' - }, - 'Renderers': { - 'description': 'Alternatives to standard SVG rendering' - }, - 'Modules': { - 'description': '' - }, - - 'Dynamics and Interaction': { - 'description': 'Leaving these out makes your chart completely static' - } - }, - 'components': { - 'Core': { - 'description': 'This module is required for all other modules.' - }, - 'Stock': { - 'description': 'For general stock and timeline chart, including navigator, scrollbar and range selector' - }, - 'VML renderer': { - 'description': 'This concerns old IE, which doesn\'t support SVG.' - }, - 'Tooltip': { - 'description': 'The tooltip appears when hovering over a point in a series' - }, - 'Interaction': { - 'description': 'Enabling mouse interaction with the chart' - }, - 'Touch': { - 'description': 'Zooming the preferred way, by two-finger gestures. In response to the zoomType settings, the charts can be zoomed in and out as well as panned by one finger.' - }, - 'Html': { - 'description': 'Use HTML to render the contents of the tooltip instead of SVG. Using HTML allows advanced formatting like tables and images in the tooltip. It is also recommended for rtl languages' - }, - 'Datetime axis': { - 'description': 'Enable support for an Axis based on time units' - }, - 'Plotlines or bands': { - 'description': 'Enable drawing plotlines and -bands on your chart.' - }, - 'Logarithmic axis': { - 'description': 'Enable logarithmic axis. On a logarithmic axis the numbers along the axis increase logarithmically and the axis adjusts itself to the data series present in the chart.' - }, - 'Stacking': { - 'description': 'Stack the data in your series on top of each other instead of overlapping.' - }, - 'Datalabels': { - 'description': 'Data labels display each point\'s value or other information related to the point' - }, - 'Polar': { - 'description': 'For turning the regular chart into a polar chart.' - }, - 'MS Touch': { - 'description': 'Optimised touch support for Microsoft touch devices' - }, - 'Dynamics': { - 'description': 'Adds support for creating more dynamic charts, by adding API methods for adding series, points, etc.' - }, - 'Line': { - 'description': '' - }, - 'Area': { - 'description': '' - }, - 'Spline': { - 'description': '' - }, - 'Column': { - 'description': '' - }, - 'Bar': { - 'description': '' - }, - 'Scatter': { - 'description': '' - }, - 'Pie': { - 'description': '' - }, - 'Arearange': { - 'description': '' - }, - 'Areaspline': { - 'description': '' - }, - 'Areasplinerange': { - 'description': '' - }, - 'Columnrange': { - 'description': '' - }, - 'Gauge': { - 'description': '' - }, - 'BoxPlot': { - 'description': 'A box plot, or box-and-whiskers chart, displays groups of data by their five point summaries: minimum, lower quartile, median, upper quartile and maximum. ' - }, - 'Bubble': { - 'description': 'Bubble charts allow three dimensional data to be plotted in an X/Y diagram with sized bubbles.' - }, - 'Waterfall': { - 'description': 'Waterfall charts display the cumulative effects of income and expences, or other similar data. In Highcharts, a point can either be positive or negative, an intermediate sum or the total sum.' - }, - 'Funnel': { - 'description': 'A funnel is a chart type mainly used by sales personnel to monitor the stages of the sales cycle, from first interest to the closed sale.' - }, - 'ErrorBar': { - 'description': 'An error bar series is a secondary series that lies on top of a parent series and displays the possible error range of each parent point.' - }, - 'OHLC': { - 'description': 'The Open-High-Low-Close chart is typically used to illustrate movements in the price over time' - }, - 'Candlestick': { - 'description': 'Like the OHLC chart, using columns to represent the range of price movement.' - }, - 'Flags': { - 'description': 'Series consists of flags marking events or points of interests' - }, - 'Exporting': { - 'description': 'For saving the chart to an image' - }, - 'Data': { - 'description': 'Intended to ease the common process of loading data from CSV, HTML tables and even Google Spreadsheets' - }, - 'No data to display': { - 'description': 'When there\'s no data to display, the chart is showing a message' - }, - 'Drilldown': { - 'description': 'Add drill down features, allowing point click to show detailed data series related to each point.' - }, - 'Solid gauge': { - 'description': 'Display your data in a solid gauge' - }, - 'Heatmap': { - 'description': 'Make heatmap out of your data' - } - } + 'version': [{ + 'highcharts': '4.0.1-modified' + }, { + 'Highstock': '2.0.1-modified' + }], + 'parts': [{ + 'name': 'Intro', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Globals', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Utilities', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Options', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Color', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'SvgRenderer', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Html', + 'component': 'Html', + 'group': 'Features', + 'baseUrl': 'parts', + 'depends': { + 'component': ['Core'] + } + }, { + 'name': 'VmlRenderer', + 'component': 'VML renderer', + 'group': 'Renderers', + 'depends': { + 'component': ['Html'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Tick', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'PlotLineOrBand', + 'component': 'Plotlines or bands', + 'group': 'Features', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Axis', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'DateTimeAxis', + 'component': 'Datetime axis', + 'group': 'Features', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'LogarithmicAxis', + 'component': 'Logarithmic axis', + 'group': 'Features', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Tooltip', + 'component': 'Tooltip', + 'group': 'Dynamics and Interaction', + 'depends': { + 'component': ['Interaction'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Pointer', + 'component': 'Interaction', + 'group': 'Dynamics and Interaction', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'TouchPointer', + 'component': 'Touch', + 'group': 'Dynamics and Interaction', + 'depends': { + 'component': ['Interaction', 'Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'MSPointer', + 'component': 'MS Touch', + 'group': 'Dynamics and Interaction', + 'depends': { + 'component': ['Touch'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Legend', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Chart', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'CenteredSeriesMixin', + 'component': 'CenteredSeriesMixin', + 'baseUrl': 'parts' + }, { + 'name': 'Point', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Series', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Stacking', + 'component': 'Stacking', + 'group': 'Features', + 'baseUrl': 'parts' + }, { + 'name': 'Dynamics', + 'component': 'Dynamics', + 'group': 'Dynamics and Interaction', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'LineSeries', + 'component': 'Line', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'AreaSeries', + 'component': 'Area', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'SplineSeries', + 'component': 'Spline', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'AreaSplineSeries', + 'component': 'AreaSpline', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core', 'Area', 'Spline'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'ColumnSeries', + 'component': 'Column', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'BarSeries', + 'component': 'Bar', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core', 'Column'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'ScatterSeries', + 'component': 'Scatter', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core', 'Column'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'PieSeries', + 'component': 'Pie', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core'], + 'name': ['CenteredSeriesMixin'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'DataLabels', + 'component': 'Datalabels', + 'group': 'Features', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Interaction', + 'component': 'Interaction', + 'group': 'Dynamics and Interaction', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'OrdinalAxis', + 'component': 'Stock', + 'group': 'Stock', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'DataGrouping', + 'component': 'Stock', + 'group': 'Stock', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'OHLCSeries', + 'component': 'OHLC', + 'group': 'Stock', + 'depends': { + 'component': ['Stock', 'Column'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'CandlestickSeries', + 'component': 'Candlestick', + 'group': 'Stock', + 'depends': { + 'component': ['Stock', 'OHLC', 'Column'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'FlagsSeries', + 'component': 'Flags', + 'group': 'Stock', + 'depends': { + 'component': ['Stock', 'Column'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Scroller', + 'component': 'Stock', + 'group': 'Stock', + 'depends': { + 'component': ['Core', 'Line'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'RangeSelector', + 'component': 'Stock', + 'group': 'Stock', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'StockNavigation', + 'component': 'Stock', + 'group': 'Stock', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'StockChart', + 'component': 'Stock', + 'group': 'Stock', + 'depends': { + 'component': ['Core', 'Interaction', 'Tooltip'] + }, + 'baseUrl': 'parts' + }, { + 'name': 'Pane', + 'baseUrl': 'parts-more' + }, { + 'name': 'RadialAxis', + 'depends': { + 'name': ['CenteredSeriesMixin'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'AreaRangeSeries', + 'component': 'Arearange', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Column', 'Area'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'AreaSplineRangeSeries', + 'component': 'Areasplinerange', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Arearange', 'Spline'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'ColumnRangeSeries', + 'component': 'Columnrange', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core', 'Column', 'Arearange'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'GaugeSeries', + 'component': 'Gauge', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core', 'Line'], + 'name': ['RadialAxis', 'Pane', 'PlotLineOrBand'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'BoxPlotSeries', + 'component': 'Boxplot', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Column'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'ErrorBarSeries', + 'component': 'Errorbar', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Boxplot'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'WaterfallSeries', + 'component': 'Waterfall', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Column', 'Stacking'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'BubbleSeries', + 'component': 'Bubble', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core', 'Scatter'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'Polar', + 'component': 'Polar', + 'group': 'Features', + 'depends': { + 'component': ['Core'], + 'name': ['RadialAxis', 'Pane', 'Column', 'Area'] + }, + 'baseUrl': 'parts-more' + }, { + 'name': 'Facade', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'Outro', + 'component': 'Core', + 'group': 'Core', + 'baseUrl': 'parts' + }, { + 'name': 'funnel.src', + 'component': 'Funnel', + 'group': 'Chart and Serie types', + 'depends': { + 'component': ['Core', 'Datalabels', 'Pie'] + }, + 'baseUrl': 'modules' + }, { + 'name': 'exporting.src', + 'component': 'Exporting', + 'group': 'Modules', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'modules' + }, { + 'name': 'offline-exporting.src', + 'component': 'Offline exporting', + 'group': 'Modules', + 'depends': { + 'component': ['Core', 'Exporting'] + }, + 'baseUrl': 'modules' + }, { + 'name': 'data.src', + 'component': 'Data', + 'group': 'Modules', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'modules' + }, { + 'name': 'no-data-to-display.src', + 'component': 'No data to display', + 'group': 'Modules', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'modules' + }, { + 'name': 'drilldown.src', + 'component': 'Drilldown', + 'group': 'Modules', + 'depends': { + 'component': ['Core'] + }, + 'baseUrl': 'modules' + }, { + 'name': 'solid-gauge.src', + 'component': 'Solid gauge', + 'group': 'Modules', + 'depends': { + 'component': ['Gauge'] + }, + 'baseUrl': 'modules' + }, { + 'name': 'HeatmapIntro', + 'component': 'Heatmap', + 'group': 'Modules', + 'depends': { + 'component': ['Core', 'Column', 'Scatter'] + }, + 'baseUrl': 'parts-map' + }, { + 'name': 'HeatmapGlobals', + 'component': 'Heatmap', + 'group': 'Modules', + 'depends': { + 'component': [] + }, + 'baseUrl': 'parts-map' + }, { + 'name': 'ColorAxis', + 'component': 'Heatmap', + 'group': 'Modules', + 'depends': { + 'component': [] + }, + 'baseUrl': 'parts-map' + }, { + 'name': 'ColorSeriesMixin', + 'component': 'Heatmap', + 'group': 'Modules', + 'depends': { + 'component': [] + }, + 'baseUrl': 'parts-map' + }, { + 'name': 'HeatmapSeries', + 'component': 'Heatmap', + 'group': 'Modules', + 'depends': { + 'component': [] + }, + 'baseUrl': 'parts-map' + }, { + 'name': 'Outro', + 'component': 'Heatmap', + 'group': 'Modules', + 'depends': { + 'component': [] + }, + 'baseUrl': 'parts-map' + }], + 'groups': { + 'Core': { + 'description': 'The Core of Highcharts', + 'depends': { + 'component': ['Line'] + } + }, + 'Stock': { + 'description': 'Highstock lets you create stock or general timeline charts' + }, + 'Chart and Serie types': { + 'description': 'All the serie types available with Highcharts. Note: Line series is the base serie, required by the Core module' + }, + 'Features': { + 'description': 'Enable behaviours to the chart' + }, + 'Renderers': { + 'description': 'Alternatives to standard SVG rendering' + }, + 'Modules': { + 'description': '' + }, + + 'Dynamics and Interaction': { + 'description': 'Leaving these out makes your chart completely static' + } + }, + 'components': { + 'Core': { + 'description': 'This module is required for all other modules.' + }, + 'Stock': { + 'description': 'For general stock and timeline chart, including navigator, scrollbar and range selector' + }, + 'VML renderer': { + 'description': 'This concerns old IE, which doesn\'t support SVG.' + }, + 'Tooltip': { + 'description': 'The tooltip appears when hovering over a point in a series' + }, + 'Interaction': { + 'description': 'Enabling mouse interaction with the chart' + }, + 'Touch': { + 'description': 'Zooming the preferred way, by two-finger gestures. In response to the zoomType settings, the charts can be zoomed in and out as well as panned by one finger.' + }, + 'Html': { + 'description': 'Use HTML to render the contents of the tooltip instead of SVG. Using HTML allows advanced formatting like tables and images in the tooltip. It is also recommended for rtl languages' + }, + 'Datetime axis': { + 'description': 'Enable support for an Axis based on time units' + }, + 'Plotlines or bands': { + 'description': 'Enable drawing plotlines and -bands on your chart.' + }, + 'Logarithmic axis': { + 'description': 'Enable logarithmic axis. On a logarithmic axis the numbers along the axis increase logarithmically and the axis adjusts itself to the data series present in the chart.' + }, + 'Stacking': { + 'description': 'Stack the data in your series on top of each other instead of overlapping.' + }, + 'Datalabels': { + 'description': 'Data labels display each point\'s value or other information related to the point' + }, + 'Polar': { + 'description': 'For turning the regular chart into a polar chart.' + }, + 'MS Touch': { + 'description': 'Optimised touch support for Microsoft touch devices' + }, + 'Dynamics': { + 'description': 'Adds support for creating more dynamic charts, by adding API methods for adding series, points, etc.' + }, + 'Line': { + 'description': '' + }, + 'Area': { + 'description': '' + }, + 'Spline': { + 'description': '' + }, + 'Column': { + 'description': '' + }, + 'Bar': { + 'description': '' + }, + 'Scatter': { + 'description': '' + }, + 'Pie': { + 'description': '' + }, + 'Arearange': { + 'description': '' + }, + 'Areaspline': { + 'description': '' + }, + 'Areasplinerange': { + 'description': '' + }, + 'Columnrange': { + 'description': '' + }, + 'Gauge': { + 'description': '' + }, + 'BoxPlot': { + 'description': 'A box plot, or box-and-whiskers chart, displays groups of data by their five point summaries: minimum, lower quartile, median, upper quartile and maximum. ' + }, + 'Bubble': { + 'description': 'Bubble charts allow three dimensional data to be plotted in an X/Y diagram with sized bubbles.' + }, + 'Waterfall': { + 'description': 'Waterfall charts display the cumulative effects of income and expences, or other similar data. In Highcharts, a point can either be positive or negative, an intermediate sum or the total sum.' + }, + 'Funnel': { + 'description': 'A funnel is a chart type mainly used by sales personnel to monitor the stages of the sales cycle, from first interest to the closed sale.' + }, + 'ErrorBar': { + 'description': 'An error bar series is a secondary series that lies on top of a parent series and displays the possible error range of each parent point.' + }, + 'OHLC': { + 'description': 'The Open-High-Low-Close chart is typically used to illustrate movements in the price over time' + }, + 'Candlestick': { + 'description': 'Like the OHLC chart, using columns to represent the range of price movement.' + }, + 'Flags': { + 'description': 'Series consists of flags marking events or points of interests' + }, + 'Exporting': { + 'description': 'For saving the chart to an image' + }, + 'Data': { + 'description': 'Intended to ease the common process of loading data from CSV, HTML tables and even Google Spreadsheets' + }, + 'No data to display': { + 'description': 'When there\'s no data to display, the chart is showing a message' + }, + 'Drilldown': { + 'description': 'Add drill down features, allowing point click to show detailed data series related to each point.' + }, + 'Solid gauge': { + 'description': 'Display your data in a solid gauge' + }, + 'Heatmap': { + 'description': 'Make heatmap out of your data' + } + } }; /* eslint-enable no-unused-vars */ diff --git a/js/parts/.eslintrc b/js/parts/.eslintrc index 74f5416c9f5..30c4026477f 100644 --- a/js/parts/.eslintrc +++ b/js/parts/.eslintrc @@ -1,5 +1,5 @@ { - "rules": { - "no-dupe-keys": 0 /*template code sometimes has one key for classic, one for unstyled*/ - } + "rules": { + "no-dupe-keys": 0 /*template code sometimes has one key for classic, one for unstyled*/ + } } \ No newline at end of file diff --git a/js/parts/AreaSeries.js b/js/parts/AreaSeries.js index 58c280d78e3..52babbf248d 100644 --- a/js/parts/AreaSeries.js +++ b/js/parts/AreaSeries.js @@ -11,12 +11,12 @@ import './Legend.js'; import './Series.js'; import './Options.js'; var color = H.color, - each = H.each, - LegendSymbolMixin = H.LegendSymbolMixin, - map = H.map, - pick = H.pick, - Series = H.Series, - seriesType = H.seriesType; + each = H.each, + LegendSymbolMixin = H.LegendSymbolMixin, + map = H.map, + pick = H.pick, + Series = H.Series, + seriesType = H.seriesType; /** * Area series type. @@ -37,454 +37,454 @@ var color = H.color, */ seriesType('area', 'line', { - /** - * Fill color or gradient for the area. When `null`, the series' `color` - * is used with the series' `fillOpacity`. - * - * In styled mode, the fill color can be set with the `.highcharts-area` - * class name. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/area-fillcolor-default/ - * Null by default - * @sample {highcharts} highcharts/plotoptions/area-fillcolor-gradient/ - * Gradient - * @default null - * @product highcharts highstock - * @apioption plotOptions.area.fillColor - */ - - /** - * Fill opacity for the area. When you set an explicit `fillColor`, - * the `fillOpacity` is not applied. Instead, you should define the - * opacity in the `fillColor` with an rgba color definition. The - * `fillOpacity` setting, also the default setting, overrides the alpha - * component of the `color` setting. - * - * In styled mode, the fill opacity can be set with the `.highcharts-area` - * class name. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/area-fillopacity/ - * Automatic fill color and fill opacity of 0.1 - * @default {highcharts} 0.75 - * @default {highstock} .75 - * @product highcharts highstock - * @apioption plotOptions.area.fillOpacity - */ - - /** - * A separate color for the graph line. By default the line takes the - * `color` of the series, but the lineColor setting allows setting a - * separate color for the line without altering the `fillColor`. - * - * In styled mode, the line stroke can be set with the `.highcharts-graph` - * class name. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/area-linecolor/ - * Dark gray line - * @default null - * @product highcharts highstock - * @apioption plotOptions.area.lineColor - */ - - /** - * A separate color for the negative part of the area. - * - * In styled mode, a negative color is set with the `.highcharts-negative` - * class name. - * - * @type {Color} - * @see [negativeColor](#plotOptions.area.negativeColor). - * @sample {highcharts} highcharts/css/series-negative-color/ - * Negative color in styled mode - * @since 3.0 - * @product highcharts - * @apioption plotOptions.area.negativeFillColor - */ - - /** - * Whether the whole area or just the line should respond to mouseover - * tooltips and other mouse or touch events. - * - * @type {Boolean} - * @sample {highcharts|highstock} - * highcharts/plotoptions/area-trackbyarea/ - * Display the tooltip when the area is hovered - * @default false - * @since 1.1.6 - * @product highcharts highstock - * @apioption plotOptions.area.trackByArea - */ - - /** - * When this is true, the series will not cause the Y axis to cross - * the zero plane (or [threshold](#plotOptions.series.threshold) option) - * unless the data actually crosses the plane. - * - * For example, if `softThreshold` is `false`, a series of 0, 1, 2, - * 3 will make the Y axis show negative values according to the `minPadding` - * option. If `softThreshold` is `true`, the Y axis starts at 0. - * - * @since 4.1.9 - * @product highcharts highstock - */ - softThreshold: false, - - /** - * The Y axis value to serve as the base for the area, for distinguishing - * between values above and below a threshold. If `null`, the area - * behaves like a line series with fill between the graph and the Y - * axis minimum. - * - * @sample {highcharts} highcharts/plotoptions/area-threshold/ - * A threshold of 100 - * @since 2.0 - * @product highcharts highstock - */ - threshold: 0 - + /** + * Fill color or gradient for the area. When `null`, the series' `color` + * is used with the series' `fillOpacity`. + * + * In styled mode, the fill color can be set with the `.highcharts-area` + * class name. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/area-fillcolor-default/ + * Null by default + * @sample {highcharts} highcharts/plotoptions/area-fillcolor-gradient/ + * Gradient + * @default null + * @product highcharts highstock + * @apioption plotOptions.area.fillColor + */ + + /** + * Fill opacity for the area. When you set an explicit `fillColor`, + * the `fillOpacity` is not applied. Instead, you should define the + * opacity in the `fillColor` with an rgba color definition. The + * `fillOpacity` setting, also the default setting, overrides the alpha + * component of the `color` setting. + * + * In styled mode, the fill opacity can be set with the `.highcharts-area` + * class name. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/area-fillopacity/ + * Automatic fill color and fill opacity of 0.1 + * @default {highcharts} 0.75 + * @default {highstock} .75 + * @product highcharts highstock + * @apioption plotOptions.area.fillOpacity + */ + + /** + * A separate color for the graph line. By default the line takes the + * `color` of the series, but the lineColor setting allows setting a + * separate color for the line without altering the `fillColor`. + * + * In styled mode, the line stroke can be set with the `.highcharts-graph` + * class name. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/area-linecolor/ + * Dark gray line + * @default null + * @product highcharts highstock + * @apioption plotOptions.area.lineColor + */ + + /** + * A separate color for the negative part of the area. + * + * In styled mode, a negative color is set with the `.highcharts-negative` + * class name. + * + * @type {Color} + * @see [negativeColor](#plotOptions.area.negativeColor). + * @sample {highcharts} highcharts/css/series-negative-color/ + * Negative color in styled mode + * @since 3.0 + * @product highcharts + * @apioption plotOptions.area.negativeFillColor + */ + + /** + * Whether the whole area or just the line should respond to mouseover + * tooltips and other mouse or touch events. + * + * @type {Boolean} + * @sample {highcharts|highstock} + * highcharts/plotoptions/area-trackbyarea/ + * Display the tooltip when the area is hovered + * @default false + * @since 1.1.6 + * @product highcharts highstock + * @apioption plotOptions.area.trackByArea + */ + + /** + * When this is true, the series will not cause the Y axis to cross + * the zero plane (or [threshold](#plotOptions.series.threshold) option) + * unless the data actually crosses the plane. + * + * For example, if `softThreshold` is `false`, a series of 0, 1, 2, + * 3 will make the Y axis show negative values according to the `minPadding` + * option. If `softThreshold` is `true`, the Y axis starts at 0. + * + * @since 4.1.9 + * @product highcharts highstock + */ + softThreshold: false, + + /** + * The Y axis value to serve as the base for the area, for distinguishing + * between values above and below a threshold. If `null`, the area + * behaves like a line series with fill between the graph and the Y + * axis minimum. + * + * @sample {highcharts} highcharts/plotoptions/area-threshold/ + * A threshold of 100 + * @since 2.0 + * @product highcharts highstock + */ + threshold: 0 + }, /** @lends seriesTypes.area.prototype */ { - singleStacks: false, - /** - * Return an array of stacked points, where null and missing points are - * replaced by dummy points in order for gaps to be drawn correctly - * in stacks. - */ - getStackPoints: function (points) { - var series = this, - segment = [], - keys = [], - xAxis = this.xAxis, - yAxis = this.yAxis, - stack = yAxis.stacks[this.stackKey], - pointMap = {}, - seriesIndex = series.index, - yAxisSeries = yAxis.series, - seriesLength = yAxisSeries.length, - visibleSeries, - upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1, - i; - - - points = points || this.points; - - if (this.options.stacking) { - - for (i = 0; i < points.length; i++) { - // Reset after point update (#7326) - points[i].leftNull = points[i].rightNull = null; - - // Create a map where we can quickly look up the points by their - // X values. - pointMap[points[i].x] = points[i]; - } - - // Sort the keys (#1651) - H.objectEach(stack, function (stackX, x) { - // nulled after switching between - // grouping and not (#1651, #2336) - if (stackX.total !== null) { - keys.push(x); - } - }); - keys.sort(function (a, b) { - return a - b; - }); - - visibleSeries = map(yAxisSeries, function () { - return this.visible; - }); - - each(keys, function (x, idx) { - var y = 0, - stackPoint, - stackedValues; - - if (pointMap[x] && !pointMap[x].isNull) { - segment.push(pointMap[x]); - - // Find left and right cliff. -1 goes left, 1 goes right. - each([-1, 1], function (direction) { - var nullName = direction === 1 ? - 'rightNull' : - 'leftNull', - cliffName = direction === 1 ? - 'rightCliff' : - 'leftCliff', - cliff = 0, - otherStack = stack[keys[idx + direction]]; - - // If there is a stack next to this one, - // to the left or to the right... - if (otherStack) { - i = seriesIndex; - // Can go either up or down, - // depending on reversedStacks - while (i >= 0 && i < seriesLength) { - stackPoint = otherStack.points[i]; - if (!stackPoint) { - // If the next point in this series - // is missing, mark the point - // with point.leftNull or - // point.rightNull = true. - if (i === seriesIndex) { - pointMap[x][nullName] = true; - - // If there are missing points in - // the next stack in any of the - // series below this one, we need - // to substract the missing values - // and add a hiatus to the left or right. - } else if (visibleSeries[i]) { - stackedValues = stack[x].points[i]; - if (stackedValues) { - cliff -= stackedValues[1] - - stackedValues[0]; - } - } - } - // When reversedStacks is true, loop up, - // else loop down - i += upOrDown; - } - } - pointMap[x][cliffName] = cliff; - }); - - - // There is no point for this X value in this series, so we - // insert a dummy point in order for the areas to be drawn - // correctly. - } else { - - // Loop down the stack to find the series below this - // one that has a value (#1991) - i = seriesIndex; - while (i >= 0 && i < seriesLength) { - stackPoint = stack[x].points[i]; - if (stackPoint) { - y = stackPoint[1]; - break; - } - // When reversedStacks is true, loop up, else loop down - i += upOrDown; - } - y = yAxis.translate(y, 0, 1, 0, 1); // #6272 - segment.push({ - isNull: true, - plotX: xAxis.translate(x, 0, 0, 0, 1), // #6272 - x: x, - plotY: y, - yBottom: y - }); - } - }); - - } - - return segment; - }, - - getGraphPath: function (points) { - var getGraphPath = Series.prototype.getGraphPath, - graphPath, - options = this.options, - stacking = options.stacking, - yAxis = this.yAxis, - topPath, - bottomPath, - bottomPoints = [], - graphPoints = [], - seriesIndex = this.index, - i, - areaPath, - plotX, - stacks = yAxis.stacks[this.stackKey], - threshold = options.threshold, - translatedThreshold = yAxis.getThreshold(options.threshold), - isNull, - yBottom, - connectNulls = options.connectNulls || stacking === 'percent', - /** - * To display null points in underlying stacked series, this - * series graph must be broken, and the area also fall down - * to fill the gap left by the null point. #2069 - */ - addDummyPoints = function (i, otherI, side) { - var point = points[i], - stackedValues = stacking && - stacks[point.x].points[seriesIndex], - nullVal = point[side + 'Null'] || 0, - cliffVal = point[side + 'Cliff'] || 0, - top, - bottom, - isNull = true; - - if (cliffVal || nullVal) { - - top = (nullVal ? stackedValues[0] : stackedValues[1]) + - cliffVal; - bottom = stackedValues[0] + cliffVal; - isNull = !!nullVal; - - } else if ( - !stacking && - points[otherI] && - points[otherI].isNull - ) { - top = bottom = threshold; - } - - // Add to the top and bottom line of the area - if (top !== undefined) { - graphPoints.push({ - plotX: plotX, - plotY: top === null ? - translatedThreshold : - yAxis.getThreshold(top), - isNull: isNull, - isCliff: true - }); - bottomPoints.push({ - plotX: plotX, - plotY: bottom === null ? - translatedThreshold : - yAxis.getThreshold(bottom), - doCurve: false // #1041, gaps in areaspline areas - }); - } - }; - - // Find what points to use - points = points || this.points; - - // Fill in missing points - if (stacking) { - points = this.getStackPoints(points); - } - - for (i = 0; i < points.length; i++) { - isNull = points[i].isNull; - plotX = pick(points[i].rectPlotX, points[i].plotX); - yBottom = pick(points[i].yBottom, translatedThreshold); - - if (!isNull || connectNulls) { - - if (!connectNulls) { - addDummyPoints(i, i - 1, 'left'); - } - // Skip null point when stacking is false and connectNulls true - if (!(isNull && !stacking && connectNulls)) { - graphPoints.push(points[i]); - bottomPoints.push({ - x: i, - plotX: plotX, - plotY: yBottom - }); - } - - if (!connectNulls) { - addDummyPoints(i, i + 1, 'right'); - } - } - } - - topPath = getGraphPath.call(this, graphPoints, true, true); - - bottomPoints.reversed = true; - bottomPath = getGraphPath.call(this, bottomPoints, true, true); - if (bottomPath.length) { - bottomPath[0] = 'L'; - } - - areaPath = topPath.concat(bottomPath); - // TODO: don't set leftCliff and rightCliff when connectNulls? - graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); - areaPath.xMap = topPath.xMap; - this.areaPath = areaPath; - - return graphPath; - }, - - /** - * Draw the graph and the underlying area. This method calls the Series base - * function and adds the area. The areaPath is calculated in the - * getSegmentPath method called from Series.prototype.drawGraph. - */ - drawGraph: function () { - - // Define or reset areaPath - this.areaPath = []; - - // Call the base method - Series.prototype.drawGraph.apply(this); - - // Define local variables - var series = this, - areaPath = this.areaPath, - options = this.options, - zones = this.zones, - props = [[ - 'area', - 'highcharts-area', - /*= if (build.classic) { =*/ - this.color, - options.fillColor - /*= } =*/ - ]]; // area name, main color, fill color - - each(zones, function (zone, i) { - props.push([ - 'zone-area-' + i, - 'highcharts-area highcharts-zone-area-' + i + ' ' + - zone.className, - /*= if (build.classic) { =*/ - zone.color || series.color, - zone.fillColor || options.fillColor - /*= } =*/ - ]); - }); - - each(props, function (prop) { - var areaKey = prop[0], - area = series[areaKey]; - - // Create or update the area - if (area) { // update - area.endX = series.preventGraphAnimation ? null : areaPath.xMap; - area.animate({ d: areaPath }); - - } else { // create - area = series[areaKey] = series.chart.renderer.path(areaPath) - .addClass(prop[1]) - .attr({ - /*= if (build.classic) { =*/ - fill: pick( - prop[3], - color(prop[2]) - .setOpacity(pick(options.fillOpacity, 0.75)) - .get() - ), - /*= } =*/ - zIndex: 0 // #1069 - }).add(series.group); - area.isArea = true; - } - area.startX = areaPath.xMap; - area.shiftUnit = options.step ? 2 : 1; - }); - }, - - drawLegendSymbol: LegendSymbolMixin.drawRectangle + singleStacks: false, + /** + * Return an array of stacked points, where null and missing points are + * replaced by dummy points in order for gaps to be drawn correctly + * in stacks. + */ + getStackPoints: function (points) { + var series = this, + segment = [], + keys = [], + xAxis = this.xAxis, + yAxis = this.yAxis, + stack = yAxis.stacks[this.stackKey], + pointMap = {}, + seriesIndex = series.index, + yAxisSeries = yAxis.series, + seriesLength = yAxisSeries.length, + visibleSeries, + upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1, + i; + + + points = points || this.points; + + if (this.options.stacking) { + + for (i = 0; i < points.length; i++) { + // Reset after point update (#7326) + points[i].leftNull = points[i].rightNull = null; + + // Create a map where we can quickly look up the points by their + // X values. + pointMap[points[i].x] = points[i]; + } + + // Sort the keys (#1651) + H.objectEach(stack, function (stackX, x) { + // nulled after switching between + // grouping and not (#1651, #2336) + if (stackX.total !== null) { + keys.push(x); + } + }); + keys.sort(function (a, b) { + return a - b; + }); + + visibleSeries = map(yAxisSeries, function () { + return this.visible; + }); + + each(keys, function (x, idx) { + var y = 0, + stackPoint, + stackedValues; + + if (pointMap[x] && !pointMap[x].isNull) { + segment.push(pointMap[x]); + + // Find left and right cliff. -1 goes left, 1 goes right. + each([-1, 1], function (direction) { + var nullName = direction === 1 ? + 'rightNull' : + 'leftNull', + cliffName = direction === 1 ? + 'rightCliff' : + 'leftCliff', + cliff = 0, + otherStack = stack[keys[idx + direction]]; + + // If there is a stack next to this one, + // to the left or to the right... + if (otherStack) { + i = seriesIndex; + // Can go either up or down, + // depending on reversedStacks + while (i >= 0 && i < seriesLength) { + stackPoint = otherStack.points[i]; + if (!stackPoint) { + // If the next point in this series + // is missing, mark the point + // with point.leftNull or + // point.rightNull = true. + if (i === seriesIndex) { + pointMap[x][nullName] = true; + + // If there are missing points in + // the next stack in any of the + // series below this one, we need + // to substract the missing values + // and add a hiatus to the left or right. + } else if (visibleSeries[i]) { + stackedValues = stack[x].points[i]; + if (stackedValues) { + cliff -= stackedValues[1] - + stackedValues[0]; + } + } + } + // When reversedStacks is true, loop up, + // else loop down + i += upOrDown; + } + } + pointMap[x][cliffName] = cliff; + }); + + + // There is no point for this X value in this series, so we + // insert a dummy point in order for the areas to be drawn + // correctly. + } else { + + // Loop down the stack to find the series below this + // one that has a value (#1991) + i = seriesIndex; + while (i >= 0 && i < seriesLength) { + stackPoint = stack[x].points[i]; + if (stackPoint) { + y = stackPoint[1]; + break; + } + // When reversedStacks is true, loop up, else loop down + i += upOrDown; + } + y = yAxis.translate(y, 0, 1, 0, 1); // #6272 + segment.push({ + isNull: true, + plotX: xAxis.translate(x, 0, 0, 0, 1), // #6272 + x: x, + plotY: y, + yBottom: y + }); + } + }); + + } + + return segment; + }, + + getGraphPath: function (points) { + var getGraphPath = Series.prototype.getGraphPath, + graphPath, + options = this.options, + stacking = options.stacking, + yAxis = this.yAxis, + topPath, + bottomPath, + bottomPoints = [], + graphPoints = [], + seriesIndex = this.index, + i, + areaPath, + plotX, + stacks = yAxis.stacks[this.stackKey], + threshold = options.threshold, + translatedThreshold = yAxis.getThreshold(options.threshold), + isNull, + yBottom, + connectNulls = options.connectNulls || stacking === 'percent', + /** + * To display null points in underlying stacked series, this + * series graph must be broken, and the area also fall down + * to fill the gap left by the null point. #2069 + */ + addDummyPoints = function (i, otherI, side) { + var point = points[i], + stackedValues = stacking && + stacks[point.x].points[seriesIndex], + nullVal = point[side + 'Null'] || 0, + cliffVal = point[side + 'Cliff'] || 0, + top, + bottom, + isNull = true; + + if (cliffVal || nullVal) { + + top = (nullVal ? stackedValues[0] : stackedValues[1]) + + cliffVal; + bottom = stackedValues[0] + cliffVal; + isNull = !!nullVal; + + } else if ( + !stacking && + points[otherI] && + points[otherI].isNull + ) { + top = bottom = threshold; + } + + // Add to the top and bottom line of the area + if (top !== undefined) { + graphPoints.push({ + plotX: plotX, + plotY: top === null ? + translatedThreshold : + yAxis.getThreshold(top), + isNull: isNull, + isCliff: true + }); + bottomPoints.push({ + plotX: plotX, + plotY: bottom === null ? + translatedThreshold : + yAxis.getThreshold(bottom), + doCurve: false // #1041, gaps in areaspline areas + }); + } + }; + + // Find what points to use + points = points || this.points; + + // Fill in missing points + if (stacking) { + points = this.getStackPoints(points); + } + + for (i = 0; i < points.length; i++) { + isNull = points[i].isNull; + plotX = pick(points[i].rectPlotX, points[i].plotX); + yBottom = pick(points[i].yBottom, translatedThreshold); + + if (!isNull || connectNulls) { + + if (!connectNulls) { + addDummyPoints(i, i - 1, 'left'); + } + // Skip null point when stacking is false and connectNulls true + if (!(isNull && !stacking && connectNulls)) { + graphPoints.push(points[i]); + bottomPoints.push({ + x: i, + plotX: plotX, + plotY: yBottom + }); + } + + if (!connectNulls) { + addDummyPoints(i, i + 1, 'right'); + } + } + } + + topPath = getGraphPath.call(this, graphPoints, true, true); + + bottomPoints.reversed = true; + bottomPath = getGraphPath.call(this, bottomPoints, true, true); + if (bottomPath.length) { + bottomPath[0] = 'L'; + } + + areaPath = topPath.concat(bottomPath); + // TODO: don't set leftCliff and rightCliff when connectNulls? + graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); + areaPath.xMap = topPath.xMap; + this.areaPath = areaPath; + + return graphPath; + }, + + /** + * Draw the graph and the underlying area. This method calls the Series base + * function and adds the area. The areaPath is calculated in the + * getSegmentPath method called from Series.prototype.drawGraph. + */ + drawGraph: function () { + + // Define or reset areaPath + this.areaPath = []; + + // Call the base method + Series.prototype.drawGraph.apply(this); + + // Define local variables + var series = this, + areaPath = this.areaPath, + options = this.options, + zones = this.zones, + props = [[ + 'area', + 'highcharts-area', + /*= if (build.classic) { =*/ + this.color, + options.fillColor + /*= } =*/ + ]]; // area name, main color, fill color + + each(zones, function (zone, i) { + props.push([ + 'zone-area-' + i, + 'highcharts-area highcharts-zone-area-' + i + ' ' + + zone.className, + /*= if (build.classic) { =*/ + zone.color || series.color, + zone.fillColor || options.fillColor + /*= } =*/ + ]); + }); + + each(props, function (prop) { + var areaKey = prop[0], + area = series[areaKey]; + + // Create or update the area + if (area) { // update + area.endX = series.preventGraphAnimation ? null : areaPath.xMap; + area.animate({ d: areaPath }); + + } else { // create + area = series[areaKey] = series.chart.renderer.path(areaPath) + .addClass(prop[1]) + .attr({ + /*= if (build.classic) { =*/ + fill: pick( + prop[3], + color(prop[2]) + .setOpacity(pick(options.fillOpacity, 0.75)) + .get() + ), + /*= } =*/ + zIndex: 0 // #1069 + }).add(series.group); + area.isArea = true; + } + area.startX = areaPath.xMap; + area.shiftUnit = options.step ? 2 : 1; + }); + }, + + drawLegendSymbol: LegendSymbolMixin.drawRectangle }); /** * A `area` series. If the [type](#series.area.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.area * @excluding dataParser,dataURL @@ -495,21 +495,21 @@ seriesType('area', 'line', { /** * An array of data points for the series. For the `area` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 9], @@ -517,12 +517,12 @@ seriesType('area', 'line', { * [2, 6] * ] * ``` - * + * * 3. 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.area.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -536,7 +536,7 @@ seriesType('area', 'line', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -548,7 +548,7 @@ seriesType('area', 'line', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts highstock * @apioption series.area.data */ diff --git a/js/parts/AreaSplineSeries.js b/js/parts/AreaSplineSeries.js index ba55c2d7418..5c23e1b3015 100644 --- a/js/parts/AreaSplineSeries.js +++ b/js/parts/AreaSplineSeries.js @@ -10,16 +10,16 @@ import './Legend.js'; import './AreaSeries.js'; import './SplineSeries.js'; var areaProto = H.seriesTypes.area.prototype, - defaultPlotOptions = H.defaultPlotOptions, - LegendSymbolMixin = H.LegendSymbolMixin, - seriesType = H.seriesType; + defaultPlotOptions = H.defaultPlotOptions, + LegendSymbolMixin = H.LegendSymbolMixin, + seriesType = H.seriesType; /** * AreaSplineSeries object */ /** * The area spline series is an area series where the graph between the points * is smoothed into a spline. - * + * * @extends plotOptions.area * @excluding step * @sample {highcharts} highcharts/demo/areaspline/ Area spline chart @@ -28,16 +28,16 @@ var areaProto = H.seriesTypes.area.prototype, * @apioption plotOptions.areaspline */ seriesType('areaspline', 'spline', defaultPlotOptions.area, { - getStackPoints: areaProto.getStackPoints, - getGraphPath: areaProto.getGraphPath, - drawGraph: areaProto.drawGraph, - drawLegendSymbol: LegendSymbolMixin.drawRectangle + getStackPoints: areaProto.getStackPoints, + getGraphPath: areaProto.getGraphPath, + drawGraph: areaProto.drawGraph, + drawLegendSymbol: LegendSymbolMixin.drawRectangle }); /** * A `areaspline` series. If the [type](#series.areaspline.type) option * is not specified, it is inherited from [chart.type](#chart.type). - * - * + * + * * @type {Object} * @extends series,plotOptions.areaspline * @excluding dataParser,dataURL @@ -49,21 +49,21 @@ seriesType('areaspline', 'spline', defaultPlotOptions.area, { /** * An array of data points for the series. For the `areaspline` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 10], @@ -71,13 +71,13 @@ seriesType('areaspline', 'spline', defaultPlotOptions.area, { * [2, 3] * ] * ``` - * + * * 3. 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.areaspline.turboThreshold), this option is not * available. - * + * * ```js * data: [{ * x: 1, @@ -91,7 +91,7 @@ seriesType('areaspline', 'spline', defaultPlotOptions.area, { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ diff --git a/js/parts/Axis.js b/js/parts/Axis.js index 81eee5ff168..fb6dcb8d90c 100644 --- a/js/parts/Axis.js +++ b/js/parts/Axis.js @@ -11,5023 +11,5023 @@ import './Options.js'; import './Tick.js'; var addEvent = H.addEvent, - animObject = H.animObject, - arrayMax = H.arrayMax, - arrayMin = H.arrayMin, - color = H.color, - correctFloat = H.correctFloat, - defaultOptions = H.defaultOptions, - defined = H.defined, - deg2rad = H.deg2rad, - destroyObjectProperties = H.destroyObjectProperties, - each = H.each, - extend = H.extend, - fireEvent = H.fireEvent, - format = H.format, - getMagnitude = H.getMagnitude, - grep = H.grep, - inArray = H.inArray, - isArray = H.isArray, - isNumber = H.isNumber, - isString = H.isString, - merge = H.merge, - normalizeTickInterval = H.normalizeTickInterval, - objectEach = H.objectEach, - pick = H.pick, - removeEvent = H.removeEvent, - splat = H.splat, - syncTimeout = H.syncTimeout, - Tick = H.Tick; - + animObject = H.animObject, + arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + color = H.color, + correctFloat = H.correctFloat, + defaultOptions = H.defaultOptions, + defined = H.defined, + deg2rad = H.deg2rad, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + extend = H.extend, + fireEvent = H.fireEvent, + format = H.format, + getMagnitude = H.getMagnitude, + grep = H.grep, + inArray = H.inArray, + isArray = H.isArray, + isNumber = H.isNumber, + isString = H.isString, + merge = H.merge, + normalizeTickInterval = H.normalizeTickInterval, + objectEach = H.objectEach, + pick = H.pick, + removeEvent = H.removeEvent, + splat = H.splat, + syncTimeout = H.syncTimeout, + Tick = H.Tick; + /** * Create a new axis object. Called internally when instanciating a new chart or * adding axes by {@link Highcharts.Chart#addAxis}. * * A chart can have from 0 axes (pie chart) to multiples. In a normal, single * series cartesian chart, there is one X axis and one Y axis. - * + * * The X axis or axes are referenced by {@link Highcharts.Chart.xAxis}, which is * an array of Axis objects. If there is only one axis, it can be referenced * through `chart.xAxis[0]`, and multiple axes have increasing indices. The same * pattern goes for Y axes. - * + * * If you need to get the axes from a series object, use the `series.xAxis` and * `series.yAxis` properties. These are not arrays, as one series can only be * associated to one X and one Y axis. - * + * * A third way to reference the axis programmatically is by `id`. Add an `id` in * the axis configuration options, and get the axis by * {@link Highcharts.Chart#get}. - * + * * Configuration options for the axes are given in options.xAxis and * options.yAxis. - * + * * @class Highcharts.Axis * @memberOf Highcharts * @param {Highcharts.Chart} chart - The Chart instance to apply the axis on. * @param {Object} options - Axis options */ var Axis = function () { - this.init.apply(this, arguments); + this.init.apply(this, arguments); }; H.extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */{ - /** - * The X axis or category axis. Normally this is the horizontal axis, - * though if the chart is inverted this is the vertical axis. In case of - * multiple axes, the xAxis node is an array of configuration objects. - * - * See [the Axis object](#Axis) for programmatic access to the axis. - * - * @productdesc {highmaps} - * In Highmaps, the axis is hidden, but it is used behind the scenes to - * control features like zooming and panning. Zooming is in effect the same - * as setting the extremes of one of the exes. - * - * @optionparent xAxis - */ - defaultOptions: { - /** - * Whether to allow decimals in this axis' ticks. When counting - * integers, like persons or hits on a web page, decimals should - * be avoided in the labels. - * - * @type {Boolean} - * @see [minTickInterval](#xAxis.minTickInterval) - * @sample {highcharts|highstock} - * highcharts/yaxis/allowdecimals-true/ - * True by default - * @sample {highcharts|highstock} - * highcharts/yaxis/allowdecimals-false/ - * False - * @default true - * @since 2.0 - * @apioption xAxis.allowDecimals - */ - // allowDecimals: null, - - - /** - * When using an alternate grid color, a band is painted across the - * plot area between every other grid line. - * - * @type {Color} - * @sample {highcharts} highcharts/yaxis/alternategridcolor/ - * Alternate grid color on the Y axis - * @sample {highstock} stock/xaxis/alternategridcolor/ - * Alternate grid color on the Y axis - * @default null - * @apioption xAxis.alternateGridColor - */ - // alternateGridColor: null, - - /** - * An array defining breaks in the axis, the sections defined will be - * left out and all the points shifted closer to each other. - * - * @productdesc {highcharts} - * Requires that the broken-axis.js module is loaded. - * - * @type {Array} - * @sample {highcharts} - * highcharts/axisbreak/break-simple/ - * Simple break - * @sample {highcharts|highstock} - * highcharts/axisbreak/break-visualized/ - * Advanced with callback - * @sample {highstock} - * stock/demo/intraday-breaks/ - * Break on nights and weekends - * @since 4.1.0 - * @product highcharts highstock - * @apioption xAxis.breaks - */ - - /** - * A number indicating how much space should be left between the start - * and the end of the break. The break size is given in axis units, - * so for instance on a `datetime` axis, a break size of 3600000 would - * indicate the equivalent of an hour. - * - * @type {Number} - * @default 0 - * @since 4.1.0 - * @product highcharts highstock - * @apioption xAxis.breaks.breakSize - */ - - /** - * The point where the break starts. - * - * @type {Number} - * @since 4.1.0 - * @product highcharts highstock - * @apioption xAxis.breaks.from - */ - - /** - * Defines an interval after which the break appears again. By default - * the breaks do not repeat. - * - * @type {Number} - * @default 0 - * @since 4.1.0 - * @product highcharts highstock - * @apioption xAxis.breaks.repeat - */ - - /** - * The point where the break ends. - * - * @type {Number} - * @since 4.1.0 - * @product highcharts highstock - * @apioption xAxis.breaks.to - */ - - /** - * If categories are present for the xAxis, names are used instead of - * numbers for that axis. Since Highcharts 3.0, categories can also - * be extracted by giving each point a [name](#series.data) and setting - * axis [type](#xAxis.type) to `category`. However, if you have multiple - * series, best practice remains defining the `categories` array. - * - * Example: - * - *
categories: ['Apples', 'Bananas', 'Oranges']
- * - * @type {Array} - * @sample {highcharts} highcharts/chart/reflow-true/ - * With - * @sample {highcharts} highcharts/xaxis/categories/ - * Without - * @product highcharts - * @default null - * @apioption xAxis.categories - */ - // categories: [], - - /** - * The highest allowed value for automatically computed axis extremes. - * - * @type {Number} - * @see [floor](#xAxis.floor) - * @sample {highcharts|highstock} highcharts/yaxis/floor-ceiling/ - * Floor and ceiling - * @since 4.0 - * @product highcharts highstock - * @apioption xAxis.ceiling - */ - - /** - * A class name that opens for styling the axis by CSS, especially in - * Highcharts styled mode. The class name is applied to group elements - * for the grid, axis elements and labels. - * - * @type {String} - * @sample {highcharts|highstock|highmaps} - * highcharts/css/axis/ - * Multiple axes with separate styling - * @since 5.0.0 - * @apioption xAxis.className - */ - - /** - * Configure a crosshair that follows either the mouse pointer or the - * hovered point. - * - * In styled mode, the crosshairs are styled in the - * `.highcharts-crosshair`, `.highcharts-crosshair-thin` or - * `.highcharts-xaxis-category` classes. - * - * @productdesc {highstock} - * In Highstock, bu default, the crosshair is enabled on the X axis and - * disabled on the Y axis. - * - * @type {Boolean|Object} - * @sample {highcharts} highcharts/xaxis/crosshair-both/ - * Crosshair on both axes - * @sample {highstock} stock/xaxis/crosshairs-xy/ - * Crosshair on both axes - * @sample {highmaps} highcharts/xaxis/crosshair-both/ - * Crosshair on both axes - * @default false - * @since 4.1 - * @apioption xAxis.crosshair - */ - - /** - * A class name for the crosshair, especially as a hook for styling. - * - * @type {String} - * @since 5.0.0 - * @apioption xAxis.crosshair.className - */ - - /** - * The color of the crosshair. Defaults to `#cccccc` for numeric and - * datetime axes, and `rgba(204,214,235,0.25)` for category axes, where - * the crosshair by default highlights the whole category. - * - * @type {Color} - * @sample {highcharts|highstock|highmaps} - * highcharts/xaxis/crosshair-customized/ - * Customized crosshairs - * @default #cccccc - * @since 4.1 - * @apioption xAxis.crosshair.color - */ - - /** - * The dash style for the crosshair. See - * [series.dashStyle](#plotOptions.series.dashStyle) - * for possible values. - * - * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", - * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", - * "DashDot", "LongDashDot", "LongDashDotDot"] - * @type {String} - * @sample {highcharts|highmaps} highcharts/xaxis/crosshair-dotted/ - * Dotted crosshair - * @sample {highstock} stock/xaxis/crosshair-dashed/ - * Dashed X axis crosshair - * @default Solid - * @since 4.1 - * @apioption xAxis.crosshair.dashStyle - */ - - /** - * Whether the crosshair should snap to the point or follow the pointer - * independent of points. - * - * @type {Boolean} - * @sample {highcharts|highstock} - * highcharts/xaxis/crosshair-snap-false/ - * True by default - * @sample {highmaps} - * maps/demo/latlon-advanced/ - * Snap is false - * @default true - * @since 4.1 - * @apioption xAxis.crosshair.snap - */ - - /** - * The pixel width of the crosshair. Defaults to 1 for numeric or - * datetime axes, and for one category width for category axes. - * - * @type {Number} - * @sample {highcharts} highcharts/xaxis/crosshair-customized/ - * Customized crosshairs - * @sample {highstock} highcharts/xaxis/crosshair-customized/ - * Customized crosshairs - * @sample {highmaps} highcharts/xaxis/crosshair-customized/ - * Customized crosshairs - * @default 1 - * @since 4.1 - * @apioption xAxis.crosshair.width - */ - - /** - * The Z index of the crosshair. Higher Z indices allow drawing the - * crosshair on top of the series or behind the grid lines. - * - * @type {Number} - * @default 2 - * @since 4.1 - * @apioption xAxis.crosshair.zIndex - */ - - /** - * For a datetime axis, the scale will automatically adjust to the - * appropriate unit. This member gives the default string - * representations used for each unit. For intermediate values, - * different units may be used, for example the `day` unit can be used - * on midnight and `hour` unit be used for intermediate values on the - * same axis. For an overview of the replacement codes, see - * [dateFormat](#Highcharts.dateFormat). Defaults to: - * - *
{
-		 *     millisecond: '%H:%M:%S.%L',
-		 *     second: '%H:%M:%S',
-		 *     minute: '%H:%M',
-		 *     hour: '%H:%M',
-		 *     day: '%e. %b',
-		 *     week: '%e. %b',
-		 *     month: '%b \'%y',
-		 *     year: '%Y'
-		 * }
- * - * @type {Object} - * @sample {highcharts} highcharts/xaxis/datetimelabelformats/ - * Different day format on X axis - * @sample {highstock} stock/xaxis/datetimelabelformats/ - * More information in x axis labels - * @product highcharts highstock - */ - dateTimeLabelFormats: { - millisecond: '%H:%M:%S.%L', - second: '%H:%M:%S', - minute: '%H:%M', - hour: '%H:%M', - day: '%e. %b', - week: '%e. %b', - month: '%b \'%y', - year: '%Y' - }, - - /** - * _Requires Accessibility module_ - * - * Description of the axis to screen reader users. - * - * @type {String} - * @default undefined - * @since 5.0.0 - * @apioption xAxis.description - */ - - /** - * Whether to force the axis to end on a tick. Use this option with - * the `maxPadding` option to control the axis end. - * - * @productdesc {highstock} - * In Highstock, `endOnTick` is always false when the navigator is - * enabled, to prevent jumpy scrolling. - * - * @sample {highcharts} highcharts/chart/reflow-true/ - * True by default - * @sample {highcharts} highcharts/yaxis/endontick/ - * False - * @sample {highstock} stock/demo/basic-line/ - * True by default - * @sample {highstock} stock/xaxis/endontick/ - * False - * @since 1.2.0 - */ - endOnTick: false, - - /** - * Event handlers for the axis. - * - * @apioption xAxis.events - */ - - /** - * An event fired after the breaks have rendered. - * - * @type {Function} - * @see [breaks](#xAxis.breaks) - * @sample {highcharts} highcharts/axisbreak/break-event/ - * AfterBreak Event - * @since 4.1.0 - * @product highcharts - * @apioption xAxis.events.afterBreaks - */ - - /** - * As opposed to the `setExtremes` event, this event fires after the - * final min and max values are computed and corrected for `minRange`. - * - * - * Fires when the minimum and maximum is set for the axis, either by - * calling the `.setExtremes()` method or by selecting an area in the - * chart. One parameter, `event`, is passed to the function, containing - * common event information. - * - * The new user set minimum and maximum values can be found by - * `event.min` and `event.max`. These reflect the axis minimum and - * maximum in axis values. The actual data extremes are found in - * `event.dataMin` and `event.dataMax`. - * - * @type {Function} - * @context Axis - * @since 2.3 - * @apioption xAxis.events.afterSetExtremes - */ - - /** - * An event fired when a break from this axis occurs on a point. - * - * @type {Function} - * @see [breaks](#xAxis.breaks) - * @context Axis - * @sample {highcharts} highcharts/axisbreak/break-visualized/ - * Visualization of a Break - * @since 4.1.0 - * @product highcharts - * @apioption xAxis.events.pointBreak - */ - - /** - * An event fired when a point falls inside a break from this axis. - * - * @type {Function} - * @context Axis - * @product highcharts highstock - * @apioption xAxis.events.pointInBreak - */ - - /** - * Fires when the minimum and maximum is set for the axis, either by - * calling the `.setExtremes()` method or by selecting an area in the - * chart. One parameter, `event`, is passed to the function, - * containing common event information. - * - * The new user set minimum and maximum values can be found by - * `event.min` and `event.max`. These reflect the axis minimum and - * maximum in data values. When an axis is zoomed all the way out from - * the "Reset zoom" button, `event.min` and `event.max` are null, and - * the new extremes are set based on `this.dataMin` and `this.dataMax`. - * - * @type {Function} - * @context Axis - * @sample {highstock} stock/xaxis/events-setextremes/ - * Log new extremes on x axis - * @since 1.2.0 - * @apioption xAxis.events.setExtremes - */ - - /** - * The lowest allowed value for automatically computed axis extremes. - * - * @type {Number} - * @see [ceiling](#yAxis.ceiling) - * @sample {highcharts} highcharts/yaxis/floor-ceiling/ - * Floor and ceiling - * @sample {highstock} stock/demo/lazy-loading/ - * Prevent negative stock price on Y axis - * @default null - * @since 4.0 - * @product highcharts highstock - * @apioption xAxis.floor - */ - - /** - * The dash or dot style of the grid lines. For possible values, see - * [this demonstration](http://jsfiddle.net/gh/get/library/pure/ - *highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ - *series-dashstyle-all/). - * - * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", - * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", - * "DashDot", "LongDashDot", "LongDashDotDot"] - * @type {String} - * @sample {highcharts} highcharts/yaxis/gridlinedashstyle/ - * Long dashes - * @sample {highstock} stock/xaxis/gridlinedashstyle/ - * Long dashes - * @default Solid - * @since 1.2 - * @apioption xAxis.gridLineDashStyle - */ - - /** - * The Z index of the grid lines. - * - * @type {Number} - * @sample {highcharts|highstock} highcharts/xaxis/gridzindex/ - * A Z index of 4 renders the grid above the graph - * @default 1 - * @product highcharts highstock - * @apioption xAxis.gridZIndex - */ - - /** - * An id for the axis. This can be used after render time to get - * a pointer to the axis object through `chart.get()`. - * - * @type {String} - * @sample {highcharts} highcharts/xaxis/id/ - * Get the object - * @sample {highstock} stock/xaxis/id/ - * Get the object - * @default null - * @since 1.2.0 - * @apioption xAxis.id - */ - - /** - * The axis labels show the number or category for each tick. - * - * @productdesc {highmaps} - * X and Y axis labels are by default disabled in Highmaps, but the - * functionality is inherited from Highcharts and used on `colorAxis`, - * and can be enabled on X and Y axes too. - */ - labels: { - /** - * What part of the string the given position is anchored to. - * If `left`, the left side of the string is at the axis position. - * Can be one of `"left"`, `"center"` or `"right"`. Defaults to - * an intelligent guess based on which side of the chart the axis - * is on and the rotation of the label. - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @sample {highcharts} highcharts/xaxis/labels-align-left/ - * Left - * @sample {highcharts} highcharts/xaxis/labels-align-right/ - * Right - * @sample {highcharts} - * highcharts/xaxis/labels-reservespace-true/ - * Left-aligned labels on a vertical category axis - * @see [reserveSpace](#xAxis.labels.reserveSpace) - * @apioption xAxis.labels.align - */ - // align: 'center', - - /** - * For horizontal axes, the allowed degrees of label rotation - * to prevent overlapping labels. If there is enough space, - * labels are not rotated. As the chart gets narrower, it - * will start rotating the labels -45 degrees, then remove - * every second label and try again with rotations 0 and -45 etc. - * Set it to `false` to disable rotation, which will - * cause the labels to word-wrap if possible. - * - * @type {Array} - * @sample {highcharts|highstock} - * highcharts/xaxis/labels-autorotation-default/ - * Default auto rotation of 0 or -45 - * @sample {highcharts|highstock} - * highcharts/xaxis/labels-autorotation-0-90/ - * Custom graded auto rotation - * @default [-45] - * @since 4.1.0 - * @product highcharts highstock - * @apioption xAxis.labels.autoRotation - */ - - /** - * When each category width is more than this many pixels, we don't - * apply auto rotation. Instead, we lay out the axis label with word - * wrap. A lower limit makes sense when the label contains multiple - * short words that don't extend the available horizontal space for - * each label. - * - * @type {Number} - * @sample {highcharts} - * highcharts/xaxis/labels-autorotationlimit/ - * Lower limit - * @default 80 - * @since 4.1.5 - * @product highcharts - * @apioption xAxis.labels.autoRotationLimit - */ - - /** - * Polar charts only. The label's pixel distance from the perimeter - * of the plot area. - * - * @type {Number} - * @default 15 - * @product highcharts - * @apioption xAxis.labels.distance - */ - - /** - * Enable or disable the axis labels. - * - * @sample {highcharts} highcharts/xaxis/labels-enabled/ - * X axis labels disabled - * @sample {highstock} stock/xaxis/labels-enabled/ - * X axis labels disabled - * @default {highcharts|highstock} true - * @default {highmaps} false - */ - enabled: true, - - /** - * A [format string](http://www.highcharts.com/docs/chart- - * concepts/labels-and-string-formatting) for the axis label. - * - * @type {String} - * @sample {highcharts|highstock} highcharts/yaxis/labels-format/ - * Add units to Y axis label - * @default {value} - * @since 3.0 - * @apioption xAxis.labels.format - */ - - /** - * Callback JavaScript function to format the label. The value - * is given by `this.value`. Additional properties for `this` are - * `axis`, `chart`, `isFirst` and `isLast`. The value of the default - * label formatter can be retrieved by calling - * `this.axis.defaultLabelFormatter.call(this)` within the function. - * - * Defaults to: - * - *
function() {
-			 *     return this.value;
-			 * }
- * - * @type {Function} - * @sample {highcharts} - * highcharts/xaxis/labels-formatter-linked/ - * Linked category names - * @sample {highcharts} - * highcharts/xaxis/labels-formatter-extended/ - * Modified numeric labels - * @sample {highstock} - * stock/xaxis/labels-formatter/ - * Added units on Y axis - * @apioption xAxis.labels.formatter - */ - - /** - * How to handle overflowing labels on horizontal axis. Can be - * undefined, `false` or `"justify"`. By default it aligns inside - * the chart area. If "justify", labels will not render outside - * the plot area. If `false`, it will not be aligned at all. - * If there is room to move it, it will be aligned to the edge, - * else it will be removed. - * - * @deprecated - * @validvalue [null, "justify"] - * @type {String} - * @since 2.2.5 - * @apioption xAxis.labels.overflow - */ - - /** - * The pixel padding for axis labels, to ensure white space between - * them. - * - * @type {Number} - * @default 5 - * @product highcharts - * @apioption xAxis.labels.padding - */ - - /** - * Whether to reserve space for the labels. By default, space is - * reserved for the labels in these cases: - * - * * On all horizontal axes. - * * On vertical axes if `label.align` is `right` on a left-side - * axis or `left` on a right-side axis. - * * On vertical axes if `label.align` is `center`. - * - * This can be turned off when for example the labels are rendered - * inside the plot area instead of outside. - * - * @type {Boolean} - * @sample {highcharts} highcharts/xaxis/labels-reservespace/ - * No reserved space, labels inside plot - * @sample {highcharts} - * highcharts/xaxis/labels-reservespace-true/ - * Left-aligned labels on a vertical category axis - * @see [labels.align](#xAxis.labels.align) - * @default null - * @since 4.1.10 - * @product highcharts - * @apioption xAxis.labels.reserveSpace - */ - - /** - * Rotation of the labels in degrees. - * - * @type {Number} - * @sample {highcharts} highcharts/xaxis/labels-rotation/ - * X axis labels rotated 90° - * @default 0 - * @apioption xAxis.labels.rotation - */ - - /** - * Horizontal axes only. The number of lines to spread the labels - * over to make room or tighter labels. - * - * @type {Number} - * @sample {highcharts} highcharts/xaxis/labels-staggerlines/ - * Show labels over two lines - * @sample {highstock} stock/xaxis/labels-staggerlines/ - * Show labels over two lines - * @default null - * @since 2.1 - * @apioption xAxis.labels.staggerLines - */ - - /** - * To show only every _n_'th label on the axis, set the step to _n_. - * Setting the step to 2 shows every other label. - * - * By default, the step is calculated automatically to avoid - * overlap. To prevent this, set it to 1\. This usually only - * happens on a category axis, and is often a sign that you have - * chosen the wrong axis type. - * - * Read more at - * [Axis docs](http://www.highcharts.com/docs/chart-concepts/axes) - * => What axis should I use? - * - * @type {Number} - * @sample {highcharts} highcharts/xaxis/labels-step/ - * Showing only every other axis label on a categorized - * x axis - * @sample {highcharts} highcharts/xaxis/labels-step-auto/ - * Auto steps on a category axis - * @default null - * @since 2.1 - * @apioption xAxis.labels.step - */ - - - /** - * The y position offset of the label relative to the tick position - * on the axis. The default makes it adapt to the font size on - * bottom axis. - * - * @type {Number} - * @sample {highcharts} highcharts/xaxis/labels-x/ - * Y axis labels placed on grid lines - * @default null - * @apioption xAxis.labels.y - */ - - /** - * The Z index for the axis labels. - * - * @type {Number} - * @default 7 - * @apioption xAxis.labels.zIndex - */ - - /*= if (build.classic) { =*/ - - /** - * CSS styles for the label. Use `whiteSpace: 'nowrap'` to prevent - * wrapping of category labels. Use `textOverflow: 'none'` to - * prevent ellipsis (dots). - * - * In styled mode, the labels are styled with the - * `.highcharts-axis-labels` class. - * - * @type {CSSObject} - * @sample {highcharts} highcharts/xaxis/labels-style/ - * Red X axis labels - */ - style: { - color: '${palette.neutralColor60}', - cursor: 'default', - fontSize: '11px' - }, - /*= } =*/ - - /** - * Whether to [use HTML](http://www.highcharts.com/docs/chart- - * concepts/labels-and-string-formatting#html) to render the labels. - * - * @type {Boolean} - * @default false - * @apioption xAxis.labels.useHTML - */ - - /** - * The x position offset of the label relative to the tick position - * on the axis. - * - * @sample {highcharts} highcharts/xaxis/labels-x/ - * Y axis labels placed on grid lines - */ - x: 0 - }, - - /** - * Index of another axis that this axis is linked to. When an axis is - * linked to a master axis, it will take the same extremes as - * the master, but as assigned by min or max or by setExtremes. - * It can be used to show additional info, or to ease reading the - * chart by duplicating the scales. - * - * @type {Number} - * @sample {highcharts} highcharts/xaxis/linkedto/ - * Different string formats of the same date - * @sample {highcharts} highcharts/yaxis/linkedto/ - * Y values on both sides - * @default null - * @since 2.0.2 - * @product highcharts highstock - * @apioption xAxis.linkedTo - */ - - /** - * The maximum value of the axis. If `null`, the max value is - * automatically calculated. - * - * If the `endOnTick` option is true, the `max` value might - * be rounded up. - * - * If a [tickAmount](#yAxis.tickAmount) is set, the axis may be extended - * beyond the set max in order to reach the given number of ticks. The - * same may happen in a chart with multiple axes, determined by [chart. - * alignTicks](#chart), where a `tickAmount` is applied internally. - * - * @type {Number} - * @sample {highcharts} highcharts/yaxis/max-200/ - * Y axis max of 200 - * @sample {highcharts} highcharts/yaxis/max-logarithmic/ - * Y axis max on logarithmic axis - * @sample {highstock} stock/xaxis/min-max/ - * Fixed min and max on X axis - * @sample {highmaps} maps/axis/min-max/ - * Pre-zoomed to a specific area - * @apioption xAxis.max - */ - - /** - * When using multiple axis, the ticks of two or more opposite axes - * will automatically be aligned by adding ticks to the axis or axes - * with the least ticks, as if `tickAmount` were specified. - * - * This can be prevented by setting `alignTicks` to false. If the grid - * lines look messy, it's a good idea to hide them for the secondary - * axis by setting `gridLineWidth` to 0. - * - * If `startOnTick` or `endOnTick` in an Axis options are set to false, - * then the `alignTicks ` will be disabled for the Axis. - * - * Disabled for logarithmic axes. - * - * @type {Boolean} - * @default true - * @product highcharts highstock - * @apioption xAxis.alignTicks - */ - - /** - * Padding of the max value relative to the length of the axis. A - * padding of 0.05 will make a 100px axis 5px longer. This is useful - * when you don't want the highest data value to appear on the edge - * of the plot area. When the axis' `max` option is set or a max extreme - * is set using `axis.setExtremes()`, the maxPadding will be ignored. - * - * @sample {highcharts} highcharts/yaxis/maxpadding/ - * Max padding of 0.25 on y axis - * @sample {highstock} stock/xaxis/minpadding-maxpadding/ - * Greater min- and maxPadding - * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ - * Add some padding - * @default {highcharts} 0.01 - * @default {highstock|highmaps} 0 - * @since 1.2.0 - */ - maxPadding: 0.01, - - /** - * Deprecated. Use `minRange` instead. - * - * @deprecated - * @type {Number} - * @product highcharts highstock - * @apioption xAxis.maxZoom - */ - - /** - * The minimum value of the axis. If `null` the min value is - * automatically calculated. - * - * If the `startOnTick` option is true (default), the `min` value might - * be rounded down. - * - * The automatically calculated minimum value is also affected by - * [floor](#yAxis.floor), [softMin](#yAxis.softMin), - * [minPadding](#yAxis.minPadding), [minRange](#yAxis.minRange) - * as well as [series.threshold](#plotOptions.series.threshold) - * and [series.softThreshold](#plotOptions.series.softThreshold). - * - * @type {Number} - * @sample {highcharts} highcharts/yaxis/min-startontick-false/ - * -50 with startOnTick to false - * @sample {highcharts} highcharts/yaxis/min-startontick-true/ - * -50 with startOnTick true by default - * @sample {highstock} stock/xaxis/min-max/ - * Set min and max on X axis - * @sample {highmaps} maps/axis/min-max/ - * Pre-zoomed to a specific area - * @apioption xAxis.min - */ - - /** - * The dash or dot style of the minor grid lines. For possible values, - * see [this demonstration](http://jsfiddle.net/gh/get/library/pure/ - * highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ - * series-dashstyle-all/). - * - * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", - * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", - * "DashDot", "LongDashDot", "LongDashDotDot"] - * @type {String} - * @sample {highcharts} highcharts/yaxis/minorgridlinedashstyle/ - * Long dashes on minor grid lines - * @sample {highstock} stock/xaxis/minorgridlinedashstyle/ - * Long dashes on minor grid lines - * @default Solid - * @since 1.2 - * @apioption xAxis.minorGridLineDashStyle - */ - - /** - * Specific tick interval in axis units for the minor ticks. - * On a linear axis, if `"auto"`, the minor tick interval is - * calculated as a fifth of the tickInterval. If `null`, minor - * ticks are not shown. - * - * On logarithmic axes, the unit is the power of the value. For example, - * setting the minorTickInterval to 1 puts one tick on each of 0.1, - * 1, 10, 100 etc. Setting the minorTickInterval to 0.1 produces 9 - * ticks between 1 and 10, 10 and 100 etc. - * - * If user settings dictate minor ticks to become too dense, they don't - * make sense, and will be ignored to prevent performance problems. - * - * @type {Number|String} - * @sample {highcharts} highcharts/yaxis/minortickinterval-null/ - * Null by default - * @sample {highcharts} highcharts/yaxis/minortickinterval-5/ - * 5 units - * @sample {highcharts} highcharts/yaxis/minortickinterval-log-auto/ - * "auto" - * @sample {highcharts} highcharts/yaxis/minortickinterval-log/ - * 0.1 - * @sample {highstock} stock/demo/basic-line/ - * Null by default - * @sample {highstock} stock/xaxis/minortickinterval-auto/ - * "auto" - * @apioption xAxis.minorTickInterval - */ - - /** - * The pixel length of the minor tick marks. - * - * @sample {highcharts} highcharts/yaxis/minorticklength/ - * 10px on Y axis - * @sample {highstock} stock/xaxis/minorticks/ - * 10px on Y axis - */ - minorTickLength: 2, - - /** - * The position of the minor tick marks relative to the axis line. - * Can be one of `inside` and `outside`. - * - * @validvalue ["inside", "outside"] - * @sample {highcharts} highcharts/yaxis/minortickposition-outside/ - * Outside by default - * @sample {highcharts} highcharts/yaxis/minortickposition-inside/ - * Inside - * @sample {highstock} stock/xaxis/minorticks/ - * Inside - */ - minorTickPosition: 'outside', - - /** - * Enable or disable minor ticks. Unless - * [minorTickInterval](#xAxis.minorTickInterval) is set, the tick - * interval is calculated as a fifth of the `tickInterval`. - * - * On a logarithmic axis, minor ticks are laid out based on a best - * guess, attempting to enter approximately 5 minor ticks between - * each major tick. - * - * Prior to v6.0.0, ticks were unabled in auto layout by setting - * `minorTickInterval` to `"auto"`. - * - * @productdesc {highcharts} - * On axes using [categories](#xAxis.categories), minor ticks are not - * supported. - * - * @type {Boolean} - * @default false - * @since 6.0.0 - * @sample {highcharts} highcharts/yaxis/minorticks-true/ - * Enabled on linear Y axis - * @apioption xAxis.minorTicks - */ - - /** - * The pixel width of the minor tick mark. - * - * @type {Number} - * @sample {highcharts} highcharts/yaxis/minortickwidth/ - * 3px width - * @sample {highstock} stock/xaxis/minorticks/ - * 1px width - * @default 0 - * @apioption xAxis.minorTickWidth - */ - - /** - * Padding of the min value relative to the length of the axis. A - * padding of 0.05 will make a 100px axis 5px longer. This is useful - * when you don't want the lowest data value to appear on the edge - * of the plot area. When the axis' `min` option is set or a min extreme - * is set using `axis.setExtremes()`, the minPadding will be ignored. - * - * @sample {highcharts} highcharts/yaxis/minpadding/ - * Min padding of 0.2 - * @sample {highstock} stock/xaxis/minpadding-maxpadding/ - * Greater min- and maxPadding - * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ - * Add some padding - * @default {highcharts} 0.01 - * @default {highstock|highmaps} 0 - * @since 1.2.0 - */ - minPadding: 0.01, - - /** - * The minimum range to display on this axis. The entire axis will not - * be allowed to span over a smaller interval than this. For example, - * for a datetime axis the main unit is milliseconds. If minRange is - * set to 3600000, you can't zoom in more than to one hour. - * - * The default minRange for the x axis is five times the smallest - * interval between any of the data points. - * - * On a logarithmic axis, the unit for the minimum range is the power. - * So a minRange of 1 means that the axis can be zoomed to 10-100, - * 100-1000, 1000-10000 etc. - * - * Note that the `minPadding`, `maxPadding`, `startOnTick` and - * `endOnTick` settings also affect how the extremes of the axis - * are computed. - * - * @type {Number} - * @sample {highcharts} highcharts/xaxis/minrange/ - * Minimum range of 5 - * @sample {highstock} stock/xaxis/minrange/ - * Max zoom of 6 months overrides user selections - * @sample {highmaps} maps/axis/minrange/ - * Minimum range of 1000 - * @apioption xAxis.minRange - */ - - /** - * The minimum tick interval allowed in axis values. For example on - * zooming in on an axis with daily data, this can be used to prevent - * the axis from showing hours. Defaults to the closest distance between - * two points on the axis. - * - * @type {Number} - * @since 2.3.0 - * @apioption xAxis.minTickInterval - */ - - /** - * The distance in pixels from the plot area to the axis line. - * A positive offset moves the axis with it's line, labels and ticks - * away from the plot area. This is typically used when two or more - * axes are displayed on the same side of the plot. With multiple - * axes the offset is dynamically adjusted to avoid collision, this - * can be overridden by setting offset explicitly. - * - * @type {Number} - * @sample {highcharts} highcharts/yaxis/offset/ - * Y axis offset of 70 - * @sample {highcharts} highcharts/yaxis/offset-centered/ - * Axes positioned in the center of the plot - * @sample {highstock} stock/xaxis/offset/ - * Y axis offset by 70 px - * @default 0 - * @apioption xAxis.offset - */ - - /** - * Whether to display the axis on the opposite side of the normal. The - * normal is on the left side for vertical axes and bottom for - * horizontal, so the opposite sides will be right and top respectively. - * This is typically used with dual or multiple axes. - * - * @type {Boolean} - * @sample {highcharts} highcharts/yaxis/opposite/ - * Secondary Y axis opposite - * @sample {highstock} stock/xaxis/opposite/ - * Y axis on left side - * @default false - * @apioption xAxis.opposite - */ - - /** - * Refers to the index in the [panes](#panes) array. Used for circular - * gauges and polar charts. When the option is not set then first pane - * will be used. - * - * @type {Number} - * @sample highcharts/demo/gauge-vu-meter - * Two gauges with different center - * @product highcharts - * @apioption xAxis.pane - */ - - /** - * Whether to reverse the axis so that the highest number is closest - * to the origin. If the chart is inverted, the x axis is reversed by - * default. - * - * @type {Boolean} - * @sample {highcharts} highcharts/yaxis/reversed/ - * Reversed Y axis - * @sample {highstock} stock/xaxis/reversed/ - * Reversed Y axis - * @default false - * @apioption xAxis.reversed - */ - // reversed: false, - - /** - * Whether to show the last tick label. Defaults to `true` on cartesian - * charts, and `false` on polar charts. - * - * @type {Boolean} - * @sample {highcharts} highcharts/xaxis/showlastlabel-true/ - * Set to true on X axis - * @sample {highstock} stock/xaxis/showfirstlabel/ - * Labels below plot lines on Y axis - * @default true - * @product highcharts highstock - * @apioption xAxis.showLastLabel - */ - - /** - * For datetime axes, this decides where to put the tick between weeks. - * 0 = Sunday, 1 = Monday. - * - * @sample {highcharts} highcharts/xaxis/startofweek-monday/ - * Monday by default - * @sample {highcharts} highcharts/xaxis/startofweek-sunday/ - * Sunday - * @sample {highstock} stock/xaxis/startofweek-1 - * Monday by default - * @sample {highstock} stock/xaxis/startofweek-0 - * Sunday - * @product highcharts highstock - */ - startOfWeek: 1, - - /** - * Whether to force the axis to start on a tick. Use this option with - * the `minPadding` option to control the axis start. - * - * @productdesc {highstock} - * In Highstock, `startOnTick` is always false when the navigator is - * enabled, to prevent jumpy scrolling. - * - * @sample {highcharts} highcharts/xaxis/startontick-false/ - * False by default - * @sample {highcharts} highcharts/xaxis/startontick-true/ - * True - * @sample {highstock} stock/xaxis/endontick/ - * False for Y axis - * @since 1.2.0 - */ - startOnTick: false, - - /** - * The pixel length of the main tick marks. - * - * @sample {highcharts} highcharts/xaxis/ticklength/ - * 20 px tick length on the X axis - * @sample {highstock} stock/xaxis/ticks/ - * Formatted ticks on X axis - */ - tickLength: 10, - - /** - * For categorized axes only. If `on` the tick mark is placed in the - * center of the category, if `between` the tick mark is placed between - * categories. The default is `between` if the `tickInterval` is 1, - * else `on`. - * - * @validvalue [null, "on", "between"] - * @sample {highcharts} highcharts/xaxis/tickmarkplacement-between/ - * "between" by default - * @sample {highcharts} highcharts/xaxis/tickmarkplacement-on/ - * "on" - * @product highcharts - */ - tickmarkPlacement: 'between', - - /** - * If tickInterval is `null` this option sets the approximate pixel - * interval of the tick marks. Not applicable to categorized axis. - * - * The tick interval is also influenced by the [minTickInterval]( - * #xAxis.minTickInterval) option, that, by default prevents ticks from - * being denser than the data points. - * - * @see [tickInterval](#xAxis.tickInterval), - * [tickPositioner](#xAxis.tickPositioner), - * [tickPositions](#xAxis.tickPositions). - * @sample {highcharts} highcharts/xaxis/tickpixelinterval-50/ - * 50 px on X axis - * @sample {highstock} stock/xaxis/tickpixelinterval/ - * 200 px on X axis - */ - tickPixelInterval: 100, - - /** - * The position of the major tick marks relative to the axis line. - * Can be one of `inside` and `outside`. - * - * @validvalue ["inside", "outside"] - * @sample {highcharts} highcharts/xaxis/tickposition-outside/ - * "outside" by default - * @sample {highcharts} highcharts/xaxis/tickposition-inside/ - * "inside" - * @sample {highstock} stock/xaxis/ticks/ - * Formatted ticks on X axis - */ - tickPosition: 'outside', - - /** - * The axis title, showing next to the axis line. - * - * @productdesc {highmaps} - * In Highmaps, the axis is hidden by default, but adding an axis title - * is still possible. X axis and Y axis titles will appear at the bottom - * and left by default. - */ - title: { - - /** - * Alignment of the title relative to the axis values. Possible - * values are "low", "middle" or "high". - * - * @validvalue ["low", "middle", "high"] - * @sample {highcharts} highcharts/xaxis/title-align-low/ - * "low" - * @sample {highcharts} highcharts/xaxis/title-align-center/ - * "middle" by default - * @sample {highcharts} highcharts/xaxis/title-align-high/ - * "high" - * @sample {highcharts} highcharts/yaxis/title-offset/ - * Place the Y axis title on top of the axis - * @sample {highstock} stock/xaxis/title-align/ - * Aligned to "high" value - */ - align: 'middle', - - /*= if (build.classic) { =*/ - - /** - * CSS styles for the title. If the title text is longer than the - * axis length, it will wrap to multiple lines by default. This can - * be customized by setting `textOverflow: 'ellipsis'`, by - * setting a specific `width` or by setting `whiteSpace: 'nowrap'`. - * - * In styled mode, the stroke width is given in the - * `.highcharts-axis-title` class. - * - * @type {CSSObject} - * @sample {highcharts} highcharts/xaxis/title-style/ - * Red - * @sample {highcharts} highcharts/css/axis/ - * Styled mode - * @default { "color": "#666666" } - */ - style: { - color: '${palette.neutralColor60}' - } - /*= } =*/ - }, - - /** - * The type of axis. Can be one of `linear`, `logarithmic`, `datetime` - * or `category`. In a datetime axis, the numbers are given in - * milliseconds, and tick marks are placed on appropriate values like - * full hours or days. In a category axis, the - * [point names](#series.line.data.name) of the chart's series are used - * for categories, if not a [categories](#xAxis.categories) array is - * defined. - * - * @validvalue ["linear", "logarithmic", "datetime", "category"] - * @sample {highcharts} highcharts/xaxis/type-linear/ - * Linear - * @sample {highcharts} highcharts/yaxis/type-log/ - * Logarithmic - * @sample {highcharts} highcharts/yaxis/type-log-minorgrid/ - * Logarithmic with minor grid lines - * @sample {highcharts} highcharts/xaxis/type-log-both/ - * Logarithmic on two axes - * @sample {highcharts} highcharts/yaxis/type-log-negative/ - * Logarithmic with extension to emulate negative values - * @product highcharts - */ - type: 'linear', - - /*= if (build.classic) { =*/ - - /** - * Color of the minor, secondary grid lines. - * - * In styled mode, the stroke width is given in the - * `.highcharts-minor-grid-line` class. - * - * @type {Color} - * @sample {highcharts} highcharts/yaxis/minorgridlinecolor/ - * Bright grey lines from Y axis - * @sample {highcharts|highstock} highcharts/css/axis-grid/ - * Styled mode - * @sample {highstock} stock/xaxis/minorgridlinecolor/ - * Bright grey lines from Y axis - * @default #f2f2f2 - */ - minorGridLineColor: '${palette.neutralColor5}', - // minorGridLineDashStyle: null, - - /** - * Width of the minor, secondary grid lines. - * - * In styled mode, the stroke width is given in the - * `.highcharts-grid-line` class. - * - * @sample {highcharts} highcharts/yaxis/minorgridlinewidth/ - * 2px lines from Y axis - * @sample {highcharts|highstock} highcharts/css/axis-grid/ - * Styled mode - * @sample {highstock} stock/xaxis/minorgridlinewidth/ - * 2px lines from Y axis - */ - minorGridLineWidth: 1, - - /** - * Color for the minor tick marks. - * - * @type {Color} - * @sample {highcharts} highcharts/yaxis/minortickcolor/ - * Black tick marks on Y axis - * @sample {highstock} stock/xaxis/minorticks/ - * Black tick marks on Y axis - * @default #999999 - */ - minorTickColor: '${palette.neutralColor40}', - - /** - * The color of the line marking the axis itself. - * - * In styled mode, the line stroke is given in the - * `.highcharts-axis-line` or `.highcharts-xaxis-line` class. - * - * @productdesc {highmaps} - * In Highmaps, the axis line is hidden by default, because the axis is - * not visible by default. - * - * @type {Color} - * @sample {highcharts} highcharts/yaxis/linecolor/ - * A red line on Y axis - * @sample {highcharts|highstock} highcharts/css/axis/ - * Axes in styled mode - * @sample {highstock} stock/xaxis/linecolor/ - * A red line on X axis - * @default #ccd6eb - */ - lineColor: '${palette.highlightColor20}', - - /** - * The width of the line marking the axis itself. - * - * In styled mode, the stroke width is given in the - * `.highcharts-axis-line` or `.highcharts-xaxis-line` class. - * - * @sample {highcharts} highcharts/yaxis/linecolor/ - * A 1px line on Y axis - * @sample {highcharts|highstock} highcharts/css/axis/ - * Axes in styled mode - * @sample {highstock} stock/xaxis/linewidth/ - * A 2px line on X axis - * @default {highcharts|highstock} 1 - * @default {highmaps} 0 - */ - lineWidth: 1, - - /** - * Color of the grid lines extending the ticks across the plot area. - * - * In styled mode, the stroke is given in the `.highcharts-grid-line` - * class. - * - * @productdesc {highmaps} - * In Highmaps, the grid lines are hidden by default. - * - * @type {Color} - * @sample {highcharts} highcharts/yaxis/gridlinecolor/ - * Green lines - * @sample {highcharts|highstock} highcharts/css/axis-grid/ - * Styled mode - * @sample {highstock} stock/xaxis/gridlinecolor/ - * Green lines - * @default #e6e6e6 - */ - gridLineColor: '${palette.neutralColor10}', - // gridLineDashStyle: 'solid', - - - /** - * The width of the grid lines extending the ticks across the plot area. - * - * In styled mode, the stroke width is given in the - * `.highcharts-grid-line` class. - * - * @type {Number} - * @sample {highcharts} highcharts/yaxis/gridlinewidth/ - * 2px lines - * @sample {highcharts|highstock} highcharts/css/axis-grid/ - * Styled mode - * @sample {highstock} stock/xaxis/gridlinewidth/ - * 2px lines - * @default 0 - * @apioption xAxis.gridLineWidth - */ - // gridLineWidth: 0, - - /** - * Color for the main tick marks. - * - * In styled mode, the stroke is given in the `.highcharts-tick` - * class. - * - * @type {Color} - * @sample {highcharts} highcharts/xaxis/tickcolor/ - * Red ticks on X axis - * @sample {highcharts|highstock} highcharts/css/axis-grid/ - * Styled mode - * @sample {highstock} stock/xaxis/ticks/ - * Formatted ticks on X axis - * @default #ccd6eb - */ - tickColor: '${palette.highlightColor20}' - // tickWidth: 1 - /*= } =*/ - }, - - /** - * The Y axis or value axis. Normally this is the vertical axis, - * though if the chart is inverted this is the horizontal axis. - * In case of multiple axes, the yAxis node is an array of - * configuration objects. - * - * See [the Axis object](#Axis) for programmatic access to the axis. - * - * @extends xAxis - * @excluding ordinal,overscroll - * @optionparent yAxis - */ - defaultYAxisOptions: { - /** - * @productdesc {highstock} - * In Highstock, `endOnTick` is always false when the navigator is - * enabled, to prevent jumpy scrolling. - */ - endOnTick: true, - - /** - * @productdesc {highstock} - * In Highstock 1.x, the Y axis was placed on the left side by default. - * - * @sample {highcharts} highcharts/yaxis/opposite/ - * Secondary Y axis opposite - * @sample {highstock} stock/xaxis/opposite/ - * Y axis on left side - * @default {highstock} true - * @default {highcharts} false - * @product highstock highcharts - * @apioption yAxis.opposite - */ - - /** - * @see [tickInterval](#xAxis.tickInterval), - * [tickPositioner](#xAxis.tickPositioner), - * [tickPositions](#xAxis.tickPositions). - */ - tickPixelInterval: 72, - - showLastLabel: true, - - /** - * @extends xAxis.labels - */ - labels: { - /** - * What part of the string the given position is anchored to. Can - * be one of `"left"`, `"center"` or `"right"`. The exact position - * also depends on the `labels.x` setting. - * - * Angular gauges and solid gauges defaults to `center`. - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @sample {highcharts} highcharts/yaxis/labels-align-left/ - * Left - * @default {highcharts|highmaps} right - * @default {highstock} left - * @apioption yAxis.labels.align - */ - - /** - * The x position offset of the label relative to the tick position - * on the axis. Defaults to -15 for left axis, 15 for right axis. - * - * @sample {highcharts} highcharts/xaxis/labels-x/ - * Y axis labels placed on grid lines - */ - x: -8 - }, - - /** - * @productdesc {highmaps} - * In Highmaps, the axis line is hidden by default, because the axis is - * not visible by default. - * - * @apioption yAxis.lineColor - */ - - /** - * @sample {highcharts} highcharts/yaxis/min-startontick-false/ - * -50 with startOnTick to false - * @sample {highcharts} highcharts/yaxis/min-startontick-true/ - * -50 with startOnTick true by default - * @sample {highstock} stock/yaxis/min-max/ - * Fixed min and max on Y axis - * @sample {highmaps} maps/axis/min-max/ - * Pre-zoomed to a specific area - * @apioption yAxis.min - */ - - /** - * @sample {highcharts} highcharts/yaxis/max-200/ - * Y axis max of 200 - * @sample {highcharts} highcharts/yaxis/max-logarithmic/ - * Y axis max on logarithmic axis - * @sample {highstock} stock/yaxis/min-max/ - * Fixed min and max on Y axis - * @sample {highmaps} maps/axis/min-max/ - * Pre-zoomed to a specific area - * @apioption yAxis.max - */ - - /** - * Padding of the max value relative to the length of the axis. A - * padding of 0.05 will make a 100px axis 5px longer. This is useful - * when you don't want the highest data value to appear on the edge - * of the plot area. When the axis' `max` option is set or a max extreme - * is set using `axis.setExtremes()`, the maxPadding will be ignored. - * - * @sample {highcharts} highcharts/yaxis/maxpadding-02/ - * Max padding of 0.2 - * @sample {highstock} stock/xaxis/minpadding-maxpadding/ - * Greater min- and maxPadding - * @since 1.2.0 - * @product highcharts highstock - */ - maxPadding: 0.05, - - /** - * Padding of the min value relative to the length of the axis. A - * padding of 0.05 will make a 100px axis 5px longer. This is useful - * when you don't want the lowest data value to appear on the edge - * of the plot area. When the axis' `min` option is set or a max extreme - * is set using `axis.setExtremes()`, the maxPadding will be ignored. - * - * @sample {highcharts} highcharts/yaxis/minpadding/ - * Min padding of 0.2 - * @sample {highstock} stock/xaxis/minpadding-maxpadding/ - * Greater min- and maxPadding - * @since 1.2.0 - * @product highcharts highstock - */ - minPadding: 0.05, - - /** - * Whether to force the axis to start on a tick. Use this option with - * the `maxPadding` option to control the axis start. - * - * @sample {highcharts} highcharts/xaxis/startontick-false/ - * False by default - * @sample {highcharts} highcharts/xaxis/startontick-true/ - * True - * @sample {highstock} stock/xaxis/endontick/ - * False for Y axis - * @since 1.2.0 - * @product highcharts highstock - */ - startOnTick: true, - - /** - * @extends xAxis.title - */ - title: { - - /** - * The rotation of the text in degrees. 0 is horizontal, 270 is - * vertical reading from bottom to top. - * - * @sample {highcharts} highcharts/yaxis/title-offset/ - * Horizontal - */ - rotation: 270, - - /** - * The actual text of the axis title. Horizontal texts can contain - * HTML, but rotated texts are painted using vector techniques and - * must be clean text. The Y axis title is disabled by setting the - * `text` option to `null`. - * - * @sample {highcharts} highcharts/xaxis/title-text/ - * Custom HTML - * @default {highcharts} Values - * @default {highstock} null - * @product highcharts highstock - */ - text: 'Values' - }, - - /** - * The stack labels show the total value for each bar in a stacked - * column or bar chart. The label will be placed on top of positive - * columns and below negative columns. In case of an inverted column - * chart or a bar chart the label is placed to the right of positive - * bars and to the left of negative bars. - * - * @product highcharts - */ - stackLabels: { - - /** - * Allow the stack labels to overlap. - * - * @sample {highcharts} - * highcharts/yaxis/stacklabels-allowoverlap-false/ - * Default false - * @since 5.0.13 - * @product highcharts - */ - allowOverlap: false, - - /** - * Enable or disable the stack total labels. - * - * @sample {highcharts} highcharts/yaxis/stacklabels-enabled/ - * Enabled stack total labels - * @since 2.1.5 - * @product highcharts - */ - enabled: false, - - /** - * Callback JavaScript function to format the label. The value is - * given by `this.total`. - * - * @default function() { return this.total; } - * - * @type {Function} - * @sample {highcharts} highcharts/yaxis/stacklabels-formatter/ - * Added units to stack total value - * @since 2.1.5 - * @product highcharts - */ - formatter: function () { - return H.numberFormat(this.total, -1); - }, - /*= if (build.classic) { =*/ - - /** - * CSS styles for the label. - * - * In styled mode, the styles are set in the - * `.highcharts-stack-label` class. - * - * @type {CSSObject} - * @sample {highcharts} highcharts/yaxis/stacklabels-style/ - * Red stack total labels - * @since 2.1.5 - * @product highcharts - */ - style: { - fontSize: '11px', - fontWeight: 'bold', - color: '${palette.neutralColor100}', - textOutline: '1px contrast' - } - /*= } =*/ - }, - /*= if (build.classic) { =*/ - gridLineWidth: 1, - lineWidth: 0 - // tickWidth: 0 - /*= } =*/ - }, - - /** - * These options extend the defaultOptions for left axes. - * - * @private - * @type {Object} - */ - defaultLeftAxisOptions: { - labels: { - x: -15 - }, - title: { - rotation: 270 - } - }, - - /** - * These options extend the defaultOptions for right axes. - * - * @private - * @type {Object} - */ - defaultRightAxisOptions: { - labels: { - x: 15 - }, - title: { - rotation: 90 - } - }, - - /** - * These options extend the defaultOptions for bottom axes. - * - * @private - * @type {Object} - */ - defaultBottomAxisOptions: { - labels: { - autoRotation: [-45], - x: 0 - // overflow: undefined, - // staggerLines: null - }, - title: { - rotation: 0 - } - }, - /** - * These options extend the defaultOptions for top axes. - * - * @private - * @type {Object} - */ - defaultTopAxisOptions: { - labels: { - autoRotation: [-45], - x: 0 - // overflow: undefined - // staggerLines: null - }, - title: { - rotation: 0 - } - }, - - /** - * Overrideable function to initialize the axis. - * - * @see {@link Axis} - */ - init: function (chart, userOptions) { - - - var isXAxis = userOptions.isX, - axis = this; - - /** - * The Chart that the axis belongs to. - * - * @name chart - * @memberOf Axis - * @type {Chart} - */ - axis.chart = chart; - - /** - * Whether the axis is horizontal. - * - * @name horiz - * @memberOf Axis - * @type {Boolean} - */ - axis.horiz = chart.inverted && !axis.isZAxis ? !isXAxis : isXAxis; - - // Flag, isXAxis - axis.isXAxis = isXAxis; - - /** - * The collection where the axis belongs, for example `xAxis`, `yAxis` - * or `colorAxis`. Corresponds to properties on Chart, for example - * {@link Chart.xAxis}. - * - * @name coll - * @memberOf Axis - * @type {String} - */ - axis.coll = axis.coll || (isXAxis ? 'xAxis' : 'yAxis'); - - fireEvent(this, 'init', { userOptions: userOptions }); - - axis.opposite = userOptions.opposite; // needed in setOptions - - /** - * The side on which the axis is rendered. 0 is top, 1 is right, 2 is - * bottom and 3 is left. - * - * @name side - * @memberOf Axis - * @type {Number} - */ - axis.side = userOptions.side || (axis.horiz ? - (axis.opposite ? 0 : 2) : // top : bottom - (axis.opposite ? 1 : 3)); // right : left - - axis.setOptions(userOptions); - - - var options = this.options, - type = options.type, - isDatetimeAxis = type === 'datetime'; - - axis.labelFormatter = options.labels.formatter || - axis.defaultLabelFormatter; // can be overwritten by dynamic format - - - // Flag, stagger lines or not - axis.userOptions = userOptions; - - axis.minPixelPadding = 0; - - - /** - * Whether the axis is reversed. Based on the `axis.reversed`, - * option, but inverted charts have reversed xAxis by default. - * - * @name reversed - * @memberOf Axis - * @type {Boolean} - */ - axis.reversed = options.reversed; - axis.visible = options.visible !== false; - axis.zoomEnabled = options.zoomEnabled !== false; - - // Initial categories - axis.hasNames = type === 'category' || options.categories === true; - axis.categories = options.categories || axis.hasNames; - if (!axis.names) { // Preserve on update (#3830) - axis.names = []; - axis.names.keys = {}; - } - - - // Placeholder for plotlines and plotbands groups - axis.plotLinesAndBandsGroups = {}; - - // Shorthand types - axis.isLog = type === 'logarithmic'; - axis.isDatetimeAxis = isDatetimeAxis; - axis.positiveValuesOnly = axis.isLog && !axis.allowNegativeLog; - - // Flag, if axis is linked to another axis - axis.isLinked = defined(options.linkedTo); - - // Major ticks - axis.ticks = {}; - axis.labelEdge = []; - // Minor ticks - axis.minorTicks = {}; - - // List of plotLines/Bands - axis.plotLinesAndBands = []; - - // Alternate bands - axis.alternateBands = {}; - - // Axis metrics - axis.len = 0; - axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; - axis.range = options.range; - axis.offset = options.offset || 0; - - - // Dictionary for stacks - axis.stacks = {}; - axis.oldStacks = {}; - axis.stacksTouched = 0; - - - /** - * The maximum value of the axis. In a logarithmic axis, this is the - * logarithm of the real value, and the real value can be obtained from - * {@link Axis#getExtremes}. - * - * @name max - * @memberOf Axis - * @type {Number} - */ - axis.max = null; - /** - * The minimum value of the axis. In a logarithmic axis, this is the - * logarithm of the real value, and the real value can be obtained from - * {@link Axis#getExtremes}. - * - * @name min - * @memberOf Axis - * @type {Number} - */ - axis.min = null; - - - /** - * The processed crosshair options. - * - * @name crosshair - * @memberOf Axis - * @type {AxisCrosshairOptions} - */ - axis.crosshair = pick( - options.crosshair, - splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], - false - ); - - var events = axis.options.events; - - // Register. Don't add it again on Axis.update(). - if (inArray(axis, chart.axes) === -1) { // - if (isXAxis) { // #2713 - chart.axes.splice(chart.xAxis.length, 0, axis); - } else { - chart.axes.push(axis); - } - - chart[axis.coll].push(axis); - } - - /** - * All series associated to the axis. - * - * @name series - * @memberOf Axis - * @type {Array.} - */ - axis.series = axis.series || []; // populated by Series - - // Reversed axis - if ( - chart.inverted && - !axis.isZAxis && - isXAxis && - axis.reversed === undefined - ) { - axis.reversed = true; - } - - // register event listeners - objectEach(events, function (event, eventType) { - addEvent(axis, eventType, event); - }); - - // extend logarithmic axis - axis.lin2log = options.linearToLogConverter || axis.lin2log; - if (axis.isLog) { - axis.val2lin = axis.log2lin; - axis.lin2val = axis.lin2log; - } - - fireEvent(this, 'afterInit'); - }, - - /** - * Merge and set options. - * - * @private - */ - setOptions: function (userOptions) { - this.options = merge( - this.defaultOptions, - this.coll === 'yAxis' && this.defaultYAxisOptions, - [ - this.defaultTopAxisOptions, - this.defaultRightAxisOptions, - this.defaultBottomAxisOptions, - this.defaultLeftAxisOptions - ][this.side], - merge( - defaultOptions[this.coll], // if set in setOptions (#1053) - userOptions - ) - ); - - fireEvent(this, 'afterSetOptions', { userOptions: userOptions }); - }, - - /** - * The default label formatter. The context is a special config object for - * the label. In apps, use the {@link - * https://api.highcharts.com/highcharts/xAxis.labels.formatter| - * labels.formatter} instead except when a modification is needed. - * - * @private - */ - defaultLabelFormatter: function () { - var axis = this.axis, - value = this.value, - time = axis.chart.time, - categories = axis.categories, - dateTimeLabelFormat = this.dateTimeLabelFormat, - lang = defaultOptions.lang, - numericSymbols = lang.numericSymbols, - numSymMagnitude = lang.numericSymbolMagnitude || 1000, - i = numericSymbols && numericSymbols.length, - multi, - ret, - formatOption = axis.options.labels.format, - - // make sure the same symbol is added for all labels on a linear - // axis - numericSymbolDetector = axis.isLog ? - Math.abs(value) : - axis.tickInterval; - - if (formatOption) { - ret = format(formatOption, this, time); - - } else if (categories) { - ret = value; - - } else if (dateTimeLabelFormat) { // datetime axis - ret = time.dateFormat(dateTimeLabelFormat, value); - - } else if (i && numericSymbolDetector >= 1000) { - // Decide whether we should add a numeric symbol like k (thousands) - // or M (millions). If we are to enable this in tooltip or other - // places as well, we can move this logic to the numberFormatter and - // enable it by a parameter. - while (i-- && ret === undefined) { - multi = Math.pow(numSymMagnitude, i + 1); - if ( - // Only accept a numeric symbol when the distance is more - // than a full unit. So for example if the symbol is k, we - // don't accept numbers like 0.5k. - numericSymbolDetector >= multi && - // Accept one decimal before the symbol. Accepts 0.5k but - // not 0.25k. How does this work with the previous? - (value * 10) % multi === 0 && - numericSymbols[i] !== null && - value !== 0 - ) { // #5480 - ret = H.numberFormat(value / multi, -1) + numericSymbols[i]; - } - } - } - - if (ret === undefined) { - if (Math.abs(value) >= 10000) { // add thousands separators - ret = H.numberFormat(value, -1); - } else { // small numbers - ret = H.numberFormat(value, -1, undefined, ''); // #2466 - } - } - - return ret; - }, - - /** - * Get the minimum and maximum for the series of each axis. The function - * analyzes the axis series and updates `this.dataMin` and `this.dataMax`. - * - * @private - */ - getSeriesExtremes: function () { - var axis = this, - chart = axis.chart; - - fireEvent(this, 'getSeriesExtremes', null, function () { - - axis.hasVisibleSeries = false; - - // Reset properties in case we're redrawing (#3353) - axis.dataMin = axis.dataMax = axis.threshold = null; - axis.softThreshold = !axis.isXAxis; - - if (axis.buildStacks) { - axis.buildStacks(); - } - - // loop through this axis' series - each(axis.series, function (series) { - - if (series.visible || !chart.options.chart.ignoreHiddenSeries) { - - var seriesOptions = series.options, - xData, - threshold = seriesOptions.threshold, - seriesDataMin, - seriesDataMax; - - axis.hasVisibleSeries = true; - - // Validate threshold in logarithmic axes - if (axis.positiveValuesOnly && threshold <= 0) { - threshold = null; - } - - // Get dataMin and dataMax for X axes - if (axis.isXAxis) { - xData = series.xData; - if (xData.length) { - // If xData contains values which is not numbers, - // then filter them out. To prevent performance hit, - // we only do this after we have already found - // seriesDataMin because in most cases all data is - // valid. #5234. - seriesDataMin = arrayMin(xData); - seriesDataMax = arrayMax(xData); - - if ( - !isNumber(seriesDataMin) && - !(seriesDataMin instanceof Date) // #5010 - ) { - xData = grep(xData, isNumber); - // Do it again with valid data - seriesDataMin = arrayMin(xData); - seriesDataMax = arrayMax(xData); - } - - if (xData.length) { - axis.dataMin = Math.min( - pick(axis.dataMin, xData[0], seriesDataMin), - seriesDataMin - ); - axis.dataMax = Math.max( - pick(axis.dataMax, xData[0], seriesDataMax), - seriesDataMax - ); - } - } - - // Get dataMin and dataMax for Y axes, as well as handle - // stacking and processed data - } else { - - // Get this particular series extremes - series.getExtremes(); - seriesDataMax = series.dataMax; - seriesDataMin = series.dataMin; - - // Get the dataMin and dataMax so far. If percentage is - // used, the min and max are always 0 and 100. If - // seriesDataMin and seriesDataMax is null, then series - // doesn't have active y data, we continue with nulls - if (defined(seriesDataMin) && defined(seriesDataMax)) { - axis.dataMin = Math.min( - pick(axis.dataMin, seriesDataMin), - seriesDataMin - ); - axis.dataMax = Math.max( - pick(axis.dataMax, seriesDataMax), - seriesDataMax - ); - } - - // Adjust to threshold - if (defined(threshold)) { - axis.threshold = threshold; - } - // If any series has a hard threshold, it takes - // precedence - if ( - !seriesOptions.softThreshold || - axis.positiveValuesOnly - ) { - axis.softThreshold = false; - } - } - } - }); - }); - - fireEvent(this, 'afterGetSeriesExtremes'); - }, - - /** - * Translate from axis value to pixel position on the chart, or back. Use - * the `toPixels` and `toValue` functions in applications. - * - * @private - */ - translate: function ( - val, - backwards, - cvsCoord, - old, - handleLog, - pointPlacement - ) { - var axis = this.linkedParent || this, // #1417 - sign = 1, - cvsOffset = 0, - localA = old ? axis.oldTransA : axis.transA, - localMin = old ? axis.oldMin : axis.min, - returnValue, - minPixelPadding = axis.minPixelPadding, - doPostTranslate = ( - axis.isOrdinal || - axis.isBroken || - (axis.isLog && handleLog) - ) && axis.lin2val; - - if (!localA) { - localA = axis.transA; - } - - // In vertical axes, the canvas coordinates start from 0 at the top like - // in SVG. - if (cvsCoord) { - sign *= -1; // canvas coordinates inverts the value - cvsOffset = axis.len; - } - - // Handle reversed axis - if (axis.reversed) { - sign *= -1; - cvsOffset -= sign * (axis.sector || axis.len); - } - - // From pixels to value - if (backwards) { // reverse translation - - val = val * sign + cvsOffset; - val -= minPixelPadding; - returnValue = val / localA + localMin; // from chart pixel to value - if (doPostTranslate) { // log and ordinal axes - returnValue = axis.lin2val(returnValue); - } - - // From value to pixels - } else { - if (doPostTranslate) { // log and ordinal axes - val = axis.val2lin(val); - } - returnValue = isNumber(localMin) ? - ( - sign * (val - localMin) * localA + - cvsOffset + - (sign * minPixelPadding) + - (isNumber(pointPlacement) ? localA * pointPlacement : 0) - ) : - undefined; - } - - return returnValue; - }, - - /** - * Translate a value in terms of axis units into pixels within the chart. - * - * @param {Number} value - * A value in terms of axis units. - * @param {Boolean} paneCoordinates - * Whether to return the pixel coordinate relative to the chart or - * just the axis/pane itself. - * @return {Number} Pixel position of the value on the chart or axis. - */ - toPixels: function (value, paneCoordinates) { - return this.translate(value, false, !this.horiz, null, true) + - (paneCoordinates ? 0 : this.pos); - }, - - /** - * Translate a pixel position along the axis to a value in terms of axis - * units. - * @param {Number} pixel - * The pixel value coordinate. - * @param {Boolean} paneCoordiantes - * Whether the input pixel is relative to the chart or just the - * axis/pane itself. - * @return {Number} The axis value. - */ - toValue: function (pixel, paneCoordinates) { - return this.translate( - pixel - (paneCoordinates ? 0 : this.pos), - true, - !this.horiz, - null, - true - ); - }, - - /** - * Create the path for a plot line that goes from the given value on - * this axis, across the plot to the opposite side. Also used internally for - * grid lines and crosshairs. - * - * @param {Number} value - * Axis value. - * @param {Number} [lineWidth=1] - * Used for calculation crisp line coordinates. - * @param {Boolean} [old=false] - * Use old coordinates (for resizing and rescaling). - * @param {Boolean} [force=false] - * If `false`, the function will return null when it falls outside - * the axis bounds. - * @param {Number} [translatedValue] - * If given, return the plot line path of a pixel position on the - * axis. - * - * @return {Array.} - * The SVG path definition for the plot line. - */ - getPlotLinePath: function (value, lineWidth, old, force, translatedValue) { - var axis = this, - chart = axis.chart, - axisLeft = axis.left, - axisTop = axis.top, - x1, - y1, - x2, - y2, - cHeight = (old && chart.oldChartHeight) || chart.chartHeight, - cWidth = (old && chart.oldChartWidth) || chart.chartWidth, - skip, - transB = axis.transB, - /** - * Check if x is between a and b. If not, either move to a/b - * or skip, depending on the force parameter. - */ - between = function (x, a, b) { - if (x < a || x > b) { - if (force) { - x = Math.min(Math.max(a, x), b); - } else { - skip = true; - } - } - return x; - }; - - translatedValue = pick( - translatedValue, - axis.translate(value, null, null, old) - ); - // Keep the translated value within sane bounds, and avoid Infinity to - // fail the isNumber test (#7709). - translatedValue = Math.min(Math.max(-1e5, translatedValue), 1e5); - - - x1 = x2 = Math.round(translatedValue + transB); - y1 = y2 = Math.round(cHeight - translatedValue - transB); - if (!isNumber(translatedValue)) { // no min or max - skip = true; - force = false; // #7175, don't force it when path is invalid - } else if (axis.horiz) { - y1 = axisTop; - y2 = cHeight - axis.bottom; - x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); - } else { - x1 = axisLeft; - x2 = cWidth - axis.right; - y1 = y2 = between(y1, axisTop, axisTop + axis.height); - } - return skip && !force ? - null : - chart.renderer.crispLine( - ['M', x1, y1, 'L', x2, y2], - lineWidth || 1 - ); - }, - - /** - * Internal function to et the tick positions of a linear axis to round - * values like whole tens or every five. - * - * @param {Number} tickInterval - * The normalized tick interval - * @param {Number} min - * Axis minimum. - * @param {Number} max - * Axis maximum. - * - * @return {Array.} - * An array of axis values where ticks should be placed. - */ - getLinearTickPositions: function (tickInterval, min, max) { - var pos, - lastPos, - roundedMin = - correctFloat(Math.floor(min / tickInterval) * tickInterval), - roundedMax = - correctFloat(Math.ceil(max / tickInterval) * tickInterval), - tickPositions = [], - precision; - - // When the precision is higher than what we filter out in - // correctFloat, skip it (#6183). - if (correctFloat(roundedMin + tickInterval) === roundedMin) { - precision = 20; - } - - // For single points, add a tick regardless of the relative position - // (#2662, #6274) - if (this.single) { - return [min]; - } - - // Populate the intermediate values - pos = roundedMin; - while (pos <= roundedMax) { - - // Place the tick on the rounded value - tickPositions.push(pos); - - // Always add the raw tickInterval, not the corrected one. - pos = correctFloat( - pos + tickInterval, - precision - ); - - // If the interval is not big enough in the current min - max range - // to actually increase the loop variable, we need to break out to - // prevent endless loop. Issue #619 - if (pos === lastPos) { - break; - } - - // Record the last value - lastPos = pos; - } - return tickPositions; - }, - - /** - * Resolve the new minorTicks/minorTickInterval options into the legacy - * loosely typed minorTickInterval option. - */ - getMinorTickInterval: function () { - var options = this.options; - - if (options.minorTicks === true) { - return pick(options.minorTickInterval, 'auto'); - } - if (options.minorTicks === false) { - return null; - } - return options.minorTickInterval; - }, - - /** - * Internal function to return the minor tick positions. For logarithmic - * axes, the same logic as for major ticks is reused. - * - * @return {Array.} - * An array of axis values where ticks should be placed. - */ - getMinorTickPositions: function () { - var axis = this, - options = axis.options, - tickPositions = axis.tickPositions, - minorTickInterval = axis.minorTickInterval, - minorTickPositions = [], - pos, - pointRangePadding = axis.pointRangePadding || 0, - min = axis.min - pointRangePadding, // #1498 - max = axis.max + pointRangePadding, // #1498 - range = max - min; - - // If minor ticks get too dense, they are hard to read, and may cause - // long running script. So we don't draw them. - if (range && range / minorTickInterval < axis.len / 3) { // #3875 - - if (axis.isLog) { - // For each interval in the major ticks, compute the minor ticks - // separately. - each(this.paddedTicks, function (pos, i, paddedTicks) { - if (i) { - minorTickPositions.push.apply( - minorTickPositions, - axis.getLogTickPositions( - minorTickInterval, - paddedTicks[i - 1], - paddedTicks[i], - true - ) - ); - } - }); - - } else if ( - axis.isDatetimeAxis && - this.getMinorTickInterval() === 'auto' - ) { // #1314 - minorTickPositions = minorTickPositions.concat( - axis.getTimeTicks( - axis.normalizeTimeTickInterval(minorTickInterval), - min, - max, - options.startOfWeek - ) - ); - } else { - for ( - pos = min + (tickPositions[0] - min) % minorTickInterval; - pos <= max; - pos += minorTickInterval - ) { - // Very, very, tight grid lines (#5771) - if (pos === minorTickPositions[0]) { - break; - } - minorTickPositions.push(pos); - } - } - } - - if (minorTickPositions.length !== 0) { - axis.trimTicks(minorTickPositions); // #3652 #3743 #1498 #6330 - } - return minorTickPositions; - }, - - /** - * Adjust the min and max for the minimum range. Keep in mind that the - * series data is not yet processed, so we don't have information on data - * cropping and grouping, or updated axis.pointRange or series.pointRange. - * The data can't be processed until we have finally established min and - * max. - * - * @private - */ - adjustForMinRange: function () { - var axis = this, - options = axis.options, - min = axis.min, - max = axis.max, - zoomOffset, - spaceAvailable, - closestDataRange, - i, - distance, - xData, - loopLength, - minArgs, - maxArgs, - minRange; - - // Set the automatic minimum range based on the closest point distance - if (axis.isXAxis && axis.minRange === undefined && !axis.isLog) { - - if (defined(options.min) || defined(options.max)) { - axis.minRange = null; // don't do this again - - } else { - - // Find the closest distance between raw data points, as opposed - // to closestPointRange that applies to processed points - // (cropped and grouped) - each(axis.series, function (series) { - xData = series.xData; - loopLength = series.xIncrement ? 1 : xData.length - 1; - for (i = loopLength; i > 0; i--) { - distance = xData[i] - xData[i - 1]; - if ( - closestDataRange === undefined || - distance < closestDataRange - ) { - closestDataRange = distance; - } - } - }); - axis.minRange = Math.min( - closestDataRange * 5, - axis.dataMax - axis.dataMin - ); - } - } - - // if minRange is exceeded, adjust - if (max - min < axis.minRange) { - - spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange; - minRange = axis.minRange; - zoomOffset = (minRange - max + min) / 2; - - // if min and max options have been set, don't go beyond it - minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; - // If space is available, stay within the data range - if (spaceAvailable) { - minArgs[2] = axis.isLog ? - axis.log2lin(axis.dataMin) : - axis.dataMin; - } - min = arrayMax(minArgs); - - maxArgs = [min + minRange, pick(options.max, min + minRange)]; - // If space is availabe, stay within the data range - if (spaceAvailable) { - maxArgs[2] = axis.isLog ? - axis.log2lin(axis.dataMax) : - axis.dataMax; - } - - max = arrayMin(maxArgs); - - // now if the max is adjusted, adjust the min back - if (max - min < minRange) { - minArgs[0] = max - minRange; - minArgs[1] = pick(options.min, max - minRange); - min = arrayMax(minArgs); - } - } - - // Record modified extremes - axis.min = min; - axis.max = max; - }, - - /** - * Find the closestPointRange across all series. - * - * @private - */ - getClosest: function () { - var ret; - - if (this.categories) { - ret = 1; - } else { - each(this.series, function (series) { - var seriesClosest = series.closestPointRange, - visible = series.visible || - !series.chart.options.chart.ignoreHiddenSeries; - - if ( - !series.noSharedTooltip && - defined(seriesClosest) && - visible - ) { - ret = defined(ret) ? - Math.min(ret, seriesClosest) : - seriesClosest; - } - }); - } - return ret; - }, - - /** - * When a point name is given and no x, search for the name in the existing - * categories, or if categories aren't provided, search names or create a - * new category (#2522). - * - * @private - * - * @param {Point} - * The point to inspect. - * - * @return {Number} - * The X value that the point is given. - */ - nameToX: function (point) { - var explicitCategories = isArray(this.categories), - names = explicitCategories ? this.categories : this.names, - nameX = point.options.x, - x; - - point.series.requireSorting = false; - - if (!defined(nameX)) { - nameX = this.options.uniqueNames === false ? - point.series.autoIncrement() : - ( - explicitCategories ? - inArray(point.name, names) : - pick(names.keys[point.name], -1) - - ); - } - if (nameX === -1) { // Not found in currenct categories - if (!explicitCategories) { - x = names.length; - } - } else { - x = nameX; - } - - // Write the last point's name to the names array - if (x !== undefined) { - this.names[x] = point.name; - // Backwards mapping is much faster than array searching (#7725) - this.names.keys[point.name] = x; - } - - return x; - }, - - /** - * When changes have been done to series data, update the axis.names. - * - * @private - */ - updateNames: function () { - var axis = this, - names = this.names, - i = names.length; - - if (i > 0) { - each(H.keys(names.keys), function (key) { - delete names.keys[key]; - }); - names.length = 0; - - this.minRange = this.userMinRange; // Reset - each(this.series || [], function (series) { - - // Reset incrementer (#5928) - series.xIncrement = null; - - // When adding a series, points are not yet generated - if (!series.points || series.isDirtyData) { - series.processData(); - series.generatePoints(); - } - - each(series.points, function (point, i) { - var x; - if (point.options) { - x = axis.nameToX(point); - if (x !== undefined && x !== point.x) { - point.x = x; - series.xData[i] = x; - } - } - }); - }); - } - }, - - /** - * Update translation information. - * - * @private - */ - setAxisTranslation: function (saveOld) { - var axis = this, - range = axis.max - axis.min, - pointRange = axis.axisPointRange || 0, - closestPointRange, - minPointOffset = 0, - pointRangePadding = 0, - linkedParent = axis.linkedParent, - ordinalCorrection, - hasCategories = !!axis.categories, - transA = axis.transA, - isXAxis = axis.isXAxis; - - // Adjust translation for padding. Y axis with categories need to go - // through the same (#1784). - if (isXAxis || hasCategories || pointRange) { - - // Get the closest points - closestPointRange = axis.getClosest(); - - if (linkedParent) { - minPointOffset = linkedParent.minPointOffset; - pointRangePadding = linkedParent.pointRangePadding; - } else { - each(axis.series, function (series) { - var seriesPointRange = hasCategories ? - 1 : - ( - isXAxis ? - pick( - series.options.pointRange, - closestPointRange, - 0 - ) : - (axis.axisPointRange || 0) - ), // #2806 - pointPlacement = series.options.pointPlacement; - - pointRange = Math.max(pointRange, seriesPointRange); - - if (!axis.single) { - // minPointOffset is the value padding to the left of - // the axis in order to make room for points with a - // pointRange, typically columns. When the - // pointPlacement option is 'between' or 'on', this - // padding does not apply. - minPointOffset = Math.max( - minPointOffset, - isString(pointPlacement) ? 0 : seriesPointRange / 2 - ); - - // Determine the total padding needed to the length of - // the axis to make room for the pointRange. If the - // series' pointPlacement is 'on', no padding is added. - pointRangePadding = Math.max( - pointRangePadding, - pointPlacement === 'on' ? 0 : seriesPointRange - ); - } - }); - } - - // Record minPointOffset and pointRangePadding - ordinalCorrection = axis.ordinalSlope && closestPointRange ? - axis.ordinalSlope / closestPointRange : - 1; // #988, #1853 - axis.minPointOffset = minPointOffset = - minPointOffset * ordinalCorrection; - axis.pointRangePadding = - pointRangePadding = pointRangePadding * ordinalCorrection; - - // pointRange means the width reserved for each point, like in a - // column chart - axis.pointRange = Math.min(pointRange, range); - - // closestPointRange means the closest distance between points. In - // columns it is mostly equal to pointRange, but in lines pointRange - // is 0 while closestPointRange is some other value - if (isXAxis) { - axis.closestPointRange = closestPointRange; - } - } - - // Secondary values - if (saveOld) { - axis.oldTransA = transA; - } - axis.translationSlope = axis.transA = transA = - axis.options.staticScale || - axis.len / ((range + pointRangePadding) || 1); - - // Translation addend - axis.transB = axis.horiz ? axis.left : axis.bottom; - axis.minPixelPadding = transA * minPointOffset; - - fireEvent(this, 'afterSetAxisTranslation'); - }, - - minFromRange: function () { - return this.max - this.range; - }, - - /** - * Set the tick positions to round values and optionally extend the extremes - * to the nearest tick. - * - * @private - */ - setTickInterval: function (secondPass) { - var axis = this, - chart = axis.chart, - options = axis.options, - isLog = axis.isLog, - log2lin = axis.log2lin, - isDatetimeAxis = axis.isDatetimeAxis, - isXAxis = axis.isXAxis, - isLinked = axis.isLinked, - maxPadding = options.maxPadding, - minPadding = options.minPadding, - length, - linkedParentExtremes, - tickIntervalOption = options.tickInterval, - minTickInterval, - tickPixelIntervalOption = options.tickPixelInterval, - categories = axis.categories, - threshold = axis.threshold, - softThreshold = axis.softThreshold, - thresholdMin, - thresholdMax, - hardMin, - hardMax; - - if (!isDatetimeAxis && !categories && !isLinked) { - this.getTickAmount(); - } - - // Min or max set either by zooming/setExtremes or initial options - hardMin = pick(axis.userMin, options.min); - hardMax = pick(axis.userMax, options.max); - - // Linked axis gets the extremes from the parent axis - if (isLinked) { - axis.linkedParent = chart[axis.coll][options.linkedTo]; - linkedParentExtremes = axis.linkedParent.getExtremes(); - axis.min = pick( - linkedParentExtremes.min, - linkedParentExtremes.dataMin - ); - axis.max = pick( - linkedParentExtremes.max, - linkedParentExtremes.dataMax - ); - if (options.type !== axis.linkedParent.options.type) { - H.error(11, 1); // Can't link axes of different type - } - - // Initial min and max from the extreme data values - } else { - - // Adjust to hard threshold - if (!softThreshold && defined(threshold)) { - if (axis.dataMin >= threshold) { - thresholdMin = threshold; - minPadding = 0; - } else if (axis.dataMax <= threshold) { - thresholdMax = threshold; - maxPadding = 0; - } - } - - axis.min = pick(hardMin, thresholdMin, axis.dataMin); - axis.max = pick(hardMax, thresholdMax, axis.dataMax); - - } - - if (isLog) { - if ( - axis.positiveValuesOnly && - !secondPass && - Math.min(axis.min, pick(axis.dataMin, axis.min)) <= 0 - ) { // #978 - H.error(10, 1); // Can't plot negative values on log axis - } - // The correctFloat cures #934, float errors on full tens. But it - // was too aggressive for #4360 because of conversion back to lin, - // therefore use precision 15. - axis.min = correctFloat(log2lin(axis.min), 15); - axis.max = correctFloat(log2lin(axis.max), 15); - } - - // handle zoomed range - if (axis.range && defined(axis.max)) { - axis.userMin = axis.min = hardMin = - Math.max(axis.dataMin, axis.minFromRange()); // #618, #6773 - axis.userMax = hardMax = axis.max; - - axis.range = null; // don't use it when running setExtremes - } - - // Hook for Highstock Scroller. Consider combining with beforePadding. - fireEvent(axis, 'foundExtremes'); - - // Hook for adjusting this.min and this.max. Used by bubble series. - if (axis.beforePadding) { - axis.beforePadding(); - } - - // adjust min and max for the minimum range - axis.adjustForMinRange(); - - // Pad the values to get clear of the chart's edges. To avoid - // tickInterval taking the padding into account, we do this after - // computing tick interval (#1337). - if ( - !categories && - !axis.axisPointRange && - !axis.usePercentage && - !isLinked && - defined(axis.min) && - defined(axis.max) - ) { - length = axis.max - axis.min; - if (length) { - if (!defined(hardMin) && minPadding) { - axis.min -= length * minPadding; - } - if (!defined(hardMax) && maxPadding) { - axis.max += length * maxPadding; - } - } - } - - // Handle options for floor, ceiling, softMin and softMax (#6359) - if (isNumber(options.softMin) && !isNumber(axis.userMin)) { - axis.min = Math.min(axis.min, options.softMin); - } - if (isNumber(options.softMax) && !isNumber(axis.userMax)) { - axis.max = Math.max(axis.max, options.softMax); - } - if (isNumber(options.floor)) { - axis.min = Math.max(axis.min, options.floor); - } - if (isNumber(options.ceiling)) { - axis.max = Math.min(axis.max, options.ceiling); - } - - - // When the threshold is soft, adjust the extreme value only if the data - // extreme and the padded extreme land on either side of the threshold. - // For example, a series of [0, 1, 2, 3] would make the yAxis add a tick - // for -1 because of the default minPadding and startOnTick options. - // This is prevented by the softThreshold option. - if (softThreshold && defined(axis.dataMin)) { - threshold = threshold || 0; - if ( - !defined(hardMin) && - axis.min < threshold && - axis.dataMin >= threshold - ) { - axis.min = threshold; - - } else if ( - !defined(hardMax) && - axis.max > threshold && - axis.dataMax <= threshold - ) { - axis.max = threshold; - } - } - - - // get tickInterval - if ( - axis.min === axis.max || - axis.min === undefined || - axis.max === undefined - ) { - axis.tickInterval = 1; - - } else if ( - isLinked && - !tickIntervalOption && - tickPixelIntervalOption === - axis.linkedParent.options.tickPixelInterval - ) { - axis.tickInterval = tickIntervalOption = - axis.linkedParent.tickInterval; - - } else { - axis.tickInterval = pick( - tickIntervalOption, - this.tickAmount ? - ((axis.max - axis.min) / Math.max(this.tickAmount - 1, 1)) : - undefined, - // For categoried axis, 1 is default, for linear axis use - // tickPix - categories ? - 1 : - // don't let it be more than the data range - (axis.max - axis.min) * tickPixelIntervalOption / - Math.max(axis.len, tickPixelIntervalOption) - ); - } - - /** - * Now we're finished detecting min and max, crop and group series data. - * This is in turn needed in order to find tick positions in - * ordinal axes. - */ - if (isXAxis && !secondPass) { - each(axis.series, function (series) { - series.processData( - axis.min !== axis.oldMin || axis.max !== axis.oldMax - ); - }); - } - - // set the translation factor used in translate function - axis.setAxisTranslation(true); - - // hook for ordinal axes and radial axes - if (axis.beforeSetTickPositions) { - axis.beforeSetTickPositions(); - } - - // hook for extensions, used in Highstock ordinal axes - if (axis.postProcessTickInterval) { - axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); - } - - // In column-like charts, don't cramp in more ticks than there are - // points (#1943, #4184) - if (axis.pointRange && !tickIntervalOption) { - axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval); - } - - // Before normalizing the tick interval, handle minimum tick interval. - // This applies only if tickInterval is not defined. - minTickInterval = pick( - options.minTickInterval, - axis.isDatetimeAxis && axis.closestPointRange - ); - if (!tickIntervalOption && axis.tickInterval < minTickInterval) { - axis.tickInterval = minTickInterval; - } - - // for linear axes, get magnitude and normalize the interval - if (!isDatetimeAxis && !isLog && !tickIntervalOption) { - axis.tickInterval = normalizeTickInterval( - axis.tickInterval, - null, - getMagnitude(axis.tickInterval), - // If the tick interval is between 0.5 and 5 and the axis max is - // in the order of thousands, chances are we are dealing with - // years. Don't allow decimals. #3363. - pick( - options.allowDecimals, - !( - axis.tickInterval > 0.5 && - axis.tickInterval < 5 && - axis.max > 1000 && - axis.max < 9999 - ) - ), - !!this.tickAmount - ); - } - - // Prevent ticks from getting so close that we can't draw the labels - if (!this.tickAmount) { - axis.tickInterval = axis.unsquish(); - } - - this.setTickPositions(); - }, - - /** - * Now we have computed the normalized tickInterval, get the tick positions - */ - setTickPositions: function () { - - var options = this.options, - tickPositions, - tickPositionsOption = options.tickPositions, - minorTickIntervalOption = this.getMinorTickInterval(), - tickPositioner = options.tickPositioner, - startOnTick = options.startOnTick, - endOnTick = options.endOnTick; - - // Set the tickmarkOffset - this.tickmarkOffset = ( - this.categories && - options.tickmarkPlacement === 'between' && - this.tickInterval === 1 - ) ? 0.5 : 0; // #3202 - - - // get minorTickInterval - this.minorTickInterval = - minorTickIntervalOption === 'auto' && - this.tickInterval ? - this.tickInterval / 5 : - minorTickIntervalOption; - - // When there is only one point, or all points have the same value on - // this axis, then min and max are equal and tickPositions.length is 0 - // or 1. In this case, add some padding in order to center the point, - // but leave it with one tick. #1337. - this.single = - this.min === this.max && - defined(this.min) && - !this.tickAmount && - ( - // Data is on integer (#6563) - parseInt(this.min, 10) === this.min || - - // Between integers and decimals are not allowed (#6274) - options.allowDecimals !== false - ); - - // Find the tick positions. Work on a copy (#1565) - this.tickPositions = tickPositions = - tickPositionsOption && tickPositionsOption.slice(); - if (!tickPositions) { - - if (this.isDatetimeAxis) { - tickPositions = this.getTimeTicks( - this.normalizeTimeTickInterval( - this.tickInterval, - options.units - ), - this.min, - this.max, - options.startOfWeek, - this.ordinalPositions, - this.closestPointRange, - true - ); - } else if (this.isLog) { - tickPositions = this.getLogTickPositions( - this.tickInterval, - this.min, - this.max - ); - } else { - tickPositions = this.getLinearTickPositions( - this.tickInterval, - this.min, - this.max - ); - } - - // Too dense ticks, keep only the first and last (#4477) - if (tickPositions.length > this.len) { - tickPositions = [tickPositions[0], tickPositions.pop()]; - // Reduce doubled value (#7339) - if (tickPositions[0] === tickPositions[1]) { - tickPositions.length = 1; - } - } - - this.tickPositions = tickPositions; - - // Run the tick positioner callback, that allows modifying auto tick - // positions. - if (tickPositioner) { - tickPositioner = tickPositioner.apply( - this, - [this.min, this.max] - ); - if (tickPositioner) { - this.tickPositions = tickPositions = tickPositioner; - } - } - - } - - // Reset min/max or remove extremes based on start/end on tick - this.paddedTicks = tickPositions.slice(0); // Used for logarithmic minor - this.trimTicks(tickPositions, startOnTick, endOnTick); - if (!this.isLinked) { - - // Substract half a unit (#2619, #2846, #2515, #3390), - // but not in case of multiple ticks (#6897) - if (this.single && tickPositions.length < 2) { - this.min -= 0.5; - this.max += 0.5; - } - if (!tickPositionsOption && !tickPositioner) { - this.adjustTickAmount(); - } - } - - fireEvent(this, 'afterSetTickPositions'); - }, - - /** - * Handle startOnTick and endOnTick by either adapting to padding min/max or - * rounded min/max. Also handle single data points. - * - * @private - */ - trimTicks: function (tickPositions, startOnTick, endOnTick) { - var roundedMin = tickPositions[0], - roundedMax = tickPositions[tickPositions.length - 1], - minPointOffset = this.minPointOffset || 0; - - if (!this.isLinked) { - if (startOnTick && roundedMin !== -Infinity) { // #6502 - this.min = roundedMin; - } else { - while (this.min - minPointOffset > tickPositions[0]) { - tickPositions.shift(); - } - } - - if (endOnTick) { - this.max = roundedMax; - } else { - while (this.max + minPointOffset < - tickPositions[tickPositions.length - 1]) { - tickPositions.pop(); - } - } - - // If no tick are left, set one tick in the middle (#3195) - if ( - tickPositions.length === 0 && - defined(roundedMin) && - !this.options.tickPositions - ) { - tickPositions.push((roundedMax + roundedMin) / 2); - } - } - }, - - /** - * Check if there are multiple axes in the same pane. - * - * @private - * @return {Boolean} - * True if there are other axes. - */ - alignToOthers: function () { - var others = {}, // Whether there is another axis to pair with this one - hasOther, - options = this.options; - - if ( - // Only if alignTicks is true - this.chart.options.chart.alignTicks !== false && - options.alignTicks !== false && - - // Disabled when startOnTick or endOnTick are false (#7604) - options.startOnTick !== false && - options.endOnTick !== false && - - // Don't try to align ticks on a log axis, they are not evenly - // spaced (#6021) - !this.isLog - ) { - each(this.chart[this.coll], function (axis) { - var otherOptions = axis.options, - horiz = axis.horiz, - key = [ - horiz ? otherOptions.left : otherOptions.top, - otherOptions.width, - otherOptions.height, - otherOptions.pane - ].join(','); - - - if (axis.series.length) { // #4442 - if (others[key]) { - hasOther = true; // #4201 - } else { - others[key] = 1; - } - } - }); - } - return hasOther; - }, - - /** - * Find the max ticks of either the x and y axis collection, and record it - * in `this.tickAmount`. - * - * @private - */ - getTickAmount: function () { - var options = this.options, - tickAmount = options.tickAmount, - tickPixelInterval = options.tickPixelInterval; - - if ( - !defined(options.tickInterval) && - this.len < tickPixelInterval && - !this.isRadial && - !this.isLog && - options.startOnTick && - options.endOnTick - ) { - tickAmount = 2; - } - - if (!tickAmount && this.alignToOthers()) { - // Add 1 because 4 tick intervals require 5 ticks (including first - // and last) - tickAmount = Math.ceil(this.len / tickPixelInterval) + 1; - } - - // For tick amounts of 2 and 3, compute five ticks and remove the - // intermediate ones. This prevents the axis from adding ticks that are - // too far away from the data extremes. - if (tickAmount < 4) { - this.finalTickAmt = tickAmount; - tickAmount = 5; - } - - this.tickAmount = tickAmount; - }, - - /** - * When using multiple axes, adjust the number of ticks to match the highest - * number of ticks in that group. - * - * @private - */ - adjustTickAmount: function () { - var tickInterval = this.tickInterval, - tickPositions = this.tickPositions, - tickAmount = this.tickAmount, - finalTickAmt = this.finalTickAmt, - currentTickAmount = tickPositions && tickPositions.length, - threshold = pick(this.threshold, this.softThreshold ? 0 : null), - i, - len; - - if (this.hasData()) { - if (currentTickAmount < tickAmount) { - while (tickPositions.length < tickAmount) { - - // Extend evenly for both sides unless we're on the - // threshold (#3965) - if ( - tickPositions.length % 2 || - this.min === threshold - ) { - // to the end - tickPositions.push(correctFloat( - tickPositions[tickPositions.length - 1] + - tickInterval - )); - } else { - // to the start - tickPositions.unshift(correctFloat( - tickPositions[0] - tickInterval - )); - } - } - this.transA *= (currentTickAmount - 1) / (tickAmount - 1); - this.min = tickPositions[0]; - this.max = tickPositions[tickPositions.length - 1]; - - // We have too many ticks, run second pass to try to reduce ticks - } else if (currentTickAmount > tickAmount) { - this.tickInterval *= 2; - this.setTickPositions(); - } - - // The finalTickAmt property is set in getTickAmount - if (defined(finalTickAmt)) { - i = len = tickPositions.length; - while (i--) { - if ( - // Remove every other tick - (finalTickAmt === 3 && i % 2 === 1) || - // Remove all but first and last - (finalTickAmt <= 2 && i > 0 && i < len - 1) - ) { - tickPositions.splice(i, 1); - } - } - this.finalTickAmt = undefined; - } - } - }, - - /** - * Set the scale based on data min and max, user set min and max or options. - * - * @private - */ - setScale: function () { - var axis = this, - isDirtyData, - isDirtyAxisLength; - - axis.oldMin = axis.min; - axis.oldMax = axis.max; - axis.oldAxisLength = axis.len; - - // set the new axisLength - axis.setAxisSize(); - isDirtyAxisLength = axis.len !== axis.oldAxisLength; - - // is there new data? - each(axis.series, function (series) { - if ( - series.isDirtyData || - series.isDirty || - // When x axis is dirty, we need new data extremes for y as well - series.xAxis.isDirty - ) { - isDirtyData = true; - } - }); - - // do we really need to go through all this? - if ( - isDirtyAxisLength || - isDirtyData || - axis.isLinked || - axis.forceRedraw || - axis.userMin !== axis.oldUserMin || - axis.userMax !== axis.oldUserMax || - axis.alignToOthers() - ) { - - if (axis.resetStacks) { - axis.resetStacks(); - } - - axis.forceRedraw = false; - - // get data extremes if needed - axis.getSeriesExtremes(); - - // get fixed positions based on tickInterval - axis.setTickInterval(); - - // record old values to decide whether a rescale is necessary later - // on (#540) - axis.oldUserMin = axis.userMin; - axis.oldUserMax = axis.userMax; - - // Mark as dirty if it is not already set to dirty and extremes have - // changed. #595. - if (!axis.isDirty) { - axis.isDirty = - isDirtyAxisLength || - axis.min !== axis.oldMin || - axis.max !== axis.oldMax; - } - } else if (axis.cleanStacks) { - axis.cleanStacks(); - } - - fireEvent(this, 'afterSetScale'); - }, - - /** - * Set the minimum and maximum of the axes after render time. If the - * `startOnTick` and `endOnTick` options are true, the minimum and maximum - * values are rounded off to the nearest tick. To prevent this, these - * options can be set to false before calling setExtremes. Also, setExtremes - * will not allow a range lower than the `minRange` option, which by default - * is the range of five points. - * - * @param {Number} [newMin] - * The new minimum value. - * @param {Number} [newMax] - * The new maximum value. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart or wait for an explicit call to - * {@link Highcharts.Chart#redraw} - * @param {AnimationOptions} [animation=true] - * Enable or modify animations. - * @param {Object} [eventArguments] - * Arguments to be accessed in event handler. - * - * @sample highcharts/members/axis-setextremes/ - * Set extremes from a button - * @sample highcharts/members/axis-setextremes-datetime/ - * Set extremes on a datetime axis - * @sample highcharts/members/axis-setextremes-off-ticks/ - * Set extremes off ticks - * @sample stock/members/axis-setextremes/ - * Set extremes in Highstock - * @sample maps/members/axis-setextremes/ - * Set extremes in Highmaps - */ - setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { - var axis = this, - chart = axis.chart; - - redraw = pick(redraw, true); // defaults to true - - each(axis.series, function (serie) { - delete serie.kdTree; - }); - - // Extend the arguments with min and max - eventArguments = extend(eventArguments, { - min: newMin, - max: newMax - }); - - // Fire the event - fireEvent(axis, 'setExtremes', eventArguments, function () { - - axis.userMin = newMin; - axis.userMax = newMax; - axis.eventArgs = eventArguments; - - if (redraw) { - chart.redraw(animation); - } - }); - }, - - /** - * Overridable method for zooming chart. Pulled out in a separate method to - * allow overriding in stock charts. - * - * @private - */ - zoom: function (newMin, newMax) { - var dataMin = this.dataMin, - dataMax = this.dataMax, - options = this.options, - min = Math.min(dataMin, pick(options.min, dataMin)), - max = Math.max(dataMax, pick(options.max, dataMax)); - - if (newMin !== this.min || newMax !== this.max) { // #5790 - - // Prevent pinch zooming out of range. Check for defined is for - // #1946. #1734. - if (!this.allowZoomOutside) { - // #6014, sometimes newMax will be smaller than min (or newMin - // will be larger than max). - if (defined(dataMin)) { - if (newMin < min) { - newMin = min; - } - if (newMin > max) { - newMin = max; - } - } - if (defined(dataMax)) { - if (newMax < min) { - newMax = min; - } - if (newMax > max) { - newMax = max; - } - } - } - - // In full view, displaying the reset zoom button is not required - this.displayBtn = newMin !== undefined || newMax !== undefined; - - // Do it - this.setExtremes( - newMin, - newMax, - false, - undefined, - { trigger: 'zoom' } - ); - } - - return true; - }, - - /** - * Update the axis metrics. - * - * @private - */ - setAxisSize: function () { - var chart = this.chart, - options = this.options, - // [top, right, bottom, left] - offsets = options.offsets || [0, 0, 0, 0], - horiz = this.horiz, - - // Check for percentage based input values. Rounding fixes problems - // with column overflow and plot line filtering (#4898, #4899) - width = this.width = Math.round(H.relativeLength( - pick( - options.width, - chart.plotWidth - offsets[3] + offsets[1] - ), - chart.plotWidth - )), - height = this.height = Math.round(H.relativeLength( - pick( - options.height, - chart.plotHeight - offsets[0] + offsets[2] - ), - chart.plotHeight - )), - top = this.top = Math.round(H.relativeLength( - pick(options.top, chart.plotTop + offsets[0]), - chart.plotHeight, - chart.plotTop - )), - left = this.left = Math.round(H.relativeLength( - pick(options.left, chart.plotLeft + offsets[3]), - chart.plotWidth, - chart.plotLeft - )); - - // Expose basic values to use in Series object and navigator - this.bottom = chart.chartHeight - height - top; - this.right = chart.chartWidth - width - left; - - // Direction agnostic properties - this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905 - this.pos = horiz ? left : top; // distance from SVG origin - }, - - /** - * The returned object literal from the {@link Highcharts.Axis#getExtremes} - * function. - * - * @typedef {Object} Extremes - * @property {Number} dataMax - * The maximum value of the axis' associated series. - * @property {Number} dataMin - * The minimum value of the axis' associated series. - * @property {Number} max - * The maximum axis value, either automatic or set manually. If - * the `max` option is not set, `maxPadding` is 0 and `endOnTick` - * is false, this value will be the same as `dataMax`. - * @property {Number} min - * The minimum axis value, either automatic or set manually. If - * the `min` option is not set, `minPadding` is 0 and - * `startOnTick` is false, this value will be the same - * as `dataMin`. - */ - /** - * Get the current extremes for the axis. - * - * @returns {Extremes} - * An object containing extremes information. - * - * @sample highcharts/members/axis-getextremes/ - * Report extremes by click on a button - * @sample maps/members/axis-getextremes/ - * Get extremes in Highmaps - */ - getExtremes: function () { - var axis = this, - isLog = axis.isLog, - lin2log = axis.lin2log; - - return { - min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, - max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, - dataMin: axis.dataMin, - dataMax: axis.dataMax, - userMin: axis.userMin, - userMax: axis.userMax - }; - }, - - /** - * Get the zero plane either based on zero or on the min or max value. - * Used in bar and area plots. - * - * @param {Number} threshold - * The threshold in axis values. - * - * @return {Number} - * The translated threshold position in terms of pixels, and - * corrected to stay within the axis bounds. - */ - getThreshold: function (threshold) { - var axis = this, - isLog = axis.isLog, - lin2log = axis.lin2log, - realMin = isLog ? lin2log(axis.min) : axis.min, - realMax = isLog ? lin2log(axis.max) : axis.max; - - if (threshold === null) { - threshold = realMin; - } else if (realMin > threshold) { - threshold = realMin; - } else if (realMax < threshold) { - threshold = realMax; - } - - return axis.translate(threshold, 0, 1, 0, 1); - }, - - /** - * Compute auto alignment for the axis label based on which side the axis is - * on and the given rotation for the label. - * - * @param {Number} rotation - * The rotation in degrees as set by either the `rotation` or - * `autoRotation` options. - * @private - */ - autoLabelAlign: function (rotation) { - var ret, - angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; - - if (angle > 15 && angle < 165) { - ret = 'right'; - } else if (angle > 195 && angle < 345) { - ret = 'left'; - } else { - ret = 'center'; - } - return ret; - }, - - /** - * Get the tick length and width for the axis based on axis options. - * - * @private - * - * @param {String} prefix - * 'tick' or 'minorTick' - * @return {Array.} - * An array of tickLength and tickWidth - */ - tickSize: function (prefix) { - var options = this.options, - tickLength = options[prefix + 'Length'], - tickWidth = pick( - options[prefix + 'Width'], - prefix === 'tick' && this.isXAxis ? 1 : 0 // X axis default 1 - ); - - if (tickWidth && tickLength) { - // Negate the length - if (options[prefix + 'Position'] === 'inside') { - tickLength = -tickLength; - } - return [tickLength, tickWidth]; - } - - }, - - /** - * Return the size of the labels. - * - * @private - */ - labelMetrics: function () { - var index = this.tickPositions && this.tickPositions[0] || 0; - return this.chart.renderer.fontMetrics( - this.options.labels.style && this.options.labels.style.fontSize, - this.ticks[index] && this.ticks[index].label - ); - }, - - /** - * Prevent the ticks from getting so close we can't draw the labels. On a - * horizontal axis, this is handled by rotating the labels, removing ticks - * and adding ellipsis. On a vertical axis remove ticks and add ellipsis. - * - * @private - */ - unsquish: function () { - var labelOptions = this.options.labels, - horiz = this.horiz, - tickInterval = this.tickInterval, - newTickInterval = tickInterval, - slotSize = this.len / ( - ((this.categories ? 1 : 0) + this.max - this.min) / tickInterval - ), - rotation, - rotationOption = labelOptions.rotation, - labelMetrics = this.labelMetrics(), - step, - bestScore = Number.MAX_VALUE, - autoRotation, - // Return the multiple of tickInterval that is needed to avoid - // collision - getStep = function (spaceNeeded) { - var step = spaceNeeded / (slotSize || 1); - step = step > 1 ? Math.ceil(step) : 1; - return step * tickInterval; - }; - - if (horiz) { - autoRotation = !labelOptions.staggerLines && - !labelOptions.step && - ( // #3971 - defined(rotationOption) ? - [rotationOption] : - slotSize < pick(labelOptions.autoRotationLimit, 80) && - labelOptions.autoRotation - ); - - if (autoRotation) { - - // Loop over the given autoRotation options, and determine - // which gives the best score. The best score is that with - // the lowest number of steps and a rotation closest - // to horizontal. - each(autoRotation, function (rot) { - var score; - - if ( - rot === rotationOption || - (rot && rot >= -90 && rot <= 90) - ) { // #3891 - - step = getStep( - Math.abs(labelMetrics.h / Math.sin(deg2rad * rot)) - ); - - score = step + Math.abs(rot / 360); - - if (score < bestScore) { - bestScore = score; - rotation = rot; - newTickInterval = step; - } - } - }); - } - - } else if (!labelOptions.step) { // #4411 - newTickInterval = getStep(labelMetrics.h); - } - - this.autoRotation = autoRotation; - this.labelRotation = pick(rotation, rotationOption); - - return newTickInterval; - }, - - /** - * Get the general slot width for labels/categories on this axis. This may - * change between the pre-render (from Axis.getOffset) and the final tick - * rendering and placement. - * - * @private - * @return {Number} - * The pixel width allocated to each axis label. - */ - getSlotWidth: function () { - // #5086, #1580, #1931 - var chart = this.chart, - horiz = this.horiz, - labelOptions = this.options.labels, - slotCount = Math.max( - this.tickPositions.length - (this.categories ? 0 : 1), - 1 - ), - marginLeft = chart.margin[3]; - - return ( - horiz && - (labelOptions.step || 0) < 2 && - !labelOptions.rotation && // #4415 - ((this.staggerLines || 1) * this.len) / slotCount - ) || ( - !horiz && ( - // #7028 - ( - labelOptions.style && - parseInt(labelOptions.style.width, 10) - ) || - ( - marginLeft && - (marginLeft - chart.spacing[3]) - ) || - chart.chartWidth * 0.33 - ) - ); - - }, - - /** - * Render the axis labels and determine whether ellipsis or rotation need - * to be applied. - * - * @private - */ - renderUnsquish: function () { - var chart = this.chart, - renderer = chart.renderer, - tickPositions = this.tickPositions, - ticks = this.ticks, - labelOptions = this.options.labels, - horiz = this.horiz, - slotWidth = this.getSlotWidth(), - innerWidth = Math.max( - 1, - Math.round(slotWidth - 2 * (labelOptions.padding || 5)) - ), - attr = {}, - labelMetrics = this.labelMetrics(), - textOverflowOption = labelOptions.style && - labelOptions.style.textOverflow, - commonWidth, - commonTextOverflow, - maxLabelLength = 0, - label, - i, - pos; - - // Set rotation option unless it is "auto", like in gauges - if (!isString(labelOptions.rotation)) { - attr.rotation = labelOptions.rotation || 0; // #4443 - } - - // Get the longest label length - each(tickPositions, function (tick) { - tick = ticks[tick]; - if ( - tick && - tick.label && - tick.label.textPxLength > maxLabelLength - ) { - maxLabelLength = tick.label.textPxLength; - } - }); - this.maxLabelLength = maxLabelLength; - - - // Handle auto rotation on horizontal axis - if (this.autoRotation) { - - // Apply rotation only if the label is too wide for the slot, and - // the label is wider than its height. - if ( - maxLabelLength > innerWidth && - maxLabelLength > labelMetrics.h - ) { - attr.rotation = this.labelRotation; - } else { - this.labelRotation = 0; - } - - // Handle word-wrap or ellipsis on vertical axis - } else if (slotWidth) { - // For word-wrap or ellipsis - commonWidth = innerWidth; - - if (!textOverflowOption) { - commonTextOverflow = 'clip'; - - // On vertical axis, only allow word wrap if there is room - // for more lines. - i = tickPositions.length; - while (!horiz && i--) { - pos = tickPositions[i]; - label = ticks[pos].label; - if (label) { - // Reset ellipsis in order to get the correct - // bounding box (#4070) - if ( - label.styles && - label.styles.textOverflow === 'ellipsis' - ) { - label.css({ textOverflow: 'clip' }); - - // Set the correct width in order to read - // the bounding box height (#4678, #5034) - } else if (label.textPxLength > slotWidth) { - label.css({ width: slotWidth + 'px' }); - } - - if ( - label.getBBox().height > ( - this.len / tickPositions.length - - (labelMetrics.h - labelMetrics.f) - ) - ) { - label.specificTextOverflow = 'ellipsis'; - } - } - } - } - } - - - // Add ellipsis if the label length is significantly longer than ideal - if (attr.rotation) { - commonWidth = ( - maxLabelLength > chart.chartHeight * 0.5 ? - chart.chartHeight * 0.33 : - chart.chartHeight - ); - if (!textOverflowOption) { - commonTextOverflow = 'ellipsis'; - } - } - - // Set the explicit or automatic label alignment - this.labelAlign = labelOptions.align || - this.autoLabelAlign(this.labelRotation); - if (this.labelAlign) { - attr.align = this.labelAlign; - } - - // Apply general and specific CSS - each(tickPositions, function (pos) { - var tick = ticks[pos], - label = tick && tick.label, - css = {}; - if (label) { - // This needs to go before the CSS in old IE (#4502) - label.attr(attr); - - if ( - commonWidth && - !(labelOptions.style && labelOptions.style.width) && - ( - // Speed optimizing, #7656 - commonWidth < label.textPxLength || - // Resetting CSS, #4928 - label.element.tagName === 'SPAN' - ) - ) { - css.width = commonWidth; - if (!textOverflowOption) { - css.textOverflow = ( - label.specificTextOverflow || - commonTextOverflow - ); - } - label.css(css); - - } - delete label.specificTextOverflow; - tick.rotation = attr.rotation; - } - }); - - // Note: Why is this not part of getLabelPosition? - this.tickRotCorr = renderer.rotCorr( - labelMetrics.b, - this.labelRotation || 0, - this.side !== 0 - ); - }, - - /** - * Return true if the axis has associated data. - * - * @return {Boolean} - * True if the axis has associated visible series and those series - * have either valid data points or explicit `min` and `max` - * settings. - */ - hasData: function () { - return ( - this.hasVisibleSeries || - ( - defined(this.min) && - defined(this.max) && - this.tickPositions && - this.tickPositions.length > 0 - ) - ); - }, - - /** - * Adds the title defined in axis.options.title. - * @param {Boolean} display - whether or not to display the title - */ - addTitle: function (display) { - var axis = this, - renderer = axis.chart.renderer, - horiz = axis.horiz, - opposite = axis.opposite, - options = axis.options, - axisTitleOptions = options.title, - textAlign; - - if (!axis.axisTitle) { - textAlign = axisTitleOptions.textAlign; - if (!textAlign) { - textAlign = (horiz ? { - low: 'left', - middle: 'center', - high: 'right' - } : { - low: opposite ? 'right' : 'left', - middle: 'center', - high: opposite ? 'left' : 'right' - })[axisTitleOptions.align]; - } - axis.axisTitle = renderer.text( - axisTitleOptions.text, - 0, - 0, - axisTitleOptions.useHTML - ) - .attr({ - zIndex: 7, - rotation: axisTitleOptions.rotation || 0, - align: textAlign - }) - .addClass('highcharts-axis-title') - /*= if (build.classic) { =*/ - // #7814, don't mutate style option - .css(merge(axisTitleOptions.style)) - /*= } =*/ - .add(axis.axisGroup); - axis.axisTitle.isNew = true; - } - - // Max width defaults to the length of the axis - /*= if (build.classic) { =*/ - if (!axisTitleOptions.style.width && !axis.isRadial) { - /*= } =*/ - axis.axisTitle.css({ - width: axis.len - }); - /*= if (build.classic) { =*/ - } - /*= } =*/ - - // hide or show the title depending on whether showEmpty is set - axis.axisTitle[display ? 'show' : 'hide'](true); - }, - - /** - * Generates a tick for initial positioning. - * - * @private - * @param {number} pos - * The tick position in axis values. - * @param {number} i - * The index of the tick in {@link Axis.tickPositions}. - */ - generateTick: function (pos) { - var ticks = this.ticks; - - if (!ticks[pos]) { - ticks[pos] = new Tick(this, pos); - } else { - ticks[pos].addLabel(); // update labels depending on tick interval - } - }, - - /** - * Render the tick labels to a preliminary position to get their sizes. - * - * @private - */ - getOffset: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - tickPositions = axis.tickPositions, - ticks = axis.ticks, - horiz = axis.horiz, - side = axis.side, - invertedSide = chart.inverted && - !axis.isZAxis ? [1, 0, 3, 2][side] : side, - hasData, - showAxis, - titleOffset = 0, - titleOffsetOption, - titleMargin = 0, - axisTitleOptions = options.title, - labelOptions = options.labels, - labelOffset = 0, // reset - labelOffsetPadded, - axisOffset = chart.axisOffset, - clipOffset = chart.clipOffset, - clip, - directionFactor = [-1, 1, 1, -1][side], - className = options.className, - axisParent = axis.axisParent, // Used in color axis - lineHeightCorrection, - tickSize = this.tickSize('tick'); - - // For reuse in Axis.render - hasData = axis.hasData(); - axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); - - // Set/reset staggerLines - axis.staggerLines = axis.horiz && labelOptions.staggerLines; - - // Create the axisGroup and gridGroup elements on first iteration - if (!axis.axisGroup) { - axis.gridGroup = renderer.g('grid') - .attr({ zIndex: options.gridZIndex || 1 }) - .addClass( - 'highcharts-' + this.coll.toLowerCase() + '-grid ' + - (className || '') - ) - .add(axisParent); - axis.axisGroup = renderer.g('axis') - .attr({ zIndex: options.zIndex || 2 }) - .addClass( - 'highcharts-' + this.coll.toLowerCase() + ' ' + - (className || '') - ) - .add(axisParent); - axis.labelGroup = renderer.g('axis-labels') - .attr({ zIndex: labelOptions.zIndex || 7 }) - .addClass( - 'highcharts-' + axis.coll.toLowerCase() + '-labels ' + - (className || '') - ) - .add(axisParent); - } - - if (hasData || axis.isLinked) { - - // Generate ticks - each(tickPositions, function (pos, i) { - // i is not used here, but may be used in overrides - axis.generateTick(pos, i); - }); - - axis.renderUnsquish(); - - - // Left side must be align: right and right side must - // have align: left for labels - axis.reserveSpaceDefault = ( - side === 0 || - side === 2 || - { 1: 'left', 3: 'right' }[side] === axis.labelAlign - ); - if (pick( - labelOptions.reserveSpace, - axis.labelAlign === 'center' ? true : null, - axis.reserveSpaceDefault) - ) { - each(tickPositions, function (pos) { - // get the highest offset - labelOffset = Math.max( - ticks[pos].getLabelSize(), - labelOffset - ); - }); - } - - if (axis.staggerLines) { - labelOffset *= axis.staggerLines; - } - axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1); - - } else { // doesn't have data - objectEach(ticks, function (tick, n) { - tick.destroy(); - delete ticks[n]; - }); - } - - if ( - axisTitleOptions && - axisTitleOptions.text && - axisTitleOptions.enabled !== false - ) { - axis.addTitle(showAxis); - - if (showAxis && axisTitleOptions.reserveSpace !== false) { - axis.titleOffset = titleOffset = - axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; - titleOffsetOption = axisTitleOptions.offset; - titleMargin = defined(titleOffsetOption) ? - 0 : - pick(axisTitleOptions.margin, horiz ? 5 : 10); - } - } - - // Render the axis line - axis.renderLine(); - - // handle automatic or user set offset - axis.offset = directionFactor * pick(options.offset, axisOffset[side]); - - axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar - if (side === 0) { - lineHeightCorrection = -axis.labelMetrics().h; - } else if (side === 2) { - lineHeightCorrection = axis.tickRotCorr.y; - } else { - lineHeightCorrection = 0; - } - - // Find the padded label offset - labelOffsetPadded = Math.abs(labelOffset) + titleMargin; - if (labelOffset) { - labelOffsetPadded -= lineHeightCorrection; - labelOffsetPadded += directionFactor * ( - horiz ? - pick( - labelOptions.y, - axis.tickRotCorr.y + directionFactor * 8 - ) : - labelOptions.x - ); - } - - axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); - - axisOffset[side] = Math.max( - axisOffset[side], - axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, - labelOffsetPadded, // #3027 - hasData && tickPositions.length && tickSize ? - tickSize[0] + directionFactor * axis.offset : - 0 // #4866 - ); - - // Decide the clipping needed to keep the graph inside - // the plot area and axis lines - clip = options.offset ? - 0 : - Math.floor(axis.axisLine.strokeWidth() / 2) * 2; // #4308, #4371 - clipOffset[invertedSide] = Math.max(clipOffset[invertedSide], clip); - }, - - /** - * Internal function to get the path for the axis line. Extended for polar - * charts. - * - * @param {Number} lineWidth - * The line width in pixels. - * @return {Array} - * The SVG path definition in array form. - */ - getLinePath: function (lineWidth) { - var chart = this.chart, - opposite = this.opposite, - offset = this.offset, - horiz = this.horiz, - lineLeft = this.left + (opposite ? this.width : 0) + offset, - lineTop = chart.chartHeight - this.bottom - - (opposite ? this.height : 0) + offset; - - if (opposite) { - lineWidth *= -1; // crispify the other way - #1480, #1687 - } - - return chart.renderer - .crispLine([ - 'M', - horiz ? - this.left : - lineLeft, - horiz ? - lineTop : - this.top, - 'L', - horiz ? - chart.chartWidth - this.right : - lineLeft, - horiz ? - lineTop : - chart.chartHeight - this.bottom - ], lineWidth); - }, - - /** - * Render the axis line. Called internally when rendering and redrawing the - * axis. - */ - renderLine: function () { - if (!this.axisLine) { - this.axisLine = this.chart.renderer.path() - .addClass('highcharts-axis-line') - .add(this.axisGroup); - - /*= if (build.classic) { =*/ - this.axisLine.attr({ - stroke: this.options.lineColor, - 'stroke-width': this.options.lineWidth, - zIndex: 7 - }); - /*= } =*/ - } - }, - - /** - * Position the axis title. - * - * @private - * - * @return {Object} - * X and Y positions for the title. - */ - getTitlePosition: function () { - // compute anchor points for each of the title align options - var horiz = this.horiz, - axisLeft = this.left, - axisTop = this.top, - axisLength = this.len, - axisTitleOptions = this.options.title, - margin = horiz ? axisLeft : axisTop, - opposite = this.opposite, - offset = this.offset, - xOption = axisTitleOptions.x || 0, - yOption = axisTitleOptions.y || 0, - axisTitle = this.axisTitle, - fontMetrics = this.chart.renderer.fontMetrics( - axisTitleOptions.style && axisTitleOptions.style.fontSize, - axisTitle - ), - // The part of a multiline text that is below the baseline of the - // first line. Subtract 1 to preserve pixel-perfectness from the - // old behaviour (v5.0.12), where only one line was allowed. - textHeightOvershoot = Math.max( - axisTitle.getBBox(null, 0).height - fontMetrics.h - 1, - 0 - ), - - // the position in the length direction of the axis - alongAxis = { - low: margin + (horiz ? 0 : axisLength), - middle: margin + axisLength / 2, - high: margin + (horiz ? axisLength : 0) - }[axisTitleOptions.align], - - // the position in the perpendicular direction of the axis - offAxis = (horiz ? axisTop + this.height : axisLeft) + - (horiz ? 1 : -1) * // horizontal axis reverses the margin - (opposite ? -1 : 1) * // so does opposite axes - this.axisTitleMargin + - [ - -textHeightOvershoot, // top - textHeightOvershoot, // right - fontMetrics.f, // bottom - -textHeightOvershoot // left - ][this.side]; - - - return { - x: horiz ? - alongAxis + xOption : - offAxis + (opposite ? this.width : 0) + offset + xOption, - y: horiz ? - offAxis + yOption - (opposite ? this.height : 0) + offset : - alongAxis + yOption - }; - }, - - /** - * Render a minor tick into the given position. If a minor tick already - * exists in this position, move it. - * - * @param {number} pos - * The position in axis values. - */ - renderMinorTick: function (pos) { - var slideInTicks = this.chart.hasRendered && isNumber(this.oldMin), - minorTicks = this.minorTicks; - - if (!minorTicks[pos]) { - minorTicks[pos] = new Tick(this, pos, 'minor'); - } - - // Render new ticks in old position - if (slideInTicks && minorTicks[pos].isNew) { - minorTicks[pos].render(null, true); - } - - minorTicks[pos].render(null, false, 1); - }, - - /** - * Render a major tick into the given position. If a tick already exists - * in this position, move it. - * - * @param {number} pos - * The position in axis values. - * @param {number} i - * The tick index. - */ - renderTick: function (pos, i) { - var isLinked = this.isLinked, - ticks = this.ticks, - slideInTicks = this.chart.hasRendered && isNumber(this.oldMin); - - // Linked axes need an extra check to find out if - if (!isLinked || (pos >= this.min && pos <= this.max)) { - - if (!ticks[pos]) { - ticks[pos] = new Tick(this, pos); - } - - // render new ticks in old position - if (slideInTicks && ticks[pos].isNew) { - ticks[pos].render(i, true, 0.1); - } - - ticks[pos].render(i); - } - }, - - /** - * Render the axis. - * - * @private - */ - render: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - isLog = axis.isLog, - lin2log = axis.lin2log, - isLinked = axis.isLinked, - tickPositions = axis.tickPositions, - axisTitle = axis.axisTitle, - ticks = axis.ticks, - minorTicks = axis.minorTicks, - alternateBands = axis.alternateBands, - stackLabelOptions = options.stackLabels, - alternateGridColor = options.alternateGridColor, - tickmarkOffset = axis.tickmarkOffset, - axisLine = axis.axisLine, - showAxis = axis.showAxis, - animation = animObject(renderer.globalAnimation), - from, - to; - - // Reset - axis.labelEdge.length = 0; - axis.overlap = false; - - // Mark all elements inActive before we go over and mark the active ones - each([ticks, minorTicks, alternateBands], function (coll) { - objectEach(coll, function (tick) { - tick.isActive = false; - }); - }); - - // If the series has data draw the ticks. Else only the line and title - if (axis.hasData() || isLinked) { - - // minor ticks - if (axis.minorTickInterval && !axis.categories) { - each(axis.getMinorTickPositions(), function (pos) { - axis.renderMinorTick(pos); - }); - } - - // Major ticks. Pull out the first item and render it last so that - // we can get the position of the neighbour label. #808. - if (tickPositions.length) { // #1300 - each(tickPositions, function (pos, i) { - axis.renderTick(pos, i); - }); - // In a categorized axis, the tick marks are displayed - // between labels. So we need to add a tick mark and - // grid line at the left edge of the X axis. - if (tickmarkOffset && (axis.min === 0 || axis.single)) { - if (!ticks[-1]) { - ticks[-1] = new Tick(axis, -1, null, true); - } - ticks[-1].render(-1); - } - - } - - // alternate grid color - if (alternateGridColor) { - each(tickPositions, function (pos, i) { - to = tickPositions[i + 1] !== undefined ? - tickPositions[i + 1] + tickmarkOffset : - axis.max - tickmarkOffset; - - if ( - i % 2 === 0 && - pos < axis.max && - to <= axis.max + ( - chart.polar ? - -tickmarkOffset : - tickmarkOffset - ) - ) { // #2248, #4660 - if (!alternateBands[pos]) { - alternateBands[pos] = new H.PlotLineOrBand(axis); - } - from = pos + tickmarkOffset; // #949 - alternateBands[pos].options = { - from: isLog ? lin2log(from) : from, - to: isLog ? lin2log(to) : to, - color: alternateGridColor - }; - alternateBands[pos].render(); - alternateBands[pos].isActive = true; - } - }); - } - - // custom plot lines and bands - if (!axis._addedPlotLB) { // only first time - each( - (options.plotLines || []).concat(options.plotBands || []), - function (plotLineOptions) { - axis.addPlotBandOrLine(plotLineOptions); - } - ); - axis._addedPlotLB = true; - } - - } // end if hasData - - // Remove inactive ticks - each([ticks, minorTicks, alternateBands], function (coll) { - var i, - forDestruction = [], - delay = animation.duration, - destroyInactiveItems = function () { - i = forDestruction.length; - while (i--) { - // When resizing rapidly, the same items - // may be destroyed in different timeouts, - // or the may be reactivated - if ( - coll[forDestruction[i]] && - !coll[forDestruction[i]].isActive - ) { - coll[forDestruction[i]].destroy(); - delete coll[forDestruction[i]]; - } - } - - }; - - objectEach(coll, function (tick, pos) { - if (!tick.isActive) { - // Render to zero opacity - tick.render(pos, false, 0); - tick.isActive = false; - forDestruction.push(pos); - } - }); - - // When the objects are finished fading out, destroy them - syncTimeout( - destroyInactiveItems, - coll === alternateBands || - !chart.hasRendered || - !delay ? - 0 : - delay - ); - }); - - // Set the axis line path - if (axisLine) { - axisLine[axisLine.isPlaced ? 'animate' : 'attr']({ - d: this.getLinePath(axisLine.strokeWidth()) - }); - axisLine.isPlaced = true; - - // Show or hide the line depending on options.showEmpty - axisLine[showAxis ? 'show' : 'hide'](true); - } - - if (axisTitle && showAxis) { - var titleXy = axis.getTitlePosition(); - if (isNumber(titleXy.y)) { - axisTitle[axisTitle.isNew ? 'attr' : 'animate'](titleXy); - axisTitle.isNew = false; - } else { - axisTitle.attr('y', -9999); - axisTitle.isNew = true; - } - } - - // Stacked totals: - if (stackLabelOptions && stackLabelOptions.enabled) { - axis.renderStackTotals(); - } - // End stacked totals - - axis.isDirty = false; - - fireEvent(this, 'afterRender'); - }, - - /** - * Redraw the axis to reflect changes in the data or axis extremes. Called - * internally from {@link Chart#redraw}. - * - * @private - */ - redraw: function () { - - if (this.visible) { - // render the axis - this.render(); - - // move plot lines and bands - each(this.plotLinesAndBands, function (plotLine) { - plotLine.render(); - }); - } - - // mark associated series as dirty and ready for redraw - each(this.series, function (series) { - series.isDirty = true; - }); - - }, - - // Properties to survive after destroy, needed for Axis.update (#4317, - // #5773, #5881). - keepProps: ['extKey', 'hcEvents', 'names', 'series', 'userMax', 'userMin'], - - /** - * Destroys an Axis instance. See {@link Axis#remove} for the API endpoint - * to fully remove the axis. - * - * @private - * @param {Boolean} keepEvents - * Whether to preserve events, used internally in Axis.update. - */ - destroy: function (keepEvents) { - var axis = this, - stacks = axis.stacks, - plotLinesAndBands = axis.plotLinesAndBands, - plotGroup, - i; - - fireEvent(this, 'destroy', { keepEvents: keepEvents }); - - // Remove the events - if (!keepEvents) { - removeEvent(axis); - } - - // Destroy each stack total - objectEach(stacks, function (stack, stackKey) { - destroyObjectProperties(stack); - - stacks[stackKey] = null; - }); - - // Destroy collections - each( - [axis.ticks, axis.minorTicks, axis.alternateBands], - function (coll) { - destroyObjectProperties(coll); - } - ); - if (plotLinesAndBands) { - i = plotLinesAndBands.length; - while (i--) { // #1975 - plotLinesAndBands[i].destroy(); - } - } - - // Destroy local variables - each( - ['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', - 'gridGroup', 'labelGroup', 'cross'], - function (prop) { - if (axis[prop]) { - axis[prop] = axis[prop].destroy(); - } - } - ); - - // Destroy each generated group for plotlines and plotbands - for (plotGroup in axis.plotLinesAndBandsGroups) { - axis.plotLinesAndBandsGroups[plotGroup] = - axis.plotLinesAndBandsGroups[plotGroup].destroy(); - } - - // Delete all properties and fall back to the prototype. - objectEach(axis, function (val, key) { - if (inArray(key, axis.keepProps) === -1) { - delete axis[key]; - } - }); - }, - - /** - * Internal function to draw a crosshair. - * - * @param {PointerEvent} [e] - * The event arguments from the modified pointer event, extended - * with `chartX` and `chartY` - * @param {Point} [point] - * The Point object if the crosshair snaps to points. - */ - drawCrosshair: function (e, point) { - - var path, - options = this.crosshair, - snap = pick(options.snap, true), - pos, - categorized, - graphic = this.cross; - - fireEvent(this, 'drawCrosshair', { e: e, point: point }); - - // Use last available event when updating non-snapped crosshairs without - // mouse interaction (#5287) - if (!e) { - e = this.cross && this.cross.e; - } - - if ( - // Disabled in options - !this.crosshair || - // Snap - ((defined(point) || !snap) === false) - ) { - this.hideCrosshair(); - } else { - - // Get the path - if (!snap) { - pos = e && - ( - this.horiz ? - e.chartX - this.pos : - this.len - e.chartY + this.pos - ); - } else if (defined(point)) { - // #3834 - pos = pick( - point.crosshairPos, // 3D axis extension - this.isXAxis ? point.plotX : this.len - point.plotY - ); - } - - if (defined(pos)) { - path = this.getPlotLinePath( - // First argument, value, only used on radial - point && (this.isXAxis ? - point.x : - pick(point.stackY, point.y) - ), - null, - null, - null, - pos // Translated position - ) || null; // #3189 - } - - if (!defined(path)) { - this.hideCrosshair(); - return; - } - - categorized = this.categories && !this.isRadial; - - // Draw the cross - if (!graphic) { - this.cross = graphic = this.chart.renderer - .path() - .addClass( - 'highcharts-crosshair highcharts-crosshair-' + - (categorized ? 'category ' : 'thin ') + - options.className - ) - .attr({ - zIndex: pick(options.zIndex, 2) - }) - .add(); - - /*= if (build.classic) { =*/ - // Presentational attributes - graphic.attr({ - 'stroke': options.color || - ( - categorized ? - color('${palette.highlightColor20}') - .setOpacity(0.25).get() : - '${palette.neutralColor20}' - ), - 'stroke-width': pick(options.width, 1) - }).css({ - 'pointer-events': 'none' - }); - if (options.dashStyle) { - graphic.attr({ - dashstyle: options.dashStyle - }); - } - /*= } =*/ - - } - - graphic.show().attr({ - d: path - }); - - if (categorized && !options.width) { - graphic.attr({ - 'stroke-width': this.transA - }); - } - this.cross.e = e; - } - - fireEvent(this, 'afterDrawCrosshair', { e: e, point: point }); - }, - - /** - * Hide the crosshair if visible. - */ - hideCrosshair: function () { - if (this.cross) { - this.cross.hide(); - } - } + /** + * The X axis or category axis. Normally this is the horizontal axis, + * though if the chart is inverted this is the vertical axis. In case of + * multiple axes, the xAxis node is an array of configuration objects. + * + * See [the Axis object](#Axis) for programmatic access to the axis. + * + * @productdesc {highmaps} + * In Highmaps, the axis is hidden, but it is used behind the scenes to + * control features like zooming and panning. Zooming is in effect the same + * as setting the extremes of one of the exes. + * + * @optionparent xAxis + */ + defaultOptions: { + /** + * Whether to allow decimals in this axis' ticks. When counting + * integers, like persons or hits on a web page, decimals should + * be avoided in the labels. + * + * @type {Boolean} + * @see [minTickInterval](#xAxis.minTickInterval) + * @sample {highcharts|highstock} + * highcharts/yaxis/allowdecimals-true/ + * True by default + * @sample {highcharts|highstock} + * highcharts/yaxis/allowdecimals-false/ + * False + * @default true + * @since 2.0 + * @apioption xAxis.allowDecimals + */ + // allowDecimals: null, + + + /** + * When using an alternate grid color, a band is painted across the + * plot area between every other grid line. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/alternategridcolor/ + * Alternate grid color on the Y axis + * @sample {highstock} stock/xaxis/alternategridcolor/ + * Alternate grid color on the Y axis + * @default null + * @apioption xAxis.alternateGridColor + */ + // alternateGridColor: null, + + /** + * An array defining breaks in the axis, the sections defined will be + * left out and all the points shifted closer to each other. + * + * @productdesc {highcharts} + * Requires that the broken-axis.js module is loaded. + * + * @type {Array} + * @sample {highcharts} + * highcharts/axisbreak/break-simple/ + * Simple break + * @sample {highcharts|highstock} + * highcharts/axisbreak/break-visualized/ + * Advanced with callback + * @sample {highstock} + * stock/demo/intraday-breaks/ + * Break on nights and weekends + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks + */ + + /** + * A number indicating how much space should be left between the start + * and the end of the break. The break size is given in axis units, + * so for instance on a `datetime` axis, a break size of 3600000 would + * indicate the equivalent of an hour. + * + * @type {Number} + * @default 0 + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.breakSize + */ + + /** + * The point where the break starts. + * + * @type {Number} + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.from + */ + + /** + * Defines an interval after which the break appears again. By default + * the breaks do not repeat. + * + * @type {Number} + * @default 0 + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.repeat + */ + + /** + * The point where the break ends. + * + * @type {Number} + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.to + */ + + /** + * If categories are present for the xAxis, names are used instead of + * numbers for that axis. Since Highcharts 3.0, categories can also + * be extracted by giving each point a [name](#series.data) and setting + * axis [type](#xAxis.type) to `category`. However, if you have multiple + * series, best practice remains defining the `categories` array. + * + * Example: + * + *
categories: ['Apples', 'Bananas', 'Oranges']
+ * + * @type {Array} + * @sample {highcharts} highcharts/chart/reflow-true/ + * With + * @sample {highcharts} highcharts/xaxis/categories/ + * Without + * @product highcharts + * @default null + * @apioption xAxis.categories + */ + // categories: [], + + /** + * The highest allowed value for automatically computed axis extremes. + * + * @type {Number} + * @see [floor](#xAxis.floor) + * @sample {highcharts|highstock} highcharts/yaxis/floor-ceiling/ + * Floor and ceiling + * @since 4.0 + * @product highcharts highstock + * @apioption xAxis.ceiling + */ + + /** + * A class name that opens for styling the axis by CSS, especially in + * Highcharts styled mode. The class name is applied to group elements + * for the grid, axis elements and labels. + * + * @type {String} + * @sample {highcharts|highstock|highmaps} + * highcharts/css/axis/ + * Multiple axes with separate styling + * @since 5.0.0 + * @apioption xAxis.className + */ + + /** + * Configure a crosshair that follows either the mouse pointer or the + * hovered point. + * + * In styled mode, the crosshairs are styled in the + * `.highcharts-crosshair`, `.highcharts-crosshair-thin` or + * `.highcharts-xaxis-category` classes. + * + * @productdesc {highstock} + * In Highstock, bu default, the crosshair is enabled on the X axis and + * disabled on the Y axis. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/xaxis/crosshair-both/ + * Crosshair on both axes + * @sample {highstock} stock/xaxis/crosshairs-xy/ + * Crosshair on both axes + * @sample {highmaps} highcharts/xaxis/crosshair-both/ + * Crosshair on both axes + * @default false + * @since 4.1 + * @apioption xAxis.crosshair + */ + + /** + * A class name for the crosshair, especially as a hook for styling. + * + * @type {String} + * @since 5.0.0 + * @apioption xAxis.crosshair.className + */ + + /** + * The color of the crosshair. Defaults to `#cccccc` for numeric and + * datetime axes, and `rgba(204,214,235,0.25)` for category axes, where + * the crosshair by default highlights the whole category. + * + * @type {Color} + * @sample {highcharts|highstock|highmaps} + * highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @default #cccccc + * @since 4.1 + * @apioption xAxis.crosshair.color + */ + + /** + * The dash style for the crosshair. See + * [series.dashStyle](#plotOptions.series.dashStyle) + * for possible values. + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", + * "DashDot", "LongDashDot", "LongDashDotDot"] + * @type {String} + * @sample {highcharts|highmaps} highcharts/xaxis/crosshair-dotted/ + * Dotted crosshair + * @sample {highstock} stock/xaxis/crosshair-dashed/ + * Dashed X axis crosshair + * @default Solid + * @since 4.1 + * @apioption xAxis.crosshair.dashStyle + */ + + /** + * Whether the crosshair should snap to the point or follow the pointer + * independent of points. + * + * @type {Boolean} + * @sample {highcharts|highstock} + * highcharts/xaxis/crosshair-snap-false/ + * True by default + * @sample {highmaps} + * maps/demo/latlon-advanced/ + * Snap is false + * @default true + * @since 4.1 + * @apioption xAxis.crosshair.snap + */ + + /** + * The pixel width of the crosshair. Defaults to 1 for numeric or + * datetime axes, and for one category width for category axes. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @sample {highstock} highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @sample {highmaps} highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @default 1 + * @since 4.1 + * @apioption xAxis.crosshair.width + */ + + /** + * The Z index of the crosshair. Higher Z indices allow drawing the + * crosshair on top of the series or behind the grid lines. + * + * @type {Number} + * @default 2 + * @since 4.1 + * @apioption xAxis.crosshair.zIndex + */ + + /** + * For a datetime axis, the scale will automatically adjust to the + * appropriate unit. This member gives the default string + * representations used for each unit. For intermediate values, + * different units may be used, for example the `day` unit can be used + * on midnight and `hour` unit be used for intermediate values on the + * same axis. For an overview of the replacement codes, see + * [dateFormat](#Highcharts.dateFormat). Defaults to: + * + *
{
+         *     millisecond: '%H:%M:%S.%L',
+         *     second: '%H:%M:%S',
+         *     minute: '%H:%M',
+         *     hour: '%H:%M',
+         *     day: '%e. %b',
+         *     week: '%e. %b',
+         *     month: '%b \'%y',
+         *     year: '%Y'
+         * }
+ * + * @type {Object} + * @sample {highcharts} highcharts/xaxis/datetimelabelformats/ + * Different day format on X axis + * @sample {highstock} stock/xaxis/datetimelabelformats/ + * More information in x axis labels + * @product highcharts highstock + */ + dateTimeLabelFormats: { + millisecond: '%H:%M:%S.%L', + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b \'%y', + year: '%Y' + }, + + /** + * _Requires Accessibility module_ + * + * Description of the axis to screen reader users. + * + * @type {String} + * @default undefined + * @since 5.0.0 + * @apioption xAxis.description + */ + + /** + * Whether to force the axis to end on a tick. Use this option with + * the `maxPadding` option to control the axis end. + * + * @productdesc {highstock} + * In Highstock, `endOnTick` is always false when the navigator is + * enabled, to prevent jumpy scrolling. + * + * @sample {highcharts} highcharts/chart/reflow-true/ + * True by default + * @sample {highcharts} highcharts/yaxis/endontick/ + * False + * @sample {highstock} stock/demo/basic-line/ + * True by default + * @sample {highstock} stock/xaxis/endontick/ + * False + * @since 1.2.0 + */ + endOnTick: false, + + /** + * Event handlers for the axis. + * + * @apioption xAxis.events + */ + + /** + * An event fired after the breaks have rendered. + * + * @type {Function} + * @see [breaks](#xAxis.breaks) + * @sample {highcharts} highcharts/axisbreak/break-event/ + * AfterBreak Event + * @since 4.1.0 + * @product highcharts + * @apioption xAxis.events.afterBreaks + */ + + /** + * As opposed to the `setExtremes` event, this event fires after the + * final min and max values are computed and corrected for `minRange`. + * + * + * Fires when the minimum and maximum is set for the axis, either by + * calling the `.setExtremes()` method or by selecting an area in the + * chart. One parameter, `event`, is passed to the function, containing + * common event information. + * + * The new user set minimum and maximum values can be found by + * `event.min` and `event.max`. These reflect the axis minimum and + * maximum in axis values. The actual data extremes are found in + * `event.dataMin` and `event.dataMax`. + * + * @type {Function} + * @context Axis + * @since 2.3 + * @apioption xAxis.events.afterSetExtremes + */ + + /** + * An event fired when a break from this axis occurs on a point. + * + * @type {Function} + * @see [breaks](#xAxis.breaks) + * @context Axis + * @sample {highcharts} highcharts/axisbreak/break-visualized/ + * Visualization of a Break + * @since 4.1.0 + * @product highcharts + * @apioption xAxis.events.pointBreak + */ + + /** + * An event fired when a point falls inside a break from this axis. + * + * @type {Function} + * @context Axis + * @product highcharts highstock + * @apioption xAxis.events.pointInBreak + */ + + /** + * Fires when the minimum and maximum is set for the axis, either by + * calling the `.setExtremes()` method or by selecting an area in the + * chart. One parameter, `event`, is passed to the function, + * containing common event information. + * + * The new user set minimum and maximum values can be found by + * `event.min` and `event.max`. These reflect the axis minimum and + * maximum in data values. When an axis is zoomed all the way out from + * the "Reset zoom" button, `event.min` and `event.max` are null, and + * the new extremes are set based on `this.dataMin` and `this.dataMax`. + * + * @type {Function} + * @context Axis + * @sample {highstock} stock/xaxis/events-setextremes/ + * Log new extremes on x axis + * @since 1.2.0 + * @apioption xAxis.events.setExtremes + */ + + /** + * The lowest allowed value for automatically computed axis extremes. + * + * @type {Number} + * @see [ceiling](#yAxis.ceiling) + * @sample {highcharts} highcharts/yaxis/floor-ceiling/ + * Floor and ceiling + * @sample {highstock} stock/demo/lazy-loading/ + * Prevent negative stock price on Y axis + * @default null + * @since 4.0 + * @product highcharts highstock + * @apioption xAxis.floor + */ + + /** + * The dash or dot style of the grid lines. For possible values, see + * [this demonstration](http://jsfiddle.net/gh/get/library/pure/ + *highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ + *series-dashstyle-all/). + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", + * "DashDot", "LongDashDot", "LongDashDotDot"] + * @type {String} + * @sample {highcharts} highcharts/yaxis/gridlinedashstyle/ + * Long dashes + * @sample {highstock} stock/xaxis/gridlinedashstyle/ + * Long dashes + * @default Solid + * @since 1.2 + * @apioption xAxis.gridLineDashStyle + */ + + /** + * The Z index of the grid lines. + * + * @type {Number} + * @sample {highcharts|highstock} highcharts/xaxis/gridzindex/ + * A Z index of 4 renders the grid above the graph + * @default 1 + * @product highcharts highstock + * @apioption xAxis.gridZIndex + */ + + /** + * An id for the axis. This can be used after render time to get + * a pointer to the axis object through `chart.get()`. + * + * @type {String} + * @sample {highcharts} highcharts/xaxis/id/ + * Get the object + * @sample {highstock} stock/xaxis/id/ + * Get the object + * @default null + * @since 1.2.0 + * @apioption xAxis.id + */ + + /** + * The axis labels show the number or category for each tick. + * + * @productdesc {highmaps} + * X and Y axis labels are by default disabled in Highmaps, but the + * functionality is inherited from Highcharts and used on `colorAxis`, + * and can be enabled on X and Y axes too. + */ + labels: { + /** + * What part of the string the given position is anchored to. + * If `left`, the left side of the string is at the axis position. + * Can be one of `"left"`, `"center"` or `"right"`. Defaults to + * an intelligent guess based on which side of the chart the axis + * is on and the rotation of the label. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/xaxis/labels-align-left/ + * Left + * @sample {highcharts} highcharts/xaxis/labels-align-right/ + * Right + * @sample {highcharts} + * highcharts/xaxis/labels-reservespace-true/ + * Left-aligned labels on a vertical category axis + * @see [reserveSpace](#xAxis.labels.reserveSpace) + * @apioption xAxis.labels.align + */ + // align: 'center', + + /** + * For horizontal axes, the allowed degrees of label rotation + * to prevent overlapping labels. If there is enough space, + * labels are not rotated. As the chart gets narrower, it + * will start rotating the labels -45 degrees, then remove + * every second label and try again with rotations 0 and -45 etc. + * Set it to `false` to disable rotation, which will + * cause the labels to word-wrap if possible. + * + * @type {Array} + * @sample {highcharts|highstock} + * highcharts/xaxis/labels-autorotation-default/ + * Default auto rotation of 0 or -45 + * @sample {highcharts|highstock} + * highcharts/xaxis/labels-autorotation-0-90/ + * Custom graded auto rotation + * @default [-45] + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.labels.autoRotation + */ + + /** + * When each category width is more than this many pixels, we don't + * apply auto rotation. Instead, we lay out the axis label with word + * wrap. A lower limit makes sense when the label contains multiple + * short words that don't extend the available horizontal space for + * each label. + * + * @type {Number} + * @sample {highcharts} + * highcharts/xaxis/labels-autorotationlimit/ + * Lower limit + * @default 80 + * @since 4.1.5 + * @product highcharts + * @apioption xAxis.labels.autoRotationLimit + */ + + /** + * Polar charts only. The label's pixel distance from the perimeter + * of the plot area. + * + * @type {Number} + * @default 15 + * @product highcharts + * @apioption xAxis.labels.distance + */ + + /** + * Enable or disable the axis labels. + * + * @sample {highcharts} highcharts/xaxis/labels-enabled/ + * X axis labels disabled + * @sample {highstock} stock/xaxis/labels-enabled/ + * X axis labels disabled + * @default {highcharts|highstock} true + * @default {highmaps} false + */ + enabled: true, + + /** + * A [format string](http://www.highcharts.com/docs/chart- + * concepts/labels-and-string-formatting) for the axis label. + * + * @type {String} + * @sample {highcharts|highstock} highcharts/yaxis/labels-format/ + * Add units to Y axis label + * @default {value} + * @since 3.0 + * @apioption xAxis.labels.format + */ + + /** + * Callback JavaScript function to format the label. The value + * is given by `this.value`. Additional properties for `this` are + * `axis`, `chart`, `isFirst` and `isLast`. The value of the default + * label formatter can be retrieved by calling + * `this.axis.defaultLabelFormatter.call(this)` within the function. + * + * Defaults to: + * + *
function() {
+             *     return this.value;
+             * }
+ * + * @type {Function} + * @sample {highcharts} + * highcharts/xaxis/labels-formatter-linked/ + * Linked category names + * @sample {highcharts} + * highcharts/xaxis/labels-formatter-extended/ + * Modified numeric labels + * @sample {highstock} + * stock/xaxis/labels-formatter/ + * Added units on Y axis + * @apioption xAxis.labels.formatter + */ + + /** + * How to handle overflowing labels on horizontal axis. Can be + * undefined, `false` or `"justify"`. By default it aligns inside + * the chart area. If "justify", labels will not render outside + * the plot area. If `false`, it will not be aligned at all. + * If there is room to move it, it will be aligned to the edge, + * else it will be removed. + * + * @deprecated + * @validvalue [null, "justify"] + * @type {String} + * @since 2.2.5 + * @apioption xAxis.labels.overflow + */ + + /** + * The pixel padding for axis labels, to ensure white space between + * them. + * + * @type {Number} + * @default 5 + * @product highcharts + * @apioption xAxis.labels.padding + */ + + /** + * Whether to reserve space for the labels. By default, space is + * reserved for the labels in these cases: + * + * * On all horizontal axes. + * * On vertical axes if `label.align` is `right` on a left-side + * axis or `left` on a right-side axis. + * * On vertical axes if `label.align` is `center`. + * + * This can be turned off when for example the labels are rendered + * inside the plot area instead of outside. + * + * @type {Boolean} + * @sample {highcharts} highcharts/xaxis/labels-reservespace/ + * No reserved space, labels inside plot + * @sample {highcharts} + * highcharts/xaxis/labels-reservespace-true/ + * Left-aligned labels on a vertical category axis + * @see [labels.align](#xAxis.labels.align) + * @default null + * @since 4.1.10 + * @product highcharts + * @apioption xAxis.labels.reserveSpace + */ + + /** + * Rotation of the labels in degrees. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-rotation/ + * X axis labels rotated 90° + * @default 0 + * @apioption xAxis.labels.rotation + */ + + /** + * Horizontal axes only. The number of lines to spread the labels + * over to make room or tighter labels. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-staggerlines/ + * Show labels over two lines + * @sample {highstock} stock/xaxis/labels-staggerlines/ + * Show labels over two lines + * @default null + * @since 2.1 + * @apioption xAxis.labels.staggerLines + */ + + /** + * To show only every _n_'th label on the axis, set the step to _n_. + * Setting the step to 2 shows every other label. + * + * By default, the step is calculated automatically to avoid + * overlap. To prevent this, set it to 1\. This usually only + * happens on a category axis, and is often a sign that you have + * chosen the wrong axis type. + * + * Read more at + * [Axis docs](http://www.highcharts.com/docs/chart-concepts/axes) + * => What axis should I use? + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-step/ + * Showing only every other axis label on a categorized + * x axis + * @sample {highcharts} highcharts/xaxis/labels-step-auto/ + * Auto steps on a category axis + * @default null + * @since 2.1 + * @apioption xAxis.labels.step + */ + + + /** + * The y position offset of the label relative to the tick position + * on the axis. The default makes it adapt to the font size on + * bottom axis. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-x/ + * Y axis labels placed on grid lines + * @default null + * @apioption xAxis.labels.y + */ + + /** + * The Z index for the axis labels. + * + * @type {Number} + * @default 7 + * @apioption xAxis.labels.zIndex + */ + + /*= if (build.classic) { =*/ + + /** + * CSS styles for the label. Use `whiteSpace: 'nowrap'` to prevent + * wrapping of category labels. Use `textOverflow: 'none'` to + * prevent ellipsis (dots). + * + * In styled mode, the labels are styled with the + * `.highcharts-axis-labels` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/xaxis/labels-style/ + * Red X axis labels + */ + style: { + color: '${palette.neutralColor60}', + cursor: 'default', + fontSize: '11px' + }, + /*= } =*/ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart- + * concepts/labels-and-string-formatting#html) to render the labels. + * + * @type {Boolean} + * @default false + * @apioption xAxis.labels.useHTML + */ + + /** + * The x position offset of the label relative to the tick position + * on the axis. + * + * @sample {highcharts} highcharts/xaxis/labels-x/ + * Y axis labels placed on grid lines + */ + x: 0 + }, + + /** + * Index of another axis that this axis is linked to. When an axis is + * linked to a master axis, it will take the same extremes as + * the master, but as assigned by min or max or by setExtremes. + * It can be used to show additional info, or to ease reading the + * chart by duplicating the scales. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/linkedto/ + * Different string formats of the same date + * @sample {highcharts} highcharts/yaxis/linkedto/ + * Y values on both sides + * @default null + * @since 2.0.2 + * @product highcharts highstock + * @apioption xAxis.linkedTo + */ + + /** + * The maximum value of the axis. If `null`, the max value is + * automatically calculated. + * + * If the `endOnTick` option is true, the `max` value might + * be rounded up. + * + * If a [tickAmount](#yAxis.tickAmount) is set, the axis may be extended + * beyond the set max in order to reach the given number of ticks. The + * same may happen in a chart with multiple axes, determined by [chart. + * alignTicks](#chart), where a `tickAmount` is applied internally. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/max-200/ + * Y axis max of 200 + * @sample {highcharts} highcharts/yaxis/max-logarithmic/ + * Y axis max on logarithmic axis + * @sample {highstock} stock/xaxis/min-max/ + * Fixed min and max on X axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption xAxis.max + */ + + /** + * When using multiple axis, the ticks of two or more opposite axes + * will automatically be aligned by adding ticks to the axis or axes + * with the least ticks, as if `tickAmount` were specified. + * + * This can be prevented by setting `alignTicks` to false. If the grid + * lines look messy, it's a good idea to hide them for the secondary + * axis by setting `gridLineWidth` to 0. + * + * If `startOnTick` or `endOnTick` in an Axis options are set to false, + * then the `alignTicks ` will be disabled for the Axis. + * + * Disabled for logarithmic axes. + * + * @type {Boolean} + * @default true + * @product highcharts highstock + * @apioption xAxis.alignTicks + */ + + /** + * Padding of the max value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the highest data value to appear on the edge + * of the plot area. When the axis' `max` option is set or a max extreme + * is set using `axis.setExtremes()`, the maxPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/maxpadding/ + * Max padding of 0.25 on y axis + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ + * Add some padding + * @default {highcharts} 0.01 + * @default {highstock|highmaps} 0 + * @since 1.2.0 + */ + maxPadding: 0.01, + + /** + * Deprecated. Use `minRange` instead. + * + * @deprecated + * @type {Number} + * @product highcharts highstock + * @apioption xAxis.maxZoom + */ + + /** + * The minimum value of the axis. If `null` the min value is + * automatically calculated. + * + * If the `startOnTick` option is true (default), the `min` value might + * be rounded down. + * + * The automatically calculated minimum value is also affected by + * [floor](#yAxis.floor), [softMin](#yAxis.softMin), + * [minPadding](#yAxis.minPadding), [minRange](#yAxis.minRange) + * as well as [series.threshold](#plotOptions.series.threshold) + * and [series.softThreshold](#plotOptions.series.softThreshold). + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/min-startontick-false/ + * -50 with startOnTick to false + * @sample {highcharts} highcharts/yaxis/min-startontick-true/ + * -50 with startOnTick true by default + * @sample {highstock} stock/xaxis/min-max/ + * Set min and max on X axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption xAxis.min + */ + + /** + * The dash or dot style of the minor grid lines. For possible values, + * see [this demonstration](http://jsfiddle.net/gh/get/library/pure/ + * highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ + * series-dashstyle-all/). + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", + * "DashDot", "LongDashDot", "LongDashDotDot"] + * @type {String} + * @sample {highcharts} highcharts/yaxis/minorgridlinedashstyle/ + * Long dashes on minor grid lines + * @sample {highstock} stock/xaxis/minorgridlinedashstyle/ + * Long dashes on minor grid lines + * @default Solid + * @since 1.2 + * @apioption xAxis.minorGridLineDashStyle + */ + + /** + * Specific tick interval in axis units for the minor ticks. + * On a linear axis, if `"auto"`, the minor tick interval is + * calculated as a fifth of the tickInterval. If `null`, minor + * ticks are not shown. + * + * On logarithmic axes, the unit is the power of the value. For example, + * setting the minorTickInterval to 1 puts one tick on each of 0.1, + * 1, 10, 100 etc. Setting the minorTickInterval to 0.1 produces 9 + * ticks between 1 and 10, 10 and 100 etc. + * + * If user settings dictate minor ticks to become too dense, they don't + * make sense, and will be ignored to prevent performance problems. + * + * @type {Number|String} + * @sample {highcharts} highcharts/yaxis/minortickinterval-null/ + * Null by default + * @sample {highcharts} highcharts/yaxis/minortickinterval-5/ + * 5 units + * @sample {highcharts} highcharts/yaxis/minortickinterval-log-auto/ + * "auto" + * @sample {highcharts} highcharts/yaxis/minortickinterval-log/ + * 0.1 + * @sample {highstock} stock/demo/basic-line/ + * Null by default + * @sample {highstock} stock/xaxis/minortickinterval-auto/ + * "auto" + * @apioption xAxis.minorTickInterval + */ + + /** + * The pixel length of the minor tick marks. + * + * @sample {highcharts} highcharts/yaxis/minorticklength/ + * 10px on Y axis + * @sample {highstock} stock/xaxis/minorticks/ + * 10px on Y axis + */ + minorTickLength: 2, + + /** + * The position of the minor tick marks relative to the axis line. + * Can be one of `inside` and `outside`. + * + * @validvalue ["inside", "outside"] + * @sample {highcharts} highcharts/yaxis/minortickposition-outside/ + * Outside by default + * @sample {highcharts} highcharts/yaxis/minortickposition-inside/ + * Inside + * @sample {highstock} stock/xaxis/minorticks/ + * Inside + */ + minorTickPosition: 'outside', + + /** + * Enable or disable minor ticks. Unless + * [minorTickInterval](#xAxis.minorTickInterval) is set, the tick + * interval is calculated as a fifth of the `tickInterval`. + * + * On a logarithmic axis, minor ticks are laid out based on a best + * guess, attempting to enter approximately 5 minor ticks between + * each major tick. + * + * Prior to v6.0.0, ticks were unabled in auto layout by setting + * `minorTickInterval` to `"auto"`. + * + * @productdesc {highcharts} + * On axes using [categories](#xAxis.categories), minor ticks are not + * supported. + * + * @type {Boolean} + * @default false + * @since 6.0.0 + * @sample {highcharts} highcharts/yaxis/minorticks-true/ + * Enabled on linear Y axis + * @apioption xAxis.minorTicks + */ + + /** + * The pixel width of the minor tick mark. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/minortickwidth/ + * 3px width + * @sample {highstock} stock/xaxis/minorticks/ + * 1px width + * @default 0 + * @apioption xAxis.minorTickWidth + */ + + /** + * Padding of the min value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the lowest data value to appear on the edge + * of the plot area. When the axis' `min` option is set or a min extreme + * is set using `axis.setExtremes()`, the minPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/minpadding/ + * Min padding of 0.2 + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ + * Add some padding + * @default {highcharts} 0.01 + * @default {highstock|highmaps} 0 + * @since 1.2.0 + */ + minPadding: 0.01, + + /** + * The minimum range to display on this axis. The entire axis will not + * be allowed to span over a smaller interval than this. For example, + * for a datetime axis the main unit is milliseconds. If minRange is + * set to 3600000, you can't zoom in more than to one hour. + * + * The default minRange for the x axis is five times the smallest + * interval between any of the data points. + * + * On a logarithmic axis, the unit for the minimum range is the power. + * So a minRange of 1 means that the axis can be zoomed to 10-100, + * 100-1000, 1000-10000 etc. + * + * Note that the `minPadding`, `maxPadding`, `startOnTick` and + * `endOnTick` settings also affect how the extremes of the axis + * are computed. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/minrange/ + * Minimum range of 5 + * @sample {highstock} stock/xaxis/minrange/ + * Max zoom of 6 months overrides user selections + * @sample {highmaps} maps/axis/minrange/ + * Minimum range of 1000 + * @apioption xAxis.minRange + */ + + /** + * The minimum tick interval allowed in axis values. For example on + * zooming in on an axis with daily data, this can be used to prevent + * the axis from showing hours. Defaults to the closest distance between + * two points on the axis. + * + * @type {Number} + * @since 2.3.0 + * @apioption xAxis.minTickInterval + */ + + /** + * The distance in pixels from the plot area to the axis line. + * A positive offset moves the axis with it's line, labels and ticks + * away from the plot area. This is typically used when two or more + * axes are displayed on the same side of the plot. With multiple + * axes the offset is dynamically adjusted to avoid collision, this + * can be overridden by setting offset explicitly. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/offset/ + * Y axis offset of 70 + * @sample {highcharts} highcharts/yaxis/offset-centered/ + * Axes positioned in the center of the plot + * @sample {highstock} stock/xaxis/offset/ + * Y axis offset by 70 px + * @default 0 + * @apioption xAxis.offset + */ + + /** + * Whether to display the axis on the opposite side of the normal. The + * normal is on the left side for vertical axes and bottom for + * horizontal, so the opposite sides will be right and top respectively. + * This is typically used with dual or multiple axes. + * + * @type {Boolean} + * @sample {highcharts} highcharts/yaxis/opposite/ + * Secondary Y axis opposite + * @sample {highstock} stock/xaxis/opposite/ + * Y axis on left side + * @default false + * @apioption xAxis.opposite + */ + + /** + * Refers to the index in the [panes](#panes) array. Used for circular + * gauges and polar charts. When the option is not set then first pane + * will be used. + * + * @type {Number} + * @sample highcharts/demo/gauge-vu-meter + * Two gauges with different center + * @product highcharts + * @apioption xAxis.pane + */ + + /** + * Whether to reverse the axis so that the highest number is closest + * to the origin. If the chart is inverted, the x axis is reversed by + * default. + * + * @type {Boolean} + * @sample {highcharts} highcharts/yaxis/reversed/ + * Reversed Y axis + * @sample {highstock} stock/xaxis/reversed/ + * Reversed Y axis + * @default false + * @apioption xAxis.reversed + */ + // reversed: false, + + /** + * Whether to show the last tick label. Defaults to `true` on cartesian + * charts, and `false` on polar charts. + * + * @type {Boolean} + * @sample {highcharts} highcharts/xaxis/showlastlabel-true/ + * Set to true on X axis + * @sample {highstock} stock/xaxis/showfirstlabel/ + * Labels below plot lines on Y axis + * @default true + * @product highcharts highstock + * @apioption xAxis.showLastLabel + */ + + /** + * For datetime axes, this decides where to put the tick between weeks. + * 0 = Sunday, 1 = Monday. + * + * @sample {highcharts} highcharts/xaxis/startofweek-monday/ + * Monday by default + * @sample {highcharts} highcharts/xaxis/startofweek-sunday/ + * Sunday + * @sample {highstock} stock/xaxis/startofweek-1 + * Monday by default + * @sample {highstock} stock/xaxis/startofweek-0 + * Sunday + * @product highcharts highstock + */ + startOfWeek: 1, + + /** + * Whether to force the axis to start on a tick. Use this option with + * the `minPadding` option to control the axis start. + * + * @productdesc {highstock} + * In Highstock, `startOnTick` is always false when the navigator is + * enabled, to prevent jumpy scrolling. + * + * @sample {highcharts} highcharts/xaxis/startontick-false/ + * False by default + * @sample {highcharts} highcharts/xaxis/startontick-true/ + * True + * @sample {highstock} stock/xaxis/endontick/ + * False for Y axis + * @since 1.2.0 + */ + startOnTick: false, + + /** + * The pixel length of the main tick marks. + * + * @sample {highcharts} highcharts/xaxis/ticklength/ + * 20 px tick length on the X axis + * @sample {highstock} stock/xaxis/ticks/ + * Formatted ticks on X axis + */ + tickLength: 10, + + /** + * For categorized axes only. If `on` the tick mark is placed in the + * center of the category, if `between` the tick mark is placed between + * categories. The default is `between` if the `tickInterval` is 1, + * else `on`. + * + * @validvalue [null, "on", "between"] + * @sample {highcharts} highcharts/xaxis/tickmarkplacement-between/ + * "between" by default + * @sample {highcharts} highcharts/xaxis/tickmarkplacement-on/ + * "on" + * @product highcharts + */ + tickmarkPlacement: 'between', + + /** + * If tickInterval is `null` this option sets the approximate pixel + * interval of the tick marks. Not applicable to categorized axis. + * + * The tick interval is also influenced by the [minTickInterval]( + * #xAxis.minTickInterval) option, that, by default prevents ticks from + * being denser than the data points. + * + * @see [tickInterval](#xAxis.tickInterval), + * [tickPositioner](#xAxis.tickPositioner), + * [tickPositions](#xAxis.tickPositions). + * @sample {highcharts} highcharts/xaxis/tickpixelinterval-50/ + * 50 px on X axis + * @sample {highstock} stock/xaxis/tickpixelinterval/ + * 200 px on X axis + */ + tickPixelInterval: 100, + + /** + * The position of the major tick marks relative to the axis line. + * Can be one of `inside` and `outside`. + * + * @validvalue ["inside", "outside"] + * @sample {highcharts} highcharts/xaxis/tickposition-outside/ + * "outside" by default + * @sample {highcharts} highcharts/xaxis/tickposition-inside/ + * "inside" + * @sample {highstock} stock/xaxis/ticks/ + * Formatted ticks on X axis + */ + tickPosition: 'outside', + + /** + * The axis title, showing next to the axis line. + * + * @productdesc {highmaps} + * In Highmaps, the axis is hidden by default, but adding an axis title + * is still possible. X axis and Y axis titles will appear at the bottom + * and left by default. + */ + title: { + + /** + * Alignment of the title relative to the axis values. Possible + * values are "low", "middle" or "high". + * + * @validvalue ["low", "middle", "high"] + * @sample {highcharts} highcharts/xaxis/title-align-low/ + * "low" + * @sample {highcharts} highcharts/xaxis/title-align-center/ + * "middle" by default + * @sample {highcharts} highcharts/xaxis/title-align-high/ + * "high" + * @sample {highcharts} highcharts/yaxis/title-offset/ + * Place the Y axis title on top of the axis + * @sample {highstock} stock/xaxis/title-align/ + * Aligned to "high" value + */ + align: 'middle', + + /*= if (build.classic) { =*/ + + /** + * CSS styles for the title. If the title text is longer than the + * axis length, it will wrap to multiple lines by default. This can + * be customized by setting `textOverflow: 'ellipsis'`, by + * setting a specific `width` or by setting `whiteSpace: 'nowrap'`. + * + * In styled mode, the stroke width is given in the + * `.highcharts-axis-title` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/xaxis/title-style/ + * Red + * @sample {highcharts} highcharts/css/axis/ + * Styled mode + * @default { "color": "#666666" } + */ + style: { + color: '${palette.neutralColor60}' + } + /*= } =*/ + }, + + /** + * The type of axis. Can be one of `linear`, `logarithmic`, `datetime` + * or `category`. In a datetime axis, the numbers are given in + * milliseconds, and tick marks are placed on appropriate values like + * full hours or days. In a category axis, the + * [point names](#series.line.data.name) of the chart's series are used + * for categories, if not a [categories](#xAxis.categories) array is + * defined. + * + * @validvalue ["linear", "logarithmic", "datetime", "category"] + * @sample {highcharts} highcharts/xaxis/type-linear/ + * Linear + * @sample {highcharts} highcharts/yaxis/type-log/ + * Logarithmic + * @sample {highcharts} highcharts/yaxis/type-log-minorgrid/ + * Logarithmic with minor grid lines + * @sample {highcharts} highcharts/xaxis/type-log-both/ + * Logarithmic on two axes + * @sample {highcharts} highcharts/yaxis/type-log-negative/ + * Logarithmic with extension to emulate negative values + * @product highcharts + */ + type: 'linear', + + /*= if (build.classic) { =*/ + + /** + * Color of the minor, secondary grid lines. + * + * In styled mode, the stroke width is given in the + * `.highcharts-minor-grid-line` class. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/minorgridlinecolor/ + * Bright grey lines from Y axis + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/minorgridlinecolor/ + * Bright grey lines from Y axis + * @default #f2f2f2 + */ + minorGridLineColor: '${palette.neutralColor5}', + // minorGridLineDashStyle: null, + + /** + * Width of the minor, secondary grid lines. + * + * In styled mode, the stroke width is given in the + * `.highcharts-grid-line` class. + * + * @sample {highcharts} highcharts/yaxis/minorgridlinewidth/ + * 2px lines from Y axis + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/minorgridlinewidth/ + * 2px lines from Y axis + */ + minorGridLineWidth: 1, + + /** + * Color for the minor tick marks. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/minortickcolor/ + * Black tick marks on Y axis + * @sample {highstock} stock/xaxis/minorticks/ + * Black tick marks on Y axis + * @default #999999 + */ + minorTickColor: '${palette.neutralColor40}', + + /** + * The color of the line marking the axis itself. + * + * In styled mode, the line stroke is given in the + * `.highcharts-axis-line` or `.highcharts-xaxis-line` class. + * + * @productdesc {highmaps} + * In Highmaps, the axis line is hidden by default, because the axis is + * not visible by default. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/linecolor/ + * A red line on Y axis + * @sample {highcharts|highstock} highcharts/css/axis/ + * Axes in styled mode + * @sample {highstock} stock/xaxis/linecolor/ + * A red line on X axis + * @default #ccd6eb + */ + lineColor: '${palette.highlightColor20}', + + /** + * The width of the line marking the axis itself. + * + * In styled mode, the stroke width is given in the + * `.highcharts-axis-line` or `.highcharts-xaxis-line` class. + * + * @sample {highcharts} highcharts/yaxis/linecolor/ + * A 1px line on Y axis + * @sample {highcharts|highstock} highcharts/css/axis/ + * Axes in styled mode + * @sample {highstock} stock/xaxis/linewidth/ + * A 2px line on X axis + * @default {highcharts|highstock} 1 + * @default {highmaps} 0 + */ + lineWidth: 1, + + /** + * Color of the grid lines extending the ticks across the plot area. + * + * In styled mode, the stroke is given in the `.highcharts-grid-line` + * class. + * + * @productdesc {highmaps} + * In Highmaps, the grid lines are hidden by default. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/gridlinecolor/ + * Green lines + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/gridlinecolor/ + * Green lines + * @default #e6e6e6 + */ + gridLineColor: '${palette.neutralColor10}', + // gridLineDashStyle: 'solid', + + + /** + * The width of the grid lines extending the ticks across the plot area. + * + * In styled mode, the stroke width is given in the + * `.highcharts-grid-line` class. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/gridlinewidth/ + * 2px lines + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/gridlinewidth/ + * 2px lines + * @default 0 + * @apioption xAxis.gridLineWidth + */ + // gridLineWidth: 0, + + /** + * Color for the main tick marks. + * + * In styled mode, the stroke is given in the `.highcharts-tick` + * class. + * + * @type {Color} + * @sample {highcharts} highcharts/xaxis/tickcolor/ + * Red ticks on X axis + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/ticks/ + * Formatted ticks on X axis + * @default #ccd6eb + */ + tickColor: '${palette.highlightColor20}' + // tickWidth: 1 + /*= } =*/ + }, + + /** + * The Y axis or value axis. Normally this is the vertical axis, + * though if the chart is inverted this is the horizontal axis. + * In case of multiple axes, the yAxis node is an array of + * configuration objects. + * + * See [the Axis object](#Axis) for programmatic access to the axis. + * + * @extends xAxis + * @excluding ordinal,overscroll + * @optionparent yAxis + */ + defaultYAxisOptions: { + /** + * @productdesc {highstock} + * In Highstock, `endOnTick` is always false when the navigator is + * enabled, to prevent jumpy scrolling. + */ + endOnTick: true, + + /** + * @productdesc {highstock} + * In Highstock 1.x, the Y axis was placed on the left side by default. + * + * @sample {highcharts} highcharts/yaxis/opposite/ + * Secondary Y axis opposite + * @sample {highstock} stock/xaxis/opposite/ + * Y axis on left side + * @default {highstock} true + * @default {highcharts} false + * @product highstock highcharts + * @apioption yAxis.opposite + */ + + /** + * @see [tickInterval](#xAxis.tickInterval), + * [tickPositioner](#xAxis.tickPositioner), + * [tickPositions](#xAxis.tickPositions). + */ + tickPixelInterval: 72, + + showLastLabel: true, + + /** + * @extends xAxis.labels + */ + labels: { + /** + * What part of the string the given position is anchored to. Can + * be one of `"left"`, `"center"` or `"right"`. The exact position + * also depends on the `labels.x` setting. + * + * Angular gauges and solid gauges defaults to `center`. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/yaxis/labels-align-left/ + * Left + * @default {highcharts|highmaps} right + * @default {highstock} left + * @apioption yAxis.labels.align + */ + + /** + * The x position offset of the label relative to the tick position + * on the axis. Defaults to -15 for left axis, 15 for right axis. + * + * @sample {highcharts} highcharts/xaxis/labels-x/ + * Y axis labels placed on grid lines + */ + x: -8 + }, + + /** + * @productdesc {highmaps} + * In Highmaps, the axis line is hidden by default, because the axis is + * not visible by default. + * + * @apioption yAxis.lineColor + */ + + /** + * @sample {highcharts} highcharts/yaxis/min-startontick-false/ + * -50 with startOnTick to false + * @sample {highcharts} highcharts/yaxis/min-startontick-true/ + * -50 with startOnTick true by default + * @sample {highstock} stock/yaxis/min-max/ + * Fixed min and max on Y axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption yAxis.min + */ + + /** + * @sample {highcharts} highcharts/yaxis/max-200/ + * Y axis max of 200 + * @sample {highcharts} highcharts/yaxis/max-logarithmic/ + * Y axis max on logarithmic axis + * @sample {highstock} stock/yaxis/min-max/ + * Fixed min and max on Y axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption yAxis.max + */ + + /** + * Padding of the max value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the highest data value to appear on the edge + * of the plot area. When the axis' `max` option is set or a max extreme + * is set using `axis.setExtremes()`, the maxPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/maxpadding-02/ + * Max padding of 0.2 + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @since 1.2.0 + * @product highcharts highstock + */ + maxPadding: 0.05, + + /** + * Padding of the min value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the lowest data value to appear on the edge + * of the plot area. When the axis' `min` option is set or a max extreme + * is set using `axis.setExtremes()`, the maxPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/minpadding/ + * Min padding of 0.2 + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @since 1.2.0 + * @product highcharts highstock + */ + minPadding: 0.05, + + /** + * Whether to force the axis to start on a tick. Use this option with + * the `maxPadding` option to control the axis start. + * + * @sample {highcharts} highcharts/xaxis/startontick-false/ + * False by default + * @sample {highcharts} highcharts/xaxis/startontick-true/ + * True + * @sample {highstock} stock/xaxis/endontick/ + * False for Y axis + * @since 1.2.0 + * @product highcharts highstock + */ + startOnTick: true, + + /** + * @extends xAxis.title + */ + title: { + + /** + * The rotation of the text in degrees. 0 is horizontal, 270 is + * vertical reading from bottom to top. + * + * @sample {highcharts} highcharts/yaxis/title-offset/ + * Horizontal + */ + rotation: 270, + + /** + * The actual text of the axis title. Horizontal texts can contain + * HTML, but rotated texts are painted using vector techniques and + * must be clean text. The Y axis title is disabled by setting the + * `text` option to `null`. + * + * @sample {highcharts} highcharts/xaxis/title-text/ + * Custom HTML + * @default {highcharts} Values + * @default {highstock} null + * @product highcharts highstock + */ + text: 'Values' + }, + + /** + * The stack labels show the total value for each bar in a stacked + * column or bar chart. The label will be placed on top of positive + * columns and below negative columns. In case of an inverted column + * chart or a bar chart the label is placed to the right of positive + * bars and to the left of negative bars. + * + * @product highcharts + */ + stackLabels: { + + /** + * Allow the stack labels to overlap. + * + * @sample {highcharts} + * highcharts/yaxis/stacklabels-allowoverlap-false/ + * Default false + * @since 5.0.13 + * @product highcharts + */ + allowOverlap: false, + + /** + * Enable or disable the stack total labels. + * + * @sample {highcharts} highcharts/yaxis/stacklabels-enabled/ + * Enabled stack total labels + * @since 2.1.5 + * @product highcharts + */ + enabled: false, + + /** + * Callback JavaScript function to format the label. The value is + * given by `this.total`. + * + * @default function() { return this.total; } + * + * @type {Function} + * @sample {highcharts} highcharts/yaxis/stacklabels-formatter/ + * Added units to stack total value + * @since 2.1.5 + * @product highcharts + */ + formatter: function () { + return H.numberFormat(this.total, -1); + }, + /*= if (build.classic) { =*/ + + /** + * CSS styles for the label. + * + * In styled mode, the styles are set in the + * `.highcharts-stack-label` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/yaxis/stacklabels-style/ + * Red stack total labels + * @since 2.1.5 + * @product highcharts + */ + style: { + fontSize: '11px', + fontWeight: 'bold', + color: '${palette.neutralColor100}', + textOutline: '1px contrast' + } + /*= } =*/ + }, + /*= if (build.classic) { =*/ + gridLineWidth: 1, + lineWidth: 0 + // tickWidth: 0 + /*= } =*/ + }, + + /** + * These options extend the defaultOptions for left axes. + * + * @private + * @type {Object} + */ + defaultLeftAxisOptions: { + labels: { + x: -15 + }, + title: { + rotation: 270 + } + }, + + /** + * These options extend the defaultOptions for right axes. + * + * @private + * @type {Object} + */ + defaultRightAxisOptions: { + labels: { + x: 15 + }, + title: { + rotation: 90 + } + }, + + /** + * These options extend the defaultOptions for bottom axes. + * + * @private + * @type {Object} + */ + defaultBottomAxisOptions: { + labels: { + autoRotation: [-45], + x: 0 + // overflow: undefined, + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + /** + * These options extend the defaultOptions for top axes. + * + * @private + * @type {Object} + */ + defaultTopAxisOptions: { + labels: { + autoRotation: [-45], + x: 0 + // overflow: undefined + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + + /** + * Overrideable function to initialize the axis. + * + * @see {@link Axis} + */ + init: function (chart, userOptions) { + + + var isXAxis = userOptions.isX, + axis = this; + + /** + * The Chart that the axis belongs to. + * + * @name chart + * @memberOf Axis + * @type {Chart} + */ + axis.chart = chart; + + /** + * Whether the axis is horizontal. + * + * @name horiz + * @memberOf Axis + * @type {Boolean} + */ + axis.horiz = chart.inverted && !axis.isZAxis ? !isXAxis : isXAxis; + + // Flag, isXAxis + axis.isXAxis = isXAxis; + + /** + * The collection where the axis belongs, for example `xAxis`, `yAxis` + * or `colorAxis`. Corresponds to properties on Chart, for example + * {@link Chart.xAxis}. + * + * @name coll + * @memberOf Axis + * @type {String} + */ + axis.coll = axis.coll || (isXAxis ? 'xAxis' : 'yAxis'); + + fireEvent(this, 'init', { userOptions: userOptions }); + + axis.opposite = userOptions.opposite; // needed in setOptions + + /** + * The side on which the axis is rendered. 0 is top, 1 is right, 2 is + * bottom and 3 is left. + * + * @name side + * @memberOf Axis + * @type {Number} + */ + axis.side = userOptions.side || (axis.horiz ? + (axis.opposite ? 0 : 2) : // top : bottom + (axis.opposite ? 1 : 3)); // right : left + + axis.setOptions(userOptions); + + + var options = this.options, + type = options.type, + isDatetimeAxis = type === 'datetime'; + + axis.labelFormatter = options.labels.formatter || + axis.defaultLabelFormatter; // can be overwritten by dynamic format + + + // Flag, stagger lines or not + axis.userOptions = userOptions; + + axis.minPixelPadding = 0; + + + /** + * Whether the axis is reversed. Based on the `axis.reversed`, + * option, but inverted charts have reversed xAxis by default. + * + * @name reversed + * @memberOf Axis + * @type {Boolean} + */ + axis.reversed = options.reversed; + axis.visible = options.visible !== false; + axis.zoomEnabled = options.zoomEnabled !== false; + + // Initial categories + axis.hasNames = type === 'category' || options.categories === true; + axis.categories = options.categories || axis.hasNames; + if (!axis.names) { // Preserve on update (#3830) + axis.names = []; + axis.names.keys = {}; + } + + + // Placeholder for plotlines and plotbands groups + axis.plotLinesAndBandsGroups = {}; + + // Shorthand types + axis.isLog = type === 'logarithmic'; + axis.isDatetimeAxis = isDatetimeAxis; + axis.positiveValuesOnly = axis.isLog && !axis.allowNegativeLog; + + // Flag, if axis is linked to another axis + axis.isLinked = defined(options.linkedTo); + + // Major ticks + axis.ticks = {}; + axis.labelEdge = []; + // Minor ticks + axis.minorTicks = {}; + + // List of plotLines/Bands + axis.plotLinesAndBands = []; + + // Alternate bands + axis.alternateBands = {}; + + // Axis metrics + axis.len = 0; + axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; + axis.range = options.range; + axis.offset = options.offset || 0; + + + // Dictionary for stacks + axis.stacks = {}; + axis.oldStacks = {}; + axis.stacksTouched = 0; + + + /** + * The maximum value of the axis. In a logarithmic axis, this is the + * logarithm of the real value, and the real value can be obtained from + * {@link Axis#getExtremes}. + * + * @name max + * @memberOf Axis + * @type {Number} + */ + axis.max = null; + /** + * The minimum value of the axis. In a logarithmic axis, this is the + * logarithm of the real value, and the real value can be obtained from + * {@link Axis#getExtremes}. + * + * @name min + * @memberOf Axis + * @type {Number} + */ + axis.min = null; + + + /** + * The processed crosshair options. + * + * @name crosshair + * @memberOf Axis + * @type {AxisCrosshairOptions} + */ + axis.crosshair = pick( + options.crosshair, + splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], + false + ); + + var events = axis.options.events; + + // Register. Don't add it again on Axis.update(). + if (inArray(axis, chart.axes) === -1) { // + if (isXAxis) { // #2713 + chart.axes.splice(chart.xAxis.length, 0, axis); + } else { + chart.axes.push(axis); + } + + chart[axis.coll].push(axis); + } + + /** + * All series associated to the axis. + * + * @name series + * @memberOf Axis + * @type {Array.} + */ + axis.series = axis.series || []; // populated by Series + + // Reversed axis + if ( + chart.inverted && + !axis.isZAxis && + isXAxis && + axis.reversed === undefined + ) { + axis.reversed = true; + } + + // register event listeners + objectEach(events, function (event, eventType) { + addEvent(axis, eventType, event); + }); + + // extend logarithmic axis + axis.lin2log = options.linearToLogConverter || axis.lin2log; + if (axis.isLog) { + axis.val2lin = axis.log2lin; + axis.lin2val = axis.lin2log; + } + + fireEvent(this, 'afterInit'); + }, + + /** + * Merge and set options. + * + * @private + */ + setOptions: function (userOptions) { + this.options = merge( + this.defaultOptions, + this.coll === 'yAxis' && this.defaultYAxisOptions, + [ + this.defaultTopAxisOptions, + this.defaultRightAxisOptions, + this.defaultBottomAxisOptions, + this.defaultLeftAxisOptions + ][this.side], + merge( + defaultOptions[this.coll], // if set in setOptions (#1053) + userOptions + ) + ); + + fireEvent(this, 'afterSetOptions', { userOptions: userOptions }); + }, + + /** + * The default label formatter. The context is a special config object for + * the label. In apps, use the {@link + * https://api.highcharts.com/highcharts/xAxis.labels.formatter| + * labels.formatter} instead except when a modification is needed. + * + * @private + */ + defaultLabelFormatter: function () { + var axis = this.axis, + value = this.value, + time = axis.chart.time, + categories = axis.categories, + dateTimeLabelFormat = this.dateTimeLabelFormat, + lang = defaultOptions.lang, + numericSymbols = lang.numericSymbols, + numSymMagnitude = lang.numericSymbolMagnitude || 1000, + i = numericSymbols && numericSymbols.length, + multi, + ret, + formatOption = axis.options.labels.format, + + // make sure the same symbol is added for all labels on a linear + // axis + numericSymbolDetector = axis.isLog ? + Math.abs(value) : + axis.tickInterval; + + if (formatOption) { + ret = format(formatOption, this, time); + + } else if (categories) { + ret = value; + + } else if (dateTimeLabelFormat) { // datetime axis + ret = time.dateFormat(dateTimeLabelFormat, value); + + } else if (i && numericSymbolDetector >= 1000) { + // Decide whether we should add a numeric symbol like k (thousands) + // or M (millions). If we are to enable this in tooltip or other + // places as well, we can move this logic to the numberFormatter and + // enable it by a parameter. + while (i-- && ret === undefined) { + multi = Math.pow(numSymMagnitude, i + 1); + if ( + // Only accept a numeric symbol when the distance is more + // than a full unit. So for example if the symbol is k, we + // don't accept numbers like 0.5k. + numericSymbolDetector >= multi && + // Accept one decimal before the symbol. Accepts 0.5k but + // not 0.25k. How does this work with the previous? + (value * 10) % multi === 0 && + numericSymbols[i] !== null && + value !== 0 + ) { // #5480 + ret = H.numberFormat(value / multi, -1) + numericSymbols[i]; + } + } + } + + if (ret === undefined) { + if (Math.abs(value) >= 10000) { // add thousands separators + ret = H.numberFormat(value, -1); + } else { // small numbers + ret = H.numberFormat(value, -1, undefined, ''); // #2466 + } + } + + return ret; + }, + + /** + * Get the minimum and maximum for the series of each axis. The function + * analyzes the axis series and updates `this.dataMin` and `this.dataMax`. + * + * @private + */ + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart; + + fireEvent(this, 'getSeriesExtremes', null, function () { + + axis.hasVisibleSeries = false; + + // Reset properties in case we're redrawing (#3353) + axis.dataMin = axis.dataMax = axis.threshold = null; + axis.softThreshold = !axis.isXAxis; + + if (axis.buildStacks) { + axis.buildStacks(); + } + + // loop through this axis' series + each(axis.series, function (series) { + + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + + var seriesOptions = series.options, + xData, + threshold = seriesOptions.threshold, + seriesDataMin, + seriesDataMax; + + axis.hasVisibleSeries = true; + + // Validate threshold in logarithmic axes + if (axis.positiveValuesOnly && threshold <= 0) { + threshold = null; + } + + // Get dataMin and dataMax for X axes + if (axis.isXAxis) { + xData = series.xData; + if (xData.length) { + // If xData contains values which is not numbers, + // then filter them out. To prevent performance hit, + // we only do this after we have already found + // seriesDataMin because in most cases all data is + // valid. #5234. + seriesDataMin = arrayMin(xData); + seriesDataMax = arrayMax(xData); + + if ( + !isNumber(seriesDataMin) && + !(seriesDataMin instanceof Date) // #5010 + ) { + xData = grep(xData, isNumber); + // Do it again with valid data + seriesDataMin = arrayMin(xData); + seriesDataMax = arrayMax(xData); + } + + if (xData.length) { + axis.dataMin = Math.min( + pick(axis.dataMin, xData[0], seriesDataMin), + seriesDataMin + ); + axis.dataMax = Math.max( + pick(axis.dataMax, xData[0], seriesDataMax), + seriesDataMax + ); + } + } + + // Get dataMin and dataMax for Y axes, as well as handle + // stacking and processed data + } else { + + // Get this particular series extremes + series.getExtremes(); + seriesDataMax = series.dataMax; + seriesDataMin = series.dataMin; + + // Get the dataMin and dataMax so far. If percentage is + // used, the min and max are always 0 and 100. If + // seriesDataMin and seriesDataMax is null, then series + // doesn't have active y data, we continue with nulls + if (defined(seriesDataMin) && defined(seriesDataMax)) { + axis.dataMin = Math.min( + pick(axis.dataMin, seriesDataMin), + seriesDataMin + ); + axis.dataMax = Math.max( + pick(axis.dataMax, seriesDataMax), + seriesDataMax + ); + } + + // Adjust to threshold + if (defined(threshold)) { + axis.threshold = threshold; + } + // If any series has a hard threshold, it takes + // precedence + if ( + !seriesOptions.softThreshold || + axis.positiveValuesOnly + ) { + axis.softThreshold = false; + } + } + } + }); + }); + + fireEvent(this, 'afterGetSeriesExtremes'); + }, + + /** + * Translate from axis value to pixel position on the chart, or back. Use + * the `toPixels` and `toValue` functions in applications. + * + * @private + */ + translate: function ( + val, + backwards, + cvsCoord, + old, + handleLog, + pointPlacement + ) { + var axis = this.linkedParent || this, // #1417 + sign = 1, + cvsOffset = 0, + localA = old ? axis.oldTransA : axis.transA, + localMin = old ? axis.oldMin : axis.min, + returnValue, + minPixelPadding = axis.minPixelPadding, + doPostTranslate = ( + axis.isOrdinal || + axis.isBroken || + (axis.isLog && handleLog) + ) && axis.lin2val; + + if (!localA) { + localA = axis.transA; + } + + // In vertical axes, the canvas coordinates start from 0 at the top like + // in SVG. + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axis.len; + } + + // Handle reversed axis + if (axis.reversed) { + sign *= -1; + cvsOffset -= sign * (axis.sector || axis.len); + } + + // From pixels to value + if (backwards) { // reverse translation + + val = val * sign + cvsOffset; + val -= minPixelPadding; + returnValue = val / localA + localMin; // from chart pixel to value + if (doPostTranslate) { // log and ordinal axes + returnValue = axis.lin2val(returnValue); + } + + // From value to pixels + } else { + if (doPostTranslate) { // log and ordinal axes + val = axis.val2lin(val); + } + returnValue = isNumber(localMin) ? + ( + sign * (val - localMin) * localA + + cvsOffset + + (sign * minPixelPadding) + + (isNumber(pointPlacement) ? localA * pointPlacement : 0) + ) : + undefined; + } + + return returnValue; + }, + + /** + * Translate a value in terms of axis units into pixels within the chart. + * + * @param {Number} value + * A value in terms of axis units. + * @param {Boolean} paneCoordinates + * Whether to return the pixel coordinate relative to the chart or + * just the axis/pane itself. + * @return {Number} Pixel position of the value on the chart or axis. + */ + toPixels: function (value, paneCoordinates) { + return this.translate(value, false, !this.horiz, null, true) + + (paneCoordinates ? 0 : this.pos); + }, + + /** + * Translate a pixel position along the axis to a value in terms of axis + * units. + * @param {Number} pixel + * The pixel value coordinate. + * @param {Boolean} paneCoordiantes + * Whether the input pixel is relative to the chart or just the + * axis/pane itself. + * @return {Number} The axis value. + */ + toValue: function (pixel, paneCoordinates) { + return this.translate( + pixel - (paneCoordinates ? 0 : this.pos), + true, + !this.horiz, + null, + true + ); + }, + + /** + * Create the path for a plot line that goes from the given value on + * this axis, across the plot to the opposite side. Also used internally for + * grid lines and crosshairs. + * + * @param {Number} value + * Axis value. + * @param {Number} [lineWidth=1] + * Used for calculation crisp line coordinates. + * @param {Boolean} [old=false] + * Use old coordinates (for resizing and rescaling). + * @param {Boolean} [force=false] + * If `false`, the function will return null when it falls outside + * the axis bounds. + * @param {Number} [translatedValue] + * If given, return the plot line path of a pixel position on the + * axis. + * + * @return {Array.} + * The SVG path definition for the plot line. + */ + getPlotLinePath: function (value, lineWidth, old, force, translatedValue) { + var axis = this, + chart = axis.chart, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + cWidth = (old && chart.oldChartWidth) || chart.chartWidth, + skip, + transB = axis.transB, + /** + * Check if x is between a and b. If not, either move to a/b + * or skip, depending on the force parameter. + */ + between = function (x, a, b) { + if (x < a || x > b) { + if (force) { + x = Math.min(Math.max(a, x), b); + } else { + skip = true; + } + } + return x; + }; + + translatedValue = pick( + translatedValue, + axis.translate(value, null, null, old) + ); + // Keep the translated value within sane bounds, and avoid Infinity to + // fail the isNumber test (#7709). + translatedValue = Math.min(Math.max(-1e5, translatedValue), 1e5); + + + x1 = x2 = Math.round(translatedValue + transB); + y1 = y2 = Math.round(cHeight - translatedValue - transB); + if (!isNumber(translatedValue)) { // no min or max + skip = true; + force = false; // #7175, don't force it when path is invalid + } else if (axis.horiz) { + y1 = axisTop; + y2 = cHeight - axis.bottom; + x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); + } else { + x1 = axisLeft; + x2 = cWidth - axis.right; + y1 = y2 = between(y1, axisTop, axisTop + axis.height); + } + return skip && !force ? + null : + chart.renderer.crispLine( + ['M', x1, y1, 'L', x2, y2], + lineWidth || 1 + ); + }, + + /** + * Internal function to et the tick positions of a linear axis to round + * values like whole tens or every five. + * + * @param {Number} tickInterval + * The normalized tick interval + * @param {Number} min + * Axis minimum. + * @param {Number} max + * Axis maximum. + * + * @return {Array.} + * An array of axis values where ticks should be placed. + */ + getLinearTickPositions: function (tickInterval, min, max) { + var pos, + lastPos, + roundedMin = + correctFloat(Math.floor(min / tickInterval) * tickInterval), + roundedMax = + correctFloat(Math.ceil(max / tickInterval) * tickInterval), + tickPositions = [], + precision; + + // When the precision is higher than what we filter out in + // correctFloat, skip it (#6183). + if (correctFloat(roundedMin + tickInterval) === roundedMin) { + precision = 20; + } + + // For single points, add a tick regardless of the relative position + // (#2662, #6274) + if (this.single) { + return [min]; + } + + // Populate the intermediate values + pos = roundedMin; + while (pos <= roundedMax) { + + // Place the tick on the rounded value + tickPositions.push(pos); + + // Always add the raw tickInterval, not the corrected one. + pos = correctFloat( + pos + tickInterval, + precision + ); + + // If the interval is not big enough in the current min - max range + // to actually increase the loop variable, we need to break out to + // prevent endless loop. Issue #619 + if (pos === lastPos) { + break; + } + + // Record the last value + lastPos = pos; + } + return tickPositions; + }, + + /** + * Resolve the new minorTicks/minorTickInterval options into the legacy + * loosely typed minorTickInterval option. + */ + getMinorTickInterval: function () { + var options = this.options; + + if (options.minorTicks === true) { + return pick(options.minorTickInterval, 'auto'); + } + if (options.minorTicks === false) { + return null; + } + return options.minorTickInterval; + }, + + /** + * Internal function to return the minor tick positions. For logarithmic + * axes, the same logic as for major ticks is reused. + * + * @return {Array.} + * An array of axis values where ticks should be placed. + */ + getMinorTickPositions: function () { + var axis = this, + options = axis.options, + tickPositions = axis.tickPositions, + minorTickInterval = axis.minorTickInterval, + minorTickPositions = [], + pos, + pointRangePadding = axis.pointRangePadding || 0, + min = axis.min - pointRangePadding, // #1498 + max = axis.max + pointRangePadding, // #1498 + range = max - min; + + // If minor ticks get too dense, they are hard to read, and may cause + // long running script. So we don't draw them. + if (range && range / minorTickInterval < axis.len / 3) { // #3875 + + if (axis.isLog) { + // For each interval in the major ticks, compute the minor ticks + // separately. + each(this.paddedTicks, function (pos, i, paddedTicks) { + if (i) { + minorTickPositions.push.apply( + minorTickPositions, + axis.getLogTickPositions( + minorTickInterval, + paddedTicks[i - 1], + paddedTicks[i], + true + ) + ); + } + }); + + } else if ( + axis.isDatetimeAxis && + this.getMinorTickInterval() === 'auto' + ) { // #1314 + minorTickPositions = minorTickPositions.concat( + axis.getTimeTicks( + axis.normalizeTimeTickInterval(minorTickInterval), + min, + max, + options.startOfWeek + ) + ); + } else { + for ( + pos = min + (tickPositions[0] - min) % minorTickInterval; + pos <= max; + pos += minorTickInterval + ) { + // Very, very, tight grid lines (#5771) + if (pos === minorTickPositions[0]) { + break; + } + minorTickPositions.push(pos); + } + } + } + + if (minorTickPositions.length !== 0) { + axis.trimTicks(minorTickPositions); // #3652 #3743 #1498 #6330 + } + return minorTickPositions; + }, + + /** + * Adjust the min and max for the minimum range. Keep in mind that the + * series data is not yet processed, so we don't have information on data + * cropping and grouping, or updated axis.pointRange or series.pointRange. + * The data can't be processed until we have finally established min and + * max. + * + * @private + */ + adjustForMinRange: function () { + var axis = this, + options = axis.options, + min = axis.min, + max = axis.max, + zoomOffset, + spaceAvailable, + closestDataRange, + i, + distance, + xData, + loopLength, + minArgs, + maxArgs, + minRange; + + // Set the automatic minimum range based on the closest point distance + if (axis.isXAxis && axis.minRange === undefined && !axis.isLog) { + + if (defined(options.min) || defined(options.max)) { + axis.minRange = null; // don't do this again + + } else { + + // Find the closest distance between raw data points, as opposed + // to closestPointRange that applies to processed points + // (cropped and grouped) + each(axis.series, function (series) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if ( + closestDataRange === undefined || + distance < closestDataRange + ) { + closestDataRange = distance; + } + } + }); + axis.minRange = Math.min( + closestDataRange * 5, + axis.dataMax - axis.dataMin + ); + } + } + + // if minRange is exceeded, adjust + if (max - min < axis.minRange) { + + spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange; + minRange = axis.minRange; + zoomOffset = (minRange - max + min) / 2; + + // if min and max options have been set, don't go beyond it + minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; + // If space is available, stay within the data range + if (spaceAvailable) { + minArgs[2] = axis.isLog ? + axis.log2lin(axis.dataMin) : + axis.dataMin; + } + min = arrayMax(minArgs); + + maxArgs = [min + minRange, pick(options.max, min + minRange)]; + // If space is availabe, stay within the data range + if (spaceAvailable) { + maxArgs[2] = axis.isLog ? + axis.log2lin(axis.dataMax) : + axis.dataMax; + } + + max = arrayMin(maxArgs); + + // now if the max is adjusted, adjust the min back + if (max - min < minRange) { + minArgs[0] = max - minRange; + minArgs[1] = pick(options.min, max - minRange); + min = arrayMax(minArgs); + } + } + + // Record modified extremes + axis.min = min; + axis.max = max; + }, + + /** + * Find the closestPointRange across all series. + * + * @private + */ + getClosest: function () { + var ret; + + if (this.categories) { + ret = 1; + } else { + each(this.series, function (series) { + var seriesClosest = series.closestPointRange, + visible = series.visible || + !series.chart.options.chart.ignoreHiddenSeries; + + if ( + !series.noSharedTooltip && + defined(seriesClosest) && + visible + ) { + ret = defined(ret) ? + Math.min(ret, seriesClosest) : + seriesClosest; + } + }); + } + return ret; + }, + + /** + * When a point name is given and no x, search for the name in the existing + * categories, or if categories aren't provided, search names or create a + * new category (#2522). + * + * @private + * + * @param {Point} + * The point to inspect. + * + * @return {Number} + * The X value that the point is given. + */ + nameToX: function (point) { + var explicitCategories = isArray(this.categories), + names = explicitCategories ? this.categories : this.names, + nameX = point.options.x, + x; + + point.series.requireSorting = false; + + if (!defined(nameX)) { + nameX = this.options.uniqueNames === false ? + point.series.autoIncrement() : + ( + explicitCategories ? + inArray(point.name, names) : + pick(names.keys[point.name], -1) + + ); + } + if (nameX === -1) { // Not found in currenct categories + if (!explicitCategories) { + x = names.length; + } + } else { + x = nameX; + } + + // Write the last point's name to the names array + if (x !== undefined) { + this.names[x] = point.name; + // Backwards mapping is much faster than array searching (#7725) + this.names.keys[point.name] = x; + } + + return x; + }, + + /** + * When changes have been done to series data, update the axis.names. + * + * @private + */ + updateNames: function () { + var axis = this, + names = this.names, + i = names.length; + + if (i > 0) { + each(H.keys(names.keys), function (key) { + delete names.keys[key]; + }); + names.length = 0; + + this.minRange = this.userMinRange; // Reset + each(this.series || [], function (series) { + + // Reset incrementer (#5928) + series.xIncrement = null; + + // When adding a series, points are not yet generated + if (!series.points || series.isDirtyData) { + series.processData(); + series.generatePoints(); + } + + each(series.points, function (point, i) { + var x; + if (point.options) { + x = axis.nameToX(point); + if (x !== undefined && x !== point.x) { + point.x = x; + series.xData[i] = x; + } + } + }); + }); + } + }, + + /** + * Update translation information. + * + * @private + */ + setAxisTranslation: function (saveOld) { + var axis = this, + range = axis.max - axis.min, + pointRange = axis.axisPointRange || 0, + closestPointRange, + minPointOffset = 0, + pointRangePadding = 0, + linkedParent = axis.linkedParent, + ordinalCorrection, + hasCategories = !!axis.categories, + transA = axis.transA, + isXAxis = axis.isXAxis; + + // Adjust translation for padding. Y axis with categories need to go + // through the same (#1784). + if (isXAxis || hasCategories || pointRange) { + + // Get the closest points + closestPointRange = axis.getClosest(); + + if (linkedParent) { + minPointOffset = linkedParent.minPointOffset; + pointRangePadding = linkedParent.pointRangePadding; + } else { + each(axis.series, function (series) { + var seriesPointRange = hasCategories ? + 1 : + ( + isXAxis ? + pick( + series.options.pointRange, + closestPointRange, + 0 + ) : + (axis.axisPointRange || 0) + ), // #2806 + pointPlacement = series.options.pointPlacement; + + pointRange = Math.max(pointRange, seriesPointRange); + + if (!axis.single) { + // minPointOffset is the value padding to the left of + // the axis in order to make room for points with a + // pointRange, typically columns. When the + // pointPlacement option is 'between' or 'on', this + // padding does not apply. + minPointOffset = Math.max( + minPointOffset, + isString(pointPlacement) ? 0 : seriesPointRange / 2 + ); + + // Determine the total padding needed to the length of + // the axis to make room for the pointRange. If the + // series' pointPlacement is 'on', no padding is added. + pointRangePadding = Math.max( + pointRangePadding, + pointPlacement === 'on' ? 0 : seriesPointRange + ); + } + }); + } + + // Record minPointOffset and pointRangePadding + ordinalCorrection = axis.ordinalSlope && closestPointRange ? + axis.ordinalSlope / closestPointRange : + 1; // #988, #1853 + axis.minPointOffset = minPointOffset = + minPointOffset * ordinalCorrection; + axis.pointRangePadding = + pointRangePadding = pointRangePadding * ordinalCorrection; + + // pointRange means the width reserved for each point, like in a + // column chart + axis.pointRange = Math.min(pointRange, range); + + // closestPointRange means the closest distance between points. In + // columns it is mostly equal to pointRange, but in lines pointRange + // is 0 while closestPointRange is some other value + if (isXAxis) { + axis.closestPointRange = closestPointRange; + } + } + + // Secondary values + if (saveOld) { + axis.oldTransA = transA; + } + axis.translationSlope = axis.transA = transA = + axis.options.staticScale || + axis.len / ((range + pointRangePadding) || 1); + + // Translation addend + axis.transB = axis.horiz ? axis.left : axis.bottom; + axis.minPixelPadding = transA * minPointOffset; + + fireEvent(this, 'afterSetAxisTranslation'); + }, + + minFromRange: function () { + return this.max - this.range; + }, + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick. + * + * @private + */ + setTickInterval: function (secondPass) { + var axis = this, + chart = axis.chart, + options = axis.options, + isLog = axis.isLog, + log2lin = axis.log2lin, + isDatetimeAxis = axis.isDatetimeAxis, + isXAxis = axis.isXAxis, + isLinked = axis.isLinked, + maxPadding = options.maxPadding, + minPadding = options.minPadding, + length, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + minTickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + categories = axis.categories, + threshold = axis.threshold, + softThreshold = axis.softThreshold, + thresholdMin, + thresholdMax, + hardMin, + hardMax; + + if (!isDatetimeAxis && !categories && !isLinked) { + this.getTickAmount(); + } + + // Min or max set either by zooming/setExtremes or initial options + hardMin = pick(axis.userMin, options.min); + hardMax = pick(axis.userMax, options.max); + + // Linked axis gets the extremes from the parent axis + if (isLinked) { + axis.linkedParent = chart[axis.coll][options.linkedTo]; + linkedParentExtremes = axis.linkedParent.getExtremes(); + axis.min = pick( + linkedParentExtremes.min, + linkedParentExtremes.dataMin + ); + axis.max = pick( + linkedParentExtremes.max, + linkedParentExtremes.dataMax + ); + if (options.type !== axis.linkedParent.options.type) { + H.error(11, 1); // Can't link axes of different type + } + + // Initial min and max from the extreme data values + } else { + + // Adjust to hard threshold + if (!softThreshold && defined(threshold)) { + if (axis.dataMin >= threshold) { + thresholdMin = threshold; + minPadding = 0; + } else if (axis.dataMax <= threshold) { + thresholdMax = threshold; + maxPadding = 0; + } + } + + axis.min = pick(hardMin, thresholdMin, axis.dataMin); + axis.max = pick(hardMax, thresholdMax, axis.dataMax); + + } + + if (isLog) { + if ( + axis.positiveValuesOnly && + !secondPass && + Math.min(axis.min, pick(axis.dataMin, axis.min)) <= 0 + ) { // #978 + H.error(10, 1); // Can't plot negative values on log axis + } + // The correctFloat cures #934, float errors on full tens. But it + // was too aggressive for #4360 because of conversion back to lin, + // therefore use precision 15. + axis.min = correctFloat(log2lin(axis.min), 15); + axis.max = correctFloat(log2lin(axis.max), 15); + } + + // handle zoomed range + if (axis.range && defined(axis.max)) { + axis.userMin = axis.min = hardMin = + Math.max(axis.dataMin, axis.minFromRange()); // #618, #6773 + axis.userMax = hardMax = axis.max; + + axis.range = null; // don't use it when running setExtremes + } + + // Hook for Highstock Scroller. Consider combining with beforePadding. + fireEvent(axis, 'foundExtremes'); + + // Hook for adjusting this.min and this.max. Used by bubble series. + if (axis.beforePadding) { + axis.beforePadding(); + } + + // adjust min and max for the minimum range + axis.adjustForMinRange(); + + // Pad the values to get clear of the chart's edges. To avoid + // tickInterval taking the padding into account, we do this after + // computing tick interval (#1337). + if ( + !categories && + !axis.axisPointRange && + !axis.usePercentage && + !isLinked && + defined(axis.min) && + defined(axis.max) + ) { + length = axis.max - axis.min; + if (length) { + if (!defined(hardMin) && minPadding) { + axis.min -= length * minPadding; + } + if (!defined(hardMax) && maxPadding) { + axis.max += length * maxPadding; + } + } + } + + // Handle options for floor, ceiling, softMin and softMax (#6359) + if (isNumber(options.softMin) && !isNumber(axis.userMin)) { + axis.min = Math.min(axis.min, options.softMin); + } + if (isNumber(options.softMax) && !isNumber(axis.userMax)) { + axis.max = Math.max(axis.max, options.softMax); + } + if (isNumber(options.floor)) { + axis.min = Math.max(axis.min, options.floor); + } + if (isNumber(options.ceiling)) { + axis.max = Math.min(axis.max, options.ceiling); + } + + + // When the threshold is soft, adjust the extreme value only if the data + // extreme and the padded extreme land on either side of the threshold. + // For example, a series of [0, 1, 2, 3] would make the yAxis add a tick + // for -1 because of the default minPadding and startOnTick options. + // This is prevented by the softThreshold option. + if (softThreshold && defined(axis.dataMin)) { + threshold = threshold || 0; + if ( + !defined(hardMin) && + axis.min < threshold && + axis.dataMin >= threshold + ) { + axis.min = threshold; + + } else if ( + !defined(hardMax) && + axis.max > threshold && + axis.dataMax <= threshold + ) { + axis.max = threshold; + } + } + + + // get tickInterval + if ( + axis.min === axis.max || + axis.min === undefined || + axis.max === undefined + ) { + axis.tickInterval = 1; + + } else if ( + isLinked && + !tickIntervalOption && + tickPixelIntervalOption === + axis.linkedParent.options.tickPixelInterval + ) { + axis.tickInterval = tickIntervalOption = + axis.linkedParent.tickInterval; + + } else { + axis.tickInterval = pick( + tickIntervalOption, + this.tickAmount ? + ((axis.max - axis.min) / Math.max(this.tickAmount - 1, 1)) : + undefined, + // For categoried axis, 1 is default, for linear axis use + // tickPix + categories ? + 1 : + // don't let it be more than the data range + (axis.max - axis.min) * tickPixelIntervalOption / + Math.max(axis.len, tickPixelIntervalOption) + ); + } + + /** + * Now we're finished detecting min and max, crop and group series data. + * This is in turn needed in order to find tick positions in + * ordinal axes. + */ + if (isXAxis && !secondPass) { + each(axis.series, function (series) { + series.processData( + axis.min !== axis.oldMin || axis.max !== axis.oldMax + ); + }); + } + + // set the translation factor used in translate function + axis.setAxisTranslation(true); + + // hook for ordinal axes and radial axes + if (axis.beforeSetTickPositions) { + axis.beforeSetTickPositions(); + } + + // hook for extensions, used in Highstock ordinal axes + if (axis.postProcessTickInterval) { + axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); + } + + // In column-like charts, don't cramp in more ticks than there are + // points (#1943, #4184) + if (axis.pointRange && !tickIntervalOption) { + axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval); + } + + // Before normalizing the tick interval, handle minimum tick interval. + // This applies only if tickInterval is not defined. + minTickInterval = pick( + options.minTickInterval, + axis.isDatetimeAxis && axis.closestPointRange + ); + if (!tickIntervalOption && axis.tickInterval < minTickInterval) { + axis.tickInterval = minTickInterval; + } + + // for linear axes, get magnitude and normalize the interval + if (!isDatetimeAxis && !isLog && !tickIntervalOption) { + axis.tickInterval = normalizeTickInterval( + axis.tickInterval, + null, + getMagnitude(axis.tickInterval), + // If the tick interval is between 0.5 and 5 and the axis max is + // in the order of thousands, chances are we are dealing with + // years. Don't allow decimals. #3363. + pick( + options.allowDecimals, + !( + axis.tickInterval > 0.5 && + axis.tickInterval < 5 && + axis.max > 1000 && + axis.max < 9999 + ) + ), + !!this.tickAmount + ); + } + + // Prevent ticks from getting so close that we can't draw the labels + if (!this.tickAmount) { + axis.tickInterval = axis.unsquish(); + } + + this.setTickPositions(); + }, + + /** + * Now we have computed the normalized tickInterval, get the tick positions + */ + setTickPositions: function () { + + var options = this.options, + tickPositions, + tickPositionsOption = options.tickPositions, + minorTickIntervalOption = this.getMinorTickInterval(), + tickPositioner = options.tickPositioner, + startOnTick = options.startOnTick, + endOnTick = options.endOnTick; + + // Set the tickmarkOffset + this.tickmarkOffset = ( + this.categories && + options.tickmarkPlacement === 'between' && + this.tickInterval === 1 + ) ? 0.5 : 0; // #3202 + + + // get minorTickInterval + this.minorTickInterval = + minorTickIntervalOption === 'auto' && + this.tickInterval ? + this.tickInterval / 5 : + minorTickIntervalOption; + + // When there is only one point, or all points have the same value on + // this axis, then min and max are equal and tickPositions.length is 0 + // or 1. In this case, add some padding in order to center the point, + // but leave it with one tick. #1337. + this.single = + this.min === this.max && + defined(this.min) && + !this.tickAmount && + ( + // Data is on integer (#6563) + parseInt(this.min, 10) === this.min || + + // Between integers and decimals are not allowed (#6274) + options.allowDecimals !== false + ); + + // Find the tick positions. Work on a copy (#1565) + this.tickPositions = tickPositions = + tickPositionsOption && tickPositionsOption.slice(); + if (!tickPositions) { + + if (this.isDatetimeAxis) { + tickPositions = this.getTimeTicks( + this.normalizeTimeTickInterval( + this.tickInterval, + options.units + ), + this.min, + this.max, + options.startOfWeek, + this.ordinalPositions, + this.closestPointRange, + true + ); + } else if (this.isLog) { + tickPositions = this.getLogTickPositions( + this.tickInterval, + this.min, + this.max + ); + } else { + tickPositions = this.getLinearTickPositions( + this.tickInterval, + this.min, + this.max + ); + } + + // Too dense ticks, keep only the first and last (#4477) + if (tickPositions.length > this.len) { + tickPositions = [tickPositions[0], tickPositions.pop()]; + // Reduce doubled value (#7339) + if (tickPositions[0] === tickPositions[1]) { + tickPositions.length = 1; + } + } + + this.tickPositions = tickPositions; + + // Run the tick positioner callback, that allows modifying auto tick + // positions. + if (tickPositioner) { + tickPositioner = tickPositioner.apply( + this, + [this.min, this.max] + ); + if (tickPositioner) { + this.tickPositions = tickPositions = tickPositioner; + } + } + + } + + // Reset min/max or remove extremes based on start/end on tick + this.paddedTicks = tickPositions.slice(0); // Used for logarithmic minor + this.trimTicks(tickPositions, startOnTick, endOnTick); + if (!this.isLinked) { + + // Substract half a unit (#2619, #2846, #2515, #3390), + // but not in case of multiple ticks (#6897) + if (this.single && tickPositions.length < 2) { + this.min -= 0.5; + this.max += 0.5; + } + if (!tickPositionsOption && !tickPositioner) { + this.adjustTickAmount(); + } + } + + fireEvent(this, 'afterSetTickPositions'); + }, + + /** + * Handle startOnTick and endOnTick by either adapting to padding min/max or + * rounded min/max. Also handle single data points. + * + * @private + */ + trimTicks: function (tickPositions, startOnTick, endOnTick) { + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1], + minPointOffset = this.minPointOffset || 0; + + if (!this.isLinked) { + if (startOnTick && roundedMin !== -Infinity) { // #6502 + this.min = roundedMin; + } else { + while (this.min - minPointOffset > tickPositions[0]) { + tickPositions.shift(); + } + } + + if (endOnTick) { + this.max = roundedMax; + } else { + while (this.max + minPointOffset < + tickPositions[tickPositions.length - 1]) { + tickPositions.pop(); + } + } + + // If no tick are left, set one tick in the middle (#3195) + if ( + tickPositions.length === 0 && + defined(roundedMin) && + !this.options.tickPositions + ) { + tickPositions.push((roundedMax + roundedMin) / 2); + } + } + }, + + /** + * Check if there are multiple axes in the same pane. + * + * @private + * @return {Boolean} + * True if there are other axes. + */ + alignToOthers: function () { + var others = {}, // Whether there is another axis to pair with this one + hasOther, + options = this.options; + + if ( + // Only if alignTicks is true + this.chart.options.chart.alignTicks !== false && + options.alignTicks !== false && + + // Disabled when startOnTick or endOnTick are false (#7604) + options.startOnTick !== false && + options.endOnTick !== false && + + // Don't try to align ticks on a log axis, they are not evenly + // spaced (#6021) + !this.isLog + ) { + each(this.chart[this.coll], function (axis) { + var otherOptions = axis.options, + horiz = axis.horiz, + key = [ + horiz ? otherOptions.left : otherOptions.top, + otherOptions.width, + otherOptions.height, + otherOptions.pane + ].join(','); + + + if (axis.series.length) { // #4442 + if (others[key]) { + hasOther = true; // #4201 + } else { + others[key] = 1; + } + } + }); + } + return hasOther; + }, + + /** + * Find the max ticks of either the x and y axis collection, and record it + * in `this.tickAmount`. + * + * @private + */ + getTickAmount: function () { + var options = this.options, + tickAmount = options.tickAmount, + tickPixelInterval = options.tickPixelInterval; + + if ( + !defined(options.tickInterval) && + this.len < tickPixelInterval && + !this.isRadial && + !this.isLog && + options.startOnTick && + options.endOnTick + ) { + tickAmount = 2; + } + + if (!tickAmount && this.alignToOthers()) { + // Add 1 because 4 tick intervals require 5 ticks (including first + // and last) + tickAmount = Math.ceil(this.len / tickPixelInterval) + 1; + } + + // For tick amounts of 2 and 3, compute five ticks and remove the + // intermediate ones. This prevents the axis from adding ticks that are + // too far away from the data extremes. + if (tickAmount < 4) { + this.finalTickAmt = tickAmount; + tickAmount = 5; + } + + this.tickAmount = tickAmount; + }, + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group. + * + * @private + */ + adjustTickAmount: function () { + var tickInterval = this.tickInterval, + tickPositions = this.tickPositions, + tickAmount = this.tickAmount, + finalTickAmt = this.finalTickAmt, + currentTickAmount = tickPositions && tickPositions.length, + threshold = pick(this.threshold, this.softThreshold ? 0 : null), + i, + len; + + if (this.hasData()) { + if (currentTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + + // Extend evenly for both sides unless we're on the + // threshold (#3965) + if ( + tickPositions.length % 2 || + this.min === threshold + ) { + // to the end + tickPositions.push(correctFloat( + tickPositions[tickPositions.length - 1] + + tickInterval + )); + } else { + // to the start + tickPositions.unshift(correctFloat( + tickPositions[0] - tickInterval + )); + } + } + this.transA *= (currentTickAmount - 1) / (tickAmount - 1); + this.min = tickPositions[0]; + this.max = tickPositions[tickPositions.length - 1]; + + // We have too many ticks, run second pass to try to reduce ticks + } else if (currentTickAmount > tickAmount) { + this.tickInterval *= 2; + this.setTickPositions(); + } + + // The finalTickAmt property is set in getTickAmount + if (defined(finalTickAmt)) { + i = len = tickPositions.length; + while (i--) { + if ( + // Remove every other tick + (finalTickAmt === 3 && i % 2 === 1) || + // Remove all but first and last + (finalTickAmt <= 2 && i > 0 && i < len - 1) + ) { + tickPositions.splice(i, 1); + } + } + this.finalTickAmt = undefined; + } + } + }, + + /** + * Set the scale based on data min and max, user set min and max or options. + * + * @private + */ + setScale: function () { + var axis = this, + isDirtyData, + isDirtyAxisLength; + + axis.oldMin = axis.min; + axis.oldMax = axis.max; + axis.oldAxisLength = axis.len; + + // set the new axisLength + axis.setAxisSize(); + isDirtyAxisLength = axis.len !== axis.oldAxisLength; + + // is there new data? + each(axis.series, function (series) { + if ( + series.isDirtyData || + series.isDirty || + // When x axis is dirty, we need new data extremes for y as well + series.xAxis.isDirty + ) { + isDirtyData = true; + } + }); + + // do we really need to go through all this? + if ( + isDirtyAxisLength || + isDirtyData || + axis.isLinked || + axis.forceRedraw || + axis.userMin !== axis.oldUserMin || + axis.userMax !== axis.oldUserMax || + axis.alignToOthers() + ) { + + if (axis.resetStacks) { + axis.resetStacks(); + } + + axis.forceRedraw = false; + + // get data extremes if needed + axis.getSeriesExtremes(); + + // get fixed positions based on tickInterval + axis.setTickInterval(); + + // record old values to decide whether a rescale is necessary later + // on (#540) + axis.oldUserMin = axis.userMin; + axis.oldUserMax = axis.userMax; + + // Mark as dirty if it is not already set to dirty and extremes have + // changed. #595. + if (!axis.isDirty) { + axis.isDirty = + isDirtyAxisLength || + axis.min !== axis.oldMin || + axis.max !== axis.oldMax; + } + } else if (axis.cleanStacks) { + axis.cleanStacks(); + } + + fireEvent(this, 'afterSetScale'); + }, + + /** + * Set the minimum and maximum of the axes after render time. If the + * `startOnTick` and `endOnTick` options are true, the minimum and maximum + * values are rounded off to the nearest tick. To prevent this, these + * options can be set to false before calling setExtremes. Also, setExtremes + * will not allow a range lower than the `minRange` option, which by default + * is the range of five points. + * + * @param {Number} [newMin] + * The new minimum value. + * @param {Number} [newMax] + * The new maximum value. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart or wait for an explicit call to + * {@link Highcharts.Chart#redraw} + * @param {AnimationOptions} [animation=true] + * Enable or modify animations. + * @param {Object} [eventArguments] + * Arguments to be accessed in event handler. + * + * @sample highcharts/members/axis-setextremes/ + * Set extremes from a button + * @sample highcharts/members/axis-setextremes-datetime/ + * Set extremes on a datetime axis + * @sample highcharts/members/axis-setextremes-off-ticks/ + * Set extremes off ticks + * @sample stock/members/axis-setextremes/ + * Set extremes in Highstock + * @sample maps/members/axis-setextremes/ + * Set extremes in Highmaps + */ + setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { + var axis = this, + chart = axis.chart; + + redraw = pick(redraw, true); // defaults to true + + each(axis.series, function (serie) { + delete serie.kdTree; + }); + + // Extend the arguments with min and max + eventArguments = extend(eventArguments, { + min: newMin, + max: newMax + }); + + // Fire the event + fireEvent(axis, 'setExtremes', eventArguments, function () { + + axis.userMin = newMin; + axis.userMax = newMax; + axis.eventArgs = eventArguments; + + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Overridable method for zooming chart. Pulled out in a separate method to + * allow overriding in stock charts. + * + * @private + */ + zoom: function (newMin, newMax) { + var dataMin = this.dataMin, + dataMax = this.dataMax, + options = this.options, + min = Math.min(dataMin, pick(options.min, dataMin)), + max = Math.max(dataMax, pick(options.max, dataMax)); + + if (newMin !== this.min || newMax !== this.max) { // #5790 + + // Prevent pinch zooming out of range. Check for defined is for + // #1946. #1734. + if (!this.allowZoomOutside) { + // #6014, sometimes newMax will be smaller than min (or newMin + // will be larger than max). + if (defined(dataMin)) { + if (newMin < min) { + newMin = min; + } + if (newMin > max) { + newMin = max; + } + } + if (defined(dataMax)) { + if (newMax < min) { + newMax = min; + } + if (newMax > max) { + newMax = max; + } + } + } + + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== undefined || newMax !== undefined; + + // Do it + this.setExtremes( + newMin, + newMax, + false, + undefined, + { trigger: 'zoom' } + ); + } + + return true; + }, + + /** + * Update the axis metrics. + * + * @private + */ + setAxisSize: function () { + var chart = this.chart, + options = this.options, + // [top, right, bottom, left] + offsets = options.offsets || [0, 0, 0, 0], + horiz = this.horiz, + + // Check for percentage based input values. Rounding fixes problems + // with column overflow and plot line filtering (#4898, #4899) + width = this.width = Math.round(H.relativeLength( + pick( + options.width, + chart.plotWidth - offsets[3] + offsets[1] + ), + chart.plotWidth + )), + height = this.height = Math.round(H.relativeLength( + pick( + options.height, + chart.plotHeight - offsets[0] + offsets[2] + ), + chart.plotHeight + )), + top = this.top = Math.round(H.relativeLength( + pick(options.top, chart.plotTop + offsets[0]), + chart.plotHeight, + chart.plotTop + )), + left = this.left = Math.round(H.relativeLength( + pick(options.left, chart.plotLeft + offsets[3]), + chart.plotWidth, + chart.plotLeft + )); + + // Expose basic values to use in Series object and navigator + this.bottom = chart.chartHeight - height - top; + this.right = chart.chartWidth - width - left; + + // Direction agnostic properties + this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905 + this.pos = horiz ? left : top; // distance from SVG origin + }, + + /** + * The returned object literal from the {@link Highcharts.Axis#getExtremes} + * function. + * + * @typedef {Object} Extremes + * @property {Number} dataMax + * The maximum value of the axis' associated series. + * @property {Number} dataMin + * The minimum value of the axis' associated series. + * @property {Number} max + * The maximum axis value, either automatic or set manually. If + * the `max` option is not set, `maxPadding` is 0 and `endOnTick` + * is false, this value will be the same as `dataMax`. + * @property {Number} min + * The minimum axis value, either automatic or set manually. If + * the `min` option is not set, `minPadding` is 0 and + * `startOnTick` is false, this value will be the same + * as `dataMin`. + */ + /** + * Get the current extremes for the axis. + * + * @returns {Extremes} + * An object containing extremes information. + * + * @sample highcharts/members/axis-getextremes/ + * Report extremes by click on a button + * @sample maps/members/axis-getextremes/ + * Get extremes in Highmaps + */ + getExtremes: function () { + var axis = this, + isLog = axis.isLog, + lin2log = axis.lin2log; + + return { + min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, + max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, + dataMin: axis.dataMin, + dataMax: axis.dataMax, + userMin: axis.userMin, + userMax: axis.userMax + }; + }, + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots. + * + * @param {Number} threshold + * The threshold in axis values. + * + * @return {Number} + * The translated threshold position in terms of pixels, and + * corrected to stay within the axis bounds. + */ + getThreshold: function (threshold) { + var axis = this, + isLog = axis.isLog, + lin2log = axis.lin2log, + realMin = isLog ? lin2log(axis.min) : axis.min, + realMax = isLog ? lin2log(axis.max) : axis.max; + + if (threshold === null) { + threshold = realMin; + } else if (realMin > threshold) { + threshold = realMin; + } else if (realMax < threshold) { + threshold = realMax; + } + + return axis.translate(threshold, 0, 1, 0, 1); + }, + + /** + * Compute auto alignment for the axis label based on which side the axis is + * on and the given rotation for the label. + * + * @param {Number} rotation + * The rotation in degrees as set by either the `rotation` or + * `autoRotation` options. + * @private + */ + autoLabelAlign: function (rotation) { + var ret, + angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + + if (angle > 15 && angle < 165) { + ret = 'right'; + } else if (angle > 195 && angle < 345) { + ret = 'left'; + } else { + ret = 'center'; + } + return ret; + }, + + /** + * Get the tick length and width for the axis based on axis options. + * + * @private + * + * @param {String} prefix + * 'tick' or 'minorTick' + * @return {Array.} + * An array of tickLength and tickWidth + */ + tickSize: function (prefix) { + var options = this.options, + tickLength = options[prefix + 'Length'], + tickWidth = pick( + options[prefix + 'Width'], + prefix === 'tick' && this.isXAxis ? 1 : 0 // X axis default 1 + ); + + if (tickWidth && tickLength) { + // Negate the length + if (options[prefix + 'Position'] === 'inside') { + tickLength = -tickLength; + } + return [tickLength, tickWidth]; + } + + }, + + /** + * Return the size of the labels. + * + * @private + */ + labelMetrics: function () { + var index = this.tickPositions && this.tickPositions[0] || 0; + return this.chart.renderer.fontMetrics( + this.options.labels.style && this.options.labels.style.fontSize, + this.ticks[index] && this.ticks[index].label + ); + }, + + /** + * Prevent the ticks from getting so close we can't draw the labels. On a + * horizontal axis, this is handled by rotating the labels, removing ticks + * and adding ellipsis. On a vertical axis remove ticks and add ellipsis. + * + * @private + */ + unsquish: function () { + var labelOptions = this.options.labels, + horiz = this.horiz, + tickInterval = this.tickInterval, + newTickInterval = tickInterval, + slotSize = this.len / ( + ((this.categories ? 1 : 0) + this.max - this.min) / tickInterval + ), + rotation, + rotationOption = labelOptions.rotation, + labelMetrics = this.labelMetrics(), + step, + bestScore = Number.MAX_VALUE, + autoRotation, + // Return the multiple of tickInterval that is needed to avoid + // collision + getStep = function (spaceNeeded) { + var step = spaceNeeded / (slotSize || 1); + step = step > 1 ? Math.ceil(step) : 1; + return step * tickInterval; + }; + + if (horiz) { + autoRotation = !labelOptions.staggerLines && + !labelOptions.step && + ( // #3971 + defined(rotationOption) ? + [rotationOption] : + slotSize < pick(labelOptions.autoRotationLimit, 80) && + labelOptions.autoRotation + ); + + if (autoRotation) { + + // Loop over the given autoRotation options, and determine + // which gives the best score. The best score is that with + // the lowest number of steps and a rotation closest + // to horizontal. + each(autoRotation, function (rot) { + var score; + + if ( + rot === rotationOption || + (rot && rot >= -90 && rot <= 90) + ) { // #3891 + + step = getStep( + Math.abs(labelMetrics.h / Math.sin(deg2rad * rot)) + ); + + score = step + Math.abs(rot / 360); + + if (score < bestScore) { + bestScore = score; + rotation = rot; + newTickInterval = step; + } + } + }); + } + + } else if (!labelOptions.step) { // #4411 + newTickInterval = getStep(labelMetrics.h); + } + + this.autoRotation = autoRotation; + this.labelRotation = pick(rotation, rotationOption); + + return newTickInterval; + }, + + /** + * Get the general slot width for labels/categories on this axis. This may + * change between the pre-render (from Axis.getOffset) and the final tick + * rendering and placement. + * + * @private + * @return {Number} + * The pixel width allocated to each axis label. + */ + getSlotWidth: function () { + // #5086, #1580, #1931 + var chart = this.chart, + horiz = this.horiz, + labelOptions = this.options.labels, + slotCount = Math.max( + this.tickPositions.length - (this.categories ? 0 : 1), + 1 + ), + marginLeft = chart.margin[3]; + + return ( + horiz && + (labelOptions.step || 0) < 2 && + !labelOptions.rotation && // #4415 + ((this.staggerLines || 1) * this.len) / slotCount + ) || ( + !horiz && ( + // #7028 + ( + labelOptions.style && + parseInt(labelOptions.style.width, 10) + ) || + ( + marginLeft && + (marginLeft - chart.spacing[3]) + ) || + chart.chartWidth * 0.33 + ) + ); + + }, + + /** + * Render the axis labels and determine whether ellipsis or rotation need + * to be applied. + * + * @private + */ + renderUnsquish: function () { + var chart = this.chart, + renderer = chart.renderer, + tickPositions = this.tickPositions, + ticks = this.ticks, + labelOptions = this.options.labels, + horiz = this.horiz, + slotWidth = this.getSlotWidth(), + innerWidth = Math.max( + 1, + Math.round(slotWidth - 2 * (labelOptions.padding || 5)) + ), + attr = {}, + labelMetrics = this.labelMetrics(), + textOverflowOption = labelOptions.style && + labelOptions.style.textOverflow, + commonWidth, + commonTextOverflow, + maxLabelLength = 0, + label, + i, + pos; + + // Set rotation option unless it is "auto", like in gauges + if (!isString(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation || 0; // #4443 + } + + // Get the longest label length + each(tickPositions, function (tick) { + tick = ticks[tick]; + if ( + tick && + tick.label && + tick.label.textPxLength > maxLabelLength + ) { + maxLabelLength = tick.label.textPxLength; + } + }); + this.maxLabelLength = maxLabelLength; + + + // Handle auto rotation on horizontal axis + if (this.autoRotation) { + + // Apply rotation only if the label is too wide for the slot, and + // the label is wider than its height. + if ( + maxLabelLength > innerWidth && + maxLabelLength > labelMetrics.h + ) { + attr.rotation = this.labelRotation; + } else { + this.labelRotation = 0; + } + + // Handle word-wrap or ellipsis on vertical axis + } else if (slotWidth) { + // For word-wrap or ellipsis + commonWidth = innerWidth; + + if (!textOverflowOption) { + commonTextOverflow = 'clip'; + + // On vertical axis, only allow word wrap if there is room + // for more lines. + i = tickPositions.length; + while (!horiz && i--) { + pos = tickPositions[i]; + label = ticks[pos].label; + if (label) { + // Reset ellipsis in order to get the correct + // bounding box (#4070) + if ( + label.styles && + label.styles.textOverflow === 'ellipsis' + ) { + label.css({ textOverflow: 'clip' }); + + // Set the correct width in order to read + // the bounding box height (#4678, #5034) + } else if (label.textPxLength > slotWidth) { + label.css({ width: slotWidth + 'px' }); + } + + if ( + label.getBBox().height > ( + this.len / tickPositions.length - + (labelMetrics.h - labelMetrics.f) + ) + ) { + label.specificTextOverflow = 'ellipsis'; + } + } + } + } + } + + + // Add ellipsis if the label length is significantly longer than ideal + if (attr.rotation) { + commonWidth = ( + maxLabelLength > chart.chartHeight * 0.5 ? + chart.chartHeight * 0.33 : + chart.chartHeight + ); + if (!textOverflowOption) { + commonTextOverflow = 'ellipsis'; + } + } + + // Set the explicit or automatic label alignment + this.labelAlign = labelOptions.align || + this.autoLabelAlign(this.labelRotation); + if (this.labelAlign) { + attr.align = this.labelAlign; + } + + // Apply general and specific CSS + each(tickPositions, function (pos) { + var tick = ticks[pos], + label = tick && tick.label, + css = {}; + if (label) { + // This needs to go before the CSS in old IE (#4502) + label.attr(attr); + + if ( + commonWidth && + !(labelOptions.style && labelOptions.style.width) && + ( + // Speed optimizing, #7656 + commonWidth < label.textPxLength || + // Resetting CSS, #4928 + label.element.tagName === 'SPAN' + ) + ) { + css.width = commonWidth; + if (!textOverflowOption) { + css.textOverflow = ( + label.specificTextOverflow || + commonTextOverflow + ); + } + label.css(css); + + } + delete label.specificTextOverflow; + tick.rotation = attr.rotation; + } + }); + + // Note: Why is this not part of getLabelPosition? + this.tickRotCorr = renderer.rotCorr( + labelMetrics.b, + this.labelRotation || 0, + this.side !== 0 + ); + }, + + /** + * Return true if the axis has associated data. + * + * @return {Boolean} + * True if the axis has associated visible series and those series + * have either valid data points or explicit `min` and `max` + * settings. + */ + hasData: function () { + return ( + this.hasVisibleSeries || + ( + defined(this.min) && + defined(this.max) && + this.tickPositions && + this.tickPositions.length > 0 + ) + ); + }, + + /** + * Adds the title defined in axis.options.title. + * @param {Boolean} display - whether or not to display the title + */ + addTitle: function (display) { + var axis = this, + renderer = axis.chart.renderer, + horiz = axis.horiz, + opposite = axis.opposite, + options = axis.options, + axisTitleOptions = options.title, + textAlign; + + if (!axis.axisTitle) { + textAlign = axisTitleOptions.textAlign; + if (!textAlign) { + textAlign = (horiz ? { + low: 'left', + middle: 'center', + high: 'right' + } : { + low: opposite ? 'right' : 'left', + middle: 'center', + high: opposite ? 'left' : 'right' + })[axisTitleOptions.align]; + } + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: textAlign + }) + .addClass('highcharts-axis-title') + /*= if (build.classic) { =*/ + // #7814, don't mutate style option + .css(merge(axisTitleOptions.style)) + /*= } =*/ + .add(axis.axisGroup); + axis.axisTitle.isNew = true; + } + + // Max width defaults to the length of the axis + /*= if (build.classic) { =*/ + if (!axisTitleOptions.style.width && !axis.isRadial) { + /*= } =*/ + axis.axisTitle.css({ + width: axis.len + }); + /*= if (build.classic) { =*/ + } + /*= } =*/ + + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[display ? 'show' : 'hide'](true); + }, + + /** + * Generates a tick for initial positioning. + * + * @private + * @param {number} pos + * The tick position in axis values. + * @param {number} i + * The index of the tick in {@link Axis.tickPositions}. + */ + generateTick: function (pos) { + var ticks = this.ticks; + + if (!ticks[pos]) { + ticks[pos] = new Tick(this, pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + }, + + /** + * Render the tick labels to a preliminary position to get their sizes. + * + * @private + */ + getOffset: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + tickPositions = axis.tickPositions, + ticks = axis.ticks, + horiz = axis.horiz, + side = axis.side, + invertedSide = chart.inverted && + !axis.isZAxis ? [1, 0, 3, 2][side] : side, + hasData, + showAxis, + titleOffset = 0, + titleOffsetOption, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + labelOffset = 0, // reset + labelOffsetPadded, + axisOffset = chart.axisOffset, + clipOffset = chart.clipOffset, + clip, + directionFactor = [-1, 1, 1, -1][side], + className = options.className, + axisParent = axis.axisParent, // Used in color axis + lineHeightCorrection, + tickSize = this.tickSize('tick'); + + // For reuse in Axis.render + hasData = axis.hasData(); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + + // Set/reset staggerLines + axis.staggerLines = axis.horiz && labelOptions.staggerLines; + + // Create the axisGroup and gridGroup elements on first iteration + if (!axis.axisGroup) { + axis.gridGroup = renderer.g('grid') + .attr({ zIndex: options.gridZIndex || 1 }) + .addClass( + 'highcharts-' + this.coll.toLowerCase() + '-grid ' + + (className || '') + ) + .add(axisParent); + axis.axisGroup = renderer.g('axis') + .attr({ zIndex: options.zIndex || 2 }) + .addClass( + 'highcharts-' + this.coll.toLowerCase() + ' ' + + (className || '') + ) + .add(axisParent); + axis.labelGroup = renderer.g('axis-labels') + .attr({ zIndex: labelOptions.zIndex || 7 }) + .addClass( + 'highcharts-' + axis.coll.toLowerCase() + '-labels ' + + (className || '') + ) + .add(axisParent); + } + + if (hasData || axis.isLinked) { + + // Generate ticks + each(tickPositions, function (pos, i) { + // i is not used here, but may be used in overrides + axis.generateTick(pos, i); + }); + + axis.renderUnsquish(); + + + // Left side must be align: right and right side must + // have align: left for labels + axis.reserveSpaceDefault = ( + side === 0 || + side === 2 || + { 1: 'left', 3: 'right' }[side] === axis.labelAlign + ); + if (pick( + labelOptions.reserveSpace, + axis.labelAlign === 'center' ? true : null, + axis.reserveSpaceDefault) + ) { + each(tickPositions, function (pos) { + // get the highest offset + labelOffset = Math.max( + ticks[pos].getLabelSize(), + labelOffset + ); + }); + } + + if (axis.staggerLines) { + labelOffset *= axis.staggerLines; + } + axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1); + + } else { // doesn't have data + objectEach(ticks, function (tick, n) { + tick.destroy(); + delete ticks[n]; + }); + } + + if ( + axisTitleOptions && + axisTitleOptions.text && + axisTitleOptions.enabled !== false + ) { + axis.addTitle(showAxis); + + if (showAxis && axisTitleOptions.reserveSpace !== false) { + axis.titleOffset = titleOffset = + axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleOffsetOption = axisTitleOptions.offset; + titleMargin = defined(titleOffsetOption) ? + 0 : + pick(axisTitleOptions.margin, horiz ? 5 : 10); + } + } + + // Render the axis line + axis.renderLine(); + + // handle automatic or user set offset + axis.offset = directionFactor * pick(options.offset, axisOffset[side]); + + axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar + if (side === 0) { + lineHeightCorrection = -axis.labelMetrics().h; + } else if (side === 2) { + lineHeightCorrection = axis.tickRotCorr.y; + } else { + lineHeightCorrection = 0; + } + + // Find the padded label offset + labelOffsetPadded = Math.abs(labelOffset) + titleMargin; + if (labelOffset) { + labelOffsetPadded -= lineHeightCorrection; + labelOffsetPadded += directionFactor * ( + horiz ? + pick( + labelOptions.y, + axis.tickRotCorr.y + directionFactor * 8 + ) : + labelOptions.x + ); + } + + axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); + + axisOffset[side] = Math.max( + axisOffset[side], + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, + labelOffsetPadded, // #3027 + hasData && tickPositions.length && tickSize ? + tickSize[0] + directionFactor * axis.offset : + 0 // #4866 + ); + + // Decide the clipping needed to keep the graph inside + // the plot area and axis lines + clip = options.offset ? + 0 : + Math.floor(axis.axisLine.strokeWidth() / 2) * 2; // #4308, #4371 + clipOffset[invertedSide] = Math.max(clipOffset[invertedSide], clip); + }, + + /** + * Internal function to get the path for the axis line. Extended for polar + * charts. + * + * @param {Number} lineWidth + * The line width in pixels. + * @return {Array} + * The SVG path definition in array form. + */ + getLinePath: function (lineWidth) { + var chart = this.chart, + opposite = this.opposite, + offset = this.offset, + horiz = this.horiz, + lineLeft = this.left + (opposite ? this.width : 0) + offset, + lineTop = chart.chartHeight - this.bottom - + (opposite ? this.height : 0) + offset; + + if (opposite) { + lineWidth *= -1; // crispify the other way - #1480, #1687 + } + + return chart.renderer + .crispLine([ + 'M', + horiz ? + this.left : + lineLeft, + horiz ? + lineTop : + this.top, + 'L', + horiz ? + chart.chartWidth - this.right : + lineLeft, + horiz ? + lineTop : + chart.chartHeight - this.bottom + ], lineWidth); + }, + + /** + * Render the axis line. Called internally when rendering and redrawing the + * axis. + */ + renderLine: function () { + if (!this.axisLine) { + this.axisLine = this.chart.renderer.path() + .addClass('highcharts-axis-line') + .add(this.axisGroup); + + /*= if (build.classic) { =*/ + this.axisLine.attr({ + stroke: this.options.lineColor, + 'stroke-width': this.options.lineWidth, + zIndex: 7 + }); + /*= } =*/ + } + }, + + /** + * Position the axis title. + * + * @private + * + * @return {Object} + * X and Y positions for the title. + */ + getTitlePosition: function () { + // compute anchor points for each of the title align options + var horiz = this.horiz, + axisLeft = this.left, + axisTop = this.top, + axisLength = this.len, + axisTitleOptions = this.options.title, + margin = horiz ? axisLeft : axisTop, + opposite = this.opposite, + offset = this.offset, + xOption = axisTitleOptions.x || 0, + yOption = axisTitleOptions.y || 0, + axisTitle = this.axisTitle, + fontMetrics = this.chart.renderer.fontMetrics( + axisTitleOptions.style && axisTitleOptions.style.fontSize, + axisTitle + ), + // The part of a multiline text that is below the baseline of the + // first line. Subtract 1 to preserve pixel-perfectness from the + // old behaviour (v5.0.12), where only one line was allowed. + textHeightOvershoot = Math.max( + axisTitle.getBBox(null, 0).height - fontMetrics.h - 1, + 0 + ), + + // the position in the length direction of the axis + alongAxis = { + low: margin + (horiz ? 0 : axisLength), + middle: margin + axisLength / 2, + high: margin + (horiz ? axisLength : 0) + }[axisTitleOptions.align], + + // the position in the perpendicular direction of the axis + offAxis = (horiz ? axisTop + this.height : axisLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + this.axisTitleMargin + + [ + -textHeightOvershoot, // top + textHeightOvershoot, // right + fontMetrics.f, // bottom + -textHeightOvershoot // left + ][this.side]; + + + return { + x: horiz ? + alongAxis + xOption : + offAxis + (opposite ? this.width : 0) + offset + xOption, + y: horiz ? + offAxis + yOption - (opposite ? this.height : 0) + offset : + alongAxis + yOption + }; + }, + + /** + * Render a minor tick into the given position. If a minor tick already + * exists in this position, move it. + * + * @param {number} pos + * The position in axis values. + */ + renderMinorTick: function (pos) { + var slideInTicks = this.chart.hasRendered && isNumber(this.oldMin), + minorTicks = this.minorTicks; + + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(this, pos, 'minor'); + } + + // Render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } + + minorTicks[pos].render(null, false, 1); + }, + + /** + * Render a major tick into the given position. If a tick already exists + * in this position, move it. + * + * @param {number} pos + * The position in axis values. + * @param {number} i + * The tick index. + */ + renderTick: function (pos, i) { + var isLinked = this.isLinked, + ticks = this.ticks, + slideInTicks = this.chart.hasRendered && isNumber(this.oldMin); + + // Linked axes need an extra check to find out if + if (!isLinked || (pos >= this.min && pos <= this.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(this, pos); + } + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true, 0.1); + } + + ticks[pos].render(i); + } + }, + + /** + * Render the axis. + * + * @private + */ + render: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + isLog = axis.isLog, + lin2log = axis.lin2log, + isLinked = axis.isLinked, + tickPositions = axis.tickPositions, + axisTitle = axis.axisTitle, + ticks = axis.ticks, + minorTicks = axis.minorTicks, + alternateBands = axis.alternateBands, + stackLabelOptions = options.stackLabels, + alternateGridColor = options.alternateGridColor, + tickmarkOffset = axis.tickmarkOffset, + axisLine = axis.axisLine, + showAxis = axis.showAxis, + animation = animObject(renderer.globalAnimation), + from, + to; + + // Reset + axis.labelEdge.length = 0; + axis.overlap = false; + + // Mark all elements inActive before we go over and mark the active ones + each([ticks, minorTicks, alternateBands], function (coll) { + objectEach(coll, function (tick) { + tick.isActive = false; + }); + }); + + // If the series has data draw the ticks. Else only the line and title + if (axis.hasData() || isLinked) { + + // minor ticks + if (axis.minorTickInterval && !axis.categories) { + each(axis.getMinorTickPositions(), function (pos) { + axis.renderMinorTick(pos); + }); + } + + // Major ticks. Pull out the first item and render it last so that + // we can get the position of the neighbour label. #808. + if (tickPositions.length) { // #1300 + each(tickPositions, function (pos, i) { + axis.renderTick(pos, i); + }); + // In a categorized axis, the tick marks are displayed + // between labels. So we need to add a tick mark and + // grid line at the left edge of the X axis. + if (tickmarkOffset && (axis.min === 0 || axis.single)) { + if (!ticks[-1]) { + ticks[-1] = new Tick(axis, -1, null, true); + } + ticks[-1].render(-1); + } + + } + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function (pos, i) { + to = tickPositions[i + 1] !== undefined ? + tickPositions[i + 1] + tickmarkOffset : + axis.max - tickmarkOffset; + + if ( + i % 2 === 0 && + pos < axis.max && + to <= axis.max + ( + chart.polar ? + -tickmarkOffset : + tickmarkOffset + ) + ) { // #2248, #4660 + if (!alternateBands[pos]) { + alternateBands[pos] = new H.PlotLineOrBand(axis); + } + from = pos + tickmarkOffset; // #949 + alternateBands[pos].options = { + from: isLog ? lin2log(from) : from, + to: isLog ? lin2log(to) : to, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot lines and bands + if (!axis._addedPlotLB) { // only first time + each( + (options.plotLines || []).concat(options.plotBands || []), + function (plotLineOptions) { + axis.addPlotBandOrLine(plotLineOptions); + } + ); + axis._addedPlotLB = true; + } + + } // end if hasData + + // Remove inactive ticks + each([ticks, minorTicks, alternateBands], function (coll) { + var i, + forDestruction = [], + delay = animation.duration, + destroyInactiveItems = function () { + i = forDestruction.length; + while (i--) { + // When resizing rapidly, the same items + // may be destroyed in different timeouts, + // or the may be reactivated + if ( + coll[forDestruction[i]] && + !coll[forDestruction[i]].isActive + ) { + coll[forDestruction[i]].destroy(); + delete coll[forDestruction[i]]; + } + } + + }; + + objectEach(coll, function (tick, pos) { + if (!tick.isActive) { + // Render to zero opacity + tick.render(pos, false, 0); + tick.isActive = false; + forDestruction.push(pos); + } + }); + + // When the objects are finished fading out, destroy them + syncTimeout( + destroyInactiveItems, + coll === alternateBands || + !chart.hasRendered || + !delay ? + 0 : + delay + ); + }); + + // Set the axis line path + if (axisLine) { + axisLine[axisLine.isPlaced ? 'animate' : 'attr']({ + d: this.getLinePath(axisLine.strokeWidth()) + }); + axisLine.isPlaced = true; + + // Show or hide the line depending on options.showEmpty + axisLine[showAxis ? 'show' : 'hide'](true); + } + + if (axisTitle && showAxis) { + var titleXy = axis.getTitlePosition(); + if (isNumber(titleXy.y)) { + axisTitle[axisTitle.isNew ? 'attr' : 'animate'](titleXy); + axisTitle.isNew = false; + } else { + axisTitle.attr('y', -9999); + axisTitle.isNew = true; + } + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + axis.renderStackTotals(); + } + // End stacked totals + + axis.isDirty = false; + + fireEvent(this, 'afterRender'); + }, + + /** + * Redraw the axis to reflect changes in the data or axis extremes. Called + * internally from {@link Chart#redraw}. + * + * @private + */ + redraw: function () { + + if (this.visible) { + // render the axis + this.render(); + + // move plot lines and bands + each(this.plotLinesAndBands, function (plotLine) { + plotLine.render(); + }); + } + + // mark associated series as dirty and ready for redraw + each(this.series, function (series) { + series.isDirty = true; + }); + + }, + + // Properties to survive after destroy, needed for Axis.update (#4317, + // #5773, #5881). + keepProps: ['extKey', 'hcEvents', 'names', 'series', 'userMax', 'userMin'], + + /** + * Destroys an Axis instance. See {@link Axis#remove} for the API endpoint + * to fully remove the axis. + * + * @private + * @param {Boolean} keepEvents + * Whether to preserve events, used internally in Axis.update. + */ + destroy: function (keepEvents) { + var axis = this, + stacks = axis.stacks, + plotLinesAndBands = axis.plotLinesAndBands, + plotGroup, + i; + + fireEvent(this, 'destroy', { keepEvents: keepEvents }); + + // Remove the events + if (!keepEvents) { + removeEvent(axis); + } + + // Destroy each stack total + objectEach(stacks, function (stack, stackKey) { + destroyObjectProperties(stack); + + stacks[stackKey] = null; + }); + + // Destroy collections + each( + [axis.ticks, axis.minorTicks, axis.alternateBands], + function (coll) { + destroyObjectProperties(coll); + } + ); + if (plotLinesAndBands) { + i = plotLinesAndBands.length; + while (i--) { // #1975 + plotLinesAndBands[i].destroy(); + } + } + + // Destroy local variables + each( + ['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', + 'gridGroup', 'labelGroup', 'cross'], + function (prop) { + if (axis[prop]) { + axis[prop] = axis[prop].destroy(); + } + } + ); + + // Destroy each generated group for plotlines and plotbands + for (plotGroup in axis.plotLinesAndBandsGroups) { + axis.plotLinesAndBandsGroups[plotGroup] = + axis.plotLinesAndBandsGroups[plotGroup].destroy(); + } + + // Delete all properties and fall back to the prototype. + objectEach(axis, function (val, key) { + if (inArray(key, axis.keepProps) === -1) { + delete axis[key]; + } + }); + }, + + /** + * Internal function to draw a crosshair. + * + * @param {PointerEvent} [e] + * The event arguments from the modified pointer event, extended + * with `chartX` and `chartY` + * @param {Point} [point] + * The Point object if the crosshair snaps to points. + */ + drawCrosshair: function (e, point) { + + var path, + options = this.crosshair, + snap = pick(options.snap, true), + pos, + categorized, + graphic = this.cross; + + fireEvent(this, 'drawCrosshair', { e: e, point: point }); + + // Use last available event when updating non-snapped crosshairs without + // mouse interaction (#5287) + if (!e) { + e = this.cross && this.cross.e; + } + + if ( + // Disabled in options + !this.crosshair || + // Snap + ((defined(point) || !snap) === false) + ) { + this.hideCrosshair(); + } else { + + // Get the path + if (!snap) { + pos = e && + ( + this.horiz ? + e.chartX - this.pos : + this.len - e.chartY + this.pos + ); + } else if (defined(point)) { + // #3834 + pos = pick( + point.crosshairPos, // 3D axis extension + this.isXAxis ? point.plotX : this.len - point.plotY + ); + } + + if (defined(pos)) { + path = this.getPlotLinePath( + // First argument, value, only used on radial + point && (this.isXAxis ? + point.x : + pick(point.stackY, point.y) + ), + null, + null, + null, + pos // Translated position + ) || null; // #3189 + } + + if (!defined(path)) { + this.hideCrosshair(); + return; + } + + categorized = this.categories && !this.isRadial; + + // Draw the cross + if (!graphic) { + this.cross = graphic = this.chart.renderer + .path() + .addClass( + 'highcharts-crosshair highcharts-crosshair-' + + (categorized ? 'category ' : 'thin ') + + options.className + ) + .attr({ + zIndex: pick(options.zIndex, 2) + }) + .add(); + + /*= if (build.classic) { =*/ + // Presentational attributes + graphic.attr({ + 'stroke': options.color || + ( + categorized ? + color('${palette.highlightColor20}') + .setOpacity(0.25).get() : + '${palette.neutralColor20}' + ), + 'stroke-width': pick(options.width, 1) + }).css({ + 'pointer-events': 'none' + }); + if (options.dashStyle) { + graphic.attr({ + dashstyle: options.dashStyle + }); + } + /*= } =*/ + + } + + graphic.show().attr({ + d: path + }); + + if (categorized && !options.width) { + graphic.attr({ + 'stroke-width': this.transA + }); + } + this.cross.e = e; + } + + fireEvent(this, 'afterDrawCrosshair', { e: e, point: point }); + }, + + /** + * Hide the crosshair if visible. + */ + hideCrosshair: function () { + if (this.cross) { + this.cross.hide(); + } + } }); // end Axis H.Axis = Axis; diff --git a/js/parts/BarSeries.js b/js/parts/BarSeries.js index 44c194b08a8..679d96a1aa2 100644 --- a/js/parts/BarSeries.js +++ b/js/parts/BarSeries.js @@ -14,7 +14,7 @@ var seriesType = H.seriesType; * The Bar series class */ seriesType('bar', 'column', null, { - inverted: true + inverted: true }); /** * A bar series is a special type of column series where the columns are @@ -31,7 +31,7 @@ seriesType('bar', 'column', null, { /** * A `bar` series. If the [type](#series.bar.type) option is not specified, * it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.bar * @excluding connectNulls,dashStyle,dataParser,dataURL,gapSize,gapUnit,linecap, @@ -43,21 +43,21 @@ seriesType('bar', 'column', null, { /** * An array of data points for the series. For the `bar` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 5], @@ -65,12 +65,12 @@ seriesType('bar', 'column', null, { * [2, 3] * ] * ``` - * + * * 3. 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.bar.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -84,7 +84,7 @@ seriesType('bar', 'column', null, { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.column.data * @sample {highcharts} highcharts/chart/reflow-true/ @@ -115,7 +115,7 @@ seriesType('bar', 'column', null, { /** * Alignment of the data label relative to the data point. - * + * * @type {String} * @sample {highcharts} * highcharts/plotoptions/bar-datalabels-align-inside-bar/ @@ -127,7 +127,7 @@ seriesType('bar', 'column', null, { /** * The x position of the data label relative to the data point. - * + * * @type {Number} * @sample {highcharts} * highcharts/plotoptions/bar-datalabels-align-inside-bar/ diff --git a/js/parts/CandlestickSeries.js b/js/parts/CandlestickSeries.js index 8d110d0c11d..17fe3423eaa 100644 --- a/js/parts/CandlestickSeries.js +++ b/js/parts/CandlestickSeries.js @@ -8,112 +8,112 @@ import H from './Globals.js'; import './Utilities.js'; var defaultPlotOptions = H.defaultPlotOptions, - each = H.each, - merge = H.merge, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + each = H.each, + 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} * @excluding borderColor,borderRadius,borderWidth * @product highstock * @optionparent plotOptions.candlestick */ var candlestickOptions = { - - /** - * 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 - */ - - /** - * @default ohlc - * @apioption plotOptions.candlestick.dataGrouping.approximation - */ - - states: { - - /** - * @extends plotOptions.column.states.hover - * @product highstock - */ - hover: { - - /** - * 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, - /*= if (build.classic) { =*/ - - /** - * The color of the line/border of the candlestick. - * - * 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 - * @default #000000 - * @product highstock - */ - lineColor: '${palette.neutralColor100}', - - /** - * The pixel width of the candlestick line/border. Defaults to `1`. - * - * - * In styled mode, the line stroke width can be set with the - * `.highcharts-candlestick-series .highcahrts-point` rule. - * - * @type {Number} - * @default 1 - * @product highstock - */ - lineWidth: 1, - - /** - * The fill color of the candlestick when values are rising. - * - * 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: '${palette.backgroundColor}', - /*= } =*/ - - 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 + */ + + /** + * @default ohlc + * @apioption plotOptions.candlestick.dataGrouping.approximation + */ + + states: { + + /** + * @extends plotOptions.column.states.hover + * @product highstock + */ + hover: { + + /** + * 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, + /*= if (build.classic) { =*/ + + /** + * The color of the line/border of the candlestick. + * + * 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 + * @default #000000 + * @product highstock + */ + lineColor: '${palette.neutralColor100}', + + /** + * The pixel width of the candlestick line/border. Defaults to `1`. + * + * + * In styled mode, the line stroke width can be set with the + * `.highcharts-candlestick-series .highcahrts-point` rule. + * + * @type {Number} + * @default 1 + * @product highstock + */ + lineWidth: 1, + + /** + * The fill color of the candlestick when values are rising. + * + * 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: '${palette.backgroundColor}', + /*= } =*/ + + stickyTracking: true }; @@ -124,120 +124,120 @@ var candlestickOptions = { * @augments seriesTypes.ohlc */ seriesType('candlestick', 'ohlc', merge( - defaultPlotOptions.column, - candlestickOptions + defaultPlotOptions.column, + candlestickOptions ), /** @lends seriesTypes.candlestick */ { - /*= if (build.classic) { =*/ - /** - * Postprocess mapping between options and SVG attributes - */ - pointAttribs: function (point, state) { - var attribs = seriesTypes.column.prototype.pointAttribs.call(this, point, state), - options = this.options, - isUp = point.open < point.close, - stroke = options.lineColor || this.color, - stateOptions; - - attribs['stroke-width'] = options.lineWidth; - - attribs.fill = point.options.color || (isUp ? (options.upColor || this.color) : this.color); - attribs.stroke = point.lineColor || (isUp ? (options.upLineColor || stroke) : stroke); - - // Select or hover states - if (state) { - stateOptions = options.states[state]; - attribs.fill = stateOptions.color || attribs.fill; - attribs.stroke = stateOptions.lineColor || attribs.stroke; - attribs['stroke-width'] = - stateOptions.lineWidth || attribs['stroke-width']; - } - - - return attribs; - }, - /*= } =*/ - /** - * Draw the data points - */ - drawPoints: function () { - var series = this, - points = series.points, - chart = series.chart; - - - each(points, function (point) { - - var graphic = point.graphic, - plotOpen, - plotClose, - topBox, - bottomBox, - hasTopWhisker, - hasBottomWhisker, - crispCorr, - crispX, - path, - halfWidth, - isNew = !graphic; - - if (point.plotY !== undefined) { - - if (!graphic) { - point.graphic = graphic = chart.renderer.path() - .add(series.group); - } - - /*= if (build.classic) { =*/ - graphic - .attr(series.pointAttribs(point, point.selected && 'select')) // #3897 - .shadow(series.options.shadow); - /*= } =*/ - - // Crisp vector coordinates - crispCorr = (graphic.strokeWidth() % 2) / 2; - crispX = Math.round(point.plotX) - crispCorr; // #2596 - plotOpen = point.plotOpen; - plotClose = point.plotClose; - topBox = Math.min(plotOpen, plotClose); - bottomBox = Math.max(plotOpen, plotClose); - halfWidth = Math.round(point.shapeArgs.width / 2); - hasTopWhisker = Math.round(topBox) !== Math.round(point.plotHigh); - hasBottomWhisker = bottomBox !== point.yBottom; - topBox = Math.round(topBox) + crispCorr; - bottomBox = Math.round(bottomBox) + crispCorr; - - // Create the path. Due to a bug in Chrome 49, the path is first instanciated - // with no values, then the values pushed. For unknown reasons, instanciated - // the path array with all the values would lead to a crash when updating - // frequently (#5193). - path = []; - path.push( - 'M', - crispX - halfWidth, bottomBox, - 'L', - crispX - halfWidth, topBox, - 'L', - crispX + halfWidth, topBox, - 'L', - crispX + halfWidth, bottomBox, - 'Z', // Use a close statement to ensure a nice rectangle #2602 - 'M', - crispX, topBox, - 'L', - crispX, hasTopWhisker ? Math.round(point.plotHigh) : topBox, // #460, #2094 - 'M', - crispX, bottomBox, - 'L', - crispX, hasBottomWhisker ? Math.round(point.yBottom) : bottomBox // #460, #2094 - ); - - graphic[isNew ? 'attr' : 'animate']({ d: path }) - .addClass(point.getClassName(), true); - - } - }); - - } + /*= if (build.classic) { =*/ + /** + * Postprocess mapping between options and SVG attributes + */ + pointAttribs: function (point, state) { + var attribs = seriesTypes.column.prototype.pointAttribs.call(this, point, state), + options = this.options, + isUp = point.open < point.close, + stroke = options.lineColor || this.color, + stateOptions; + + attribs['stroke-width'] = options.lineWidth; + + attribs.fill = point.options.color || (isUp ? (options.upColor || this.color) : this.color); + attribs.stroke = point.lineColor || (isUp ? (options.upLineColor || stroke) : stroke); + + // Select or hover states + if (state) { + stateOptions = options.states[state]; + attribs.fill = stateOptions.color || attribs.fill; + attribs.stroke = stateOptions.lineColor || attribs.stroke; + attribs['stroke-width'] = + stateOptions.lineWidth || attribs['stroke-width']; + } + + + return attribs; + }, + /*= } =*/ + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart; + + + each(points, function (point) { + + var graphic = point.graphic, + plotOpen, + plotClose, + topBox, + bottomBox, + hasTopWhisker, + hasBottomWhisker, + crispCorr, + crispX, + path, + halfWidth, + isNew = !graphic; + + if (point.plotY !== undefined) { + + if (!graphic) { + point.graphic = graphic = chart.renderer.path() + .add(series.group); + } + + /*= if (build.classic) { =*/ + graphic + .attr(series.pointAttribs(point, point.selected && 'select')) // #3897 + .shadow(series.options.shadow); + /*= } =*/ + + // Crisp vector coordinates + crispCorr = (graphic.strokeWidth() % 2) / 2; + crispX = Math.round(point.plotX) - crispCorr; // #2596 + plotOpen = point.plotOpen; + plotClose = point.plotClose; + topBox = Math.min(plotOpen, plotClose); + bottomBox = Math.max(plotOpen, plotClose); + halfWidth = Math.round(point.shapeArgs.width / 2); + hasTopWhisker = Math.round(topBox) !== Math.round(point.plotHigh); + hasBottomWhisker = bottomBox !== point.yBottom; + topBox = Math.round(topBox) + crispCorr; + bottomBox = Math.round(bottomBox) + crispCorr; + + // Create the path. Due to a bug in Chrome 49, the path is first instanciated + // with no values, then the values pushed. For unknown reasons, instanciated + // the path array with all the values would lead to a crash when updating + // frequently (#5193). + path = []; + path.push( + 'M', + crispX - halfWidth, bottomBox, + 'L', + crispX - halfWidth, topBox, + 'L', + crispX + halfWidth, topBox, + 'L', + crispX + halfWidth, bottomBox, + 'Z', // Use a close statement to ensure a nice rectangle #2602 + 'M', + crispX, topBox, + 'L', + crispX, hasTopWhisker ? Math.round(point.plotHigh) : topBox, // #460, #2094 + 'M', + crispX, bottomBox, + 'L', + crispX, hasBottomWhisker ? Math.round(point.yBottom) : bottomBox // #460, #2094 + ); + + graphic[isNew ? 'attr' : 'animate']({ d: path }) + .addClass(point.getClassName(), true); + + } + }); + + } }); @@ -246,7 +246,7 @@ seriesType('candlestick', 'ohlc', merge( * A `candlestick` series. If the [type](#series.candlestick.type) * option is not specified, it is inherited from [chart.type]( * #chart.type). - * + * * @type {Object} * @extends series,plotOptions.candlestick * @excluding dataParser,dataURL @@ -257,7 +257,7 @@ seriesType('candlestick', 'ohlc', merge( /** * 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. @@ -265,7 +265,7 @@ seriesType('candlestick', 'ohlc', merge( * 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], @@ -273,12 +273,12 @@ seriesType('candlestick', 'ohlc', merge( * [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, @@ -298,7 +298,7 @@ seriesType('candlestick', 'ohlc', merge( * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.ohlc.data * @excluding y diff --git a/js/parts/Chart.js b/js/parts/Chart.js index 0d461a76ce8..cd8fc30e36a 100644 --- a/js/parts/Chart.js +++ b/js/parts/Chart.js @@ -3,7 +3,7 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from './Globals.js'; import './Utilities.js'; @@ -12,37 +12,37 @@ import './Legend.js'; import './Options.js'; import './Pointer.js'; var addEvent = H.addEvent, - animate = H.animate, - animObject = H.animObject, - attr = H.attr, - doc = H.doc, - Axis = H.Axis, // @todo add as requirement - createElement = H.createElement, - defaultOptions = H.defaultOptions, - discardElement = H.discardElement, - charts = H.charts, - css = H.css, - defined = H.defined, - each = H.each, - extend = H.extend, - find = H.find, - fireEvent = H.fireEvent, - grep = H.grep, - isNumber = H.isNumber, - isObject = H.isObject, - isString = H.isString, - Legend = H.Legend, // @todo add as requirement - marginNames = H.marginNames, - merge = H.merge, - objectEach = H.objectEach, - Pointer = H.Pointer, // @todo add as requirement - pick = H.pick, - pInt = H.pInt, - removeEvent = H.removeEvent, - seriesTypes = H.seriesTypes, - splat = H.splat, - syncTimeout = H.syncTimeout, - win = H.win; + animate = H.animate, + animObject = H.animObject, + attr = H.attr, + doc = H.doc, + Axis = H.Axis, // @todo add as requirement + createElement = H.createElement, + defaultOptions = H.defaultOptions, + discardElement = H.discardElement, + charts = H.charts, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + find = H.find, + fireEvent = H.fireEvent, + grep = H.grep, + isNumber = H.isNumber, + isObject = H.isObject, + isString = H.isString, + Legend = H.Legend, // @todo add as requirement + marginNames = H.marginNames, + merge = H.merge, + objectEach = H.objectEach, + Pointer = H.Pointer, // @todo add as requirement + pick = H.pick, + pInt = H.pInt, + removeEvent = H.removeEvent, + seriesTypes = H.seriesTypes, + splat = H.splat, + syncTimeout = H.syncTimeout, + win = H.win; /** * The Chart class. The recommended constructor is {@link Highcharts#chart}. * @class Highcharts.Chart @@ -58,20 +58,20 @@ var addEvent = H.addEvent, * * @example * var chart = Highcharts.chart('container', { - * title: { - * text: 'My chart' - * }, - * series: [{ - * data: [1, 3, 2, 4] - * }] + * title: { + * text: 'My chart' + * }, + * series: [{ + * data: [1, 3, 2, 4] + * }] * }) */ var Chart = H.Chart = function () { - this.getArgs.apply(this, arguments); + this.getArgs.apply(this, arguments); }; /** - * Factory function for basic charts. + * Factory function for basic charts. * * @function #chart * @memberOf Highcharts @@ -96,1972 +96,1972 @@ var Chart = H.Chart = function () { * }); */ H.chart = function (a, b, c) { - return new Chart(a, b, c); + return new Chart(a, b, c); }; extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { - // Hook for adding callbacks in modules - callbacks: [], - - /** - * Handle the arguments passed to the constructor. - * - * @private - * @returns {Array} Arguments without renderTo - */ - getArgs: function () { - var args = [].slice.call(arguments); - - // Remove the optional first argument, renderTo, and - // set it on this. - if (isString(args[0]) || args[0].nodeName) { - this.renderTo = args.shift(); - } - this.init(args[0], args[1]); - }, - - /** - * Overridable function that initializes the chart. The constructor's - * arguments are passed on directly. - */ - init: function (userOptions, callback) { - - // Handle regular options - var options, - type, - // skip merging data points to increase performance - seriesOptions = userOptions.series, - userPlotOptions = userOptions.plotOptions || {}; - - // Fire the event with a default function - fireEvent(this, 'init', { args: arguments }, function () { - - userOptions.series = null; - options = merge(defaultOptions, userOptions); // do the merge - - // Override (by copy of user options) or clear tooltip options - // in chart.options.plotOptions (#6218) - for (type in options.plotOptions) { - options.plotOptions[type].tooltip = ( - userPlotOptions[type] && - merge(userPlotOptions[type].tooltip) // override by copy - ) || undefined; // or clear - } - // User options have higher priority than default options - // (#6218). In case of exporting: path is changed - options.tooltip.userOptions = ( - userOptions.chart && - userOptions.chart.forExport && - userOptions.tooltip.userOptions - ) || userOptions.tooltip; - - // set back the series data - options.series = userOptions.series = seriesOptions; - this.userOptions = userOptions; - - var optionsChart = options.chart; - - var chartEvents = optionsChart.events; - - this.margin = []; - this.spacing = []; - - // Pixel data bounds for touch zoom - this.bounds = { h: {}, v: {} }; - - // An array of functions that returns labels that should be - // considered for anti-collision - this.labelCollectors = []; - - this.callback = callback; - this.isResizing = 0; - - /** - * The options structure for the chart. It contains members for - * the sub elements like series, legend, tooltip etc. - * - * @memberof Highcharts.Chart - * @name options - * @type {Options} - */ - this.options = options; - /** - * All the axes in the chart. - * - * @memberof Highcharts.Chart - * @name axes - * @see Highcharts.Chart.xAxis - * @see Highcharts.Chart.yAxis - * @type {Array.} - */ - this.axes = []; - - /** - * All the current series in the chart. - * - * @memberof Highcharts.Chart - * @name series - * @type {Array.} - */ - this.series = []; - - /** - * The chart title. The title has an `update` method that allows - * modifying the options directly or indirectly via - * `chart.update`. - * - * @memberof Highcharts.Chart - * @name title - * @type Object - * - * @sample highcharts/members/title-update/ - * Updating titles - */ - - /** - * The chart subtitle. The subtitle has an `update` method that - * allows modifying the options directly or indirectly via - * `chart.update`. - * - * @memberof Highcharts.Chart - * @name subtitle - * @type Object - */ - - /** - * The `Time` object associated with the chart. Since v6.0.5, - * time settings can be applied individually for each chart. If - * no individual settings apply, the `Time` object is shared by - * all instances. - * - * @memberof Highcharts.Chart - * @name time - * @type Highcharts.Time - */ - this.time = - userOptions.time && H.keys(userOptions.time).length ? - new H.Time(userOptions.time) : - H.time; - - - this.hasCartesianSeries = optionsChart.showAxes; - - var chart = this; - - // Add the chart to the global lookup - chart.index = charts.length; - - charts.push(chart); - H.chartCount++; - - // Chart event handlers - if (chartEvents) { - objectEach(chartEvents, function (event, eventType) { - addEvent(chart, eventType, event); - }); - } - - /** - * A collection of the X axes in the chart. - * @type {Array.} - * @name xAxis - * @memberOf Highcharts.Chart - */ - chart.xAxis = []; - /** - * A collection of the Y axes in the chart. - * @type {Array.} - * @name yAxis - * @memberOf Highcharts.Chart - */ - chart.yAxis = []; - - chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; - - // Fire after init but before first render, before axes and series - // have been initialized. - fireEvent(chart, 'afterInit'); - - chart.firstRender(); - }); - }, - - /** - * Internal function to unitialize an individual series. - * - * @private - */ - initSeries: function (options) { - var chart = this, - optionsChart = chart.options.chart, - type = ( - options.type || - optionsChart.type || - optionsChart.defaultSeriesType - ), - series, - Constr = seriesTypes[type]; - - // No such series type - if (!Constr) { - H.error(17, true); - } - - series = new Constr(); - series.init(this, options); - return series; - }, - - /** - * Order all series above a given index. When series are added and ordered - * by configuration, only the last series is handled (#248, #1123, #2456, - * #6112). This function is called on series initialization and destroy. - * - * @private - * - * @param {number} fromIndex - * If this is given, only the series above this index are handled. - */ - orderSeries: function (fromIndex) { - var series = this.series, - i = fromIndex || 0; - for (; i < series.length; i++) { - if (series[i]) { - series[i].index = i; - series[i].name = series[i].getName(); - } - } - }, - - /** - * Check whether a given point is within the plot area. - * - * @param {Number} plotX - * Pixel x relative to the plot area. - * @param {Number} plotY - * Pixel y relative to the plot area. - * @param {Boolean} inverted - * Whether the chart is inverted. - * - * @return {Boolean} - * Returns true if the given point is inside the plot area. - */ - isInsidePlot: function (plotX, plotY, inverted) { - var x = inverted ? plotY : plotX, - y = inverted ? plotX : plotY; - - return x >= 0 && - x <= this.plotWidth && - y >= 0 && - y <= this.plotHeight; - }, - - /** - * Redraw the chart after changes have been done to the data, axis extremes - * chart size or chart elements. All methods for updating axes, series or - * points have a parameter for redrawing the chart. This is `true` by - * default. But in many cases you want to do more than one operation on the - * chart before redrawing, for example add a number of points. In those - * cases it is a waste of resources to redraw the chart for each new point - * added. So you add the points and call `chart.redraw()` after. - * - * @param {AnimationOptions} animation - * If or how to apply animation to the redraw. - */ - redraw: function (animation) { - - fireEvent(this, 'beforeRedraw'); - - var chart = this, - axes = chart.axes, - series = chart.series, - pointer = chart.pointer, - legend = chart.legend, - redrawLegend = chart.isDirtyLegend, - hasStackedSeries, - hasDirtyStacks, - hasCartesianSeries = chart.hasCartesianSeries, - isDirtyBox = chart.isDirtyBox, - i, - serie, - renderer = chart.renderer, - isHiddenChart = renderer.isHidden(), - afterRedraw = []; - - // Handle responsive rules, not only on resize (#6130) - if (chart.setResponsive) { - chart.setResponsive(false); - } - - H.setAnimation(animation, chart); - - if (isHiddenChart) { - chart.temporaryDisplay(); - } - - // Adjust title layout (reflow multiline text) - chart.layOutTitles(); - - // link stacked series - i = series.length; - while (i--) { - serie = series[i]; - - if (serie.options.stacking) { - hasStackedSeries = true; - - if (serie.isDirty) { - hasDirtyStacks = true; - break; - } - } - } - if (hasDirtyStacks) { // mark others as dirty - i = series.length; - while (i--) { - serie = series[i]; - if (serie.options.stacking) { - serie.isDirty = true; - } - } - } - - // Handle updated data in the series - each(series, function (serie) { - if (serie.isDirty) { - if (serie.options.legendType === 'point') { - if (serie.updateTotals) { - serie.updateTotals(); - } - redrawLegend = true; - } - } - if (serie.isDirtyData) { - fireEvent(serie, 'updatedData'); - } - }); - - // handle added or removed series - if (redrawLegend && legend.options.enabled) { - // draw legend graphics - legend.render(); - - chart.isDirtyLegend = false; - } - - // reset stacks - if (hasStackedSeries) { - chart.getStacks(); - } - - - if (hasCartesianSeries) { - // set axes scales - each(axes, function (axis) { - axis.updateNames(); - axis.setScale(); - }); - } - - chart.getMargins(); // #3098 - - if (hasCartesianSeries) { - // If one axis is dirty, all axes must be redrawn (#792, #2169) - each(axes, function (axis) { - if (axis.isDirty) { - isDirtyBox = true; - } - }); - - // redraw axes - each(axes, function (axis) { - - // Fire 'afterSetExtremes' only if extremes are set - var key = axis.min + ',' + axis.max; - if (axis.extKey !== key) { // #821, #4452 - axis.extKey = key; - - // prevent a recursive call to chart.redraw() (#1119) - afterRedraw.push(function () { - fireEvent( - axis, - 'afterSetExtremes', - extend(axis.eventArgs, axis.getExtremes()) - ); // #747, #751 - delete axis.eventArgs; - }); - } - if (isDirtyBox || hasStackedSeries) { - axis.redraw(); - } - }); - } - - // the plot areas size has changed - if (isDirtyBox) { - chart.drawChartBox(); - } - - // Fire an event before redrawing series, used by the boost module to - // clear previous series renderings. - fireEvent(chart, 'predraw'); - - // redraw affected series - each(series, function (serie) { - if ((isDirtyBox || serie.isDirty) && serie.visible) { - serie.redraw(); - } - // Set it here, otherwise we will have unlimited 'updatedData' calls - // for a hidden series after setData(). Fixes #6012 - serie.isDirtyData = false; - }); - - // move tooltip or reset - if (pointer) { - pointer.reset(true); - } - - // redraw if canvas - renderer.draw(); - - // Fire the events - fireEvent(chart, 'redraw'); - fireEvent(chart, 'render'); - - if (isHiddenChart) { - chart.temporaryDisplay(true); - } - - // Fire callbacks that are put on hold until after the redraw - each(afterRedraw, function (callback) { - callback.call(); - }); - }, - - /** - * Get an axis, series or point object by `id` as given in the configuration - * options. Returns `undefined` if no item is found. - * @param id {String} The id as given in the configuration options. - * @return {Highcharts.Axis|Highcharts.Series|Highcharts.Point|undefined} - * The retrieved item. - * @sample highcharts/plotoptions/series-id/ - * Get series by id - */ - get: function (id) { - - var ret, - series = this.series, - i; - - function itemById(item) { - return item.id === id || (item.options && item.options.id === id); - } - - ret = - // Search axes - find(this.axes, itemById) || - - // Search series - find(this.series, itemById); - - // Search points - for (i = 0; !ret && i < series.length; i++) { - ret = find(series[i].points || [], itemById); - } - - return ret; - }, - - /** - * Create the Axis instances based on the config options. - * - * @private - */ - getAxes: function () { - var chart = this, - options = this.options, - xAxisOptions = options.xAxis = splat(options.xAxis || {}), - yAxisOptions = options.yAxis = splat(options.yAxis || {}), - optionsArray; - - fireEvent(this, 'getAxes'); - - // make sure the options are arrays and add some members - each(xAxisOptions, function (axis, i) { - axis.index = i; - axis.isX = true; - }); - - each(yAxisOptions, function (axis, i) { - axis.index = i; - }); - - // concatenate all axis options into one array - optionsArray = xAxisOptions.concat(yAxisOptions); - - each(optionsArray, function (axisOptions) { - new Axis(chart, axisOptions); // eslint-disable-line no-new - }); - - fireEvent(this, 'afterGetAxes'); - }, - - - /** - * Returns an array of all currently selected points in the chart. Points - * can be selected by clicking or programmatically by the {@link - * Highcharts.Point#select} function. - * - * @return {Array.} - * The currently selected points. - * - * @sample highcharts/plotoptions/series-allowpointselect-line/ - * Get selected points - */ - getSelectedPoints: function () { - var points = []; - each(this.series, function (serie) { - // series.data - for points outside of viewed range (#6445) - points = points.concat(grep(serie.data || [], function (point) { - return point.selected; - })); - }); - return points; - }, - - /** - * Returns an array of all currently selected series in the chart. Series - * can be selected either programmatically by the {@link - * Highcharts.Series#select} function or by checking the checkbox next to - * the legend item if {@link - * https://api.highcharts.com/highcharts/plotOptions.series.showCheckbox| - * series.showCheckBox} is true. - * - * @return {Array.} - * The currently selected series. - * - * @sample highcharts/members/chart-getselectedseries/ - * Get selected series - */ - getSelectedSeries: function () { - return grep(this.series, function (serie) { - return serie.selected; - }); - }, - - /** - * Set a new title or subtitle for the chart. - * - * @param titleOptions {TitleOptions} - * New title options. The title text itself is set by the - * `titleOptions.text` property. - * @param subtitleOptions {SubtitleOptions} - * New subtitle options. The subtitle text itself is set by the - * `subtitleOptions.text` property. - * @param redraw {Boolean} - * Whether to redraw the chart or wait for a later call to - * `chart.redraw()`. - * - * @sample highcharts/members/chart-settitle/ Set title text and styles - * - */ - setTitle: function (titleOptions, subtitleOptions, redraw) { - var chart = this, - options = chart.options, - chartTitleOptions, - chartSubtitleOptions; - - chartTitleOptions = options.title = merge( - /*= if (build.classic) { =*/ - // Default styles - { - style: { - color: '${palette.neutralColor80}', - fontSize: options.isStock ? '16px' : '18px' // #2944 - } - }, - /*= } =*/ - options.title, - titleOptions - ); - chartSubtitleOptions = options.subtitle = merge( - /*= if (build.classic) { =*/ - // Default styles - { - style: { - color: '${palette.neutralColor60}' - } - }, - /*= } =*/ - options.subtitle, - subtitleOptions - ); - - // add title and subtitle - each([ - ['title', titleOptions, chartTitleOptions], - ['subtitle', subtitleOptions, chartSubtitleOptions] - ], function (arr, i) { - var name = arr[0], - title = chart[name], - titleOptions = arr[1], - chartTitleOptions = arr[2]; - - if (title && titleOptions) { - chart[name] = title = title.destroy(); // remove old - } - - if (chartTitleOptions && !title) { - chart[name] = chart.renderer.text( - chartTitleOptions.text, - 0, - 0, - chartTitleOptions.useHTML - ) - .attr({ - align: chartTitleOptions.align, - 'class': 'highcharts-' + name, - zIndex: chartTitleOptions.zIndex || 4 - }) - .add(); - - // Update methods, shortcut to Chart.setTitle - chart[name].update = function (o) { - chart.setTitle(!i && o, i && o); - }; - - /*= if (build.classic) { =*/ - // Presentational - chart[name].css(chartTitleOptions.style); - /*= } =*/ - - } - }); - chart.layOutTitles(redraw); - }, - - /** - * Internal function to lay out the chart titles and cache the full offset - * height for use in `getMargins`. The result is stored in - * `this.titleOffset`. - * - * @private - */ - layOutTitles: function (redraw) { - var titleOffset = 0, - requiresDirtyBox, - renderer = this.renderer, - spacingBox = this.spacingBox; - - // Lay out the title and the subtitle respectively - each(['title', 'subtitle'], function (key) { - var title = this[key], - titleOptions = this.options[key], - offset = key === 'title' ? -3 : - // Floating subtitle (#6574) - titleOptions.verticalAlign ? 0 : titleOffset + 2, - titleSize; - - if (title) { - /*= if (build.classic) { =*/ - titleSize = titleOptions.style.fontSize; - /*= } =*/ - titleSize = renderer.fontMetrics(titleSize, title).b; - title - .css({ - width: (titleOptions.width || - spacingBox.width + titleOptions.widthAdjust) + 'px' - }) - .align(extend({ - y: offset + titleSize - }, titleOptions), false, 'spacingBox'); - - if (!titleOptions.floating && !titleOptions.verticalAlign) { - titleOffset = Math.ceil( - titleOffset + - // Skip the cache for HTML (#3481) - title.getBBox(titleOptions.useHTML).height - ); - } - } - }, this); - - requiresDirtyBox = this.titleOffset !== titleOffset; - this.titleOffset = titleOffset; // used in getMargins - - if (!this.isDirtyBox && requiresDirtyBox) { - this.isDirtyBox = this.isDirtyLegend = requiresDirtyBox; - // Redraw if necessary (#2719, #2744) - if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { - this.redraw(); - } - } - }, - - /** - * Internal function to get the chart width and height according to options - * and container size. Sets {@link Chart.chartWidth} and {@link - * Chart.chartHeight}. - */ - getChartSize: function () { - var chart = this, - optionsChart = chart.options.chart, - widthOption = optionsChart.width, - heightOption = optionsChart.height, - renderTo = chart.renderTo; - - // Get inner width and height - if (!defined(widthOption)) { - chart.containerWidth = H.getStyle(renderTo, 'width'); - } - if (!defined(heightOption)) { - chart.containerHeight = H.getStyle(renderTo, 'height'); - } - - /** - * The current pixel width of the chart. - * - * @name chartWidth - * @memberOf Chart - * @type {Number} - */ - chart.chartWidth = Math.max( // #1393 - 0, - widthOption || chart.containerWidth || 600 // #1460 - ); - /** - * The current pixel height of the chart. - * - * @name chartHeight - * @memberOf Chart - * @type {Number} - */ - chart.chartHeight = Math.max( - 0, - H.relativeLength( - heightOption, - chart.chartWidth - ) || - (chart.containerHeight > 1 ? chart.containerHeight : 400) - ); - }, - - /** - * If the renderTo element has no offsetWidth, most likely one or more of - * its parents are hidden. Loop up the DOM tree to temporarily display the - * parents, then save the original display properties, and when the true - * size is retrieved, reset them. Used on first render and on redraws. - * - * @private - * - * @param {Boolean} revert - * Revert to the saved original styles. - */ - temporaryDisplay: function (revert) { - var node = this.renderTo, - tempStyle; - if (!revert) { - while (node && node.style) { - - // When rendering to a detached node, it needs to be temporarily - // attached in order to read styling and bounding boxes (#5783, - // #7024). - if (!doc.body.contains(node) && !node.parentNode) { - node.hcOrigDetached = true; - doc.body.appendChild(node); - } - if ( - H.getStyle(node, 'display', false) === 'none' || - node.hcOricDetached - ) { - node.hcOrigStyle = { - display: node.style.display, - height: node.style.height, - overflow: node.style.overflow - }; - tempStyle = { - display: 'block', - overflow: 'hidden' - }; - if (node !== this.renderTo) { - tempStyle.height = 0; - } - - H.css(node, tempStyle); - - // If it still doesn't have an offset width after setting - // display to block, it probably has an !important priority - // #2631, 6803 - if (!node.offsetWidth) { - node.style.setProperty('display', 'block', 'important'); - } - } - node = node.parentNode; - - if (node === doc.body) { - break; - } - } - } else { - while (node && node.style) { - if (node.hcOrigStyle) { - H.css(node, node.hcOrigStyle); - delete node.hcOrigStyle; - } - if (node.hcOrigDetached) { - doc.body.removeChild(node); - node.hcOrigDetached = false; - } - node = node.parentNode; - } - } - }, - - /** - * Set the {@link Chart.container|chart container's} class name, in - * addition to `highcharts-container`. - */ - setClassName: function (className) { - this.container.className = 'highcharts-container ' + (className || ''); - }, - - /** - * Get the containing element, determine the size and create the inner - * container div to hold the chart. - * - * @private - */ - getContainer: function () { - var chart = this, - container, - options = chart.options, - optionsChart = options.chart, - chartWidth, - chartHeight, - renderTo = chart.renderTo, - indexAttrName = 'data-highcharts-chart', - oldChartIndex, - Ren, - containerId = H.uniqueKey(), - containerStyle, - key; - - if (!renderTo) { - chart.renderTo = renderTo = optionsChart.renderTo; - } - - if (isString(renderTo)) { - chart.renderTo = renderTo = doc.getElementById(renderTo); - } - - // Display an error if the renderTo is wrong - if (!renderTo) { - H.error(13, true); - } - - // If the container already holds a chart, destroy it. The check for - // hasRendered is there because web pages that are saved to disk from - // the browser, will preserve the data-highcharts-chart attribute and - // the SVG contents, but not an interactive chart. So in this case, - // charts[oldChartIndex] will point to the wrong chart if any (#2609). - oldChartIndex = pInt(attr(renderTo, indexAttrName)); - if ( - isNumber(oldChartIndex) && - charts[oldChartIndex] && - charts[oldChartIndex].hasRendered - ) { - charts[oldChartIndex].destroy(); - } - - // Make a reference to the chart from the div - attr(renderTo, indexAttrName, chart.index); - - // remove previous chart - renderTo.innerHTML = ''; - - // If the container doesn't have an offsetWidth, it has or is a child of - // a node that has display:none. We need to temporarily move it out to a - // visible state to determine the size, else the legend and tooltips - // won't render properly. The skipClone option is used in sparklines as - // a micro optimization, saving about 1-2 ms each chart. - if (!optionsChart.skipClone && !renderTo.offsetWidth) { - chart.temporaryDisplay(); - } - - // get the width and height - chart.getChartSize(); - chartWidth = chart.chartWidth; - chartHeight = chart.chartHeight; - - // Create the inner container - /*= if (build.classic) { =*/ - containerStyle = extend({ - position: 'relative', - overflow: 'hidden', // needed for context menu (avoid scrollbars) - // and content overflow in IE - width: chartWidth + 'px', - height: chartHeight + 'px', - textAlign: 'left', - lineHeight: 'normal', // #427 - zIndex: 0, // #1072 - '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' - }, optionsChart.style); - /*= } =*/ - - /** - * The containing HTML element of the chart. The container is - * dynamically inserted into the element given as the `renderTo` - * parameterin the {@link Highcharts#chart} constructor. - * - * @memberOf Highcharts.Chart - * @type {HTMLDOMElement} - */ - container = createElement( - 'div', - { - id: containerId - }, - containerStyle, - renderTo - ); - chart.container = container; - - // cache the cursor (#1650) - chart._cursor = container.style.cursor; - - // Initialize the renderer - Ren = H[optionsChart.renderer] || H.Renderer; - - /** - * The renderer instance of the chart. Each chart instance has only one - * associated renderer. - * @type {SVGRenderer} - * @name renderer - * @memberOf Chart - */ - chart.renderer = new Ren( - container, - chartWidth, - chartHeight, - null, - optionsChart.forExport, - options.exporting && options.exporting.allowHTML - ); - - - chart.setClassName(optionsChart.className); - /*= if (build.classic) { =*/ - chart.renderer.setStyle(optionsChart.style); - /*= } else { =*/ - // Initialize definitions - for (key in options.defs) { - this.renderer.definition(options.defs[key]); - } - /*= } =*/ - - // Add a reference to the charts index - chart.renderer.chartIndex = chart.index; - - fireEvent(this, 'afterGetContainer'); - }, - - /** - * Calculate margins by rendering axis labels in a preliminary position. - * Title, subtitle and legend have already been rendered at this stage, but - * will be moved into their final positions. - * - * @private - */ - getMargins: function (skipAxes) { - var chart = this, - spacing = chart.spacing, - margin = chart.margin, - titleOffset = chart.titleOffset; - - chart.resetMargins(); - - // Adjust for title and subtitle - if (titleOffset && !defined(margin[0])) { - chart.plotTop = Math.max( - chart.plotTop, - titleOffset + chart.options.title.margin + spacing[0] - ); - } - - // Adjust for legend - if (chart.legend && chart.legend.display) { - chart.legend.adjustMargins(margin, spacing); - } - - // adjust for scroller - if (chart.extraMargin) { - chart[chart.extraMargin.type] = - (chart[chart.extraMargin.type] || 0) + chart.extraMargin.value; - } - - // adjust for rangeSelector - if (chart.adjustPlotArea) { - chart.adjustPlotArea(); - } - - if (!skipAxes) { - this.getAxisMargins(); - } - }, - - getAxisMargins: function () { - - var chart = this, - // [top, right, bottom, left] - axisOffset = chart.axisOffset = [0, 0, 0, 0], - margin = chart.margin; - - // pre-render axes to get labels offset width - if (chart.hasCartesianSeries) { - each(chart.axes, function (axis) { - if (axis.visible) { - axis.getOffset(); - } - }); - } - - // Add the axis offsets - each(marginNames, function (m, side) { - if (!defined(margin[side])) { - chart[m] += axisOffset[side]; - } - }); - - chart.setChartSize(); - - }, - - /** - * Reflows the chart to its container. By default, the chart reflows - * automatically to its container following a `window.resize` event, as per - * the {@link https://api.highcharts/highcharts/chart.reflow|chart.reflow} - * option. However, there are no reliable events for div resize, so if the - * container is resized without a window resize event, this must be called - * explicitly. - * - * @param {Object} e - * Event arguments. Used primarily when the function is called - * internally as a response to window resize. - * - * @sample highcharts/members/chart-reflow/ - * Resize div and reflow - * @sample highcharts/chart/events-container/ - * Pop up and reflow - */ - reflow: function (e) { - var chart = this, - optionsChart = chart.options.chart, - renderTo = chart.renderTo, - hasUserSize = ( - defined(optionsChart.width) && - defined(optionsChart.height) - ), - width = optionsChart.width || H.getStyle(renderTo, 'width'), - height = optionsChart.height || H.getStyle(renderTo, 'height'), - target = e ? e.target : win; - - // Width and height checks for display:none. Target is doc in IE8 and - // Opera, win in Firefox, Chrome and IE9. - if ( - !hasUserSize && - !chart.isPrinting && - width && - height && - (target === win || target === doc) - ) { - if ( - width !== chart.containerWidth || - height !== chart.containerHeight - ) { - H.clearTimeout(chart.reflowTimeout); - // When called from window.resize, e is set, else it's called - // directly (#2224) - chart.reflowTimeout = syncTimeout(function () { - // Set size, it may have been destroyed in the meantime - // (#1257) - if (chart.container) { - chart.setSize(undefined, undefined, false); - } - }, e ? 100 : 0); - } - chart.containerWidth = width; - chart.containerHeight = height; - } - }, - - /** - * Toggle the event handlers necessary for auto resizing, depending on the - * `chart.reflow` option. - * - * @private - */ - setReflow: function (reflow) { - - var chart = this; - - if (reflow !== false && !this.unbindReflow) { - this.unbindReflow = addEvent(win, 'resize', function (e) { - chart.reflow(e); - }); - addEvent(this, 'destroy', this.unbindReflow); - - } else if (reflow === false && this.unbindReflow) { - - // Unbind and unset - this.unbindReflow = this.unbindReflow(); - } - - // The following will add listeners to re-fit the chart before and after - // printing (#2284). However it only works in WebKit. Should have worked - // in Firefox, but not supported in IE. - /* - if (win.matchMedia) { - win.matchMedia('print').addListener(function reflow() { - chart.reflow(); - }); - } - //*/ - }, - - /** - * Resize the chart to a given width and height. In order to set the width - * only, the height argument may be skipped. To set the height only, pass - * `undefined` for the width. - * @param {Number|undefined|null} [width] - * The new pixel width of the chart. Since v4.2.6, the argument can - * be `undefined` in order to preserve the current value (when - * setting height only), or `null` to adapt to the width of the - * containing element. - * @param {Number|undefined|null} [height] - * The new pixel height of the chart. Since v4.2.6, the argument can - * be `undefined` in order to preserve the current value, or `null` - * in order to adapt to the height of the containing element. - * @param {AnimationOptions} [animation=true] - * Whether and how to apply animation. - * - * @sample highcharts/members/chart-setsize-button/ - * Test resizing from buttons - * @sample highcharts/members/chart-setsize-jquery-resizable/ - * Add a jQuery UI resizable - * @sample stock/members/chart-setsize/ - * Highstock with UI resizable - */ - setSize: function (width, height, animation) { - var chart = this, - renderer = chart.renderer, - globalAnimation; - - // Handle the isResizing counter - chart.isResizing += 1; - - // set the animation for the current process - H.setAnimation(animation, chart); - - chart.oldChartHeight = chart.chartHeight; - chart.oldChartWidth = chart.chartWidth; - if (width !== undefined) { - chart.options.chart.width = width; - } - if (height !== undefined) { - chart.options.chart.height = height; - } - chart.getChartSize(); - - // Resize the container with the global animation applied if enabled - // (#2503) - /*= if (build.classic) { =*/ - globalAnimation = renderer.globalAnimation; - (globalAnimation ? animate : css)(chart.container, { - width: chart.chartWidth + 'px', - height: chart.chartHeight + 'px' - }, globalAnimation); - /*= } =*/ - - chart.setChartSize(true); - renderer.setSize(chart.chartWidth, chart.chartHeight, animation); - - // handle axes - each(chart.axes, function (axis) { - axis.isDirty = true; - axis.setScale(); - }); - - chart.isDirtyLegend = true; // force legend redraw - chart.isDirtyBox = true; // force redraw of plot and chart border - - chart.layOutTitles(); // #2857 - chart.getMargins(); - - chart.redraw(animation); - - - chart.oldChartHeight = null; - fireEvent(chart, 'resize'); - - // Fire endResize and set isResizing back. If animation is disabled, - // fire without delay - syncTimeout(function () { - if (chart) { - fireEvent(chart, 'endResize', null, function () { - chart.isResizing -= 1; - }); - } - }, animObject(globalAnimation).duration); - }, - - /** - * Set the public chart properties. This is done before and after the - * pre-render to determine margin sizes. - * - * @private - */ - setChartSize: function (skipAxes) { - var chart = this, - inverted = chart.inverted, - renderer = chart.renderer, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - optionsChart = chart.options.chart, - spacing = chart.spacing, - clipOffset = chart.clipOffset, - clipX, - clipY, - plotLeft, - plotTop, - plotWidth, - plotHeight, - plotBorderWidth; - - /** - * The current left position of the plot area in pixels. - * - * @name plotLeft - * @memberOf Chart - * @type {Number} - */ - chart.plotLeft = plotLeft = Math.round(chart.plotLeft); - - /** - * The current top position of the plot area in pixels. - * - * @name plotTop - * @memberOf Chart - * @type {Number} - */ - chart.plotTop = plotTop = Math.round(chart.plotTop); - - /** - * The current width of the plot area in pixels. - * - * @name plotWidth - * @memberOf Chart - * @type {Number} - */ - chart.plotWidth = plotWidth = Math.max( - 0, - Math.round(chartWidth - plotLeft - chart.marginRight) - ); - - /** - * The current height of the plot area in pixels. - * - * @name plotHeight - * @memberOf Chart - * @type {Number} - */ - chart.plotHeight = plotHeight = Math.max( - 0, - Math.round(chartHeight - plotTop - chart.marginBottom) - ); - - chart.plotSizeX = inverted ? plotHeight : plotWidth; - chart.plotSizeY = inverted ? plotWidth : plotHeight; - - chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; - - // Set boxes used for alignment - chart.spacingBox = renderer.spacingBox = { - x: spacing[3], - y: spacing[0], - width: chartWidth - spacing[3] - spacing[1], - height: chartHeight - spacing[0] - spacing[2] - }; - chart.plotBox = renderer.plotBox = { - x: plotLeft, - y: plotTop, - width: plotWidth, - height: plotHeight - }; - - plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2); - clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2); - clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2); - chart.clipBox = { - x: clipX, - y: clipY, - width: Math.floor( - chart.plotSizeX - - Math.max(plotBorderWidth, clipOffset[1]) / 2 - - clipX - ), - height: Math.max( - 0, - Math.floor( - chart.plotSizeY - - Math.max(plotBorderWidth, clipOffset[2]) / 2 - - clipY - ) - ) - }; - - if (!skipAxes) { - each(chart.axes, function (axis) { - axis.setAxisSize(); - axis.setAxisTranslation(); - }); - } - - fireEvent(chart, 'afterSetChartSize'); - }, - - /** - * Initial margins before auto size margins are applied. - * - * @private - */ - resetMargins: function () { - var chart = this, - chartOptions = chart.options.chart; - - // Create margin and spacing array - each(['margin', 'spacing'], function splashArrays(target) { - var value = chartOptions[target], - values = isObject(value) ? value : [value, value, value, value]; - - each(['Top', 'Right', 'Bottom', 'Left'], function (sideName, side) { - chart[target][side] = pick( - chartOptions[target + sideName], - values[side] - ); - }); - }); - - // Set margin names like chart.plotTop, chart.plotLeft, - // chart.marginRight, chart.marginBottom. - each(marginNames, function (m, side) { - chart[m] = pick(chart.margin[side], chart.spacing[side]); - }); - chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left - chart.clipOffset = [0, 0, 0, 0]; - }, - - /** - * Internal function to draw or redraw the borders and backgrounds for chart - * and plot area. - * - * @private - */ - drawChartBox: function () { - var chart = this, - optionsChart = chart.options.chart, - renderer = chart.renderer, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - chartBackground = chart.chartBackground, - plotBackground = chart.plotBackground, - plotBorder = chart.plotBorder, - chartBorderWidth, - /*= if (build.classic) { =*/ - plotBGImage = chart.plotBGImage, - chartBackgroundColor = optionsChart.backgroundColor, - plotBackgroundColor = optionsChart.plotBackgroundColor, - plotBackgroundImage = optionsChart.plotBackgroundImage, - /*= } =*/ - mgn, - bgAttr, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - plotBox = chart.plotBox, - clipRect = chart.clipRect, - clipBox = chart.clipBox, - verb = 'animate'; - - // Chart area - if (!chartBackground) { - chart.chartBackground = chartBackground = renderer.rect() - .addClass('highcharts-background') - .add(); - verb = 'attr'; - } - - /*= if (build.classic) { =*/ - // Presentational - chartBorderWidth = optionsChart.borderWidth || 0; - mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); - - bgAttr = { - fill: chartBackgroundColor || 'none' - }; - - if (chartBorderWidth || chartBackground['stroke-width']) { // #980 - bgAttr.stroke = optionsChart.borderColor; - bgAttr['stroke-width'] = chartBorderWidth; - } - chartBackground - .attr(bgAttr) - .shadow(optionsChart.shadow); - /*= } else { =*/ - chartBorderWidth = mgn = chartBackground.strokeWidth(); - /*= } =*/ - chartBackground[verb]({ - x: mgn / 2, - y: mgn / 2, - width: chartWidth - mgn - chartBorderWidth % 2, - height: chartHeight - mgn - chartBorderWidth % 2, - r: optionsChart.borderRadius - }); - - // Plot background - verb = 'animate'; - if (!plotBackground) { - verb = 'attr'; - chart.plotBackground = plotBackground = renderer.rect() - .addClass('highcharts-plot-background') - .add(); - } - plotBackground[verb](plotBox); - - /*= if (build.classic) { =*/ - // Presentational attributes for the background - plotBackground - .attr({ - fill: plotBackgroundColor || 'none' - }) - .shadow(optionsChart.plotShadow); - - // Create the background image - if (plotBackgroundImage) { - if (!plotBGImage) { - chart.plotBGImage = renderer.image( - plotBackgroundImage, - plotLeft, - plotTop, - plotWidth, - plotHeight - ).add(); - } else { - plotBGImage.animate(plotBox); - } - } - /*= } =*/ - - // Plot clip - if (!clipRect) { - chart.clipRect = renderer.clipRect(clipBox); - } else { - clipRect.animate({ - width: clipBox.width, - height: clipBox.height - }); - } - - // Plot area border - verb = 'animate'; - if (!plotBorder) { - verb = 'attr'; - chart.plotBorder = plotBorder = renderer.rect() - .addClass('highcharts-plot-border') - .attr({ - zIndex: 1 // Above the grid - }) - .add(); - } - - /*= if (build.classic) { =*/ - // Presentational - plotBorder.attr({ - stroke: optionsChart.plotBorderColor, - 'stroke-width': optionsChart.plotBorderWidth || 0, - fill: 'none' - }); - /*= } =*/ - - plotBorder[verb](plotBorder.crisp({ - x: plotLeft, - y: plotTop, - width: plotWidth, - height: plotHeight - }, -plotBorder.strokeWidth())); // #3282 plotBorder should be negative; - - // reset - chart.isDirtyBox = false; - - fireEvent(this, 'afterDrawChartBox'); - }, - - /** - * Detect whether a certain chart property is needed based on inspecting its - * options and series. This mainly applies to the chart.inverted property, - * and in extensions to the chart.angular and chart.polar properties. - * - * @private - */ - propFromSeries: function () { - var chart = this, - optionsChart = chart.options.chart, - klass, - seriesOptions = chart.options.series, - i, - value; - - - each(['inverted', 'angular', 'polar'], function (key) { - - // The default series type's class - klass = seriesTypes[optionsChart.type || - optionsChart.defaultSeriesType]; - - // Get the value from available chart-wide properties - value = - optionsChart[key] || // It is set in the options - (klass && klass.prototype[key]); // The default series class - // requires it - - // 4. Check if any the chart's series require it - i = seriesOptions && seriesOptions.length; - while (!value && i--) { - klass = seriesTypes[seriesOptions[i].type]; - if (klass && klass.prototype[key]) { - value = true; - } - } - - // Set the chart property - chart[key] = value; - }); - - }, - - /** - * Internal function to link two or more series together, based on the - * `linkedTo` option. This is done from `Chart.render`, and after - * `Chart.addSeries` and `Series.remove`. - * - * @private - */ - linkSeries: function () { - var chart = this, - chartSeries = chart.series; - - // Reset links - each(chartSeries, function (series) { - series.linkedSeries.length = 0; - }); - - // Apply new links - each(chartSeries, function (series) { - var linkedTo = series.options.linkedTo; - if (isString(linkedTo)) { - if (linkedTo === ':previous') { - linkedTo = chart.series[series.index - 1]; - } else { - linkedTo = chart.get(linkedTo); - } - // #3341 avoid mutual linking - if (linkedTo && linkedTo.linkedParent !== series) { - linkedTo.linkedSeries.push(series); - series.linkedParent = linkedTo; - series.visible = pick( - series.options.visible, - linkedTo.options.visible, - series.visible - ); // #3879 - } - } - }); - - fireEvent(this, 'afterLinkSeries'); - }, - - /** - * Render series for the chart. - * - * @private - */ - renderSeries: function () { - each(this.series, function (serie) { - serie.translate(); - serie.render(); - }); - }, - - /** - * Render labels for the chart. - * - * @private - */ - renderLabels: function () { - var chart = this, - labels = chart.options.labels; - if (labels.items) { - each(labels.items, function (label) { - var style = extend(labels.style, label.style), - x = pInt(style.left) + chart.plotLeft, - y = pInt(style.top) + chart.plotTop + 12; - - // delete to prevent rewriting in IE - delete style.left; - delete style.top; - - chart.renderer.text( - label.html, - x, - y - ) - .attr({ zIndex: 2 }) - .css(style) - .add(); - - }); - } - }, - - /** - * Render all graphics for the chart. Runs internally on initialization. - * - * @private - */ - render: function () { - var chart = this, - axes = chart.axes, - renderer = chart.renderer, - options = chart.options, - tempWidth, - tempHeight, - redoHorizontal, - redoVertical; - - // Title - chart.setTitle(); - - - // Legend - chart.legend = new Legend(chart, options.legend); - - // Get stacks - if (chart.getStacks) { - chart.getStacks(); - } - - // Get chart margins - chart.getMargins(true); - chart.setChartSize(); - - // Record preliminary dimensions for later comparison - tempWidth = chart.plotWidth; - // 21 is the most common correction for X axis labels - // use Math.max to prevent negative plotHeight - tempHeight = chart.plotHeight = Math.max(chart.plotHeight - 21, 0); - - // Get margins by pre-rendering axes - each(axes, function (axis) { - axis.setScale(); - }); - chart.getAxisMargins(); - - // If the plot area size has changed significantly, calculate tick - // positions again - redoHorizontal = tempWidth / chart.plotWidth > 1.1; - // Height is more sensitive, use lower threshold - redoVertical = tempHeight / chart.plotHeight > 1.05; - - if (redoHorizontal || redoVertical) { - - each(axes, function (axis) { - if ( - (axis.horiz && redoHorizontal) || - (!axis.horiz && redoVertical) - ) { - // update to reflect the new margins - axis.setTickInterval(true); - } - }); - chart.getMargins(); // second pass to check for new labels - } - - // Draw the borders and backgrounds - chart.drawChartBox(); - - - // Axes - if (chart.hasCartesianSeries) { - each(axes, function (axis) { - if (axis.visible) { - axis.render(); - } - }); - } - - // The series - if (!chart.seriesGroup) { - chart.seriesGroup = renderer.g('series-group') - .attr({ zIndex: 3 }) - .add(); - } - chart.renderSeries(); - - // Labels - chart.renderLabels(); - - // Credits - chart.addCredits(); - - // Handle responsiveness - if (chart.setResponsive) { - chart.setResponsive(); - } - - // Set flag - chart.hasRendered = true; - - }, - - /** - * Set a new credits label for the chart. - * - * @param {CreditOptions} options - * A configuration object for the new credits. - * @sample highcharts/credits/credits-update/ Add and update credits - */ - addCredits: function (credits) { - var chart = this; - - credits = merge(true, this.options.credits, credits); - if (credits.enabled && !this.credits) { - - /** - * The chart's credits label. The label has an `update` method that - * allows setting new options as per the {@link - * https://api.highcharts.com/highcharts/credits| - * credits options set}. - * - * @memberof Highcharts.Chart - * @name credits - * @type {Highcharts.SVGElement} - */ - this.credits = this.renderer.text( - credits.text + (this.mapCredits || ''), - 0, - 0 - ) - .addClass('highcharts-credits') - .on('click', function () { - if (credits.href) { - win.location.href = credits.href; - } - }) - .attr({ - align: credits.position.align, - zIndex: 8 - }) - /*= if (build.classic) { =*/ - .css(credits.style) - /*= } =*/ - .add() - .align(credits.position); - - // Dynamically update - this.credits.update = function (options) { - chart.credits = chart.credits.destroy(); - chart.addCredits(options); - }; - } - }, - - /** - * Remove the chart and purge memory. This method is called internally - * before adding a second chart into the same container, as well as on - * window unload to prevent leaks. - * - * @sample highcharts/members/chart-destroy/ - * Destroy the chart from a button - * @sample stock/members/chart-destroy/ - * Destroy with Highstock - */ - destroy: function () { - var chart = this, - axes = chart.axes, - series = chart.series, - container = chart.container, - i, - parentNode = container && container.parentNode; - - // fire the chart.destoy event - fireEvent(chart, 'destroy'); - - // Delete the chart from charts lookup array - if (chart.renderer.forExport) { - H.erase(charts, chart); // #6569 - } else { - charts[chart.index] = undefined; - } - H.chartCount--; - chart.renderTo.removeAttribute('data-highcharts-chart'); - - // remove events - removeEvent(chart); - - // ==== Destroy collections: - // Destroy axes - i = axes.length; - while (i--) { - axes[i] = axes[i].destroy(); - } - - // Destroy scroller & scroller series before destroying base series - if (this.scroller && this.scroller.destroy) { - this.scroller.destroy(); - } - - // Destroy each series - i = series.length; - while (i--) { - series[i] = series[i].destroy(); - } - - // ==== Destroy chart properties: - each([ - 'title', 'subtitle', 'chartBackground', 'plotBackground', - 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', - 'pointer', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', - 'renderer' - ], function (name) { - var prop = chart[name]; - - if (prop && prop.destroy) { - chart[name] = prop.destroy(); - } - }); - - // Remove container and all SVG, check container as it can break in IE - // when destroyed before finished loading - if (container) { - container.innerHTML = ''; - removeEvent(container); - if (parentNode) { - discardElement(container); - } - - } - - // clean it all up - objectEach(chart, function (val, key) { - delete chart[key]; - }); - - }, - - /** - * Prepare for first rendering after all data are loaded. - * - * @private - */ - firstRender: function () { - var chart = this, - options = chart.options; - - // Hook for oldIE to check whether the chart is ready to render - if (chart.isReadyToRender && !chart.isReadyToRender()) { - return; - } - - // Create the container - chart.getContainer(); - - chart.resetMargins(); - chart.setChartSize(); - - // Set the common chart properties (mainly invert) from the given series - chart.propFromSeries(); - - // get axes - chart.getAxes(); - - // Initialize the series - each(options.series || [], function (serieOptions) { - chart.initSeries(serieOptions); - }); - - chart.linkSeries(); - - // Run an event after axes and series are initialized, but before - // render. At this stage, the series data is indexed and cached in the - // xData and yData arrays, so we can access those before rendering. Used - // in Highstock. - fireEvent(chart, 'beforeRender'); - - // depends on inverted and on margins being set - if (Pointer) { - - /** - * The Pointer that keeps track of mouse and touch interaction. - * - * @memberof Chart - * @name pointer - * @type Pointer - */ - chart.pointer = new Pointer(chart, options); - } - - chart.render(); - - // Fire the load event if there are no external images - if (!chart.renderer.imgCount && chart.onload) { - chart.onload(); - } - - // If the chart was rendered outside the top container, put it back in - // (#3679) - chart.temporaryDisplay(true); - - }, - - /** - * Internal function that runs on chart load, async if any images are loaded - * in the chart. Runs the callbacks and triggers the `load` and `render` - * events. - * - * @private - */ - onload: function () { - - // Run callbacks - each([this.callback].concat(this.callbacks), function (fn) { - // Chart destroyed in its own callback (#3600) - if (fn && this.index !== undefined) { - fn.apply(this, [this]); - } - }, this); - - fireEvent(this, 'load'); - fireEvent(this, 'render'); - - - // Set up auto resize, check for not destroyed (#6068) - if (defined(this.index)) { - this.setReflow(this.options.chart.reflow); - } - - // Don't run again - this.onload = null; - } + // Hook for adding callbacks in modules + callbacks: [], + + /** + * Handle the arguments passed to the constructor. + * + * @private + * @returns {Array} Arguments without renderTo + */ + getArgs: function () { + var args = [].slice.call(arguments); + + // Remove the optional first argument, renderTo, and + // set it on this. + if (isString(args[0]) || args[0].nodeName) { + this.renderTo = args.shift(); + } + this.init(args[0], args[1]); + }, + + /** + * Overridable function that initializes the chart. The constructor's + * arguments are passed on directly. + */ + init: function (userOptions, callback) { + + // Handle regular options + var options, + type, + // skip merging data points to increase performance + seriesOptions = userOptions.series, + userPlotOptions = userOptions.plotOptions || {}; + + // Fire the event with a default function + fireEvent(this, 'init', { args: arguments }, function () { + + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + + // Override (by copy of user options) or clear tooltip options + // in chart.options.plotOptions (#6218) + for (type in options.plotOptions) { + options.plotOptions[type].tooltip = ( + userPlotOptions[type] && + merge(userPlotOptions[type].tooltip) // override by copy + ) || undefined; // or clear + } + // User options have higher priority than default options + // (#6218). In case of exporting: path is changed + options.tooltip.userOptions = ( + userOptions.chart && + userOptions.chart.forExport && + userOptions.tooltip.userOptions + ) || userOptions.tooltip; + + // set back the series data + options.series = userOptions.series = seriesOptions; + this.userOptions = userOptions; + + var optionsChart = options.chart; + + var chartEvents = optionsChart.events; + + this.margin = []; + this.spacing = []; + + // Pixel data bounds for touch zoom + this.bounds = { h: {}, v: {} }; + + // An array of functions that returns labels that should be + // considered for anti-collision + this.labelCollectors = []; + + this.callback = callback; + this.isResizing = 0; + + /** + * The options structure for the chart. It contains members for + * the sub elements like series, legend, tooltip etc. + * + * @memberof Highcharts.Chart + * @name options + * @type {Options} + */ + this.options = options; + /** + * All the axes in the chart. + * + * @memberof Highcharts.Chart + * @name axes + * @see Highcharts.Chart.xAxis + * @see Highcharts.Chart.yAxis + * @type {Array.} + */ + this.axes = []; + + /** + * All the current series in the chart. + * + * @memberof Highcharts.Chart + * @name series + * @type {Array.} + */ + this.series = []; + + /** + * The chart title. The title has an `update` method that allows + * modifying the options directly or indirectly via + * `chart.update`. + * + * @memberof Highcharts.Chart + * @name title + * @type Object + * + * @sample highcharts/members/title-update/ + * Updating titles + */ + + /** + * The chart subtitle. The subtitle has an `update` method that + * allows modifying the options directly or indirectly via + * `chart.update`. + * + * @memberof Highcharts.Chart + * @name subtitle + * @type Object + */ + + /** + * The `Time` object associated with the chart. Since v6.0.5, + * time settings can be applied individually for each chart. If + * no individual settings apply, the `Time` object is shared by + * all instances. + * + * @memberof Highcharts.Chart + * @name time + * @type Highcharts.Time + */ + this.time = + userOptions.time && H.keys(userOptions.time).length ? + new H.Time(userOptions.time) : + H.time; + + + this.hasCartesianSeries = optionsChart.showAxes; + + var chart = this; + + // Add the chart to the global lookup + chart.index = charts.length; + + charts.push(chart); + H.chartCount++; + + // Chart event handlers + if (chartEvents) { + objectEach(chartEvents, function (event, eventType) { + addEvent(chart, eventType, event); + }); + } + + /** + * A collection of the X axes in the chart. + * @type {Array.} + * @name xAxis + * @memberOf Highcharts.Chart + */ + chart.xAxis = []; + /** + * A collection of the Y axes in the chart. + * @type {Array.} + * @name yAxis + * @memberOf Highcharts.Chart + */ + chart.yAxis = []; + + chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; + + // Fire after init but before first render, before axes and series + // have been initialized. + fireEvent(chart, 'afterInit'); + + chart.firstRender(); + }); + }, + + /** + * Internal function to unitialize an individual series. + * + * @private + */ + initSeries: function (options) { + var chart = this, + optionsChart = chart.options.chart, + type = ( + options.type || + optionsChart.type || + optionsChart.defaultSeriesType + ), + series, + Constr = seriesTypes[type]; + + // No such series type + if (!Constr) { + H.error(17, true); + } + + series = new Constr(); + series.init(this, options); + return series; + }, + + /** + * Order all series above a given index. When series are added and ordered + * by configuration, only the last series is handled (#248, #1123, #2456, + * #6112). This function is called on series initialization and destroy. + * + * @private + * + * @param {number} fromIndex + * If this is given, only the series above this index are handled. + */ + orderSeries: function (fromIndex) { + var series = this.series, + i = fromIndex || 0; + for (; i < series.length; i++) { + if (series[i]) { + series[i].index = i; + series[i].name = series[i].getName(); + } + } + }, + + /** + * Check whether a given point is within the plot area. + * + * @param {Number} plotX + * Pixel x relative to the plot area. + * @param {Number} plotY + * Pixel y relative to the plot area. + * @param {Boolean} inverted + * Whether the chart is inverted. + * + * @return {Boolean} + * Returns true if the given point is inside the plot area. + */ + isInsidePlot: function (plotX, plotY, inverted) { + var x = inverted ? plotY : plotX, + y = inverted ? plotX : plotY; + + return x >= 0 && + x <= this.plotWidth && + y >= 0 && + y <= this.plotHeight; + }, + + /** + * Redraw the chart after changes have been done to the data, axis extremes + * chart size or chart elements. All methods for updating axes, series or + * points have a parameter for redrawing the chart. This is `true` by + * default. But in many cases you want to do more than one operation on the + * chart before redrawing, for example add a number of points. In those + * cases it is a waste of resources to redraw the chart for each new point + * added. So you add the points and call `chart.redraw()` after. + * + * @param {AnimationOptions} animation + * If or how to apply animation to the redraw. + */ + redraw: function (animation) { + + fireEvent(this, 'beforeRedraw'); + + var chart = this, + axes = chart.axes, + series = chart.series, + pointer = chart.pointer, + legend = chart.legend, + redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + hasDirtyStacks, + hasCartesianSeries = chart.hasCartesianSeries, + isDirtyBox = chart.isDirtyBox, + i, + serie, + renderer = chart.renderer, + isHiddenChart = renderer.isHidden(), + afterRedraw = []; + + // Handle responsive rules, not only on resize (#6130) + if (chart.setResponsive) { + chart.setResponsive(false); + } + + H.setAnimation(animation, chart); + + if (isHiddenChart) { + chart.temporaryDisplay(); + } + + // Adjust title layout (reflow multiline text) + chart.layOutTitles(); + + // link stacked series + i = series.length; + while (i--) { + serie = series[i]; + + if (serie.options.stacking) { + hasStackedSeries = true; + + if (serie.isDirty) { + hasDirtyStacks = true; + break; + } + } + } + if (hasDirtyStacks) { // mark others as dirty + i = series.length; + while (i--) { + serie = series[i]; + if (serie.options.stacking) { + serie.isDirty = true; + } + } + } + + // Handle updated data in the series + each(series, function (serie) { + if (serie.isDirty) { + if (serie.options.legendType === 'point') { + if (serie.updateTotals) { + serie.updateTotals(); + } + redrawLegend = true; + } + } + if (serie.isDirtyData) { + fireEvent(serie, 'updatedData'); + } + }); + + // handle added or removed series + if (redrawLegend && legend.options.enabled) { + // draw legend graphics + legend.render(); + + chart.isDirtyLegend = false; + } + + // reset stacks + if (hasStackedSeries) { + chart.getStacks(); + } + + + if (hasCartesianSeries) { + // set axes scales + each(axes, function (axis) { + axis.updateNames(); + axis.setScale(); + }); + } + + chart.getMargins(); // #3098 + + if (hasCartesianSeries) { + // If one axis is dirty, all axes must be redrawn (#792, #2169) + each(axes, function (axis) { + if (axis.isDirty) { + isDirtyBox = true; + } + }); + + // redraw axes + each(axes, function (axis) { + + // Fire 'afterSetExtremes' only if extremes are set + var key = axis.min + ',' + axis.max; + if (axis.extKey !== key) { // #821, #4452 + axis.extKey = key; + + // prevent a recursive call to chart.redraw() (#1119) + afterRedraw.push(function () { + fireEvent( + axis, + 'afterSetExtremes', + extend(axis.eventArgs, axis.getExtremes()) + ); // #747, #751 + delete axis.eventArgs; + }); + } + if (isDirtyBox || hasStackedSeries) { + axis.redraw(); + } + }); + } + + // the plot areas size has changed + if (isDirtyBox) { + chart.drawChartBox(); + } + + // Fire an event before redrawing series, used by the boost module to + // clear previous series renderings. + fireEvent(chart, 'predraw'); + + // redraw affected series + each(series, function (serie) { + if ((isDirtyBox || serie.isDirty) && serie.visible) { + serie.redraw(); + } + // Set it here, otherwise we will have unlimited 'updatedData' calls + // for a hidden series after setData(). Fixes #6012 + serie.isDirtyData = false; + }); + + // move tooltip or reset + if (pointer) { + pointer.reset(true); + } + + // redraw if canvas + renderer.draw(); + + // Fire the events + fireEvent(chart, 'redraw'); + fireEvent(chart, 'render'); + + if (isHiddenChart) { + chart.temporaryDisplay(true); + } + + // Fire callbacks that are put on hold until after the redraw + each(afterRedraw, function (callback) { + callback.call(); + }); + }, + + /** + * Get an axis, series or point object by `id` as given in the configuration + * options. Returns `undefined` if no item is found. + * @param id {String} The id as given in the configuration options. + * @return {Highcharts.Axis|Highcharts.Series|Highcharts.Point|undefined} + * The retrieved item. + * @sample highcharts/plotoptions/series-id/ + * Get series by id + */ + get: function (id) { + + var ret, + series = this.series, + i; + + function itemById(item) { + return item.id === id || (item.options && item.options.id === id); + } + + ret = + // Search axes + find(this.axes, itemById) || + + // Search series + find(this.series, itemById); + + // Search points + for (i = 0; !ret && i < series.length; i++) { + ret = find(series[i].points || [], itemById); + } + + return ret; + }, + + /** + * Create the Axis instances based on the config options. + * + * @private + */ + getAxes: function () { + var chart = this, + options = this.options, + xAxisOptions = options.xAxis = splat(options.xAxis || {}), + yAxisOptions = options.yAxis = splat(options.yAxis || {}), + optionsArray; + + fireEvent(this, 'getAxes'); + + // make sure the options are arrays and add some members + each(xAxisOptions, function (axis, i) { + axis.index = i; + axis.isX = true; + }); + + each(yAxisOptions, function (axis, i) { + axis.index = i; + }); + + // concatenate all axis options into one array + optionsArray = xAxisOptions.concat(yAxisOptions); + + each(optionsArray, function (axisOptions) { + new Axis(chart, axisOptions); // eslint-disable-line no-new + }); + + fireEvent(this, 'afterGetAxes'); + }, + + + /** + * Returns an array of all currently selected points in the chart. Points + * can be selected by clicking or programmatically by the {@link + * Highcharts.Point#select} function. + * + * @return {Array.} + * The currently selected points. + * + * @sample highcharts/plotoptions/series-allowpointselect-line/ + * Get selected points + */ + getSelectedPoints: function () { + var points = []; + each(this.series, function (serie) { + // series.data - for points outside of viewed range (#6445) + points = points.concat(grep(serie.data || [], function (point) { + return point.selected; + })); + }); + return points; + }, + + /** + * Returns an array of all currently selected series in the chart. Series + * can be selected either programmatically by the {@link + * Highcharts.Series#select} function or by checking the checkbox next to + * the legend item if {@link + * https://api.highcharts.com/highcharts/plotOptions.series.showCheckbox| + * series.showCheckBox} is true. + * + * @return {Array.} + * The currently selected series. + * + * @sample highcharts/members/chart-getselectedseries/ + * Get selected series + */ + getSelectedSeries: function () { + return grep(this.series, function (serie) { + return serie.selected; + }); + }, + + /** + * Set a new title or subtitle for the chart. + * + * @param titleOptions {TitleOptions} + * New title options. The title text itself is set by the + * `titleOptions.text` property. + * @param subtitleOptions {SubtitleOptions} + * New subtitle options. The subtitle text itself is set by the + * `subtitleOptions.text` property. + * @param redraw {Boolean} + * Whether to redraw the chart or wait for a later call to + * `chart.redraw()`. + * + * @sample highcharts/members/chart-settitle/ Set title text and styles + * + */ + setTitle: function (titleOptions, subtitleOptions, redraw) { + var chart = this, + options = chart.options, + chartTitleOptions, + chartSubtitleOptions; + + chartTitleOptions = options.title = merge( + /*= if (build.classic) { =*/ + // Default styles + { + style: { + color: '${palette.neutralColor80}', + fontSize: options.isStock ? '16px' : '18px' // #2944 + } + }, + /*= } =*/ + options.title, + titleOptions + ); + chartSubtitleOptions = options.subtitle = merge( + /*= if (build.classic) { =*/ + // Default styles + { + style: { + color: '${palette.neutralColor60}' + } + }, + /*= } =*/ + options.subtitle, + subtitleOptions + ); + + // add title and subtitle + each([ + ['title', titleOptions, chartTitleOptions], + ['subtitle', subtitleOptions, chartSubtitleOptions] + ], function (arr, i) { + var name = arr[0], + title = chart[name], + titleOptions = arr[1], + chartTitleOptions = arr[2]; + + if (title && titleOptions) { + chart[name] = title = title.destroy(); // remove old + } + + if (chartTitleOptions && !title) { + chart[name] = chart.renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .attr({ + align: chartTitleOptions.align, + 'class': 'highcharts-' + name, + zIndex: chartTitleOptions.zIndex || 4 + }) + .add(); + + // Update methods, shortcut to Chart.setTitle + chart[name].update = function (o) { + chart.setTitle(!i && o, i && o); + }; + + /*= if (build.classic) { =*/ + // Presentational + chart[name].css(chartTitleOptions.style); + /*= } =*/ + + } + }); + chart.layOutTitles(redraw); + }, + + /** + * Internal function to lay out the chart titles and cache the full offset + * height for use in `getMargins`. The result is stored in + * `this.titleOffset`. + * + * @private + */ + layOutTitles: function (redraw) { + var titleOffset = 0, + requiresDirtyBox, + renderer = this.renderer, + spacingBox = this.spacingBox; + + // Lay out the title and the subtitle respectively + each(['title', 'subtitle'], function (key) { + var title = this[key], + titleOptions = this.options[key], + offset = key === 'title' ? -3 : + // Floating subtitle (#6574) + titleOptions.verticalAlign ? 0 : titleOffset + 2, + titleSize; + + if (title) { + /*= if (build.classic) { =*/ + titleSize = titleOptions.style.fontSize; + /*= } =*/ + titleSize = renderer.fontMetrics(titleSize, title).b; + title + .css({ + width: (titleOptions.width || + spacingBox.width + titleOptions.widthAdjust) + 'px' + }) + .align(extend({ + y: offset + titleSize + }, titleOptions), false, 'spacingBox'); + + if (!titleOptions.floating && !titleOptions.verticalAlign) { + titleOffset = Math.ceil( + titleOffset + + // Skip the cache for HTML (#3481) + title.getBBox(titleOptions.useHTML).height + ); + } + } + }, this); + + requiresDirtyBox = this.titleOffset !== titleOffset; + this.titleOffset = titleOffset; // used in getMargins + + if (!this.isDirtyBox && requiresDirtyBox) { + this.isDirtyBox = this.isDirtyLegend = requiresDirtyBox; + // Redraw if necessary (#2719, #2744) + if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { + this.redraw(); + } + } + }, + + /** + * Internal function to get the chart width and height according to options + * and container size. Sets {@link Chart.chartWidth} and {@link + * Chart.chartHeight}. + */ + getChartSize: function () { + var chart = this, + optionsChart = chart.options.chart, + widthOption = optionsChart.width, + heightOption = optionsChart.height, + renderTo = chart.renderTo; + + // Get inner width and height + if (!defined(widthOption)) { + chart.containerWidth = H.getStyle(renderTo, 'width'); + } + if (!defined(heightOption)) { + chart.containerHeight = H.getStyle(renderTo, 'height'); + } + + /** + * The current pixel width of the chart. + * + * @name chartWidth + * @memberOf Chart + * @type {Number} + */ + chart.chartWidth = Math.max( // #1393 + 0, + widthOption || chart.containerWidth || 600 // #1460 + ); + /** + * The current pixel height of the chart. + * + * @name chartHeight + * @memberOf Chart + * @type {Number} + */ + chart.chartHeight = Math.max( + 0, + H.relativeLength( + heightOption, + chart.chartWidth + ) || + (chart.containerHeight > 1 ? chart.containerHeight : 400) + ); + }, + + /** + * If the renderTo element has no offsetWidth, most likely one or more of + * its parents are hidden. Loop up the DOM tree to temporarily display the + * parents, then save the original display properties, and when the true + * size is retrieved, reset them. Used on first render and on redraws. + * + * @private + * + * @param {Boolean} revert + * Revert to the saved original styles. + */ + temporaryDisplay: function (revert) { + var node = this.renderTo, + tempStyle; + if (!revert) { + while (node && node.style) { + + // When rendering to a detached node, it needs to be temporarily + // attached in order to read styling and bounding boxes (#5783, + // #7024). + if (!doc.body.contains(node) && !node.parentNode) { + node.hcOrigDetached = true; + doc.body.appendChild(node); + } + if ( + H.getStyle(node, 'display', false) === 'none' || + node.hcOricDetached + ) { + node.hcOrigStyle = { + display: node.style.display, + height: node.style.height, + overflow: node.style.overflow + }; + tempStyle = { + display: 'block', + overflow: 'hidden' + }; + if (node !== this.renderTo) { + tempStyle.height = 0; + } + + H.css(node, tempStyle); + + // If it still doesn't have an offset width after setting + // display to block, it probably has an !important priority + // #2631, 6803 + if (!node.offsetWidth) { + node.style.setProperty('display', 'block', 'important'); + } + } + node = node.parentNode; + + if (node === doc.body) { + break; + } + } + } else { + while (node && node.style) { + if (node.hcOrigStyle) { + H.css(node, node.hcOrigStyle); + delete node.hcOrigStyle; + } + if (node.hcOrigDetached) { + doc.body.removeChild(node); + node.hcOrigDetached = false; + } + node = node.parentNode; + } + } + }, + + /** + * Set the {@link Chart.container|chart container's} class name, in + * addition to `highcharts-container`. + */ + setClassName: function (className) { + this.container.className = 'highcharts-container ' + (className || ''); + }, + + /** + * Get the containing element, determine the size and create the inner + * container div to hold the chart. + * + * @private + */ + getContainer: function () { + var chart = this, + container, + options = chart.options, + optionsChart = options.chart, + chartWidth, + chartHeight, + renderTo = chart.renderTo, + indexAttrName = 'data-highcharts-chart', + oldChartIndex, + Ren, + containerId = H.uniqueKey(), + containerStyle, + key; + + if (!renderTo) { + chart.renderTo = renderTo = optionsChart.renderTo; + } + + if (isString(renderTo)) { + chart.renderTo = renderTo = doc.getElementById(renderTo); + } + + // Display an error if the renderTo is wrong + if (!renderTo) { + H.error(13, true); + } + + // If the container already holds a chart, destroy it. The check for + // hasRendered is there because web pages that are saved to disk from + // the browser, will preserve the data-highcharts-chart attribute and + // the SVG contents, but not an interactive chart. So in this case, + // charts[oldChartIndex] will point to the wrong chart if any (#2609). + oldChartIndex = pInt(attr(renderTo, indexAttrName)); + if ( + isNumber(oldChartIndex) && + charts[oldChartIndex] && + charts[oldChartIndex].hasRendered + ) { + charts[oldChartIndex].destroy(); + } + + // Make a reference to the chart from the div + attr(renderTo, indexAttrName, chart.index); + + // remove previous chart + renderTo.innerHTML = ''; + + // If the container doesn't have an offsetWidth, it has or is a child of + // a node that has display:none. We need to temporarily move it out to a + // visible state to determine the size, else the legend and tooltips + // won't render properly. The skipClone option is used in sparklines as + // a micro optimization, saving about 1-2 ms each chart. + if (!optionsChart.skipClone && !renderTo.offsetWidth) { + chart.temporaryDisplay(); + } + + // get the width and height + chart.getChartSize(); + chartWidth = chart.chartWidth; + chartHeight = chart.chartHeight; + + // Create the inner container + /*= if (build.classic) { =*/ + containerStyle = extend({ + position: 'relative', + overflow: 'hidden', // needed for context menu (avoid scrollbars) + // and content overflow in IE + width: chartWidth + 'px', + height: chartHeight + 'px', + textAlign: 'left', + lineHeight: 'normal', // #427 + zIndex: 0, // #1072 + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' + }, optionsChart.style); + /*= } =*/ + + /** + * The containing HTML element of the chart. The container is + * dynamically inserted into the element given as the `renderTo` + * parameterin the {@link Highcharts#chart} constructor. + * + * @memberOf Highcharts.Chart + * @type {HTMLDOMElement} + */ + container = createElement( + 'div', + { + id: containerId + }, + containerStyle, + renderTo + ); + chart.container = container; + + // cache the cursor (#1650) + chart._cursor = container.style.cursor; + + // Initialize the renderer + Ren = H[optionsChart.renderer] || H.Renderer; + + /** + * The renderer instance of the chart. Each chart instance has only one + * associated renderer. + * @type {SVGRenderer} + * @name renderer + * @memberOf Chart + */ + chart.renderer = new Ren( + container, + chartWidth, + chartHeight, + null, + optionsChart.forExport, + options.exporting && options.exporting.allowHTML + ); + + + chart.setClassName(optionsChart.className); + /*= if (build.classic) { =*/ + chart.renderer.setStyle(optionsChart.style); + /*= } else { =*/ + // Initialize definitions + for (key in options.defs) { + this.renderer.definition(options.defs[key]); + } + /*= } =*/ + + // Add a reference to the charts index + chart.renderer.chartIndex = chart.index; + + fireEvent(this, 'afterGetContainer'); + }, + + /** + * Calculate margins by rendering axis labels in a preliminary position. + * Title, subtitle and legend have already been rendered at this stage, but + * will be moved into their final positions. + * + * @private + */ + getMargins: function (skipAxes) { + var chart = this, + spacing = chart.spacing, + margin = chart.margin, + titleOffset = chart.titleOffset; + + chart.resetMargins(); + + // Adjust for title and subtitle + if (titleOffset && !defined(margin[0])) { + chart.plotTop = Math.max( + chart.plotTop, + titleOffset + chart.options.title.margin + spacing[0] + ); + } + + // Adjust for legend + if (chart.legend && chart.legend.display) { + chart.legend.adjustMargins(margin, spacing); + } + + // adjust for scroller + if (chart.extraMargin) { + chart[chart.extraMargin.type] = + (chart[chart.extraMargin.type] || 0) + chart.extraMargin.value; + } + + // adjust for rangeSelector + if (chart.adjustPlotArea) { + chart.adjustPlotArea(); + } + + if (!skipAxes) { + this.getAxisMargins(); + } + }, + + getAxisMargins: function () { + + var chart = this, + // [top, right, bottom, left] + axisOffset = chart.axisOffset = [0, 0, 0, 0], + margin = chart.margin; + + // pre-render axes to get labels offset width + if (chart.hasCartesianSeries) { + each(chart.axes, function (axis) { + if (axis.visible) { + axis.getOffset(); + } + }); + } + + // Add the axis offsets + each(marginNames, function (m, side) { + if (!defined(margin[side])) { + chart[m] += axisOffset[side]; + } + }); + + chart.setChartSize(); + + }, + + /** + * Reflows the chart to its container. By default, the chart reflows + * automatically to its container following a `window.resize` event, as per + * the {@link https://api.highcharts/highcharts/chart.reflow|chart.reflow} + * option. However, there are no reliable events for div resize, so if the + * container is resized without a window resize event, this must be called + * explicitly. + * + * @param {Object} e + * Event arguments. Used primarily when the function is called + * internally as a response to window resize. + * + * @sample highcharts/members/chart-reflow/ + * Resize div and reflow + * @sample highcharts/chart/events-container/ + * Pop up and reflow + */ + reflow: function (e) { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderTo, + hasUserSize = ( + defined(optionsChart.width) && + defined(optionsChart.height) + ), + width = optionsChart.width || H.getStyle(renderTo, 'width'), + height = optionsChart.height || H.getStyle(renderTo, 'height'), + target = e ? e.target : win; + + // Width and height checks for display:none. Target is doc in IE8 and + // Opera, win in Firefox, Chrome and IE9. + if ( + !hasUserSize && + !chart.isPrinting && + width && + height && + (target === win || target === doc) + ) { + if ( + width !== chart.containerWidth || + height !== chart.containerHeight + ) { + H.clearTimeout(chart.reflowTimeout); + // When called from window.resize, e is set, else it's called + // directly (#2224) + chart.reflowTimeout = syncTimeout(function () { + // Set size, it may have been destroyed in the meantime + // (#1257) + if (chart.container) { + chart.setSize(undefined, undefined, false); + } + }, e ? 100 : 0); + } + chart.containerWidth = width; + chart.containerHeight = height; + } + }, + + /** + * Toggle the event handlers necessary for auto resizing, depending on the + * `chart.reflow` option. + * + * @private + */ + setReflow: function (reflow) { + + var chart = this; + + if (reflow !== false && !this.unbindReflow) { + this.unbindReflow = addEvent(win, 'resize', function (e) { + chart.reflow(e); + }); + addEvent(this, 'destroy', this.unbindReflow); + + } else if (reflow === false && this.unbindReflow) { + + // Unbind and unset + this.unbindReflow = this.unbindReflow(); + } + + // The following will add listeners to re-fit the chart before and after + // printing (#2284). However it only works in WebKit. Should have worked + // in Firefox, but not supported in IE. + /* + if (win.matchMedia) { + win.matchMedia('print').addListener(function reflow() { + chart.reflow(); + }); + } + //*/ + }, + + /** + * Resize the chart to a given width and height. In order to set the width + * only, the height argument may be skipped. To set the height only, pass + * `undefined` for the width. + * @param {Number|undefined|null} [width] + * The new pixel width of the chart. Since v4.2.6, the argument can + * be `undefined` in order to preserve the current value (when + * setting height only), or `null` to adapt to the width of the + * containing element. + * @param {Number|undefined|null} [height] + * The new pixel height of the chart. Since v4.2.6, the argument can + * be `undefined` in order to preserve the current value, or `null` + * in order to adapt to the height of the containing element. + * @param {AnimationOptions} [animation=true] + * Whether and how to apply animation. + * + * @sample highcharts/members/chart-setsize-button/ + * Test resizing from buttons + * @sample highcharts/members/chart-setsize-jquery-resizable/ + * Add a jQuery UI resizable + * @sample stock/members/chart-setsize/ + * Highstock with UI resizable + */ + setSize: function (width, height, animation) { + var chart = this, + renderer = chart.renderer, + globalAnimation; + + // Handle the isResizing counter + chart.isResizing += 1; + + // set the animation for the current process + H.setAnimation(animation, chart); + + chart.oldChartHeight = chart.chartHeight; + chart.oldChartWidth = chart.chartWidth; + if (width !== undefined) { + chart.options.chart.width = width; + } + if (height !== undefined) { + chart.options.chart.height = height; + } + chart.getChartSize(); + + // Resize the container with the global animation applied if enabled + // (#2503) + /*= if (build.classic) { =*/ + globalAnimation = renderer.globalAnimation; + (globalAnimation ? animate : css)(chart.container, { + width: chart.chartWidth + 'px', + height: chart.chartHeight + 'px' + }, globalAnimation); + /*= } =*/ + + chart.setChartSize(true); + renderer.setSize(chart.chartWidth, chart.chartHeight, animation); + + // handle axes + each(chart.axes, function (axis) { + axis.isDirty = true; + axis.setScale(); + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + chart.layOutTitles(); // #2857 + chart.getMargins(); + + chart.redraw(animation); + + + chart.oldChartHeight = null; + fireEvent(chart, 'resize'); + + // Fire endResize and set isResizing back. If animation is disabled, + // fire without delay + syncTimeout(function () { + if (chart) { + fireEvent(chart, 'endResize', null, function () { + chart.isResizing -= 1; + }); + } + }, animObject(globalAnimation).duration); + }, + + /** + * Set the public chart properties. This is done before and after the + * pre-render to determine margin sizes. + * + * @private + */ + setChartSize: function (skipAxes) { + var chart = this, + inverted = chart.inverted, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + optionsChart = chart.options.chart, + spacing = chart.spacing, + clipOffset = chart.clipOffset, + clipX, + clipY, + plotLeft, + plotTop, + plotWidth, + plotHeight, + plotBorderWidth; + + /** + * The current left position of the plot area in pixels. + * + * @name plotLeft + * @memberOf Chart + * @type {Number} + */ + chart.plotLeft = plotLeft = Math.round(chart.plotLeft); + + /** + * The current top position of the plot area in pixels. + * + * @name plotTop + * @memberOf Chart + * @type {Number} + */ + chart.plotTop = plotTop = Math.round(chart.plotTop); + + /** + * The current width of the plot area in pixels. + * + * @name plotWidth + * @memberOf Chart + * @type {Number} + */ + chart.plotWidth = plotWidth = Math.max( + 0, + Math.round(chartWidth - plotLeft - chart.marginRight) + ); + + /** + * The current height of the plot area in pixels. + * + * @name plotHeight + * @memberOf Chart + * @type {Number} + */ + chart.plotHeight = plotHeight = Math.max( + 0, + Math.round(chartHeight - plotTop - chart.marginBottom) + ); + + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; + + chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; + + // Set boxes used for alignment + chart.spacingBox = renderer.spacingBox = { + x: spacing[3], + y: spacing[0], + width: chartWidth - spacing[3] - spacing[1], + height: chartHeight - spacing[0] - spacing[2] + }; + chart.plotBox = renderer.plotBox = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; + + plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2); + clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2); + clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2); + chart.clipBox = { + x: clipX, + y: clipY, + width: Math.floor( + chart.plotSizeX - + Math.max(plotBorderWidth, clipOffset[1]) / 2 - + clipX + ), + height: Math.max( + 0, + Math.floor( + chart.plotSizeY - + Math.max(plotBorderWidth, clipOffset[2]) / 2 - + clipY + ) + ) + }; + + if (!skipAxes) { + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + } + + fireEvent(chart, 'afterSetChartSize'); + }, + + /** + * Initial margins before auto size margins are applied. + * + * @private + */ + resetMargins: function () { + var chart = this, + chartOptions = chart.options.chart; + + // Create margin and spacing array + each(['margin', 'spacing'], function splashArrays(target) { + var value = chartOptions[target], + values = isObject(value) ? value : [value, value, value, value]; + + each(['Top', 'Right', 'Bottom', 'Left'], function (sideName, side) { + chart[target][side] = pick( + chartOptions[target + sideName], + values[side] + ); + }); + }); + + // Set margin names like chart.plotTop, chart.plotLeft, + // chart.marginRight, chart.marginBottom. + each(marginNames, function (m, side) { + chart[m] = pick(chart.margin[side], chart.spacing[side]); + }); + chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + chart.clipOffset = [0, 0, 0, 0]; + }, + + /** + * Internal function to draw or redraw the borders and backgrounds for chart + * and plot area. + * + * @private + */ + drawChartBox: function () { + var chart = this, + optionsChart = chart.options.chart, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartBackground = chart.chartBackground, + plotBackground = chart.plotBackground, + plotBorder = chart.plotBorder, + chartBorderWidth, + /*= if (build.classic) { =*/ + plotBGImage = chart.plotBGImage, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + /*= } =*/ + mgn, + bgAttr, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + plotBox = chart.plotBox, + clipRect = chart.clipRect, + clipBox = chart.clipBox, + verb = 'animate'; + + // Chart area + if (!chartBackground) { + chart.chartBackground = chartBackground = renderer.rect() + .addClass('highcharts-background') + .add(); + verb = 'attr'; + } + + /*= if (build.classic) { =*/ + // Presentational + chartBorderWidth = optionsChart.borderWidth || 0; + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + bgAttr = { + fill: chartBackgroundColor || 'none' + }; + + if (chartBorderWidth || chartBackground['stroke-width']) { // #980 + bgAttr.stroke = optionsChart.borderColor; + bgAttr['stroke-width'] = chartBorderWidth; + } + chartBackground + .attr(bgAttr) + .shadow(optionsChart.shadow); + /*= } else { =*/ + chartBorderWidth = mgn = chartBackground.strokeWidth(); + /*= } =*/ + chartBackground[verb]({ + x: mgn / 2, + y: mgn / 2, + width: chartWidth - mgn - chartBorderWidth % 2, + height: chartHeight - mgn - chartBorderWidth % 2, + r: optionsChart.borderRadius + }); + + // Plot background + verb = 'animate'; + if (!plotBackground) { + verb = 'attr'; + chart.plotBackground = plotBackground = renderer.rect() + .addClass('highcharts-plot-background') + .add(); + } + plotBackground[verb](plotBox); + + /*= if (build.classic) { =*/ + // Presentational attributes for the background + plotBackground + .attr({ + fill: plotBackgroundColor || 'none' + }) + .shadow(optionsChart.plotShadow); + + // Create the background image + if (plotBackgroundImage) { + if (!plotBGImage) { + chart.plotBGImage = renderer.image( + plotBackgroundImage, + plotLeft, + plotTop, + plotWidth, + plotHeight + ).add(); + } else { + plotBGImage.animate(plotBox); + } + } + /*= } =*/ + + // Plot clip + if (!clipRect) { + chart.clipRect = renderer.clipRect(clipBox); + } else { + clipRect.animate({ + width: clipBox.width, + height: clipBox.height + }); + } + + // Plot area border + verb = 'animate'; + if (!plotBorder) { + verb = 'attr'; + chart.plotBorder = plotBorder = renderer.rect() + .addClass('highcharts-plot-border') + .attr({ + zIndex: 1 // Above the grid + }) + .add(); + } + + /*= if (build.classic) { =*/ + // Presentational + plotBorder.attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': optionsChart.plotBorderWidth || 0, + fill: 'none' + }); + /*= } =*/ + + plotBorder[verb](plotBorder.crisp({ + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }, -plotBorder.strokeWidth())); // #3282 plotBorder should be negative; + + // reset + chart.isDirtyBox = false; + + fireEvent(this, 'afterDrawChartBox'); + }, + + /** + * Detect whether a certain chart property is needed based on inspecting its + * options and series. This mainly applies to the chart.inverted property, + * and in extensions to the chart.angular and chart.polar properties. + * + * @private + */ + propFromSeries: function () { + var chart = this, + optionsChart = chart.options.chart, + klass, + seriesOptions = chart.options.series, + i, + value; + + + each(['inverted', 'angular', 'polar'], function (key) { + + // The default series type's class + klass = seriesTypes[optionsChart.type || + optionsChart.defaultSeriesType]; + + // Get the value from available chart-wide properties + value = + optionsChart[key] || // It is set in the options + (klass && klass.prototype[key]); // The default series class + // requires it + + // 4. Check if any the chart's series require it + i = seriesOptions && seriesOptions.length; + while (!value && i--) { + klass = seriesTypes[seriesOptions[i].type]; + if (klass && klass.prototype[key]) { + value = true; + } + } + + // Set the chart property + chart[key] = value; + }); + + }, + + /** + * Internal function to link two or more series together, based on the + * `linkedTo` option. This is done from `Chart.render`, and after + * `Chart.addSeries` and `Series.remove`. + * + * @private + */ + linkSeries: function () { + var chart = this, + chartSeries = chart.series; + + // Reset links + each(chartSeries, function (series) { + series.linkedSeries.length = 0; + }); + + // Apply new links + each(chartSeries, function (series) { + var linkedTo = series.options.linkedTo; + if (isString(linkedTo)) { + if (linkedTo === ':previous') { + linkedTo = chart.series[series.index - 1]; + } else { + linkedTo = chart.get(linkedTo); + } + // #3341 avoid mutual linking + if (linkedTo && linkedTo.linkedParent !== series) { + linkedTo.linkedSeries.push(series); + series.linkedParent = linkedTo; + series.visible = pick( + series.options.visible, + linkedTo.options.visible, + series.visible + ); // #3879 + } + } + }); + + fireEvent(this, 'afterLinkSeries'); + }, + + /** + * Render series for the chart. + * + * @private + */ + renderSeries: function () { + each(this.series, function (serie) { + serie.translate(); + serie.render(); + }); + }, + + /** + * Render labels for the chart. + * + * @private + */ + renderLabels: function () { + var chart = this, + labels = chart.options.labels; + if (labels.items) { + each(labels.items, function (label) { + var style = extend(labels.style, label.style), + x = pInt(style.left) + chart.plotLeft, + y = pInt(style.top) + chart.plotTop + 12; + + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + chart.renderer.text( + label.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + }, + + /** + * Render all graphics for the chart. Runs internally on initialization. + * + * @private + */ + render: function () { + var chart = this, + axes = chart.axes, + renderer = chart.renderer, + options = chart.options, + tempWidth, + tempHeight, + redoHorizontal, + redoVertical; + + // Title + chart.setTitle(); + + + // Legend + chart.legend = new Legend(chart, options.legend); + + // Get stacks + if (chart.getStacks) { + chart.getStacks(); + } + + // Get chart margins + chart.getMargins(true); + chart.setChartSize(); + + // Record preliminary dimensions for later comparison + tempWidth = chart.plotWidth; + // 21 is the most common correction for X axis labels + // use Math.max to prevent negative plotHeight + tempHeight = chart.plotHeight = Math.max(chart.plotHeight - 21, 0); + + // Get margins by pre-rendering axes + each(axes, function (axis) { + axis.setScale(); + }); + chart.getAxisMargins(); + + // If the plot area size has changed significantly, calculate tick + // positions again + redoHorizontal = tempWidth / chart.plotWidth > 1.1; + // Height is more sensitive, use lower threshold + redoVertical = tempHeight / chart.plotHeight > 1.05; + + if (redoHorizontal || redoVertical) { + + each(axes, function (axis) { + if ( + (axis.horiz && redoHorizontal) || + (!axis.horiz && redoVertical) + ) { + // update to reflect the new margins + axis.setTickInterval(true); + } + }); + chart.getMargins(); // second pass to check for new labels + } + + // Draw the borders and backgrounds + chart.drawChartBox(); + + + // Axes + if (chart.hasCartesianSeries) { + each(axes, function (axis) { + if (axis.visible) { + axis.render(); + } + }); + } + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + chart.renderSeries(); + + // Labels + chart.renderLabels(); + + // Credits + chart.addCredits(); + + // Handle responsiveness + if (chart.setResponsive) { + chart.setResponsive(); + } + + // Set flag + chart.hasRendered = true; + + }, + + /** + * Set a new credits label for the chart. + * + * @param {CreditOptions} options + * A configuration object for the new credits. + * @sample highcharts/credits/credits-update/ Add and update credits + */ + addCredits: function (credits) { + var chart = this; + + credits = merge(true, this.options.credits, credits); + if (credits.enabled && !this.credits) { + + /** + * The chart's credits label. The label has an `update` method that + * allows setting new options as per the {@link + * https://api.highcharts.com/highcharts/credits| + * credits options set}. + * + * @memberof Highcharts.Chart + * @name credits + * @type {Highcharts.SVGElement} + */ + this.credits = this.renderer.text( + credits.text + (this.mapCredits || ''), + 0, + 0 + ) + .addClass('highcharts-credits') + .on('click', function () { + if (credits.href) { + win.location.href = credits.href; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + /*= if (build.classic) { =*/ + .css(credits.style) + /*= } =*/ + .add() + .align(credits.position); + + // Dynamically update + this.credits.update = function (options) { + chart.credits = chart.credits.destroy(); + chart.addCredits(options); + }; + } + }, + + /** + * Remove the chart and purge memory. This method is called internally + * before adding a second chart into the same container, as well as on + * window unload to prevent leaks. + * + * @sample highcharts/members/chart-destroy/ + * Destroy the chart from a button + * @sample stock/members/chart-destroy/ + * Destroy with Highstock + */ + destroy: function () { + var chart = this, + axes = chart.axes, + series = chart.series, + container = chart.container, + i, + parentNode = container && container.parentNode; + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // Delete the chart from charts lookup array + if (chart.renderer.forExport) { + H.erase(charts, chart); // #6569 + } else { + charts[chart.index] = undefined; + } + H.chartCount--; + chart.renderTo.removeAttribute('data-highcharts-chart'); + + // remove events + removeEvent(chart); + + // ==== Destroy collections: + // Destroy axes + i = axes.length; + while (i--) { + axes[i] = axes[i].destroy(); + } + + // Destroy scroller & scroller series before destroying base series + if (this.scroller && this.scroller.destroy) { + this.scroller.destroy(); + } + + // Destroy each series + i = series.length; + while (i--) { + series[i] = series[i].destroy(); + } + + // ==== Destroy chart properties: + each([ + 'title', 'subtitle', 'chartBackground', 'plotBackground', + 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', + 'pointer', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', + 'renderer' + ], function (name) { + var prop = chart[name]; + + if (prop && prop.destroy) { + chart[name] = prop.destroy(); + } + }); + + // Remove container and all SVG, check container as it can break in IE + // when destroyed before finished loading + if (container) { + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } + + } + + // clean it all up + objectEach(chart, function (val, key) { + delete chart[key]; + }); + + }, + + /** + * Prepare for first rendering after all data are loaded. + * + * @private + */ + firstRender: function () { + var chart = this, + options = chart.options; + + // Hook for oldIE to check whether the chart is ready to render + if (chart.isReadyToRender && !chart.isReadyToRender()) { + return; + } + + // Create the container + chart.getContainer(); + + chart.resetMargins(); + chart.setChartSize(); + + // Set the common chart properties (mainly invert) from the given series + chart.propFromSeries(); + + // get axes + chart.getAxes(); + + // Initialize the series + each(options.series || [], function (serieOptions) { + chart.initSeries(serieOptions); + }); + + chart.linkSeries(); + + // Run an event after axes and series are initialized, but before + // render. At this stage, the series data is indexed and cached in the + // xData and yData arrays, so we can access those before rendering. Used + // in Highstock. + fireEvent(chart, 'beforeRender'); + + // depends on inverted and on margins being set + if (Pointer) { + + /** + * The Pointer that keeps track of mouse and touch interaction. + * + * @memberof Chart + * @name pointer + * @type Pointer + */ + chart.pointer = new Pointer(chart, options); + } + + chart.render(); + + // Fire the load event if there are no external images + if (!chart.renderer.imgCount && chart.onload) { + chart.onload(); + } + + // If the chart was rendered outside the top container, put it back in + // (#3679) + chart.temporaryDisplay(true); + + }, + + /** + * Internal function that runs on chart load, async if any images are loaded + * in the chart. Runs the callbacks and triggers the `load` and `render` + * events. + * + * @private + */ + onload: function () { + + // Run callbacks + each([this.callback].concat(this.callbacks), function (fn) { + // Chart destroyed in its own callback (#3600) + if (fn && this.index !== undefined) { + fn.apply(this, [this]); + } + }, this); + + fireEvent(this, 'load'); + fireEvent(this, 'render'); + + + // Set up auto resize, check for not destroyed (#6068) + if (defined(this.index)) { + this.setReflow(this.options.chart.reflow); + } + + // Don't run again + this.onload = null; + } }); // end Chart diff --git a/js/parts/Color.js b/js/parts/Color.js index e0e23127bff..7c3535f1a4f 100644 --- a/js/parts/Color.js +++ b/js/parts/Color.js @@ -7,14 +7,14 @@ import H from './Globals.js'; import './Utilities.js'; var each = H.each, - isNumber = H.isNumber, - map = H.map, - merge = H.merge, - pInt = H.pInt; + isNumber = H.isNumber, + map = H.map, + merge = H.merge, + pInt = H.pInt; /** * @typedef {string} ColorString - * A valid color to be parsed and handled by Highcharts. Highcharts internally + * A valid color to be parsed and handled by Highcharts. Highcharts internally * supports hex colors like `#ffffff`, rgb colors like `rgb(255,255,255)` and * rgba colors like `rgba(255,255,255,1)`. Other colors may be supported by the * browsers and displayed correctly, but Highcharts is not able to process them @@ -25,230 +25,230 @@ var each = H.each, * @param {String} input The input color in either rbga or hex format */ H.Color = function (input) { - // Backwards compatibility, allow instanciation without new - if (!(this instanceof H.Color)) { - return new H.Color(input); - } + // Backwards compatibility, allow instanciation without new + if (!(this instanceof H.Color)) { + return new H.Color(input); + } // Initialize - this.init(input); + this.init(input); }; H.Color.prototype = { - // Collection of parsers. This can be extended from the outside by pushing - // parsers to Highcharts.Color.prototype.parsers. - parsers: [{ - // RGBA color - regex: /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/, // eslint-disable-line security/detect-unsafe-regex - parse: function (result) { - return [ - pInt(result[1]), - pInt(result[2]), - pInt(result[3]), - parseFloat(result[4], 10) - ]; - } - }, { - // RGB color - regex: - /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/, - parse: function (result) { - return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; - } - }], - - // Collection of named colors. Can be extended from the outside by adding - // colors to Highcharts.Color.prototype.names. - names: { - none: 'rgba(255,255,255,0)', - white: '#ffffff', - black: '#000000' - }, - - /** - * Parse the input color to rgba array - * @param {String} input - */ - init: function (input) { - var result, - rgba, - i, - parser, - len; - - this.input = input = this.names[ - input && input.toLowerCase ? - input.toLowerCase() : - '' - ] || input; - - // Gradients - if (input && input.stops) { - this.stops = map(input.stops, function (stop) { - return new H.Color(stop[1]); - }); - - // Solid colors - } else { - - // Bitmasking as input[0] is not working for legacy IE. - if (input && input.charAt && input.charAt() === '#') { - - len = input.length; - input = parseInt(input.substr(1), 16); - - // Handle long-form, e.g. #AABBCC - if (len === 7) { - - rgba = [ - (input & 0xFF0000) >> 16, - (input & 0xFF00) >> 8, - (input & 0xFF), - 1 - ]; - - // Handle short-form, e.g. #ABC - // In short form, the value is assumed to be the same - // for both nibbles for each component. e.g. #ABC = #AABBCC - } else if (len === 4) { - - rgba = [ - ((input & 0xF00) >> 4) | (input & 0xF00) >> 8, - ((input & 0xF0) >> 4) | (input & 0xF0), - ((input & 0xF) << 4) | (input & 0xF), - 1 - ]; - } - } - - // Otherwise, check regex parsers - if (!rgba) { - i = this.parsers.length; - while (i-- && !rgba) { - parser = this.parsers[i]; - result = parser.regex.exec(input); - if (result) { - rgba = parser.parse(result); - } - } - } - } - this.rgba = rgba || []; - }, - - /** - * Return the color a specified format - * @param {String} format - */ - get: function (format) { - var input = this.input, - rgba = this.rgba, - ret; - - if (this.stops) { - ret = merge(input); - ret.stops = [].concat(ret.stops); - each(this.stops, function (stop, i) { - ret.stops[i] = [ret.stops[i][0], stop.get(format)]; - }); - - // it's NaN if gradient colors on a column chart - } else if (rgba && isNumber(rgba[0])) { - if (format === 'rgb' || (!format && rgba[3] === 1)) { - ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; - } else if (format === 'a') { - ret = rgba[3]; - } else { - ret = 'rgba(' + rgba.join(',') + ')'; - } - } else { - ret = input; - } - return ret; - }, - - /** - * Brighten the color - * @param {Number} alpha - */ - brighten: function (alpha) { - var i, - rgba = this.rgba; - - if (this.stops) { - each(this.stops, function (stop) { - stop.brighten(alpha); - }); - - } else if (isNumber(alpha) && alpha !== 0) { - for (i = 0; i < 3; i++) { - rgba[i] += pInt(alpha * 255); - - if (rgba[i] < 0) { - rgba[i] = 0; - } - if (rgba[i] > 255) { - rgba[i] = 255; - } - } - } - return this; - }, - - /** - * Set the color's opacity to a given alpha value - * @param {Number} alpha - */ - setOpacity: function (alpha) { - this.rgba[3] = alpha; - return this; - }, - - /* - * Return an intermediate color between two colors. - * - * @param {Highcharts.Color} to - * The color object to tween to. - * @param {Number} pos - * The intermediate position, where 0 is the from color (current - * color item), and 1 is the `to` color. - * - * @return {String} - * The intermediate color in rgba notation. - */ - tweenTo: function (to, pos) { - // Check for has alpha, because rgba colors perform worse due to lack of - // support in WebKit. - var fromRgba = this.rgba, - toRgba = to.rgba, - hasAlpha, - ret; - - // Unsupported color, return to-color (#3920, #7034) - if (!toRgba.length || !fromRgba || !fromRgba.length) { - ret = to.input || 'none'; - - // Interpolate - } else { - hasAlpha = (toRgba[3] !== 1 || fromRgba[3] !== 1); - ret = (hasAlpha ? 'rgba(' : 'rgb(') + - Math.round(toRgba[0] + (fromRgba[0] - toRgba[0]) * (1 - pos)) + - ',' + - Math.round(toRgba[1] + (fromRgba[1] - toRgba[1]) * (1 - pos)) + - ',' + - Math.round(toRgba[2] + (fromRgba[2] - toRgba[2]) * (1 - pos)) + - ( - hasAlpha ? - ( - ',' + - (toRgba[3] + (fromRgba[3] - toRgba[3]) * (1 - pos)) - ) : - '' - ) + - ')'; - } - return ret; - } + // Collection of parsers. This can be extended from the outside by pushing + // parsers to Highcharts.Color.prototype.parsers. + parsers: [{ + // RGBA color + regex: /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/, // eslint-disable-line security/detect-unsafe-regex + parse: function (result) { + return [ + pInt(result[1]), + pInt(result[2]), + pInt(result[3]), + parseFloat(result[4], 10) + ]; + } + }, { + // RGB color + regex: + /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/, + parse: function (result) { + return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; + } + }], + + // Collection of named colors. Can be extended from the outside by adding + // colors to Highcharts.Color.prototype.names. + names: { + none: 'rgba(255,255,255,0)', + white: '#ffffff', + black: '#000000' + }, + + /** + * Parse the input color to rgba array + * @param {String} input + */ + init: function (input) { + var result, + rgba, + i, + parser, + len; + + this.input = input = this.names[ + input && input.toLowerCase ? + input.toLowerCase() : + '' + ] || input; + + // Gradients + if (input && input.stops) { + this.stops = map(input.stops, function (stop) { + return new H.Color(stop[1]); + }); + + // Solid colors + } else { + + // Bitmasking as input[0] is not working for legacy IE. + if (input && input.charAt && input.charAt() === '#') { + + len = input.length; + input = parseInt(input.substr(1), 16); + + // Handle long-form, e.g. #AABBCC + if (len === 7) { + + rgba = [ + (input & 0xFF0000) >> 16, + (input & 0xFF00) >> 8, + (input & 0xFF), + 1 + ]; + + // Handle short-form, e.g. #ABC + // In short form, the value is assumed to be the same + // for both nibbles for each component. e.g. #ABC = #AABBCC + } else if (len === 4) { + + rgba = [ + ((input & 0xF00) >> 4) | (input & 0xF00) >> 8, + ((input & 0xF0) >> 4) | (input & 0xF0), + ((input & 0xF) << 4) | (input & 0xF), + 1 + ]; + } + } + + // Otherwise, check regex parsers + if (!rgba) { + i = this.parsers.length; + while (i-- && !rgba) { + parser = this.parsers[i]; + result = parser.regex.exec(input); + if (result) { + rgba = parser.parse(result); + } + } + } + } + this.rgba = rgba || []; + }, + + /** + * Return the color a specified format + * @param {String} format + */ + get: function (format) { + var input = this.input, + rgba = this.rgba, + ret; + + if (this.stops) { + ret = merge(input); + ret.stops = [].concat(ret.stops); + each(this.stops, function (stop, i) { + ret.stops[i] = [ret.stops[i][0], stop.get(format)]; + }); + + // it's NaN if gradient colors on a column chart + } else if (rgba && isNumber(rgba[0])) { + if (format === 'rgb' || (!format && rgba[3] === 1)) { + ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; + } else if (format === 'a') { + ret = rgba[3]; + } else { + ret = 'rgba(' + rgba.join(',') + ')'; + } + } else { + ret = input; + } + return ret; + }, + + /** + * Brighten the color + * @param {Number} alpha + */ + brighten: function (alpha) { + var i, + rgba = this.rgba; + + if (this.stops) { + each(this.stops, function (stop) { + stop.brighten(alpha); + }); + + } else if (isNumber(alpha) && alpha !== 0) { + for (i = 0; i < 3; i++) { + rgba[i] += pInt(alpha * 255); + + if (rgba[i] < 0) { + rgba[i] = 0; + } + if (rgba[i] > 255) { + rgba[i] = 255; + } + } + } + return this; + }, + + /** + * Set the color's opacity to a given alpha value + * @param {Number} alpha + */ + setOpacity: function (alpha) { + this.rgba[3] = alpha; + return this; + }, + + /* + * Return an intermediate color between two colors. + * + * @param {Highcharts.Color} to + * The color object to tween to. + * @param {Number} pos + * The intermediate position, where 0 is the from color (current + * color item), and 1 is the `to` color. + * + * @return {String} + * The intermediate color in rgba notation. + */ + tweenTo: function (to, pos) { + // Check for has alpha, because rgba colors perform worse due to lack of + // support in WebKit. + var fromRgba = this.rgba, + toRgba = to.rgba, + hasAlpha, + ret; + + // Unsupported color, return to-color (#3920, #7034) + if (!toRgba.length || !fromRgba || !fromRgba.length) { + ret = to.input || 'none'; + + // Interpolate + } else { + hasAlpha = (toRgba[3] !== 1 || fromRgba[3] !== 1); + ret = (hasAlpha ? 'rgba(' : 'rgb(') + + Math.round(toRgba[0] + (fromRgba[0] - toRgba[0]) * (1 - pos)) + + ',' + + Math.round(toRgba[1] + (fromRgba[1] - toRgba[1]) * (1 - pos)) + + ',' + + Math.round(toRgba[2] + (fromRgba[2] - toRgba[2]) * (1 - pos)) + + ( + hasAlpha ? + ( + ',' + + (toRgba[3] + (fromRgba[3] - toRgba[3]) * (1 - pos)) + ) : + '' + ) + + ')'; + } + return ret; + } }; H.color = function (input) { - return new H.Color(input); + return new H.Color(input); }; diff --git a/js/parts/ColumnSeries.js b/js/parts/ColumnSeries.js index 41f4b50207b..37803c11602 100644 --- a/js/parts/ColumnSeries.js +++ b/js/parts/ColumnSeries.js @@ -11,17 +11,17 @@ import './Legend.js'; import './Series.js'; import './Options.js'; var animObject = H.animObject, - color = H.color, - each = H.each, - extend = H.extend, - isNumber = H.isNumber, - LegendSymbolMixin = H.LegendSymbolMixin, - merge = H.merge, - noop = H.noop, - pick = H.pick, - Series = H.Series, - seriesType = H.seriesType, - svg = H.svg; + color = H.color, + each = H.each, + extend = H.extend, + isNumber = H.isNumber, + LegendSymbolMixin = H.LegendSymbolMixin, + merge = H.merge, + noop = H.noop, + pick = H.pick, + Series = H.Series, + seriesType = H.seriesType, + svg = H.svg; /** * The column series type. * @@ -43,826 +43,826 @@ var animObject = H.animObject, */ seriesType('column', 'line', { - /** - * The corner radius of the border surrounding each column or bar. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/column-borderradius/ - * Rounded columns - * @default 0 - * @product highcharts highstock - */ - borderRadius: 0, - - /** - * When using automatic point colors pulled from the `options.colors` - * collection, this option determines whether the chart should receive - * one color per series or one color per point. - * - * @type {Boolean} - * @see [series colors](#plotOptions.column.colors) - * @sample {highcharts} highcharts/plotoptions/column-colorbypoint-false/ - * False by default - * @sample {highcharts} highcharts/plotoptions/column-colorbypoint-true/ - * True - * @default false - * @since 2.0 - * @product highcharts highstock - * @apioption plotOptions.column.colorByPoint - */ - - /** - * A series specific or series type specific color set to apply instead - * of the global [colors](#colors) when [colorByPoint]( - * #plotOptions.column.colorByPoint) is true. - * - * @type {Array} - * @since 3.0 - * @product highcharts highstock - * @apioption plotOptions.column.colors - */ - - /** - * When true, each column edge is rounded to its nearest pixel in order - * to render sharp on screen. In some cases, when there are a lot of - * densely packed columns, this leads to visible difference in column - * widths or distance between columns. In these cases, setting `crisp` - * to `false` may look better, even though each column is rendered - * blurry. - * - * @sample {highcharts} highcharts/plotoptions/column-crisp-false/ - * Crisp is false - * @since 5.0.10 - * @product highcharts highstock - */ - crisp: true, - - /** - * Padding between each value groups, in x axis units. - * - * @sample {highcharts} highcharts/plotoptions/column-grouppadding-default/ - * 0.2 by default - * @sample {highcharts} highcharts/plotoptions/column-grouppadding-none/ - * No group padding - all columns are evenly spaced - * @product highcharts highstock - */ - groupPadding: 0.2, - - /** - * Whether to group non-stacked columns or to let them render independent - * of each other. Non-grouped columns will be laid out individually - * and overlap each other. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/column-grouping-false/ - * Grouping disabled - * @sample {highstock} highcharts/plotoptions/column-grouping-false/ - * Grouping disabled - * @default true - * @since 2.3.0 - * @product highcharts highstock - * @apioption plotOptions.column.grouping - */ - - /** - * @ignore - */ - marker: null, // point options are specified in the base options - - /** - * The maximum allowed pixel width for a column, translated to the height - * of a bar in a bar chart. This prevents the columns from becoming - * too wide when there is a small number of points in the chart. - * - * @type {Number} - * @see [pointWidth](#plotOptions.column.pointWidth) - * @sample {highcharts} highcharts/plotoptions/column-maxpointwidth-20/ - * Limited to 50 - * @sample {highstock} highcharts/plotoptions/column-maxpointwidth-20/ - * Limited to 50 - * @default null - * @since 4.1.8 - * @product highcharts highstock - * @apioption plotOptions.column.maxPointWidth - */ - - /** - * Padding between each column or bar, in x axis units. - * - * @sample {highcharts} highcharts/plotoptions/column-pointpadding-default/ - * 0.1 by default - * @sample {highcharts} highcharts/plotoptions/column-pointpadding-025/ - * 0.25 - * @sample {highcharts} highcharts/plotoptions/column-pointpadding-none/ - * 0 for tightly packed columns - * @product highcharts highstock - */ - pointPadding: 0.1, - - /** - * A pixel value specifying a fixed width for each column or bar. When - * `null`, the width is calculated from the `pointPadding` and - * `groupPadding`. - * - * @type {Number} - * @see [maxPointWidth](#plotOptions.column.maxPointWidth) - * @sample {highcharts} highcharts/plotoptions/column-pointwidth-20/ - * 20px wide columns regardless of chart width or the amount - * of data points - * @default null - * @since 1.2.5 - * @product highcharts highstock - * @apioption plotOptions.column.pointWidth - */ - - /** - * The minimal height for a column or width for a bar. By default, - * 0 values are not shown. To visualize a 0 (or close to zero) point, - * set the minimal point length to a pixel value like 3\. In stacked - * column charts, minPointLength might not be respected for tightly - * packed values. - * - * @sample {highcharts} - * highcharts/plotoptions/column-minpointlength/ - * Zero base value - * @sample {highcharts} - * highcharts/plotoptions/column-minpointlength-pos-and-neg/ - * Positive and negative close to zero values - * @product highcharts highstock - */ - minPointLength: 0, - - /** - * When the series contains less points than the crop threshold, all - * points are drawn, event if the points fall outside the visible plot - * area at the current zoom. The advantage of drawing all points (including - * markers and columns), is that animation is performed on updates. - * On the other hand, when the series contains more points than the - * crop threshold, the series data is cropped to only contain points - * that fall within the plot area. The advantage of cropping away invisible - * points is to increase performance on large series. . - * - * @product highcharts highstock - */ - cropThreshold: 50, - - /** - * The X axis range that each point is valid for. This determines the - * width of the column. On a categorized axis, the range will be 1 - * by default (one category unit). On linear and datetime axes, the - * range will be computed as the distance between the two closest data - * points. - * - * The default `null` means it is computed automatically, but this option - * can be used to override the automatic value. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/column-pointrange/ - * Set the point range to one day on a data set with one week - * between the points - * @since 2.3 - * @product highcharts highstock - */ - pointRange: null, - - states: { - - /** - * Options for the hovered point. These settings override the normal - * state options when a point is moused over or touched. - * - * @extends plotOptions.series.states.hover - * @excluding halo,lineWidth,lineWidthPlus,marker - * @product highcharts highstock - */ - hover: { - - /** @ignore-option */ - halo: false, - - /** - * A specific border color for the hovered point. Defaults to - * inherit the normal state border color. - * - * @type {Color} - * @product highcharts - * @apioption plotOptions.column.states.hover.borderColor - */ - - /** - * A specific color for the hovered point. - * - * @type {Color} - * @default undefined - * @product highcharts - * @apioption plotOptions.column.states.hover.color - */ - - /*= if (build.classic) { =*/ - - /** - * How much to brighten the point on interaction. Requires the main - * color to be defined in hex or rgb(a) format. - * - * In styled mode, the hover brightening is by default replaced - * with a fill-opacity set in the `.highcharts-point:hover` rule. - * - * @sample {highcharts} - * highcharts/plotoptions/column-states-hover-brightness/ - * Brighten by 0.5 - * @product highcharts highstock - */ - brightness: 0.1 - - /*= } =*/ - }, - /*= if (build.classic) { =*/ - - /** - * Options for the selected point. These settings override the normal - * state options when a point is selected. - * - * @excluding halo,lineWidth,lineWidthPlus,marker - * @product highcharts highstock - */ - select: { - /** - * A specific color for the selected point. - * - * @type {Color} - * @default #cccccc - * @product highcharts highstock - */ - color: '${palette.neutralColor20}', - - /** - * A specific border color for the selected point. - * - * @type {Color} - * @default #000000 - * @product highcharts highstock - */ - borderColor: '${palette.neutralColor100}' - } - /*= } =*/ - }, - - dataLabels: { - align: null, // auto - verticalAlign: null, // auto - y: null - }, - - /** - * When this is true, the series will not cause the Y axis to cross - * the zero plane (or [threshold](#plotOptions.series.threshold) option) - * unless the data actually crosses the plane. - * - * For example, if `softThreshold` is `false`, a series of 0, 1, 2, - * 3 will make the Y axis show negative values according to the `minPadding` - * option. If `softThreshold` is `true`, the Y axis starts at 0. - * - * @since 4.1.9 - * @product highcharts highstock - */ - softThreshold: false, - - // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/ - /** - * @ignore - */ - startFromThreshold: true, - - stickyTracking: false, - - tooltip: { - distance: 6 - }, - - /** - * The Y axis value to serve as the base for the columns, for distinguishing - * between values above and below a threshold. If `null`, the columns - * extend from the padding Y axis minimum. - * - * @since 2.0 - * @product highcharts - */ - threshold: 0, - - /*= if (build.classic) { =*/ - - /** - * The width of the border surrounding each column or bar. - * - * In styled mode, the stroke width can be set with the `.highcharts-point` - * rule. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/column-borderwidth/ - * 2px black border - * @default 1 - * @product highcharts highstock - * @apioption plotOptions.column.borderWidth - */ - - /** - * The color of the border surrounding each column or bar. - * - * In styled mode, the border stroke can be set with the `.highcharts-point` - * rule. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/column-bordercolor/ - * Dark gray border - * @default #ffffff - * @product highcharts highstock - */ - borderColor: '${palette.backgroundColor}' - - /*= } =*/ + /** + * The corner radius of the border surrounding each column or bar. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-borderradius/ + * Rounded columns + * @default 0 + * @product highcharts highstock + */ + borderRadius: 0, + + /** + * When using automatic point colors pulled from the `options.colors` + * collection, this option determines whether the chart should receive + * one color per series or one color per point. + * + * @type {Boolean} + * @see [series colors](#plotOptions.column.colors) + * @sample {highcharts} highcharts/plotoptions/column-colorbypoint-false/ + * False by default + * @sample {highcharts} highcharts/plotoptions/column-colorbypoint-true/ + * True + * @default false + * @since 2.0 + * @product highcharts highstock + * @apioption plotOptions.column.colorByPoint + */ + + /** + * A series specific or series type specific color set to apply instead + * of the global [colors](#colors) when [colorByPoint]( + * #plotOptions.column.colorByPoint) is true. + * + * @type {Array} + * @since 3.0 + * @product highcharts highstock + * @apioption plotOptions.column.colors + */ + + /** + * When true, each column edge is rounded to its nearest pixel in order + * to render sharp on screen. In some cases, when there are a lot of + * densely packed columns, this leads to visible difference in column + * widths or distance between columns. In these cases, setting `crisp` + * to `false` may look better, even though each column is rendered + * blurry. + * + * @sample {highcharts} highcharts/plotoptions/column-crisp-false/ + * Crisp is false + * @since 5.0.10 + * @product highcharts highstock + */ + crisp: true, + + /** + * Padding between each value groups, in x axis units. + * + * @sample {highcharts} highcharts/plotoptions/column-grouppadding-default/ + * 0.2 by default + * @sample {highcharts} highcharts/plotoptions/column-grouppadding-none/ + * No group padding - all columns are evenly spaced + * @product highcharts highstock + */ + groupPadding: 0.2, + + /** + * Whether to group non-stacked columns or to let them render independent + * of each other. Non-grouped columns will be laid out individually + * and overlap each other. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/column-grouping-false/ + * Grouping disabled + * @sample {highstock} highcharts/plotoptions/column-grouping-false/ + * Grouping disabled + * @default true + * @since 2.3.0 + * @product highcharts highstock + * @apioption plotOptions.column.grouping + */ + + /** + * @ignore + */ + marker: null, // point options are specified in the base options + + /** + * The maximum allowed pixel width for a column, translated to the height + * of a bar in a bar chart. This prevents the columns from becoming + * too wide when there is a small number of points in the chart. + * + * @type {Number} + * @see [pointWidth](#plotOptions.column.pointWidth) + * @sample {highcharts} highcharts/plotoptions/column-maxpointwidth-20/ + * Limited to 50 + * @sample {highstock} highcharts/plotoptions/column-maxpointwidth-20/ + * Limited to 50 + * @default null + * @since 4.1.8 + * @product highcharts highstock + * @apioption plotOptions.column.maxPointWidth + */ + + /** + * Padding between each column or bar, in x axis units. + * + * @sample {highcharts} highcharts/plotoptions/column-pointpadding-default/ + * 0.1 by default + * @sample {highcharts} highcharts/plotoptions/column-pointpadding-025/ + * 0.25 + * @sample {highcharts} highcharts/plotoptions/column-pointpadding-none/ + * 0 for tightly packed columns + * @product highcharts highstock + */ + pointPadding: 0.1, + + /** + * A pixel value specifying a fixed width for each column or bar. When + * `null`, the width is calculated from the `pointPadding` and + * `groupPadding`. + * + * @type {Number} + * @see [maxPointWidth](#plotOptions.column.maxPointWidth) + * @sample {highcharts} highcharts/plotoptions/column-pointwidth-20/ + * 20px wide columns regardless of chart width or the amount + * of data points + * @default null + * @since 1.2.5 + * @product highcharts highstock + * @apioption plotOptions.column.pointWidth + */ + + /** + * The minimal height for a column or width for a bar. By default, + * 0 values are not shown. To visualize a 0 (or close to zero) point, + * set the minimal point length to a pixel value like 3\. In stacked + * column charts, minPointLength might not be respected for tightly + * packed values. + * + * @sample {highcharts} + * highcharts/plotoptions/column-minpointlength/ + * Zero base value + * @sample {highcharts} + * highcharts/plotoptions/column-minpointlength-pos-and-neg/ + * Positive and negative close to zero values + * @product highcharts highstock + */ + minPointLength: 0, + + /** + * When the series contains less points than the crop threshold, all + * points are drawn, event if the points fall outside the visible plot + * area at the current zoom. The advantage of drawing all points (including + * markers and columns), is that animation is performed on updates. + * On the other hand, when the series contains more points than the + * crop threshold, the series data is cropped to only contain points + * that fall within the plot area. The advantage of cropping away invisible + * points is to increase performance on large series. . + * + * @product highcharts highstock + */ + cropThreshold: 50, + + /** + * The X axis range that each point is valid for. This determines the + * width of the column. On a categorized axis, the range will be 1 + * by default (one category unit). On linear and datetime axes, the + * range will be computed as the distance between the two closest data + * points. + * + * The default `null` means it is computed automatically, but this option + * can be used to override the automatic value. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-pointrange/ + * Set the point range to one day on a data set with one week + * between the points + * @since 2.3 + * @product highcharts highstock + */ + pointRange: null, + + states: { + + /** + * Options for the hovered point. These settings override the normal + * state options when a point is moused over or touched. + * + * @extends plotOptions.series.states.hover + * @excluding halo,lineWidth,lineWidthPlus,marker + * @product highcharts highstock + */ + hover: { + + /** @ignore-option */ + halo: false, + + /** + * A specific border color for the hovered point. Defaults to + * inherit the normal state border color. + * + * @type {Color} + * @product highcharts + * @apioption plotOptions.column.states.hover.borderColor + */ + + /** + * A specific color for the hovered point. + * + * @type {Color} + * @default undefined + * @product highcharts + * @apioption plotOptions.column.states.hover.color + */ + + /*= if (build.classic) { =*/ + + /** + * How much to brighten the point on interaction. Requires the main + * color to be defined in hex or rgb(a) format. + * + * In styled mode, the hover brightening is by default replaced + * with a fill-opacity set in the `.highcharts-point:hover` rule. + * + * @sample {highcharts} + * highcharts/plotoptions/column-states-hover-brightness/ + * Brighten by 0.5 + * @product highcharts highstock + */ + brightness: 0.1 + + /*= } =*/ + }, + /*= if (build.classic) { =*/ + + /** + * Options for the selected point. These settings override the normal + * state options when a point is selected. + * + * @excluding halo,lineWidth,lineWidthPlus,marker + * @product highcharts highstock + */ + select: { + /** + * A specific color for the selected point. + * + * @type {Color} + * @default #cccccc + * @product highcharts highstock + */ + color: '${palette.neutralColor20}', + + /** + * A specific border color for the selected point. + * + * @type {Color} + * @default #000000 + * @product highcharts highstock + */ + borderColor: '${palette.neutralColor100}' + } + /*= } =*/ + }, + + dataLabels: { + align: null, // auto + verticalAlign: null, // auto + y: null + }, + + /** + * When this is true, the series will not cause the Y axis to cross + * the zero plane (or [threshold](#plotOptions.series.threshold) option) + * unless the data actually crosses the plane. + * + * For example, if `softThreshold` is `false`, a series of 0, 1, 2, + * 3 will make the Y axis show negative values according to the `minPadding` + * option. If `softThreshold` is `true`, the Y axis starts at 0. + * + * @since 4.1.9 + * @product highcharts highstock + */ + softThreshold: false, + + // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/ + /** + * @ignore + */ + startFromThreshold: true, + + stickyTracking: false, + + tooltip: { + distance: 6 + }, + + /** + * The Y axis value to serve as the base for the columns, for distinguishing + * between values above and below a threshold. If `null`, the columns + * extend from the padding Y axis minimum. + * + * @since 2.0 + * @product highcharts + */ + threshold: 0, + + /*= if (build.classic) { =*/ + + /** + * The width of the border surrounding each column or bar. + * + * In styled mode, the stroke width can be set with the `.highcharts-point` + * rule. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-borderwidth/ + * 2px black border + * @default 1 + * @product highcharts highstock + * @apioption plotOptions.column.borderWidth + */ + + /** + * The color of the border surrounding each column or bar. + * + * In styled mode, the border stroke can be set with the `.highcharts-point` + * rule. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/column-bordercolor/ + * Dark gray border + * @default #ffffff + * @product highcharts highstock + */ + borderColor: '${palette.backgroundColor}' + + /*= } =*/ }, /** @lends seriesTypes.column.prototype */ { - cropShoulder: 0, - // When tooltip is not shared, this series (and derivatives) requires direct - // touch/hover. KD-tree does not apply. - directTouch: true, - trackerGroups: ['group', 'dataLabelsGroup'], - // use separate negative stacks, unlike area stacks where a negative point - // is substracted from previous (#1910) - negStacks: true, - - /** - * Initialize the series. Extends the basic Series.init method by - * marking other series of the same type as dirty. - * - * @function #init - * @memberOf seriesTypes.column - * - */ - init: function () { - Series.prototype.init.apply(this, arguments); - - var series = this, - chart = series.chart; - - // if the series is added dynamically, force redraw of other - // series affected by a new column - if (chart.hasRendered) { - each(chart.series, function (otherSeries) { - if (otherSeries.type === series.type) { - otherSeries.isDirty = true; - } - }); - } - }, - - /** - * Return the width and x offset of the columns adjusted for grouping, - * groupPadding, pointPadding, pointWidth etc. - */ - getColumnMetrics: function () { - - var series = this, - options = series.options, - xAxis = series.xAxis, - yAxis = series.yAxis, - reversedXAxis = xAxis.reversed, - stackKey, - stackGroups = {}, - columnCount = 0; - - // Get the total number of column type series. This is called on every - // series. Consider moving this logic to a chart.orderStacks() function - // and call it on init, addSeries and removeSeries - if (options.grouping === false) { - columnCount = 1; - } else { - each(series.chart.series, function (otherSeries) { - var otherOptions = otherSeries.options, - otherYAxis = otherSeries.yAxis, - columnIndex; - if ( - otherSeries.type === series.type && - ( - otherSeries.visible || - !series.chart.options.chart.ignoreHiddenSeries - ) && - yAxis.len === otherYAxis.len && - yAxis.pos === otherYAxis.pos - ) { // #642, #2086 - if (otherOptions.stacking) { - stackKey = otherSeries.stackKey; - if (stackGroups[stackKey] === undefined) { - stackGroups[stackKey] = columnCount++; - } - columnIndex = stackGroups[stackKey]; - } else if (otherOptions.grouping !== false) { // #1162 - columnIndex = columnCount++; - } - otherSeries.columnIndex = columnIndex; - } - }); - } - - var categoryWidth = Math.min( - Math.abs(xAxis.transA) * ( - xAxis.ordinalSlope || - options.pointRange || - xAxis.closestPointRange || - xAxis.tickInterval || - 1 - ), // #2610 - xAxis.len // #1535 - ), - groupPadding = categoryWidth * options.groupPadding, - groupWidth = categoryWidth - 2 * groupPadding, - pointOffsetWidth = groupWidth / (columnCount || 1), - pointWidth = Math.min( - options.maxPointWidth || xAxis.len, - pick( - options.pointWidth, - pointOffsetWidth * (1 - 2 * options.pointPadding) - ) - ), - pointPadding = (pointOffsetWidth - pointWidth) / 2, - // #1251, #3737 - colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), - pointXOffset = - pointPadding + - ( - groupPadding + - colIndex * pointOffsetWidth - - (categoryWidth / 2) - ) * (reversedXAxis ? -1 : 1); - - // Save it for reading in linked series (Error bars particularly) - series.columnMetrics = { - width: pointWidth, - offset: pointXOffset - }; - return series.columnMetrics; - - }, - - /** - * Make the columns crisp. The edges are rounded to the nearest full pixel. - */ - crispCol: function (x, y, w, h) { - var chart = this.chart, - borderWidth = this.borderWidth, - xCrisp = -(borderWidth % 2 ? 0.5 : 0), - yCrisp = borderWidth % 2 ? 0.5 : 1, - right, - bottom, - fromTop; - - if (chart.inverted && chart.renderer.isVML) { - yCrisp += 1; - } - - // Horizontal. We need to first compute the exact right edge, then round - // it and compute the width from there. - if (this.options.crisp) { - right = Math.round(x + w) + xCrisp; - x = Math.round(x) + xCrisp; - w = right - x; - } - - // Vertical - bottom = Math.round(y + h) + yCrisp; - fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656 - y = Math.round(y) + yCrisp; - h = bottom - y; - - // Top edges are exceptions - if (fromTop && h) { // #5146 - y -= 1; - h += 1; - } - - return { - x: x, - y: y, - width: w, - height: h - }; - }, - - /** - * Translate each point to the plot area coordinate system and find shape - * positions - */ - translate: function () { - var series = this, - chart = series.chart, - options = series.options, - dense = series.dense = - series.closestPointRange * series.xAxis.transA < 2, - borderWidth = series.borderWidth = pick( - options.borderWidth, - dense ? 0 : 1 // #3635 - ), - yAxis = series.yAxis, - threshold = options.threshold, - translatedThreshold = series.translatedThreshold = - yAxis.getThreshold(threshold), - minPointLength = pick(options.minPointLength, 5), - metrics = series.getColumnMetrics(), - pointWidth = metrics.width, - // postprocessed for border width - seriesBarW = series.barW = - Math.max(pointWidth, 1 + 2 * borderWidth), - pointXOffset = series.pointXOffset = metrics.offset; - - if (chart.inverted) { - translatedThreshold -= 0.5; // #3355 - } - - // When the pointPadding is 0, we want the columns to be packed tightly, - // so we allow individual columns to have individual sizes. When - // pointPadding is greater, we strive for equal-width columns (#2694). - if (options.pointPadding) { - seriesBarW = Math.ceil(seriesBarW); - } - - Series.prototype.translate.apply(series); - - // Record the new values - each(series.points, function (point) { - var yBottom = pick(point.yBottom, translatedThreshold), - safeDistance = 999 + Math.abs(yBottom), - plotY = Math.min( - Math.max(-safeDistance, point.plotY), - yAxis.len + safeDistance - ), // Don't draw too far outside plot area (#1303, #2241, #4264) - barX = point.plotX + pointXOffset, - barW = seriesBarW, - barY = Math.min(plotY, yBottom), - up, - barH = Math.max(plotY, yBottom) - barY; - - // Handle options.minPointLength - if (minPointLength && Math.abs(barH) < minPointLength) { - barH = minPointLength; - up = (!yAxis.reversed && !point.negative) || - (yAxis.reversed && point.negative); - - // Reverse zeros if there's no positive value in the series - // in visible range (#7046) - if ( - point.y === threshold && - series.dataMax <= threshold && - yAxis.min < threshold // and if there's room for it (#7311) - ) { - up = !up; - } - - // If stacked... - barY = Math.abs(barY - translatedThreshold) > minPointLength ? - // ...keep position - yBottom - minPointLength : - // #1485, #4051 - translatedThreshold - (up ? minPointLength : 0); - } - - // Cache for access in polar - point.barX = barX; - point.pointWidth = pointWidth; - - // Fix the tooltip on center of grouped columns (#1216, #424, #3648) - point.tooltipPos = chart.inverted ? - [ - yAxis.len + yAxis.pos - chart.plotLeft - plotY, - series.xAxis.len - barX - barW / 2, barH - ] : - [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH]; - - // Register shape type and arguments to be used in drawPoints - point.shapeType = 'rect'; - point.shapeArgs = series.crispCol.apply( - series, - point.isNull ? - // #3169, drilldown from null must have a position to work - // from #6585, dataLabel should be placed on xAxis, not - // floating in the middle of the chart - [barX, translatedThreshold, barW, 0] : - [barX, barY, barW, barH] - ); - }); - - }, - - getSymbol: noop, - - /** - * Use a solid rectangle like the area series types - */ - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - - - /** - * Columns have no graph - */ - drawGraph: function () { - this.group[ - this.dense ? 'addClass' : 'removeClass' - ]('highcharts-dense-data'); - }, - - /*= if (build.classic) { =*/ - /** - * Get presentational attributes - */ - pointAttribs: function (point, state) { - var options = this.options, - stateOptions, - ret, - p2o = this.pointAttrToOptions || {}, - strokeOption = p2o.stroke || 'borderColor', - strokeWidthOption = p2o['stroke-width'] || 'borderWidth', - fill = (point && point.color) || this.color, - stroke = (point && point[strokeOption]) || options[strokeOption] || - this.color || fill, // set to fill when borderColor null - strokeWidth = (point && point[strokeWidthOption]) || - options[strokeWidthOption] || this[strokeWidthOption] || 0, - dashstyle = options.dashStyle, - zone, - brightness; - - // Handle zone colors - if (point && this.zones.length) { - zone = point.getZone(); - // When zones are present, don't use point.color (#4267). Changed - // order (#6527) - fill = point.options.color || (zone && zone.color) || this.color; - } - - // Select or hover states - if (state) { - stateOptions = merge( - options.states[state], - // #6401 - point.options.states && point.options.states[state] || {} - ); - brightness = stateOptions.brightness; - fill = stateOptions.color || - ( - brightness !== undefined && - color(fill).brighten(stateOptions.brightness).get() - ) || - fill; - stroke = stateOptions[strokeOption] || stroke; - strokeWidth = stateOptions[strokeWidthOption] || strokeWidth; - dashstyle = stateOptions.dashStyle || dashstyle; - } - - ret = { - 'fill': fill, - 'stroke': stroke, - 'stroke-width': strokeWidth - }; - - if (dashstyle) { - ret.dashstyle = dashstyle; - } - - return ret; - }, - /*= } =*/ - - /** - * Draw the columns. For bars, the series.group is rotated, so the same - * coordinates apply for columns and bars. This method is inherited by - * scatter series. - */ - drawPoints: function () { - var series = this, - chart = this.chart, - options = series.options, - renderer = chart.renderer, - animationLimit = options.animationLimit || 250, - shapeArgs; - - // draw the columns - each(series.points, function (point) { - var plotY = point.plotY, - graphic = point.graphic, - verb = graphic && chart.pointCount < animationLimit ? - 'animate' : 'attr'; - - if (isNumber(plotY) && point.y !== null) { - shapeArgs = point.shapeArgs; - - if (graphic) { // update - graphic[verb]( - merge(shapeArgs) - ); - - } else { - point.graphic = graphic = - renderer[point.shapeType](shapeArgs) - .add(point.group || series.group); - } - - // Border radius is not stylable (#6900) - if (options.borderRadius) { - graphic.attr({ - r: options.borderRadius - }); - } - - /*= if (build.classic) { =*/ - // Presentational - graphic[verb](series.pointAttribs( - point, - point.selected && 'select' - )) - .shadow( - options.shadow, - null, - options.stacking && !options.borderRadius - ); - /*= } =*/ - - graphic.addClass(point.getClassName(), true); - - - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 - } - }); - }, - - /** - * Animate the column heights one by one from zero - * @param {Boolean} init Whether to initialize the animation or run it - */ - animate: function (init) { - var series = this, - yAxis = this.yAxis, - options = series.options, - inverted = this.chart.inverted, - attr = {}, - translateProp = inverted ? 'translateX' : 'translateY', - translateStart, - translatedThreshold; - - if (svg) { // VML is too slow anyway - if (init) { - attr.scaleY = 0.001; - translatedThreshold = Math.min( - yAxis.pos + yAxis.len, - Math.max(yAxis.pos, yAxis.toPixels(options.threshold)) - ); - if (inverted) { - attr.translateX = translatedThreshold - yAxis.len; - } else { - attr.translateY = translatedThreshold; - } - series.group.attr(attr); - - } else { // run the animation - translateStart = series.group.attr(translateProp); - series.group.animate( - { scaleY: 1 }, - extend(animObject(series.options.animation - ), { - // Do the scale synchronously to ensure smooth updating - // (#5030, #7228) - step: function (val, fx) { - - attr[translateProp] = - translateStart + - fx.pos * (yAxis.pos - translateStart); - series.group.attr(attr); - } - })); - - // delete this function to allow it only once - series.animate = null; - } - } - }, - - /** - * Remove this series from the chart - */ - remove: function () { - var series = this, - chart = series.chart; - - // column and bar series affects other series of the same type - // as they are either stacked or grouped - if (chart.hasRendered) { - each(chart.series, function (otherSeries) { - if (otherSeries.type === series.type) { - otherSeries.isDirty = true; - } - }); - } - - Series.prototype.remove.apply(series, arguments); - } + cropShoulder: 0, + // When tooltip is not shared, this series (and derivatives) requires direct + // touch/hover. KD-tree does not apply. + directTouch: true, + trackerGroups: ['group', 'dataLabelsGroup'], + // use separate negative stacks, unlike area stacks where a negative point + // is substracted from previous (#1910) + negStacks: true, + + /** + * Initialize the series. Extends the basic Series.init method by + * marking other series of the same type as dirty. + * + * @function #init + * @memberOf seriesTypes.column + * + */ + init: function () { + Series.prototype.init.apply(this, arguments); + + var series = this, + chart = series.chart; + + // if the series is added dynamically, force redraw of other + // series affected by a new column + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + }, + + /** + * Return the width and x offset of the columns adjusted for grouping, + * groupPadding, pointPadding, pointWidth etc. + */ + getColumnMetrics: function () { + + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + reversedXAxis = xAxis.reversed, + stackKey, + stackGroups = {}, + columnCount = 0; + + // Get the total number of column type series. This is called on every + // series. Consider moving this logic to a chart.orderStacks() function + // and call it on init, addSeries and removeSeries + if (options.grouping === false) { + columnCount = 1; + } else { + each(series.chart.series, function (otherSeries) { + var otherOptions = otherSeries.options, + otherYAxis = otherSeries.yAxis, + columnIndex; + if ( + otherSeries.type === series.type && + ( + otherSeries.visible || + !series.chart.options.chart.ignoreHiddenSeries + ) && + yAxis.len === otherYAxis.len && + yAxis.pos === otherYAxis.pos + ) { // #642, #2086 + if (otherOptions.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === undefined) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherOptions.grouping !== false) { // #1162 + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + } + + var categoryWidth = Math.min( + Math.abs(xAxis.transA) * ( + xAxis.ordinalSlope || + options.pointRange || + xAxis.closestPointRange || + xAxis.tickInterval || + 1 + ), // #2610 + xAxis.len // #1535 + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / (columnCount || 1), + pointWidth = Math.min( + options.maxPointWidth || xAxis.len, + pick( + options.pointWidth, + pointOffsetWidth * (1 - 2 * options.pointPadding) + ) + ), + pointPadding = (pointOffsetWidth - pointWidth) / 2, + // #1251, #3737 + colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), + pointXOffset = + pointPadding + + ( + groupPadding + + colIndex * pointOffsetWidth - + (categoryWidth / 2) + ) * (reversedXAxis ? -1 : 1); + + // Save it for reading in linked series (Error bars particularly) + series.columnMetrics = { + width: pointWidth, + offset: pointXOffset + }; + return series.columnMetrics; + + }, + + /** + * Make the columns crisp. The edges are rounded to the nearest full pixel. + */ + crispCol: function (x, y, w, h) { + var chart = this.chart, + borderWidth = this.borderWidth, + xCrisp = -(borderWidth % 2 ? 0.5 : 0), + yCrisp = borderWidth % 2 ? 0.5 : 1, + right, + bottom, + fromTop; + + if (chart.inverted && chart.renderer.isVML) { + yCrisp += 1; + } + + // Horizontal. We need to first compute the exact right edge, then round + // it and compute the width from there. + if (this.options.crisp) { + right = Math.round(x + w) + xCrisp; + x = Math.round(x) + xCrisp; + w = right - x; + } + + // Vertical + bottom = Math.round(y + h) + yCrisp; + fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656 + y = Math.round(y) + yCrisp; + h = bottom - y; + + // Top edges are exceptions + if (fromTop && h) { // #5146 + y -= 1; + h += 1; + } + + return { + x: x, + y: y, + width: w, + height: h + }; + }, + + /** + * Translate each point to the plot area coordinate system and find shape + * positions + */ + translate: function () { + var series = this, + chart = series.chart, + options = series.options, + dense = series.dense = + series.closestPointRange * series.xAxis.transA < 2, + borderWidth = series.borderWidth = pick( + options.borderWidth, + dense ? 0 : 1 // #3635 + ), + yAxis = series.yAxis, + threshold = options.threshold, + translatedThreshold = series.translatedThreshold = + yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5), + metrics = series.getColumnMetrics(), + pointWidth = metrics.width, + // postprocessed for border width + seriesBarW = series.barW = + Math.max(pointWidth, 1 + 2 * borderWidth), + pointXOffset = series.pointXOffset = metrics.offset; + + if (chart.inverted) { + translatedThreshold -= 0.5; // #3355 + } + + // When the pointPadding is 0, we want the columns to be packed tightly, + // so we allow individual columns to have individual sizes. When + // pointPadding is greater, we strive for equal-width columns (#2694). + if (options.pointPadding) { + seriesBarW = Math.ceil(seriesBarW); + } + + Series.prototype.translate.apply(series); + + // Record the new values + each(series.points, function (point) { + var yBottom = pick(point.yBottom, translatedThreshold), + safeDistance = 999 + Math.abs(yBottom), + plotY = Math.min( + Math.max(-safeDistance, point.plotY), + yAxis.len + safeDistance + ), // Don't draw too far outside plot area (#1303, #2241, #4264) + barX = point.plotX + pointXOffset, + barW = seriesBarW, + barY = Math.min(plotY, yBottom), + up, + barH = Math.max(plotY, yBottom) - barY; + + // Handle options.minPointLength + if (minPointLength && Math.abs(barH) < minPointLength) { + barH = minPointLength; + up = (!yAxis.reversed && !point.negative) || + (yAxis.reversed && point.negative); + + // Reverse zeros if there's no positive value in the series + // in visible range (#7046) + if ( + point.y === threshold && + series.dataMax <= threshold && + yAxis.min < threshold // and if there's room for it (#7311) + ) { + up = !up; + } + + // If stacked... + barY = Math.abs(barY - translatedThreshold) > minPointLength ? + // ...keep position + yBottom - minPointLength : + // #1485, #4051 + translatedThreshold - (up ? minPointLength : 0); + } + + // Cache for access in polar + point.barX = barX; + point.pointWidth = pointWidth; + + // Fix the tooltip on center of grouped columns (#1216, #424, #3648) + point.tooltipPos = chart.inverted ? + [ + yAxis.len + yAxis.pos - chart.plotLeft - plotY, + series.xAxis.len - barX - barW / 2, barH + ] : + [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH]; + + // Register shape type and arguments to be used in drawPoints + point.shapeType = 'rect'; + point.shapeArgs = series.crispCol.apply( + series, + point.isNull ? + // #3169, drilldown from null must have a position to work + // from #6585, dataLabel should be placed on xAxis, not + // floating in the middle of the chart + [barX, translatedThreshold, barW, 0] : + [barX, barY, barW, barH] + ); + }); + + }, + + getSymbol: noop, + + /** + * Use a solid rectangle like the area series types + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + + /** + * Columns have no graph + */ + drawGraph: function () { + this.group[ + this.dense ? 'addClass' : 'removeClass' + ]('highcharts-dense-data'); + }, + + /*= if (build.classic) { =*/ + /** + * Get presentational attributes + */ + pointAttribs: function (point, state) { + var options = this.options, + stateOptions, + ret, + p2o = this.pointAttrToOptions || {}, + strokeOption = p2o.stroke || 'borderColor', + strokeWidthOption = p2o['stroke-width'] || 'borderWidth', + fill = (point && point.color) || this.color, + stroke = (point && point[strokeOption]) || options[strokeOption] || + this.color || fill, // set to fill when borderColor null + strokeWidth = (point && point[strokeWidthOption]) || + options[strokeWidthOption] || this[strokeWidthOption] || 0, + dashstyle = options.dashStyle, + zone, + brightness; + + // Handle zone colors + if (point && this.zones.length) { + zone = point.getZone(); + // When zones are present, don't use point.color (#4267). Changed + // order (#6527) + fill = point.options.color || (zone && zone.color) || this.color; + } + + // Select or hover states + if (state) { + stateOptions = merge( + options.states[state], + // #6401 + point.options.states && point.options.states[state] || {} + ); + brightness = stateOptions.brightness; + fill = stateOptions.color || + ( + brightness !== undefined && + color(fill).brighten(stateOptions.brightness).get() + ) || + fill; + stroke = stateOptions[strokeOption] || stroke; + strokeWidth = stateOptions[strokeWidthOption] || strokeWidth; + dashstyle = stateOptions.dashStyle || dashstyle; + } + + ret = { + 'fill': fill, + 'stroke': stroke, + 'stroke-width': strokeWidth + }; + + if (dashstyle) { + ret.dashstyle = dashstyle; + } + + return ret; + }, + /*= } =*/ + + /** + * Draw the columns. For bars, the series.group is rotated, so the same + * coordinates apply for columns and bars. This method is inherited by + * scatter series. + */ + drawPoints: function () { + var series = this, + chart = this.chart, + options = series.options, + renderer = chart.renderer, + animationLimit = options.animationLimit || 250, + shapeArgs; + + // draw the columns + each(series.points, function (point) { + var plotY = point.plotY, + graphic = point.graphic, + verb = graphic && chart.pointCount < animationLimit ? + 'animate' : 'attr'; + + if (isNumber(plotY) && point.y !== null) { + shapeArgs = point.shapeArgs; + + if (graphic) { // update + graphic[verb]( + merge(shapeArgs) + ); + + } else { + point.graphic = graphic = + renderer[point.shapeType](shapeArgs) + .add(point.group || series.group); + } + + // Border radius is not stylable (#6900) + if (options.borderRadius) { + graphic.attr({ + r: options.borderRadius + }); + } + + /*= if (build.classic) { =*/ + // Presentational + graphic[verb](series.pointAttribs( + point, + point.selected && 'select' + )) + .shadow( + options.shadow, + null, + options.stacking && !options.borderRadius + ); + /*= } =*/ + + graphic.addClass(point.getClassName(), true); + + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + }); + }, + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function (init) { + var series = this, + yAxis = this.yAxis, + options = series.options, + inverted = this.chart.inverted, + attr = {}, + translateProp = inverted ? 'translateX' : 'translateY', + translateStart, + translatedThreshold; + + if (svg) { // VML is too slow anyway + if (init) { + attr.scaleY = 0.001; + translatedThreshold = Math.min( + yAxis.pos + yAxis.len, + Math.max(yAxis.pos, yAxis.toPixels(options.threshold)) + ); + if (inverted) { + attr.translateX = translatedThreshold - yAxis.len; + } else { + attr.translateY = translatedThreshold; + } + series.group.attr(attr); + + } else { // run the animation + translateStart = series.group.attr(translateProp); + series.group.animate( + { scaleY: 1 }, + extend(animObject(series.options.animation + ), { + // Do the scale synchronously to ensure smooth updating + // (#5030, #7228) + step: function (val, fx) { + + attr[translateProp] = + translateStart + + fx.pos * (yAxis.pos - translateStart); + series.group.attr(attr); + } + })); + + // delete this function to allow it only once + series.animate = null; + } + } + }, + + /** + * Remove this series from the chart + */ + remove: function () { + var series = this, + chart = series.chart; + + // column and bar series affects other series of the same type + // as they are either stacked or grouped + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + + Series.prototype.remove.apply(series, arguments); + } }); diff --git a/js/parts/DataGrouping.js b/js/parts/DataGrouping.js index 40952719f38..7b7ec62980c 100644 --- a/js/parts/DataGrouping.js +++ b/js/parts/DataGrouping.js @@ -3,7 +3,7 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from './Globals.js'; import './Utilities.js'; @@ -11,24 +11,24 @@ import './Axis.js'; import './Series.js'; import './Tooltip.js'; var addEvent = H.addEvent, - arrayMax = H.arrayMax, - arrayMin = H.arrayMin, - Axis = H.Axis, - defaultPlotOptions = H.defaultPlotOptions, - defined = H.defined, - each = H.each, - extend = H.extend, - format = H.format, - isNumber = H.isNumber, - merge = H.merge, - pick = H.pick, - Point = H.Point, - Series = H.Series, - Tooltip = H.Tooltip, - wrap = H.wrap; - + arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + Axis = H.Axis, + defaultPlotOptions = H.defaultPlotOptions, + defined = H.defined, + each = H.each, + extend = H.extend, + format = H.format, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + Point = H.Point, + Series = H.Series, + Tooltip = H.Tooltip, + wrap = H.wrap; + /* **************************************************************************** - * Start data grouping module * + * Start data grouping module * ******************************************************************************/ /** @@ -37,10 +37,10 @@ var addEvent = H.addEvent, * 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 */ @@ -55,7 +55,7 @@ var addEvent = H.addEvent, * 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 @@ -65,14 +65,14 @@ var addEvent = H.addEvent, * 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 @@ -85,9 +85,9 @@ var addEvent = H.addEvent, * 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: - * + * *
{
  *     millisecond: [
  *         '%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'
@@ -100,7 +100,7 @@ var addEvent = H.addEvent,
  *     month: ['%B %Y', '%B', '-%B %Y'],
  *     year: ['%Y', '%Y', '-%Y']
  * }
- * + * * 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 @@ -108,7 +108,7 @@ var addEvent = H.addEvent, * 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 @@ -116,7 +116,7 @@ var addEvent = H.addEvent, /** * Enable or disable data grouping. - * + * * @type {Boolean} * @default true * @product highstock @@ -127,7 +127,7 @@ var addEvent = H.addEvent, * 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 @@ -145,7 +145,7 @@ var addEvent = H.addEvent, * 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 @@ -159,7 +159,7 @@ var addEvent = H.addEvent, * a grouped point partially. The effect is similar to * [Series.getExtremesFromAll](#plotOptions.series.getExtremesFromAll) but does * not affect yAxis extremes. - * + * * @type {Boolean} * @sample {highstock} stock/plotoptions/series-datagrouping-groupall/ * Two series with the same data but different groupAll setting @@ -176,7 +176,7 @@ var addEvent = H.addEvent, * 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 @@ -188,7 +188,7 @@ var addEvent = H.addEvent, * 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: - * + * *
units: [[
  *     'millisecond', // unit name
  *     [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
@@ -214,7 +214,7 @@ var addEvent = H.addEvent,
  *     'year',
  *     null
  * ]]
- * + * * @type {Array} * @product highstock * @apioption plotOptions.series.dataGrouping.units @@ -227,7 +227,7 @@ var addEvent = H.addEvent, * 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 @@ -237,352 +237,352 @@ var addEvent = H.addEvent, */ var seriesProto = Series.prototype, - baseProcessData = seriesProto.processData, - baseGeneratePoints = seriesProto.generatePoints, - - /** - * - */ - commonOptions = { - approximation: 'average', // average, open, high, low, close, sum - // 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' - ], - 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' - ] - } - // smoothed = false, // enable this for navigator series only - }, - - specificOptions = { // extends common options - line: {}, - spline: {}, - area: {}, - areaspline: {}, - column: { - approximation: 'sum', - groupPixelWidth: 10 - }, - arearange: { - approximation: 'range' - }, - areasplinerange: { - approximation: 'range' - }, - columnrange: { - approximation: 'range', - groupPixelWidth: 10 - }, - candlestick: { - approximation: 'ohlc', - groupPixelWidth: 10 - }, - ohlc: { - approximation: 'ohlc', - groupPixelWidth: 5 - } - }, - - // units are defined in a separate array to allow complete overriding in - // case of a user option - defaultDataGroupingUnits = H.defaultDataGroupingUnits = [ - [ - '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 - ] - ], - - - /** - * Define the available approximation types. The data grouping - * 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 = H.approximations = { - sum: function (arr) { - var len = arr.length, - ret; - - // 1. it consists of nulls exclusively - if (!len && arr.hasNulls) { - ret = null; - // 2. it has a length and real values - } else if (len) { - ret = 0; - while (len--) { - ret += arr[len]; - } - } - // 3. it has zero length, so just return undefined - // => doNothing() - - return ret; - }, - average: function (arr) { - var len = arr.length, - ret = approximations.sum(arr); - - // If we have a number, return it divided by the length. If not, - // return null or undefined based on what the sum method finds. - if (isNumber(ret) && len) { - ret = ret / len; - } - - return ret; - }, - // The same as average, but for series with multiple values, like area - // ranges. - averages: function () { // #5479 - var ret = []; - - each(arguments, function (arr) { - ret.push(approximations.average(arr)); - }); - - // Return undefined when first elem. is undefined and let - // sum method handle null (#7377) - return ret[0] === undefined ? undefined : ret; - }, - open: function (arr) { - return arr.length ? arr[0] : (arr.hasNulls ? null : undefined); - }, - high: function (arr) { - return arr.length ? - arrayMax(arr) : - (arr.hasNulls ? null : undefined); - }, - low: function (arr) { - return arr.length ? - arrayMin(arr) : - (arr.hasNulls ? null : undefined); - }, - close: function (arr) { - return arr.length ? - arr[arr.length - 1] : - (arr.hasNulls ? null : undefined); - }, - // ohlc and range are special cases where a multidimensional array is - // input and an array is output - ohlc: function (open, high, low, close) { - open = approximations.open(open); - high = approximations.high(high); - low = approximations.low(low); - close = approximations.close(close); - - if ( - isNumber(open) || - isNumber(high) || - isNumber(low) || - isNumber(close) - ) { - return [open, high, low, close]; - } - // else, return is undefined - }, - range: function (low, high) { - low = approximations.low(low); - high = approximations.high(high); - - if (isNumber(low) || isNumber(high)) { - return [low, high]; - } else if (low === null && high === null) { - return null; - } - // else, return is undefined - } - }; + baseProcessData = seriesProto.processData, + baseGeneratePoints = seriesProto.generatePoints, + + /** + * + */ + commonOptions = { + approximation: 'average', // average, open, high, low, close, sum + // 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' + ], + 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' + ] + } + // smoothed = false, // enable this for navigator series only + }, + + specificOptions = { // extends common options + line: {}, + spline: {}, + area: {}, + areaspline: {}, + column: { + approximation: 'sum', + groupPixelWidth: 10 + }, + arearange: { + approximation: 'range' + }, + areasplinerange: { + approximation: 'range' + }, + columnrange: { + approximation: 'range', + groupPixelWidth: 10 + }, + candlestick: { + approximation: 'ohlc', + groupPixelWidth: 10 + }, + ohlc: { + approximation: 'ohlc', + groupPixelWidth: 5 + } + }, + + // units are defined in a separate array to allow complete overriding in + // case of a user option + defaultDataGroupingUnits = H.defaultDataGroupingUnits = [ + [ + '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 + ] + ], + + + /** + * Define the available approximation types. The data grouping + * 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 = H.approximations = { + sum: function (arr) { + var len = arr.length, + ret; + + // 1. it consists of nulls exclusively + if (!len && arr.hasNulls) { + ret = null; + // 2. it has a length and real values + } else if (len) { + ret = 0; + while (len--) { + ret += arr[len]; + } + } + // 3. it has zero length, so just return undefined + // => doNothing() + + return ret; + }, + average: function (arr) { + var len = arr.length, + ret = approximations.sum(arr); + + // If we have a number, return it divided by the length. If not, + // return null or undefined based on what the sum method finds. + if (isNumber(ret) && len) { + ret = ret / len; + } + + return ret; + }, + // The same as average, but for series with multiple values, like area + // ranges. + averages: function () { // #5479 + var ret = []; + + each(arguments, function (arr) { + ret.push(approximations.average(arr)); + }); + + // Return undefined when first elem. is undefined and let + // sum method handle null (#7377) + return ret[0] === undefined ? undefined : ret; + }, + open: function (arr) { + return arr.length ? arr[0] : (arr.hasNulls ? null : undefined); + }, + high: function (arr) { + return arr.length ? + arrayMax(arr) : + (arr.hasNulls ? null : undefined); + }, + low: function (arr) { + return arr.length ? + arrayMin(arr) : + (arr.hasNulls ? null : undefined); + }, + close: function (arr) { + return arr.length ? + arr[arr.length - 1] : + (arr.hasNulls ? null : undefined); + }, + // ohlc and range are special cases where a multidimensional array is + // input and an array is output + ohlc: function (open, high, low, close) { + open = approximations.open(open); + high = approximations.high(high); + low = approximations.low(low); + close = approximations.close(close); + + if ( + isNumber(open) || + isNumber(high) || + isNumber(low) || + isNumber(close) + ) { + return [open, high, low, close]; + } + // else, return is undefined + }, + range: function (low, high) { + low = approximations.low(low); + high = approximations.high(high); + + if (isNumber(low) || isNumber(high)) { + return [low, high]; + } else if (low === null && high === null) { + return null; + } + // else, return is undefined + } + }; /** - * Takes parallel arrays of x and y data and groups the data into intervals + * 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) { - var series = this, - data = series.data, - dataOptions = series.options.data, - groupedXData = [], - groupedYData = [], - groupMap = [], - dataLength = xData.length, - pointX, - pointY, - groupedY, - // when grouping the fake extended axis for panning, - // we don't need to consider y - handleYData = !!yData, - values = [], - approximationFn = typeof approximation === 'function' ? - approximation : - approximations[approximation] || - // if the approximation is not found use default series type - // approximation (#2914) - ( - specificOptions[series.type] && - approximations[specificOptions[series.type].approximation] - ) || approximations[commonOptions.approximation], - pointArrayMap = series.pointArrayMap, - pointArrayMapLength = pointArrayMap && pointArrayMap.length, - pos = 0, - start = 0, - valuesLen, - i, j; - - // Calculate values array size from pointArrayMap length - if (pointArrayMapLength) { - each(pointArrayMap, function () { - values.push([]); - }); - } else { - values.push([]); - } - valuesLen = pointArrayMapLength || 1; - - // Start with the first point within the X axis range (#2696) - for (i = 0; i <= dataLength; i++) { - if (xData[i] >= groupPositions[0]) { - break; - } - } - - for (i; i <= dataLength; i++) { - - // when a new group is entered, summarize and initiate - // the previous group - while (( - groupPositions[pos + 1] !== undefined && - xData[i] >= groupPositions[pos + 1] - ) || i === dataLength) { // get the last group - - // get group x and y - pointX = groupPositions[pos]; - series.dataGroupInfo = { start: start, length: values[0].length }; - groupedY = approximationFn.apply(series, values); - - // push the grouped data - if (groupedY !== undefined) { - groupedXData.push(pointX); - groupedYData.push(groupedY); - groupMap.push(series.dataGroupInfo); - } - - // reset the aggregate arrays - start = i; - for (j = 0; j < valuesLen; j++) { - values[j].length = 0; // faster than values[j] = [] - values[j].hasNulls = false; - } - - // Advance on the group positions - pos += 1; - - // don't loop beyond the last group - if (i === dataLength) { - break; - } - } - - // break out - if (i === dataLength) { - break; - } - - // for each raw data point, push it to an array that contains all values - // for this specific group - if (pointArrayMap) { - - var index = series.cropStart + i, - point = (data && data[index]) || - series.pointClass.prototype.applyOptions.apply({ - series: series - }, [dataOptions[index]]), - val; - - for (j = 0; j < pointArrayMapLength; j++) { - val = point[pointArrayMap[j]]; - if (isNumber(val)) { - values[j].push(val); - } else if (val === null) { - values[j].hasNulls = true; - } - } - - } else { - pointY = handleYData ? yData[i] : null; - - if (isNumber(pointY)) { - values[0].push(pointY); - } else if (pointY === null) { - values[0].hasNulls = true; - } - } - } - - return [groupedXData, groupedYData, groupMap]; + var series = this, + data = series.data, + dataOptions = series.options.data, + groupedXData = [], + groupedYData = [], + groupMap = [], + dataLength = xData.length, + pointX, + pointY, + groupedY, + // when grouping the fake extended axis for panning, + // we don't need to consider y + handleYData = !!yData, + values = [], + approximationFn = typeof approximation === 'function' ? + approximation : + approximations[approximation] || + // if the approximation is not found use default series type + // approximation (#2914) + ( + specificOptions[series.type] && + approximations[specificOptions[series.type].approximation] + ) || approximations[commonOptions.approximation], + pointArrayMap = series.pointArrayMap, + pointArrayMapLength = pointArrayMap && pointArrayMap.length, + pos = 0, + start = 0, + valuesLen, + i, j; + + // Calculate values array size from pointArrayMap length + if (pointArrayMapLength) { + each(pointArrayMap, function () { + values.push([]); + }); + } else { + values.push([]); + } + valuesLen = pointArrayMapLength || 1; + + // Start with the first point within the X axis range (#2696) + for (i = 0; i <= dataLength; i++) { + if (xData[i] >= groupPositions[0]) { + break; + } + } + + for (i; i <= dataLength; i++) { + + // when a new group is entered, summarize and initiate + // the previous group + while (( + groupPositions[pos + 1] !== undefined && + xData[i] >= groupPositions[pos + 1] + ) || i === dataLength) { // get the last group + + // get group x and y + pointX = groupPositions[pos]; + series.dataGroupInfo = { start: start, length: values[0].length }; + groupedY = approximationFn.apply(series, values); + + // push the grouped data + if (groupedY !== undefined) { + groupedXData.push(pointX); + groupedYData.push(groupedY); + groupMap.push(series.dataGroupInfo); + } + + // reset the aggregate arrays + start = i; + for (j = 0; j < valuesLen; j++) { + values[j].length = 0; // faster than values[j] = [] + values[j].hasNulls = false; + } + + // Advance on the group positions + pos += 1; + + // don't loop beyond the last group + if (i === dataLength) { + break; + } + } + + // break out + if (i === dataLength) { + break; + } + + // for each raw data point, push it to an array that contains all values + // for this specific group + if (pointArrayMap) { + + var index = series.cropStart + i, + point = (data && data[index]) || + series.pointClass.prototype.applyOptions.apply({ + series: series + }, [dataOptions[index]]), + val; + + for (j = 0; j < pointArrayMapLength; j++) { + val = point[pointArrayMap[j]]; + if (isNumber(val)) { + values[j].push(val); + } else if (val === null) { + values[j].hasNulls = true; + } + } + + } else { + pointY = handleYData ? yData[i] : null; + + if (isNumber(pointY)) { + values[0].push(pointY); + } else if (pointY === null) { + values[0].hasNulls = true; + } + } + } + + return [groupedXData, groupedYData, groupMap]; }; /** @@ -590,141 +590,141 @@ seriesProto.groupData = function (xData, yData, groupPositions, approximation) { * range, with data grouping logic. */ seriesProto.processData = function () { - var series = this, - chart = series.chart, - options = series.options, - dataGroupingOptions = options.dataGrouping, - groupingEnabled = series.allowDG !== false && dataGroupingOptions && - pick(dataGroupingOptions.enabled, chart.options.isStock), - visible = series.visible || !chart.options.chart.ignoreHiddenSeries, - hasGroupedData, - skip, - lastDataGrouping = this.currentDataGrouping, - currentDataGrouping, - croppedData; - - // Run base method - series.forceCrop = groupingEnabled; // #334 - series.groupPixelWidth = null; // #2110 - series.hasProcessed = true; // #2692 - - // Skip if processData returns false or if grouping is disabled (in that - // order) - skip = ( - baseProcessData.apply(series, arguments) === false || - !groupingEnabled - ); - if (!skip) { - series.destroyGroupedData(); - - var i, - processedXData = dataGroupingOptions.groupAll ? series.xData : - series.processedXData, - processedYData = dataGroupingOptions.groupAll ? series.yData : - series.processedYData, - plotSizeX = chart.plotSizeX, - xAxis = series.xAxis, - ordinal = xAxis.options.ordinal, - groupPixelWidth = series.groupPixelWidth = - xAxis.getGroupPixelWidth && xAxis.getGroupPixelWidth(); - - // Execute grouping if the amount of points is greater than the limit - // defined in groupPixelWidth - if (groupPixelWidth) { - hasGroupedData = true; - - // Force recreation of point instances in series.translate, #5699 - series.isDirty = true; - series.points = null; // #6709 - - var extremes = xAxis.getExtremes(), - xMin = extremes.min, - xMax = extremes.max, - groupIntervalFactor = ( - ordinal && - xAxis.getGroupIntervalFactor(xMin, xMax, series) - ) || 1, - interval = - (groupPixelWidth * (xMax - xMin) / plotSizeX) * - groupIntervalFactor, - groupPositions = xAxis.getTimeTicks( - xAxis.normalizeTimeTickInterval( - interval, - dataGroupingOptions.units || defaultDataGroupingUnits - ), - // Processed data may extend beyond axis (#4907) - Math.min(xMin, processedXData[0]), - Math.max(xMax, processedXData[processedXData.length - 1]), - xAxis.options.startOfWeek, - processedXData, - series.closestPointRange - ), - groupedData = seriesProto.groupData.apply( - series, - [ - processedXData, - processedYData, - groupPositions, - dataGroupingOptions.approximation - ]), - 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 && groupedXData.length) { - i = groupedXData.length - 1; - groupedXData[i] = Math.min(groupedXData[i], xMax); - while (i-- && i > 0) { - groupedXData[i] += interval / 2; - } - groupedXData[0] = Math.max(groupedXData[0], xMin); - } - - // Record what data grouping values were used - currentDataGrouping = groupPositions.info; - series.closestPointRange = groupPositions.info.totalRange; - series.groupMap = groupedData[2]; - - // Make sure the X axis extends to show the first group (#2533) - // But only for visible series (#5493, #6393) - if ( - defined(groupedXData[0]) && - groupedXData[0] < xAxis.dataMin && - visible - ) { - if (xAxis.min <= xAxis.dataMin) { - xAxis.min = groupedXData[0]; - } - xAxis.dataMin = groupedXData[0]; - } - - // We calculated all group positions but we should render - // only the ones within the visible range - if (dataGroupingOptions.groupAll) { - croppedData = series.cropData( - groupedXData, - groupedYData, - xAxis.min, - xAxis.max, - 1 // Ordinal xAxis will remove left-most points otherwise - ); - groupedXData = croppedData.xData; - groupedYData = croppedData.yData; - } - // Set series props - series.processedXData = groupedXData; - series.processedYData = groupedYData; - } else { - series.groupMap = null; - } - series.hasGroupedData = hasGroupedData; - series.currentDataGrouping = currentDataGrouping; - - series.preventGraphAnimation = - (lastDataGrouping && lastDataGrouping.totalRange) !== - (currentDataGrouping && currentDataGrouping.totalRange); - } + var series = this, + chart = series.chart, + options = series.options, + dataGroupingOptions = options.dataGrouping, + groupingEnabled = series.allowDG !== false && dataGroupingOptions && + pick(dataGroupingOptions.enabled, chart.options.isStock), + visible = series.visible || !chart.options.chart.ignoreHiddenSeries, + hasGroupedData, + skip, + lastDataGrouping = this.currentDataGrouping, + currentDataGrouping, + croppedData; + + // Run base method + series.forceCrop = groupingEnabled; // #334 + series.groupPixelWidth = null; // #2110 + series.hasProcessed = true; // #2692 + + // Skip if processData returns false or if grouping is disabled (in that + // order) + skip = ( + baseProcessData.apply(series, arguments) === false || + !groupingEnabled + ); + if (!skip) { + series.destroyGroupedData(); + + var i, + processedXData = dataGroupingOptions.groupAll ? series.xData : + series.processedXData, + processedYData = dataGroupingOptions.groupAll ? series.yData : + series.processedYData, + plotSizeX = chart.plotSizeX, + xAxis = series.xAxis, + ordinal = xAxis.options.ordinal, + groupPixelWidth = series.groupPixelWidth = + xAxis.getGroupPixelWidth && xAxis.getGroupPixelWidth(); + + // Execute grouping if the amount of points is greater than the limit + // defined in groupPixelWidth + if (groupPixelWidth) { + hasGroupedData = true; + + // Force recreation of point instances in series.translate, #5699 + series.isDirty = true; + series.points = null; // #6709 + + var extremes = xAxis.getExtremes(), + xMin = extremes.min, + xMax = extremes.max, + groupIntervalFactor = ( + ordinal && + xAxis.getGroupIntervalFactor(xMin, xMax, series) + ) || 1, + interval = + (groupPixelWidth * (xMax - xMin) / plotSizeX) * + groupIntervalFactor, + groupPositions = xAxis.getTimeTicks( + xAxis.normalizeTimeTickInterval( + interval, + dataGroupingOptions.units || defaultDataGroupingUnits + ), + // Processed data may extend beyond axis (#4907) + Math.min(xMin, processedXData[0]), + Math.max(xMax, processedXData[processedXData.length - 1]), + xAxis.options.startOfWeek, + processedXData, + series.closestPointRange + ), + groupedData = seriesProto.groupData.apply( + series, + [ + processedXData, + processedYData, + groupPositions, + dataGroupingOptions.approximation + ]), + 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 && groupedXData.length) { + i = groupedXData.length - 1; + groupedXData[i] = Math.min(groupedXData[i], xMax); + while (i-- && i > 0) { + groupedXData[i] += interval / 2; + } + groupedXData[0] = Math.max(groupedXData[0], xMin); + } + + // Record what data grouping values were used + currentDataGrouping = groupPositions.info; + series.closestPointRange = groupPositions.info.totalRange; + series.groupMap = groupedData[2]; + + // Make sure the X axis extends to show the first group (#2533) + // But only for visible series (#5493, #6393) + if ( + defined(groupedXData[0]) && + groupedXData[0] < xAxis.dataMin && + visible + ) { + if (xAxis.min <= xAxis.dataMin) { + xAxis.min = groupedXData[0]; + } + xAxis.dataMin = groupedXData[0]; + } + + // We calculated all group positions but we should render + // only the ones within the visible range + if (dataGroupingOptions.groupAll) { + croppedData = series.cropData( + groupedXData, + groupedYData, + xAxis.min, + xAxis.max, + 1 // Ordinal xAxis will remove left-most points otherwise + ); + groupedXData = croppedData.xData; + groupedYData = croppedData.yData; + } + // Set series props + series.processedXData = groupedXData; + series.processedYData = groupedYData; + } else { + series.groupMap = null; + } + series.hasGroupedData = hasGroupedData; + series.currentDataGrouping = currentDataGrouping; + + series.preventGraphAnimation = + (lastDataGrouping && lastDataGrouping.totalRange) !== + (currentDataGrouping && currentDataGrouping.totalRange); + } }; /** @@ -732,15 +732,15 @@ seriesProto.processData = function () { */ seriesProto.destroyGroupedData = function () { - var groupedData = this.groupedData; + var groupedData = this.groupedData; - // clear previous groups - each(groupedData || [], function (point, i) { - if (point) { - groupedData[i] = point.destroy ? point.destroy() : null; - } - }); - this.groupedData = null; + // clear previous groups + each(groupedData || [], function (point, i) { + if (point) { + groupedData[i] = point.destroy ? point.destroy() : null; + } + }); + this.groupedData = null; }; /** @@ -748,12 +748,12 @@ seriesProto.destroyGroupedData = function () { */ seriesProto.generatePoints = function () { - baseGeneratePoints.apply(this); + baseGeneratePoints.apply(this); - // Record grouped data in order to let it be destroyed the next time - // processData runs - this.destroyGroupedData(); // #622 - this.groupedData = this.hasGroupedData ? this.points : null; + // Record grouped data in order to let it be destroyed the next time + // processData runs + this.destroyGroupedData(); // #622 + this.groupedData = this.hasGroupedData ? this.points : null; }; /** @@ -761,10 +761,10 @@ seriesProto.generatePoints = function () { * points */ addEvent(Point, 'update', function () { - if (this.dataGroup) { - H.error(24); - return false; - } + if (this.dataGroup) { + H.error(24); + return false; + } }); /** @@ -772,79 +772,79 @@ addEvent(Point, 'update', function () { * range */ wrap(Tooltip.prototype, 'tooltipFooterHeaderFormatter', function ( - proceed, - labelConfig, - isFooter + proceed, + labelConfig, + isFooter ) { - var tooltip = this, - time = this.chart.time, - series = labelConfig.series, - options = series.options, - tooltipOptions = series.tooltipOptions, - dataGroupingOptions = options.dataGrouping, - xDateFormat = tooltipOptions.xDateFormat, - xDateFormatEnd, - xAxis = series.xAxis, - currentDataGrouping, - dateTimeLabelFormats, - labelFormats, - formattedKey; - - // apply only to grouped series - if ( - xAxis && - xAxis.options.type === 'datetime' && - dataGroupingOptions && - isNumber(labelConfig.key) - ) { - - // set variables - currentDataGrouping = series.currentDataGrouping; - dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats; - - // if we have grouped data, use the grouping information to get the - // right format - if (currentDataGrouping) { - labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName]; - if (currentDataGrouping.count === 1) { - xDateFormat = labelFormats[0]; - } else { - xDateFormat = labelFormats[1]; - xDateFormatEnd = labelFormats[2]; - } - // if not grouped, and we don't have set the xDateFormat option, get the - // best fit, so if the least distance between points is one minute, show - // it, but if the least distance is one day, skip hours and minutes etc. - } else if (!xDateFormat && dateTimeLabelFormats) { - xDateFormat = tooltip.getXDateFormat( - labelConfig, - tooltipOptions, - xAxis - ); - } - - // now format the key - formattedKey = time.dateFormat(xDateFormat, labelConfig.key); - if (xDateFormatEnd) { - formattedKey += time.dateFormat( - xDateFormatEnd, - labelConfig.key + currentDataGrouping.totalRange - 1 - ); - } - - // return the replaced format - return format( - tooltipOptions[(isFooter ? 'footer' : 'header') + 'Format'], { - point: extend(labelConfig.point, { key: formattedKey }), - series: series - }, - time - ); - - } - - // else, fall back to the regular formatter - return proceed.call(tooltip, labelConfig, isFooter); + var tooltip = this, + time = this.chart.time, + series = labelConfig.series, + options = series.options, + tooltipOptions = series.tooltipOptions, + dataGroupingOptions = options.dataGrouping, + xDateFormat = tooltipOptions.xDateFormat, + xDateFormatEnd, + xAxis = series.xAxis, + currentDataGrouping, + dateTimeLabelFormats, + labelFormats, + formattedKey; + + // apply only to grouped series + if ( + xAxis && + xAxis.options.type === 'datetime' && + dataGroupingOptions && + isNumber(labelConfig.key) + ) { + + // set variables + currentDataGrouping = series.currentDataGrouping; + dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats; + + // if we have grouped data, use the grouping information to get the + // right format + if (currentDataGrouping) { + labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName]; + if (currentDataGrouping.count === 1) { + xDateFormat = labelFormats[0]; + } else { + xDateFormat = labelFormats[1]; + xDateFormatEnd = labelFormats[2]; + } + // if not grouped, and we don't have set the xDateFormat option, get the + // best fit, so if the least distance between points is one minute, show + // it, but if the least distance is one day, skip hours and minutes etc. + } else if (!xDateFormat && dateTimeLabelFormats) { + xDateFormat = tooltip.getXDateFormat( + labelConfig, + tooltipOptions, + xAxis + ); + } + + // now format the key + formattedKey = time.dateFormat(xDateFormat, labelConfig.key); + if (xDateFormatEnd) { + formattedKey += time.dateFormat( + xDateFormatEnd, + labelConfig.key + currentDataGrouping.totalRange - 1 + ); + } + + // return the replaced format + return format( + tooltipOptions[(isFooter ? 'footer' : 'header') + 'Format'], { + point: extend(labelConfig.point, { key: formattedKey }), + series: series + }, + time + ); + + } + + // else, fall back to the regular formatter + return proceed.call(tooltip, labelConfig, isFooter); }); /** @@ -857,31 +857,31 @@ addEvent(Series, 'destroy', seriesProto.destroyGroupedData); // some series types are defined after this. addEvent(Series, 'afterSetOptions', function (e) { - var options = e.options, - type = this.type, - plotOptions = this.chart.options.plotOptions, - defaultOptions = defaultPlotOptions[type].dataGrouping, - // External series, for example technical indicators should also - // inherit commonOptions which are not available outside this module - baseOptions = this.useCommonDataGrouping && commonOptions; - - if (specificOptions[type] || baseOptions) { // #1284 - if (!defaultOptions) { - defaultOptions = merge(commonOptions, specificOptions[type]); - } - - options.dataGrouping = merge( - baseOptions, - defaultOptions, - plotOptions.series && plotOptions.series.dataGrouping, // #1228 - plotOptions[type].dataGrouping, // Set by the StockChart constructor - this.userOptions.dataGrouping - ); - } - - if (this.chart.options.isStock) { - this.requireSorting = true; - } + var options = e.options, + type = this.type, + plotOptions = this.chart.options.plotOptions, + defaultOptions = defaultPlotOptions[type].dataGrouping, + // External series, for example technical indicators should also + // inherit commonOptions which are not available outside this module + baseOptions = this.useCommonDataGrouping && commonOptions; + + if (specificOptions[type] || baseOptions) { // #1284 + if (!defaultOptions) { + defaultOptions = merge(commonOptions, specificOptions[type]); + } + + options.dataGrouping = merge( + baseOptions, + defaultOptions, + plotOptions.series && plotOptions.series.dataGrouping, // #1228 + plotOptions[type].dataGrouping, // Set by the StockChart constructor + this.userOptions.dataGrouping + ); + } + + if (this.chart.options.isStock) { + this.requireSorting = true; + } }); @@ -891,9 +891,9 @@ addEvent(Series, 'afterSetOptions', function (e) { * group pixel width (#2692). */ addEvent(Axis, 'afterSetScale', function () { - each(this.series, function (series) { - series.hasProcessed = false; - }); + each(this.series, function (series) { + series.hasProcessed = false; + }); }); /** @@ -903,50 +903,50 @@ addEvent(Axis, 'afterSetScale', function () { */ Axis.prototype.getGroupPixelWidth = function () { - var series = this.series, - len = series.length, - i, - groupPixelWidth = 0, - doGrouping = false, - dataLength, - dgOptions; - - // If multiple series are compared on the same x axis, give them the same - // group pixel width (#334) - i = len; - while (i--) { - dgOptions = series[i].options.dataGrouping; - if (dgOptions) { - groupPixelWidth = Math.max( - groupPixelWidth, - dgOptions.groupPixelWidth - ); - - } - } - - // If one of the series needs grouping, apply it to all (#1634) - i = len; - while (i--) { - dgOptions = series[i].options.dataGrouping; - - if (dgOptions && series[i].hasProcessed) { // #2692 - - dataLength = (series[i].processedXData || series[i].data).length; - - // Execute grouping if the amount of points is greater than the - // limit defined in groupPixelWidth - if ( - series[i].groupPixelWidth || - dataLength > (this.chart.plotSizeX / groupPixelWidth) || - (dataLength && dgOptions.forced) - ) { - doGrouping = true; - } - } - } - - return doGrouping ? groupPixelWidth : 0; + var series = this.series, + len = series.length, + i, + groupPixelWidth = 0, + doGrouping = false, + dataLength, + dgOptions; + + // If multiple series are compared on the same x axis, give them the same + // group pixel width (#334) + i = len; + while (i--) { + dgOptions = series[i].options.dataGrouping; + if (dgOptions) { + groupPixelWidth = Math.max( + groupPixelWidth, + dgOptions.groupPixelWidth + ); + + } + } + + // If one of the series needs grouping, apply it to all (#1634) + i = len; + while (i--) { + dgOptions = series[i].options.dataGrouping; + + if (dgOptions && series[i].hasProcessed) { // #2692 + + dataLength = (series[i].processedXData || series[i].data).length; + + // Execute grouping if the amount of points is greater than the + // limit defined in groupPixelWidth + if ( + series[i].groupPixelWidth || + dataLength > (this.chart.plotSizeX / groupPixelWidth) || + (dataLength && dgOptions.forced) + ) { + doGrouping = true; + } + } + } + + return doGrouping ? groupPixelWidth : 0; }; /** @@ -963,43 +963,43 @@ Axis.prototype.getGroupPixelWidth = function () { * @memberOf Axis.prototype */ Axis.prototype.setDataGrouping = function (dataGrouping, redraw) { - var i; - - redraw = pick(redraw, true); - - if (!dataGrouping) { - dataGrouping = { - forced: false, - units: null - }; - } - - // Axis is instantiated, update all series - if (this instanceof Axis) { - i = this.series.length; - while (i--) { - this.series[i].update({ - dataGrouping: dataGrouping - }, false); - } - - // Axis not yet instanciated, alter series options - } else { - each(this.chart.options.series, function (seriesOptions) { - seriesOptions.dataGrouping = dataGrouping; - }, false); - } - - // Clear ordinal slope, so we won't accidentaly use the old one (#7827) - this.ordinalSlope = null; - - if (redraw) { - this.chart.redraw(); - } + var i; + + redraw = pick(redraw, true); + + if (!dataGrouping) { + dataGrouping = { + forced: false, + units: null + }; + } + + // Axis is instantiated, update all series + if (this instanceof Axis) { + i = this.series.length; + while (i--) { + this.series[i].update({ + dataGrouping: dataGrouping + }, false); + } + + // Axis not yet instanciated, alter series options + } else { + each(this.chart.options.series, function (seriesOptions) { + seriesOptions.dataGrouping = dataGrouping; + }, false); + } + + // Clear ordinal slope, so we won't accidentaly use the old one (#7827) + this.ordinalSlope = null; + + if (redraw) { + this.chart.redraw(); + } }; /* **************************************************************************** - * End data grouping module * + * End data grouping module * ******************************************************************************/ diff --git a/js/parts/DataLabels.js b/js/parts/DataLabels.js index bdc18c5b81e..fbf0df50bca 100644 --- a/js/parts/DataLabels.js +++ b/js/parts/DataLabels.js @@ -8,22 +8,22 @@ import H from './Globals.js'; import './Utilities.js'; import './Series.js'; var addEvent = H.addEvent, - arrayMax = H.arrayMax, - defined = H.defined, - each = H.each, - extend = H.extend, - format = H.format, - map = H.map, - merge = H.merge, - noop = H.noop, - pick = H.pick, - relativeLength = H.relativeLength, - Series = H.Series, - seriesTypes = H.seriesTypes, - some = H.some, - stableSort = H.stableSort; - - + arrayMax = H.arrayMax, + defined = H.defined, + each = H.each, + extend = H.extend, + format = H.format, + map = H.map, + merge = H.merge, + noop = H.noop, + pick = H.pick, + relativeLength = H.relativeLength, + Series = H.Series, + seriesTypes = H.seriesTypes, + some = H.some, + stableSort = H.stableSort; + + /** * General distribution algorithm for distributing labels of differing size * along a confined length in two dimensions. The algorithm takes an array of @@ -33,142 +33,142 @@ var addEvent = H.addEvent, */ H.distribute = function (boxes, len, maxDistance) { - var i, - overlapping = true, - origBoxes = boxes, // Original array will be altered with added .pos - restBoxes = [], // The outranked overshoot - box, - target, - total = 0, - reducedLen = origBoxes.reducedLen || len; - - function sortByTarget(a, b) { - return a.target - b.target; - } - - // If the total size exceeds the len, remove those boxes with the lowest - // rank - i = boxes.length; - while (i--) { - total += boxes[i].size; - } - - // Sort by rank, then slice away overshoot - if (total > reducedLen) { - stableSort(boxes, function (a, b) { - return (b.rank || 0) - (a.rank || 0); - }); - i = 0; - total = 0; - while (total <= reducedLen) { - total += boxes[i].size; - i++; - } - restBoxes = boxes.splice(i - 1, boxes.length); - } - - // Order by target - stableSort(boxes, sortByTarget); - - - // So far we have been mutating the original array. Now - // create a copy with target arrays - boxes = map(boxes, function (box) { - return { - size: box.size, - targets: [box.target], - align: pick(box.align, 0.5) - }; - }); - - while (overlapping) { - // Initial positions: target centered in box - i = boxes.length; - while (i--) { - box = boxes[i]; - // Composite box, average of targets - target = ( - Math.min.apply(0, box.targets) + - Math.max.apply(0, box.targets) - ) / 2; - box.pos = Math.min( - Math.max(0, target - box.size * box.align), - len - box.size - ); - } - - // Detect overlap and join boxes - i = boxes.length; - overlapping = false; - while (i--) { - // Overlap - if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { - // Add this size to the previous box - boxes[i - 1].size += boxes[i].size; - boxes[i - 1].targets = boxes[i - 1] - .targets - .concat(boxes[i].targets); - boxes[i - 1].align = 0.5; - - // Overlapping right, push left - if (boxes[i - 1].pos + boxes[i - 1].size > len) { - boxes[i - 1].pos = len - boxes[i - 1].size; - } - boxes.splice(i, 1); // Remove this item - overlapping = true; - } - } - } - - // Add the rest (hidden boxes) - origBoxes.push.apply(origBoxes, restBoxes); - - - // Now the composite boxes are placed, we need to put the original boxes - // within them - i = 0; - some(boxes, function (box) { - var posInCompositeBox = 0; - if (some(box.targets, function () { - origBoxes[i].pos = box.pos + posInCompositeBox; - - // If the distance between the position and the target exceeds - // maxDistance, abort the loop and decrease the length in increments - // of 10% to recursively reduce the number of visible boxes by - // rank. Once all boxes are within the maxDistance, we're good. - if ( - Math.abs(origBoxes[i].pos - origBoxes[i].target) > - maxDistance - ) { - // Reset the positions that are already set - each(origBoxes.slice(0, i + 1), function (box) { - delete box.pos; - }); - - // Try with a smaller length - origBoxes.reducedLen = - (origBoxes.reducedLen || len) - (len * 0.1); - - // Recurse - if (origBoxes.reducedLen > len * 0.1) { - H.distribute(origBoxes, len, maxDistance); - } - - // Exceeded maxDistance => abort - return true; - } - - posInCompositeBox += origBoxes[i].size; - i++; - - })) { - // Exceeded maxDistance => abort - return true; - } - }); - - // Add the rest (hidden) boxes and sort by target - stableSort(origBoxes, sortByTarget); + var i, + overlapping = true, + origBoxes = boxes, // Original array will be altered with added .pos + restBoxes = [], // The outranked overshoot + box, + target, + total = 0, + reducedLen = origBoxes.reducedLen || len; + + function sortByTarget(a, b) { + return a.target - b.target; + } + + // If the total size exceeds the len, remove those boxes with the lowest + // rank + i = boxes.length; + while (i--) { + total += boxes[i].size; + } + + // Sort by rank, then slice away overshoot + if (total > reducedLen) { + stableSort(boxes, function (a, b) { + return (b.rank || 0) - (a.rank || 0); + }); + i = 0; + total = 0; + while (total <= reducedLen) { + total += boxes[i].size; + i++; + } + restBoxes = boxes.splice(i - 1, boxes.length); + } + + // Order by target + stableSort(boxes, sortByTarget); + + + // So far we have been mutating the original array. Now + // create a copy with target arrays + boxes = map(boxes, function (box) { + return { + size: box.size, + targets: [box.target], + align: pick(box.align, 0.5) + }; + }); + + while (overlapping) { + // Initial positions: target centered in box + i = boxes.length; + while (i--) { + box = boxes[i]; + // Composite box, average of targets + target = ( + Math.min.apply(0, box.targets) + + Math.max.apply(0, box.targets) + ) / 2; + box.pos = Math.min( + Math.max(0, target - box.size * box.align), + len - box.size + ); + } + + // Detect overlap and join boxes + i = boxes.length; + overlapping = false; + while (i--) { + // Overlap + if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { + // Add this size to the previous box + boxes[i - 1].size += boxes[i].size; + boxes[i - 1].targets = boxes[i - 1] + .targets + .concat(boxes[i].targets); + boxes[i - 1].align = 0.5; + + // Overlapping right, push left + if (boxes[i - 1].pos + boxes[i - 1].size > len) { + boxes[i - 1].pos = len - boxes[i - 1].size; + } + boxes.splice(i, 1); // Remove this item + overlapping = true; + } + } + } + + // Add the rest (hidden boxes) + origBoxes.push.apply(origBoxes, restBoxes); + + + // Now the composite boxes are placed, we need to put the original boxes + // within them + i = 0; + some(boxes, function (box) { + var posInCompositeBox = 0; + if (some(box.targets, function () { + origBoxes[i].pos = box.pos + posInCompositeBox; + + // If the distance between the position and the target exceeds + // maxDistance, abort the loop and decrease the length in increments + // of 10% to recursively reduce the number of visible boxes by + // rank. Once all boxes are within the maxDistance, we're good. + if ( + Math.abs(origBoxes[i].pos - origBoxes[i].target) > + maxDistance + ) { + // Reset the positions that are already set + each(origBoxes.slice(0, i + 1), function (box) { + delete box.pos; + }); + + // Try with a smaller length + origBoxes.reducedLen = + (origBoxes.reducedLen || len) - (len * 0.1); + + // Recurse + if (origBoxes.reducedLen > len * 0.1) { + H.distribute(origBoxes, len, maxDistance); + } + + // Exceeded maxDistance => abort + return true; + } + + posInCompositeBox += origBoxes[i].size; + i++; + + })) { + // Exceeded maxDistance => abort + return true; + } + }); + + // Add the rest (hidden) boxes and sort by target + stableSort(origBoxes, sortByTarget); }; @@ -176,362 +176,362 @@ H.distribute = function (boxes, len, maxDistance) { * Draw the data labels */ Series.prototype.drawDataLabels = function () { - var series = this, - chart = series.chart, - seriesOptions = series.options, - options = seriesOptions.dataLabels, - points = series.points, - pointOptions, - generalOptions, - hasRendered = series.hasRendered || 0, - str, - dataLabelsGroup, - defer = pick(options.defer, !!seriesOptions.animation), - renderer = chart.renderer; - - /* - * Handle the dataLabels.filter option. - */ - function applyFilter(point, options) { - var filter = options.filter, - op, - prop, - val; - if (filter) { - op = filter.operator; - prop = point[filter.property]; - val = filter.value; - if ( - (op === '>' && prop > val) || - (op === '<' && prop < val) || - (op === '>=' && prop >= val) || - (op === '<=' && prop <= val) || - (op === '==' && prop == val) || // eslint-disable-line eqeqeq - (op === '===' && prop === val) - ) { - return true; - } - return false; - } - return true; - } - - if (options.enabled || series._hasPointLabels) { - - // Process default alignment of data labels for columns - if (series.dlProcessOptions) { - series.dlProcessOptions(options); - } - - // Create a separate group for the data labels to avoid rotation - dataLabelsGroup = series.plotGroup( - 'dataLabelsGroup', - 'data-labels', - defer && !hasRendered ? 'hidden' : 'visible', // #5133 - options.zIndex || 6 - ); - - if (defer) { - dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300 - if (!hasRendered) { - addEvent(series, 'afterAnimate', function () { - if (series.visible) { // #2597, #3023, #3024 - dataLabelsGroup.show(true); - } - dataLabelsGroup[ - seriesOptions.animation ? 'animate' : 'attr' - ]({ opacity: 1 }, { duration: 200 }); - }); - } - } - - // Make the labels for each point - generalOptions = options; - each(points, function (point) { - var enabled, - dataLabel = point.dataLabel, - labelConfig, - attr, - rotation, - connector = point.connector, - isNew = !dataLabel, - style, - formatString; - - // Determine if each data label is enabled - // @note dataLabelAttribs (like pointAttribs) would eradicate - // the need for dlOptions, and simplify the section below. - pointOptions = point.dlOptions || // dlOptions is used in treemaps - (point.options && point.options.dataLabels); - enabled = pick( - pointOptions && pointOptions.enabled, - generalOptions.enabled - ) && !point.isNull; // #2282, #4641, #7112 - - if (enabled) { - enabled = applyFilter(point, pointOptions || options) === true; - } - - if (enabled) { - // Create individual options structure that can be extended - // without affecting others - options = merge(generalOptions, pointOptions); - labelConfig = point.getLabelConfig(); - formatString = ( - options[point.formatPrefix + 'Format'] || - options.format - ); - - str = defined(formatString) ? - format(formatString, labelConfig, chart.time) : - ( - options[point.formatPrefix + 'Formatter'] || - options.formatter - ).call(labelConfig, options); - - style = options.style; - rotation = options.rotation; - /*= if (build.classic) { =*/ - // Determine the color - style.color = pick( - options.color, - style.color, - series.color, - '${palette.neutralColor100}' - ); - // Get automated contrast color - if (style.color === 'contrast') { - point.contrastColor = - renderer.getContrast(point.color || series.color); - style.color = options.inside || - pick(point.labelDistance, options.distance) < 0 || - !!seriesOptions.stacking ? - point.contrastColor : - '${palette.neutralColor100}'; - } - if (seriesOptions.cursor) { - style.cursor = seriesOptions.cursor; - } - /*= } =*/ - - attr = { - /*= if (build.classic) { =*/ - fill: options.backgroundColor, - stroke: options.borderColor, - 'stroke-width': options.borderWidth, - /*= } =*/ - r: options.borderRadius || 0, - rotation: rotation, - padding: options.padding, - zIndex: 1 - }; - - // Remove unused attributes (#947) - H.objectEach(attr, function (val, name) { - if (val === undefined) { - delete attr[name]; - } - }); - } - // If the point is outside the plot area, destroy it. #678, #820 - if (dataLabel && (!enabled || !defined(str))) { - point.dataLabel = dataLabel = dataLabel.destroy(); - if (connector) { - point.connector = connector.destroy(); - } - // Individual labels are disabled if the are explicitly disabled - // in the point options, or if they fall outside the plot area. - } else if (enabled && defined(str)) { - // create new label - if (!dataLabel) { - dataLabel = point.dataLabel = rotation ? - - renderer.text(str, 0, -9999) // labels don't rotate - .addClass('highcharts-data-label') : - - renderer.label( - str, - 0, - -9999, - options.shape, - null, - null, - options.useHTML, - null, - 'data-label' - ); - - dataLabel.addClass( - ' highcharts-data-label-color-' + point.colorIndex + - ' ' + (options.className || '') + - (options.useHTML ? 'highcharts-tracker' : '') // #3398 - ); - } else { - attr.text = str; - } - dataLabel.attr(attr); - /*= if (build.classic) { =*/ - // Styles must be applied before add in order to read text - // bounding box - dataLabel.css(style).shadow(options.shadow); - /*= } =*/ - - if (!dataLabel.added) { - dataLabel.add(dataLabelsGroup); - } - // Now the data label is created and placed at 0,0, so we need - // to align it - series.alignDataLabel(point, dataLabel, options, null, isNew); - } - }); - } - - H.fireEvent(this, 'afterDrawDataLabels'); + var series = this, + chart = series.chart, + seriesOptions = series.options, + options = seriesOptions.dataLabels, + points = series.points, + pointOptions, + generalOptions, + hasRendered = series.hasRendered || 0, + str, + dataLabelsGroup, + defer = pick(options.defer, !!seriesOptions.animation), + renderer = chart.renderer; + + /* + * Handle the dataLabels.filter option. + */ + function applyFilter(point, options) { + var filter = options.filter, + op, + prop, + val; + if (filter) { + op = filter.operator; + prop = point[filter.property]; + val = filter.value; + if ( + (op === '>' && prop > val) || + (op === '<' && prop < val) || + (op === '>=' && prop >= val) || + (op === '<=' && prop <= val) || + (op === '==' && prop == val) || // eslint-disable-line eqeqeq + (op === '===' && prop === val) + ) { + return true; + } + return false; + } + return true; + } + + if (options.enabled || series._hasPointLabels) { + + // Process default alignment of data labels for columns + if (series.dlProcessOptions) { + series.dlProcessOptions(options); + } + + // Create a separate group for the data labels to avoid rotation + dataLabelsGroup = series.plotGroup( + 'dataLabelsGroup', + 'data-labels', + defer && !hasRendered ? 'hidden' : 'visible', // #5133 + options.zIndex || 6 + ); + + if (defer) { + dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300 + if (!hasRendered) { + addEvent(series, 'afterAnimate', function () { + if (series.visible) { // #2597, #3023, #3024 + dataLabelsGroup.show(true); + } + dataLabelsGroup[ + seriesOptions.animation ? 'animate' : 'attr' + ]({ opacity: 1 }, { duration: 200 }); + }); + } + } + + // Make the labels for each point + generalOptions = options; + each(points, function (point) { + var enabled, + dataLabel = point.dataLabel, + labelConfig, + attr, + rotation, + connector = point.connector, + isNew = !dataLabel, + style, + formatString; + + // Determine if each data label is enabled + // @note dataLabelAttribs (like pointAttribs) would eradicate + // the need for dlOptions, and simplify the section below. + pointOptions = point.dlOptions || // dlOptions is used in treemaps + (point.options && point.options.dataLabels); + enabled = pick( + pointOptions && pointOptions.enabled, + generalOptions.enabled + ) && !point.isNull; // #2282, #4641, #7112 + + if (enabled) { + enabled = applyFilter(point, pointOptions || options) === true; + } + + if (enabled) { + // Create individual options structure that can be extended + // without affecting others + options = merge(generalOptions, pointOptions); + labelConfig = point.getLabelConfig(); + formatString = ( + options[point.formatPrefix + 'Format'] || + options.format + ); + + str = defined(formatString) ? + format(formatString, labelConfig, chart.time) : + ( + options[point.formatPrefix + 'Formatter'] || + options.formatter + ).call(labelConfig, options); + + style = options.style; + rotation = options.rotation; + /*= if (build.classic) { =*/ + // Determine the color + style.color = pick( + options.color, + style.color, + series.color, + '${palette.neutralColor100}' + ); + // Get automated contrast color + if (style.color === 'contrast') { + point.contrastColor = + renderer.getContrast(point.color || series.color); + style.color = options.inside || + pick(point.labelDistance, options.distance) < 0 || + !!seriesOptions.stacking ? + point.contrastColor : + '${palette.neutralColor100}'; + } + if (seriesOptions.cursor) { + style.cursor = seriesOptions.cursor; + } + /*= } =*/ + + attr = { + /*= if (build.classic) { =*/ + fill: options.backgroundColor, + stroke: options.borderColor, + 'stroke-width': options.borderWidth, + /*= } =*/ + r: options.borderRadius || 0, + rotation: rotation, + padding: options.padding, + zIndex: 1 + }; + + // Remove unused attributes (#947) + H.objectEach(attr, function (val, name) { + if (val === undefined) { + delete attr[name]; + } + }); + } + // If the point is outside the plot area, destroy it. #678, #820 + if (dataLabel && (!enabled || !defined(str))) { + point.dataLabel = dataLabel = dataLabel.destroy(); + if (connector) { + point.connector = connector.destroy(); + } + // Individual labels are disabled if the are explicitly disabled + // in the point options, or if they fall outside the plot area. + } else if (enabled && defined(str)) { + // create new label + if (!dataLabel) { + dataLabel = point.dataLabel = rotation ? + + renderer.text(str, 0, -9999) // labels don't rotate + .addClass('highcharts-data-label') : + + renderer.label( + str, + 0, + -9999, + options.shape, + null, + null, + options.useHTML, + null, + 'data-label' + ); + + dataLabel.addClass( + ' highcharts-data-label-color-' + point.colorIndex + + ' ' + (options.className || '') + + (options.useHTML ? 'highcharts-tracker' : '') // #3398 + ); + } else { + attr.text = str; + } + dataLabel.attr(attr); + /*= if (build.classic) { =*/ + // Styles must be applied before add in order to read text + // bounding box + dataLabel.css(style).shadow(options.shadow); + /*= } =*/ + + if (!dataLabel.added) { + dataLabel.add(dataLabelsGroup); + } + // Now the data label is created and placed at 0,0, so we need + // to align it + series.alignDataLabel(point, dataLabel, options, null, isNew); + } + }); + } + + H.fireEvent(this, 'afterDrawDataLabels'); }; /** * Align each individual data label */ Series.prototype.alignDataLabel = function ( - point, - dataLabel, - options, - alignTo, - isNew + point, + dataLabel, + options, + alignTo, + isNew ) { - var chart = this.chart, - inverted = chart.inverted, - plotX = pick(point.dlBox && point.dlBox.centerX, point.plotX, -9999), - plotY = pick(point.plotY, -9999), - bBox = dataLabel.getBBox(), - fontSize, - baseline, - rotation = options.rotation, - normRotation, - negRotation, - align = options.align, - rotCorr, // rotation correction - // Math.round for rounding errors (#2683), alignTo to allow column - // labels (#2700) - visible = - this.visible && - ( - point.series.forceDL || - chart.isInsidePlot(plotX, Math.round(plotY), inverted) || - ( - alignTo && chart.isInsidePlot( - plotX, - inverted ? - alignTo.x + 1 : - alignTo.y + alignTo.height - 1, - inverted - ) - ) - ), - alignAttr, // the final position; - justify = pick(options.overflow, 'justify') === 'justify'; - - if (visible) { - - /*= if (build.classic) { =*/ - fontSize = options.style.fontSize; - /*= } =*/ - - baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b; - - // The alignment box is a singular point - alignTo = extend({ - x: inverted ? this.yAxis.len - plotY : plotX, - y: Math.round(inverted ? this.xAxis.len - plotX : plotY), - width: 0, - height: 0 - }, alignTo); - - // Add the text size for alignment calculation - extend(options, { - width: bBox.width, - height: bBox.height - }); - - // Allow a hook for changing alignment in the last moment, then do the - // alignment - if (rotation) { - justify = false; // Not supported for rotated text - rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723 - alignAttr = { - x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, - y: ( - alignTo.y + - options.y + - { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] * - alignTo.height - ) - }; - dataLabel[isNew ? 'attr' : 'animate'](alignAttr) - .attr({ // #3003 - align: align - }); - - // Compensate for the rotated label sticking out on the sides - normRotation = (rotation + 720) % 360; - negRotation = normRotation > 180 && normRotation < 360; - - if (align === 'left') { - alignAttr.y -= negRotation ? bBox.height : 0; - } else if (align === 'center') { - alignAttr.x -= bBox.width / 2; - alignAttr.y -= bBox.height / 2; - } else if (align === 'right') { - alignAttr.x -= bBox.width; - alignAttr.y -= negRotation ? 0 : bBox.height; - } - dataLabel.placed = true; - dataLabel.alignAttr = alignAttr; - - } else { - dataLabel.align(options, null, alignTo); - alignAttr = dataLabel.alignAttr; - } - - // Handle justify or crop - if (justify) { - point.isLabelJustified = this.justifyDataLabel( - dataLabel, - options, - alignAttr, - bBox, - alignTo, - isNew - ); - - // Now check that the data label is within the plot area - } else if (pick(options.crop, true)) { - visible = - chart.isInsidePlot( - alignAttr.x, - alignAttr.y - ) && - chart.isInsidePlot( - alignAttr.x + bBox.width, - alignAttr.y + bBox.height - ); - } - - // When we're using a shape, make it possible with a connector or an - // arrow pointing to thie point - if (options.shape && !rotation) { - dataLabel[isNew ? 'attr' : 'animate']({ - anchorX: inverted ? chart.plotWidth - point.plotY : point.plotX, - anchorY: inverted ? chart.plotHeight - point.plotX : point.plotY - }); - } - } - - // Show or hide based on the final aligned position - if (!visible) { - dataLabel.attr({ y: -9999 }); - dataLabel.placed = false; // don't animate back in - } + var chart = this.chart, + inverted = chart.inverted, + plotX = pick(point.dlBox && point.dlBox.centerX, point.plotX, -9999), + plotY = pick(point.plotY, -9999), + bBox = dataLabel.getBBox(), + fontSize, + baseline, + rotation = options.rotation, + normRotation, + negRotation, + align = options.align, + rotCorr, // rotation correction + // Math.round for rounding errors (#2683), alignTo to allow column + // labels (#2700) + visible = + this.visible && + ( + point.series.forceDL || + chart.isInsidePlot(plotX, Math.round(plotY), inverted) || + ( + alignTo && chart.isInsidePlot( + plotX, + inverted ? + alignTo.x + 1 : + alignTo.y + alignTo.height - 1, + inverted + ) + ) + ), + alignAttr, // the final position; + justify = pick(options.overflow, 'justify') === 'justify'; + + if (visible) { + + /*= if (build.classic) { =*/ + fontSize = options.style.fontSize; + /*= } =*/ + + baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b; + + // The alignment box is a singular point + alignTo = extend({ + x: inverted ? this.yAxis.len - plotY : plotX, + y: Math.round(inverted ? this.xAxis.len - plotX : plotY), + width: 0, + height: 0 + }, alignTo); + + // Add the text size for alignment calculation + extend(options, { + width: bBox.width, + height: bBox.height + }); + + // Allow a hook for changing alignment in the last moment, then do the + // alignment + if (rotation) { + justify = false; // Not supported for rotated text + rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723 + alignAttr = { + x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, + y: ( + alignTo.y + + options.y + + { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] * + alignTo.height + ) + }; + dataLabel[isNew ? 'attr' : 'animate'](alignAttr) + .attr({ // #3003 + align: align + }); + + // Compensate for the rotated label sticking out on the sides + normRotation = (rotation + 720) % 360; + negRotation = normRotation > 180 && normRotation < 360; + + if (align === 'left') { + alignAttr.y -= negRotation ? bBox.height : 0; + } else if (align === 'center') { + alignAttr.x -= bBox.width / 2; + alignAttr.y -= bBox.height / 2; + } else if (align === 'right') { + alignAttr.x -= bBox.width; + alignAttr.y -= negRotation ? 0 : bBox.height; + } + dataLabel.placed = true; + dataLabel.alignAttr = alignAttr; + + } else { + dataLabel.align(options, null, alignTo); + alignAttr = dataLabel.alignAttr; + } + + // Handle justify or crop + if (justify) { + point.isLabelJustified = this.justifyDataLabel( + dataLabel, + options, + alignAttr, + bBox, + alignTo, + isNew + ); + + // Now check that the data label is within the plot area + } else if (pick(options.crop, true)) { + visible = + chart.isInsidePlot( + alignAttr.x, + alignAttr.y + ) && + chart.isInsidePlot( + alignAttr.x + bBox.width, + alignAttr.y + bBox.height + ); + } + + // When we're using a shape, make it possible with a connector or an + // arrow pointing to thie point + if (options.shape && !rotation) { + dataLabel[isNew ? 'attr' : 'animate']({ + anchorX: inverted ? chart.plotWidth - point.plotY : point.plotX, + anchorY: inverted ? chart.plotHeight - point.plotX : point.plotY + }); + } + } + + // Show or hide based on the final aligned position + if (!visible) { + dataLabel.attr({ y: -9999 }); + dataLabel.placed = false; // don't animate back in + } }; @@ -540,622 +540,622 @@ Series.prototype.alignDataLabel = function ( * way that doesn't hide the point. */ Series.prototype.justifyDataLabel = function ( - dataLabel, - options, - alignAttr, - bBox, - alignTo, - isNew + dataLabel, + options, + alignAttr, + bBox, + alignTo, + isNew ) { - var chart = this.chart, - align = options.align, - verticalAlign = options.verticalAlign, - off, - justified, - padding = dataLabel.box ? 0 : (dataLabel.padding || 0); - - // Off left - off = alignAttr.x + padding; - if (off < 0) { - if (align === 'right') { - options.align = 'left'; - } else { - options.x = -off; - } - justified = true; - } - - // Off right - off = alignAttr.x + bBox.width - padding; - if (off > chart.plotWidth) { - if (align === 'left') { - options.align = 'right'; - } else { - options.x = chart.plotWidth - off; - } - justified = true; - } - - // Off top - off = alignAttr.y + padding; - if (off < 0) { - if (verticalAlign === 'bottom') { - options.verticalAlign = 'top'; - } else { - options.y = -off; - } - justified = true; - } - - // Off bottom - off = alignAttr.y + bBox.height - padding; - if (off > chart.plotHeight) { - if (verticalAlign === 'top') { - options.verticalAlign = 'bottom'; - } else { - options.y = chart.plotHeight - off; - } - justified = true; - } - - if (justified) { - dataLabel.placed = !isNew; - dataLabel.align(options, null, alignTo); - } - - return justified; + var chart = this.chart, + align = options.align, + verticalAlign = options.verticalAlign, + off, + justified, + padding = dataLabel.box ? 0 : (dataLabel.padding || 0); + + // Off left + off = alignAttr.x + padding; + if (off < 0) { + if (align === 'right') { + options.align = 'left'; + } else { + options.x = -off; + } + justified = true; + } + + // Off right + off = alignAttr.x + bBox.width - padding; + if (off > chart.plotWidth) { + if (align === 'left') { + options.align = 'right'; + } else { + options.x = chart.plotWidth - off; + } + justified = true; + } + + // Off top + off = alignAttr.y + padding; + if (off < 0) { + if (verticalAlign === 'bottom') { + options.verticalAlign = 'top'; + } else { + options.y = -off; + } + justified = true; + } + + // Off bottom + off = alignAttr.y + bBox.height - padding; + if (off > chart.plotHeight) { + if (verticalAlign === 'top') { + options.verticalAlign = 'bottom'; + } else { + options.y = chart.plotHeight - off; + } + justified = true; + } + + if (justified) { + dataLabel.placed = !isNew; + dataLabel.align(options, null, alignTo); + } + + return justified; }; /** * Override the base drawDataLabels method by pie specific functionality */ if (seriesTypes.pie) { - seriesTypes.pie.prototype.drawDataLabels = function () { - var series = this, - data = series.data, - point, - chart = series.chart, - options = series.options.dataLabels, - connectorPadding = pick(options.connectorPadding, 10), - connectorWidth = pick(options.connectorWidth, 1), - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - maxWidth = Math.round(chart.chartWidth / 3), - connector, - seriesCenter = series.center, - radius = seriesCenter[2] / 2, - centerY = seriesCenter[1], - dataLabel, - dataLabelWidth, - labelPos, - labelHeight, - // divide the points into right and left halves for anti collision - halves = [ - [], // right - [] // left - ], - x, - y, - visibility, - j, - overflow = [0, 0, 0, 0]; // top, right, bottom, left - - // get out if not enabled - if (!series.visible || (!options.enabled && !series._hasPointLabels)) { - return; - } - - // Reset all labels that have been shortened - each(data, function (point) { - if (point.dataLabel && point.visible && point.dataLabel.shortened) { - point.dataLabel - .attr({ - width: 'auto' - }).css({ - width: 'auto', - textOverflow: 'clip' - }); - point.dataLabel.shortened = false; - } - }); - - - // run parent method - Series.prototype.drawDataLabels.apply(series); - - each(data, function (point) { - if (point.dataLabel && point.visible) { // #407, #2510 - - // Arrange points for detection collision - halves[point.half].push(point); - - // Reset positions (#4905) - point.dataLabel._pos = null; - - // Avoid long labels squeezing the pie size too far down - /*= if (build.classic) { =*/ - if ( - !defined(options.style.width) && - !defined( - point.options.dataLabels && - point.options.dataLabels.style && - point.options.dataLabels.style.width - ) - ) { - /*= } =*/ - if (point.dataLabel.getBBox().width > maxWidth) { - point.dataLabel.css({ - // Use a fraction of the maxWidth to avoid wrapping - // close to the end of the string. - width: maxWidth * 0.7 - }); - point.dataLabel.shortened = true; - } - /*= if (build.classic) { =*/ - } - /*= } =*/ - } - }); - - /* Loop over the points in each half, starting from the top and bottom - * of the pie to detect overlapping labels. - */ - each(halves, function (points, i) { - - var top, - bottom, - length = points.length, - positions = [], - naturalY, - sideOverflow, - positionsIndex, // Point index in positions array. - size, - distributionLength; - - if (!length) { - return; - } - - // Sort by angle - series.sortByAngle(points, i - 0.5); - // Only do anti-collision when we have dataLabels outside the pie - // and have connectors. (#856) - if (series.maxLabelDistance > 0) { - top = Math.max( - 0, - centerY - radius - series.maxLabelDistance - ); - bottom = Math.min( - centerY + radius + series.maxLabelDistance, - chart.plotHeight - ); - each(points, function (point) { - // check if specific points' label is outside the pie - if (point.labelDistance > 0 && point.dataLabel) { - // point.top depends on point.labelDistance value - // Used for calculation of y value in getX method - point.top = Math.max( - 0, - centerY - radius - point.labelDistance - ); - point.bottom = Math.min( - centerY + radius + point.labelDistance, - chart.plotHeight - ); - size = point.dataLabel.getBBox().height || 21; - - // point.positionsIndex is needed for getting index of - // parameter related to specific point inside positions - // array - not every point is in positions array. - point.positionsIndex = positions.push({ - target: point.labelPos[1] - point.top + size / 2, - size: size, - rank: point.y - }) - 1; - } - }); - distributionLength = bottom + size - top; - H.distribute( - positions, - distributionLength, - distributionLength / 5 - ); - } - - // Now the used slots are sorted, fill them up sequentially - for (j = 0; j < length; j++) { - - point = points[j]; - positionsIndex = point.positionsIndex; - labelPos = point.labelPos; - dataLabel = point.dataLabel; - visibility = point.visible === false ? 'hidden' : 'inherit'; - naturalY = labelPos[1]; - y = naturalY; - - if (positions && defined(positions[positionsIndex])) { - if (positions[positionsIndex].pos === undefined) { - visibility = 'hidden'; - } else { - labelHeight = positions[positionsIndex].size; - y = point.top + positions[positionsIndex].pos; - } - } - - // It is needed to delete point.positionIndex for - // dynamically added points etc. - - delete point.positionIndex; - - // get the x - use the natural x position for labels near the - // top and bottom, to prevent the top and botton slice - // connectors from touching each other on either side - if (options.justify) { - x = seriesCenter[0] + - (i ? -1 : 1) * (radius + point.labelDistance); - } else { - x = series.getX( - y < point.top + 2 || y > point.bottom - 2 ? - naturalY : - y, - i, - point - ); - } - - - // Record the placement and visibility - dataLabel._attr = { - visibility: visibility, - align: labelPos[6] - }; - dataLabel._pos = { - x: ( - x + - options.x + - ({ - left: connectorPadding, - right: -connectorPadding - }[labelPos[6]] || 0) - ), - - // 10 is for the baseline (label vs text) - y: y + options.y - 10 - }; - labelPos.x = x; - labelPos.y = y; - - - // Detect overflowing data labels - if (pick(options.crop, true)) { - dataLabelWidth = dataLabel.getBBox().width; - - sideOverflow = null; - // Overflow left - if ( - x - dataLabelWidth < connectorPadding && - i === 1 // left half - ) { - sideOverflow = Math.round( - dataLabelWidth - x + connectorPadding - ); - overflow[3] = Math.max(sideOverflow, overflow[3]); - - // Overflow right - } else if ( - x + dataLabelWidth > plotWidth - connectorPadding && - i === 0 // right half - ) { - sideOverflow = Math.round( - x + dataLabelWidth - plotWidth + connectorPadding - ); - overflow[1] = Math.max(sideOverflow, overflow[1]); - } - - // Overflow top - if (y - labelHeight / 2 < 0) { - overflow[0] = Math.max( - Math.round(-y + labelHeight / 2), - overflow[0] - ); - - // Overflow left - } else if (y + labelHeight / 2 > plotHeight) { - overflow[2] = Math.max( - Math.round(y + labelHeight / 2 - plotHeight), - overflow[2] - ); - } - dataLabel.sideOverflow = sideOverflow; - } - } // for each point - }); // for each half - - // Do not apply the final placement and draw the connectors until we - // have verified that labels are not spilling over. - if ( - arrayMax(overflow) === 0 || - this.verifyDataLabelOverflow(overflow) - ) { - - // Place the labels in the final position - this.placeDataLabels(); - - // Draw the connectors - if (connectorWidth) { - each(this.points, function (point) { - var isNew; - - connector = point.connector; - dataLabel = point.dataLabel; - - if ( - dataLabel && - dataLabel._pos && - point.visible && - point.labelDistance > 0 - ) { - visibility = dataLabel._attr.visibility; - - isNew = !connector; - - if (isNew) { - point.connector = connector = chart.renderer.path() - .addClass('highcharts-data-label-connector ' + - ' highcharts-color-' + point.colorIndex) - .add(series.dataLabelsGroup); - - /*= if (build.classic) { =*/ - connector.attr({ - 'stroke-width': connectorWidth, - 'stroke': ( - options.connectorColor || - point.color || - '${palette.neutralColor60}' - ) - }); - /*= } =*/ - } - connector[isNew ? 'attr' : 'animate']({ - d: series.connectorPath(point.labelPos) - }); - connector.attr('visibility', visibility); - - } else if (connector) { - point.connector = connector.destroy(); - } - }); - } - } - }; - - /** - * Extendable method for getting the path of the connector between the data - * label and the pie slice. - */ - seriesTypes.pie.prototype.connectorPath = function (labelPos) { - var x = labelPos.x, - y = labelPos.y; - return pick(this.options.dataLabels.softConnector, true) ? [ - 'M', - // end of the string at the label - x + (labelPos[6] === 'left' ? 5 : -5), y, - 'C', - x, y, // first break, next to the label - 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], - labelPos[2], labelPos[3], // second break - 'L', - labelPos[4], labelPos[5] // base - ] : [ - 'M', - // end of the string at the label - x + (labelPos[6] === 'left' ? 5 : -5), y, - 'L', - labelPos[2], labelPos[3], // second break - 'L', - labelPos[4], labelPos[5] // base - ]; - }; - - /** - * Perform the final placement of the data labels after we have verified - * that they fall within the plot area. - */ - seriesTypes.pie.prototype.placeDataLabels = function () { - each(this.points, function (point) { - var dataLabel = point.dataLabel, - _pos; - if (dataLabel && point.visible) { - _pos = dataLabel._pos; - if (_pos) { - - // Shorten data labels with ellipsis if they still overflow - // after the pie has reached minSize (#223). - if (dataLabel.sideOverflow) { - dataLabel._attr.width = - dataLabel.getBBox().width - dataLabel.sideOverflow; - - dataLabel.css({ - width: dataLabel._attr.width + 'px', - textOverflow: ( - this.options.dataLabels.style.textOverflow || - 'ellipsis' - ) - }); - dataLabel.shortened = true; - } - - dataLabel.attr(dataLabel._attr); - dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); - dataLabel.moved = true; - } else if (dataLabel) { - dataLabel.attr({ y: -9999 }); - } - } - }, this); - }; - - seriesTypes.pie.prototype.alignDataLabel = noop; - - /** - * Verify whether the data labels are allowed to draw, or we should run more - * translation and data label positioning to keep them inside the plot area. - * Returns true when data labels are ready to draw. - */ - seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) { - - var center = this.center, - options = this.options, - centerOption = options.center, - minSize = options.minSize || 80, - newSize = minSize, - // If a size is set, return true and don't try to shrink the pie - // to fit the labels. - ret = options.size !== null; - - if (!ret) { - // Handle horizontal size and center - if (centerOption[0] !== null) { // Fixed center - newSize = Math.max(center[2] - - Math.max(overflow[1], overflow[3]), minSize); - - } else { // Auto center - newSize = Math.max( - // horizontal overflow - center[2] - overflow[1] - overflow[3], - minSize - ); - // horizontal center - center[0] += (overflow[3] - overflow[1]) / 2; - } - - // Handle vertical size and center - if (centerOption[1] !== null) { // Fixed center - newSize = Math.max(Math.min(newSize, center[2] - - Math.max(overflow[0], overflow[2])), minSize); - - } else { // Auto center - newSize = Math.max( - Math.min( - newSize, - // vertical overflow - center[2] - overflow[0] - overflow[2] - ), - minSize - ); - // vertical center - center[1] += (overflow[0] - overflow[2]) / 2; - } - - // If the size must be decreased, we need to run translate and - // drawDataLabels again - if (newSize < center[2]) { - center[2] = newSize; - center[3] = Math.min( // #3632 - relativeLength(options.innerSize || 0, newSize), - newSize - ); - this.translate(center); - - if (this.drawDataLabels) { - this.drawDataLabels(); - } - // Else, return true to indicate that the pie and its labels is - // within the plot area - } else { - ret = true; - } - } - return ret; - }; + seriesTypes.pie.prototype.drawDataLabels = function () { + var series = this, + data = series.data, + point, + chart = series.chart, + options = series.options.dataLabels, + connectorPadding = pick(options.connectorPadding, 10), + connectorWidth = pick(options.connectorWidth, 1), + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + maxWidth = Math.round(chart.chartWidth / 3), + connector, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + dataLabel, + dataLabelWidth, + labelPos, + labelHeight, + // divide the points into right and left halves for anti collision + halves = [ + [], // right + [] // left + ], + x, + y, + visibility, + j, + overflow = [0, 0, 0, 0]; // top, right, bottom, left + + // get out if not enabled + if (!series.visible || (!options.enabled && !series._hasPointLabels)) { + return; + } + + // Reset all labels that have been shortened + each(data, function (point) { + if (point.dataLabel && point.visible && point.dataLabel.shortened) { + point.dataLabel + .attr({ + width: 'auto' + }).css({ + width: 'auto', + textOverflow: 'clip' + }); + point.dataLabel.shortened = false; + } + }); + + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + each(data, function (point) { + if (point.dataLabel && point.visible) { // #407, #2510 + + // Arrange points for detection collision + halves[point.half].push(point); + + // Reset positions (#4905) + point.dataLabel._pos = null; + + // Avoid long labels squeezing the pie size too far down + /*= if (build.classic) { =*/ + if ( + !defined(options.style.width) && + !defined( + point.options.dataLabels && + point.options.dataLabels.style && + point.options.dataLabels.style.width + ) + ) { + /*= } =*/ + if (point.dataLabel.getBBox().width > maxWidth) { + point.dataLabel.css({ + // Use a fraction of the maxWidth to avoid wrapping + // close to the end of the string. + width: maxWidth * 0.7 + }); + point.dataLabel.shortened = true; + } + /*= if (build.classic) { =*/ + } + /*= } =*/ + } + }); + + /* Loop over the points in each half, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + each(halves, function (points, i) { + + var top, + bottom, + length = points.length, + positions = [], + naturalY, + sideOverflow, + positionsIndex, // Point index in positions array. + size, + distributionLength; + + if (!length) { + return; + } + + // Sort by angle + series.sortByAngle(points, i - 0.5); + // Only do anti-collision when we have dataLabels outside the pie + // and have connectors. (#856) + if (series.maxLabelDistance > 0) { + top = Math.max( + 0, + centerY - radius - series.maxLabelDistance + ); + bottom = Math.min( + centerY + radius + series.maxLabelDistance, + chart.plotHeight + ); + each(points, function (point) { + // check if specific points' label is outside the pie + if (point.labelDistance > 0 && point.dataLabel) { + // point.top depends on point.labelDistance value + // Used for calculation of y value in getX method + point.top = Math.max( + 0, + centerY - radius - point.labelDistance + ); + point.bottom = Math.min( + centerY + radius + point.labelDistance, + chart.plotHeight + ); + size = point.dataLabel.getBBox().height || 21; + + // point.positionsIndex is needed for getting index of + // parameter related to specific point inside positions + // array - not every point is in positions array. + point.positionsIndex = positions.push({ + target: point.labelPos[1] - point.top + size / 2, + size: size, + rank: point.y + }) - 1; + } + }); + distributionLength = bottom + size - top; + H.distribute( + positions, + distributionLength, + distributionLength / 5 + ); + } + + // Now the used slots are sorted, fill them up sequentially + for (j = 0; j < length; j++) { + + point = points[j]; + positionsIndex = point.positionsIndex; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + visibility = point.visible === false ? 'hidden' : 'inherit'; + naturalY = labelPos[1]; + y = naturalY; + + if (positions && defined(positions[positionsIndex])) { + if (positions[positionsIndex].pos === undefined) { + visibility = 'hidden'; + } else { + labelHeight = positions[positionsIndex].size; + y = point.top + positions[positionsIndex].pos; + } + } + + // It is needed to delete point.positionIndex for + // dynamically added points etc. + + delete point.positionIndex; + + // get the x - use the natural x position for labels near the + // top and bottom, to prevent the top and botton slice + // connectors from touching each other on either side + if (options.justify) { + x = seriesCenter[0] + + (i ? -1 : 1) * (radius + point.labelDistance); + } else { + x = series.getX( + y < point.top + 2 || y > point.bottom - 2 ? + naturalY : + y, + i, + point + ); + } + + + // Record the placement and visibility + dataLabel._attr = { + visibility: visibility, + align: labelPos[6] + }; + dataLabel._pos = { + x: ( + x + + options.x + + ({ + left: connectorPadding, + right: -connectorPadding + }[labelPos[6]] || 0) + ), + + // 10 is for the baseline (label vs text) + y: y + options.y - 10 + }; + labelPos.x = x; + labelPos.y = y; + + + // Detect overflowing data labels + if (pick(options.crop, true)) { + dataLabelWidth = dataLabel.getBBox().width; + + sideOverflow = null; + // Overflow left + if ( + x - dataLabelWidth < connectorPadding && + i === 1 // left half + ) { + sideOverflow = Math.round( + dataLabelWidth - x + connectorPadding + ); + overflow[3] = Math.max(sideOverflow, overflow[3]); + + // Overflow right + } else if ( + x + dataLabelWidth > plotWidth - connectorPadding && + i === 0 // right half + ) { + sideOverflow = Math.round( + x + dataLabelWidth - plotWidth + connectorPadding + ); + overflow[1] = Math.max(sideOverflow, overflow[1]); + } + + // Overflow top + if (y - labelHeight / 2 < 0) { + overflow[0] = Math.max( + Math.round(-y + labelHeight / 2), + overflow[0] + ); + + // Overflow left + } else if (y + labelHeight / 2 > plotHeight) { + overflow[2] = Math.max( + Math.round(y + labelHeight / 2 - plotHeight), + overflow[2] + ); + } + dataLabel.sideOverflow = sideOverflow; + } + } // for each point + }); // for each half + + // Do not apply the final placement and draw the connectors until we + // have verified that labels are not spilling over. + if ( + arrayMax(overflow) === 0 || + this.verifyDataLabelOverflow(overflow) + ) { + + // Place the labels in the final position + this.placeDataLabels(); + + // Draw the connectors + if (connectorWidth) { + each(this.points, function (point) { + var isNew; + + connector = point.connector; + dataLabel = point.dataLabel; + + if ( + dataLabel && + dataLabel._pos && + point.visible && + point.labelDistance > 0 + ) { + visibility = dataLabel._attr.visibility; + + isNew = !connector; + + if (isNew) { + point.connector = connector = chart.renderer.path() + .addClass('highcharts-data-label-connector ' + + ' highcharts-color-' + point.colorIndex) + .add(series.dataLabelsGroup); + + /*= if (build.classic) { =*/ + connector.attr({ + 'stroke-width': connectorWidth, + 'stroke': ( + options.connectorColor || + point.color || + '${palette.neutralColor60}' + ) + }); + /*= } =*/ + } + connector[isNew ? 'attr' : 'animate']({ + d: series.connectorPath(point.labelPos) + }); + connector.attr('visibility', visibility); + + } else if (connector) { + point.connector = connector.destroy(); + } + }); + } + } + }; + + /** + * Extendable method for getting the path of the connector between the data + * label and the pie slice. + */ + seriesTypes.pie.prototype.connectorPath = function (labelPos) { + var x = labelPos.x, + y = labelPos.y; + return pick(this.options.dataLabels.softConnector, true) ? [ + 'M', + // end of the string at the label + x + (labelPos[6] === 'left' ? 5 : -5), y, + 'C', + x, y, // first break, next to the label + 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], + labelPos[2], labelPos[3], // second break + 'L', + labelPos[4], labelPos[5] // base + ] : [ + 'M', + // end of the string at the label + x + (labelPos[6] === 'left' ? 5 : -5), y, + 'L', + labelPos[2], labelPos[3], // second break + 'L', + labelPos[4], labelPos[5] // base + ]; + }; + + /** + * Perform the final placement of the data labels after we have verified + * that they fall within the plot area. + */ + seriesTypes.pie.prototype.placeDataLabels = function () { + each(this.points, function (point) { + var dataLabel = point.dataLabel, + _pos; + if (dataLabel && point.visible) { + _pos = dataLabel._pos; + if (_pos) { + + // Shorten data labels with ellipsis if they still overflow + // after the pie has reached minSize (#223). + if (dataLabel.sideOverflow) { + dataLabel._attr.width = + dataLabel.getBBox().width - dataLabel.sideOverflow; + + dataLabel.css({ + width: dataLabel._attr.width + 'px', + textOverflow: ( + this.options.dataLabels.style.textOverflow || + 'ellipsis' + ) + }); + dataLabel.shortened = true; + } + + dataLabel.attr(dataLabel._attr); + dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); + dataLabel.moved = true; + } else if (dataLabel) { + dataLabel.attr({ y: -9999 }); + } + } + }, this); + }; + + seriesTypes.pie.prototype.alignDataLabel = noop; + + /** + * Verify whether the data labels are allowed to draw, or we should run more + * translation and data label positioning to keep them inside the plot area. + * Returns true when data labels are ready to draw. + */ + seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) { + + var center = this.center, + options = this.options, + centerOption = options.center, + minSize = options.minSize || 80, + newSize = minSize, + // If a size is set, return true and don't try to shrink the pie + // to fit the labels. + ret = options.size !== null; + + if (!ret) { + // Handle horizontal size and center + if (centerOption[0] !== null) { // Fixed center + newSize = Math.max(center[2] - + Math.max(overflow[1], overflow[3]), minSize); + + } else { // Auto center + newSize = Math.max( + // horizontal overflow + center[2] - overflow[1] - overflow[3], + minSize + ); + // horizontal center + center[0] += (overflow[3] - overflow[1]) / 2; + } + + // Handle vertical size and center + if (centerOption[1] !== null) { // Fixed center + newSize = Math.max(Math.min(newSize, center[2] - + Math.max(overflow[0], overflow[2])), minSize); + + } else { // Auto center + newSize = Math.max( + Math.min( + newSize, + // vertical overflow + center[2] - overflow[0] - overflow[2] + ), + minSize + ); + // vertical center + center[1] += (overflow[0] - overflow[2]) / 2; + } + + // If the size must be decreased, we need to run translate and + // drawDataLabels again + if (newSize < center[2]) { + center[2] = newSize; + center[3] = Math.min( // #3632 + relativeLength(options.innerSize || 0, newSize), + newSize + ); + this.translate(center); + + if (this.drawDataLabels) { + this.drawDataLabels(); + } + // Else, return true to indicate that the pie and its labels is + // within the plot area + } else { + ret = true; + } + } + return ret; + }; } if (seriesTypes.column) { - /** - * Override the basic data label alignment by adjusting for the position of - * the column - */ - seriesTypes.column.prototype.alignDataLabel = function ( - point, - dataLabel, - options, - alignTo, - isNew - ) { - var inverted = this.chart.inverted, - series = point.series, - // data label box for alignment - dlBox = point.dlBox || point.shapeArgs, - below = pick( - point.below, // range series - point.plotY > pick(this.translatedThreshold, series.yAxis.len) - ), - // draw it inside the box? - inside = pick(options.inside, !!this.options.stacking), - overshoot; - - // Align to the column itself, or the top of it - if (dlBox) { // Area range uses this method but not alignTo - alignTo = merge(dlBox); - - if (alignTo.y < 0) { - alignTo.height += alignTo.y; - alignTo.y = 0; - } - overshoot = alignTo.y + alignTo.height - series.yAxis.len; - if (overshoot > 0) { - alignTo.height -= overshoot; - } - - if (inverted) { - alignTo = { - x: series.yAxis.len - alignTo.y - alignTo.height, - y: series.xAxis.len - alignTo.x - alignTo.width, - width: alignTo.height, - height: alignTo.width - }; - } - - // Compute the alignment box - if (!inside) { - if (inverted) { - alignTo.x += below ? 0 : alignTo.width; - alignTo.width = 0; - } else { - alignTo.y += below ? alignTo.height : 0; - alignTo.height = 0; - } - } - } - - - // When alignment is undefined (typically columns and bars), display the - // individual point below or above the point depending on the threshold - options.align = pick( - options.align, - !inverted || inside ? 'center' : below ? 'right' : 'left' - ); - options.verticalAlign = pick( - options.verticalAlign, - inverted || inside ? 'middle' : below ? 'top' : 'bottom' - ); - - // Call the parent method - Series.prototype.alignDataLabel.call( - this, - point, - dataLabel, - options, - alignTo, - isNew - ); - - // If label was justified and we have contrast, set it: - if (point.isLabelJustified && point.contrastColor) { - point.dataLabel.css({ - color: point.contrastColor - }); - } - }; + /** + * Override the basic data label alignment by adjusting for the position of + * the column + */ + seriesTypes.column.prototype.alignDataLabel = function ( + point, + dataLabel, + options, + alignTo, + isNew + ) { + var inverted = this.chart.inverted, + series = point.series, + // data label box for alignment + dlBox = point.dlBox || point.shapeArgs, + below = pick( + point.below, // range series + point.plotY > pick(this.translatedThreshold, series.yAxis.len) + ), + // draw it inside the box? + inside = pick(options.inside, !!this.options.stacking), + overshoot; + + // Align to the column itself, or the top of it + if (dlBox) { // Area range uses this method but not alignTo + alignTo = merge(dlBox); + + if (alignTo.y < 0) { + alignTo.height += alignTo.y; + alignTo.y = 0; + } + overshoot = alignTo.y + alignTo.height - series.yAxis.len; + if (overshoot > 0) { + alignTo.height -= overshoot; + } + + if (inverted) { + alignTo = { + x: series.yAxis.len - alignTo.y - alignTo.height, + y: series.xAxis.len - alignTo.x - alignTo.width, + width: alignTo.height, + height: alignTo.width + }; + } + + // Compute the alignment box + if (!inside) { + if (inverted) { + alignTo.x += below ? 0 : alignTo.width; + alignTo.width = 0; + } else { + alignTo.y += below ? alignTo.height : 0; + alignTo.height = 0; + } + } + } + + + // When alignment is undefined (typically columns and bars), display the + // individual point below or above the point depending on the threshold + options.align = pick( + options.align, + !inverted || inside ? 'center' : below ? 'right' : 'left' + ); + options.verticalAlign = pick( + options.verticalAlign, + inverted || inside ? 'middle' : below ? 'top' : 'bottom' + ); + + // Call the parent method + Series.prototype.alignDataLabel.call( + this, + point, + dataLabel, + options, + alignTo, + isNew + ); + + // If label was justified and we have contrast, set it: + if (point.isLabelJustified && point.contrastColor) { + point.dataLabel.css({ + color: point.contrastColor + }); + } + }; } diff --git a/js/parts/DateTimeAxis.js b/js/parts/DateTimeAxis.js index ac38b427e86..c14a796b103 100644 --- a/js/parts/DateTimeAxis.js +++ b/js/parts/DateTimeAxis.js @@ -3,14 +3,14 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from './Globals.js'; import './Utilities.js'; var Axis = H.Axis, - getMagnitude = H.getMagnitude, - normalizeTickInterval = H.normalizeTickInterval, - timeUnits = H.timeUnits; + getMagnitude = H.getMagnitude, + normalizeTickInterval = H.normalizeTickInterval, + timeUnits = H.timeUnits; /** * Set the tick positions to a time unit that makes sense, for example * on the first of each month or on every Monday. Return an array @@ -24,7 +24,7 @@ var Axis = H.Axis, * @param {Number} startOfWeek */ Axis.prototype.getTimeTicks = function () { - return this.chart.time.getTimeTicks.apply(this.chart.time, arguments); + return this.chart.time.getTimeTicks.apply(this.chart.time, arguments); }; /** @@ -36,77 +36,77 @@ Axis.prototype.getTimeTicks = function () { * #662, #697. */ Axis.prototype.normalizeTimeTickInterval = function ( - tickInterval, - unitsOption + tickInterval, + unitsOption ) { - var units = unitsOption || [[ - '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, 2] - ], [ - 'week', - [1, 2] - ], [ - 'month', - [1, 2, 3, 4, 6] - ], [ - 'year', - null - ]], - unit = units[units.length - 1], // default unit is years - interval = timeUnits[unit[0]], - multiples = unit[1], - count, - i; + var units = unitsOption || [[ + '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, 2] + ], [ + 'week', + [1, 2] + ], [ + 'month', + [1, 2, 3, 4, 6] + ], [ + 'year', + null + ]], + unit = units[units.length - 1], // default unit is years + interval = timeUnits[unit[0]], + multiples = unit[1], + count, + i; - // loop through the units to find the one that best fits the tickInterval - for (i = 0; i < units.length; i++) { - unit = units[i]; - interval = timeUnits[unit[0]]; - multiples = unit[1]; + // loop through the units to find the one that best fits the tickInterval + for (i = 0; i < units.length; i++) { + unit = units[i]; + interval = timeUnits[unit[0]]; + multiples = unit[1]; - if (units[i + 1]) { - // lessThan is in the middle between the highest multiple and the - // next unit. - var lessThan = (interval * multiples[multiples.length - 1] + - timeUnits[units[i + 1][0]]) / 2; + if (units[i + 1]) { + // lessThan is in the middle between the highest multiple and the + // next unit. + var lessThan = (interval * multiples[multiples.length - 1] + + timeUnits[units[i + 1][0]]) / 2; - // break and keep the current unit - if (tickInterval <= lessThan) { - break; - } - } - } + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval === timeUnits.year && tickInterval < 5 * interval) { - multiples = [1, 2, 5]; - } + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval === timeUnits.year && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } - // get the count - count = normalizeTickInterval( - tickInterval / interval, - multiples, - unit[0] === 'year' ? - Math.max(getMagnitude(tickInterval / interval), 1) : // #1913, #2360 - 1 - ); + // get the count + count = normalizeTickInterval( + tickInterval / interval, + multiples, + unit[0] === 'year' ? + Math.max(getMagnitude(tickInterval / interval), 1) : // #1913, #2360 + 1 + ); - return { - unitRange: interval, - count: count, - unitName: unit[0] - }; + return { + unitRange: interval, + count: count, + unitName: unit[0] + }; }; diff --git a/js/parts/Dynamics.js b/js/parts/Dynamics.js index 27273cc7d7e..c546bf29a01 100644 --- a/js/parts/Dynamics.js +++ b/js/parts/Dynamics.js @@ -11,80 +11,80 @@ import './Chart.js'; import './Point.js'; import './Series.js'; var addEvent = H.addEvent, - animate = H.animate, - Axis = H.Axis, - Chart = H.Chart, - createElement = H.createElement, - css = H.css, - defined = H.defined, - each = H.each, - erase = H.erase, - extend = H.extend, - fireEvent = H.fireEvent, - inArray = H.inArray, - isNumber = H.isNumber, - isObject = H.isObject, - isArray = H.isArray, - merge = H.merge, - objectEach = H.objectEach, - pick = H.pick, - Point = H.Point, - Series = H.Series, - seriesTypes = H.seriesTypes, - setAnimation = H.setAnimation, - splat = H.splat; - + animate = H.animate, + Axis = H.Axis, + Chart = H.Chart, + createElement = H.createElement, + css = H.css, + defined = H.defined, + each = H.each, + erase = H.erase, + extend = H.extend, + fireEvent = H.fireEvent, + inArray = H.inArray, + isNumber = H.isNumber, + isObject = H.isObject, + isArray = H.isArray, + merge = H.merge, + objectEach = H.objectEach, + pick = H.pick, + Point = H.Point, + Series = H.Series, + seriesTypes = H.seriesTypes, + setAnimation = H.setAnimation, + splat = H.splat; + // Extend the Chart prototype for dynamic methods extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { - /** - * Add a series to the chart after render time. Note that this method should - * never be used when adding data synchronously at chart render time, as it - * adds expense to the calculations and rendering. When adding data at the - * same time as the chart is initialized, add the series as a configuration - * option instead. With multiple axes, the `offset` is dynamically adjusted. - * - * @param {SeriesOptions} options - * The config options for the series. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after adding. - * @param {AnimationOptions} animation - * Whether to apply animation, and optionally animation - * configuration. - * - * @return {Highcharts.Series} - * The newly created series object. - * - * @sample highcharts/members/chart-addseries/ - * Add a series from a button - * @sample stock/members/chart-addseries/ - * Add a series in Highstock - */ - addSeries: function (options, redraw, animation) { - var series, - chart = this; - - if (options) { - redraw = pick(redraw, true); // defaults to true - - fireEvent(chart, 'addSeries', { options: options }, function () { - series = chart.initSeries(options); - - chart.isDirtyLegend = true; - chart.linkSeries(); - - fireEvent(chart, 'afterAddSeries'); - - if (redraw) { - chart.redraw(animation); - } - }); - } - - return series; - }, - - /** + /** + * Add a series to the chart after render time. Note that this method should + * never be used when adding data synchronously at chart render time, as it + * adds expense to the calculations and rendering. When adding data at the + * same time as the chart is initialized, add the series as a configuration + * option instead. With multiple axes, the `offset` is dynamically adjusted. + * + * @param {SeriesOptions} options + * The config options for the series. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after adding. + * @param {AnimationOptions} animation + * Whether to apply animation, and optionally animation + * configuration. + * + * @return {Highcharts.Series} + * The newly created series object. + * + * @sample highcharts/members/chart-addseries/ + * Add a series from a button + * @sample stock/members/chart-addseries/ + * Add a series in Highstock + */ + addSeries: function (options, redraw, animation) { + var series, + chart = this; + + if (options) { + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function () { + series = chart.initSeries(options); + + chart.isDirtyLegend = true; + chart.linkSeries(); + + fireEvent(chart, 'afterAddSeries'); + + if (redraw) { + chart.redraw(animation); + } + }); + } + + return series; + }, + + /** * Add an axis to the chart after render time. Note that this method should * never be used when adding data synchronously at chart render time, as it * adds expense to the calculations and rendering. When adding data at the @@ -104,948 +104,948 @@ extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { * @return {Axis} * The newly generated Axis object. */ - addAxis: function (options, isX, redraw, animation) { - var key = isX ? 'xAxis' : 'yAxis', - chartOptions = this.options, - userOptions = merge(options, { - index: this[key].length, - isX: isX - }), - axis; - - axis = new Axis(this, userOptions); - - // Push the new axis options to the chart options - chartOptions[key] = splat(chartOptions[key] || {}); - chartOptions[key].push(userOptions); - - if (pick(redraw, true)) { - this.redraw(animation); - } - - return axis; - }, - - /** - * Dim the chart and show a loading text or symbol. Options for the loading - * screen are defined in {@link - * https://api.highcharts.com/highcharts/loading|the loading options}. - * - * @param {String} str - * An optional text to show in the loading label instead of the - * default one. The default text is set in {@link - * http://api.highcharts.com/highcharts/lang.loading|lang.loading}. - * - * @sample highcharts/members/chart-hideloading/ - * Show and hide loading from a button - * @sample highcharts/members/chart-showloading/ - * Apply different text labels - * @sample stock/members/chart-show-hide-loading/ - * Toggle loading in Highstock - */ - showLoading: function (str) { - var chart = this, - options = chart.options, - loadingDiv = chart.loadingDiv, - loadingOptions = options.loading, - setLoadingSize = function () { - if (loadingDiv) { - css(loadingDiv, { - left: chart.plotLeft + 'px', - top: chart.plotTop + 'px', - width: chart.plotWidth + 'px', - height: chart.plotHeight + 'px' - }); - } - }; - - // create the layer at the first call - if (!loadingDiv) { - chart.loadingDiv = loadingDiv = createElement('div', { - className: 'highcharts-loading highcharts-loading-hidden' - }, null, chart.container); - - chart.loadingSpan = createElement( - 'span', - { className: 'highcharts-loading-inner' }, - null, - loadingDiv - ); - addEvent(chart, 'redraw', setLoadingSize); // #1080 - } - - loadingDiv.className = 'highcharts-loading'; - - // Update text - chart.loadingSpan.innerHTML = str || options.lang.loading; - - /*= if (build.classic) { =*/ - // Update visuals - css(loadingDiv, extend(loadingOptions.style, { - zIndex: 10 - })); - css(chart.loadingSpan, loadingOptions.labelStyle); - - // Show it - if (!chart.loadingShown) { - css(loadingDiv, { - opacity: 0, - display: '' - }); - animate(loadingDiv, { - opacity: loadingOptions.style.opacity || 0.5 - }, { - duration: loadingOptions.showDuration || 0 - }); - } - /*= } =*/ - - chart.loadingShown = true; - setLoadingSize(); - }, - - /** - * Hide the loading layer. - * - * @see Highcharts.Chart#showLoading - * @sample highcharts/members/chart-hideloading/ - * Show and hide loading from a button - * @sample stock/members/chart-show-hide-loading/ - * Toggle loading in Highstock - */ - hideLoading: function () { - var options = this.options, - loadingDiv = this.loadingDiv; - - if (loadingDiv) { - loadingDiv.className = - 'highcharts-loading highcharts-loading-hidden'; - /*= if (build.classic) { =*/ - animate(loadingDiv, { - opacity: 0 - }, { - duration: options.loading.hideDuration || 100, - complete: function () { - css(loadingDiv, { display: 'none' }); - } - }); - /*= } =*/ - } - this.loadingShown = false; - }, - - /** - * These properties cause isDirtyBox to be set to true when updating. Can be - * extended from plugins. - */ - propsRequireDirtyBox: [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'margin', - 'marginTop', - 'marginRight', - 'marginBottom', - 'marginLeft', - 'spacing', - 'spacingTop', - 'spacingRight', - 'spacingBottom', - 'spacingLeft', - 'borderRadius', - 'plotBackgroundColor', - 'plotBackgroundImage', - 'plotBorderColor', - 'plotBorderWidth', - 'plotShadow', - 'shadow' - ], - - /** - * These properties cause all series to be updated when updating. Can be - * extended from plugins. - */ - propsRequireUpdateSeries: [ - 'chart.inverted', - 'chart.polar', - 'chart.ignoreHiddenSeries', - 'chart.type', - 'colors', - 'plotOptions', - 'time', - 'tooltip' - ], - - /** - * A generic function to update any element of the chart. Elements can be - * enabled and disabled, moved, re-styled, re-formatted etc. - * - * A special case is configuration objects that take arrays, for example - * {@link https://api.highcharts.com/highcharts/xAxis|xAxis}, - * {@link https://api.highcharts.com/highcharts/yAxis|yAxis} or - * {@link https://api.highcharts.com/highcharts/series|series}. For these - * collections, an `id` option is used to map the new option set to an - * existing object. If an existing object of the same id is not found, the - * corresponding item is updated. So for example, running `chart.update` - * with a series item without an id, will cause the existing chart's series - * with the same index in the series array to be updated. When the - * `oneToOne` parameter is true, `chart.update` will also take care of - * adding and removing items from the collection. Read more under the - * parameter description below. - * - * See also the {@link https://api.highcharts.com/highcharts/responsive| - * responsive option set}. Switching between `responsive.rules` basically - * runs `chart.update` under the hood. - * - * @param {Options} options - * A configuration object for the new chart options. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart. - * @param {Boolean} [oneToOne=false] - * When `true`, the `series`, `xAxis` and `yAxis` collections will - * be updated one to one, and items will be either added or removed - * to match the new updated options. For example, if the chart has - * two series and we call `chart.update` with a configuration - * containing three series, one will be added. If we call - * `chart.update` with one series, one will be removed. Setting an - * empty `series` array will remove all series, but leaving out the - * `series` property will leave all series untouched. If the series - * have id's, the new series options will be matched by id, and the - * remaining ones removed. - * @param {AnimationOptions} [animation=true] - * Whether to apply animation, and optionally animation - * configuration. - * - * @sample highcharts/members/chart-update/ - * Update chart geometry - */ - update: function (options, redraw, oneToOne, animation) { - var chart = this, - adders = { - credits: 'addCredits', - title: 'setTitle', - subtitle: 'setSubtitle' - }, - optionsChart = options.chart, - updateAllAxes, - updateAllSeries, - newWidth, - newHeight, - itemsForRemoval = []; - - fireEvent(chart, 'update', { options: options }); - - // If the top-level chart option is present, some special updates are - // required - if (optionsChart) { - merge(true, chart.options.chart, optionsChart); - - // Setter function - if ('className' in optionsChart) { - chart.setClassName(optionsChart.className); - } - - if ('reflow' in optionsChart) { - chart.setReflow(optionsChart.reflow); - } - - if ('inverted' in optionsChart || 'polar' in optionsChart) { - // Parse options.chart.inverted and options.chart.polar together - // with the available series. - chart.propFromSeries(); - updateAllAxes = true; - } - - if ('alignTicks' in optionsChart) { // #6452 - updateAllAxes = true; - } - - objectEach(optionsChart, function (val, key) { - if ( - inArray('chart.' + key, chart.propsRequireUpdateSeries) !== - -1 - ) { - updateAllSeries = true; - } - // Only dirty box - if (inArray(key, chart.propsRequireDirtyBox) !== -1) { - chart.isDirtyBox = true; - } - }); - - /*= if (build.classic) { =*/ - if ('style' in optionsChart) { - chart.renderer.setStyle(optionsChart.style); - } - /*= } =*/ - } - - // Moved up, because tooltip needs updated plotOptions (#6218) - /*= if (build.classic) { =*/ - if (options.colors) { - this.options.colors = options.colors; - } - /*= } =*/ - - if (options.plotOptions) { - merge(true, this.options.plotOptions, options.plotOptions); - } - - // Some option stuctures correspond one-to-one to chart objects that - // have update methods, for example - // options.credits => chart.credits - // options.legend => chart.legend - // options.title => chart.title - // options.tooltip => chart.tooltip - // options.subtitle => chart.subtitle - // options.mapNavigation => chart.mapNavigation - // options.navigator => chart.navigator - // options.scrollbar => chart.scrollbar - objectEach(options, function (val, key) { - if (chart[key] && typeof chart[key].update === 'function') { - chart[key].update(val, false); - - // If a one-to-one object does not exist, look for an adder function - } else if (typeof chart[adders[key]] === 'function') { - chart[adders[key]](val); - } - - if ( - key !== 'chart' && - inArray(key, chart.propsRequireUpdateSeries) !== -1 - ) { - updateAllSeries = true; - } - }); - - // Setters for collections. For axes and series, each item is referred - // by an id. If the id is not found, it defaults to the corresponding - // item in the collection, so setting one series without an id, will - // update the first series in the chart. Setting two series without - // an id will update the first and the second respectively (#6019) - // chart.update and responsive. - each([ - 'xAxis', - 'yAxis', - 'zAxis', - 'series', - 'colorAxis', - 'pane' - ], function (coll) { - if (options[coll]) { - each(splat(options[coll]), function (newOptions, i) { - var item = ( - defined(newOptions.id) && - chart.get(newOptions.id) - ) || chart[coll][i]; - if (item && item.coll === coll) { - item.update(newOptions, false); - - if (oneToOne) { - item.touched = true; - } - } - - // If oneToOne and no matching item is found, add one - if (!item && oneToOne) { - if (coll === 'series') { - chart.addSeries(newOptions, false) - .touched = true; - } else if (coll === 'xAxis' || coll === 'yAxis') { - chart.addAxis(newOptions, coll === 'xAxis', false) - .touched = true; - } - } - - }); - - // Add items for removal - if (oneToOne) { - each(chart[coll], function (item) { - if (!item.touched) { - itemsForRemoval.push(item); - } else { - delete item.touched; - } - }); - } - - - } - }); - - each(itemsForRemoval, function (item) { - item.remove(false); - }); - - if (updateAllAxes) { - each(chart.axes, function (axis) { - axis.update({}, false); - }); - } - - // Certain options require the whole series structure to be thrown away - // and rebuilt - if (updateAllSeries) { - each(chart.series, function (series) { - series.update({}, false); - }); - } - - // For loading, just update the options, do not redraw - if (options.loading) { - merge(true, chart.options.loading, options.loading); - } - - // Update size. Redraw is forced. - newWidth = optionsChart && optionsChart.width; - newHeight = optionsChart && optionsChart.height; - if ((isNumber(newWidth) && newWidth !== chart.chartWidth) || - (isNumber(newHeight) && newHeight !== chart.chartHeight)) { - chart.setSize(newWidth, newHeight, animation); - } else if (pick(redraw, true)) { - chart.redraw(animation); - } - }, - - /** - * Shortcut to set the subtitle options. This can also be done from {@link - * Chart#update} or {@link Chart#setTitle}. - * - * @param {SubtitleOptions} options - * New subtitle options. The subtitle text itself is set by the - * `options.text` property. - */ - setSubtitle: function (options) { - this.setTitle(undefined, options); - } - - + addAxis: function (options, isX, redraw, animation) { + var key = isX ? 'xAxis' : 'yAxis', + chartOptions = this.options, + userOptions = merge(options, { + index: this[key].length, + isX: isX + }), + axis; + + axis = new Axis(this, userOptions); + + // Push the new axis options to the chart options + chartOptions[key] = splat(chartOptions[key] || {}); + chartOptions[key].push(userOptions); + + if (pick(redraw, true)) { + this.redraw(animation); + } + + return axis; + }, + + /** + * Dim the chart and show a loading text or symbol. Options for the loading + * screen are defined in {@link + * https://api.highcharts.com/highcharts/loading|the loading options}. + * + * @param {String} str + * An optional text to show in the loading label instead of the + * default one. The default text is set in {@link + * http://api.highcharts.com/highcharts/lang.loading|lang.loading}. + * + * @sample highcharts/members/chart-hideloading/ + * Show and hide loading from a button + * @sample highcharts/members/chart-showloading/ + * Apply different text labels + * @sample stock/members/chart-show-hide-loading/ + * Toggle loading in Highstock + */ + showLoading: function (str) { + var chart = this, + options = chart.options, + loadingDiv = chart.loadingDiv, + loadingOptions = options.loading, + setLoadingSize = function () { + if (loadingDiv) { + css(loadingDiv, { + left: chart.plotLeft + 'px', + top: chart.plotTop + 'px', + width: chart.plotWidth + 'px', + height: chart.plotHeight + 'px' + }); + } + }; + + // create the layer at the first call + if (!loadingDiv) { + chart.loadingDiv = loadingDiv = createElement('div', { + className: 'highcharts-loading highcharts-loading-hidden' + }, null, chart.container); + + chart.loadingSpan = createElement( + 'span', + { className: 'highcharts-loading-inner' }, + null, + loadingDiv + ); + addEvent(chart, 'redraw', setLoadingSize); // #1080 + } + + loadingDiv.className = 'highcharts-loading'; + + // Update text + chart.loadingSpan.innerHTML = str || options.lang.loading; + + /*= if (build.classic) { =*/ + // Update visuals + css(loadingDiv, extend(loadingOptions.style, { + zIndex: 10 + })); + css(chart.loadingSpan, loadingOptions.labelStyle); + + // Show it + if (!chart.loadingShown) { + css(loadingDiv, { + opacity: 0, + display: '' + }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity || 0.5 + }, { + duration: loadingOptions.showDuration || 0 + }); + } + /*= } =*/ + + chart.loadingShown = true; + setLoadingSize(); + }, + + /** + * Hide the loading layer. + * + * @see Highcharts.Chart#showLoading + * @sample highcharts/members/chart-hideloading/ + * Show and hide loading from a button + * @sample stock/members/chart-show-hide-loading/ + * Toggle loading in Highstock + */ + hideLoading: function () { + var options = this.options, + loadingDiv = this.loadingDiv; + + if (loadingDiv) { + loadingDiv.className = + 'highcharts-loading highcharts-loading-hidden'; + /*= if (build.classic) { =*/ + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration || 100, + complete: function () { + css(loadingDiv, { display: 'none' }); + } + }); + /*= } =*/ + } + this.loadingShown = false; + }, + + /** + * These properties cause isDirtyBox to be set to true when updating. Can be + * extended from plugins. + */ + propsRequireDirtyBox: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'margin', + 'marginTop', + 'marginRight', + 'marginBottom', + 'marginLeft', + 'spacing', + 'spacingTop', + 'spacingRight', + 'spacingBottom', + 'spacingLeft', + 'borderRadius', + 'plotBackgroundColor', + 'plotBackgroundImage', + 'plotBorderColor', + 'plotBorderWidth', + 'plotShadow', + 'shadow' + ], + + /** + * These properties cause all series to be updated when updating. Can be + * extended from plugins. + */ + propsRequireUpdateSeries: [ + 'chart.inverted', + 'chart.polar', + 'chart.ignoreHiddenSeries', + 'chart.type', + 'colors', + 'plotOptions', + 'time', + 'tooltip' + ], + + /** + * A generic function to update any element of the chart. Elements can be + * enabled and disabled, moved, re-styled, re-formatted etc. + * + * A special case is configuration objects that take arrays, for example + * {@link https://api.highcharts.com/highcharts/xAxis|xAxis}, + * {@link https://api.highcharts.com/highcharts/yAxis|yAxis} or + * {@link https://api.highcharts.com/highcharts/series|series}. For these + * collections, an `id` option is used to map the new option set to an + * existing object. If an existing object of the same id is not found, the + * corresponding item is updated. So for example, running `chart.update` + * with a series item without an id, will cause the existing chart's series + * with the same index in the series array to be updated. When the + * `oneToOne` parameter is true, `chart.update` will also take care of + * adding and removing items from the collection. Read more under the + * parameter description below. + * + * See also the {@link https://api.highcharts.com/highcharts/responsive| + * responsive option set}. Switching between `responsive.rules` basically + * runs `chart.update` under the hood. + * + * @param {Options} options + * A configuration object for the new chart options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart. + * @param {Boolean} [oneToOne=false] + * When `true`, the `series`, `xAxis` and `yAxis` collections will + * be updated one to one, and items will be either added or removed + * to match the new updated options. For example, if the chart has + * two series and we call `chart.update` with a configuration + * containing three series, one will be added. If we call + * `chart.update` with one series, one will be removed. Setting an + * empty `series` array will remove all series, but leaving out the + * `series` property will leave all series untouched. If the series + * have id's, the new series options will be matched by id, and the + * remaining ones removed. + * @param {AnimationOptions} [animation=true] + * Whether to apply animation, and optionally animation + * configuration. + * + * @sample highcharts/members/chart-update/ + * Update chart geometry + */ + update: function (options, redraw, oneToOne, animation) { + var chart = this, + adders = { + credits: 'addCredits', + title: 'setTitle', + subtitle: 'setSubtitle' + }, + optionsChart = options.chart, + updateAllAxes, + updateAllSeries, + newWidth, + newHeight, + itemsForRemoval = []; + + fireEvent(chart, 'update', { options: options }); + + // If the top-level chart option is present, some special updates are + // required + if (optionsChart) { + merge(true, chart.options.chart, optionsChart); + + // Setter function + if ('className' in optionsChart) { + chart.setClassName(optionsChart.className); + } + + if ('reflow' in optionsChart) { + chart.setReflow(optionsChart.reflow); + } + + if ('inverted' in optionsChart || 'polar' in optionsChart) { + // Parse options.chart.inverted and options.chart.polar together + // with the available series. + chart.propFromSeries(); + updateAllAxes = true; + } + + if ('alignTicks' in optionsChart) { // #6452 + updateAllAxes = true; + } + + objectEach(optionsChart, function (val, key) { + if ( + inArray('chart.' + key, chart.propsRequireUpdateSeries) !== + -1 + ) { + updateAllSeries = true; + } + // Only dirty box + if (inArray(key, chart.propsRequireDirtyBox) !== -1) { + chart.isDirtyBox = true; + } + }); + + /*= if (build.classic) { =*/ + if ('style' in optionsChart) { + chart.renderer.setStyle(optionsChart.style); + } + /*= } =*/ + } + + // Moved up, because tooltip needs updated plotOptions (#6218) + /*= if (build.classic) { =*/ + if (options.colors) { + this.options.colors = options.colors; + } + /*= } =*/ + + if (options.plotOptions) { + merge(true, this.options.plotOptions, options.plotOptions); + } + + // Some option stuctures correspond one-to-one to chart objects that + // have update methods, for example + // options.credits => chart.credits + // options.legend => chart.legend + // options.title => chart.title + // options.tooltip => chart.tooltip + // options.subtitle => chart.subtitle + // options.mapNavigation => chart.mapNavigation + // options.navigator => chart.navigator + // options.scrollbar => chart.scrollbar + objectEach(options, function (val, key) { + if (chart[key] && typeof chart[key].update === 'function') { + chart[key].update(val, false); + + // If a one-to-one object does not exist, look for an adder function + } else if (typeof chart[adders[key]] === 'function') { + chart[adders[key]](val); + } + + if ( + key !== 'chart' && + inArray(key, chart.propsRequireUpdateSeries) !== -1 + ) { + updateAllSeries = true; + } + }); + + // Setters for collections. For axes and series, each item is referred + // by an id. If the id is not found, it defaults to the corresponding + // item in the collection, so setting one series without an id, will + // update the first series in the chart. Setting two series without + // an id will update the first and the second respectively (#6019) + // chart.update and responsive. + each([ + 'xAxis', + 'yAxis', + 'zAxis', + 'series', + 'colorAxis', + 'pane' + ], function (coll) { + if (options[coll]) { + each(splat(options[coll]), function (newOptions, i) { + var item = ( + defined(newOptions.id) && + chart.get(newOptions.id) + ) || chart[coll][i]; + if (item && item.coll === coll) { + item.update(newOptions, false); + + if (oneToOne) { + item.touched = true; + } + } + + // If oneToOne and no matching item is found, add one + if (!item && oneToOne) { + if (coll === 'series') { + chart.addSeries(newOptions, false) + .touched = true; + } else if (coll === 'xAxis' || coll === 'yAxis') { + chart.addAxis(newOptions, coll === 'xAxis', false) + .touched = true; + } + } + + }); + + // Add items for removal + if (oneToOne) { + each(chart[coll], function (item) { + if (!item.touched) { + itemsForRemoval.push(item); + } else { + delete item.touched; + } + }); + } + + + } + }); + + each(itemsForRemoval, function (item) { + item.remove(false); + }); + + if (updateAllAxes) { + each(chart.axes, function (axis) { + axis.update({}, false); + }); + } + + // Certain options require the whole series structure to be thrown away + // and rebuilt + if (updateAllSeries) { + each(chart.series, function (series) { + series.update({}, false); + }); + } + + // For loading, just update the options, do not redraw + if (options.loading) { + merge(true, chart.options.loading, options.loading); + } + + // Update size. Redraw is forced. + newWidth = optionsChart && optionsChart.width; + newHeight = optionsChart && optionsChart.height; + if ((isNumber(newWidth) && newWidth !== chart.chartWidth) || + (isNumber(newHeight) && newHeight !== chart.chartHeight)) { + chart.setSize(newWidth, newHeight, animation); + } else if (pick(redraw, true)) { + chart.redraw(animation); + } + }, + + /** + * Shortcut to set the subtitle options. This can also be done from {@link + * Chart#update} or {@link Chart#setTitle}. + * + * @param {SubtitleOptions} options + * New subtitle options. The subtitle text itself is set by the + * `options.text` property. + */ + setSubtitle: function (options) { + this.setTitle(undefined, options); + } + + }); // extend the Point prototype for dynamic methods extend(Point.prototype, /** @lends Highcharts.Point.prototype */ { - /** - * Update point with new options (typically x/y data) and optionally redraw - * the series. - * - * @param {Object} options - * The point options. Point options are handled as described under - * the `series.type.data` item for each series type. For example - * for a line series, if options is a single number, the point will - * be given that number as the main y value. If it is an array, it - * will be interpreted as x and y values respectively. If it is an - * object, advanced options are applied. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after the point is updated. If doing - * more operations on the chart, it is best practice to set - * `redraw` to false and call `chart.redraw()` after. - * @param {AnimationOptions} [animation=true] - * Whether to apply animation, and optionally animation - * configuration. - * - * @sample highcharts/members/point-update-column/ - * Update column value - * @sample highcharts/members/point-update-pie/ - * Update pie slice - * @sample maps/members/point-update/ - * Update map area value in Highmaps - */ - update: function (options, redraw, animation, runEvent) { - var point = this, - series = point.series, - graphic = point.graphic, - i, - chart = series.chart, - seriesOptions = series.options; - - redraw = pick(redraw, true); - - function update() { - - point.applyOptions(options); - - // Update visuals - if (point.y === null && graphic) { // #4146 - point.graphic = graphic.destroy(); - } - if (isObject(options, true)) { - // Destroy so we can get new elements - if (graphic && graphic.element) { - // "null" is also a valid symbol - if ( - options && - options.marker && - options.marker.symbol !== undefined - ) { - point.graphic = graphic.destroy(); - } - } - if (options && options.dataLabels && point.dataLabel) { // #2468 - point.dataLabel = point.dataLabel.destroy(); - } - if (point.connector) { - point.connector = point.connector.destroy(); // #7243 - } - } - - // record changes in the parallel arrays - i = point.index; - series.updateParallelArrays(point, i); - - // Record the options to options.data. If the old or the new config - // is an object, use point options, otherwise use raw options - // (#4701, #4916). - seriesOptions.data[i] = ( - isObject(seriesOptions.data[i], true) || - isObject(options, true) - ) ? - point.options : - pick(options, seriesOptions.data[i]); - - // redraw - series.isDirty = series.isDirtyData = true; - if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 - chart.isDirtyBox = true; - } - - if (seriesOptions.legendType === 'point') { // #1831, #1885 - chart.isDirtyLegend = true; - } - if (redraw) { - chart.redraw(animation); - } - } - - // Fire the event with a default handler of doing the update - if (runEvent === false) { // When called from setData - update(); - } else { - point.firePointEvent('update', { options: options }, update); - } - }, - - /** - * Remove a point and optionally redraw the series and if necessary the axes - * @param {Boolean} redraw - * Whether to redraw the chart or wait for an explicit call. When - * doing more operations on the chart, for example running - * `point.remove()` in a loop, it is best practice to set `redraw` - * to false and call `chart.redraw()` after. - * @param {AnimationOptions} [animation=false] - * Whether to apply animation, and optionally animation - * configuration. - * - * @sample highcharts/plotoptions/series-point-events-remove/ - * Remove point and confirm - * @sample highcharts/members/point-remove/ - * Remove pie slice - * @sample maps/members/point-remove/ - * Remove selected points in Highmaps - */ - remove: function (redraw, animation) { - this.series.removePoint( - inArray(this, this.series.data), - redraw, - animation - ); - } + /** + * Update point with new options (typically x/y data) and optionally redraw + * the series. + * + * @param {Object} options + * The point options. Point options are handled as described under + * the `series.type.data` item for each series type. For example + * for a line series, if options is a single number, the point will + * be given that number as the main y value. If it is an array, it + * will be interpreted as x and y values respectively. If it is an + * object, advanced options are applied. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the point is updated. If doing + * more operations on the chart, it is best practice to set + * `redraw` to false and call `chart.redraw()` after. + * @param {AnimationOptions} [animation=true] + * Whether to apply animation, and optionally animation + * configuration. + * + * @sample highcharts/members/point-update-column/ + * Update column value + * @sample highcharts/members/point-update-pie/ + * Update pie slice + * @sample maps/members/point-update/ + * Update map area value in Highmaps + */ + update: function (options, redraw, animation, runEvent) { + var point = this, + series = point.series, + graphic = point.graphic, + i, + chart = series.chart, + seriesOptions = series.options; + + redraw = pick(redraw, true); + + function update() { + + point.applyOptions(options); + + // Update visuals + if (point.y === null && graphic) { // #4146 + point.graphic = graphic.destroy(); + } + if (isObject(options, true)) { + // Destroy so we can get new elements + if (graphic && graphic.element) { + // "null" is also a valid symbol + if ( + options && + options.marker && + options.marker.symbol !== undefined + ) { + point.graphic = graphic.destroy(); + } + } + if (options && options.dataLabels && point.dataLabel) { // #2468 + point.dataLabel = point.dataLabel.destroy(); + } + if (point.connector) { + point.connector = point.connector.destroy(); // #7243 + } + } + + // record changes in the parallel arrays + i = point.index; + series.updateParallelArrays(point, i); + + // Record the options to options.data. If the old or the new config + // is an object, use point options, otherwise use raw options + // (#4701, #4916). + seriesOptions.data[i] = ( + isObject(seriesOptions.data[i], true) || + isObject(options, true) + ) ? + point.options : + pick(options, seriesOptions.data[i]); + + // redraw + series.isDirty = series.isDirtyData = true; + if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 + chart.isDirtyBox = true; + } + + if (seriesOptions.legendType === 'point') { // #1831, #1885 + chart.isDirtyLegend = true; + } + if (redraw) { + chart.redraw(animation); + } + } + + // Fire the event with a default handler of doing the update + if (runEvent === false) { // When called from setData + update(); + } else { + point.firePointEvent('update', { options: options }, update); + } + }, + + /** + * Remove a point and optionally redraw the series and if necessary the axes + * @param {Boolean} redraw + * Whether to redraw the chart or wait for an explicit call. When + * doing more operations on the chart, for example running + * `point.remove()` in a loop, it is best practice to set `redraw` + * to false and call `chart.redraw()` after. + * @param {AnimationOptions} [animation=false] + * Whether to apply animation, and optionally animation + * configuration. + * + * @sample highcharts/plotoptions/series-point-events-remove/ + * Remove point and confirm + * @sample highcharts/members/point-remove/ + * Remove pie slice + * @sample maps/members/point-remove/ + * Remove selected points in Highmaps + */ + remove: function (redraw, animation) { + this.series.removePoint( + inArray(this, this.series.data), + redraw, + animation + ); + } }); // Extend the series prototype for dynamic methods extend(Series.prototype, /** @lends Series.prototype */ { - /** - * Add a point to the series after render time. The point can be added at - * the end, or by giving it an X value, to the start or in the middle of the - * series. - * - * @param {Number|Array|Object} options - * The point options. If options is a single number, a point with - * that y value is appended to the series.If it is an array, it will - * be interpreted as x and y values respectively. If it is an - * object, advanced options as outlined under `series.data` are - * applied. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after the point is added. When adding - * more than one point, it is highly recommended that the redraw - * option be set to false, and instead {@link Chart#redraw} - * is explicitly called after the adding of points is finished. - * Otherwise, the chart will redraw after adding each point. - * @param {Boolean} [shift=false] - * If true, a point is shifted off the start of the series as one is - * appended to the end. - * @param {AnimationOptions} [animation] - * Whether to apply animation, and optionally animation - * configuration. - * - * @sample highcharts/members/series-addpoint-append/ - * Append point - * @sample highcharts/members/series-addpoint-append-and-shift/ - * Append and shift - * @sample highcharts/members/series-addpoint-x-and-y/ - * Both X and Y values given - * @sample highcharts/members/series-addpoint-pie/ - * Append pie slice - * @sample stock/members/series-addpoint/ - * Append 100 points in Highstock - * @sample stock/members/series-addpoint-shift/ - * Append and shift in Highstock - * @sample maps/members/series-addpoint/ - * Add a point in Highmaps - */ - addPoint: function (options, redraw, shift, animation) { - var series = this, - seriesOptions = series.options, - data = series.data, - chart = series.chart, - xAxis = series.xAxis, - names = xAxis && xAxis.hasNames && xAxis.names, - dataOptions = seriesOptions.data, - point, - isInTheMiddle, - xData = series.xData, - i, - x; - - // Optional redraw, defaults to true - redraw = pick(redraw, true); - - // Get options and push the point to xData, yData and series.options. In - // series.generatePoints the Point instance will be created on demand - // and pushed to the series.data array. - point = { series: series }; - series.pointClass.prototype.applyOptions.apply(point, [options]); - x = point.x; - - // Get the insertion point - i = xData.length; - if (series.requireSorting && x < xData[i - 1]) { - isInTheMiddle = true; - while (i && xData[i - 1] > x) { - i--; - } - } - - // Insert undefined item - series.updateParallelArrays(point, 'splice', i, 0, 0); - // Update it - series.updateParallelArrays(point, i); - - if (names && point.name) { - names[x] = point.name; - } - dataOptions.splice(i, 0, options); - - if (isInTheMiddle) { - series.data.splice(i, 0, null); - series.processData(); - } - - // Generate points to be added to the legend (#1329) - if (seriesOptions.legendType === 'point') { - series.generatePoints(); - } - - // Shift the first point off the parallel arrays - if (shift) { - if (data[0] && data[0].remove) { - data[0].remove(false); - } else { - data.shift(); - series.updateParallelArrays(point, 'shift'); - - dataOptions.shift(); - } - } - - // redraw - series.isDirty = true; - series.isDirtyData = true; - - if (redraw) { - chart.redraw(animation); // Animation is set anyway on redraw, #5665 - } - }, - - /** - * Remove a point from the series. Unlike the - * {@link Highcharts.Point#remove} method, this can also be done on a point - * that is not instanciated because it is outside the view or subject to - * Highstock data grouping. - * - * @param {Number} i - * The index of the point in the {@link Highcharts.Series.data|data} - * array. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after the point is added. When - * removing more than one point, it is highly recommended that the - * `redraw` option be set to `false`, and instead {@link - * Highcharts.Chart#redraw} is explicitly called after the adding of - * points is finished. - * @param {AnimationOptions} [animation] - * Whether and optionally how the series should be animated. - * - * @sample highcharts/members/series-removepoint/ - * Remove cropped point - */ - removePoint: function (i, redraw, animation) { - - var series = this, - data = series.data, - point = data[i], - points = series.points, - chart = series.chart, - remove = function () { - - if (points && points.length === data.length) { // #4935 - points.splice(i, 1); - } - data.splice(i, 1); - series.options.data.splice(i, 1); - series.updateParallelArrays( - point || { series: series }, - 'splice', - i, - 1 - ); - - if (point) { - point.destroy(); - } - - // redraw - series.isDirty = true; - series.isDirtyData = true; - if (redraw) { - chart.redraw(); - } - }; - - setAnimation(animation, chart); - redraw = pick(redraw, true); - - // Fire the event with a default handler of removing the point - if (point) { - point.firePointEvent('remove', null, remove); - } else { - remove(); - } - }, - - /** - * Remove a series and optionally redraw the chart. - * - * @param {Boolean} [redraw=true] - * Whether to redraw the chart or wait for an explicit call to - * {@link Highcharts.Chart#redraw}. - * @param {AnimationOptions} [animation] - * Whether to apply animation, and optionally animation - * configuration - * @param {Boolean} [withEvent=true] - * Used internally, whether to fire the series `remove` event. - * - * @sample highcharts/members/series-remove/ - * Remove first series from a button - */ - remove: function (redraw, animation, withEvent) { - var series = this, - chart = series.chart; - - function remove() { - - // Destroy elements - series.destroy(); - - // Redraw - chart.isDirtyLegend = chart.isDirtyBox = true; - chart.linkSeries(); - - if (pick(redraw, true)) { - chart.redraw(animation); - } - } - - // Fire the event with a default handler of removing the point - if (withEvent !== false) { - fireEvent(series, 'remove', null, remove); - } else { - remove(); - } - }, - - /** - * Update the series with a new set of options. For a clean and precise - * handling of new options, all methods and elements from the series are - * removed, and it is initiated from scratch. Therefore, this method is more - * performance expensive than some other utility methods like {@link - * Series#setData} or {@link Series#setVisible}. - * - * @param {SeriesOptions} options - * New options that will be merged with the series' existing - * options. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after the series is altered. If doing - * more operations on the chart, it is a good idea to set redraw to - * false and call {@link Chart#redraw} after. - * - * @sample highcharts/members/series-update/ - * Updating series options - * @sample maps/members/series-update/ - * Update series options in Highmaps - */ - update: function (newOptions, redraw) { - var series = this, - chart = series.chart, - // must use user options when changing type because series.options - // is merged in with type specific plotOptions - oldOptions = series.userOptions, - oldType = series.oldType || series.type, - newType = ( - newOptions.type || - oldOptions.type || - chart.options.chart.type - ), - proto = seriesTypes[oldType].prototype, - n, - groups = [ - 'group', - 'markerGroup', - 'dataLabelsGroup' - ], - preserve = [ - 'navigatorSeries', - 'baseSeries' - ], - - // Animation must be enabled when calling update before the initial - // animation has first run. This happens when calling update - // directly after chart initialization, or when applying responsive - // rules (#6912). - animation = series.finishedAnimating && { animation: false }, - allowSoftUpdate = [ - 'data', - 'name', - 'turboThreshold' - ], - keys = H.keys(newOptions), - doSoftUpdate = keys.length > 0; - - // Running Series.update to update the data only is an intuitive usage, - // so we want to make sure that when used like this, we run the - // cheaper setData function and allow animation instead of completely - // recreating the series instance. This includes sideways animation when - // adding points to the data set. The `name` should also support soft - // update because the data module sets name and data when setting new - // data by `chart.update`. - each(keys, function (key) { - if (inArray(key, allowSoftUpdate) === -1) { - doSoftUpdate = false; - } - }); - if (doSoftUpdate) { - if (newOptions.data) { - this.setData(newOptions.data, false); - } - if (newOptions.name) { - this.setName(newOptions.name, false); - } - } else { - - // Make sure preserved properties are not destroyed (#3094) - preserve = groups.concat(preserve); - each(preserve, function (prop) { - preserve[prop] = series[prop]; - delete series[prop]; - }); - - // Do the merge, with some forced options - newOptions = merge(oldOptions, animation, { - index: series.index, - pointStart: pick( - oldOptions.pointStart, // when updating from blank (#7933) - series.xData[0] // when updating after addPoint - ) - }, { data: series.options.data }, newOptions); - - // Destroy the series and delete all properties. Reinsert all - // methods and properties from the new type prototype (#2270, - // #3719). - series.remove(false, null, false); - for (n in proto) { - series[n] = undefined; - } - extend(series, seriesTypes[newType || oldType].prototype); - - // Re-register groups (#3094) and other preserved properties - each(preserve, function (prop) { - series[prop] = preserve[prop]; - }); - - series.init(chart, newOptions); - - // Update the Z index of groups (#3380, #7397) - if (newOptions.zIndex !== oldOptions.zIndex) { - each(groups, function (groupName) { - if (series[groupName]) { - series[groupName].attr({ - zIndex: newOptions.zIndex - }); - } - }); - } - - - series.oldType = oldType; - chart.linkSeries(); // Links are lost in series.remove (#3028) - - } - fireEvent(this, 'afterUpdate'); - - if (pick(redraw, true)) { - chart.redraw(false); - } - }, - - /** - * Used from within series.update - * @private - */ - setName: function (name) { - this.name = this.options.name = this.userOptions.name = name; - this.chart.isDirtyLegend = true; - } + /** + * Add a point to the series after render time. The point can be added at + * the end, or by giving it an X value, to the start or in the middle of the + * series. + * + * @param {Number|Array|Object} options + * The point options. If options is a single number, a point with + * that y value is appended to the series.If it is an array, it will + * be interpreted as x and y values respectively. If it is an + * object, advanced options as outlined under `series.data` are + * applied. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the point is added. When adding + * more than one point, it is highly recommended that the redraw + * option be set to false, and instead {@link Chart#redraw} + * is explicitly called after the adding of points is finished. + * Otherwise, the chart will redraw after adding each point. + * @param {Boolean} [shift=false] + * If true, a point is shifted off the start of the series as one is + * appended to the end. + * @param {AnimationOptions} [animation] + * Whether to apply animation, and optionally animation + * configuration. + * + * @sample highcharts/members/series-addpoint-append/ + * Append point + * @sample highcharts/members/series-addpoint-append-and-shift/ + * Append and shift + * @sample highcharts/members/series-addpoint-x-and-y/ + * Both X and Y values given + * @sample highcharts/members/series-addpoint-pie/ + * Append pie slice + * @sample stock/members/series-addpoint/ + * Append 100 points in Highstock + * @sample stock/members/series-addpoint-shift/ + * Append and shift in Highstock + * @sample maps/members/series-addpoint/ + * Add a point in Highmaps + */ + addPoint: function (options, redraw, shift, animation) { + var series = this, + seriesOptions = series.options, + data = series.data, + chart = series.chart, + xAxis = series.xAxis, + names = xAxis && xAxis.hasNames && xAxis.names, + dataOptions = seriesOptions.data, + point, + isInTheMiddle, + xData = series.xData, + i, + x; + + // Optional redraw, defaults to true + redraw = pick(redraw, true); + + // Get options and push the point to xData, yData and series.options. In + // series.generatePoints the Point instance will be created on demand + // and pushed to the series.data array. + point = { series: series }; + series.pointClass.prototype.applyOptions.apply(point, [options]); + x = point.x; + + // Get the insertion point + i = xData.length; + if (series.requireSorting && x < xData[i - 1]) { + isInTheMiddle = true; + while (i && xData[i - 1] > x) { + i--; + } + } + + // Insert undefined item + series.updateParallelArrays(point, 'splice', i, 0, 0); + // Update it + series.updateParallelArrays(point, i); + + if (names && point.name) { + names[x] = point.name; + } + dataOptions.splice(i, 0, options); + + if (isInTheMiddle) { + series.data.splice(i, 0, null); + series.processData(); + } + + // Generate points to be added to the legend (#1329) + if (seriesOptions.legendType === 'point') { + series.generatePoints(); + } + + // Shift the first point off the parallel arrays + if (shift) { + if (data[0] && data[0].remove) { + data[0].remove(false); + } else { + data.shift(); + series.updateParallelArrays(point, 'shift'); + + dataOptions.shift(); + } + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + + if (redraw) { + chart.redraw(animation); // Animation is set anyway on redraw, #5665 + } + }, + + /** + * Remove a point from the series. Unlike the + * {@link Highcharts.Point#remove} method, this can also be done on a point + * that is not instanciated because it is outside the view or subject to + * Highstock data grouping. + * + * @param {Number} i + * The index of the point in the {@link Highcharts.Series.data|data} + * array. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the point is added. When + * removing more than one point, it is highly recommended that the + * `redraw` option be set to `false`, and instead {@link + * Highcharts.Chart#redraw} is explicitly called after the adding of + * points is finished. + * @param {AnimationOptions} [animation] + * Whether and optionally how the series should be animated. + * + * @sample highcharts/members/series-removepoint/ + * Remove cropped point + */ + removePoint: function (i, redraw, animation) { + + var series = this, + data = series.data, + point = data[i], + points = series.points, + chart = series.chart, + remove = function () { + + if (points && points.length === data.length) { // #4935 + points.splice(i, 1); + } + data.splice(i, 1); + series.options.data.splice(i, 1); + series.updateParallelArrays( + point || { series: series }, + 'splice', + i, + 1 + ); + + if (point) { + point.destroy(); + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + chart.redraw(); + } + }; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // Fire the event with a default handler of removing the point + if (point) { + point.firePointEvent('remove', null, remove); + } else { + remove(); + } + }, + + /** + * Remove a series and optionally redraw the chart. + * + * @param {Boolean} [redraw=true] + * Whether to redraw the chart or wait for an explicit call to + * {@link Highcharts.Chart#redraw}. + * @param {AnimationOptions} [animation] + * Whether to apply animation, and optionally animation + * configuration + * @param {Boolean} [withEvent=true] + * Used internally, whether to fire the series `remove` event. + * + * @sample highcharts/members/series-remove/ + * Remove first series from a button + */ + remove: function (redraw, animation, withEvent) { + var series = this, + chart = series.chart; + + function remove() { + + // Destroy elements + series.destroy(); + + // Redraw + chart.isDirtyLegend = chart.isDirtyBox = true; + chart.linkSeries(); + + if (pick(redraw, true)) { + chart.redraw(animation); + } + } + + // Fire the event with a default handler of removing the point + if (withEvent !== false) { + fireEvent(series, 'remove', null, remove); + } else { + remove(); + } + }, + + /** + * Update the series with a new set of options. For a clean and precise + * handling of new options, all methods and elements from the series are + * removed, and it is initiated from scratch. Therefore, this method is more + * performance expensive than some other utility methods like {@link + * Series#setData} or {@link Series#setVisible}. + * + * @param {SeriesOptions} options + * New options that will be merged with the series' existing + * options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the series is altered. If doing + * more operations on the chart, it is a good idea to set redraw to + * false and call {@link Chart#redraw} after. + * + * @sample highcharts/members/series-update/ + * Updating series options + * @sample maps/members/series-update/ + * Update series options in Highmaps + */ + update: function (newOptions, redraw) { + var series = this, + chart = series.chart, + // must use user options when changing type because series.options + // is merged in with type specific plotOptions + oldOptions = series.userOptions, + oldType = series.oldType || series.type, + newType = ( + newOptions.type || + oldOptions.type || + chart.options.chart.type + ), + proto = seriesTypes[oldType].prototype, + n, + groups = [ + 'group', + 'markerGroup', + 'dataLabelsGroup' + ], + preserve = [ + 'navigatorSeries', + 'baseSeries' + ], + + // Animation must be enabled when calling update before the initial + // animation has first run. This happens when calling update + // directly after chart initialization, or when applying responsive + // rules (#6912). + animation = series.finishedAnimating && { animation: false }, + allowSoftUpdate = [ + 'data', + 'name', + 'turboThreshold' + ], + keys = H.keys(newOptions), + doSoftUpdate = keys.length > 0; + + // Running Series.update to update the data only is an intuitive usage, + // so we want to make sure that when used like this, we run the + // cheaper setData function and allow animation instead of completely + // recreating the series instance. This includes sideways animation when + // adding points to the data set. The `name` should also support soft + // update because the data module sets name and data when setting new + // data by `chart.update`. + each(keys, function (key) { + if (inArray(key, allowSoftUpdate) === -1) { + doSoftUpdate = false; + } + }); + if (doSoftUpdate) { + if (newOptions.data) { + this.setData(newOptions.data, false); + } + if (newOptions.name) { + this.setName(newOptions.name, false); + } + } else { + + // Make sure preserved properties are not destroyed (#3094) + preserve = groups.concat(preserve); + each(preserve, function (prop) { + preserve[prop] = series[prop]; + delete series[prop]; + }); + + // Do the merge, with some forced options + newOptions = merge(oldOptions, animation, { + index: series.index, + pointStart: pick( + oldOptions.pointStart, // when updating from blank (#7933) + series.xData[0] // when updating after addPoint + ) + }, { data: series.options.data }, newOptions); + + // Destroy the series and delete all properties. Reinsert all + // methods and properties from the new type prototype (#2270, + // #3719). + series.remove(false, null, false); + for (n in proto) { + series[n] = undefined; + } + extend(series, seriesTypes[newType || oldType].prototype); + + // Re-register groups (#3094) and other preserved properties + each(preserve, function (prop) { + series[prop] = preserve[prop]; + }); + + series.init(chart, newOptions); + + // Update the Z index of groups (#3380, #7397) + if (newOptions.zIndex !== oldOptions.zIndex) { + each(groups, function (groupName) { + if (series[groupName]) { + series[groupName].attr({ + zIndex: newOptions.zIndex + }); + } + }); + } + + + series.oldType = oldType; + chart.linkSeries(); // Links are lost in series.remove (#3028) + + } + fireEvent(this, 'afterUpdate'); + + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + + /** + * Used from within series.update + * @private + */ + setName: function (name) { + this.name = this.options.name = this.userOptions.name = name; + this.chart.isDirtyLegend = true; + } }); // Extend the Axis.prototype for dynamic methods extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ { - /** - * Update an axis object with a new set of options. The options are merged - * with the existing options, so only new or altered options need to be - * specified. - * - * @param {Object} options - * The new options that will be merged in with existing options on - * the axis. - * @sample highcharts/members/axis-update/ Axis update demo - */ - update: function (options, redraw) { - var chart = this.chart; - - options = chart.options[this.coll][this.options.index] = - merge(this.userOptions, options); - - this.destroy(true); - - this.init(chart, extend(options, { events: undefined })); - - chart.isDirtyBox = true; - if (pick(redraw, true)) { - chart.redraw(); - } - }, - - /** + /** + * Update an axis object with a new set of options. The options are merged + * with the existing options, so only new or altered options need to be + * specified. + * + * @param {Object} options + * The new options that will be merged in with existing options on + * the axis. + * @sample highcharts/members/axis-update/ Axis update demo + */ + update: function (options, redraw) { + var chart = this.chart; + + options = chart.options[this.coll][this.options.index] = + merge(this.userOptions, options); + + this.destroy(true); + + this.init(chart, extend(options, { events: undefined })); + + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** * Remove the axis from the chart. * * @param {Boolean} [redraw=true] Whether to redraw the chart following the @@ -1053,62 +1053,62 @@ extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ { * * @sample highcharts/members/chart-addaxis/ Add and remove axes */ - remove: function (redraw) { - var chart = this.chart, - key = this.coll, // xAxis or yAxis - axisSeries = this.series, - i = axisSeries.length; - - // Remove associated series (#2687) - while (i--) { - if (axisSeries[i]) { - axisSeries[i].remove(false); - } - } - - // Remove the axis - erase(chart.axes, this); - erase(chart[key], this); - - if (isArray(chart.options[key])) { - chart.options[key].splice(this.options.index, 1); - } else { // color axis, #6488 - delete chart.options[key]; - } - - each(chart[key], function (axis, i) { // Re-index, #1706 - axis.options.index = i; - }); - this.destroy(); - chart.isDirtyBox = true; - - if (pick(redraw, true)) { - chart.redraw(); - } - }, - - /** - * Update the axis title by options after render time. - * - * @param {TitleOptions} titleOptions - * The additional title options. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after setting the title. - * @sample highcharts/members/axis-settitle/ Set a new Y axis title - */ - setTitle: function (titleOptions, redraw) { - this.update({ title: titleOptions }, redraw); - }, - - /** - * Set new axis categories and optionally redraw. - * @param {Array.} categories - The new categories. - * @param {Boolean} [redraw=true] - Whether to redraw the chart. - * @sample highcharts/members/axis-setcategories/ Set categories by click on - * a button - */ - setCategories: function (categories, redraw) { - this.update({ categories: categories }, redraw); - } + remove: function (redraw) { + var chart = this.chart, + key = this.coll, // xAxis or yAxis + axisSeries = this.series, + i = axisSeries.length; + + // Remove associated series (#2687) + while (i--) { + if (axisSeries[i]) { + axisSeries[i].remove(false); + } + } + + // Remove the axis + erase(chart.axes, this); + erase(chart[key], this); + + if (isArray(chart.options[key])) { + chart.options[key].splice(this.options.index, 1); + } else { // color axis, #6488 + delete chart.options[key]; + } + + each(chart[key], function (axis, i) { // Re-index, #1706 + axis.options.index = i; + }); + this.destroy(); + chart.isDirtyBox = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Update the axis title by options after render time. + * + * @param {TitleOptions} titleOptions + * The additional title options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after setting the title. + * @sample highcharts/members/axis-settitle/ Set a new Y axis title + */ + setTitle: function (titleOptions, redraw) { + this.update({ title: titleOptions }, redraw); + }, + + /** + * Set new axis categories and optionally redraw. + * @param {Array.} categories - The new categories. + * @param {Boolean} [redraw=true] - Whether to redraw the chart. + * @sample highcharts/members/axis-setcategories/ Set categories by click on + * a button + */ + setCategories: function (categories, redraw) { + this.update({ categories: categories }, redraw); + } }); diff --git a/js/parts/FlagsSeries.js b/js/parts/FlagsSeries.js index 5dd1bdfbd5e..058701f74c4 100644 --- a/js/parts/FlagsSeries.js +++ b/js/parts/FlagsSeries.js @@ -11,16 +11,16 @@ import './Series.js'; import './SvgRenderer.js'; import onSeriesMixin from '../mixins/on-series.js'; var addEvent = H.addEvent, - each = H.each, - merge = H.merge, - noop = H.noop, - Renderer = H.Renderer, - Series = H.Series, - seriesType = H.seriesType, - SVGRenderer = H.SVGRenderer, - TrackerMixin = H.TrackerMixin, - VMLRenderer = H.VMLRenderer, - symbols = SVGRenderer.prototype.symbols; + each = H.each, + merge = H.merge, + noop = H.noop, + Renderer = H.Renderer, + Series = H.Series, + seriesType = H.seriesType, + SVGRenderer = H.SVGRenderer, + TrackerMixin = H.TrackerMixin, + VMLRenderer = H.VMLRenderer, + symbols = SVGRenderer.prototype.symbols; /** * The Flags series. @@ -41,522 +41,522 @@ var addEvent = H.addEvent, */ 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 - - /** - * Whether the flags are allowed to overlap sideways. If `false`, the flags - * are moved sideways using an algorithm that seeks to place every flag as - * close as possible to its original position. - * - * @sample {highstock} stock/plotoptions/flags-allowoverlapx - * Allow sideways overlap - * @since 6.0.4 - */ - allowOverlapX: false, - - /** - * 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. - * - * @validvalue ["flag", "circlepin", "squarepin"] - * @sample {highstock} stock/plotoptions/flags/ Different shapes - * @product highstock - */ - shape: 'flag', - - /** - * When multiple flags in the same series fall on the same value, this - * number determines the vertical offset between them. - * - * @sample {highstock} stock/plotoptions/flags-stackdistance/ - * A greater stack distance - * @product highstock - */ - stackDistance: 12, - - /** - * Text alignment for the text inside the flag. - * - * @validvalue ["left", "center", "right"] - * @since 5.0.0 - * @product highstock - */ - textAlign: 'center', - - /** - * Specific tooltip options for flag series. Flag series tooltips are - * different from most other types in that a flag doesn't have a data - * value, so the tooltip rather displays the `text` option for each - * point. - * - * @type {Object} - * @extends plotOptions.series.tooltip - * @excluding changeDecimals,valueDecimals,valuePrefix,valueSuffix - * @product highstock - */ - tooltip: { - pointFormat: '{point.text}
' - }, - - 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 - */ - - /** - * 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`. - * - * @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 - */ - - /*= if (build.classic) { =*/ - - /** - * The fill color for the flags. - * - * @type {Color} - * @default #ffffff - * @product highstock - */ - fillColor: '${palette.backgroundColor}', - - /** - * 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. - * - * @product highstock - */ - lineWidth: 1, - - states: { - - /** - * @extends plotOptions.column.states.hover - * @product highstock - */ - hover: { - - /** - * The color of the line/border of the flag. - * - * @type {Color} - * @default #000000 - * @product highstock - */ - lineColor: '${palette.neutralColor100}', - - /** - * The fill or background color of the flag. - * - * @type {Color} - * @default #ccd6eb - * @product highstock - */ - fillColor: '${palette.highlightColor20}' - } - }, - - /** - * The text styles of the 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' - } - /*= } =*/ + /** + * 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 + + /** + * Whether the flags are allowed to overlap sideways. If `false`, the flags + * are moved sideways using an algorithm that seeks to place every flag as + * close as possible to its original position. + * + * @sample {highstock} stock/plotoptions/flags-allowoverlapx + * Allow sideways overlap + * @since 6.0.4 + */ + allowOverlapX: false, + + /** + * 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. + * + * @validvalue ["flag", "circlepin", "squarepin"] + * @sample {highstock} stock/plotoptions/flags/ Different shapes + * @product highstock + */ + shape: 'flag', + + /** + * When multiple flags in the same series fall on the same value, this + * number determines the vertical offset between them. + * + * @sample {highstock} stock/plotoptions/flags-stackdistance/ + * A greater stack distance + * @product highstock + */ + stackDistance: 12, + + /** + * Text alignment for the text inside the flag. + * + * @validvalue ["left", "center", "right"] + * @since 5.0.0 + * @product highstock + */ + textAlign: 'center', + + /** + * Specific tooltip options for flag series. Flag series tooltips are + * different from most other types in that a flag doesn't have a data + * value, so the tooltip rather displays the `text` option for each + * point. + * + * @type {Object} + * @extends plotOptions.series.tooltip + * @excluding changeDecimals,valueDecimals,valuePrefix,valueSuffix + * @product highstock + */ + tooltip: { + pointFormat: '{point.text}
' + }, + + 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 + */ + + /** + * 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`. + * + * @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 + */ + + /*= if (build.classic) { =*/ + + /** + * The fill color for the flags. + * + * @type {Color} + * @default #ffffff + * @product highstock + */ + fillColor: '${palette.backgroundColor}', + + /** + * 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. + * + * @product highstock + */ + lineWidth: 1, + + states: { + + /** + * @extends plotOptions.column.states.hover + * @product highstock + */ + hover: { + + /** + * The color of the line/border of the flag. + * + * @type {Color} + * @default #000000 + * @product highstock + */ + lineColor: '${palette.neutralColor100}', + + /** + * The fill or background color of the flag. + * + * @type {Color} + * @default #ccd6eb + * @product highstock + */ + fillColor: '${palette.highlightColor20}' + } + }, + + /** + * The text styles of the 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 */ { - sorted: false, - noSharedTooltip: true, - allowDG: false, - takeOrdinalPosition: false, // #1074 - trackerGroups: ['markerGroup'], - forceCrop: true, - /** - * Inherit the initialization from base Series. - */ - init: Series.prototype.init, - - /*= if (build.classic) { =*/ - /** - * Get presentational attributes - */ - pointAttribs: function (point, state) { - var options = this.options, - color = (point && point.color) || this.color, - lineColor = options.lineColor, - lineWidth = (point && point.lineWidth), - fill = (point && point.fillColor) || options.fillColor; - - if (state) { - fill = options.states[state].fillColor; - lineColor = options.states[state].lineColor; - lineWidth = options.states[state].lineWidth; - } - - return { - 'fill': fill || color, - 'stroke': lineColor || color, - 'stroke-width': lineWidth || options.lineWidth || 0 - }; - }, - /*= } =*/ - - translate: onSeriesMixin.translate, - getPlotBox: onSeriesMixin.getPlotBox, - - /** - * Draw the markers - */ - drawPoints: function () { - var series = this, - points = series.points, - chart = series.chart, - renderer = chart.renderer, - plotX, - plotY, - inverted = chart.inverted, - options = series.options, - optionsY = options.y, - shape, - i, - point, - graphic, - stackIndex, - anchorY, - attribs, - outsideRight, - yAxis = series.yAxis, - boxesMap = {}, - boxes = []; - - i = points.length; - while (i--) { - point = points[i]; - outsideRight = (inverted ? point.plotY : point.plotX) > series.xAxis.len; - plotX = point.plotX; - stackIndex = point.stackIndex; - shape = point.options.shape || options.shape; - plotY = point.plotY; - - if (plotY !== undefined) { - plotY = point.plotY + optionsY - - ( - stackIndex !== undefined && - stackIndex * options.stackDistance - ); - } - // skip connectors for higher level stacked points - point.anchorX = stackIndex ? undefined : point.plotX; - anchorY = stackIndex ? undefined : point.plotY; - - graphic = point.graphic; - - // Only draw the point if y is defined and the flag is within - // the visible area - if (plotY !== undefined && plotX >= 0 && !outsideRight) { - - // Create the flag - if (!graphic) { - graphic = point.graphic = renderer.label( - '', - null, - null, - shape, - null, - null, - options.useHTML - ) - /*= if (build.classic) { =*/ - .attr(series.pointAttribs(point)) - .css(merge(options.style, point.style)) - /*= } =*/ - .attr({ - align: shape === 'flag' ? 'left' : 'center', - width: options.width, - height: options.height, - 'text-align': options.textAlign - }) - .addClass('highcharts-point') - .add(series.markerGroup); - - // Add reference to the point for tracker (#6303) - if (point.graphic.div) { - point.graphic.div.point = point; - } - - /*= if (build.classic) { =*/ - graphic.shadow(options.shadow); - /*= } =*/ - graphic.isNew = true; - } - - if (plotX > 0) { // #3119 - plotX -= graphic.strokeWidth() % 2; // #4285 - } - - // Plant the flag - attribs = { - y: plotY, - anchorY: anchorY - }; - if (options.allowOverlapX) { - attribs.x = plotX; - attribs.anchorX = point.anchorX; - } - graphic.attr({ - text: point.options.title || options.title || 'A' - })[graphic.isNew ? 'attr' : 'animate'](attribs); - - // Rig for the distribute function - if (!options.allowOverlapX) { - if (!boxesMap[point.plotX]) { - boxesMap[point.plotX] = { - align: 0, - size: graphic.width, - target: plotX, - anchorX: plotX - }; - } else { - boxesMap[point.plotX].size = Math.max( - boxesMap[point.plotX].size, - graphic.width - ); - } - } - - // Set the tooltip anchor position - point.tooltipPos = [ - plotX, - plotY + yAxis.pos - chart.plotTop - ]; // #6327 - - } else if (graphic) { - point.graphic = graphic.destroy(); - } - - } - - // Handle X-dimension overlapping - if (!options.allowOverlapX) { - H.objectEach(boxesMap, function (box) { - box.plotX = box.anchorX; - boxes.push(box); - }); - - H.distribute(boxes, inverted ? yAxis.len : this.xAxis.len, 100); - - each(points, function (point) { - var box = point.graphic && boxesMap[point.plotX]; - if (box) { - point.graphic[point.graphic.isNew ? 'attr' : 'animate']({ - x: box.pos, - anchorX: point.anchorX - }); - point.graphic.isNew = false; - } - }); - } - - // Might be a mix of SVG and HTML and we need events for both (#6303) - if (options.useHTML) { - H.wrap(series.markerGroup, 'on', function (proceed) { - return H.SVGElement.prototype.on.apply( - // for HTML - proceed.apply(this, [].slice.call(arguments, 1)), - // and for SVG - [].slice.call(arguments, 1)); - }); - } - - }, - - /** - * Extend the column trackers with listeners to expand and contract stacks - */ - drawTracker: function () { - var series = this, - points = series.points; - - TrackerMixin.drawTrackerPoint.apply(this); - - /** - * Bring each stacked flag up on mouse over, this allows readability - * of vertically stacked elements as well as tight points on - * the x axis. #1924. - */ - each(points, function (point) { - var graphic = point.graphic; - if (graphic) { - addEvent(graphic.element, 'mouseover', function () { - - // Raise this point - if (point.stackIndex > 0 && !point.raised) { - point._y = graphic.y; - graphic.attr({ - y: point._y - 8 - }); - point.raised = true; - } - - // Revert other raised points - each(points, function (otherPoint) { - if ( - otherPoint !== point && - otherPoint.raised && - otherPoint.graphic - ) { - otherPoint.graphic.attr({ - y: otherPoint._y - }); - otherPoint.raised = false; - } - }); - }); - } - }); - }, - - animate: noop, // Disable animation - buildKDTree: noop, - setClip: noop, - /** - * Don't invert the flag marker group (#4960) - */ - invertGroups: noop + sorted: false, + noSharedTooltip: true, + allowDG: false, + takeOrdinalPosition: false, // #1074 + trackerGroups: ['markerGroup'], + forceCrop: true, + /** + * Inherit the initialization from base Series. + */ + init: Series.prototype.init, + + /*= if (build.classic) { =*/ + /** + * Get presentational attributes + */ + pointAttribs: function (point, state) { + var options = this.options, + color = (point && point.color) || this.color, + lineColor = options.lineColor, + lineWidth = (point && point.lineWidth), + fill = (point && point.fillColor) || options.fillColor; + + if (state) { + fill = options.states[state].fillColor; + lineColor = options.states[state].lineColor; + lineWidth = options.states[state].lineWidth; + } + + return { + 'fill': fill || color, + 'stroke': lineColor || color, + 'stroke-width': lineWidth || options.lineWidth || 0 + }; + }, + /*= } =*/ + + translate: onSeriesMixin.translate, + getPlotBox: onSeriesMixin.getPlotBox, + + /** + * Draw the markers + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart, + renderer = chart.renderer, + plotX, + plotY, + inverted = chart.inverted, + options = series.options, + optionsY = options.y, + shape, + i, + point, + graphic, + stackIndex, + anchorY, + attribs, + outsideRight, + yAxis = series.yAxis, + boxesMap = {}, + boxes = []; + + i = points.length; + while (i--) { + point = points[i]; + outsideRight = (inverted ? point.plotY : point.plotX) > series.xAxis.len; + plotX = point.plotX; + stackIndex = point.stackIndex; + shape = point.options.shape || options.shape; + plotY = point.plotY; + + if (plotY !== undefined) { + plotY = point.plotY + optionsY - + ( + stackIndex !== undefined && + stackIndex * options.stackDistance + ); + } + // skip connectors for higher level stacked points + point.anchorX = stackIndex ? undefined : point.plotX; + anchorY = stackIndex ? undefined : point.plotY; + + graphic = point.graphic; + + // Only draw the point if y is defined and the flag is within + // the visible area + if (plotY !== undefined && plotX >= 0 && !outsideRight) { + + // Create the flag + if (!graphic) { + graphic = point.graphic = renderer.label( + '', + null, + null, + shape, + null, + null, + options.useHTML + ) + /*= if (build.classic) { =*/ + .attr(series.pointAttribs(point)) + .css(merge(options.style, point.style)) + /*= } =*/ + .attr({ + align: shape === 'flag' ? 'left' : 'center', + width: options.width, + height: options.height, + 'text-align': options.textAlign + }) + .addClass('highcharts-point') + .add(series.markerGroup); + + // Add reference to the point for tracker (#6303) + if (point.graphic.div) { + point.graphic.div.point = point; + } + + /*= if (build.classic) { =*/ + graphic.shadow(options.shadow); + /*= } =*/ + graphic.isNew = true; + } + + if (plotX > 0) { // #3119 + plotX -= graphic.strokeWidth() % 2; // #4285 + } + + // Plant the flag + attribs = { + y: plotY, + anchorY: anchorY + }; + if (options.allowOverlapX) { + attribs.x = plotX; + attribs.anchorX = point.anchorX; + } + graphic.attr({ + text: point.options.title || options.title || 'A' + })[graphic.isNew ? 'attr' : 'animate'](attribs); + + // Rig for the distribute function + if (!options.allowOverlapX) { + if (!boxesMap[point.plotX]) { + boxesMap[point.plotX] = { + align: 0, + size: graphic.width, + target: plotX, + anchorX: plotX + }; + } else { + boxesMap[point.plotX].size = Math.max( + boxesMap[point.plotX].size, + graphic.width + ); + } + } + + // Set the tooltip anchor position + point.tooltipPos = [ + plotX, + plotY + yAxis.pos - chart.plotTop + ]; // #6327 + + } else if (graphic) { + point.graphic = graphic.destroy(); + } + + } + + // Handle X-dimension overlapping + if (!options.allowOverlapX) { + H.objectEach(boxesMap, function (box) { + box.plotX = box.anchorX; + boxes.push(box); + }); + + H.distribute(boxes, inverted ? yAxis.len : this.xAxis.len, 100); + + each(points, function (point) { + var box = point.graphic && boxesMap[point.plotX]; + if (box) { + point.graphic[point.graphic.isNew ? 'attr' : 'animate']({ + x: box.pos, + anchorX: point.anchorX + }); + point.graphic.isNew = false; + } + }); + } + + // Might be a mix of SVG and HTML and we need events for both (#6303) + if (options.useHTML) { + H.wrap(series.markerGroup, 'on', function (proceed) { + return H.SVGElement.prototype.on.apply( + // for HTML + proceed.apply(this, [].slice.call(arguments, 1)), + // and for SVG + [].slice.call(arguments, 1)); + }); + } + + }, + + /** + * Extend the column trackers with listeners to expand and contract stacks + */ + drawTracker: function () { + var series = this, + points = series.points; + + TrackerMixin.drawTrackerPoint.apply(this); + + /** + * Bring each stacked flag up on mouse over, this allows readability + * of vertically stacked elements as well as tight points on + * the x axis. #1924. + */ + each(points, function (point) { + var graphic = point.graphic; + if (graphic) { + addEvent(graphic.element, 'mouseover', function () { + + // Raise this point + if (point.stackIndex > 0 && !point.raised) { + point._y = graphic.y; + graphic.attr({ + y: point._y - 8 + }); + point.raised = true; + } + + // Revert other raised points + each(points, function (otherPoint) { + if ( + otherPoint !== point && + otherPoint.raised && + otherPoint.graphic + ) { + otherPoint.graphic.attr({ + y: otherPoint._y + }); + otherPoint.raised = false; + } + }); + }); + } + }); + }, + + animate: noop, // Disable animation + buildKDTree: noop, + setClip: noop, + /** + * Don't invert the flag marker group (#4960) + */ + invertGroups: noop }); // create the flag icon with anchor symbols.flag = function (x, y, w, h, options) { - var anchorX = (options && options.anchorX) || x, - anchorY = (options && options.anchorY) || y; - - return symbols.circle(anchorX - 1, anchorY - 1, 2, 2).concat( - [ - 'M', anchorX, anchorY, - 'L', x, y + h, - x, y, - x + w, y, - x + w, y + h, - x, y + h, - 'Z' - ] - ); + var anchorX = (options && options.anchorX) || x, + anchorY = (options && options.anchorY) || y; + + return symbols.circle(anchorX - 1, anchorY - 1, 2, 2).concat( + [ + 'M', anchorX, anchorY, + 'L', x, y + h, + x, y, + x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ] + ); }; /* * Create the circlepin and squarepin icons with anchor */ function createPinSymbol(shape) { - symbols[shape + 'pin'] = function (x, y, w, h, options) { - - var anchorX = options && options.anchorX, - anchorY = options && options.anchorY, - path, - labelTopOrBottomY; - - // For single-letter flags, make sure circular flags are not taller - // than their width - if (shape === 'circle' && h > w) { - x -= Math.round((h - w) / 2); - w = h; - } - - path = symbols[shape](x, y, w, h); - - if (anchorX && anchorY) { - /** - * If the label is below the anchor, draw the connecting line - * from the top edge of the label - * otherwise start drawing from the bottom edge - */ - labelTopOrBottomY = (y > anchorY) ? y : y + h; - path.push( - 'M', - shape === 'circle' ? path[1] - path[4] : path[1] + path[4] / 2, - labelTopOrBottomY, - 'L', - anchorX, - anchorY - ); - path = path.concat( - symbols.circle(anchorX - 1, anchorY - 1, 2, 2) - ); - } - - return path; - }; + symbols[shape + 'pin'] = function (x, y, w, h, options) { + + var anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path, + labelTopOrBottomY; + + // For single-letter flags, make sure circular flags are not taller + // than their width + if (shape === 'circle' && h > w) { + x -= Math.round((h - w) / 2); + w = h; + } + + path = symbols[shape](x, y, w, h); + + if (anchorX && anchorY) { + /** + * If the label is below the anchor, draw the connecting line + * from the top edge of the label + * otherwise start drawing from the bottom edge + */ + labelTopOrBottomY = (y > anchorY) ? y : y + h; + path.push( + 'M', + shape === 'circle' ? path[1] - path[4] : path[1] + path[4] / 2, + labelTopOrBottomY, + 'L', + anchorX, + anchorY + ); + path = path.concat( + symbols.circle(anchorX - 1, anchorY - 1, 2, 2) + ); + } + + return path; + }; } createPinSymbol('circle'); createPinSymbol('square'); @@ -568,16 +568,16 @@ createPinSymbol('square'); * them with the VMLRenderer. */ if (Renderer === VMLRenderer) { - each(['flag', 'circlepin', 'squarepin'], function (shape) { - VMLRenderer.prototype.symbols[shape] = symbols[shape]; - }); + each(['flag', 'circlepin', 'squarepin'], function (shape) { + VMLRenderer.prototype.symbols[shape] = symbols[shape]; + }); } /*= } =*/ /** * A `flags` series. If the [type](#series.flags.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.flags * @excluding dataParser,dataURL @@ -588,12 +588,12 @@ if (Renderer === VMLRenderer) { /** * 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, @@ -604,7 +604,7 @@ if (Renderer === VMLRenderer) { * title: "B", * text: "Second event" * }] - * + * * @type {Array} * @extends series.line.data * @excluding y,dataLabels,marker,name @@ -615,7 +615,7 @@ if (Renderer === VMLRenderer) { /** * The fill color of an individual flag. By default it inherits from * the series color. - * + * * @type {Color} * @product highstock * @apioption series.flags.data.fillColor @@ -623,7 +623,7 @@ if (Renderer === VMLRenderer) { /** * The longer text to be shown in the flag's tooltip. - * + * * @type {String} * @product highstock * @apioption series.flags.data.text @@ -631,7 +631,7 @@ if (Renderer === VMLRenderer) { /** * The short text to be shown on the flag. - * + * * @type {String} * @product highstock * @apioption series.flags.data.title diff --git a/js/parts/Globals.js b/js/parts/Globals.js index 621b6851bb3..ecbd1bf0f35 100644 --- a/js/parts/Globals.js +++ b/js/parts/Globals.js @@ -8,52 +8,52 @@ // glob is a temporary fix to allow our es-modules to work. var glob = typeof win === 'undefined' ? window : win, - doc = glob.document, - SVG_NS = 'http://www.w3.org/2000/svg', - userAgent = (glob.navigator && glob.navigator.userAgent) || '', - svg = ( - doc && - doc.createElementNS && - !!doc.createElementNS(SVG_NS, 'svg').createSVGRect - ), - isMS = /(edge|msie|trident)/i.test(userAgent) && !glob.opera, - isFirefox = userAgent.indexOf('Firefox') !== -1, - isChrome = userAgent.indexOf('Chrome') !== -1, - hasBidiBug = ( - isFirefox && - parseInt(userAgent.split('Firefox/')[1], 10) < 4 // issue #38 - ); + doc = glob.document, + SVG_NS = 'http://www.w3.org/2000/svg', + userAgent = (glob.navigator && glob.navigator.userAgent) || '', + svg = ( + doc && + doc.createElementNS && + !!doc.createElementNS(SVG_NS, 'svg').createSVGRect + ), + isMS = /(edge|msie|trident)/i.test(userAgent) && !glob.opera, + isFirefox = userAgent.indexOf('Firefox') !== -1, + isChrome = userAgent.indexOf('Chrome') !== -1, + hasBidiBug = ( + isFirefox && + parseInt(userAgent.split('Firefox/')[1], 10) < 4 // issue #38 + ); var Highcharts = glob.Highcharts ? glob.Highcharts.error(16, true) : { - product: '@product.name@', - version: '@product.version@', - deg2rad: Math.PI * 2 / 360, - doc: doc, - hasBidiBug: hasBidiBug, - hasTouch: doc && doc.documentElement.ontouchstart !== undefined, - isMS: isMS, - isWebKit: userAgent.indexOf('AppleWebKit') !== -1, - isFirefox: isFirefox, - isChrome: isChrome, - isSafari: !isChrome && userAgent.indexOf('Safari') !== -1, - isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent), - SVG_NS: SVG_NS, - chartCount: 0, - seriesTypes: {}, - symbolSizes: {}, - svg: svg, - win: glob, - marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], - noop: function () { - return undefined; - }, - /** - * An array containing the current chart objects in the page. A chart's - * position in the array is preserved throughout the page's lifetime. When - * a chart is destroyed, the array item becomes `undefined`. - * @type {Array.} - * @memberOf Highcharts - */ - charts: [] + product: '@product.name@', + version: '@product.version@', + deg2rad: Math.PI * 2 / 360, + doc: doc, + hasBidiBug: hasBidiBug, + hasTouch: doc && doc.documentElement.ontouchstart !== undefined, + isMS: isMS, + isWebKit: userAgent.indexOf('AppleWebKit') !== -1, + isFirefox: isFirefox, + isChrome: isChrome, + isSafari: !isChrome && userAgent.indexOf('Safari') !== -1, + isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent), + SVG_NS: SVG_NS, + chartCount: 0, + seriesTypes: {}, + symbolSizes: {}, + svg: svg, + win: glob, + marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], + noop: function () { + return undefined; + }, + /** + * An array containing the current chart objects in the page. A chart's + * position in the array is preserved throughout the page's lifetime. When + * a chart is destroyed, the array item becomes `undefined`. + * @type {Array.} + * @memberOf Highcharts + */ + charts: [] }; export default Highcharts; diff --git a/js/parts/Html.js b/js/parts/Html.js index 2cb6409cea8..0da34b53395 100644 --- a/js/parts/Html.js +++ b/js/parts/Html.js @@ -3,456 +3,456 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from './Globals.js'; import './Utilities.js'; import './SvgRenderer.js'; var attr = H.attr, - createElement = H.createElement, - css = H.css, - defined = H.defined, - each = H.each, - extend = H.extend, - isFirefox = H.isFirefox, - isMS = H.isMS, - isWebKit = H.isWebKit, - pick = H.pick, - pInt = H.pInt, - SVGElement = H.SVGElement, - SVGRenderer = H.SVGRenderer, - win = H.win, - wrap = H.wrap; + createElement = H.createElement, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + isFirefox = H.isFirefox, + isMS = H.isMS, + isWebKit = H.isWebKit, + pick = H.pick, + pInt = H.pInt, + SVGElement = H.SVGElement, + SVGRenderer = H.SVGRenderer, + win = H.win, + wrap = H.wrap; // Extend SvgElement for useHTML option extend(SVGElement.prototype, /** @lends SVGElement.prototype */ { - /** - * Apply CSS to HTML elements. This is used in text within SVG rendering and - * by the VML renderer - */ - htmlCss: function (styles) { - var wrapper = this, - element = wrapper.element, - textWidth = styles && element.tagName === 'SPAN' && styles.width; - - if (textWidth) { - delete styles.width; - wrapper.textWidth = textWidth; - wrapper.htmlUpdateTransform(); - } - if (styles && styles.textOverflow === 'ellipsis') { - styles.whiteSpace = 'nowrap'; - styles.overflow = 'hidden'; - } - wrapper.styles = extend(wrapper.styles, styles); - css(wrapper.element, styles); - - return wrapper; - }, - - /** - * VML and useHTML method for calculating the bounding box based on offsets - * @param {Boolean} refresh Whether to force a fresh value from the DOM or - * to use the cached value. - * - * @return {Object} A hash containing values for x, y, width and height - */ - - htmlGetBBox: function () { - var wrapper = this, - element = wrapper.element; - - return { - x: element.offsetLeft, - y: element.offsetTop, - width: element.offsetWidth, - height: element.offsetHeight - }; - }, - - /** - * VML override private method to update elements based on internal - * properties based on SVG transform - */ - htmlUpdateTransform: function () { - // aligning non added elements is expensive - if (!this.added) { - this.alignOnAdd = true; - return; - } - - var wrapper = this, - renderer = wrapper.renderer, - elem = wrapper.element, - translateX = wrapper.translateX || 0, - translateY = wrapper.translateY || 0, - x = wrapper.x || 0, - y = wrapper.y || 0, - align = wrapper.textAlign || 'left', - alignCorrection = { left: 0, center: 0.5, right: 1 }[align], - styles = wrapper.styles, - whiteSpace = styles && styles.whiteSpace; - - function getTextPxLength() { - // Reset multiline/ellipsis in order to read width (#4928, - // #5417) - css(elem, { - width: '', - whiteSpace: whiteSpace || 'nowrap' - }); - return elem.offsetWidth; - } - - // apply translate - css(elem, { - marginLeft: translateX, - marginTop: translateY - }); - - /*= if (build.classic) { =*/ - if (wrapper.shadows) { // used in labels/tooltip - each(wrapper.shadows, function (shadow) { - css(shadow, { - marginLeft: translateX + 1, - marginTop: translateY + 1 - }); - }); - } - /*= } =*/ - - // apply inversion - if (wrapper.inverted) { // wrapper is a group - each(elem.childNodes, function (child) { - renderer.invertChild(child, elem); - }); - } - - if (elem.tagName === 'SPAN') { - - var rotation = wrapper.rotation, - baseline, - textWidth = wrapper.textWidth && pInt(wrapper.textWidth), - currentTextTransform = [ - rotation, - align, - elem.innerHTML, - wrapper.textWidth, - wrapper.textAlign - ].join(','); - - // Update textWidth. Use the memoized textPxLength if possible, to - // avoid the getTextPxLength function using elem.offsetWidth. - // Calling offsetWidth affects rendering time as it forces layout - // (#7656). - if ( - textWidth !== wrapper.oldTextWidth && - ( - (textWidth > wrapper.oldTextWidth) || - (wrapper.textPxLength || getTextPxLength()) > textWidth - ) && - /[ \-]/.test(elem.textContent || elem.innerText) - ) { // #983, #1254 - css(elem, { - width: textWidth + 'px', - display: 'block', - whiteSpace: whiteSpace || 'normal' // #3331 - }); - wrapper.oldTextWidth = textWidth; - } - - // Do the calculations and DOM access only if properties changed - if (currentTextTransform !== wrapper.cTT) { - baseline = renderer.fontMetrics(elem.style.fontSize).b; - - // Renderer specific handling of span rotation, but only if we - // have something to update. - if ( - defined(rotation) && - rotation !== (wrapper.oldRotation || 0) - ) { - wrapper.setSpanRotation( - rotation, - alignCorrection, - baseline - ); - } - - wrapper.getSpanCorrection( - // Avoid elem.offsetWidth if we can, it affects rendering - // time heavily (#7656) - ( - (!defined(rotation) && wrapper.textPxLength) || // #7920 - elem.offsetWidth - ), - baseline, - alignCorrection, - rotation, - align - ); - } - - // apply position with correction - css(elem, { - left: (x + (wrapper.xCorr || 0)) + 'px', - top: (y + (wrapper.yCorr || 0)) + 'px' - }); - - // record current text transform - wrapper.cTT = currentTextTransform; - wrapper.oldRotation = rotation; - } - }, - - /** - * Set the rotation of an individual HTML span - */ - setSpanRotation: function (rotation, alignCorrection, baseline) { - var rotationStyle = {}, - cssTransformKey = this.renderer.getTransformKey(); - - rotationStyle[cssTransformKey] = rotationStyle.transform = - 'rotate(' + rotation + 'deg)'; - rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = - rotationStyle.transformOrigin = - (alignCorrection * 100) + '% ' + baseline + 'px'; - css(this.element, rotationStyle); - }, - - /** - * Get the correction in X and Y positioning as the element is rotated. - */ - getSpanCorrection: function (width, baseline, alignCorrection) { - this.xCorr = -width * alignCorrection; - this.yCorr = -baseline; - } + /** + * Apply CSS to HTML elements. This is used in text within SVG rendering and + * by the VML renderer + */ + htmlCss: function (styles) { + var wrapper = this, + element = wrapper.element, + textWidth = styles && element.tagName === 'SPAN' && styles.width; + + if (textWidth) { + delete styles.width; + wrapper.textWidth = textWidth; + wrapper.htmlUpdateTransform(); + } + if (styles && styles.textOverflow === 'ellipsis') { + styles.whiteSpace = 'nowrap'; + styles.overflow = 'hidden'; + } + wrapper.styles = extend(wrapper.styles, styles); + css(wrapper.element, styles); + + return wrapper; + }, + + /** + * VML and useHTML method for calculating the bounding box based on offsets + * @param {Boolean} refresh Whether to force a fresh value from the DOM or + * to use the cached value. + * + * @return {Object} A hash containing values for x, y, width and height + */ + + htmlGetBBox: function () { + var wrapper = this, + element = wrapper.element; + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + }, + + /** + * VML override private method to update elements based on internal + * properties based on SVG transform + */ + htmlUpdateTransform: function () { + // aligning non added elements is expensive + if (!this.added) { + this.alignOnAdd = true; + return; + } + + var wrapper = this, + renderer = wrapper.renderer, + elem = wrapper.element, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + x = wrapper.x || 0, + y = wrapper.y || 0, + align = wrapper.textAlign || 'left', + alignCorrection = { left: 0, center: 0.5, right: 1 }[align], + styles = wrapper.styles, + whiteSpace = styles && styles.whiteSpace; + + function getTextPxLength() { + // Reset multiline/ellipsis in order to read width (#4928, + // #5417) + css(elem, { + width: '', + whiteSpace: whiteSpace || 'nowrap' + }); + return elem.offsetWidth; + } + + // apply translate + css(elem, { + marginLeft: translateX, + marginTop: translateY + }); + + /*= if (build.classic) { =*/ + if (wrapper.shadows) { // used in labels/tooltip + each(wrapper.shadows, function (shadow) { + css(shadow, { + marginLeft: translateX + 1, + marginTop: translateY + 1 + }); + }); + } + /*= } =*/ + + // apply inversion + if (wrapper.inverted) { // wrapper is a group + each(elem.childNodes, function (child) { + renderer.invertChild(child, elem); + }); + } + + if (elem.tagName === 'SPAN') { + + var rotation = wrapper.rotation, + baseline, + textWidth = wrapper.textWidth && pInt(wrapper.textWidth), + currentTextTransform = [ + rotation, + align, + elem.innerHTML, + wrapper.textWidth, + wrapper.textAlign + ].join(','); + + // Update textWidth. Use the memoized textPxLength if possible, to + // avoid the getTextPxLength function using elem.offsetWidth. + // Calling offsetWidth affects rendering time as it forces layout + // (#7656). + if ( + textWidth !== wrapper.oldTextWidth && + ( + (textWidth > wrapper.oldTextWidth) || + (wrapper.textPxLength || getTextPxLength()) > textWidth + ) && + /[ \-]/.test(elem.textContent || elem.innerText) + ) { // #983, #1254 + css(elem, { + width: textWidth + 'px', + display: 'block', + whiteSpace: whiteSpace || 'normal' // #3331 + }); + wrapper.oldTextWidth = textWidth; + } + + // Do the calculations and DOM access only if properties changed + if (currentTextTransform !== wrapper.cTT) { + baseline = renderer.fontMetrics(elem.style.fontSize).b; + + // Renderer specific handling of span rotation, but only if we + // have something to update. + if ( + defined(rotation) && + rotation !== (wrapper.oldRotation || 0) + ) { + wrapper.setSpanRotation( + rotation, + alignCorrection, + baseline + ); + } + + wrapper.getSpanCorrection( + // Avoid elem.offsetWidth if we can, it affects rendering + // time heavily (#7656) + ( + (!defined(rotation) && wrapper.textPxLength) || // #7920 + elem.offsetWidth + ), + baseline, + alignCorrection, + rotation, + align + ); + } + + // apply position with correction + css(elem, { + left: (x + (wrapper.xCorr || 0)) + 'px', + top: (y + (wrapper.yCorr || 0)) + 'px' + }); + + // record current text transform + wrapper.cTT = currentTextTransform; + wrapper.oldRotation = rotation; + } + }, + + /** + * Set the rotation of an individual HTML span + */ + setSpanRotation: function (rotation, alignCorrection, baseline) { + var rotationStyle = {}, + cssTransformKey = this.renderer.getTransformKey(); + + rotationStyle[cssTransformKey] = rotationStyle.transform = + 'rotate(' + rotation + 'deg)'; + rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = + rotationStyle.transformOrigin = + (alignCorrection * 100) + '% ' + baseline + 'px'; + css(this.element, rotationStyle); + }, + + /** + * Get the correction in X and Y positioning as the element is rotated. + */ + getSpanCorrection: function (width, baseline, alignCorrection) { + this.xCorr = -width * alignCorrection; + this.yCorr = -baseline; + } }); // Extend SvgRenderer for useHTML option. extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ { - getTransformKey: function () { - return isMS && !/Edge/.test(win.navigator.userAgent) ? - '-ms-transform' : - isWebKit ? - '-webkit-transform' : - isFirefox ? - 'MozTransform' : - win.opera ? - '-o-transform' : - ''; - }, - - /** - * Create HTML text node. This is used by the VML renderer as well as the - * SVG renderer through the useHTML option. - * - * @param {String} str - * @param {Number} x - * @param {Number} y - */ - html: function (str, x, y) { - var wrapper = this.createElement('span'), - element = wrapper.element, - renderer = wrapper.renderer, - isSVG = renderer.isSVG, - addSetters = function (element, style) { - // These properties are set as attributes on the SVG group, and - // as identical CSS properties on the div. (#3542) - each(['opacity', 'visibility'], function (prop) { - wrap(element, prop + 'Setter', function ( - proceed, - value, - key, - elem - ) { - proceed.call(this, value, key, elem); - style[key] = value; - }); - }); - element.addedSetters = true; - }; - - // Text setter - wrapper.textSetter = function (value) { - if (value !== element.innerHTML) { - delete this.bBox; - } - this.textStr = value; - element.innerHTML = pick(value, ''); - wrapper.doTransform = true; - }; - - // Add setters for the element itself (#4938) - if (isSVG) { // #4938, only for HTML within SVG - addSetters(wrapper, wrapper.element.style); - } - - // Various setters which rely on update transform - wrapper.xSetter = - wrapper.ySetter = - wrapper.alignSetter = - wrapper.rotationSetter = - function (value, key) { - if (key === 'align') { - // Do not overwrite the SVGElement.align method. Same as VML. - key = 'textAlign'; - } - wrapper[key] = value; - wrapper.doTransform = true; - }; - - // Runs at the end of .attr() - wrapper.afterSetters = function () { - // Update transform. Do this outside the loop to prevent redundant - // updating for batch setting of attributes. - if (this.doTransform) { - this.htmlUpdateTransform(); - this.doTransform = false; - } - }; - - // Set the default attributes - wrapper - .attr({ - text: str, - x: Math.round(x), - y: Math.round(y) - }) - .css({ - /*= if (build.classic) { =*/ - fontFamily: this.style.fontFamily, - fontSize: this.style.fontSize, - /*= } =*/ - position: 'absolute' - }); - - // Keep the whiteSpace style outside the wrapper.styles collection - element.style.whiteSpace = 'nowrap'; - - // Use the HTML specific .css method - wrapper.css = wrapper.htmlCss; - - // This is specific for HTML within SVG - if (isSVG) { - wrapper.add = function (svgGroupWrapper) { - - var htmlGroup, - container = renderer.box.parentNode, - parentGroup, - parents = []; - - this.parentGroup = svgGroupWrapper; - - // Create a mock group to hold the HTML elements - if (svgGroupWrapper) { - htmlGroup = svgGroupWrapper.div; - if (!htmlGroup) { - - // Read the parent chain into an array and read from top - // down - parentGroup = svgGroupWrapper; - while (parentGroup) { - - parents.push(parentGroup); - - // Move up to the next parent group - parentGroup = parentGroup.parentGroup; - } - - // Ensure dynamically updating position when any parent - // is translated - each(parents.reverse(), function (parentGroup) { - var htmlGroupStyle, - cls = attr(parentGroup.element, 'class'); - - // Common translate setter for X and Y on the HTML - // group. Reverted the fix for #6957 du to - // positioning problems and offline export (#7254, - // #7280, #7529) - function translateSetter(value, key) { - parentGroup[key] = value; - - if (key === 'translateX') { - htmlGroupStyle.left = value + 'px'; - } else { - htmlGroupStyle.top = value + 'px'; - } - - parentGroup.doTransform = true; - } - - if (cls) { - cls = { className: cls }; - } // else null - - // Create a HTML div and append it to the parent div - // to emulate the SVG group structure - htmlGroup = - parentGroup.div = - parentGroup.div || createElement('div', cls, { - position: 'absolute', - left: (parentGroup.translateX || 0) + 'px', - top: (parentGroup.translateY || 0) + 'px', - display: parentGroup.display, - opacity: parentGroup.opacity, // #5075 - pointerEvents: ( - parentGroup.styles && - parentGroup.styles.pointerEvents - ) // #5595 - - // the top group is appended to container - }, htmlGroup || container); - - // Shortcut - htmlGroupStyle = htmlGroup.style; - - // Set listeners to update the HTML div's position - // whenever the SVG group position is changed. - extend(parentGroup, { - // (#7287) Pass htmlGroup to use - // the related group - classSetter: (function (htmlGroup) { - return function (value) { - this.element.setAttribute( - 'class', - value - ); - htmlGroup.className = value; - }; - }(htmlGroup)), - on: function () { - if (parents[0].div) { // #6418 - wrapper.on.apply( - { element: parents[0].div }, - arguments - ); - } - return parentGroup; - }, - translateXSetter: translateSetter, - translateYSetter: translateSetter - }); - if (!parentGroup.addedSetters) { - addSetters(parentGroup, htmlGroupStyle); - } - }); - - } - } else { - htmlGroup = container; - } - - htmlGroup.appendChild(element); - - // Shared with VML: - wrapper.added = true; - if (wrapper.alignOnAdd) { - wrapper.htmlUpdateTransform(); - } - - return wrapper; - }; - } - return wrapper; - } + getTransformKey: function () { + return isMS && !/Edge/.test(win.navigator.userAgent) ? + '-ms-transform' : + isWebKit ? + '-webkit-transform' : + isFirefox ? + 'MozTransform' : + win.opera ? + '-o-transform' : + ''; + }, + + /** + * Create HTML text node. This is used by the VML renderer as well as the + * SVG renderer through the useHTML option. + * + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + html: function (str, x, y) { + var wrapper = this.createElement('span'), + element = wrapper.element, + renderer = wrapper.renderer, + isSVG = renderer.isSVG, + addSetters = function (element, style) { + // These properties are set as attributes on the SVG group, and + // as identical CSS properties on the div. (#3542) + each(['opacity', 'visibility'], function (prop) { + wrap(element, prop + 'Setter', function ( + proceed, + value, + key, + elem + ) { + proceed.call(this, value, key, elem); + style[key] = value; + }); + }); + element.addedSetters = true; + }; + + // Text setter + wrapper.textSetter = function (value) { + if (value !== element.innerHTML) { + delete this.bBox; + } + this.textStr = value; + element.innerHTML = pick(value, ''); + wrapper.doTransform = true; + }; + + // Add setters for the element itself (#4938) + if (isSVG) { // #4938, only for HTML within SVG + addSetters(wrapper, wrapper.element.style); + } + + // Various setters which rely on update transform + wrapper.xSetter = + wrapper.ySetter = + wrapper.alignSetter = + wrapper.rotationSetter = + function (value, key) { + if (key === 'align') { + // Do not overwrite the SVGElement.align method. Same as VML. + key = 'textAlign'; + } + wrapper[key] = value; + wrapper.doTransform = true; + }; + + // Runs at the end of .attr() + wrapper.afterSetters = function () { + // Update transform. Do this outside the loop to prevent redundant + // updating for batch setting of attributes. + if (this.doTransform) { + this.htmlUpdateTransform(); + this.doTransform = false; + } + }; + + // Set the default attributes + wrapper + .attr({ + text: str, + x: Math.round(x), + y: Math.round(y) + }) + .css({ + /*= if (build.classic) { =*/ + fontFamily: this.style.fontFamily, + fontSize: this.style.fontSize, + /*= } =*/ + position: 'absolute' + }); + + // Keep the whiteSpace style outside the wrapper.styles collection + element.style.whiteSpace = 'nowrap'; + + // Use the HTML specific .css method + wrapper.css = wrapper.htmlCss; + + // This is specific for HTML within SVG + if (isSVG) { + wrapper.add = function (svgGroupWrapper) { + + var htmlGroup, + container = renderer.box.parentNode, + parentGroup, + parents = []; + + this.parentGroup = svgGroupWrapper; + + // Create a mock group to hold the HTML elements + if (svgGroupWrapper) { + htmlGroup = svgGroupWrapper.div; + if (!htmlGroup) { + + // Read the parent chain into an array and read from top + // down + parentGroup = svgGroupWrapper; + while (parentGroup) { + + parents.push(parentGroup); + + // Move up to the next parent group + parentGroup = parentGroup.parentGroup; + } + + // Ensure dynamically updating position when any parent + // is translated + each(parents.reverse(), function (parentGroup) { + var htmlGroupStyle, + cls = attr(parentGroup.element, 'class'); + + // Common translate setter for X and Y on the HTML + // group. Reverted the fix for #6957 du to + // positioning problems and offline export (#7254, + // #7280, #7529) + function translateSetter(value, key) { + parentGroup[key] = value; + + if (key === 'translateX') { + htmlGroupStyle.left = value + 'px'; + } else { + htmlGroupStyle.top = value + 'px'; + } + + parentGroup.doTransform = true; + } + + if (cls) { + cls = { className: cls }; + } // else null + + // Create a HTML div and append it to the parent div + // to emulate the SVG group structure + htmlGroup = + parentGroup.div = + parentGroup.div || createElement('div', cls, { + position: 'absolute', + left: (parentGroup.translateX || 0) + 'px', + top: (parentGroup.translateY || 0) + 'px', + display: parentGroup.display, + opacity: parentGroup.opacity, // #5075 + pointerEvents: ( + parentGroup.styles && + parentGroup.styles.pointerEvents + ) // #5595 + + // the top group is appended to container + }, htmlGroup || container); + + // Shortcut + htmlGroupStyle = htmlGroup.style; + + // Set listeners to update the HTML div's position + // whenever the SVG group position is changed. + extend(parentGroup, { + // (#7287) Pass htmlGroup to use + // the related group + classSetter: (function (htmlGroup) { + return function (value) { + this.element.setAttribute( + 'class', + value + ); + htmlGroup.className = value; + }; + }(htmlGroup)), + on: function () { + if (parents[0].div) { // #6418 + wrapper.on.apply( + { element: parents[0].div }, + arguments + ); + } + return parentGroup; + }, + translateXSetter: translateSetter, + translateYSetter: translateSetter + }); + if (!parentGroup.addedSetters) { + addSetters(parentGroup, htmlGroupStyle); + } + }); + + } + } else { + htmlGroup = container; + } + + htmlGroup.appendChild(element); + + // Shared with VML: + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.htmlUpdateTransform(); + } + + return wrapper; + }; + } + return wrapper; + } }); diff --git a/js/parts/Interaction.js b/js/parts/Interaction.js index 808246cd3bb..1f3ec6fcc41 100644 --- a/js/parts/Interaction.js +++ b/js/parts/Interaction.js @@ -12,198 +12,198 @@ import './Legend.js'; import './Point.js'; import './Series.js'; var addEvent = H.addEvent, - Chart = H.Chart, - createElement = H.createElement, - css = H.css, - defaultOptions = H.defaultOptions, - defaultPlotOptions = H.defaultPlotOptions, - each = H.each, - extend = H.extend, - fireEvent = H.fireEvent, - hasTouch = H.hasTouch, - inArray = H.inArray, - isObject = H.isObject, - Legend = H.Legend, - merge = H.merge, - pick = H.pick, - Point = H.Point, - Series = H.Series, - seriesTypes = H.seriesTypes, - svg = H.svg, - TrackerMixin; + Chart = H.Chart, + createElement = H.createElement, + css = H.css, + defaultOptions = H.defaultOptions, + defaultPlotOptions = H.defaultPlotOptions, + each = H.each, + extend = H.extend, + fireEvent = H.fireEvent, + hasTouch = H.hasTouch, + inArray = H.inArray, + isObject = H.isObject, + Legend = H.Legend, + merge = H.merge, + pick = H.pick, + Point = H.Point, + Series = H.Series, + seriesTypes = H.seriesTypes, + svg = H.svg, + TrackerMixin; /** * TrackerMixin for points and graphs. */ TrackerMixin = H.TrackerMixin = { - /** - * Draw the tracker for a point. - */ - drawTrackerPoint: function () { - var series = this, - chart = series.chart, - pointer = chart.pointer, - onMouseOver = function (e) { - var point = pointer.getPointFromEvent(e); - // undefined on graph in scatterchart - if (point !== undefined) { - pointer.isDirectTouch = true; - point.onMouseOver(e); - } - }; - - // Add reference to the point - each(series.points, function (point) { - if (point.graphic) { - point.graphic.element.point = point; - } - if (point.dataLabel) { - if (point.dataLabel.div) { - point.dataLabel.div.point = point; - } else { - point.dataLabel.element.point = point; - } - } - }); - - // Add the event listeners, we need to do this only once - if (!series._hasTracking) { - each(series.trackerGroups, function (key) { - if (series[key]) { // we don't always have dataLabelsGroup - series[key] - .addClass('highcharts-tracker') - .on('mouseover', onMouseOver) - .on('mouseout', function (e) { - pointer.onTrackerMouseOut(e); - }); - if (hasTouch) { - series[key].on('touchstart', onMouseOver); - } - - /*= if (build.classic) { =*/ - if (series.options.cursor) { - series[key] - .css(css) - .css({ cursor: series.options.cursor }); - } - /*= } =*/ - } - }); - series._hasTracking = true; - } - - fireEvent(this, 'afterDrawTracker'); - }, - - /** - * Draw the tracker object that sits above all data labels and markers to - * track mouse events on the graph or points. For the line type charts - * the tracker uses the same graphPath, but with a greater stroke width - * for better control. - */ - drawTrackerGraph: function () { - var series = this, - options = series.options, - trackByArea = options.trackByArea, - trackerPath = [].concat( - trackByArea ? series.areaPath : series.graphPath - ), - trackerPathLength = trackerPath.length, - chart = series.chart, - pointer = chart.pointer, - renderer = chart.renderer, - snap = chart.options.tooltip.snap, - tracker = series.tracker, - i, - onMouseOver = function () { - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } - }, - /* - * Empirical lowest possible opacities for TRACKER_FILL for an - * element to stay invisible but clickable - * IE6: 0.002 - * IE7: 0.002 - * IE8: 0.002 - * IE9: 0.00000000001 (unlimited) - * IE10: 0.0001 (exporting only) - * FF: 0.00000000001 (unlimited) - * Chrome: 0.000001 - * Safari: 0.000001 - * Opera: 0.00000000001 (unlimited) - */ - TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')'; - - // Extend end points. A better way would be to use round linecaps, - // but those are not clickable in VML. - if (trackerPathLength && !trackByArea) { - i = trackerPathLength + 1; - while (i--) { - if (trackerPath[i] === 'M') { // extend left side - trackerPath.splice( - i + 1, 0, - trackerPath[i + 1] - snap, - trackerPath[i + 2], - 'L' - ); - } - if ( - (i && trackerPath[i] === 'M') || - i === trackerPathLength - ) { // extend right side - trackerPath.splice( - i, - 0, - 'L', - trackerPath[i - 2] + snap, - trackerPath[i - 1] - ); - } - } - } - - // draw the tracker - if (tracker) { - tracker.attr({ d: trackerPath }); - } else if (series.graph) { // create - - series.tracker = renderer.path(trackerPath) - .attr({ - 'stroke-linejoin': 'round', // #1225 - visibility: series.visible ? 'visible' : 'hidden', - stroke: TRACKER_FILL, - fill: trackByArea ? TRACKER_FILL : 'none', - 'stroke-width': series.graph.strokeWidth() + - (trackByArea ? 0 : 2 * snap), - zIndex: 2 - }) - .add(series.group); - - // The tracker is added to the series group, which is clipped, but - // is covered by the marker group. So the marker group also needs to - // capture events. - each([series.tracker, series.markerGroup], function (tracker) { - tracker.addClass('highcharts-tracker') - .on('mouseover', onMouseOver) - .on('mouseout', function (e) { - pointer.onTrackerMouseOut(e); - }); - - /*= if (build.classic) { =*/ - if (options.cursor) { - tracker.css({ cursor: options.cursor }); - } - /*= } =*/ - - if (hasTouch) { - tracker.on('touchstart', onMouseOver); - } - }); - } - fireEvent(this, 'afterDrawTracker'); - } + /** + * Draw the tracker for a point. + */ + drawTrackerPoint: function () { + var series = this, + chart = series.chart, + pointer = chart.pointer, + onMouseOver = function (e) { + var point = pointer.getPointFromEvent(e); + // undefined on graph in scatterchart + if (point !== undefined) { + pointer.isDirectTouch = true; + point.onMouseOver(e); + } + }; + + // Add reference to the point + each(series.points, function (point) { + if (point.graphic) { + point.graphic.element.point = point; + } + if (point.dataLabel) { + if (point.dataLabel.div) { + point.dataLabel.div.point = point; + } else { + point.dataLabel.element.point = point; + } + } + }); + + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key] + .addClass('highcharts-tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }); + if (hasTouch) { + series[key].on('touchstart', onMouseOver); + } + + /*= if (build.classic) { =*/ + if (series.options.cursor) { + series[key] + .css(css) + .css({ cursor: series.options.cursor }); + } + /*= } =*/ + } + }); + series._hasTracking = true; + } + + fireEvent(this, 'afterDrawTracker'); + }, + + /** + * Draw the tracker object that sits above all data labels and markers to + * track mouse events on the graph or points. For the line type charts + * the tracker uses the same graphPath, but with a greater stroke width + * for better control. + */ + drawTrackerGraph: function () { + var series = this, + options = series.options, + trackByArea = options.trackByArea, + trackerPath = [].concat( + trackByArea ? series.areaPath : series.graphPath + ), + trackerPathLength = trackerPath.length, + chart = series.chart, + pointer = chart.pointer, + renderer = chart.renderer, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + i, + onMouseOver = function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }, + /* + * Empirical lowest possible opacities for TRACKER_FILL for an + * element to stay invisible but clickable + * IE6: 0.002 + * IE7: 0.002 + * IE8: 0.002 + * IE9: 0.00000000001 (unlimited) + * IE10: 0.0001 (exporting only) + * FF: 0.00000000001 (unlimited) + * Chrome: 0.000001 + * Safari: 0.000001 + * Opera: 0.00000000001 (unlimited) + */ + TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')'; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength && !trackByArea) { + i = trackerPathLength + 1; + while (i--) { + if (trackerPath[i] === 'M') { // extend left side + trackerPath.splice( + i + 1, 0, + trackerPath[i + 1] - snap, + trackerPath[i + 2], + 'L' + ); + } + if ( + (i && trackerPath[i] === 'M') || + i === trackerPathLength + ) { // extend right side + trackerPath.splice( + i, + 0, + 'L', + trackerPath[i - 2] + snap, + trackerPath[i - 1] + ); + } + } + } + + // draw the tracker + if (tracker) { + tracker.attr({ d: trackerPath }); + } else if (series.graph) { // create + + series.tracker = renderer.path(trackerPath) + .attr({ + 'stroke-linejoin': 'round', // #1225 + visibility: series.visible ? 'visible' : 'hidden', + stroke: TRACKER_FILL, + fill: trackByArea ? TRACKER_FILL : 'none', + 'stroke-width': series.graph.strokeWidth() + + (trackByArea ? 0 : 2 * snap), + zIndex: 2 + }) + .add(series.group); + + // The tracker is added to the series group, which is clipped, but + // is covered by the marker group. So the marker group also needs to + // capture events. + each([series.tracker, series.markerGroup], function (tracker) { + tracker.addClass('highcharts-tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }); + + /*= if (build.classic) { =*/ + if (options.cursor) { + tracker.css({ cursor: options.cursor }); + } + /*= } =*/ + + if (hasTouch) { + tracker.on('touchstart', onMouseOver); + } + }); + } + fireEvent(this, 'afterDrawTracker'); + } }; /* End TrackerMixin */ @@ -214,15 +214,15 @@ TrackerMixin = H.TrackerMixin = { */ if (seriesTypes.column) { - seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint; } if (seriesTypes.pie) { - seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint; } if (seriesTypes.scatter) { - seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint; } /* @@ -230,91 +230,91 @@ if (seriesTypes.scatter) { */ extend(Legend.prototype, { - setItemEvents: function (item, legendItem, useHTML) { - var legend = this, - boxWrapper = legend.chart.renderer.boxWrapper, - activeClass = 'highcharts-legend-' + - (item instanceof Point ? 'point' : 'series') + '-active'; - - // Set the events on the item group, or in case of useHTML, the item - // itself (#1249) - (useHTML ? legendItem : item.legendGroup).on('mouseover', function () { - item.setState('hover'); - - // A CSS class to dim or hide other than the hovered series - boxWrapper.addClass(activeClass); - - /*= if (build.classic) { =*/ - legendItem.css(legend.options.itemHoverStyle); - /*= } =*/ - }) - .on('mouseout', function () { - /*= if (build.classic) { =*/ - legendItem.css( - merge(item.visible ? legend.itemStyle : legend.itemHiddenStyle) - ); - /*= } =*/ - - // A CSS class to dim or hide other than the hovered series - boxWrapper.removeClass(activeClass); - - item.setState(); - }) - .on('click', function (event) { - var strLegendItemClick = 'legendItemClick', - fnLegendItemClick = function () { - if (item.setVisible) { - item.setVisible(); - } - }; - - // A CSS class to dim or hide other than the hovered series. Event - // handling in iOS causes the activeClass to be added prior to click - // in some cases (#7418). - boxWrapper.removeClass(activeClass); - - // Pass over the click/touch event. #4. - event = { - browserEvent: event - }; - - // click the name or symbol - if (item.firePointEvent) { // point - item.firePointEvent( - strLegendItemClick, - event, - fnLegendItemClick - ); - } else { - fireEvent(item, strLegendItemClick, event, fnLegendItemClick); - } - }); - }, - - createCheckboxForItem: function (item) { - var legend = this; - - item.checkbox = createElement('input', { - type: 'checkbox', - checked: item.selected, - defaultChecked: item.selected // required by IE7 - }, legend.options.itemCheckboxStyle, legend.chart.container); - - addEvent(item.checkbox, 'click', function (event) { - var target = event.target; - fireEvent( - item.series || item, - 'checkboxClick', - { // #3712 - checked: target.checked, - item: item - }, - function () { - item.select(); - } - ); - }); - } + setItemEvents: function (item, legendItem, useHTML) { + var legend = this, + boxWrapper = legend.chart.renderer.boxWrapper, + activeClass = 'highcharts-legend-' + + (item instanceof Point ? 'point' : 'series') + '-active'; + + // Set the events on the item group, or in case of useHTML, the item + // itself (#1249) + (useHTML ? legendItem : item.legendGroup).on('mouseover', function () { + item.setState('hover'); + + // A CSS class to dim or hide other than the hovered series + boxWrapper.addClass(activeClass); + + /*= if (build.classic) { =*/ + legendItem.css(legend.options.itemHoverStyle); + /*= } =*/ + }) + .on('mouseout', function () { + /*= if (build.classic) { =*/ + legendItem.css( + merge(item.visible ? legend.itemStyle : legend.itemHiddenStyle) + ); + /*= } =*/ + + // A CSS class to dim or hide other than the hovered series + boxWrapper.removeClass(activeClass); + + item.setState(); + }) + .on('click', function (event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function () { + if (item.setVisible) { + item.setVisible(); + } + }; + + // A CSS class to dim or hide other than the hovered series. Event + // handling in iOS causes the activeClass to be added prior to click + // in some cases (#7418). + boxWrapper.removeClass(activeClass); + + // Pass over the click/touch event. #4. + event = { + browserEvent: event + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent( + strLegendItemClick, + event, + fnLegendItemClick + ); + } else { + fireEvent(item, strLegendItemClick, event, fnLegendItemClick); + } + }); + }, + + createCheckboxForItem: function (item) { + var legend = this; + + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, legend.options.itemCheckboxStyle, legend.chart.container); + + addEvent(item.checkbox, 'click', function (event) { + var target = event.target; + fireEvent( + item.series || item, + 'checkboxClick', + { // #3712 + checked: target.checked, + item: item + }, + function () { + item.select(); + } + ); + }); + } }); @@ -329,539 +329,539 @@ defaultOptions.legend.itemStyle.cursor = 'pointer'; */ extend(Chart.prototype, /** @lends Chart.prototype */ { - /** - * Display the zoom button. - * - * @private - */ - showResetZoom: function () { - var chart = this, - lang = defaultOptions.lang, - btnOptions = chart.options.chart.resetZoomButton, - theme = btnOptions.theme, - states = theme.states, - alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; - - function zoomOut() { - chart.zoomOut(); - } - - fireEvent(this, 'beforeShowResetZoom', null, function () { - chart.resetZoomButton = chart.renderer.button( - lang.resetZoom, - null, - null, - zoomOut, - theme, - states && states.hover - ) - .attr({ - align: btnOptions.position.align, - title: lang.resetZoomTitle - }) - .addClass('highcharts-reset-zoom') - .add() - .align(btnOptions.position, false, alignTo); - }); - - }, - - /** - * Zoom out to 1:1. - * - * @private - */ - zoomOut: function () { - fireEvent(this, 'selection', { resetSelection: true }, this.zoom); - }, - - /** - * Zoom into a given portion of the chart given by axis coordinates. - * @param {Object} event - * - * @private - */ - zoom: function (event) { - var chart = this, - hasZoomed, - pointer = chart.pointer, - displayButton = false, - resetZoomButton; - - // If zoom is called with no arguments, reset the axes - if (!event || event.resetSelection) { - each(chart.axes, function (axis) { - hasZoomed = axis.zoom(); - }); - pointer.initiated = false; // #6804 - - } else { // else, zoom in on all axes - each(event.xAxis.concat(event.yAxis), function (axisData) { - var axis = axisData.axis, - isXAxis = axis.isXAxis; - - // don't zoom more than minRange - if (pointer[isXAxis ? 'zoomX' : 'zoomY']) { - hasZoomed = axis.zoom(axisData.min, axisData.max); - if (axis.displayBtn) { - displayButton = true; - } - } - }); - } - - // Show or hide the Reset zoom button - resetZoomButton = chart.resetZoomButton; - if (displayButton && !resetZoomButton) { - chart.showResetZoom(); - } else if (!displayButton && isObject(resetZoomButton)) { - chart.resetZoomButton = resetZoomButton.destroy(); - } - - - // Redraw - if (hasZoomed) { - chart.redraw( - pick( - chart.options.chart.animation, - event && event.animation, - chart.pointCount < 100 - ) - ); - } - }, - - /** - * Pan the chart by dragging the mouse across the pane. This function is - * called on mouse move, and the distance to pan is computed from chartX - * compared to the first chartX position in the dragging operation. - * - * @private - */ - pan: function (e, panning) { - - var chart = this, - hoverPoints = chart.hoverPoints, - doRedraw; - - // remove active points for shared tooltip - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - // xy is used in maps - each(panning === 'xy' ? [1, 0] : [1], function (isX) { - var axis = chart[isX ? 'xAxis' : 'yAxis'][0], - horiz = axis.horiz, - mousePos = e[horiz ? 'chartX' : 'chartY'], - mouseDown = horiz ? 'mouseDownX' : 'mouseDownY', - startPos = chart[mouseDown], - halfPointRange = (axis.pointRange || 0) / - (axis.reversed ? -2 : 2), - extremes = axis.getExtremes(), - panMin = axis.toValue(startPos - mousePos, true) + - halfPointRange, - panMax = axis.toValue(startPos + axis.len - mousePos, true) - - halfPointRange, - flipped = panMax < panMin, - newMin = flipped ? panMax : panMin, - newMax = flipped ? panMin : panMax, - paddedMin = Math.min( - extremes.dataMin, - halfPointRange ? - extremes.min : - axis.toValue( - axis.toPixels(extremes.min) - axis.minPixelPadding - ) - ), - paddedMax = Math.max( - extremes.dataMax, - halfPointRange ? - extremes.max : - axis.toValue( - axis.toPixels(extremes.max) + axis.minPixelPadding - ) - ), - spill; - - // If the new range spills over, either to the min or max, adjust - // the new range. - spill = paddedMin - newMin; - if (spill > 0) { - newMax += spill; - newMin = paddedMin; - } - spill = newMax - paddedMax; - if (spill > 0) { - newMax = paddedMax; - newMin -= spill; - } - - // Set new extremes if they are actually new - if ( - axis.series.length && - newMin !== extremes.min && - newMax !== extremes.max - ) { - axis.setExtremes( - newMin, - newMax, - false, - false, - { trigger: 'pan' } - ); - doRedraw = true; - } - - chart[mouseDown] = mousePos; // set new reference for next run - }); - - if (doRedraw) { - chart.redraw(false); - } - css(chart.container, { cursor: 'move' }); - } + /** + * Display the zoom button. + * + * @private + */ + showResetZoom: function () { + var chart = this, + lang = defaultOptions.lang, + btnOptions = chart.options.chart.resetZoomButton, + theme = btnOptions.theme, + states = theme.states, + alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; + + function zoomOut() { + chart.zoomOut(); + } + + fireEvent(this, 'beforeShowResetZoom', null, function () { + chart.resetZoomButton = chart.renderer.button( + lang.resetZoom, + null, + null, + zoomOut, + theme, + states && states.hover + ) + .attr({ + align: btnOptions.position.align, + title: lang.resetZoomTitle + }) + .addClass('highcharts-reset-zoom') + .add() + .align(btnOptions.position, false, alignTo); + }); + + }, + + /** + * Zoom out to 1:1. + * + * @private + */ + zoomOut: function () { + fireEvent(this, 'selection', { resetSelection: true }, this.zoom); + }, + + /** + * Zoom into a given portion of the chart given by axis coordinates. + * @param {Object} event + * + * @private + */ + zoom: function (event) { + var chart = this, + hasZoomed, + pointer = chart.pointer, + displayButton = false, + resetZoomButton; + + // If zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(chart.axes, function (axis) { + hasZoomed = axis.zoom(); + }); + pointer.initiated = false; // #6804 + + } else { // else, zoom in on all axes + each(event.xAxis.concat(event.yAxis), function (axisData) { + var axis = axisData.axis, + isXAxis = axis.isXAxis; + + // don't zoom more than minRange + if (pointer[isXAxis ? 'zoomX' : 'zoomY']) { + hasZoomed = axis.zoom(axisData.min, axisData.max); + if (axis.displayBtn) { + displayButton = true; + } + } + }); + } + + // Show or hide the Reset zoom button + resetZoomButton = chart.resetZoomButton; + if (displayButton && !resetZoomButton) { + chart.showResetZoom(); + } else if (!displayButton && isObject(resetZoomButton)) { + chart.resetZoomButton = resetZoomButton.destroy(); + } + + + // Redraw + if (hasZoomed) { + chart.redraw( + pick( + chart.options.chart.animation, + event && event.animation, + chart.pointCount < 100 + ) + ); + } + }, + + /** + * Pan the chart by dragging the mouse across the pane. This function is + * called on mouse move, and the distance to pan is computed from chartX + * compared to the first chartX position in the dragging operation. + * + * @private + */ + pan: function (e, panning) { + + var chart = this, + hoverPoints = chart.hoverPoints, + doRedraw; + + // remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + // xy is used in maps + each(panning === 'xy' ? [1, 0] : [1], function (isX) { + var axis = chart[isX ? 'xAxis' : 'yAxis'][0], + horiz = axis.horiz, + mousePos = e[horiz ? 'chartX' : 'chartY'], + mouseDown = horiz ? 'mouseDownX' : 'mouseDownY', + startPos = chart[mouseDown], + halfPointRange = (axis.pointRange || 0) / + (axis.reversed ? -2 : 2), + extremes = axis.getExtremes(), + panMin = axis.toValue(startPos - mousePos, true) + + halfPointRange, + panMax = axis.toValue(startPos + axis.len - mousePos, true) - + halfPointRange, + flipped = panMax < panMin, + newMin = flipped ? panMax : panMin, + newMax = flipped ? panMin : panMax, + paddedMin = Math.min( + extremes.dataMin, + halfPointRange ? + extremes.min : + axis.toValue( + axis.toPixels(extremes.min) - axis.minPixelPadding + ) + ), + paddedMax = Math.max( + extremes.dataMax, + halfPointRange ? + extremes.max : + axis.toValue( + axis.toPixels(extremes.max) + axis.minPixelPadding + ) + ), + spill; + + // If the new range spills over, either to the min or max, adjust + // the new range. + spill = paddedMin - newMin; + if (spill > 0) { + newMax += spill; + newMin = paddedMin; + } + spill = newMax - paddedMax; + if (spill > 0) { + newMax = paddedMax; + newMin -= spill; + } + + // Set new extremes if they are actually new + if ( + axis.series.length && + newMin !== extremes.min && + newMax !== extremes.max + ) { + axis.setExtremes( + newMin, + newMax, + false, + false, + { trigger: 'pan' } + ); + doRedraw = true; + } + + chart[mouseDown] = mousePos; // set new reference for next run + }); + + if (doRedraw) { + chart.redraw(false); + } + css(chart.container, { cursor: 'move' }); + } }); /* * Extend the Point object with interaction */ extend(Point.prototype, /** @lends Highcharts.Point.prototype */ { - /** - * Toggle the selection status of a point. - * @param {Boolean} [selected] - * When `true`, the point is selected. When `false`, the point is - * unselected. When `null` or `undefined`, the selection state is - * toggled. - * @param {Boolean} [accumulate=false] - * When `true`, the selection is added to other selected points. - * When `false`, other selected points are deselected. Internally in - * Highcharts, when {@link http://api.highcharts.com/highcharts/plotOptions.series.allowPointSelect|allowPointSelect} - * is `true`, selected points are accumulated on Control, Shift or - * Cmd clicking the point. - * - * @see Highcharts.Chart#getSelectedPoints - * - * @sample highcharts/members/point-select/ - * Select a point from a button - * @sample highcharts/chart/events-selection-points/ - * Select a range of points through a drag selection - * @sample maps/series/data-id/ - * Select a point in Highmaps - */ - select: function (selected, accumulate) { - var point = this, - series = point.series, - chart = series.chart; - - selected = pick(selected, !point.selected); - - // fire the event with the default handler - point.firePointEvent( - selected ? 'select' : 'unselect', - { accumulate: accumulate }, - function () { - - /** - * Whether the point is selected or not. - * @see Point#select - * @see Chart#getSelectedPoints - * @memberof Point - * @name selected - * @type {Boolean} - */ - point.selected = point.options.selected = selected; - series.options.data[inArray(point, series.data)] = - point.options; - - point.setState(selected && 'select'); - - // unselect all other points unless Ctrl or Cmd + click - if (!accumulate) { - each(chart.getSelectedPoints(), function (loopPoint) { - if (loopPoint.selected && loopPoint !== point) { - loopPoint.selected = loopPoint.options.selected = - false; - series.options.data[ - inArray(loopPoint, series.data) - ] = loopPoint.options; - loopPoint.setState(''); - loopPoint.firePointEvent('unselect'); - } - }); - } - } - ); - }, - - /** - * Runs on mouse over the point. Called internally from mouse and touch - * events. - * - * @param {Object} e The event arguments - */ - onMouseOver: function (e) { - var point = this, - series = point.series, - chart = series.chart, - pointer = chart.pointer; - e = e ? - pointer.normalize(e) : - // In cases where onMouseOver is called directly without an event - pointer.getChartCoordinatesFromPoint(point, chart.inverted); - pointer.runPointActions(e, point); - }, - - /** - * Runs on mouse out from the point. Called internally from mouse and touch - * events. - */ - onMouseOut: function () { - var point = this, - chart = point.series.chart; - point.firePointEvent('mouseOut'); - each(chart.hoverPoints || [], function (p) { - p.setState(); - }); - chart.hoverPoints = chart.hoverPoint = null; - }, - - /** - * Import events from the series' and point's options. Only do it on - * demand, to save processing time on hovering. - * - * @private - */ - importEvents: function () { - if (!this.hasImportedEvents) { - var point = this, - options = merge(point.series.options.point, point.options), - events = options.events; - - point.events = events; - - H.objectEach(events, function (event, eventType) { - addEvent(point, eventType, event); - }); - this.hasImportedEvents = true; - - } - }, - - /** - * Set the point's state. - * @param {String} [state] - * The new state, can be one of `''` (an empty string), `hover` or - * `select`. - */ - setState: function (state, move) { - var point = this, - plotX = Math.floor(point.plotX), // #4586 - plotY = point.plotY, - series = point.series, - stateOptions = series.options.states[state || 'normal'] || {}, - markerOptions = defaultPlotOptions[series.type].marker && - series.options.marker, - normalDisabled = markerOptions && markerOptions.enabled === false, - markerStateOptions = ( - markerOptions && - markerOptions.states && - markerOptions.states[state || 'normal'] - ) || {}, - stateDisabled = markerStateOptions.enabled === false, - stateMarkerGraphic = series.stateMarkerGraphic, - pointMarker = point.marker || {}, - chart = series.chart, - halo = series.halo, - haloOptions, - markerAttribs, - hasMarkers = markerOptions && series.markerAttribs, - newSymbol; - - state = state || ''; // empty string - - if ( - // already has this state - (state === point.state && !move) || - - // selected points don't respond to hover - (point.selected && state !== 'select') || - - // series' state options is disabled - (stateOptions.enabled === false) || - - // general point marker's state options is disabled - (state && ( - stateDisabled || - (normalDisabled && markerStateOptions.enabled === false) - )) || - - // individual point marker's state options is disabled - ( - state && - pointMarker.states && - pointMarker.states[state] && - pointMarker.states[state].enabled === false - ) // #1610 - - ) { - return; - } - - if (hasMarkers) { - markerAttribs = series.markerAttribs(point, state); - } - - // Apply hover styles to the existing point - if (point.graphic) { - - if (point.state) { - point.graphic.removeClass('highcharts-point-' + point.state); - } - if (state) { - point.graphic.addClass('highcharts-point-' + state); - } - - /*= if (build.classic) { =*/ - point.graphic.animate( - series.pointAttribs(point, state), - pick( - chart.options.chart.animation, - stateOptions.animation - ) - ); - /*= } =*/ - - if (markerAttribs) { - point.graphic.animate( - markerAttribs, - pick( - chart.options.chart.animation, // Turn off globally - markerStateOptions.animation, - markerOptions.animation - ) - ); - } - - // Zooming in from a range with no markers to a range with markers - if (stateMarkerGraphic) { - stateMarkerGraphic.hide(); - } - } else { - // if a graphic is not applied to each point in the normal state, - // create a shared graphic for the hover state - if (state && markerStateOptions) { - newSymbol = pointMarker.symbol || series.symbol; - - // If the point has another symbol than the previous one, throw - // away the state marker graphic and force a new one (#1459) - if ( - stateMarkerGraphic && - stateMarkerGraphic.currentSymbol !== newSymbol - ) { - stateMarkerGraphic = stateMarkerGraphic.destroy(); - } - - // Add a new state marker graphic - if (!stateMarkerGraphic) { - if (newSymbol) { - series.stateMarkerGraphic = stateMarkerGraphic = - chart.renderer.symbol( - newSymbol, - markerAttribs.x, - markerAttribs.y, - markerAttribs.width, - markerAttribs.height - ) - .add(series.markerGroup); - stateMarkerGraphic.currentSymbol = newSymbol; - } - - // Move the existing graphic - } else { - stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 - x: markerAttribs.x, - y: markerAttribs.y - }); - } - /*= if (build.classic) { =*/ - if (stateMarkerGraphic) { - stateMarkerGraphic.attr(series.pointAttribs(point, state)); - } - /*= } =*/ - } - - if (stateMarkerGraphic) { - stateMarkerGraphic[ - state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? - 'show' : - 'hide' - ](); // #2450 - stateMarkerGraphic.element.point = point; // #4310 - } - } - - // Show me your halo - haloOptions = stateOptions.halo; - if (haloOptions && haloOptions.size) { - if (!halo) { - series.halo = halo = chart.renderer.path() - // #5818, #5903, #6705 - .add((point.graphic || stateMarkerGraphic).parentGroup); - } - halo.show()[move ? 'animate' : 'attr']({ - d: point.haloPath(haloOptions.size) - }); - halo.attr({ - 'class': 'highcharts-halo highcharts-color-' + - pick(point.colorIndex, series.colorIndex) - }); - halo.point = point; // #6055 - - /*= if (build.classic) { =*/ - halo.attr(extend({ - 'fill': point.color || series.color, - 'fill-opacity': haloOptions.opacity, - 'zIndex': -1 // #4929, IE8 added halo above everything - }, haloOptions.attributes)); - /*= } =*/ - - } else if (halo && halo.point && halo.point.haloPath) { - // Animate back to 0 on the current halo point (#6055) - halo.animate( - { d: halo.point.haloPath(0) }, - null, - // Hide after unhovering. The `complete` callback runs in the - // halo's context (#7681). - halo.hide - ); - } - - point.state = state; - - fireEvent(point, 'afterSetState'); - }, - - /** - * Get the path definition for the halo, which is usually a shadow-like - * circle around the currently hovered point. - * @param {Number} size - * The radius of the circular halo. - * @return {Array} The path definition - */ - haloPath: function (size) { - var series = this.series, - chart = series.chart; - - return chart.renderer.symbols.circle( - Math.floor(this.plotX) - size, - this.plotY - size, - size * 2, - size * 2 - ); - } + /** + * Toggle the selection status of a point. + * @param {Boolean} [selected] + * When `true`, the point is selected. When `false`, the point is + * unselected. When `null` or `undefined`, the selection state is + * toggled. + * @param {Boolean} [accumulate=false] + * When `true`, the selection is added to other selected points. + * When `false`, other selected points are deselected. Internally in + * Highcharts, when {@link http://api.highcharts.com/highcharts/plotOptions.series.allowPointSelect|allowPointSelect} + * is `true`, selected points are accumulated on Control, Shift or + * Cmd clicking the point. + * + * @see Highcharts.Chart#getSelectedPoints + * + * @sample highcharts/members/point-select/ + * Select a point from a button + * @sample highcharts/chart/events-selection-points/ + * Select a range of points through a drag selection + * @sample maps/series/data-id/ + * Select a point in Highmaps + */ + select: function (selected, accumulate) { + var point = this, + series = point.series, + chart = series.chart; + + selected = pick(selected, !point.selected); + + // fire the event with the default handler + point.firePointEvent( + selected ? 'select' : 'unselect', + { accumulate: accumulate }, + function () { + + /** + * Whether the point is selected or not. + * @see Point#select + * @see Chart#getSelectedPoints + * @memberof Point + * @name selected + * @type {Boolean} + */ + point.selected = point.options.selected = selected; + series.options.data[inArray(point, series.data)] = + point.options; + + point.setState(selected && 'select'); + + // unselect all other points unless Ctrl or Cmd + click + if (!accumulate) { + each(chart.getSelectedPoints(), function (loopPoint) { + if (loopPoint.selected && loopPoint !== point) { + loopPoint.selected = loopPoint.options.selected = + false; + series.options.data[ + inArray(loopPoint, series.data) + ] = loopPoint.options; + loopPoint.setState(''); + loopPoint.firePointEvent('unselect'); + } + }); + } + } + ); + }, + + /** + * Runs on mouse over the point. Called internally from mouse and touch + * events. + * + * @param {Object} e The event arguments + */ + onMouseOver: function (e) { + var point = this, + series = point.series, + chart = series.chart, + pointer = chart.pointer; + e = e ? + pointer.normalize(e) : + // In cases where onMouseOver is called directly without an event + pointer.getChartCoordinatesFromPoint(point, chart.inverted); + pointer.runPointActions(e, point); + }, + + /** + * Runs on mouse out from the point. Called internally from mouse and touch + * events. + */ + onMouseOut: function () { + var point = this, + chart = point.series.chart; + point.firePointEvent('mouseOut'); + each(chart.hoverPoints || [], function (p) { + p.setState(); + }); + chart.hoverPoints = chart.hoverPoint = null; + }, + + /** + * Import events from the series' and point's options. Only do it on + * demand, to save processing time on hovering. + * + * @private + */ + importEvents: function () { + if (!this.hasImportedEvents) { + var point = this, + options = merge(point.series.options.point, point.options), + events = options.events; + + point.events = events; + + H.objectEach(events, function (event, eventType) { + addEvent(point, eventType, event); + }); + this.hasImportedEvents = true; + + } + }, + + /** + * Set the point's state. + * @param {String} [state] + * The new state, can be one of `''` (an empty string), `hover` or + * `select`. + */ + setState: function (state, move) { + var point = this, + plotX = Math.floor(point.plotX), // #4586 + plotY = point.plotY, + series = point.series, + stateOptions = series.options.states[state || 'normal'] || {}, + markerOptions = defaultPlotOptions[series.type].marker && + series.options.marker, + normalDisabled = markerOptions && markerOptions.enabled === false, + markerStateOptions = ( + markerOptions && + markerOptions.states && + markerOptions.states[state || 'normal'] + ) || {}, + stateDisabled = markerStateOptions.enabled === false, + stateMarkerGraphic = series.stateMarkerGraphic, + pointMarker = point.marker || {}, + chart = series.chart, + halo = series.halo, + haloOptions, + markerAttribs, + hasMarkers = markerOptions && series.markerAttribs, + newSymbol; + + state = state || ''; // empty string + + if ( + // already has this state + (state === point.state && !move) || + + // selected points don't respond to hover + (point.selected && state !== 'select') || + + // series' state options is disabled + (stateOptions.enabled === false) || + + // general point marker's state options is disabled + (state && ( + stateDisabled || + (normalDisabled && markerStateOptions.enabled === false) + )) || + + // individual point marker's state options is disabled + ( + state && + pointMarker.states && + pointMarker.states[state] && + pointMarker.states[state].enabled === false + ) // #1610 + + ) { + return; + } + + if (hasMarkers) { + markerAttribs = series.markerAttribs(point, state); + } + + // Apply hover styles to the existing point + if (point.graphic) { + + if (point.state) { + point.graphic.removeClass('highcharts-point-' + point.state); + } + if (state) { + point.graphic.addClass('highcharts-point-' + state); + } + + /*= if (build.classic) { =*/ + point.graphic.animate( + series.pointAttribs(point, state), + pick( + chart.options.chart.animation, + stateOptions.animation + ) + ); + /*= } =*/ + + if (markerAttribs) { + point.graphic.animate( + markerAttribs, + pick( + chart.options.chart.animation, // Turn off globally + markerStateOptions.animation, + markerOptions.animation + ) + ); + } + + // Zooming in from a range with no markers to a range with markers + if (stateMarkerGraphic) { + stateMarkerGraphic.hide(); + } + } else { + // if a graphic is not applied to each point in the normal state, + // create a shared graphic for the hover state + if (state && markerStateOptions) { + newSymbol = pointMarker.symbol || series.symbol; + + // If the point has another symbol than the previous one, throw + // away the state marker graphic and force a new one (#1459) + if ( + stateMarkerGraphic && + stateMarkerGraphic.currentSymbol !== newSymbol + ) { + stateMarkerGraphic = stateMarkerGraphic.destroy(); + } + + // Add a new state marker graphic + if (!stateMarkerGraphic) { + if (newSymbol) { + series.stateMarkerGraphic = stateMarkerGraphic = + chart.renderer.symbol( + newSymbol, + markerAttribs.x, + markerAttribs.y, + markerAttribs.width, + markerAttribs.height + ) + .add(series.markerGroup); + stateMarkerGraphic.currentSymbol = newSymbol; + } + + // Move the existing graphic + } else { + stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 + x: markerAttribs.x, + y: markerAttribs.y + }); + } + /*= if (build.classic) { =*/ + if (stateMarkerGraphic) { + stateMarkerGraphic.attr(series.pointAttribs(point, state)); + } + /*= } =*/ + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[ + state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? + 'show' : + 'hide' + ](); // #2450 + stateMarkerGraphic.element.point = point; // #4310 + } + } + + // Show me your halo + haloOptions = stateOptions.halo; + if (haloOptions && haloOptions.size) { + if (!halo) { + series.halo = halo = chart.renderer.path() + // #5818, #5903, #6705 + .add((point.graphic || stateMarkerGraphic).parentGroup); + } + halo.show()[move ? 'animate' : 'attr']({ + d: point.haloPath(haloOptions.size) + }); + halo.attr({ + 'class': 'highcharts-halo highcharts-color-' + + pick(point.colorIndex, series.colorIndex) + }); + halo.point = point; // #6055 + + /*= if (build.classic) { =*/ + halo.attr(extend({ + 'fill': point.color || series.color, + 'fill-opacity': haloOptions.opacity, + 'zIndex': -1 // #4929, IE8 added halo above everything + }, haloOptions.attributes)); + /*= } =*/ + + } else if (halo && halo.point && halo.point.haloPath) { + // Animate back to 0 on the current halo point (#6055) + halo.animate( + { d: halo.point.haloPath(0) }, + null, + // Hide after unhovering. The `complete` callback runs in the + // halo's context (#7681). + halo.hide + ); + } + + point.state = state; + + fireEvent(point, 'afterSetState'); + }, + + /** + * Get the path definition for the halo, which is usually a shadow-like + * circle around the currently hovered point. + * @param {Number} size + * The radius of the circular halo. + * @return {Array} The path definition + */ + haloPath: function (size) { + var series = this.series, + chart = series.chart; + + return chart.renderer.symbols.circle( + Math.floor(this.plotX) - size, + this.plotY - size, + size * 2, + size * 2 + ); + } }); /* @@ -869,280 +869,280 @@ extend(Point.prototype, /** @lends Highcharts.Point.prototype */ { */ extend(Series.prototype, /** @lends Highcharts.Series.prototype */ { - /** - * Runs on mouse over the series graphical items. - */ - onMouseOver: function () { - var series = this, - chart = series.chart, - hoverSeries = chart.hoverSeries; - - // set normal state to previous series - if (hoverSeries && hoverSeries !== series) { - hoverSeries.onMouseOut(); - } - - // trigger the event, but to save processing time, - // only if defined - if (series.options.events.mouseOver) { - fireEvent(series, 'mouseOver'); - } - - // hover this - series.setState('hover'); - chart.hoverSeries = series; - }, - - /** - * Runs on mouse out of the series graphical items. - */ - onMouseOut: function () { - // trigger the event only if listeners exist - var series = this, - options = series.options, - chart = series.chart, - tooltip = chart.tooltip, - hoverPoint = chart.hoverPoint; - - // #182, set to null before the mouseOut event fires - chart.hoverSeries = null; - - // trigger mouse out on the point, which must be in this series - if (hoverPoint) { - hoverPoint.onMouseOut(); - } - - // fire the mouse out event - if (series && options.events.mouseOut) { - fireEvent(series, 'mouseOut'); - } - - - // hide the tooltip - if ( - tooltip && - !series.stickyTracking && - (!tooltip.shared || series.noSharedTooltip) - ) { - tooltip.hide(); - } - - // set normal state - series.setState(); - }, - - /** - * Set the state of the series. Called internally on mouse interaction - * operations, but it can also be called directly to visually - * highlight a series. - * - * @param {String} [state] - * Can be either `hover` or undefined to set to normal - * state. - */ - setState: function (state) { - var series = this, - options = series.options, - graph = series.graph, - stateOptions = options.states, - lineWidth = options.lineWidth, - attribs, - i = 0; - - state = state || ''; - - if (series.state !== state) { - - // Toggle class names - each([ - series.group, - series.markerGroup, - series.dataLabelsGroup - ], function (group) { - if (group) { - // Old state - if (series.state) { - group.removeClass('highcharts-series-' + series.state); - } - // New state - if (state) { - group.addClass('highcharts-series-' + state); - } - } - }); - - series.state = state; - - /*= if (build.classic) { =*/ - - if (stateOptions[state] && stateOptions[state].enabled === false) { - return; - } - - if (state) { - lineWidth = ( - stateOptions[state].lineWidth || - lineWidth + (stateOptions[state].lineWidthPlus || 0) - ); // #4035 - } - - if (graph && !graph.dashstyle) { - attribs = { - 'stroke-width': lineWidth - }; - - // Animate the graph stroke-width. By default a quick animation - // to hover, slower to un-hover. - graph.animate( - attribs, - pick( - ( - stateOptions[state || 'normal'] && - stateOptions[state || 'normal'].animation - ), - series.chart.options.chart.animation - ) - ); - while (series['zone-graph-' + i]) { - series['zone-graph-' + i].attr(attribs); - i = i + 1; - } - } - /*= } =*/ - } - }, - - /** - * Show or hide the series. - * - * @param {Boolean} [visible] - * True to show the series, false to hide. If undefined, the - * visibility is toggled. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after the series is altered. If doing - * more operations on the chart, it is a good idea to set redraw to - * false and call {@link Chart#redraw|chart.redraw()} after. - */ - setVisible: function (vis, redraw) { - var series = this, - chart = series.chart, - legendItem = series.legendItem, - showOrHide, - ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, - oldVisibility = series.visible; - - // if called without an argument, toggle visibility - series.visible = - vis = - series.options.visible = - series.userOptions.visible = - vis === undefined ? !oldVisibility : vis; // #5618 - showOrHide = vis ? 'show' : 'hide'; - - // show or hide elements - each([ - 'group', - 'dataLabelsGroup', - 'markerGroup', - 'tracker', - 'tt' - ], function (key) { - if (series[key]) { - series[key][showOrHide](); - } - }); - - - // hide tooltip (#1361) - if ( - chart.hoverSeries === series || - (chart.hoverPoint && chart.hoverPoint.series) === series - ) { - series.onMouseOut(); - } - - - if (legendItem) { - chart.legend.colorizeItem(series, vis); - } - - - // rescale or adapt to resized chart - series.isDirty = true; - // in a stack, all other series are affected - if (series.options.stacking) { - each(chart.series, function (otherSeries) { - if (otherSeries.options.stacking && otherSeries.visible) { - otherSeries.isDirty = true; - } - }); - } - - // show or hide linked series - each(series.linkedSeries, function (otherSeries) { - otherSeries.setVisible(vis, false); - }); - - if (ignoreHiddenSeries) { - chart.isDirtyBox = true; - } - if (redraw !== false) { - chart.redraw(); - } - - fireEvent(series, showOrHide); - }, - - /** - * Show the series if hidden. - * - * @sample highcharts/members/series-hide/ - * Toggle visibility from a button - */ - show: function () { - this.setVisible(true); - }, - - /** - * Hide the series if visible. If the {@link - * https://api.highcharts.com/highcharts/chart.ignoreHiddenSeries| - * chart.ignoreHiddenSeries} option is true, the chart is redrawn without - * this series. - * - * @sample highcharts/members/series-hide/ - * Toggle visibility from a button - */ - hide: function () { - this.setVisible(false); - }, - - - /** - * Select or unselect the series. This means its {@link - * Highcharts.Series.selected|selected} property is set, the checkbox in the - * legend is toggled and when selected, the series is returned by the - * {@link Highcharts.Chart#getSelectedSeries} function. - * - * @param {Boolean} [selected] - * True to select the series, false to unselect. If undefined, the - * selection state is toggled. - * - * @sample highcharts/members/series-select/ - * Select a series from a button - */ - select: function (selected) { - var series = this; - - series.selected = selected = (selected === undefined) ? - !series.selected : - selected; - - if (series.checkbox) { - series.checkbox.checked = selected; - } - - fireEvent(series, selected ? 'select' : 'unselect'); - }, - - drawTracker: TrackerMixin.drawTrackerGraph + /** + * Runs on mouse over the series graphical items. + */ + onMouseOver: function () { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + // set normal state to previous series + if (hoverSeries && hoverSeries !== series) { + hoverSeries.onMouseOut(); + } + + // trigger the event, but to save processing time, + // only if defined + if (series.options.events.mouseOver) { + fireEvent(series, 'mouseOver'); + } + + // hover this + series.setState('hover'); + chart.hoverSeries = series; + }, + + /** + * Runs on mouse out of the series graphical items. + */ + onMouseOut: function () { + // trigger the event only if listeners exist + var series = this, + options = series.options, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // #182, set to null before the mouseOut event fires + chart.hoverSeries = null; + + // trigger mouse out on the point, which must be in this series + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + // fire the mouse out event + if (series && options.events.mouseOut) { + fireEvent(series, 'mouseOut'); + } + + + // hide the tooltip + if ( + tooltip && + !series.stickyTracking && + (!tooltip.shared || series.noSharedTooltip) + ) { + tooltip.hide(); + } + + // set normal state + series.setState(); + }, + + /** + * Set the state of the series. Called internally on mouse interaction + * operations, but it can also be called directly to visually + * highlight a series. + * + * @param {String} [state] + * Can be either `hover` or undefined to set to normal + * state. + */ + setState: function (state) { + var series = this, + options = series.options, + graph = series.graph, + stateOptions = options.states, + lineWidth = options.lineWidth, + attribs, + i = 0; + + state = state || ''; + + if (series.state !== state) { + + // Toggle class names + each([ + series.group, + series.markerGroup, + series.dataLabelsGroup + ], function (group) { + if (group) { + // Old state + if (series.state) { + group.removeClass('highcharts-series-' + series.state); + } + // New state + if (state) { + group.addClass('highcharts-series-' + state); + } + } + }); + + series.state = state; + + /*= if (build.classic) { =*/ + + if (stateOptions[state] && stateOptions[state].enabled === false) { + return; + } + + if (state) { + lineWidth = ( + stateOptions[state].lineWidth || + lineWidth + (stateOptions[state].lineWidthPlus || 0) + ); // #4035 + } + + if (graph && !graph.dashstyle) { + attribs = { + 'stroke-width': lineWidth + }; + + // Animate the graph stroke-width. By default a quick animation + // to hover, slower to un-hover. + graph.animate( + attribs, + pick( + ( + stateOptions[state || 'normal'] && + stateOptions[state || 'normal'].animation + ), + series.chart.options.chart.animation + ) + ); + while (series['zone-graph-' + i]) { + series['zone-graph-' + i].attr(attribs); + i = i + 1; + } + } + /*= } =*/ + } + }, + + /** + * Show or hide the series. + * + * @param {Boolean} [visible] + * True to show the series, false to hide. If undefined, the + * visibility is toggled. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the series is altered. If doing + * more operations on the chart, it is a good idea to set redraw to + * false and call {@link Chart#redraw|chart.redraw()} after. + */ + setVisible: function (vis, redraw) { + var series = this, + chart = series.chart, + legendItem = series.legendItem, + showOrHide, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = + vis = + series.options.visible = + series.userOptions.visible = + vis === undefined ? !oldVisibility : vis; // #5618 + showOrHide = vis ? 'show' : 'hide'; + + // show or hide elements + each([ + 'group', + 'dataLabelsGroup', + 'markerGroup', + 'tracker', + 'tt' + ], function (key) { + if (series[key]) { + series[key][showOrHide](); + } + }); + + + // hide tooltip (#1361) + if ( + chart.hoverSeries === series || + (chart.hoverPoint && chart.hoverPoint.series) === series + ) { + series.onMouseOut(); + } + + + if (legendItem) { + chart.legend.colorizeItem(series, vis); + } + + + // rescale or adapt to resized chart + series.isDirty = true; + // in a stack, all other series are affected + if (series.options.stacking) { + each(chart.series, function (otherSeries) { + if (otherSeries.options.stacking && otherSeries.visible) { + otherSeries.isDirty = true; + } + }); + } + + // show or hide linked series + each(series.linkedSeries, function (otherSeries) { + otherSeries.setVisible(vis, false); + }); + + if (ignoreHiddenSeries) { + chart.isDirtyBox = true; + } + if (redraw !== false) { + chart.redraw(); + } + + fireEvent(series, showOrHide); + }, + + /** + * Show the series if hidden. + * + * @sample highcharts/members/series-hide/ + * Toggle visibility from a button + */ + show: function () { + this.setVisible(true); + }, + + /** + * Hide the series if visible. If the {@link + * https://api.highcharts.com/highcharts/chart.ignoreHiddenSeries| + * chart.ignoreHiddenSeries} option is true, the chart is redrawn without + * this series. + * + * @sample highcharts/members/series-hide/ + * Toggle visibility from a button + */ + hide: function () { + this.setVisible(false); + }, + + + /** + * Select or unselect the series. This means its {@link + * Highcharts.Series.selected|selected} property is set, the checkbox in the + * legend is toggled and when selected, the series is returned by the + * {@link Highcharts.Chart#getSelectedSeries} function. + * + * @param {Boolean} [selected] + * True to select the series, false to unselect. If undefined, the + * selection state is toggled. + * + * @sample highcharts/members/series-select/ + * Select a series from a button + */ + select: function (selected) { + var series = this; + + series.selected = selected = (selected === undefined) ? + !series.selected : + selected; + + if (series.checkbox) { + series.checkbox.checked = selected; + } + + fireEvent(series, selected ? 'select' : 'unselect'); + }, + + drawTracker: TrackerMixin.drawTrackerGraph }); diff --git a/js/parts/Legend.js b/js/parts/Legend.js index 7f9df602f7b..2360f412c55 100644 --- a/js/parts/Legend.js +++ b/js/parts/Legend.js @@ -8,1020 +8,1020 @@ import Highcharts from './Globals.js'; import './Utilities.js'; var H = Highcharts, - addEvent = H.addEvent, - css = H.css, - discardElement = H.discardElement, - defined = H.defined, - each = H.each, - fireEvent = H.fireEvent, - isFirefox = H.isFirefox, - marginNames = H.marginNames, - merge = H.merge, - pick = H.pick, - setAnimation = H.setAnimation, - stableSort = H.stableSort, - win = H.win, - wrap = H.wrap; + addEvent = H.addEvent, + css = H.css, + discardElement = H.discardElement, + defined = H.defined, + each = H.each, + fireEvent = H.fireEvent, + isFirefox = H.isFirefox, + marginNames = H.marginNames, + merge = H.merge, + pick = H.pick, + setAnimation = H.setAnimation, + stableSort = H.stableSort, + win = H.win, + wrap = H.wrap; /** * The overview of the chart's series. The legend object is instanciated * internally in the chart constructor, and available from `chart.legend`. Each * chart has only one legend. - * + * * @class */ Highcharts.Legend = function (chart, options) { - this.init(chart, options); + this.init(chart, options); }; Highcharts.Legend.prototype = { - /** - * Initialize the legend. - * - * @private - */ - init: function (chart, options) { - - this.chart = chart; - - this.setOptions(options); - - if (options.enabled) { - - // Render it - this.render(); - - // move checkboxes - addEvent(this.chart, 'endResize', function () { - this.legend.positionCheckboxes(); - }); - } - }, - - setOptions: function (options) { - - var padding = pick(options.padding, 8); - - this.options = options; - - /*= if (build.classic) { =*/ - this.itemStyle = options.itemStyle; - this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle); - /*= } =*/ - this.itemMarginTop = options.itemMarginTop || 0; - this.padding = padding; - this.initialItemY = padding - 5; // 5 is pixels above the text - this.symbolWidth = pick(options.symbolWidth, 16); - this.pages = []; - - }, - - /** - * Update the legend with new options. Equivalent to running `chart.update` - * with a legend configuration option. - * @param {LegendOptions} options - * Legend options. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart. - * - * @sample highcharts/legend/legend-update/ - * Legend update - */ - update: function (options, redraw) { - var chart = this.chart; - - this.setOptions(merge(true, this.options, options)); - this.destroy(); - chart.isDirtyLegend = chart.isDirtyBox = true; - if (pick(redraw, true)) { - chart.redraw(); - } - - fireEvent(this, 'afterUpdate'); - }, - - /** - * Set the colors for the legend item. - * - * @private - * @param {Series|Point} item - * A Series or Point instance - * @param {Boolean} visible - * Dimmed or colored - */ - colorizeItem: function (item, visible) { - item.legendGroup[visible ? 'removeClass' : 'addClass']( - 'highcharts-legend-item-hidden' - ); - - /*= if (build.classic) { =*/ - var legend = this, - options = legend.options, - legendItem = item.legendItem, - legendLine = item.legendLine, - legendSymbol = item.legendSymbol, - hiddenColor = legend.itemHiddenStyle.color, - textColor = visible ? options.itemStyle.color : hiddenColor, - symbolColor = visible ? (item.color || hiddenColor) : hiddenColor, - markerOptions = item.options && item.options.marker, - symbolAttr = { fill: symbolColor }; - - if (legendItem) { - legendItem.css({ - fill: textColor, - color: textColor // #1553, oldIE - }); - } - if (legendLine) { - legendLine.attr({ stroke: symbolColor }); - } - - if (legendSymbol) { - - // Apply marker options - if (markerOptions && legendSymbol.isMarker) { // #585 - symbolAttr = item.pointAttribs(); - if (!visible) { - symbolAttr.stroke = symbolAttr.fill = hiddenColor; // #6769 - } - } - - legendSymbol.attr(symbolAttr); - } - /*= } =*/ - - fireEvent(this, 'afterColorizeItem', { item: item, visible: visible }); - }, - - /** - * Position the legend item. - * - * @private - * @param {Series|Point} item - * The item to position - */ - positionItem: function (item) { - var legend = this, - options = legend.options, - symbolPadding = options.symbolPadding, - ltr = !options.rtl, - legendItemPos = item._legendItemPos, - itemX = legendItemPos[0], - itemY = legendItemPos[1], - checkbox = item.checkbox, - legendGroup = item.legendGroup; - - if (legendGroup && legendGroup.element) { - legendGroup.translate( - ltr ? - itemX : - legend.legendWidth - itemX - 2 * symbolPadding - 4, - itemY - ); - } - - if (checkbox) { - checkbox.x = itemX; - checkbox.y = itemY; - } - }, - - /** - * Destroy a single legend item, used internally on removing series items. - * - * @param {Series|Point} item - * The item to remove - */ - destroyItem: function (item) { - var checkbox = item.checkbox; - - // destroy SVG elements - each( - ['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], - function (key) { - if (item[key]) { - item[key] = item[key].destroy(); - } - } - ); - - if (checkbox) { - discardElement(item.checkbox); - } - }, - - /** - * Destroy the legend. Used internally. To reflow objects, `chart.redraw` - * must be called after destruction. - */ - destroy: function () { - function destroyItems(key) { - if (this[key]) { - this[key] = this[key].destroy(); - } - } - - // Destroy items - each(this.getAllItems(), function (item) { - each(['legendItem', 'legendGroup'], destroyItems, item); - }); - - // Destroy legend elements - each([ - 'clipRect', - 'up', - 'down', - 'pager', - 'nav', - 'box', - 'title', - 'group' - ], destroyItems, this); - this.display = null; // Reset in .render on update. - }, - - /** - * Position the checkboxes after the width is determined. - * - * @private - */ - positionCheckboxes: function () { - var alignAttr = this.group && this.group.alignAttr, - translateY, - clipHeight = this.clipHeight || this.legendHeight, - titleHeight = this.titleHeight; - - if (alignAttr) { - translateY = alignAttr.translateY; - each(this.allItems, function (item) { - var checkbox = item.checkbox, - top; - - if (checkbox) { - top = translateY + titleHeight + checkbox.y + - (this.scrollOffset || 0) + 3; - css(checkbox, { - left: (alignAttr.translateX + item.checkboxOffset + - checkbox.x - 20) + 'px', - top: top + 'px', - display: top > translateY - 6 && top < translateY + - clipHeight - 6 ? '' : 'none' - }); - } - }, this); - } - }, - - /** - * Render the legend title on top of the legend. - * - * @private - */ - renderTitle: function () { - var options = this.options, - padding = this.padding, - titleOptions = options.title, - titleHeight = 0, - bBox; - - if (titleOptions.text) { - if (!this.title) { - this.title = this.chart.renderer.label( - titleOptions.text, - padding - 3, - padding - 4, - null, - null, - null, - options.useHTML, - null, - 'legend-title' - ) - .attr({ zIndex: 1 }) - /*= if (build.classic) { =*/ - .css(titleOptions.style) - /*= } =*/ - .add(this.group); - } - bBox = this.title.getBBox(); - titleHeight = bBox.height; - this.offsetWidth = bBox.width; // #1717 - this.contentGroup.attr({ translateY: titleHeight }); - } - this.titleHeight = titleHeight; - }, - - /** - * Set the legend item text. - * - * @param {Series|Point} item - * The item for which to update the text in the legend. - */ - setText: function (item) { - var options = this.options; - item.legendItem.attr({ - text: options.labelFormat ? - H.format(options.labelFormat, item, this.chart.time) : - options.labelFormatter.call(item) - }); - }, - - /** - * Render a single specific legend item. Called internally from the `render` - * function. - * - * @private - * @param {Series|Point} item - * The item to render. - */ - renderItem: function (item) { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - options = legend.options, - horizontal = options.layout === 'horizontal', - symbolWidth = legend.symbolWidth, - symbolPadding = options.symbolPadding, - /*= if (build.classic) { =*/ - itemStyle = legend.itemStyle, - itemHiddenStyle = legend.itemHiddenStyle, - /*= } =*/ - itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, - ltr = !options.rtl, - bBox, - li = item.legendItem, - isSeries = !item.series, - series = !isSeries && item.series.drawLegendSymbol ? - item.series : - item, - seriesOptions = series.options, - showCheckbox = legend.createCheckboxForItem && - seriesOptions && - seriesOptions.showCheckbox, - // full width minus text width - itemExtraWidth = symbolWidth + symbolPadding + itemDistance + - (showCheckbox ? 20 : 0), - useHTML = options.useHTML, - fontSize = 12, - itemClassName = item.options.className; - - if (!li) { // generate it once, later move it - - // Generate the group box, a group to hold the symbol and text. Text - // is to be appended in Legend class. - item.legendGroup = renderer.g('legend-item') - .addClass( - 'highcharts-' + series.type + '-series ' + - 'highcharts-color-' + item.colorIndex + - (itemClassName ? ' ' + itemClassName : '') + - (isSeries ? ' highcharts-series-' + item.index : '') - ) - .attr({ zIndex: 1 }) - .add(legend.scrollGroup); - - // Generate the list item text and add it to the group - item.legendItem = li = renderer.text( - '', - ltr ? symbolWidth + symbolPadding : -symbolPadding, - legend.baseline || 0, - useHTML - ) - /*= if (build.classic) { =*/ - // merge to prevent modifying original (#1021) - .css(merge(item.visible ? itemStyle : itemHiddenStyle)) - /*= } =*/ - .attr({ - align: ltr ? 'left' : 'right', - zIndex: 2 - }) - .add(item.legendGroup); - - // Get the baseline for the first item - the font size is equal for - // all - if (!legend.baseline) { - /*= if (build.classic) { =*/ - fontSize = itemStyle.fontSize; - /*= } =*/ - legend.fontMetrics = renderer.fontMetrics( - fontSize, - li - ); - legend.baseline = - legend.fontMetrics.f + 3 + legend.itemMarginTop; - li.attr('y', legend.baseline); - } - - // Draw the legend symbol inside the group box - legend.symbolHeight = options.symbolHeight || legend.fontMetrics.f; - series.drawLegendSymbol(legend, item); - - if (legend.setItemEvents) { - legend.setItemEvents(item, li, useHTML); - } - - // add the HTML checkbox on top - if (showCheckbox) { - legend.createCheckboxForItem(item); - } - } - - // Colorize the items - legend.colorizeItem(item, item.visible); - - // Take care of max width and text overflow (#6659) - /*= if (build.classic) { =*/ - if (!itemStyle.width) { - /*= } =*/ - li.css({ - width: ( - options.itemWidth || - options.width || - chart.spacingBox.width - ) - itemExtraWidth - }); - /*= if (build.classic) { =*/ - } - /*= } =*/ - - // Always update the text - legend.setText(item); - - // calculate the positions for the next line - bBox = li.getBBox(); - - item.itemWidth = item.checkboxOffset = - options.itemWidth || - item.legendItemWidth || - bBox.width + itemExtraWidth; - legend.maxItemWidth = Math.max(legend.maxItemWidth, item.itemWidth); - legend.totalItemWidth += item.itemWidth; - legend.itemHeight = item.itemHeight = Math.round( - item.legendItemHeight || bBox.height || legend.symbolHeight - ); - }, - - /** - * Get the position of the item in the layout. We now know the - * maxItemWidth from the previous loop. - * - * @private - */ - layoutItem: function (item) { - - var options = this.options, - padding = this.padding, - horizontal = options.layout === 'horizontal', - itemHeight = item.itemHeight, - itemMarginBottom = options.itemMarginBottom || 0, - itemMarginTop = this.itemMarginTop, - itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, - widthOption = options.width, - maxLegendWidth = widthOption || ( - this.chart.spacingBox.width - 2 * padding - options.x - ), - itemWidth = ( - options.alignColumns && - this.totalItemWidth > maxLegendWidth - ) ? - this.maxItemWidth : - item.itemWidth; - - // If the item exceeds the width, start a new line - if ( - horizontal && - this.itemX - padding + itemWidth > maxLegendWidth - ) { - this.itemX = padding; - this.itemY += itemMarginTop + this.lastLineHeight + - itemMarginBottom; - this.lastLineHeight = 0; // reset for next line (#915, #3976) - } - - // Set the edge positions - this.lastItemY = itemMarginTop + this.itemY + itemMarginBottom; - this.lastLineHeight = Math.max( // #915 - itemHeight, - this.lastLineHeight - ); - - // cache the position of the newly generated or reordered items - item._legendItemPos = [this.itemX, this.itemY]; - - // advance - if (horizontal) { - this.itemX += itemWidth; - - } else { - this.itemY += itemMarginTop + itemHeight + itemMarginBottom; - this.lastLineHeight = itemHeight; - } - - // the width of the widest item - this.offsetWidth = widthOption || Math.max( - ( - horizontal ? this.itemX - padding - (item.checkbox ? - // decrease by itemDistance only when no checkbox #4853 - 0 : - itemDistance - ) : itemWidth - ) + padding, - this.offsetWidth - ); - }, - - /** - * Get all items, which is one item per series for most series and one - * item per point for pie series and its derivatives. - * - * @return {Array.} - * The current items in the legend. - */ - getAllItems: function () { - var allItems = []; - each(this.chart.series, function (series) { - var seriesOptions = series && series.options; - - // Handle showInLegend. If the series is linked to another series, - // defaults to false. - if (series && pick( - seriesOptions.showInLegend, - !defined(seriesOptions.linkedTo) ? undefined : false, true - )) { - - // Use points or series for the legend item depending on - // legendType - allItems = allItems.concat( - series.legendItems || - ( - seriesOptions.legendType === 'point' ? - series.data : - series - ) - ); - } - }); - - fireEvent(this, 'afterGetAllItems', { allItems: allItems }); - - return allItems; - }, - - /** - * Get a short, three letter string reflecting the alignment and layout. - * - * @private - * @return {String} The alignment, empty string if floating - */ - getAlignment: function () { - var options = this.options; - - // Use the first letter of each alignment option in order to detect - // the side. (#4189 - use charAt(x) notation instead of [x] for IE7) - return options.floating ? '' : ( - options.align.charAt(0) + - options.verticalAlign.charAt(0) + - options.layout.charAt(0) - ); - }, - - /** - * Adjust the chart margins by reserving space for the legend on only one - * side of the chart. If the position is set to a corner, top or bottom is - * reserved for horizontal legends and left or right for vertical ones. - * - * @private - */ - adjustMargins: function (margin, spacing) { - var chart = this.chart, - options = this.options, - alignment = this.getAlignment(); - - if (alignment) { - - each([ - /(lth|ct|rth)/, - /(rtv|rm|rbv)/, - /(rbh|cb|lbh)/, - /(lbv|lm|ltv)/ - ], function (alignments, side) { - if (alignments.test(alignment) && !defined(margin[side])) { - - // Now we have detected on which side of the chart we should - // reserve space for the legend - chart[marginNames[side]] = Math.max( - chart[marginNames[side]], - ( - chart.legend[ - (side + 1) % 2 ? 'legendHeight' : 'legendWidth' - ] + - [1, -1, -1, 1][side] * options[ - (side % 2) ? 'x' : 'y' - ] + - pick(options.margin, 12) + - spacing[side] + - ( - side === 0 && - chart.options.title.margin !== undefined ? - chart.titleOffset + - chart.options.title.margin : - 0 - ) // #7428, #7894 - ) - ); - } - }); - } - }, - - /** - * Render the legend. This method can be called both before and after - * `chart.render`. If called after, it will only rearrange items instead - * of creating new ones. Called internally on initial render and after - * redraws. - */ - render: function () { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - legendGroup = legend.group, - allItems, - display, - legendWidth, - legendHeight, - box = legend.box, - options = legend.options, - padding = legend.padding, - alignTo; - - legend.itemX = padding; - legend.itemY = legend.initialItemY; - legend.offsetWidth = 0; - legend.lastItemY = 0; - - if (!legendGroup) { - legend.group = legendGroup = renderer.g('legend') - .attr({ zIndex: 7 }) - .add(); - legend.contentGroup = renderer.g() - .attr({ zIndex: 1 }) // above background - .add(legendGroup); - legend.scrollGroup = renderer.g() - .add(legend.contentGroup); - } - - legend.renderTitle(); - - // add each series or point - allItems = legend.getAllItems(); - - // sort by legendIndex - stableSort(allItems, function (a, b) { - return ((a.options && a.options.legendIndex) || 0) - - ((b.options && b.options.legendIndex) || 0); - }); - - // reversed legend - if (options.reversed) { - allItems.reverse(); - } - - legend.allItems = allItems; - legend.display = display = !!allItems.length; - - // Render the items. First we run a loop to set the text and properties - // and read all the bounding boxes. The next loop computes the item - // positions based on the bounding boxes. - legend.lastLineHeight = 0; - legend.maxItemWidth = 0; - legend.totalItemWidth = 0; - legend.itemHeight = 0; - each(allItems, legend.renderItem, legend); - each(allItems, legend.layoutItem, legend); - - // Get the box - legendWidth = (options.width || legend.offsetWidth) + padding; - legendHeight = legend.lastItemY + legend.lastLineHeight + - legend.titleHeight; - legendHeight = legend.handleOverflow(legendHeight); - legendHeight += padding; - - // Draw the border and/or background - if (!box) { - legend.box = box = renderer.rect() - .addClass('highcharts-legend-box') - .attr({ - r: options.borderRadius - }) - .add(legendGroup); - box.isNew = true; - } - - /*= if (build.classic) { =*/ - // Presentational - box - .attr({ - stroke: options.borderColor, - 'stroke-width': options.borderWidth || 0, - fill: options.backgroundColor || 'none' - }) - .shadow(options.shadow); - /*= } =*/ - - if (legendWidth > 0 && legendHeight > 0) { - box[box.isNew ? 'attr' : 'animate']( - box.crisp.call({}, { // #7260 - x: 0, - y: 0, - width: legendWidth, - height: legendHeight - }, box.strokeWidth()) - ); - box.isNew = false; - } - - // hide the border if no items - box[display ? 'show' : 'hide'](); - - /*= if (!build.classic) { =*/ - // Open for responsiveness - if (legendGroup.getStyle('display') === 'none') { - legendWidth = legendHeight = 0; - } - /*= } =*/ - - legend.legendWidth = legendWidth; - legend.legendHeight = legendHeight; - - // Now that the legend width and height are established, put the items - // in the final position - each(allItems, legend.positionItem, legend); - - if (display) { - // If aligning to the top and the layout is horizontal, adjust for - // the title (#7428) - alignTo = chart.spacingBox; - if (/(lth|ct|rth)/.test(legend.getAlignment())) { - alignTo = merge(alignTo, { - y: alignTo.y + chart.titleOffset + - chart.options.title.margin - }); - } - - legendGroup.align(merge(options, { - width: legendWidth, - height: legendHeight - }), true, alignTo); - } - - if (!chart.isResizing) { - this.positionCheckboxes(); - } - }, - - /** - * Set up the overflow handling by adding navigation with up and down arrows - * below the legend. - * - * @private - */ - handleOverflow: function (legendHeight) { - var legend = this, - chart = this.chart, - renderer = chart.renderer, - options = this.options, - optionsY = options.y, - alignTop = options.verticalAlign === 'top', - padding = this.padding, - spaceHeight = chart.spacingBox.height + - (alignTop ? -optionsY : optionsY) - padding, - maxHeight = options.maxHeight, - clipHeight, - clipRect = this.clipRect, - navOptions = options.navigation, - animation = pick(navOptions.animation, true), - arrowSize = navOptions.arrowSize || 12, - nav = this.nav, - pages = this.pages, - lastY, - allItems = this.allItems, - clipToHeight = function (height) { - if (typeof height === 'number') { - clipRect.attr({ - height: height - }); - } else if (clipRect) { // Reset (#5912) - legend.clipRect = clipRect.destroy(); - legend.contentGroup.clip(); - } - - // useHTML - if (legend.contentGroup.div) { - legend.contentGroup.div.style.clip = height ? - 'rect(' + padding + 'px,9999px,' + - (padding + height) + 'px,0)' : - 'auto'; - } - }; - - - // Adjust the height - if ( - options.layout === 'horizontal' && - options.verticalAlign !== 'middle' && - !options.floating - ) { - spaceHeight /= 2; - } - if (maxHeight) { - spaceHeight = Math.min(spaceHeight, maxHeight); - } - - // Reset the legend height and adjust the clipping rectangle - pages.length = 0; - if (legendHeight > spaceHeight && navOptions.enabled !== false) { - - this.clipHeight = clipHeight = - Math.max(spaceHeight - 20 - this.titleHeight - padding, 0); - this.currentPage = pick(this.currentPage, 1); - this.fullHeight = legendHeight; - - // Fill pages with Y positions so that the top of each a legend item - // defines the scroll top for each page (#2098) - each(allItems, function (item, i) { - var y = item._legendItemPos[1], - h = Math.round(item.legendItem.getBBox().height), - len = pages.length; - - if (!len || (y - pages[len - 1] > clipHeight && - (lastY || y) !== pages[len - 1])) { - pages.push(lastY || y); - len++; - } - - // Keep track of which page each item is on - item.pageIx = len - 1; - if (lastY) { - allItems[i - 1].pageIx = len - 1; - } - - if (i === allItems.length - 1 && - y + h - pages[len - 1] > clipHeight) { - pages.push(y); - item.pageIx = len; - } - if (y !== lastY) { - lastY = y; - } - }); - - // Only apply clipping if needed. Clipping causes blurred legend in - // PDF export (#1787) - if (!clipRect) { - clipRect = legend.clipRect = - renderer.clipRect(0, padding, 9999, 0); - legend.contentGroup.clip(clipRect); - } - - clipToHeight(clipHeight); - - // Add navigation elements - if (!nav) { - this.nav = nav = renderer.g() - .attr({ zIndex: 1 }) - .add(this.group); - - this.up = renderer - .symbol( - 'triangle', - 0, - 0, - arrowSize, - arrowSize - ) - .on('click', function () { - legend.scroll(-1, animation); - }) - .add(nav); - - this.pager = renderer.text('', 15, 10) - .addClass('highcharts-legend-navigation') - /*= if (build.classic) { =*/ - .css(navOptions.style) - /*= } =*/ - .add(nav); - - this.down = renderer - .symbol( - 'triangle-down', - 0, - 0, - arrowSize, - arrowSize - ) - .on('click', function () { - legend.scroll(1, animation); - }) - .add(nav); - } - - // Set initial position - legend.scroll(0); - - legendHeight = spaceHeight; - - // Reset - } else if (nav) { - clipToHeight(); - this.nav = nav.destroy(); // #6322 - this.scrollGroup.attr({ - translateY: 1 - }); - this.clipHeight = 0; // #1379 - } - - return legendHeight; - }, - - /** - * Scroll the legend by a number of pages. - * @param {Number} scrollBy - * The number of pages to scroll. - * @param {AnimationOptions} animation - * Whether and how to apply animation. - */ - scroll: function (scrollBy, animation) { - var pages = this.pages, - pageCount = pages.length, - currentPage = this.currentPage + scrollBy, - clipHeight = this.clipHeight, - navOptions = this.options.navigation, - pager = this.pager, - padding = this.padding; - - // When resizing while looking at the last page - if (currentPage > pageCount) { - currentPage = pageCount; - } - - if (currentPage > 0) { - - if (animation !== undefined) { - setAnimation(animation, this.chart); - } - - this.nav.attr({ - translateX: padding, - translateY: clipHeight + this.padding + 7 + this.titleHeight, - visibility: 'visible' - }); - this.up.attr({ - 'class': currentPage === 1 ? - 'highcharts-legend-nav-inactive' : - 'highcharts-legend-nav-active' - }); - pager.attr({ - text: currentPage + '/' + pageCount - }); - this.down.attr({ - 'x': 18 + this.pager.getBBox().width, // adjust to text width - 'class': currentPage === pageCount ? - 'highcharts-legend-nav-inactive' : - 'highcharts-legend-nav-active' - }); - - /*= if (build.classic) { =*/ - this.up - .attr({ - fill: currentPage === 1 ? - navOptions.inactiveColor : - navOptions.activeColor - }) - .css({ - cursor: currentPage === 1 ? 'default' : 'pointer' - }); - this.down - .attr({ - fill: currentPage === pageCount ? - navOptions.inactiveColor : - navOptions.activeColor - }) - .css({ - cursor: currentPage === pageCount ? 'default' : 'pointer' - }); - /*= } =*/ - - this.scrollOffset = -pages[currentPage - 1] + this.initialItemY; - - this.scrollGroup.animate({ - translateY: this.scrollOffset - }); - - this.currentPage = currentPage; - this.positionCheckboxes(); - } - - } + /** + * Initialize the legend. + * + * @private + */ + init: function (chart, options) { + + this.chart = chart; + + this.setOptions(options); + + if (options.enabled) { + + // Render it + this.render(); + + // move checkboxes + addEvent(this.chart, 'endResize', function () { + this.legend.positionCheckboxes(); + }); + } + }, + + setOptions: function (options) { + + var padding = pick(options.padding, 8); + + this.options = options; + + /*= if (build.classic) { =*/ + this.itemStyle = options.itemStyle; + this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle); + /*= } =*/ + this.itemMarginTop = options.itemMarginTop || 0; + this.padding = padding; + this.initialItemY = padding - 5; // 5 is pixels above the text + this.symbolWidth = pick(options.symbolWidth, 16); + this.pages = []; + + }, + + /** + * Update the legend with new options. Equivalent to running `chart.update` + * with a legend configuration option. + * @param {LegendOptions} options + * Legend options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart. + * + * @sample highcharts/legend/legend-update/ + * Legend update + */ + update: function (options, redraw) { + var chart = this.chart; + + this.setOptions(merge(true, this.options, options)); + this.destroy(); + chart.isDirtyLegend = chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + + fireEvent(this, 'afterUpdate'); + }, + + /** + * Set the colors for the legend item. + * + * @private + * @param {Series|Point} item + * A Series or Point instance + * @param {Boolean} visible + * Dimmed or colored + */ + colorizeItem: function (item, visible) { + item.legendGroup[visible ? 'removeClass' : 'addClass']( + 'highcharts-legend-item-hidden' + ); + + /*= if (build.classic) { =*/ + var legend = this, + options = legend.options, + legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = legend.itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? (item.color || hiddenColor) : hiddenColor, + markerOptions = item.options && item.options.marker, + symbolAttr = { fill: symbolColor }; + + if (legendItem) { + legendItem.css({ + fill: textColor, + color: textColor // #1553, oldIE + }); + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } + + if (legendSymbol) { + + // Apply marker options + if (markerOptions && legendSymbol.isMarker) { // #585 + symbolAttr = item.pointAttribs(); + if (!visible) { + symbolAttr.stroke = symbolAttr.fill = hiddenColor; // #6769 + } + } + + legendSymbol.attr(symbolAttr); + } + /*= } =*/ + + fireEvent(this, 'afterColorizeItem', { item: item, visible: visible }); + }, + + /** + * Position the legend item. + * + * @private + * @param {Series|Point} item + * The item to position + */ + positionItem: function (item) { + var legend = this, + options = legend.options, + symbolPadding = options.symbolPadding, + ltr = !options.rtl, + legendItemPos = item._legendItemPos, + itemX = legendItemPos[0], + itemY = legendItemPos[1], + checkbox = item.checkbox, + legendGroup = item.legendGroup; + + if (legendGroup && legendGroup.element) { + legendGroup.translate( + ltr ? + itemX : + legend.legendWidth - itemX - 2 * symbolPadding - 4, + itemY + ); + } + + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + }, + + /** + * Destroy a single legend item, used internally on removing series items. + * + * @param {Series|Point} item + * The item to remove + */ + destroyItem: function (item) { + var checkbox = item.checkbox; + + // destroy SVG elements + each( + ['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], + function (key) { + if (item[key]) { + item[key] = item[key].destroy(); + } + } + ); + + if (checkbox) { + discardElement(item.checkbox); + } + }, + + /** + * Destroy the legend. Used internally. To reflow objects, `chart.redraw` + * must be called after destruction. + */ + destroy: function () { + function destroyItems(key) { + if (this[key]) { + this[key] = this[key].destroy(); + } + } + + // Destroy items + each(this.getAllItems(), function (item) { + each(['legendItem', 'legendGroup'], destroyItems, item); + }); + + // Destroy legend elements + each([ + 'clipRect', + 'up', + 'down', + 'pager', + 'nav', + 'box', + 'title', + 'group' + ], destroyItems, this); + this.display = null; // Reset in .render on update. + }, + + /** + * Position the checkboxes after the width is determined. + * + * @private + */ + positionCheckboxes: function () { + var alignAttr = this.group && this.group.alignAttr, + translateY, + clipHeight = this.clipHeight || this.legendHeight, + titleHeight = this.titleHeight; + + if (alignAttr) { + translateY = alignAttr.translateY; + each(this.allItems, function (item) { + var checkbox = item.checkbox, + top; + + if (checkbox) { + top = translateY + titleHeight + checkbox.y + + (this.scrollOffset || 0) + 3; + css(checkbox, { + left: (alignAttr.translateX + item.checkboxOffset + + checkbox.x - 20) + 'px', + top: top + 'px', + display: top > translateY - 6 && top < translateY + + clipHeight - 6 ? '' : 'none' + }); + } + }, this); + } + }, + + /** + * Render the legend title on top of the legend. + * + * @private + */ + renderTitle: function () { + var options = this.options, + padding = this.padding, + titleOptions = options.title, + titleHeight = 0, + bBox; + + if (titleOptions.text) { + if (!this.title) { + this.title = this.chart.renderer.label( + titleOptions.text, + padding - 3, + padding - 4, + null, + null, + null, + options.useHTML, + null, + 'legend-title' + ) + .attr({ zIndex: 1 }) + /*= if (build.classic) { =*/ + .css(titleOptions.style) + /*= } =*/ + .add(this.group); + } + bBox = this.title.getBBox(); + titleHeight = bBox.height; + this.offsetWidth = bBox.width; // #1717 + this.contentGroup.attr({ translateY: titleHeight }); + } + this.titleHeight = titleHeight; + }, + + /** + * Set the legend item text. + * + * @param {Series|Point} item + * The item for which to update the text in the legend. + */ + setText: function (item) { + var options = this.options; + item.legendItem.attr({ + text: options.labelFormat ? + H.format(options.labelFormat, item, this.chart.time) : + options.labelFormatter.call(item) + }); + }, + + /** + * Render a single specific legend item. Called internally from the `render` + * function. + * + * @private + * @param {Series|Point} item + * The item to render. + */ + renderItem: function (item) { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + options = legend.options, + horizontal = options.layout === 'horizontal', + symbolWidth = legend.symbolWidth, + symbolPadding = options.symbolPadding, + /*= if (build.classic) { =*/ + itemStyle = legend.itemStyle, + itemHiddenStyle = legend.itemHiddenStyle, + /*= } =*/ + itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, + ltr = !options.rtl, + bBox, + li = item.legendItem, + isSeries = !item.series, + series = !isSeries && item.series.drawLegendSymbol ? + item.series : + item, + seriesOptions = series.options, + showCheckbox = legend.createCheckboxForItem && + seriesOptions && + seriesOptions.showCheckbox, + // full width minus text width + itemExtraWidth = symbolWidth + symbolPadding + itemDistance + + (showCheckbox ? 20 : 0), + useHTML = options.useHTML, + fontSize = 12, + itemClassName = item.options.className; + + if (!li) { // generate it once, later move it + + // Generate the group box, a group to hold the symbol and text. Text + // is to be appended in Legend class. + item.legendGroup = renderer.g('legend-item') + .addClass( + 'highcharts-' + series.type + '-series ' + + 'highcharts-color-' + item.colorIndex + + (itemClassName ? ' ' + itemClassName : '') + + (isSeries ? ' highcharts-series-' + item.index : '') + ) + .attr({ zIndex: 1 }) + .add(legend.scrollGroup); + + // Generate the list item text and add it to the group + item.legendItem = li = renderer.text( + '', + ltr ? symbolWidth + symbolPadding : -symbolPadding, + legend.baseline || 0, + useHTML + ) + /*= if (build.classic) { =*/ + // merge to prevent modifying original (#1021) + .css(merge(item.visible ? itemStyle : itemHiddenStyle)) + /*= } =*/ + .attr({ + align: ltr ? 'left' : 'right', + zIndex: 2 + }) + .add(item.legendGroup); + + // Get the baseline for the first item - the font size is equal for + // all + if (!legend.baseline) { + /*= if (build.classic) { =*/ + fontSize = itemStyle.fontSize; + /*= } =*/ + legend.fontMetrics = renderer.fontMetrics( + fontSize, + li + ); + legend.baseline = + legend.fontMetrics.f + 3 + legend.itemMarginTop; + li.attr('y', legend.baseline); + } + + // Draw the legend symbol inside the group box + legend.symbolHeight = options.symbolHeight || legend.fontMetrics.f; + series.drawLegendSymbol(legend, item); + + if (legend.setItemEvents) { + legend.setItemEvents(item, li, useHTML); + } + + // add the HTML checkbox on top + if (showCheckbox) { + legend.createCheckboxForItem(item); + } + } + + // Colorize the items + legend.colorizeItem(item, item.visible); + + // Take care of max width and text overflow (#6659) + /*= if (build.classic) { =*/ + if (!itemStyle.width) { + /*= } =*/ + li.css({ + width: ( + options.itemWidth || + options.width || + chart.spacingBox.width + ) - itemExtraWidth + }); + /*= if (build.classic) { =*/ + } + /*= } =*/ + + // Always update the text + legend.setText(item); + + // calculate the positions for the next line + bBox = li.getBBox(); + + item.itemWidth = item.checkboxOffset = + options.itemWidth || + item.legendItemWidth || + bBox.width + itemExtraWidth; + legend.maxItemWidth = Math.max(legend.maxItemWidth, item.itemWidth); + legend.totalItemWidth += item.itemWidth; + legend.itemHeight = item.itemHeight = Math.round( + item.legendItemHeight || bBox.height || legend.symbolHeight + ); + }, + + /** + * Get the position of the item in the layout. We now know the + * maxItemWidth from the previous loop. + * + * @private + */ + layoutItem: function (item) { + + var options = this.options, + padding = this.padding, + horizontal = options.layout === 'horizontal', + itemHeight = item.itemHeight, + itemMarginBottom = options.itemMarginBottom || 0, + itemMarginTop = this.itemMarginTop, + itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, + widthOption = options.width, + maxLegendWidth = widthOption || ( + this.chart.spacingBox.width - 2 * padding - options.x + ), + itemWidth = ( + options.alignColumns && + this.totalItemWidth > maxLegendWidth + ) ? + this.maxItemWidth : + item.itemWidth; + + // If the item exceeds the width, start a new line + if ( + horizontal && + this.itemX - padding + itemWidth > maxLegendWidth + ) { + this.itemX = padding; + this.itemY += itemMarginTop + this.lastLineHeight + + itemMarginBottom; + this.lastLineHeight = 0; // reset for next line (#915, #3976) + } + + // Set the edge positions + this.lastItemY = itemMarginTop + this.itemY + itemMarginBottom; + this.lastLineHeight = Math.max( // #915 + itemHeight, + this.lastLineHeight + ); + + // cache the position of the newly generated or reordered items + item._legendItemPos = [this.itemX, this.itemY]; + + // advance + if (horizontal) { + this.itemX += itemWidth; + + } else { + this.itemY += itemMarginTop + itemHeight + itemMarginBottom; + this.lastLineHeight = itemHeight; + } + + // the width of the widest item + this.offsetWidth = widthOption || Math.max( + ( + horizontal ? this.itemX - padding - (item.checkbox ? + // decrease by itemDistance only when no checkbox #4853 + 0 : + itemDistance + ) : itemWidth + ) + padding, + this.offsetWidth + ); + }, + + /** + * Get all items, which is one item per series for most series and one + * item per point for pie series and its derivatives. + * + * @return {Array.} + * The current items in the legend. + */ + getAllItems: function () { + var allItems = []; + each(this.chart.series, function (series) { + var seriesOptions = series && series.options; + + // Handle showInLegend. If the series is linked to another series, + // defaults to false. + if (series && pick( + seriesOptions.showInLegend, + !defined(seriesOptions.linkedTo) ? undefined : false, true + )) { + + // Use points or series for the legend item depending on + // legendType + allItems = allItems.concat( + series.legendItems || + ( + seriesOptions.legendType === 'point' ? + series.data : + series + ) + ); + } + }); + + fireEvent(this, 'afterGetAllItems', { allItems: allItems }); + + return allItems; + }, + + /** + * Get a short, three letter string reflecting the alignment and layout. + * + * @private + * @return {String} The alignment, empty string if floating + */ + getAlignment: function () { + var options = this.options; + + // Use the first letter of each alignment option in order to detect + // the side. (#4189 - use charAt(x) notation instead of [x] for IE7) + return options.floating ? '' : ( + options.align.charAt(0) + + options.verticalAlign.charAt(0) + + options.layout.charAt(0) + ); + }, + + /** + * Adjust the chart margins by reserving space for the legend on only one + * side of the chart. If the position is set to a corner, top or bottom is + * reserved for horizontal legends and left or right for vertical ones. + * + * @private + */ + adjustMargins: function (margin, spacing) { + var chart = this.chart, + options = this.options, + alignment = this.getAlignment(); + + if (alignment) { + + each([ + /(lth|ct|rth)/, + /(rtv|rm|rbv)/, + /(rbh|cb|lbh)/, + /(lbv|lm|ltv)/ + ], function (alignments, side) { + if (alignments.test(alignment) && !defined(margin[side])) { + + // Now we have detected on which side of the chart we should + // reserve space for the legend + chart[marginNames[side]] = Math.max( + chart[marginNames[side]], + ( + chart.legend[ + (side + 1) % 2 ? 'legendHeight' : 'legendWidth' + ] + + [1, -1, -1, 1][side] * options[ + (side % 2) ? 'x' : 'y' + ] + + pick(options.margin, 12) + + spacing[side] + + ( + side === 0 && + chart.options.title.margin !== undefined ? + chart.titleOffset + + chart.options.title.margin : + 0 + ) // #7428, #7894 + ) + ); + } + }); + } + }, + + /** + * Render the legend. This method can be called both before and after + * `chart.render`. If called after, it will only rearrange items instead + * of creating new ones. Called internally on initial render and after + * redraws. + */ + render: function () { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + legendGroup = legend.group, + allItems, + display, + legendWidth, + legendHeight, + box = legend.box, + options = legend.options, + padding = legend.padding, + alignTo; + + legend.itemX = padding; + legend.itemY = legend.initialItemY; + legend.offsetWidth = 0; + legend.lastItemY = 0; + + if (!legendGroup) { + legend.group = legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + legend.contentGroup = renderer.g() + .attr({ zIndex: 1 }) // above background + .add(legendGroup); + legend.scrollGroup = renderer.g() + .add(legend.contentGroup); + } + + legend.renderTitle(); + + // add each series or point + allItems = legend.getAllItems(); + + // sort by legendIndex + stableSort(allItems, function (a, b) { + return ((a.options && a.options.legendIndex) || 0) - + ((b.options && b.options.legendIndex) || 0); + }); + + // reversed legend + if (options.reversed) { + allItems.reverse(); + } + + legend.allItems = allItems; + legend.display = display = !!allItems.length; + + // Render the items. First we run a loop to set the text and properties + // and read all the bounding boxes. The next loop computes the item + // positions based on the bounding boxes. + legend.lastLineHeight = 0; + legend.maxItemWidth = 0; + legend.totalItemWidth = 0; + legend.itemHeight = 0; + each(allItems, legend.renderItem, legend); + each(allItems, legend.layoutItem, legend); + + // Get the box + legendWidth = (options.width || legend.offsetWidth) + padding; + legendHeight = legend.lastItemY + legend.lastLineHeight + + legend.titleHeight; + legendHeight = legend.handleOverflow(legendHeight); + legendHeight += padding; + + // Draw the border and/or background + if (!box) { + legend.box = box = renderer.rect() + .addClass('highcharts-legend-box') + .attr({ + r: options.borderRadius + }) + .add(legendGroup); + box.isNew = true; + } + + /*= if (build.classic) { =*/ + // Presentational + box + .attr({ + stroke: options.borderColor, + 'stroke-width': options.borderWidth || 0, + fill: options.backgroundColor || 'none' + }) + .shadow(options.shadow); + /*= } =*/ + + if (legendWidth > 0 && legendHeight > 0) { + box[box.isNew ? 'attr' : 'animate']( + box.crisp.call({}, { // #7260 + x: 0, + y: 0, + width: legendWidth, + height: legendHeight + }, box.strokeWidth()) + ); + box.isNew = false; + } + + // hide the border if no items + box[display ? 'show' : 'hide'](); + + /*= if (!build.classic) { =*/ + // Open for responsiveness + if (legendGroup.getStyle('display') === 'none') { + legendWidth = legendHeight = 0; + } + /*= } =*/ + + legend.legendWidth = legendWidth; + legend.legendHeight = legendHeight; + + // Now that the legend width and height are established, put the items + // in the final position + each(allItems, legend.positionItem, legend); + + if (display) { + // If aligning to the top and the layout is horizontal, adjust for + // the title (#7428) + alignTo = chart.spacingBox; + if (/(lth|ct|rth)/.test(legend.getAlignment())) { + alignTo = merge(alignTo, { + y: alignTo.y + chart.titleOffset + + chart.options.title.margin + }); + } + + legendGroup.align(merge(options, { + width: legendWidth, + height: legendHeight + }), true, alignTo); + } + + if (!chart.isResizing) { + this.positionCheckboxes(); + } + }, + + /** + * Set up the overflow handling by adding navigation with up and down arrows + * below the legend. + * + * @private + */ + handleOverflow: function (legendHeight) { + var legend = this, + chart = this.chart, + renderer = chart.renderer, + options = this.options, + optionsY = options.y, + alignTop = options.verticalAlign === 'top', + padding = this.padding, + spaceHeight = chart.spacingBox.height + + (alignTop ? -optionsY : optionsY) - padding, + maxHeight = options.maxHeight, + clipHeight, + clipRect = this.clipRect, + navOptions = options.navigation, + animation = pick(navOptions.animation, true), + arrowSize = navOptions.arrowSize || 12, + nav = this.nav, + pages = this.pages, + lastY, + allItems = this.allItems, + clipToHeight = function (height) { + if (typeof height === 'number') { + clipRect.attr({ + height: height + }); + } else if (clipRect) { // Reset (#5912) + legend.clipRect = clipRect.destroy(); + legend.contentGroup.clip(); + } + + // useHTML + if (legend.contentGroup.div) { + legend.contentGroup.div.style.clip = height ? + 'rect(' + padding + 'px,9999px,' + + (padding + height) + 'px,0)' : + 'auto'; + } + }; + + + // Adjust the height + if ( + options.layout === 'horizontal' && + options.verticalAlign !== 'middle' && + !options.floating + ) { + spaceHeight /= 2; + } + if (maxHeight) { + spaceHeight = Math.min(spaceHeight, maxHeight); + } + + // Reset the legend height and adjust the clipping rectangle + pages.length = 0; + if (legendHeight > spaceHeight && navOptions.enabled !== false) { + + this.clipHeight = clipHeight = + Math.max(spaceHeight - 20 - this.titleHeight - padding, 0); + this.currentPage = pick(this.currentPage, 1); + this.fullHeight = legendHeight; + + // Fill pages with Y positions so that the top of each a legend item + // defines the scroll top for each page (#2098) + each(allItems, function (item, i) { + var y = item._legendItemPos[1], + h = Math.round(item.legendItem.getBBox().height), + len = pages.length; + + if (!len || (y - pages[len - 1] > clipHeight && + (lastY || y) !== pages[len - 1])) { + pages.push(lastY || y); + len++; + } + + // Keep track of which page each item is on + item.pageIx = len - 1; + if (lastY) { + allItems[i - 1].pageIx = len - 1; + } + + if (i === allItems.length - 1 && + y + h - pages[len - 1] > clipHeight) { + pages.push(y); + item.pageIx = len; + } + if (y !== lastY) { + lastY = y; + } + }); + + // Only apply clipping if needed. Clipping causes blurred legend in + // PDF export (#1787) + if (!clipRect) { + clipRect = legend.clipRect = + renderer.clipRect(0, padding, 9999, 0); + legend.contentGroup.clip(clipRect); + } + + clipToHeight(clipHeight); + + // Add navigation elements + if (!nav) { + this.nav = nav = renderer.g() + .attr({ zIndex: 1 }) + .add(this.group); + + this.up = renderer + .symbol( + 'triangle', + 0, + 0, + arrowSize, + arrowSize + ) + .on('click', function () { + legend.scroll(-1, animation); + }) + .add(nav); + + this.pager = renderer.text('', 15, 10) + .addClass('highcharts-legend-navigation') + /*= if (build.classic) { =*/ + .css(navOptions.style) + /*= } =*/ + .add(nav); + + this.down = renderer + .symbol( + 'triangle-down', + 0, + 0, + arrowSize, + arrowSize + ) + .on('click', function () { + legend.scroll(1, animation); + }) + .add(nav); + } + + // Set initial position + legend.scroll(0); + + legendHeight = spaceHeight; + + // Reset + } else if (nav) { + clipToHeight(); + this.nav = nav.destroy(); // #6322 + this.scrollGroup.attr({ + translateY: 1 + }); + this.clipHeight = 0; // #1379 + } + + return legendHeight; + }, + + /** + * Scroll the legend by a number of pages. + * @param {Number} scrollBy + * The number of pages to scroll. + * @param {AnimationOptions} animation + * Whether and how to apply animation. + */ + scroll: function (scrollBy, animation) { + var pages = this.pages, + pageCount = pages.length, + currentPage = this.currentPage + scrollBy, + clipHeight = this.clipHeight, + navOptions = this.options.navigation, + pager = this.pager, + padding = this.padding; + + // When resizing while looking at the last page + if (currentPage > pageCount) { + currentPage = pageCount; + } + + if (currentPage > 0) { + + if (animation !== undefined) { + setAnimation(animation, this.chart); + } + + this.nav.attr({ + translateX: padding, + translateY: clipHeight + this.padding + 7 + this.titleHeight, + visibility: 'visible' + }); + this.up.attr({ + 'class': currentPage === 1 ? + 'highcharts-legend-nav-inactive' : + 'highcharts-legend-nav-active' + }); + pager.attr({ + text: currentPage + '/' + pageCount + }); + this.down.attr({ + 'x': 18 + this.pager.getBBox().width, // adjust to text width + 'class': currentPage === pageCount ? + 'highcharts-legend-nav-inactive' : + 'highcharts-legend-nav-active' + }); + + /*= if (build.classic) { =*/ + this.up + .attr({ + fill: currentPage === 1 ? + navOptions.inactiveColor : + navOptions.activeColor + }) + .css({ + cursor: currentPage === 1 ? 'default' : 'pointer' + }); + this.down + .attr({ + fill: currentPage === pageCount ? + navOptions.inactiveColor : + navOptions.activeColor + }) + .css({ + cursor: currentPage === pageCount ? 'default' : 'pointer' + }); + /*= } =*/ + + this.scrollOffset = -pages[currentPage - 1] + this.initialItemY; + + this.scrollGroup.animate({ + translateY: this.scrollOffset + }); + + this.currentPage = currentPage; + this.positionCheckboxes(); + } + + } }; @@ -1031,107 +1031,107 @@ Highcharts.Legend.prototype = { H.LegendSymbolMixin = { - /** - * Get the series' symbol in the legend - * - * @param {Object} legend The legend object - * @param {Object} item The series (this) or point - */ - drawRectangle: function (legend, item) { - var options = legend.options, - symbolHeight = legend.symbolHeight, - square = options.squareSymbol, - symbolWidth = square ? symbolHeight : legend.symbolWidth; - - item.legendSymbol = this.chart.renderer.rect( - square ? (legend.symbolWidth - symbolHeight) / 2 : 0, - legend.baseline - symbolHeight + 1, // #3988 - symbolWidth, - symbolHeight, - pick(legend.options.symbolRadius, symbolHeight / 2) - ) - .addClass('highcharts-point') - .attr({ - zIndex: 3 - }).add(item.legendGroup); - - }, - - /** - * Get the series' symbol in the legend. This method should be overridable - * to create custom symbols through - * Highcharts.seriesTypes[type].prototype.drawLegendSymbols. - * - * @param {Object} legend The legend object - */ - drawLineMarker: function (legend) { - - var options = this.options, - markerOptions = options.marker, - radius, - legendSymbol, - symbolWidth = legend.symbolWidth, - symbolHeight = legend.symbolHeight, - generalRadius = symbolHeight / 2, - renderer = this.chart.renderer, - legendItemGroup = this.legendGroup, - verticalCenter = legend.baseline - - Math.round(legend.fontMetrics.b * 0.3), - attr = {}; - - // Draw the line - /*= if (build.classic) { =*/ - attr = { - 'stroke-width': options.lineWidth || 0 - }; - if (options.dashStyle) { - attr.dashstyle = options.dashStyle; - } - /*= } =*/ - - this.legendLine = renderer.path([ - 'M', - 0, - verticalCenter, - 'L', - symbolWidth, - verticalCenter - ]) - .addClass('highcharts-graph') - .attr(attr) - .add(legendItemGroup); - - // Draw the marker - if (markerOptions && markerOptions.enabled !== false) { - - // Do not allow the marker to be larger than the symbolHeight - radius = Math.min( - pick(markerOptions.radius, generalRadius), - generalRadius - ); - - // Restrict symbol markers size - if (this.symbol.indexOf('url') === 0) { - markerOptions = merge(markerOptions, { - width: symbolHeight, - height: symbolHeight - }); - radius = 0; - } - - this.legendSymbol = legendSymbol = renderer.symbol( - this.symbol, - (symbolWidth / 2) - radius, - verticalCenter - radius, - 2 * radius, - 2 * radius, - markerOptions - ) - .addClass('highcharts-point') - .add(legendItemGroup); - legendSymbol.isMarker = true; - } - } + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawRectangle: function (legend, item) { + var options = legend.options, + symbolHeight = legend.symbolHeight, + square = options.squareSymbol, + symbolWidth = square ? symbolHeight : legend.symbolWidth; + + item.legendSymbol = this.chart.renderer.rect( + square ? (legend.symbolWidth - symbolHeight) / 2 : 0, + legend.baseline - symbolHeight + 1, // #3988 + symbolWidth, + symbolHeight, + pick(legend.options.symbolRadius, symbolHeight / 2) + ) + .addClass('highcharts-point') + .attr({ + zIndex: 3 + }).add(item.legendGroup); + + }, + + /** + * Get the series' symbol in the legend. This method should be overridable + * to create custom symbols through + * Highcharts.seriesTypes[type].prototype.drawLegendSymbols. + * + * @param {Object} legend The legend object + */ + drawLineMarker: function (legend) { + + var options = this.options, + markerOptions = options.marker, + radius, + legendSymbol, + symbolWidth = legend.symbolWidth, + symbolHeight = legend.symbolHeight, + generalRadius = symbolHeight / 2, + renderer = this.chart.renderer, + legendItemGroup = this.legendGroup, + verticalCenter = legend.baseline - + Math.round(legend.fontMetrics.b * 0.3), + attr = {}; + + // Draw the line + /*= if (build.classic) { =*/ + attr = { + 'stroke-width': options.lineWidth || 0 + }; + if (options.dashStyle) { + attr.dashstyle = options.dashStyle; + } + /*= } =*/ + + this.legendLine = renderer.path([ + 'M', + 0, + verticalCenter, + 'L', + symbolWidth, + verticalCenter + ]) + .addClass('highcharts-graph') + .attr(attr) + .add(legendItemGroup); + + // Draw the marker + if (markerOptions && markerOptions.enabled !== false) { + + // Do not allow the marker to be larger than the symbolHeight + radius = Math.min( + pick(markerOptions.radius, generalRadius), + generalRadius + ); + + // Restrict symbol markers size + if (this.symbol.indexOf('url') === 0) { + markerOptions = merge(markerOptions, { + width: symbolHeight, + height: symbolHeight + }); + radius = 0; + } + + this.legendSymbol = legendSymbol = renderer.symbol( + this.symbol, + (symbolWidth / 2) - radius, + verticalCenter - radius, + 2 * radius, + 2 * radius, + markerOptions + ) + .addClass('highcharts-point') + .add(legendItemGroup); + legendSymbol.isMarker = true; + } + } }; // Workaround for #2030, horizontal legend items not displaying in IE11 Preview, @@ -1140,19 +1140,19 @@ H.LegendSymbolMixin = { // to nested group elements, as the legend item texts are within 4 group // elements. if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) { - wrap(Highcharts.Legend.prototype, 'positionItem', function (proceed, item) { - var legend = this, - // If chart destroyed in sync, this is undefined (#2030) - runPositionItem = function () { - if (item._legendItemPos) { - proceed.call(legend, item); - } - }; - - // Do it now, for export and to get checkbox placement - runPositionItem(); - - // Do it after to work around the core issue - setTimeout(runPositionItem); - }); + wrap(Highcharts.Legend.prototype, 'positionItem', function (proceed, item) { + var legend = this, + // If chart destroyed in sync, this is undefined (#2030) + runPositionItem = function () { + if (item._legendItemPos) { + proceed.call(legend, item); + } + }; + + // Do it now, for export and to get checkbox placement + runPositionItem(); + + // Do it after to work around the core issue + setTimeout(runPositionItem); + }); } diff --git a/js/parts/LogarithmicAxis.js b/js/parts/LogarithmicAxis.js index 13026a41432..17b107f2abc 100644 --- a/js/parts/LogarithmicAxis.js +++ b/js/parts/LogarithmicAxis.js @@ -8,10 +8,10 @@ import H from './Globals.js'; import './Utilities.js'; var Axis = H.Axis, - getMagnitude = H.getMagnitude, - map = H.map, - normalizeTickInterval = H.normalizeTickInterval, - pick = H.pick; + getMagnitude = H.getMagnitude, + map = H.map, + normalizeTickInterval = H.normalizeTickInterval, + pick = H.pick; /** * Methods defined on the Axis prototype */ @@ -20,107 +20,107 @@ var Axis = H.Axis, * Set the tick positions of a logarithmic axis */ Axis.prototype.getLogTickPositions = function (interval, min, max, minor) { - var axis = this, - options = axis.options, - axisLength = axis.len, - lin2log = axis.lin2log, - log2lin = axis.log2lin, - // Since we use this method for both major and minor ticks, - // use a local variable and return the result - positions = []; + var axis = this, + options = axis.options, + axisLength = axis.len, + lin2log = axis.lin2log, + log2lin = axis.log2lin, + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + positions = []; - // Reset - if (!minor) { - axis._minorAutoInterval = null; - } + // Reset + if (!minor) { + axis._minorAutoInterval = null; + } - // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. - if (interval >= 0.5) { - interval = Math.round(interval); - positions = axis.getLinearTickPositions(interval, min, max); + // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. + if (interval >= 0.5) { + interval = Math.round(interval); + positions = axis.getLinearTickPositions(interval, min, max); - // Second case: We need intermediary ticks. For example - // 1, 2, 4, 6, 8, 10, 20, 40 etc. - } else if (interval >= 0.08) { - var roundedMin = Math.floor(min), - intermediate, - i, - j, - len, - pos, - lastPos, - break2; + // Second case: We need intermediary ticks. For example + // 1, 2, 4, 6, 8, 10, 20, 40 etc. + } else if (interval >= 0.08) { + var roundedMin = Math.floor(min), + intermediate, + i, + j, + len, + pos, + lastPos, + break2; - if (interval > 0.3) { - intermediate = [1, 2, 4]; - } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 4, 6, 8]; - } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - } + if (interval > 0.3) { + intermediate = [1, 2, 4]; + } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 4, 6, 8]; + } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } - for (i = roundedMin; i < max + 1 && !break2; i++) { - len = intermediate.length; - for (j = 0; j < len && !break2; j++) { - pos = log2lin(lin2log(i) * intermediate[j]); - if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113 - positions.push(lastPos); - } + for (i = roundedMin; i < max + 1 && !break2; i++) { + len = intermediate.length; + for (j = 0; j < len && !break2; j++) { + pos = log2lin(lin2log(i) * intermediate[j]); + if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113 + positions.push(lastPos); + } - if (lastPos > max) { - break2 = true; - } - lastPos = pos; - } - } + if (lastPos > max) { + break2 = true; + } + lastPos = pos; + } + } - // Third case: We are so deep in between whole logarithmic values that - // we might as well handle the tick positions like a linear axis. For - // example 1.01, 1.02, 1.03, 1.04. - } else { - var realMin = lin2log(min), - realMax = lin2log(max), - tickIntervalOption = minor ? - this.getMinorTickInterval() : - options.tickInterval, - filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, - tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), - totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; + // Third case: We are so deep in between whole logarithmic values that + // we might as well handle the tick positions like a linear axis. For + // example 1.01, 1.02, 1.03, 1.04. + } else { + var realMin = lin2log(min), + realMax = lin2log(max), + tickIntervalOption = minor ? + this.getMinorTickInterval() : + options.tickInterval, + filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, + tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), + totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; - interval = pick( - filteredTickIntervalOption, - axis._minorAutoInterval, - (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) - ); + interval = pick( + filteredTickIntervalOption, + axis._minorAutoInterval, + (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) + ); - interval = normalizeTickInterval( - interval, - null, - getMagnitude(interval) - ); + interval = normalizeTickInterval( + interval, + null, + getMagnitude(interval) + ); - positions = map(axis.getLinearTickPositions( - interval, - realMin, - realMax - ), log2lin); + positions = map(axis.getLinearTickPositions( + interval, + realMin, + realMax + ), log2lin); - if (!minor) { - axis._minorAutoInterval = interval / 5; - } - } + if (!minor) { + axis._minorAutoInterval = interval / 5; + } + } - // Set the axis-level tickInterval variable - if (!minor) { - axis.tickInterval = interval; - } - return positions; + // Set the axis-level tickInterval variable + if (!minor) { + axis.tickInterval = interval; + } + return positions; }; Axis.prototype.log2lin = function (num) { - return Math.log(num) / Math.LN10; + return Math.log(num) / Math.LN10; }; Axis.prototype.lin2log = function (num) { - return Math.pow(10, num); + return Math.pow(10, num); }; diff --git a/js/parts/MSPointer.js b/js/parts/MSPointer.js index aa23b8b2aff..a0c0f1ede3a 100644 --- a/js/parts/MSPointer.js +++ b/js/parts/MSPointer.js @@ -9,104 +9,104 @@ import H from './Globals.js'; import './Utilities.js'; import './Pointer.js'; var addEvent = H.addEvent, - charts = H.charts, - css = H.css, - doc = H.doc, - extend = H.extend, - hasTouch = H.hasTouch, - noop = H.noop, - Pointer = H.Pointer, - removeEvent = H.removeEvent, - win = H.win, - wrap = H.wrap; + charts = H.charts, + css = H.css, + doc = H.doc, + extend = H.extend, + hasTouch = H.hasTouch, + noop = H.noop, + Pointer = H.Pointer, + removeEvent = H.removeEvent, + win = H.win, + wrap = H.wrap; if (!hasTouch && (win.PointerEvent || win.MSPointerEvent)) { - - // The touches object keeps track of the points being touched at all times - var touches = {}, - hasPointerEvent = !!win.PointerEvent, - getWebkitTouches = function () { - var fake = []; - fake.item = function (i) { - return this[i]; - }; - H.objectEach(touches, function (touch) { - fake.push({ - pageX: touch.pageX, - pageY: touch.pageY, - target: touch.target - }); - }); - return fake; - }, - translateMSPointer = function (e, method, wktype, func) { - var p; - if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) { - func(e); - p = charts[H.hoverChartIndex].pointer; - p[method]({ - type: wktype, - target: e.currentTarget, - preventDefault: noop, - touches: getWebkitTouches() - }); - } - }; - /** - * Extend the Pointer prototype with methods for each event handler and more - */ - extend(Pointer.prototype, /** @lends Pointer.prototype */ { - onContainerPointerDown: function (e) { - translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) { - touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget }; - }); - }, - onContainerPointerMove: function (e) { - translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) { - touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY }; - if (!touches[e.pointerId].target) { - touches[e.pointerId].target = e.currentTarget; - } - }); - }, - onDocumentPointerUp: function (e) { - translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) { - delete touches[e.pointerId]; - }); - }, + // The touches object keeps track of the points being touched at all times + var touches = {}, + hasPointerEvent = !!win.PointerEvent, + getWebkitTouches = function () { + var fake = []; + fake.item = function (i) { + return this[i]; + }; + H.objectEach(touches, function (touch) { + fake.push({ + pageX: touch.pageX, + pageY: touch.pageY, + target: touch.target + }); + }); + return fake; + }, + translateMSPointer = function (e, method, wktype, func) { + var p; + if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) { + func(e); + p = charts[H.hoverChartIndex].pointer; + p[method]({ + type: wktype, + target: e.currentTarget, + preventDefault: noop, + touches: getWebkitTouches() + }); + } + }; - /** - * Add or remove the MS Pointer specific events - */ - batchMSEvents: function (fn) { - fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown); - fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove); - fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp); - } - }); + /** + * Extend the Pointer prototype with methods for each event handler and more + */ + extend(Pointer.prototype, /** @lends Pointer.prototype */ { + onContainerPointerDown: function (e) { + translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget }; + }); + }, + onContainerPointerMove: function (e) { + translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY }; + if (!touches[e.pointerId].target) { + touches[e.pointerId].target = e.currentTarget; + } + }); + }, + onDocumentPointerUp: function (e) { + translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) { + delete touches[e.pointerId]; + }); + }, - // Disable default IE actions for pinch and such on chart element - wrap(Pointer.prototype, 'init', function (proceed, chart, options) { - proceed.call(this, chart, options); - if (this.hasZoom) { // #4014 - css(chart.container, { - '-ms-touch-action': 'none', - 'touch-action': 'none' - }); - } - }); + /** + * Add or remove the MS Pointer specific events + */ + batchMSEvents: function (fn) { + fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown); + fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove); + fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp); + } + }); - // Add IE specific touch events to chart - wrap(Pointer.prototype, 'setDOMEvents', function (proceed) { - proceed.apply(this); - if (this.hasZoom || this.followTouchMove) { - this.batchMSEvents(addEvent); - } - }); - // Destroy MS events also - wrap(Pointer.prototype, 'destroy', function (proceed) { - this.batchMSEvents(removeEvent); - proceed.call(this); - }); + // Disable default IE actions for pinch and such on chart element + wrap(Pointer.prototype, 'init', function (proceed, chart, options) { + proceed.call(this, chart, options); + if (this.hasZoom) { // #4014 + css(chart.container, { + '-ms-touch-action': 'none', + 'touch-action': 'none' + }); + } + }); + + // Add IE specific touch events to chart + wrap(Pointer.prototype, 'setDOMEvents', function (proceed) { + proceed.apply(this); + if (this.hasZoom || this.followTouchMove) { + this.batchMSEvents(addEvent); + } + }); + // Destroy MS events also + wrap(Pointer.prototype, 'destroy', function (proceed) { + this.batchMSEvents(removeEvent); + proceed.call(this); + }); } diff --git a/js/parts/Navigator.js b/js/parts/Navigator.js index b8c7f83d6e2..766a6b9ceb1 100644 --- a/js/parts/Navigator.js +++ b/js/parts/Navigator.js @@ -3,7 +3,7 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from './Globals.js'; import './Utilities.js'; @@ -45,456 +45,456 @@ import './Scrollbar.js'; */ var addEvent = H.addEvent, - Axis = H.Axis, - Chart = H.Chart, - color = H.color, - defaultDataGroupingUnits = H.defaultDataGroupingUnits, - defaultOptions = H.defaultOptions, - defined = H.defined, - destroyObjectProperties = H.destroyObjectProperties, - each = H.each, - erase = H.erase, - error = H.error, - extend = H.extend, - grep = H.grep, - hasTouch = H.hasTouch, - isArray = H.isArray, - isNumber = H.isNumber, - isObject = H.isObject, - merge = H.merge, - pick = H.pick, - removeEvent = H.removeEvent, - Scrollbar = H.Scrollbar, - Series = H.Series, - seriesTypes = H.seriesTypes, - wrap = H.wrap, - - 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. - numExt = function (extreme) { - var numbers = grep(arguments, isNumber); - if (numbers.length) { - return Math[extreme].apply(0, numbers); - } - }; + Axis = H.Axis, + Chart = H.Chart, + color = H.color, + defaultDataGroupingUnits = H.defaultDataGroupingUnits, + defaultOptions = H.defaultOptions, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + erase = H.erase, + error = H.error, + extend = H.extend, + grep = H.grep, + hasTouch = H.hasTouch, + isArray = H.isArray, + isNumber = H.isNumber, + isObject = H.isObject, + merge = H.merge, + pick = H.pick, + removeEvent = H.removeEvent, + Scrollbar = H.Scrollbar, + Series = H.Series, + seriesTypes = H.seriesTypes, + wrap = H.wrap, + + 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. + numExt = function (extreme) { + var numbers = grep(arguments, isNumber); + if (numbers.length) { + return Math[extreme].apply(0, numbers); + } + }; // 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'; + '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. - * - * @product highstock - * @optionparent navigator - */ - navigator: { - /** - * 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 - * @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 - * @default true - * @since 2.0 - * @product highstock - */ - maskInside: true, - - /** - * Options for the handles for dragging the zoomed area. - * - * @type {Object} - * @sample {highstock} stock/navigator/handles/ Colored handles - * @product highstock - */ - handles: { - /** - * Width for handles. - * - * @type {Number} - * @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, - - /*= if (build.classic) { =*/ - /** - * 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} - * @product highstock - */ - backgroundColor: '${palette.neutralColor5}', - - /** - * The stroke for the handle border and the stripes inside. - * - * @type {Color} - * @product highstock - */ - borderColor: '${palette.neutralColor40}' - - /*= } =*/ - }, - - /*= if (build.classic) { =*/ - - /** - * 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, 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('${palette.highlightColor60}').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: '${palette.neutralColor20}', - - /** - * The width of the line marking the currently zoomed area in the - * navigator. - * - * @type {Number} - * @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 - */ - outlineWidth: 1, - /*= } =*/ - - /** - * 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: - * - *
series: {
-		 *     type: 'areaspline',
-		 *     fillOpacity: 0.05,
-		 *     dataGrouping: {
-		 *         smoothed: true
-		 *     },
-		 *     lineWidth: 1,
-		 *     marker: {
-		 *         enabled: false
-		 *     }
-		 * }
- * - * @type {Object} - * @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 - * @product highstock - */ - series: { - - /** - * The type of the navigator series. Defaults to `areaspline` if - * defined, otherwise `line`. - * - * @type {String} - */ - type: defaultSeriesType, - /*= if (build.classic) { =*/ - - - /** - * 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, // #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} - */ - threshold: null - }, - - /** - * Options for the navigator X axis. Default series options - * for the navigator xAxis are: - * - *
xAxis: {
-		 *     tickWidth: 0,
-		 *     lineWidth: 0,
-		 *     gridLineWidth: 1,
-		 *     tickPixelInterval: 200,
-		 *     labels: {
-		 *     	   align: 'left',
-		 *         style: {
-		 *             color: '#888'
-		 *         },
-		 *         x: 3,
-		 *         y: -4
-		 *     }
-		 * }
- * - * @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 - */ - overscroll: 0, - - className: 'highcharts-navigator-xaxis', - tickLength: 0, - - /*= if (build.classic) { =*/ - lineWidth: 0, - gridLineColor: '${palette.neutralColor10}', - gridLineWidth: 1, - /*= } =*/ - - tickPixelInterval: 200, - - labels: { - align: 'left', - - /*= if (build.classic) { =*/ - style: { - color: '${palette.neutralColor40}' - }, - /*= } =*/ - - x: 3, - y: -4 - }, - - crosshair: false - }, - - /** - * Options for the navigator Y axis. Default series options - * for the navigator yAxis are: - * - *
yAxis: {
-		 *     gridLineWidth: 0,
-		 *     startOnTick: false,
-		 *     endOnTick: false,
-		 *     minPadding: 0.1,
-		 *     maxPadding: 0.1,
-		 *     labels: {
-		 *         enabled: false
-		 *     },
-		 *     title: {
-		 *         text: null
-		 *     },
-		 *     tickWidth: 0
-		 * }
- * - * @type {Object} - * @extends {yAxis} - * @excluding height,linkedTo,maxZoom,minRange,ordinal,range,showEmpty, - * scrollbar,top,units,maxRange,minLength,maxLength,resize - * @product highstock - */ - yAxis: { - - className: 'highcharts-navigator-yaxis', - - /*= if (build.classic) { =*/ - gridLineWidth: 0, - /*= } =*/ - - startOnTick: false, - endOnTick: false, - minPadding: 0.1, - maxPadding: 0.1, - labels: { - enabled: false - }, - crosshair: false, - title: { - text: null - }, - tickLength: 0, - tickWidth: 0 - } - } + /** + * 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. + * + * @product highstock + * @optionparent navigator + */ + navigator: { + /** + * 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 + * @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 + * @default true + * @since 2.0 + * @product highstock + */ + maskInside: true, + + /** + * Options for the handles for dragging the zoomed area. + * + * @type {Object} + * @sample {highstock} stock/navigator/handles/ Colored handles + * @product highstock + */ + handles: { + /** + * Width for handles. + * + * @type {Number} + * @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, + + /*= if (build.classic) { =*/ + /** + * 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} + * @product highstock + */ + backgroundColor: '${palette.neutralColor5}', + + /** + * The stroke for the handle border and the stripes inside. + * + * @type {Color} + * @product highstock + */ + borderColor: '${palette.neutralColor40}' + + /*= } =*/ + }, + + /*= if (build.classic) { =*/ + + /** + * 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, 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('${palette.highlightColor60}').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: '${palette.neutralColor20}', + + /** + * The width of the line marking the currently zoomed area in the + * navigator. + * + * @type {Number} + * @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 + */ + outlineWidth: 1, + /*= } =*/ + + /** + * 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: + * + *
series: {
+         *     type: 'areaspline',
+         *     fillOpacity: 0.05,
+         *     dataGrouping: {
+         *         smoothed: true
+         *     },
+         *     lineWidth: 1,
+         *     marker: {
+         *         enabled: false
+         *     }
+         * }
+ * + * @type {Object} + * @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 + * @product highstock + */ + series: { + + /** + * The type of the navigator series. Defaults to `areaspline` if + * defined, otherwise `line`. + * + * @type {String} + */ + type: defaultSeriesType, + /*= if (build.classic) { =*/ + + + /** + * 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, // #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} + */ + threshold: null + }, + + /** + * Options for the navigator X axis. Default series options + * for the navigator xAxis are: + * + *
xAxis: {
+         *     tickWidth: 0,
+         *     lineWidth: 0,
+         *     gridLineWidth: 1,
+         *     tickPixelInterval: 200,
+         *     labels: {
+         *            align: 'left',
+         *         style: {
+         *             color: '#888'
+         *         },
+         *         x: 3,
+         *         y: -4
+         *     }
+         * }
+ * + * @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 + */ + overscroll: 0, + + className: 'highcharts-navigator-xaxis', + tickLength: 0, + + /*= if (build.classic) { =*/ + lineWidth: 0, + gridLineColor: '${palette.neutralColor10}', + gridLineWidth: 1, + /*= } =*/ + + tickPixelInterval: 200, + + labels: { + align: 'left', + + /*= if (build.classic) { =*/ + style: { + color: '${palette.neutralColor40}' + }, + /*= } =*/ + + x: 3, + y: -4 + }, + + crosshair: false + }, + + /** + * Options for the navigator Y axis. Default series options + * for the navigator yAxis are: + * + *
yAxis: {
+         *     gridLineWidth: 0,
+         *     startOnTick: false,
+         *     endOnTick: false,
+         *     minPadding: 0.1,
+         *     maxPadding: 0.1,
+         *     labels: {
+         *         enabled: false
+         *     },
+         *     title: {
+         *         text: null
+         *     },
+         *     tickWidth: 0
+         * }
+ * + * @type {Object} + * @extends {yAxis} + * @excluding height,linkedTo,maxZoom,minRange,ordinal,range,showEmpty, + * scrollbar,top,units,maxRange,minLength,maxLength,resize + * @product highstock + */ + yAxis: { + + className: 'highcharts-navigator-yaxis', + + /*= if (build.classic) { =*/ + gridLineWidth: 0, + /*= } =*/ + + startOnTick: false, + endOnTick: false, + minPadding: 0.1, + maxPadding: 0.1, + labels: { + enabled: false + }, + crosshair: false, + title: { + text: null + }, + tickLength: 0, + tickWidth: 0 + } + } }); /** @@ -503,36 +503,36 @@ extend(defaultOptions, { * @returns {Array} Path to be used in a handle */ H.Renderer.prototype.symbols['navigator-handle'] = function ( - x, - y, - w, - h, - options + 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 - ]; + 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 + ]; }; /** @@ -541,1490 +541,1490 @@ H.Renderer.prototype.symbols['navigator-handle'] = function ( * @class */ function Navigator(chart) { - this.init(chart); + this.init(chart); } Navigator.prototype = { - /** - * Draw one of the handles on the side of the zoomed range in the navigator - * @param {Number} x The x center for the handle - * @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, - height = navigator.navigatorOptions.handles.height; - - // Place it - navigator.handles[index][verb](inverted ? { - 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 - height / 2 - 1 - ) - }); - }, - - /** - * 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' - */ - drawOutline: function (zoomedMin, zoomedMax, inverted, verb) { - var navigator = this, - maskInside = navigator.navigatorOptions.maskInside, - outlineWidth = navigator.outline.strokeWidth(), - halfOutline = outlineWidth / 2, - outlineCorrection = (outlineWidth % 2) / 2, // #5800 - outlineHeight = navigator.outlineHeight, - scrollbarHeight = navigator.scrollbarHeight, - navigatorSize = navigator.size, - left = navigator.left - scrollbarHeight, - navigatorTop = navigator.top, - verticalMin, - path; - - if (inverted) { - left -= halfOutline; - verticalMin = navigatorTop + zoomedMax + outlineCorrection; - zoomedMax = navigatorTop + zoomedMin + outlineCorrection; - - path = [ - 'M', - left + outlineHeight, - navigatorTop - scrollbarHeight - outlineCorrection, // top edge - 'L', - left + outlineHeight, - verticalMin, // top right of zoomed range - 'L', - left, - verticalMin, // top left of z.r. - 'L', - left, - zoomedMax, // bottom left of z.r. - 'L', - left + outlineHeight, - zoomedMax, // bottom right of z.r. - 'L', - left + outlineHeight, - navigatorTop + navigatorSize + scrollbarHeight // bottom edge - ].concat(maskInside ? [ - 'M', - left + outlineHeight, - verticalMin - halfOutline, // upper left of zoomed range - 'L', - left + outlineHeight, - zoomedMax + halfOutline // upper right of z.r. - ] : []); - } else { - zoomedMin += left + scrollbarHeight - outlineCorrection; - zoomedMax += left + scrollbarHeight - outlineCorrection; - navigatorTop += halfOutline; - - path = [ - 'M', - left, - navigatorTop, // left - 'L', - zoomedMin, - navigatorTop, // upper left of zoomed range - 'L', - zoomedMin, - navigatorTop + outlineHeight, // lower left of z.r. - 'L', - zoomedMax, - navigatorTop + outlineHeight, // lower right of z.r. - 'L', - zoomedMax, - navigatorTop, // upper right of z.r. - 'L', - left + navigatorSize + scrollbarHeight * 2, - navigatorTop // right - ].concat(maskInside ? [ - 'M', - zoomedMin - halfOutline, - navigatorTop, // upper left of zoomed range - 'L', - zoomedMax + halfOutline, - navigatorTop // upper right of z.r. - ] : []); - } - navigator.outline[verb]({ - d: path - }); - }, - - /** - * 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' - */ - drawMasks: function (zoomedMin, zoomedMax, inverted, verb) { - var navigator = this, - left = navigator.left, - top = navigator.top, - navigatorHeight = navigator.height, - height, - width, - x, - y; - - // 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]; - height = [ - zoomedMin, - zoomedMax - zoomedMin, - navigator.size - zoomedMax - ]; - } else { - x = [left, left + zoomedMin, left + zoomedMax]; - y = [top, top, top]; - width = [ - zoomedMin, - zoomedMax - zoomedMin, - navigator.size - zoomedMax - ]; - height = [navigatorHeight, navigatorHeight, navigatorHeight]; - } - each(navigator.shades, function (shade, i) { - shade[verb]({ - x: x[i], - y: y[i], - width: width[i], - height: height[i] - }); - }); - }, - - /** - * Generate DOM elements for a navigator: - * - main navigator group - * - all shades - * - outline - * - handles - */ - renderElements: function () { - var navigator = this, - navigatorOptions = navigator.navigatorOptions, - maskInside = navigatorOptions.maskInside, - chart = navigator.chart, - inverted = chart.inverted, - renderer = chart.renderer, - navigatorGroup; - - // Create the main navigator group - navigator.navigatorGroup = navigatorGroup = renderer.g('navigator') - .attr({ - zIndex: 8, - visibility: 'hidden' - }) - .add(); - - - /*= if (build.classic) { =*/ - var mouseCursor = { - cursor: inverted ? 'ns-resize' : 'ew-resize' - }; - /*= } =*/ - - // Create masks, each mask will get events and fill: - each([!maskInside, maskInside, !maskInside], function (hasMask, index) { - navigator.shades[index] = renderer.rect() - .addClass('highcharts-navigator-mask' + - (index === 1 ? '-inside' : '-outside')) - /*= if (build.classic) { =*/ - .attr({ - fill: hasMask ? navigatorOptions.maskFill : 'rgba(0,0,0,0)' - }) - .css(index === 1 && mouseCursor) - /*= } =*/ - .add(navigatorGroup); - }); - - // Create the outline: - navigator.outline = renderer.path() - .addClass('highcharts-navigator-outline') - /*= if (build.classic) { =*/ - .attr({ - 'stroke-width': navigatorOptions.outlineWidth, - stroke: navigatorOptions.outlineColor - }) - /*= } =*/ - .add(navigatorGroup); - - // Create the handlers: - 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 - navigator.handles[index].attr({ zIndex: 7 - index }) - .addClass( - 'highcharts-navigator-handle ' + - 'highcharts-navigator-handle-' + - ['left', 'right'][index] - ).add(navigatorGroup); - - /*= if (build.classic) { =*/ - 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 - */ - update: function (options) { - // Remove references to old navigator series in base series - each(this.series || [], function (series) { - if (series.baseSeries) { - delete series.baseSeries.navigatorSeries; - } - }); - // Destroy and rebuild navigator - this.destroy(); - var chartOptions = this.chart.options; - merge(true, chartOptions.navigator, this.options, options); - this.init(this.chart); - }, - - /** - * Render the navigator - * @param {Number} min X axis value minimum - * @param {Number} max X axis value maximum - * @param {Number} pxMin Pixel value minimum - * @param {Number} pxMax Pixel value maximum - */ - render: function (min, max, pxMin, pxMax) { - - var navigator = this, - chart = navigator.chart, - navigatorWidth, - scrollbarLeft, - scrollbarTop, - scrollbarHeight = navigator.scrollbarHeight, - navigatorSize, - xAxis = navigator.xAxis, - scrollbarXAxis = xAxis.fake ? chart.xAxis[0] : xAxis, - navigatorEnabled = navigator.navigatorEnabled, - zoomedMin, - zoomedMax, - rendered = navigator.rendered, - inverted = chart.inverted, - verb, - newMin, - newMax, - 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; - } - - // Don't render the navigator until we have data (#486, #4202, #5172). - if (!isNumber(min) || !isNumber(max)) { - // However, if navigator was already rendered, we may need to resize - // it. For example hidden series, but visible navigator (#6022). - if (rendered) { - pxMin = 0; - pxMax = pick(xAxis.width, scrollbarXAxis.width); - } else { - return; - } - } - - navigator.left = pick( - xAxis.left, - // in case of scrollbar only, without navigator - chart.plotLeft + scrollbarHeight + (inverted ? chart.plotWidth : 0) - ); - - navigator.size = zoomedMax = navigatorSize = pick( - xAxis.len, - (inverted ? chart.plotHeight : chart.plotWidth) - - 2 * scrollbarHeight - ); - - if (inverted) { - navigatorWidth = scrollbarHeight; - } else { - navigatorWidth = navigatorSize + 2 * scrollbarHeight; - } - - // Get the pixel position of the handles - pxMin = pick(pxMin, xAxis.toPixels(min, true)); - pxMax = pick(pxMax, xAxis.toPixels(max, true)); - - // 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); - 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 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( - Math.max( - navigator.fixedWidth ? - navigator.zoomedMax - navigator.fixedWidth : - Math.min(pxMin, pxMax), - 0 - ), - zoomedMax - ); - - navigator.range = navigator.zoomedMax - navigator.zoomedMin; - - zoomedMax = Math.round(navigator.zoomedMax); - zoomedMin = Math.round(navigator.zoomedMin); - - if (navigatorEnabled) { - navigator.navigatorGroup.attr({ - visibility: 'visible' - }); - // Place elements - verb = rendered && !navigator.hasDragged ? 'animate' : 'attr'; - - navigator.drawMasks(zoomedMin, zoomedMax, inverted, verb); - navigator.drawOutline(zoomedMin, zoomedMax, 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; - scrollbarLeft = navigator.left - scrollbarHeight + - (navigatorEnabled || !scrollbarXAxis.opposite ? 0 : - // Multiple axes has offsets: - (scrollbarXAxis.titleOffset || 0) + - // Self margin from the axis.title - scrollbarXAxis.axisTitleMargin - ); - scrollbarHeight = navigatorSize + 2 * scrollbarHeight; - } else { - scrollbarTop = navigator.top + - (navigatorEnabled ? navigator.height : -scrollbarHeight); - scrollbarLeft = navigator.left - scrollbarHeight; - } - // Reposition scrollbar - navigator.scrollbar.position( - scrollbarLeft, - scrollbarTop, - navigatorWidth, - scrollbarHeight - ); - // Keep scale 0-1 - navigator.scrollbar.setRange( - // Use real value, not rounded because range can be very small - // (#1716) - navigator.zoomedMin / navigatorSize, - navigator.zoomedMax / navigatorSize - ); - } - navigator.rendered = true; - }, - - /** - * Set up the mouse and touch events for the navigator - */ - addMouseEvents: function () { - var navigator = this, - chart = navigator.chart, - container = chart.container, - eventsToUnbind = [], - mouseMoveHandler, - mouseUpHandler; - - /** - * Create mouse events' handlers. - * Make them as separate functions to enable wrapping them: - */ - navigator.mouseMoveHandler = mouseMoveHandler = function (e) { - navigator.onMouseMove(e); - }; - navigator.mouseUpHandler = mouseUpHandler = function (e) { - navigator.onMouseUp(e); - }; - - // 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 - eventsToUnbind.push( - addEvent(container, 'mousemove', mouseMoveHandler), - addEvent(container.ownerDocument, 'mouseup', mouseUpHandler) - ); - - // Touch events - if (hasTouch) { - eventsToUnbind.push( - addEvent(container, 'touchmove', mouseMoveHandler), - addEvent(container.ownerDocument, 'touchend', mouseUpHandler) - ); - eventsToUnbind.concat(navigator.getPartsEvents('touchstart')); - } - - navigator.eventsToUnbind = eventsToUnbind; - - // Data events - if (navigator.series && navigator.series[0]) { - eventsToUnbind.push( - addEvent( - navigator.series[0].xAxis, - 'foundExtremes', - function () { - chart.navigator.modifyNavigatorAxisExtremes(); - } - ) - ); - } - }, - - /** - * Generate events for handles and masks - * @param {String} eventName Event name handler, 'mousedown' or 'touchstart' - * @returns {Array} An array of arrays: [DOMElement, eventName, callback]. - */ - getPartsEvents: function (eventName) { - var navigator = this, - events = []; - each(['shades', 'handles'], function (name) { - each(navigator[name], function (navigatorItem, index) { - events.push( - addEvent( - navigatorItem.element, - eventName, - function (e) { - navigator[name + 'Mousedown'](e, index); - } - ) - ); - }); - }); - return events; - }, - - /** - * Mousedown on a shaded mask, either: - * - 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 - */ - shadesMousedown: function (e, index) { - e = this.chart.pointer.normalize(e); - - var navigator = this, - chart = navigator.chart, - xAxis = navigator.xAxis, - zoomedMin = navigator.zoomedMin, - navigatorPosition = navigator.left, - navigatorSize = navigator.size, - range = navigator.range, - chartX = e.chartX, - fixedMax, - fixedMin, - ext, - left; - - // For inverted chart, swap some options: - if (chart.inverted) { - chartX = e.chartY; - navigatorPosition = navigator.top; - } - - if (index === 1) { - // Store information for drag&drop - navigator.grabbedCenter = chartX; - navigator.fixedWidth = range; - navigator.dragOffset = chartX - zoomedMin; - } else { - // Shift the range by clicking on shaded areas - left = chartX - navigatorPosition - range / 2; - if (index === 0) { - left = Math.max(0, left); - } else if (index === 2 && left + range >= navigatorSize) { - left = navigatorSize - range; - if (xAxis.reversed) { - // #7713 - left -= range; - fixedMin = navigator.getUnionExtremes().dataMin; - } else { - // #2293, #3543 - fixedMax = navigator.getUnionExtremes().dataMax; - } - } - if (left !== zoomedMin) { // it has actually moved - navigator.fixedWidth = range; // #1370 - - ext = xAxis.toFixedRange( - left, - left + range, - fixedMin, - fixedMax - ); - if (defined(ext.min)) { // #7411 - chart.xAxis[0].setExtremes( - Math.min(ext.min, ext.max), - Math.max(ext.min, ext.max), - true, - null, // auto animation - { trigger: 'navigator' } - ); - } - } - } - }, - - /** - * Mousedown on a handle mask. - * Will store necessary information for drag&drop. - * - * @param {Object} e Mouse event - * @param {Number} index Index of a handle in Navigator.handles array - */ - handlesMousedown: function (e, index) { - e = this.chart.pointer.normalize(e); - - var navigator = this, - chart = navigator.chart, - baseXAxis = chart.xAxis[0], - // For reversed axes, min and max are chagned, - // so the other extreme should be stored - reverse = (chart.inverted && !baseXAxis.reversed) || - (!chart.inverted && baseXAxis.reversed); - - if (index === 0) { - // Grab the left handle - navigator.grabbedLeft = true; - navigator.otherHandlePos = navigator.zoomedMax; - navigator.fixedExtreme = reverse ? baseXAxis.min : baseXAxis.max; - } else { - // Grab the right handle - navigator.grabbedRight = true; - navigator.otherHandlePos = navigator.zoomedMin; - navigator.fixedExtreme = reverse ? baseXAxis.max : baseXAxis.min; - } - - chart.fixedRange = null; - }, - /** - * Mouse move event based on x/y mouse position. - * @param {Object} e Mouse event - */ - onMouseMove: function (e) { - var navigator = this, - chart = navigator.chart, - left = navigator.left, - navigatorSize = navigator.navigatorSize, - range = navigator.range, - 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 - - e = chart.pointer.normalize(e); - chartX = e.chartX; - - // Swap some options for inverted chart - if (inverted) { - left = navigator.top; - chartX = e.chartY; - } - - // Drag left handle or top handle - if (navigator.grabbedLeft) { - navigator.hasDragged = true; - navigator.render( - 0, - 0, - chartX - left, - navigator.otherHandlePos - ); - // Drag right handle or bottom handle - } else if (navigator.grabbedRight) { - navigator.hasDragged = true; - navigator.render( - 0, - 0, - navigator.otherHandlePos, - chartX - left - ); - // Drag scrollbar or open area in navigator - } else if (navigator.grabbedCenter) { - navigator.hasDragged = true; - if (chartX < dragOffset) { // outside left - chartX = dragOffset; - // 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 - setTimeout(function () { - navigator.onMouseUp(e); - }, 0); - } - } - }, - - /** - * Mouse up event based on x/y mouse position. - * @param {Object} e Mouse event - */ - onMouseUp: function (e) { - var navigator = this, - chart = navigator.chart, - xAxis = navigator.xAxis, - reversed = xAxis && xAxis.reversed, - scrollbar = navigator.scrollbar, - unionExtremes, - fixedMin, - fixedMax, - ext, - DOMEvent = e.DOMEvent || e; - - if ( - // MouseUp is called for both, navigator and scrollbar (that order), - // which causes calling afterSetExtremes twice. Prevent first call - // by checking if scrollbar is going to set new extremes (#6334) - (navigator.hasDragged && (!scrollbar || !scrollbar.hasDragged)) || - e.trigger === 'scrollbar' - ) { - unionExtremes = navigator.getUnionExtremes(); - - // When dragging one handle, make sure the other one doesn't change - if (navigator.zoomedMin === navigator.otherHandlePos) { - fixedMin = navigator.fixedExtreme; - } else if (navigator.zoomedMax === navigator.otherHandlePos) { - fixedMax = navigator.fixedExtreme; - } - // Snap to right edge (#4076) - if (navigator.zoomedMax === navigator.size) { - fixedMax = reversed ? - unionExtremes.dataMin : unionExtremes.dataMax; - } - - // Snap to left edge (#7576) - if (navigator.zoomedMin === 0) { - fixedMin = reversed ? - unionExtremes.dataMax : unionExtremes.dataMin; - } - - ext = xAxis.toFixedRange( - navigator.zoomedMin, - navigator.zoomedMax, - fixedMin, - fixedMax - ); - - if (defined(ext.min)) { - chart.xAxis[0].setExtremes( - Math.min(ext.min, ext.max), - Math.max(ext.min, ext.max), - true, - // 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 - } - ); - } - } - - if (e.DOMType !== 'mousemove') { - navigator.grabbedLeft = navigator.grabbedRight = - navigator.grabbedCenter = navigator.fixedWidth = - navigator.fixedExtreme = navigator.otherHandlePos = - navigator.hasDragged = navigator.dragOffset = null; - } - }, - - /** - * Removes the event handlers attached previously with addEvents. - */ - removeEvents: function () { - if (this.eventsToUnbind) { - each(this.eventsToUnbind, function (unbind) { - unbind(); - }); - this.eventsToUnbind = undefined; - } - this.removeBaseSeriesEvents(); - }, - - /** - * Remove data events. - */ - removeBaseSeriesEvents: function () { - var baseSeries = this.baseSeries || []; - if (this.navigatorEnabled && baseSeries[0]) { - if (this.navigatorOptions.adaptToUpdatedData !== false) { - each(baseSeries, function (series) { - removeEvent(series, 'updatedData', this.updatedDataHandler); - }, this); - } - - // We only listen for extremes-events on the first baseSeries - if (baseSeries[0].xAxis) { - removeEvent( - baseSeries[0].xAxis, - 'foundExtremes', - this.modifyBaseAxisExtremes - ); - } - } - }, - - /** - * Initiate the Navigator object - */ - init: function (chart) { - var chartOptions = chart.options, - navigatorOptions = chartOptions.navigator, - navigatorEnabled = navigatorOptions.enabled, - scrollbarOptions = chartOptions.scrollbar, - scrollbarEnabled = scrollbarOptions.enabled, - height = navigatorEnabled ? navigatorOptions.height : 0, - scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0; - - this.handles = []; - this.shades = []; - - this.chart = chart; - this.setBaseSeries(); - - this.height = height; - this.scrollbarHeight = scrollbarHeight; - this.scrollbarEnabled = scrollbarEnabled; - this.navigatorEnabled = navigatorEnabled; - this.navigatorOptions = navigatorOptions; - this.scrollbarOptions = scrollbarOptions; - this.outlineHeight = height + scrollbarHeight; - - 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] || { options: {} }; - - // 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 - }; - if (chart.inverted) { - chart.extraMargin.type = navigator.opposite ? - 'marginRight' : - 'plotLeft'; - } - chart.isDirtyBox = true; - - if (navigator.navigatorEnabled) { - // an x axis is required for scrollbar also - navigator.xAxis = new Axis(chart, merge({ - // inherit base xAxis' break and ordinal options - breaks: baseXaxis.options.breaks, - ordinal: baseXaxis.options.ordinal - }, navigatorOptions.xAxis, { - id: 'navigator-x-axis', - yAxis: 'navigator-y-axis', - isX: true, - type: 'datetime', - index: xAxisIndex, - offset: 0, - keepOrdinalPadding: true, // #2436 - startOnTick: false, - endOnTick: false, - minPadding: 0, - maxPadding: 0, - zoomEnabled: false - }, chart.inverted ? { - offsets: [scrollbarHeight, 0, -scrollbarHeight, 0], - width: height - } : { - offsets: [0, -scrollbarHeight, 0, scrollbarHeight], - height: height - })); - - navigator.yAxis = new Axis(chart, merge(navigatorOptions.yAxis, { - id: 'navigator-y-axis', - alignTicks: false, - offset: 0, - index: yAxisIndex, - zoomEnabled: false - }, chart.inverted ? { - width: height - } : { - height: height - })); - - // If we have a base series, initialize the navigator series - if (baseSeries || navigatorOptions.series.data) { - navigator.updateNavigatorSeries(false); - - // If not, set up an event to listen for added series - } else if (chart.series.length === 0) { - - navigator.unbindRedraw = addEvent( - chart, - 'beforeRedraw', - function () { - // We've got one, now add it as base - if (chart.series.length > 0 && !navigator.series) { - navigator.setBaseSeries(); - navigator.unbindRedraw(); // reset - } - } - ); - } - - // Render items, so we can bind events to them: - navigator.renderElements(); - // Add mouse events - navigator.addMouseEvents(); - - // in case of scrollbar only, fake an x axis to get translation - } else { - navigator.xAxis = { - 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; - - return reverse ? - // from pixel to value - (value * valueRange / scrollTrackWidth) + min : - // from value to pixel - scrollTrackWidth * (value - min) / valueRange; - }, - toPixels: function (value) { - return this.translate(value); - }, - toValue: function (value) { - return this.translate(value, true); - }, - toFixedRange: Axis.prototype.toFixedRange, - fake: true - }; - } - - - // Initialize the scrollbar - if (chart.options.scrollbar.enabled) { - chart.scrollbar = navigator.scrollbar = new Scrollbar( - chart.renderer, - merge(chart.options.scrollbar, { - margin: navigator.navigatorEnabled ? 0 : 10, - vertical: chart.inverted - }), - chart - ); - addEvent(navigator.scrollbar, 'changed', function (e) { - var range = navigator.size, - to = range * this.to, - from = range * this.from; - - navigator.hasDragged = navigator.scrollbar.hasDragged; - navigator.render(0, 0, from, to); - - if ( - chart.options.scrollbar.liveRedraw || - ( - e.DOMType !== 'mousemove' && - e.DOMType !== 'touchmove' - ) - ) { - setTimeout(function () { - navigator.onMouseUp(e); - }); - } - }); - } - - // Add data events - navigator.addBaseSeriesEvents(); - // 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. - * @param {boolean} returnFalseOnNoBaseSeries - as the param says. - */ - getUnionExtremes: function (returnFalseOnNoBaseSeries) { - var baseAxis = this.chart.xAxis[0], - navAxis = this.xAxis, - navAxisOptions = navAxis.options, - baseAxisOptions = baseAxis.options, - ret; - - if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) { - ret = { - dataMin: pick( // #4053 - navAxisOptions && navAxisOptions.min, - numExt( - 'min', - baseAxisOptions.min, - baseAxis.dataMin, - navAxis.dataMin, - navAxis.min - ) - ), - dataMax: pick( - navAxisOptions && navAxisOptions.max, - numExt( - 'max', - baseAxisOptions.max, - baseAxis.dataMax, - navAxis.dataMax, - navAxis.max - ) - ) - }; - } - 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 - * from the outside - * @param {Object} baseSeriesOptions - * Additional series options for a navigator - * @param {Boolean} [redraw] - * Whether to redraw after update. - */ - setBaseSeries: function (baseSeriesOptions, redraw) { - var chart = this.chart, - baseSeries = this.baseSeries = []; - - baseSeriesOptions = ( - baseSeriesOptions || - chart.options && chart.options.navigator.baseSeries || - 0 - ); - - // Iterate through series and add the ones that should be shown in - // navigator. - each(chart.series || [], function (series, i) { - if ( - // Don't include existing nav series - !series.options.isInternal && - ( - series.options.showInNavigator || - ( - i === baseSeriesOptions || - series.options.id === baseSeriesOptions - ) && - series.options.showInNavigator !== false - ) - ) { - baseSeries.push(series); - } - }); - - // When run after render, this.xAxis already exists - if (this.xAxis && !this.xAxis.fake) { - this.updateNavigatorSeries(true, redraw); - } - }, - - /* - * Update series in the navigator from baseSeries, adding new if does not - * exist. - */ - updateNavigatorSeries: function (addEvents, redraw) { - var navigator = this, - chart = navigator.chart, - baseSeries = navigator.baseSeries, - baseOptions, - mergedNavSeriesOptions, - chartNavigatorSeriesOptions = navigator.navigatorOptions.series, - baseNavigatorOptions, - navSeriesMixin = { - enableMouseTracking: false, - index: null, // #6162 - linkedTo: null, // #6734 - group: 'nav', // for columns - padXAxis: false, - xAxis: 'navigator-x-axis', - yAxis: 'navigator-y-axis', - showInLegend: false, - stacking: false, // #4823 - isInternal: true, - visible: true - }, - // Remove navigator series that are no longer in the baseSeries - 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 (base) { - removeEvent( - base, - 'updatedData', - navigator.updatedDataHandler - ); - delete base.navigatorSeries; - } - // Kill the nav series - navSeries.destroy(); - return false; - } - return true; - } - ); - - // Go through each base series and merge the options to create new - // series - if (baseSeries && baseSeries.length) { - each(baseSeries, function eachBaseSeries(base) { - var linkedNavSeries = base.navigatorSeries, - 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 ' + 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); - - // Update or add the series - if (linkedNavSeries && linkedNavSeries.options) { - linkedNavSeries.update(mergedNavSeriesOptions, redraw); - } else { - 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 - // 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) { - 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 - // 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] - }, - navSeriesMixin, - userSeriesOptions - ); - mergedNavSeriesOptions.data = userSeriesOptions.data; - if (mergedNavSeriesOptions.data) { - navigator.hasNavigatorData = true; - navigatorSeries.push( - chart.initSeries(mergedNavSeriesOptions) - ); - } - }); - } - - if (addEvents) { - this.addBaseSeriesEvents(); - } - }, - - /** - * Add data events. - * For example when main series is updated we need to recalculate extremes - */ - addBaseSeriesEvents: function () { - 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 - // duplicates should be discarded by the browser. - if (baseSeries[0] && baseSeries[0].xAxis) { - addEvent( - baseSeries[0].xAxis, - 'foundExtremes', - this.modifyBaseAxisExtremes - ); - } - - each(baseSeries, function (base) { - // Link base series show/hide to navigator series visibility - addEvent(base, 'show', function () { - if (this.navigatorSeries) { - this.navigatorSeries.setVisible(true, false); - } - }); - addEvent(base, 'hide', function () { - if (this.navigatorSeries) { - this.navigatorSeries.setVisible(false, false); - } - }); - - // 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); - } - } - - // Handle series removal - addEvent(base, 'remove', function () { - if (this.navigatorSeries) { - erase(navigator.series, this.navigatorSeries); - if (defined(this.navigatorSeries.options)) { - this.navigatorSeries.remove(false); - } - delete this.navigatorSeries; - } - }); - }, 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. - */ - modifyNavigatorAxisExtremes: function () { - var xAxis = this.xAxis, - unionExtremes; - - if (xAxis.getExtremes) { - unionExtremes = this.getUnionExtremes(true); - if ( - unionExtremes && - ( - unionExtremes.dataMin !== xAxis.min || - unionExtremes.dataMax !== xAxis.max - ) - ) { - xAxis.min = unionExtremes.dataMin; - xAxis.max = unionExtremes.dataMax; - } - } - }, - - /** - * Hook to modify the base axis extremes with information from the Navigator - */ - modifyBaseAxisExtremes: function () { - var baseXAxis = this, - navigator = baseXAxis.chart.navigator, - baseExtremes = baseXAxis.getExtremes(), - baseMin = baseExtremes.min, - baseMax = baseExtremes.max, - baseDataMin = baseExtremes.dataMin, - baseDataMax = baseExtremes.dataMax, - range = baseMax - baseMin, - stickToMin = navigator.stickToMin, - stickToMax = navigator.stickToMax, - overscroll = pick(baseXAxis.options.overscroll, 0), - 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'; - - if (!unmutable) { - - // 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 (stickToMax) { - 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 - ); - } - } - - // Update the extremes - if (hasSetExtremes && (stickToMin || stickToMax)) { - if (isNumber(newMin)) { - baseXAxis.min = baseXAxis.userMin = newMin; - baseXAxis.max = baseXAxis.userMax = newMax; - } - } - } - - // 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. - */ - 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 = navigator.xAxis.reversed ? - Math.round(navigator.zoomedMin) === 0 : - 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. - 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 - } - }, - - /** - * 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 - var navigator = this.navigator, - xAxis = navigator && ( - navigator.baseSeries && - navigator.baseSeries[0] && - navigator.baseSeries[0].xAxis || - navigator.scrollbar && this.xAxis[0] - ); // #5709 - - if (xAxis) { - navigator.render(xAxis.min, xAxis.max); - } - }); - }, - - /** - * Destroys allocated elements. - */ - destroy: function () { - - // Disconnect events added in addEvents - this.removeEvents(); - - if (this.xAxis) { - erase(this.chart.xAxis, this.xAxis); - erase(this.chart.axes, this.xAxis); - } - if (this.yAxis) { - erase(this.chart.yAxis, this.yAxis); - erase(this.chart.axes, this.yAxis); - } - // Destroy series - each(this.series || [], function (s) { - if (s.destroy) { - s.destroy(); - } - }); - - // Destroy properties - each([ - 'series', 'xAxis', 'yAxis', 'shades', 'outline', 'scrollbarTrack', - 'scrollbarRifles', 'scrollbarGroup', 'scrollbar', 'navigatorGroup', - 'rendered' - ], function (prop) { - if (this[prop] && this[prop].destroy) { - this[prop].destroy(); - } - this[prop] = null; - }, this); - - // Destroy elements in collection - each([this.handles], function (coll) { - destroyObjectProperties(coll); - }, this); - } + /** + * Draw one of the handles on the side of the zoomed range in the navigator + * @param {Number} x The x center for the handle + * @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, + height = navigator.navigatorOptions.handles.height; + + // Place it + navigator.handles[index][verb](inverted ? { + 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 - height / 2 - 1 + ) + }); + }, + + /** + * 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' + */ + drawOutline: function (zoomedMin, zoomedMax, inverted, verb) { + var navigator = this, + maskInside = navigator.navigatorOptions.maskInside, + outlineWidth = navigator.outline.strokeWidth(), + halfOutline = outlineWidth / 2, + outlineCorrection = (outlineWidth % 2) / 2, // #5800 + outlineHeight = navigator.outlineHeight, + scrollbarHeight = navigator.scrollbarHeight, + navigatorSize = navigator.size, + left = navigator.left - scrollbarHeight, + navigatorTop = navigator.top, + verticalMin, + path; + + if (inverted) { + left -= halfOutline; + verticalMin = navigatorTop + zoomedMax + outlineCorrection; + zoomedMax = navigatorTop + zoomedMin + outlineCorrection; + + path = [ + 'M', + left + outlineHeight, + navigatorTop - scrollbarHeight - outlineCorrection, // top edge + 'L', + left + outlineHeight, + verticalMin, // top right of zoomed range + 'L', + left, + verticalMin, // top left of z.r. + 'L', + left, + zoomedMax, // bottom left of z.r. + 'L', + left + outlineHeight, + zoomedMax, // bottom right of z.r. + 'L', + left + outlineHeight, + navigatorTop + navigatorSize + scrollbarHeight // bottom edge + ].concat(maskInside ? [ + 'M', + left + outlineHeight, + verticalMin - halfOutline, // upper left of zoomed range + 'L', + left + outlineHeight, + zoomedMax + halfOutline // upper right of z.r. + ] : []); + } else { + zoomedMin += left + scrollbarHeight - outlineCorrection; + zoomedMax += left + scrollbarHeight - outlineCorrection; + navigatorTop += halfOutline; + + path = [ + 'M', + left, + navigatorTop, // left + 'L', + zoomedMin, + navigatorTop, // upper left of zoomed range + 'L', + zoomedMin, + navigatorTop + outlineHeight, // lower left of z.r. + 'L', + zoomedMax, + navigatorTop + outlineHeight, // lower right of z.r. + 'L', + zoomedMax, + navigatorTop, // upper right of z.r. + 'L', + left + navigatorSize + scrollbarHeight * 2, + navigatorTop // right + ].concat(maskInside ? [ + 'M', + zoomedMin - halfOutline, + navigatorTop, // upper left of zoomed range + 'L', + zoomedMax + halfOutline, + navigatorTop // upper right of z.r. + ] : []); + } + navigator.outline[verb]({ + d: path + }); + }, + + /** + * 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' + */ + drawMasks: function (zoomedMin, zoomedMax, inverted, verb) { + var navigator = this, + left = navigator.left, + top = navigator.top, + navigatorHeight = navigator.height, + height, + width, + x, + y; + + // 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]; + height = [ + zoomedMin, + zoomedMax - zoomedMin, + navigator.size - zoomedMax + ]; + } else { + x = [left, left + zoomedMin, left + zoomedMax]; + y = [top, top, top]; + width = [ + zoomedMin, + zoomedMax - zoomedMin, + navigator.size - zoomedMax + ]; + height = [navigatorHeight, navigatorHeight, navigatorHeight]; + } + each(navigator.shades, function (shade, i) { + shade[verb]({ + x: x[i], + y: y[i], + width: width[i], + height: height[i] + }); + }); + }, + + /** + * Generate DOM elements for a navigator: + * - main navigator group + * - all shades + * - outline + * - handles + */ + renderElements: function () { + var navigator = this, + navigatorOptions = navigator.navigatorOptions, + maskInside = navigatorOptions.maskInside, + chart = navigator.chart, + inverted = chart.inverted, + renderer = chart.renderer, + navigatorGroup; + + // Create the main navigator group + navigator.navigatorGroup = navigatorGroup = renderer.g('navigator') + .attr({ + zIndex: 8, + visibility: 'hidden' + }) + .add(); + + + /*= if (build.classic) { =*/ + var mouseCursor = { + cursor: inverted ? 'ns-resize' : 'ew-resize' + }; + /*= } =*/ + + // Create masks, each mask will get events and fill: + each([!maskInside, maskInside, !maskInside], function (hasMask, index) { + navigator.shades[index] = renderer.rect() + .addClass('highcharts-navigator-mask' + + (index === 1 ? '-inside' : '-outside')) + /*= if (build.classic) { =*/ + .attr({ + fill: hasMask ? navigatorOptions.maskFill : 'rgba(0,0,0,0)' + }) + .css(index === 1 && mouseCursor) + /*= } =*/ + .add(navigatorGroup); + }); + + // Create the outline: + navigator.outline = renderer.path() + .addClass('highcharts-navigator-outline') + /*= if (build.classic) { =*/ + .attr({ + 'stroke-width': navigatorOptions.outlineWidth, + stroke: navigatorOptions.outlineColor + }) + /*= } =*/ + .add(navigatorGroup); + + // Create the handlers: + 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 + navigator.handles[index].attr({ zIndex: 7 - index }) + .addClass( + 'highcharts-navigator-handle ' + + 'highcharts-navigator-handle-' + + ['left', 'right'][index] + ).add(navigatorGroup); + + /*= if (build.classic) { =*/ + 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 + */ + update: function (options) { + // Remove references to old navigator series in base series + each(this.series || [], function (series) { + if (series.baseSeries) { + delete series.baseSeries.navigatorSeries; + } + }); + // Destroy and rebuild navigator + this.destroy(); + var chartOptions = this.chart.options; + merge(true, chartOptions.navigator, this.options, options); + this.init(this.chart); + }, + + /** + * Render the navigator + * @param {Number} min X axis value minimum + * @param {Number} max X axis value maximum + * @param {Number} pxMin Pixel value minimum + * @param {Number} pxMax Pixel value maximum + */ + render: function (min, max, pxMin, pxMax) { + + var navigator = this, + chart = navigator.chart, + navigatorWidth, + scrollbarLeft, + scrollbarTop, + scrollbarHeight = navigator.scrollbarHeight, + navigatorSize, + xAxis = navigator.xAxis, + scrollbarXAxis = xAxis.fake ? chart.xAxis[0] : xAxis, + navigatorEnabled = navigator.navigatorEnabled, + zoomedMin, + zoomedMax, + rendered = navigator.rendered, + inverted = chart.inverted, + verb, + newMin, + newMax, + 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; + } + + // Don't render the navigator until we have data (#486, #4202, #5172). + if (!isNumber(min) || !isNumber(max)) { + // However, if navigator was already rendered, we may need to resize + // it. For example hidden series, but visible navigator (#6022). + if (rendered) { + pxMin = 0; + pxMax = pick(xAxis.width, scrollbarXAxis.width); + } else { + return; + } + } + + navigator.left = pick( + xAxis.left, + // in case of scrollbar only, without navigator + chart.plotLeft + scrollbarHeight + (inverted ? chart.plotWidth : 0) + ); + + navigator.size = zoomedMax = navigatorSize = pick( + xAxis.len, + (inverted ? chart.plotHeight : chart.plotWidth) - + 2 * scrollbarHeight + ); + + if (inverted) { + navigatorWidth = scrollbarHeight; + } else { + navigatorWidth = navigatorSize + 2 * scrollbarHeight; + } + + // Get the pixel position of the handles + pxMin = pick(pxMin, xAxis.toPixels(min, true)); + pxMax = pick(pxMax, xAxis.toPixels(max, true)); + + // 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); + 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 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( + Math.max( + navigator.fixedWidth ? + navigator.zoomedMax - navigator.fixedWidth : + Math.min(pxMin, pxMax), + 0 + ), + zoomedMax + ); + + navigator.range = navigator.zoomedMax - navigator.zoomedMin; + + zoomedMax = Math.round(navigator.zoomedMax); + zoomedMin = Math.round(navigator.zoomedMin); + + if (navigatorEnabled) { + navigator.navigatorGroup.attr({ + visibility: 'visible' + }); + // Place elements + verb = rendered && !navigator.hasDragged ? 'animate' : 'attr'; + + navigator.drawMasks(zoomedMin, zoomedMax, inverted, verb); + navigator.drawOutline(zoomedMin, zoomedMax, 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; + scrollbarLeft = navigator.left - scrollbarHeight + + (navigatorEnabled || !scrollbarXAxis.opposite ? 0 : + // Multiple axes has offsets: + (scrollbarXAxis.titleOffset || 0) + + // Self margin from the axis.title + scrollbarXAxis.axisTitleMargin + ); + scrollbarHeight = navigatorSize + 2 * scrollbarHeight; + } else { + scrollbarTop = navigator.top + + (navigatorEnabled ? navigator.height : -scrollbarHeight); + scrollbarLeft = navigator.left - scrollbarHeight; + } + // Reposition scrollbar + navigator.scrollbar.position( + scrollbarLeft, + scrollbarTop, + navigatorWidth, + scrollbarHeight + ); + // Keep scale 0-1 + navigator.scrollbar.setRange( + // Use real value, not rounded because range can be very small + // (#1716) + navigator.zoomedMin / navigatorSize, + navigator.zoomedMax / navigatorSize + ); + } + navigator.rendered = true; + }, + + /** + * Set up the mouse and touch events for the navigator + */ + addMouseEvents: function () { + var navigator = this, + chart = navigator.chart, + container = chart.container, + eventsToUnbind = [], + mouseMoveHandler, + mouseUpHandler; + + /** + * Create mouse events' handlers. + * Make them as separate functions to enable wrapping them: + */ + navigator.mouseMoveHandler = mouseMoveHandler = function (e) { + navigator.onMouseMove(e); + }; + navigator.mouseUpHandler = mouseUpHandler = function (e) { + navigator.onMouseUp(e); + }; + + // 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 + eventsToUnbind.push( + addEvent(container, 'mousemove', mouseMoveHandler), + addEvent(container.ownerDocument, 'mouseup', mouseUpHandler) + ); + + // Touch events + if (hasTouch) { + eventsToUnbind.push( + addEvent(container, 'touchmove', mouseMoveHandler), + addEvent(container.ownerDocument, 'touchend', mouseUpHandler) + ); + eventsToUnbind.concat(navigator.getPartsEvents('touchstart')); + } + + navigator.eventsToUnbind = eventsToUnbind; + + // Data events + if (navigator.series && navigator.series[0]) { + eventsToUnbind.push( + addEvent( + navigator.series[0].xAxis, + 'foundExtremes', + function () { + chart.navigator.modifyNavigatorAxisExtremes(); + } + ) + ); + } + }, + + /** + * Generate events for handles and masks + * @param {String} eventName Event name handler, 'mousedown' or 'touchstart' + * @returns {Array} An array of arrays: [DOMElement, eventName, callback]. + */ + getPartsEvents: function (eventName) { + var navigator = this, + events = []; + each(['shades', 'handles'], function (name) { + each(navigator[name], function (navigatorItem, index) { + events.push( + addEvent( + navigatorItem.element, + eventName, + function (e) { + navigator[name + 'Mousedown'](e, index); + } + ) + ); + }); + }); + return events; + }, + + /** + * Mousedown on a shaded mask, either: + * - 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 + */ + shadesMousedown: function (e, index) { + e = this.chart.pointer.normalize(e); + + var navigator = this, + chart = navigator.chart, + xAxis = navigator.xAxis, + zoomedMin = navigator.zoomedMin, + navigatorPosition = navigator.left, + navigatorSize = navigator.size, + range = navigator.range, + chartX = e.chartX, + fixedMax, + fixedMin, + ext, + left; + + // For inverted chart, swap some options: + if (chart.inverted) { + chartX = e.chartY; + navigatorPosition = navigator.top; + } + + if (index === 1) { + // Store information for drag&drop + navigator.grabbedCenter = chartX; + navigator.fixedWidth = range; + navigator.dragOffset = chartX - zoomedMin; + } else { + // Shift the range by clicking on shaded areas + left = chartX - navigatorPosition - range / 2; + if (index === 0) { + left = Math.max(0, left); + } else if (index === 2 && left + range >= navigatorSize) { + left = navigatorSize - range; + if (xAxis.reversed) { + // #7713 + left -= range; + fixedMin = navigator.getUnionExtremes().dataMin; + } else { + // #2293, #3543 + fixedMax = navigator.getUnionExtremes().dataMax; + } + } + if (left !== zoomedMin) { // it has actually moved + navigator.fixedWidth = range; // #1370 + + ext = xAxis.toFixedRange( + left, + left + range, + fixedMin, + fixedMax + ); + if (defined(ext.min)) { // #7411 + chart.xAxis[0].setExtremes( + Math.min(ext.min, ext.max), + Math.max(ext.min, ext.max), + true, + null, // auto animation + { trigger: 'navigator' } + ); + } + } + } + }, + + /** + * Mousedown on a handle mask. + * Will store necessary information for drag&drop. + * + * @param {Object} e Mouse event + * @param {Number} index Index of a handle in Navigator.handles array + */ + handlesMousedown: function (e, index) { + e = this.chart.pointer.normalize(e); + + var navigator = this, + chart = navigator.chart, + baseXAxis = chart.xAxis[0], + // For reversed axes, min and max are chagned, + // so the other extreme should be stored + reverse = (chart.inverted && !baseXAxis.reversed) || + (!chart.inverted && baseXAxis.reversed); + + if (index === 0) { + // Grab the left handle + navigator.grabbedLeft = true; + navigator.otherHandlePos = navigator.zoomedMax; + navigator.fixedExtreme = reverse ? baseXAxis.min : baseXAxis.max; + } else { + // Grab the right handle + navigator.grabbedRight = true; + navigator.otherHandlePos = navigator.zoomedMin; + navigator.fixedExtreme = reverse ? baseXAxis.max : baseXAxis.min; + } + + chart.fixedRange = null; + }, + /** + * Mouse move event based on x/y mouse position. + * @param {Object} e Mouse event + */ + onMouseMove: function (e) { + var navigator = this, + chart = navigator.chart, + left = navigator.left, + navigatorSize = navigator.navigatorSize, + range = navigator.range, + 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 + + e = chart.pointer.normalize(e); + chartX = e.chartX; + + // Swap some options for inverted chart + if (inverted) { + left = navigator.top; + chartX = e.chartY; + } + + // Drag left handle or top handle + if (navigator.grabbedLeft) { + navigator.hasDragged = true; + navigator.render( + 0, + 0, + chartX - left, + navigator.otherHandlePos + ); + // Drag right handle or bottom handle + } else if (navigator.grabbedRight) { + navigator.hasDragged = true; + navigator.render( + 0, + 0, + navigator.otherHandlePos, + chartX - left + ); + // Drag scrollbar or open area in navigator + } else if (navigator.grabbedCenter) { + navigator.hasDragged = true; + if (chartX < dragOffset) { // outside left + chartX = dragOffset; + // 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 + setTimeout(function () { + navigator.onMouseUp(e); + }, 0); + } + } + }, + + /** + * Mouse up event based on x/y mouse position. + * @param {Object} e Mouse event + */ + onMouseUp: function (e) { + var navigator = this, + chart = navigator.chart, + xAxis = navigator.xAxis, + reversed = xAxis && xAxis.reversed, + scrollbar = navigator.scrollbar, + unionExtremes, + fixedMin, + fixedMax, + ext, + DOMEvent = e.DOMEvent || e; + + if ( + // MouseUp is called for both, navigator and scrollbar (that order), + // which causes calling afterSetExtremes twice. Prevent first call + // by checking if scrollbar is going to set new extremes (#6334) + (navigator.hasDragged && (!scrollbar || !scrollbar.hasDragged)) || + e.trigger === 'scrollbar' + ) { + unionExtremes = navigator.getUnionExtremes(); + + // When dragging one handle, make sure the other one doesn't change + if (navigator.zoomedMin === navigator.otherHandlePos) { + fixedMin = navigator.fixedExtreme; + } else if (navigator.zoomedMax === navigator.otherHandlePos) { + fixedMax = navigator.fixedExtreme; + } + // Snap to right edge (#4076) + if (navigator.zoomedMax === navigator.size) { + fixedMax = reversed ? + unionExtremes.dataMin : unionExtremes.dataMax; + } + + // Snap to left edge (#7576) + if (navigator.zoomedMin === 0) { + fixedMin = reversed ? + unionExtremes.dataMax : unionExtremes.dataMin; + } + + ext = xAxis.toFixedRange( + navigator.zoomedMin, + navigator.zoomedMax, + fixedMin, + fixedMax + ); + + if (defined(ext.min)) { + chart.xAxis[0].setExtremes( + Math.min(ext.min, ext.max), + Math.max(ext.min, ext.max), + true, + // 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 + } + ); + } + } + + if (e.DOMType !== 'mousemove') { + navigator.grabbedLeft = navigator.grabbedRight = + navigator.grabbedCenter = navigator.fixedWidth = + navigator.fixedExtreme = navigator.otherHandlePos = + navigator.hasDragged = navigator.dragOffset = null; + } + }, + + /** + * Removes the event handlers attached previously with addEvents. + */ + removeEvents: function () { + if (this.eventsToUnbind) { + each(this.eventsToUnbind, function (unbind) { + unbind(); + }); + this.eventsToUnbind = undefined; + } + this.removeBaseSeriesEvents(); + }, + + /** + * Remove data events. + */ + removeBaseSeriesEvents: function () { + var baseSeries = this.baseSeries || []; + if (this.navigatorEnabled && baseSeries[0]) { + if (this.navigatorOptions.adaptToUpdatedData !== false) { + each(baseSeries, function (series) { + removeEvent(series, 'updatedData', this.updatedDataHandler); + }, this); + } + + // We only listen for extremes-events on the first baseSeries + if (baseSeries[0].xAxis) { + removeEvent( + baseSeries[0].xAxis, + 'foundExtremes', + this.modifyBaseAxisExtremes + ); + } + } + }, + + /** + * Initiate the Navigator object + */ + init: function (chart) { + var chartOptions = chart.options, + navigatorOptions = chartOptions.navigator, + navigatorEnabled = navigatorOptions.enabled, + scrollbarOptions = chartOptions.scrollbar, + scrollbarEnabled = scrollbarOptions.enabled, + height = navigatorEnabled ? navigatorOptions.height : 0, + scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0; + + this.handles = []; + this.shades = []; + + this.chart = chart; + this.setBaseSeries(); + + this.height = height; + this.scrollbarHeight = scrollbarHeight; + this.scrollbarEnabled = scrollbarEnabled; + this.navigatorEnabled = navigatorEnabled; + this.navigatorOptions = navigatorOptions; + this.scrollbarOptions = scrollbarOptions; + this.outlineHeight = height + scrollbarHeight; + + 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] || { options: {} }; + + // 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 + }; + if (chart.inverted) { + chart.extraMargin.type = navigator.opposite ? + 'marginRight' : + 'plotLeft'; + } + chart.isDirtyBox = true; + + if (navigator.navigatorEnabled) { + // an x axis is required for scrollbar also + navigator.xAxis = new Axis(chart, merge({ + // inherit base xAxis' break and ordinal options + breaks: baseXaxis.options.breaks, + ordinal: baseXaxis.options.ordinal + }, navigatorOptions.xAxis, { + id: 'navigator-x-axis', + yAxis: 'navigator-y-axis', + isX: true, + type: 'datetime', + index: xAxisIndex, + offset: 0, + keepOrdinalPadding: true, // #2436 + startOnTick: false, + endOnTick: false, + minPadding: 0, + maxPadding: 0, + zoomEnabled: false + }, chart.inverted ? { + offsets: [scrollbarHeight, 0, -scrollbarHeight, 0], + width: height + } : { + offsets: [0, -scrollbarHeight, 0, scrollbarHeight], + height: height + })); + + navigator.yAxis = new Axis(chart, merge(navigatorOptions.yAxis, { + id: 'navigator-y-axis', + alignTicks: false, + offset: 0, + index: yAxisIndex, + zoomEnabled: false + }, chart.inverted ? { + width: height + } : { + height: height + })); + + // If we have a base series, initialize the navigator series + if (baseSeries || navigatorOptions.series.data) { + navigator.updateNavigatorSeries(false); + + // If not, set up an event to listen for added series + } else if (chart.series.length === 0) { + + navigator.unbindRedraw = addEvent( + chart, + 'beforeRedraw', + function () { + // We've got one, now add it as base + if (chart.series.length > 0 && !navigator.series) { + navigator.setBaseSeries(); + navigator.unbindRedraw(); // reset + } + } + ); + } + + // Render items, so we can bind events to them: + navigator.renderElements(); + // Add mouse events + navigator.addMouseEvents(); + + // in case of scrollbar only, fake an x axis to get translation + } else { + navigator.xAxis = { + 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; + + return reverse ? + // from pixel to value + (value * valueRange / scrollTrackWidth) + min : + // from value to pixel + scrollTrackWidth * (value - min) / valueRange; + }, + toPixels: function (value) { + return this.translate(value); + }, + toValue: function (value) { + return this.translate(value, true); + }, + toFixedRange: Axis.prototype.toFixedRange, + fake: true + }; + } + + + // Initialize the scrollbar + if (chart.options.scrollbar.enabled) { + chart.scrollbar = navigator.scrollbar = new Scrollbar( + chart.renderer, + merge(chart.options.scrollbar, { + margin: navigator.navigatorEnabled ? 0 : 10, + vertical: chart.inverted + }), + chart + ); + addEvent(navigator.scrollbar, 'changed', function (e) { + var range = navigator.size, + to = range * this.to, + from = range * this.from; + + navigator.hasDragged = navigator.scrollbar.hasDragged; + navigator.render(0, 0, from, to); + + if ( + chart.options.scrollbar.liveRedraw || + ( + e.DOMType !== 'mousemove' && + e.DOMType !== 'touchmove' + ) + ) { + setTimeout(function () { + navigator.onMouseUp(e); + }); + } + }); + } + + // Add data events + navigator.addBaseSeriesEvents(); + // 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. + * @param {boolean} returnFalseOnNoBaseSeries - as the param says. + */ + getUnionExtremes: function (returnFalseOnNoBaseSeries) { + var baseAxis = this.chart.xAxis[0], + navAxis = this.xAxis, + navAxisOptions = navAxis.options, + baseAxisOptions = baseAxis.options, + ret; + + if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) { + ret = { + dataMin: pick( // #4053 + navAxisOptions && navAxisOptions.min, + numExt( + 'min', + baseAxisOptions.min, + baseAxis.dataMin, + navAxis.dataMin, + navAxis.min + ) + ), + dataMax: pick( + navAxisOptions && navAxisOptions.max, + numExt( + 'max', + baseAxisOptions.max, + baseAxis.dataMax, + navAxis.dataMax, + navAxis.max + ) + ) + }; + } + 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 + * from the outside + * @param {Object} baseSeriesOptions + * Additional series options for a navigator + * @param {Boolean} [redraw] + * Whether to redraw after update. + */ + setBaseSeries: function (baseSeriesOptions, redraw) { + var chart = this.chart, + baseSeries = this.baseSeries = []; + + baseSeriesOptions = ( + baseSeriesOptions || + chart.options && chart.options.navigator.baseSeries || + 0 + ); + + // Iterate through series and add the ones that should be shown in + // navigator. + each(chart.series || [], function (series, i) { + if ( + // Don't include existing nav series + !series.options.isInternal && + ( + series.options.showInNavigator || + ( + i === baseSeriesOptions || + series.options.id === baseSeriesOptions + ) && + series.options.showInNavigator !== false + ) + ) { + baseSeries.push(series); + } + }); + + // When run after render, this.xAxis already exists + if (this.xAxis && !this.xAxis.fake) { + this.updateNavigatorSeries(true, redraw); + } + }, + + /* + * Update series in the navigator from baseSeries, adding new if does not + * exist. + */ + updateNavigatorSeries: function (addEvents, redraw) { + var navigator = this, + chart = navigator.chart, + baseSeries = navigator.baseSeries, + baseOptions, + mergedNavSeriesOptions, + chartNavigatorSeriesOptions = navigator.navigatorOptions.series, + baseNavigatorOptions, + navSeriesMixin = { + enableMouseTracking: false, + index: null, // #6162 + linkedTo: null, // #6734 + group: 'nav', // for columns + padXAxis: false, + xAxis: 'navigator-x-axis', + yAxis: 'navigator-y-axis', + showInLegend: false, + stacking: false, // #4823 + isInternal: true, + visible: true + }, + // Remove navigator series that are no longer in the baseSeries + 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 (base) { + removeEvent( + base, + 'updatedData', + navigator.updatedDataHandler + ); + delete base.navigatorSeries; + } + // Kill the nav series + navSeries.destroy(); + return false; + } + return true; + } + ); + + // Go through each base series and merge the options to create new + // series + if (baseSeries && baseSeries.length) { + each(baseSeries, function eachBaseSeries(base) { + var linkedNavSeries = base.navigatorSeries, + 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 ' + 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); + + // Update or add the series + if (linkedNavSeries && linkedNavSeries.options) { + linkedNavSeries.update(mergedNavSeriesOptions, redraw); + } else { + 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 + // 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) { + 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 + // 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] + }, + navSeriesMixin, + userSeriesOptions + ); + mergedNavSeriesOptions.data = userSeriesOptions.data; + if (mergedNavSeriesOptions.data) { + navigator.hasNavigatorData = true; + navigatorSeries.push( + chart.initSeries(mergedNavSeriesOptions) + ); + } + }); + } + + if (addEvents) { + this.addBaseSeriesEvents(); + } + }, + + /** + * Add data events. + * For example when main series is updated we need to recalculate extremes + */ + addBaseSeriesEvents: function () { + 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 + // duplicates should be discarded by the browser. + if (baseSeries[0] && baseSeries[0].xAxis) { + addEvent( + baseSeries[0].xAxis, + 'foundExtremes', + this.modifyBaseAxisExtremes + ); + } + + each(baseSeries, function (base) { + // Link base series show/hide to navigator series visibility + addEvent(base, 'show', function () { + if (this.navigatorSeries) { + this.navigatorSeries.setVisible(true, false); + } + }); + addEvent(base, 'hide', function () { + if (this.navigatorSeries) { + this.navigatorSeries.setVisible(false, false); + } + }); + + // 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); + } + } + + // Handle series removal + addEvent(base, 'remove', function () { + if (this.navigatorSeries) { + erase(navigator.series, this.navigatorSeries); + if (defined(this.navigatorSeries.options)) { + this.navigatorSeries.remove(false); + } + delete this.navigatorSeries; + } + }); + }, 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. + */ + modifyNavigatorAxisExtremes: function () { + var xAxis = this.xAxis, + unionExtremes; + + if (xAxis.getExtremes) { + unionExtremes = this.getUnionExtremes(true); + if ( + unionExtremes && + ( + unionExtremes.dataMin !== xAxis.min || + unionExtremes.dataMax !== xAxis.max + ) + ) { + xAxis.min = unionExtremes.dataMin; + xAxis.max = unionExtremes.dataMax; + } + } + }, + + /** + * Hook to modify the base axis extremes with information from the Navigator + */ + modifyBaseAxisExtremes: function () { + var baseXAxis = this, + navigator = baseXAxis.chart.navigator, + baseExtremes = baseXAxis.getExtremes(), + baseMin = baseExtremes.min, + baseMax = baseExtremes.max, + baseDataMin = baseExtremes.dataMin, + baseDataMax = baseExtremes.dataMax, + range = baseMax - baseMin, + stickToMin = navigator.stickToMin, + stickToMax = navigator.stickToMax, + overscroll = pick(baseXAxis.options.overscroll, 0), + 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'; + + if (!unmutable) { + + // 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 (stickToMax) { + 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 + ); + } + } + + // Update the extremes + if (hasSetExtremes && (stickToMin || stickToMax)) { + if (isNumber(newMin)) { + baseXAxis.min = baseXAxis.userMin = newMin; + baseXAxis.max = baseXAxis.userMax = newMax; + } + } + } + + // 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. + */ + 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 = navigator.xAxis.reversed ? + Math.round(navigator.zoomedMin) === 0 : + 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. + 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 + } + }, + + /** + * 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 + var navigator = this.navigator, + xAxis = navigator && ( + navigator.baseSeries && + navigator.baseSeries[0] && + navigator.baseSeries[0].xAxis || + navigator.scrollbar && this.xAxis[0] + ); // #5709 + + if (xAxis) { + navigator.render(xAxis.min, xAxis.max); + } + }); + }, + + /** + * Destroys allocated elements. + */ + destroy: function () { + + // Disconnect events added in addEvents + this.removeEvents(); + + if (this.xAxis) { + erase(this.chart.xAxis, this.xAxis); + erase(this.chart.axes, this.xAxis); + } + if (this.yAxis) { + erase(this.chart.yAxis, this.yAxis); + erase(this.chart.axes, this.yAxis); + } + // Destroy series + each(this.series || [], function (s) { + if (s.destroy) { + s.destroy(); + } + }); + + // Destroy properties + each([ + 'series', 'xAxis', 'yAxis', 'shades', 'outline', 'scrollbarTrack', + 'scrollbarRifles', 'scrollbarGroup', 'scrollbar', 'navigatorGroup', + 'rendered' + ], function (prop) { + if (this[prop] && this[prop].destroy) { + this[prop].destroy(); + } + this[prop] = null; + }, this); + + // Destroy elements in collection + each([this.handles], function (coll) { + destroyObjectProperties(coll); + }, this); + } }; H.Navigator = Navigator; @@ -2035,55 +2035,55 @@ H.Navigator = Navigator; * selector. */ wrap(Axis.prototype, 'zoom', function (proceed, newMin, newMax) { - var chart = this.chart, - chartOptions = chart.options, - zoomType = chartOptions.chart.zoomType, - pinchType = chartOptions.chart.pinchType, - previousZoom, - navigator = chartOptions.navigator, - rangeSelector = chartOptions.rangeSelector, - 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 - if (zoomType === 'x' || pinchType === '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. This - // should apply only if the chart is initialized with a range (#6612), - // otherwise zoom all the way out. - } else if ( - (zoomType === 'xy' || pinchType === 'xy') && - this.options.range - ) { - - previousZoom = this.previousZoom; - if (defined(newMin)) { - this.previousZoom = [this.min, this.max]; - } else if (previousZoom) { - newMin = previousZoom[0]; - newMax = previousZoom[1]; - delete this.previousZoom; - } - } - - } - return ret !== undefined ? ret : proceed.call(this, newMin, newMax); + var chart = this.chart, + chartOptions = chart.options, + zoomType = chartOptions.chart.zoomType, + pinchType = chartOptions.chart.pinchType, + previousZoom, + navigator = chartOptions.navigator, + rangeSelector = chartOptions.rangeSelector, + 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 + if (zoomType === 'x' || pinchType === '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. This + // should apply only if the chart is initialized with a range (#6612), + // otherwise zoom all the way out. + } else if ( + (zoomType === 'xy' || pinchType === 'xy') && + this.options.range + ) { + + previousZoom = this.previousZoom; + if (defined(newMin)) { + this.previousZoom = [this.min, this.max]; + } else if (previousZoom) { + newMin = previousZoom[0]; + newMax = previousZoom[1]; + delete this.previousZoom; + } + } + + } + return ret !== undefined ? ret : proceed.call(this, newMin, newMax); }); // Initialize navigator for stock charts addEvent(Chart, 'beforeRender', function () { - var options = this.options; - if (options.navigator.enabled || options.scrollbar.enabled) { - this.scroller = this.navigator = new Navigator(this); - } + var options = this.options; + if (options.navigator.enabled || options.scrollbar.enabled) { + this.scroller = this.navigator = new Navigator(this); + } }); /** @@ -2094,106 +2094,106 @@ addEvent(Chart, 'beforeRender', function () { */ addEvent(Chart, 'afterSetChartSize', function () { - var legend = this.legend, - navigator = this.navigator, - scrollbarHeight, - legendOptions, - xAxis, - yAxis; - - if (navigator) { - legendOptions = legend && legend.options; - xAxis = navigator.xAxis; - yAxis = navigator.yAxis; - scrollbarHeight = navigator.scrollbarHeight; - - // Compute the top position - if (this.inverted) { - navigator.left = navigator.opposite ? - this.chartWidth - scrollbarHeight - navigator.height : - 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] - - ( - 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) { - xAxis.options.left = yAxis.options.left = navigator.left; - } else { - xAxis.options.top = yAxis.options.top = navigator.top; - } - - xAxis.setAxisSize(); - yAxis.setAxisSize(); - } - } + var legend = this.legend, + navigator = this.navigator, + scrollbarHeight, + legendOptions, + xAxis, + yAxis; + + if (navigator) { + legendOptions = legend && legend.options; + xAxis = navigator.xAxis; + yAxis = navigator.yAxis; + scrollbarHeight = navigator.scrollbarHeight; + + // Compute the top position + if (this.inverted) { + navigator.left = navigator.opposite ? + this.chartWidth - scrollbarHeight - navigator.height : + 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] - + ( + 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) { + xAxis.options.left = yAxis.options.left = navigator.left; + } else { + xAxis.options.top = yAxis.options.top = navigator.top; + } + + xAxis.setAxisSize(); + yAxis.setAxisSize(); + } + } }); // Pick up badly formatted point options to addPoint wrap(Series.prototype, 'addPoint', function ( - proceed, - options, - redraw, - shift, - animation + proceed, + options, + redraw, + shift, + animation ) { - var turboThreshold = this.options.turboThreshold; - if ( - turboThreshold && - this.xData.length > turboThreshold && - isObject(options, true) && - this.chart.navigator - ) { - error(20, true); - } - proceed.call(this, options, redraw, shift, animation); + var turboThreshold = this.options.turboThreshold; + 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 addEvent(Chart, 'afterAddSeries', function () { - if (this.navigator) { - // Recompute which series should be shown in navigator, and add them - this.navigator.setBaseSeries(null, false); - } + if (this.navigator) { + // Recompute which series should be shown in navigator, and add them + this.navigator.setBaseSeries(null, false); + } }); // Handle updating series addEvent(Series, 'afterUpdate', function () { - if (this.chart.navigator && !this.options.isInternal) { - this.chart.navigator.setBaseSeries(null, false); - } + if (this.chart.navigator && !this.options.isInternal) { + this.chart.navigator.setBaseSeries(null, false); + } }); Chart.prototype.callbacks.push(function (chart) { - var extremes, - navigator = chart.navigator; - - // Initiate the navigator - if (navigator && chart.xAxis[0]) { - extremes = chart.xAxis[0].getExtremes(); - navigator.render(extremes.min, extremes.max); - } + var extremes, + navigator = chart.navigator; + + // Initiate the navigator + if (navigator && chart.xAxis[0]) { + extremes = chart.xAxis[0].getExtremes(); + navigator.render(extremes.min, extremes.max); + } }); diff --git a/js/parts/OHLCSeries.js b/js/parts/OHLCSeries.js index 19b63e156bb..70ad19cbc5b 100644 --- a/js/parts/OHLCSeries.js +++ b/js/parts/OHLCSeries.js @@ -8,9 +8,9 @@ import H from './Globals.js'; import './Utilities.js'; import './Point.js'; var each = H.each, - Point = H.Point, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes; + Point = H.Point, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; /** * The ohlc series type. @@ -31,261 +31,261 @@ var each = H.each, */ 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 - * @default 1 - * @product highstock - */ - lineWidth: 1, - - tooltip: { - /*= if (!build.classic) { =*/ - pointFormat: '\u25CF {series.name}
' + - 'Open: {point.open}
' + - 'High: {point.high}
' + - 'Low: {point.low}
' + - 'Close: {point.close}
', - /*= } else { =*/ - - pointFormat: '\u25CF {series.name}
' + - 'Open: {point.open}
' + - 'High: {point.high}
' + - 'Low: {point.low}
' + - 'Close: {point.close}
' - /*= } =*/ - }, - - threshold: null, - /*= if (build.classic) { =*/ - - states: { - - /** - * @extends plotOptions.column.states.hover - * @product highstock - */ - hover: { - - /** - * 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 + /** + * 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 + * @default 1 + * @product highstock + */ + lineWidth: 1, + + tooltip: { + /*= if (!build.classic) { =*/ + pointFormat: '\u25CF {series.name}
' + + 'Open: {point.open}
' + + 'High: {point.high}
' + + 'Low: {point.low}
' + + 'Close: {point.close}
', + /*= } else { =*/ + + pointFormat: '\u25CF {series.name}
' + + 'Open: {point.open}
' + + 'High: {point.high}
' + + 'Low: {point.low}
' + + 'Close: {point.close}
' + /*= } =*/ + }, + + threshold: null, + /*= if (build.classic) { =*/ + + states: { + + /** + * @extends plotOptions.column.states.hover + * @product highstock + */ + hover: { + + /** + * 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 }, /** @lends seriesTypes.ohlc */ { - directTouch: false, - 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', - - /*= if (build.classic) { =*/ - pointAttrToOptions: { - 'stroke': 'color', - 'stroke-width': 'lineWidth' - }, - - /** - * Postprocess mapping between options and SVG attributes - */ - pointAttribs: function (point, state) { - var attribs = seriesTypes.column.prototype.pointAttribs.call( - this, - point, - state - ), - options = this.options; - - delete attribs.fill; - - if ( - !point.options.color && - options.upColor && - point.open < point.close - ) { - attribs.stroke = options.upColor; - } - - return attribs; - }, - /*= } =*/ - - /** - * Translate data points from raw values x and y to plotX and plotY - */ - translate: function () { - var series = this, - yAxis = series.yAxis, - hasModifyValue = !!series.modifyValue, - 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); - } - 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; - }); - }, - - /** - * Draw the data points - */ - drawPoints: function () { - var series = this, - points = series.points, - chart = series.chart; - - - each(points, function (point) { - var plotOpen, - plotClose, - crispCorr, - halfWidth, - path, - graphic = point.graphic, - crispX, - isNew = !graphic; - - if (point.plotY !== undefined) { - - // Create and/or update the graphic - if (!graphic) { - point.graphic = graphic = chart.renderer.path() - .add(series.group); - } - - /*= if (build.classic) { =*/ - graphic.attr( - series.pointAttribs(point, point.selected && 'select') - ); // #3897 - /*= } =*/ - - // crisp vector coordinates - crispCorr = (graphic.strokeWidth() % 2) / 2; - crispX = Math.round(point.plotX) - crispCorr; // #2596 - halfWidth = Math.round(point.shapeArgs.width / 2); - - // the vertical stem - path = [ - 'M', - crispX, Math.round(point.yBottom), - 'L', - crispX, Math.round(point.plotHigh) - ]; - - // open - if (point.open !== null) { - plotOpen = Math.round(point.plotOpen) + crispCorr; - path.push( - 'M', - crispX, - plotOpen, - 'L', - crispX - halfWidth, - plotOpen - ); - } - - // close - if (point.close !== null) { - plotClose = Math.round(point.plotClose) + crispCorr; - path.push( - 'M', - crispX, - plotClose, - 'L', - crispX + halfWidth, - plotClose - ); - } - - graphic[isNew ? 'attr' : 'animate']({ d: path }) - .addClass(point.getClassName(), true); - - } - - - }); - - }, - - animate: null // Disable animation + directTouch: false, + 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', + + /*= if (build.classic) { =*/ + pointAttrToOptions: { + 'stroke': 'color', + 'stroke-width': 'lineWidth' + }, + + /** + * Postprocess mapping between options and SVG attributes + */ + pointAttribs: function (point, state) { + var attribs = seriesTypes.column.prototype.pointAttribs.call( + this, + point, + state + ), + options = this.options; + + delete attribs.fill; + + if ( + !point.options.color && + options.upColor && + point.open < point.close + ) { + attribs.stroke = options.upColor; + } + + return attribs; + }, + /*= } =*/ + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + hasModifyValue = !!series.modifyValue, + 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); + } + 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; + }); + }, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart; + + + each(points, function (point) { + var plotOpen, + plotClose, + crispCorr, + halfWidth, + path, + graphic = point.graphic, + crispX, + isNew = !graphic; + + if (point.plotY !== undefined) { + + // Create and/or update the graphic + if (!graphic) { + point.graphic = graphic = chart.renderer.path() + .add(series.group); + } + + /*= if (build.classic) { =*/ + graphic.attr( + series.pointAttribs(point, point.selected && 'select') + ); // #3897 + /*= } =*/ + + // crisp vector coordinates + crispCorr = (graphic.strokeWidth() % 2) / 2; + crispX = Math.round(point.plotX) - crispCorr; // #2596 + halfWidth = Math.round(point.shapeArgs.width / 2); + + // the vertical stem + path = [ + 'M', + crispX, Math.round(point.yBottom), + 'L', + crispX, Math.round(point.plotHigh) + ]; + + // open + if (point.open !== null) { + plotOpen = Math.round(point.plotOpen) + crispCorr; + path.push( + 'M', + crispX, + plotOpen, + 'L', + crispX - halfWidth, + plotOpen + ); + } + + // close + if (point.close !== null) { + plotClose = Math.round(point.plotClose) + crispCorr; + path.push( + 'M', + crispX, + plotClose, + 'L', + crispX + halfWidth, + plotClose + ); + } + + graphic[isNew ? 'attr' : 'animate']({ d: path }) + .addClass(point.getClassName(), true); + + } + + + }); + + }, + + animate: null // Disable animation }, /** @lends seriesTypes.ohlc.prototype.pointClass.prototype */ { - /** - * 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' - ); - } + /** + * 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' + ); + } }); /** * A `ohlc` series. If the [type](#series.ohlc.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.ohlc * @excluding dataParser,dataURL @@ -296,7 +296,7 @@ seriesType('ohlc', 'column', { /** * 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. @@ -304,7 +304,7 @@ seriesType('ohlc', 'column', { * 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], @@ -312,12 +312,12 @@ seriesType('ohlc', 'column', { * [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, @@ -337,7 +337,7 @@ seriesType('ohlc', 'column', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.arearange.data * @excluding y,marker @@ -347,7 +347,7 @@ seriesType('ohlc', 'column', { /** * The closing value of each data point. - * + * * @type {Number} * @product highstock * @apioption series.ohlc.data.close @@ -355,7 +355,7 @@ seriesType('ohlc', 'column', { /** * The opening value of each data point. - * + * * @type {Number} * @product highstock * @apioption series.ohlc.data.open diff --git a/js/parts/Options.js b/js/parts/Options.js index 76799e02ec6..df0e63ca4b0 100644 --- a/js/parts/Options.js +++ b/js/parts/Options.js @@ -11,9 +11,9 @@ import './Utilities.js'; import './Time.js'; var color = H.color, - isTouchDevice = H.isTouchDevice, - merge = H.merge, - svg = H.svg; + isTouchDevice = H.isTouchDevice, + merge = H.merge, + svg = H.svg; /* **************************************************************************** * Handle the options * @@ -22,2901 +22,2901 @@ var color = H.color, * @optionparent */ H.defaultOptions = { - /*= if (build.classic) { =*/ - - /** - * An array containing the default colors for the chart's series. When - * all colors are used, new colors are pulled from the start again. - * - * Default colors can also be set on a series or series.type basis, - * see [column.colors](#plotOptions.column.colors), - * [pie.colors](#plotOptions.pie.colors). - * - * In styled mode, the colors option doesn't exist. Instead, colors - * are defined in CSS and applied either through series or point class - * names, or through the [chart.colorCount](#chart.colorCount) option. - * - * - * ### Legacy - * - * In Highcharts 3.x, the default colors were: - * - *
colors: ['#2f7ed8', '#0d233a', '#8bbc21', '#910000', '#1aadce',
-	 *     '#492970', '#f28f43', '#77a1e5', '#c42525', '#a6c96a']
- * - * In Highcharts 2.x, the default colors were: - * - *
colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE',
-	 *    '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92']
- * - * @type {Array} - * @sample {highcharts} highcharts/chart/colors/ Assign a global color theme - * @default ["#7cb5ec", "#434348", "#90ed7d", "#f7a35c", "#8085e9", - * "#f15c80", "#e4d354", "#2b908f", "#f45b5b", "#91e8e1"] - */ - colors: '${palette.colors}'.split(' '), - /*= } =*/ - - - /** - * Styled mode only. Configuration object for adding SVG definitions for - * reusable elements. See [gradients, shadows and patterns](http://www. - * highcharts.com/docs/chart-design-and-style/gradients-shadows-and- - * patterns) for more information and code examples. - * - * @type {Object} - * @since 5.0.0 - * @apioption defs - */ - - /** - * @ignore-option - */ - symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], - lang: { - - /** - * The loading text that appears when the chart is set into the loading - * state following a call to `chart.showLoading`. - * - * @type {String} - * @default Loading... - */ - loading: 'Loading...', - - /** - * An array containing the months names. Corresponds to the `%B` format - * in `Highcharts.dateFormat()`. - * - * @type {Array} - * @default [ "January" , "February" , "March" , "April" , "May" , - * "June" , "July" , "August" , "September" , "October" , - * "November" , "December"] - */ - months: [ - 'January', 'February', 'March', 'April', 'May', 'June', 'July', - 'August', 'September', 'October', 'November', 'December' - ], - - /** - * An array containing the months names in abbreviated form. Corresponds - * to the `%b` format in `Highcharts.dateFormat()`. - * - * @type {Array} - * @default [ "Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , - * "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec"] - */ - shortMonths: [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', - 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' - ], - - /** - * An array containing the weekday names. - * - * @type {Array} - * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", - * "Friday", "Saturday"] - */ - weekdays: [ - 'Sunday', 'Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday' - ], - - /** - * Short week days, starting Sunday. If not specified, Highcharts uses - * the first three letters of the `lang.weekdays` option. - * - * @type {Array} - * @sample highcharts/lang/shortweekdays/ - * Finnish two-letter abbreviations - * @since 4.2.4 - * @apioption lang.shortWeekdays - */ - - /** - * What to show in a date field for invalid dates. Defaults to an empty - * string. - * - * @type {String} - * @since 4.1.8 - * @product highcharts highstock - * @apioption lang.invalidDate - */ - - /** - * The default decimal point used in the `Highcharts.numberFormat` - * method unless otherwise specified in the function arguments. - * - * @type {String} - * @default . - * @since 1.2.2 - */ - decimalPoint: '.', - - /** - * [Metric prefixes](http://en.wikipedia.org/wiki/Metric_prefix) used - * to shorten high numbers in axis labels. Replacing any of the positions - * with `null` causes the full number to be written. Setting `numericSymbols` - * to `null` disables shortening altogether. - * - * @type {Array} - * @sample {highcharts} highcharts/lang/numericsymbols/ - * Replacing the symbols with text - * @sample {highstock} highcharts/lang/numericsymbols/ - * Replacing the symbols with text - * @default [ "k" , "M" , "G" , "T" , "P" , "E"] - * @since 2.3.0 - */ - numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], - - /** - * The magnitude of [numericSymbols](#lang.numericSymbol) replacements. - * Use 10000 for Japanese, Korean and various Chinese locales, which - * use symbols for 10^4, 10^8 and 10^12. - * - * @type {Number} - * @sample highcharts/lang/numericsymbolmagnitude/ - * 10000 magnitude for Japanese - * @default 1000 - * @since 5.0.3 - * @apioption lang.numericSymbolMagnitude - */ - - /** - * The text for the label appearing when a chart is zoomed. - * - * @type {String} - * @default Reset zoom - * @since 1.2.4 - */ - resetZoom: 'Reset zoom', - - /** - * The tooltip title for the label appearing when a chart is zoomed. - * - * @type {String} - * @default Reset zoom level 1:1 - * @since 1.2.4 - */ - resetZoomTitle: 'Reset zoom level 1:1', - - /** - * The default thousands separator used in the `Highcharts.numberFormat` - * method unless otherwise specified in the function arguments. Since - * Highcharts 4.1 it defaults to a single space character, which is - * compatible with ISO and works across Anglo-American and continental - * European languages. - * - * The default is a single space. - * - * @type {String} - * @default - * @since 1.2.2 - */ - thousandsSep: ' ' - }, - - /** - * Global options that don't apply to each chart. These options, like - * the `lang` options, must be set using the `Highcharts.setOptions` - * method. - * - *
Highcharts.setOptions({
-	 *     global: {
-	 *         useUTC: false
-	 *     }
-	 * });
- * - */ - - /** - * _Canvg rendering for Android 2.x is removed as of Highcharts 5.0\. - * Use the [libURL](#exporting.libURL) option to configure exporting._ - * - * The URL to the additional file to lazy load for Android 2.x devices. - * These devices don't support SVG, so we download a helper file that - * contains [canvg](http://code.google.com/p/canvg/), its dependency - * rbcolor, and our own CanVG Renderer class. To avoid hotlinking to - * our site, you can install canvas-tools.js on your own server and - * change this option accordingly. - * - * @type {String} - * @deprecated - * @default http://code.highcharts.com/{version}/modules/canvas-tools.js - * @product highcharts highmaps - * @apioption global.canvasToolsURL - */ - - /** - * This option is deprecated since v6.0.5. Instead, use - * [time.useUTC](#time.useUTC) that supports individual time settings - * per chart. - * - * @deprecated - * @apioption global.useUTC - */ - - /** - * This option is deprecated since v6.0.5. Instead, use - * [time.Date](#time.Date) that supports individual time settings - * per chart. - * - * @deprecated - * @product highcharts highstock - * @apioption global.Date - */ - - /** - * This option is deprecated since v6.0.5. Instead, use - * [time.getTimezoneOffset](#time.getTimezoneOffset) that supports - * individual time settings per chart. - * - * @deprecated - * @product highcharts highstock - * @apioption global.getTimezoneOffset - */ - - /** - * This option is deprecated since v6.0.5. Instead, use - * [time.timezone](#time.timezone) that supports individual time - * settings per chart. - * - * @deprecated - * @product highcharts highstock - * @apioption global.timezone - */ - - /** - * This option is deprecated since v6.0.5. Instead, use - * [time.timezoneOffset](#time.timezoneOffset) that supports individual - * time settings per chart. - * - * @deprecated - * @product highcharts highstock - * @apioption global.timezoneOffset - */ - global: {}, - - - time: H.Time.prototype.defaultOptions, - chart: { - - /** - * When using multiple axis, the ticks of two or more opposite axes - * will automatically be aligned by adding ticks to the axis or axes - * with the least ticks, as if `tickAmount` were specified. - * - * This can be prevented by setting `alignTicks` to false. If the grid - * lines look messy, it's a good idea to hide them for the secondary - * axis by setting `gridLineWidth` to 0. - * - * If `startOnTick` or `endOnTick` in an Axis options are set to false, - * then the `alignTicks ` will be disabled for the Axis. - * - * Disabled for logarithmic axes. - * - * @type {Boolean} - * @sample {highcharts} highcharts/chart/alignticks-true/ - * True by default - * @sample {highcharts} highcharts/chart/alignticks-false/ - * False - * @sample {highstock} stock/chart/alignticks-true/ - * True by default - * @sample {highstock} stock/chart/alignticks-false/ - * False - * @default true - * @product highcharts highstock - * @apioption chart.alignTicks - */ - - - /** - * Set the overall animation for all chart updating. Animation can be - * disabled throughout the chart by setting it to false here. It can - * be overridden for each individual API method as a function parameter. - * The only animation not affected by this option is the initial series - * animation, see [plotOptions.series.animation]( - * #plotOptions.series.animation). - * - * The animation can either be set as a boolean or a configuration - * object. If `true`, it will use the 'swing' jQuery easing and a - * duration of 500 ms. If used as a configuration object, the following - * properties are supported: - * - *
- * - *
duration
- * - *
The duration of the animation in milliseconds.
- * - *
easing
- * - *
A string reference to an easing function set on the `Math` object. - * See [the easing demo](http://jsfiddle.net/gh/get/library/pure/ - * highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ - * series-animation-easing/).
- * - *
- * - * @type {Boolean|Object} - * @sample {highcharts} highcharts/chart/animation-none/ - * Updating with no animation - * @sample {highcharts} highcharts/chart/animation-duration/ - * With a longer duration - * @sample {highcharts} highcharts/chart/animation-easing/ - * With a jQuery UI easing - * @sample {highmaps} maps/chart/animation-none/ - * Updating with no animation - * @sample {highmaps} maps/chart/animation-duration/ - * With a longer duration - * @default true - * @apioption chart.animation - */ - - /** - * A CSS class name to apply to the charts container `div`, allowing - * unique CSS styling for each chart. - * - * @type {String} - * @apioption chart.className - */ - - /** - * Event listeners for the chart. - * - * @apioption chart.events - */ - - /** - * Fires when a series is added to the chart after load time, using - * the `addSeries` method. One parameter, `event`, is passed to the - * function, containing common event information. - * Through `event.options` you can access the series options that was - * passed to the `addSeries` method. Returning false prevents the series - * from being added. - * - * @type {Function} - * @context Chart - * @sample {highcharts} highcharts/chart/events-addseries/ Alert on add series - * @sample {highstock} stock/chart/events-addseries/ Alert on add series - * @since 1.2.0 - * @apioption chart.events.addSeries - */ - - /** - * Fires when clicking on the plot background. One parameter, `event`, - * is passed to the function, containing common event information. - * - * Information on the clicked spot can be found through `event.xAxis` - * and `event.yAxis`, which are arrays containing the axes of each dimension - * and each axis' value at the clicked spot. The primary axes are - * `event.xAxis[0]` and `event.yAxis[0]`. Remember the unit of a - * datetime axis is milliseconds since 1970-01-01 00:00:00. - * - *
click: function(e) {
-		 *     console.log(
-		 *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', e.xAxis[0].value),
-		 *         e.yAxis[0].value
-		 *     )
-		 * }
- * - * @type {Function} - * @context Chart - * @sample {highcharts} highcharts/chart/events-click/ - * Alert coordinates on click - * @sample {highcharts} highcharts/chart/events-container/ - * Alternatively, attach event to container - * @sample {highstock} stock/chart/events-click/ - * Alert coordinates on click - * @sample {highstock} highcharts/chart/events-container/ - * Alternatively, attach event to container - * @sample {highmaps} maps/chart/events-click/ - * Record coordinates on click - * @sample {highmaps} highcharts/chart/events-container/ - * Alternatively, attach event to container - * @since 1.2.0 - * @apioption chart.events.click - */ - - - /** - * Fires when the chart is finished loading. Since v4.2.2, it also waits - * for images to be loaded, for example from point markers. One parameter, - * `event`, is passed to the function, containing common event information. - * - * There is also a second parameter to the chart constructor where a - * callback function can be passed to be executed on chart.load. - * - * @type {Function} - * @context Chart - * @sample {highcharts} highcharts/chart/events-load/ - * Alert on chart load - * @sample {highstock} stock/chart/events-load/ - * Alert on chart load - * @sample {highmaps} maps/chart/events-load/ - * Add series on chart load - * @apioption chart.events.load - */ - - /** - * Fires when the chart is redrawn, either after a call to chart.redraw() - * or after an axis, series or point is modified with the `redraw` option - * set to true. One parameter, `event`, is passed to the function, containing common event information. - * - * @type {Function} - * @context Chart - * @sample {highcharts} highcharts/chart/events-redraw/ - * Alert on chart redraw - * @sample {highstock} stock/chart/events-redraw/ - * Alert on chart redraw when adding a series or moving the - * zoomed range - * @sample {highmaps} maps/chart/events-redraw/ - * Set subtitle on chart redraw - * @since 1.2.0 - * @apioption chart.events.redraw - */ - - /** - * Fires after initial load of the chart (directly after the `load` - * event), and after each redraw (directly after the `redraw` event). - * - * @type {Function} - * @context Chart - * @since 5.0.7 - * @apioption chart.events.render - */ - - /** - * Fires when an area of the chart has been selected. Selection is enabled - * by setting the chart's zoomType. One parameter, `event`, is passed - * to the function, containing common event information. The default action for the selection event is to - * zoom the chart to the selected area. It can be prevented by calling - * `event.preventDefault()`. - * - * Information on the selected area can be found through `event.xAxis` - * and `event.yAxis`, which are arrays containing the axes of each dimension - * and each axis' min and max values. The primary axes are `event.xAxis[0]` - * and `event.yAxis[0]`. Remember the unit of a datetime axis is milliseconds - * since 1970-01-01 00:00:00. - * - *
selection: function(event) {
-		 *     // log the min and max of the primary, datetime x-axis
-		 *     console.log(
-		 *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', event.xAxis[0].min),
-		 *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', event.xAxis[0].max)
-		 *     );
-		 *     // log the min and max of the y axis
-		 *     console.log(event.yAxis[0].min, event.yAxis[0].max);
-		 * }
- * - * @type {Function} - * @sample {highcharts} highcharts/chart/events-selection/ - * Report on selection and reset - * @sample {highcharts} highcharts/chart/events-selection-points/ - * Select a range of points through a drag selection - * @sample {highstock} stock/chart/events-selection/ - * Report on selection and reset - * @sample {highstock} highcharts/chart/events-selection-points/ - * Select a range of points through a drag selection (Highcharts) - * @apioption chart.events.selection - */ - - /** - * The margin between the outer edge of the chart and the plot area. - * The numbers in the array designate top, right, bottom and left - * respectively. Use the options `marginTop`, `marginRight`, - * `marginBottom` and `marginLeft` for shorthand setting of one option. - * - * By default there is no margin. The actual space is dynamically calculated - * from the offset of axis labels, axis title, title, subtitle and legend - * in addition to the `spacingTop`, `spacingRight`, `spacingBottom` - * and `spacingLeft` options. - * - * @type {Array} - * @sample {highcharts} highcharts/chart/margins-zero/ - * Zero margins - * @sample {highstock} stock/chart/margin-zero/ - * Zero margins - * - * @defaults {all} null - * @apioption chart.margin - */ - - /** - * The margin between the bottom outer edge of the chart and the plot - * area. Use this to set a fixed pixel value for the margin as opposed - * to the default dynamic margin. See also `spacingBottom`. - * - * @type {Number} - * @sample {highcharts} highcharts/chart/marginbottom/ - * 100px bottom margin - * @sample {highstock} stock/chart/marginbottom/ - * 100px bottom margin - * @sample {highmaps} maps/chart/margin/ - * 100px margins - * @since 2.0 - * @apioption chart.marginBottom - */ - - /** - * The margin between the left outer edge of the chart and the plot - * area. Use this to set a fixed pixel value for the margin as opposed - * to the default dynamic margin. See also `spacingLeft`. - * - * @type {Number} - * @sample {highcharts} highcharts/chart/marginleft/ - * 150px left margin - * @sample {highstock} stock/chart/marginleft/ - * 150px left margin - * @sample {highmaps} maps/chart/margin/ - * 100px margins - * @default null - * @since 2.0 - * @apioption chart.marginLeft - */ - - /** - * The margin between the right outer edge of the chart and the plot - * area. Use this to set a fixed pixel value for the margin as opposed - * to the default dynamic margin. See also `spacingRight`. - * - * @type {Number} - * @sample {highcharts} highcharts/chart/marginright/ - * 100px right margin - * @sample {highstock} stock/chart/marginright/ - * 100px right margin - * @sample {highmaps} maps/chart/margin/ - * 100px margins - * @default null - * @since 2.0 - * @apioption chart.marginRight - */ - - /** - * The margin between the top outer edge of the chart and the plot area. - * Use this to set a fixed pixel value for the margin as opposed to - * the default dynamic margin. See also `spacingTop`. - * - * @type {Number} - * @sample {highcharts} highcharts/chart/margintop/ 100px top margin - * @sample {highstock} stock/chart/margintop/ - * 100px top margin - * @sample {highmaps} maps/chart/margin/ - * 100px margins - * @default null - * @since 2.0 - * @apioption chart.marginTop - */ - - /** - * Allows setting a key to switch between zooming and panning. Can be - * one of `alt`, `ctrl`, `meta` (the command key on Mac and Windows - * key on Windows) or `shift`. The keys are mapped directly to the key - * properties of the click event argument (`event.altKey`, `event.ctrlKey`, - * `event.metaKey` and `event.shiftKey`). - * - * @validvalue [null, "alt", "ctrl", "meta", "shift"] - * @type {String} - * @since 4.0.3 - * @product highcharts - * @apioption chart.panKey - */ - - /** - * Allow panning in a chart. Best used with [panKey](#chart.panKey) - * to combine zooming and panning. - * - * On touch devices, when the [tooltip.followTouchMove](#tooltip.followTouchMove) - * option is `true` (default), panning requires two fingers. To allow - * panning with one finger, set `followTouchMove` to `false`. - * - * @type {Boolean} - * @sample {highcharts} highcharts/chart/pankey/ Zooming and panning - * @default {highcharts} false - * @default {highstock} true - * @since 4.0.3 - * @product highcharts highstock - * @apioption chart.panning - */ - - - /** - * Equivalent to [zoomType](#chart.zoomType), but for multitouch gestures - * only. By default, the `pinchType` is the same as the `zoomType` setting. - * However, pinching can be enabled separately in some cases, for example - * in stock charts where a mouse drag pans the chart, while pinching - * is enabled. When [tooltip.followTouchMove](#tooltip.followTouchMove) - * is true, pinchType only applies to two-finger touches. - * - * @validvalue ["x", "y", "xy"] - * @type {String} - * @default {highcharts} null - * @default {highstock} x - * @since 3.0 - * @product highcharts highstock - * @apioption chart.pinchType - */ - - /** - * The corner radius of the outer chart border. - * - * @type {Number} - * @sample {highcharts} highcharts/chart/borderradius/ 20px radius - * @sample {highstock} stock/chart/border/ 10px radius - * @sample {highmaps} maps/chart/border/ Border options - * @default 0 - */ - borderRadius: 0, - /*= if (!build.classic) { =*/ - - /** - * In styled mode, this sets how many colors the class names - * should rotate between. With ten colors, series (or points) are - * given class names like `highcharts-color-0`, `highcharts-color-0` - * [...] `highcharts-color-9`. The equivalent in non-styled mode - * is to set colors using the [colors](#colors) setting. - * - * @type {Number} - * @default 10 - * @since 5.0.0 - */ - colorCount: 10, - /*= } =*/ - - /** - * Alias of `type`. - * - * @validvalue ["line", "spline", "column", "area", "areaspline", "pie"] - * @type {String} - * @deprecated - * @sample {highcharts} highcharts/chart/defaultseriestype/ Bar - * @default line - * @product highcharts - */ - defaultSeriesType: 'line', - - /** - * If true, the axes will scale to the remaining visible series once - * one series is hidden. If false, hiding and showing a series will - * not affect the axes or the other series. For stacks, once one series - * within the stack is hidden, the rest of the stack will close in - * around it even if the axis is not affected. - * - * @type {Boolean} - * @sample {highcharts} highcharts/chart/ignorehiddenseries-true/ - * True by default - * @sample {highcharts} highcharts/chart/ignorehiddenseries-false/ - * False - * @sample {highcharts} highcharts/chart/ignorehiddenseries-true-stacked/ - * True with stack - * @sample {highstock} stock/chart/ignorehiddenseries-true/ - * True by default - * @sample {highstock} stock/chart/ignorehiddenseries-false/ - * False - * @default true - * @since 1.2.0 - * @product highcharts highstock - */ - ignoreHiddenSeries: true, - - - /** - * Whether to invert the axes so that the x axis is vertical and y axis - * is horizontal. When `true`, the x axis is [reversed](#xAxis.reversed) - * by default. - * - * @productdesc {highcharts} - * If a bar series is present in the chart, it will be inverted - * automatically. Inverting the chart doesn't have an effect if there - * are no cartesian series in the chart, or if the chart is - * [polar](#chart.polar). - * - * @type {Boolean} - * @sample {highcharts} highcharts/chart/inverted/ - * Inverted line - * @sample {highstock} stock/navigator/inverted/ - * Inverted stock chart - * @default false - * @product highcharts highstock - * @apioption chart.inverted - */ - - /** - * The distance between the outer edge of the chart and the content, - * like title or legend, or axis title and labels if present. The - * numbers in the array designate top, right, bottom and left respectively. - * Use the options spacingTop, spacingRight, spacingBottom and spacingLeft - * options for shorthand setting of one option. - * - * @type {Array} - * @see [chart.margin](#chart.margin) - * @default [10, 10, 15, 10] - * @since 3.0.6 - */ - spacing: [10, 10, 15, 10], - - /** - * The button that appears after a selection zoom, allowing the user - * to reset zoom. - * - */ - resetZoomButton: { - - /** - * What frame the button should be placed related to. Can be either - * `plot` or `chart` - * - * @validvalue ["plot", "chart"] - * @type {String} - * @sample {highcharts} highcharts/chart/resetzoombutton-relativeto/ - * Relative to the chart - * @sample {highstock} highcharts/chart/resetzoombutton-relativeto/ - * Relative to the chart - * @default plot - * @since 2.2 - * @apioption chart.resetZoomButton.relativeTo - */ - - /** - * A collection of attributes for the button. The object takes SVG - * attributes like `fill`, `stroke`, `stroke-width` or `r`, the border - * radius. The theme also supports `style`, a collection of CSS properties - * for the text. Equivalent attributes for the hover state are given - * in `theme.states.hover`. - * - * @type {Object} - * @sample {highcharts} highcharts/chart/resetzoombutton-theme/ - * Theming the button - * @sample {highstock} highcharts/chart/resetzoombutton-theme/ - * Theming the button - * @since 2.2 - */ - theme: { - - /** - * The Z index for the reset zoom button. The default value - * places it below the tooltip that has Z index 7. - */ - zIndex: 6 - }, - - /** - * The position of the button. - * - * @type {Object} - * @sample {highcharts} highcharts/chart/resetzoombutton-position/ - * Above the plot area - * @sample {highstock} highcharts/chart/resetzoombutton-position/ - * Above the plot area - * @sample {highmaps} highcharts/chart/resetzoombutton-position/ - * Above the plot area - * @since 2.2 - */ - position: { - - /** - * The horizontal alignment of the button. - * - * @type {String} - */ - align: 'right', - - /** - * The horizontal offset of the button. - * - * @type {Number} - */ - x: -10, - - /** - * The vertical alignment of the button. - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @default top - * @apioption chart.resetZoomButton.position.verticalAlign - */ - - /** - * The vertical offset of the button. - * - * @type {Number} - */ - y: 10 - } - }, - - /** - * The pixel width of the plot area border. - * - * @type {Number} - * @sample {highcharts} highcharts/chart/plotborderwidth/ 1px border - * @sample {highstock} stock/chart/plotborder/ - * 2px border - * @sample {highmaps} maps/chart/plotborder/ - * Plot border options - * @default 0 - * @apioption chart.plotBorderWidth - */ - - /** - * Whether to apply a drop shadow to the plot area. Requires that - * plotBackgroundColor be set. The shadow can be an object configuration - * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. - * - * @type {Boolean|Object} - * @sample {highcharts} highcharts/chart/plotshadow/ Plot shadow - * @sample {highstock} stock/chart/plotshadow/ - * Plot shadow - * @sample {highmaps} maps/chart/plotborder/ - * Plot border options - * @default false - * @apioption chart.plotShadow - */ - - /** - * When true, cartesian charts like line, spline, area and column are - * transformed into the polar coordinate system. Requires - * `highcharts-more.js`. - * - * @type {Boolean} - * @default false - * @since 2.3.0 - * @product highcharts - * @apioption chart.polar - */ - - /** - * Whether to reflow the chart to fit the width of the container div - * on resizing the window. - * - * @type {Boolean} - * @sample {highcharts} highcharts/chart/reflow-true/ True by default - * @sample {highcharts} highcharts/chart/reflow-false/ False - * @sample {highstock} stock/chart/reflow-true/ - * True by default - * @sample {highstock} stock/chart/reflow-false/ - * False - * @sample {highmaps} maps/chart/reflow-true/ - * True by default - * @sample {highmaps} maps/chart/reflow-false/ - * False - * @default true - * @since 2.1 - * @apioption chart.reflow - */ - - /** - * The HTML element where the chart will be rendered. If it is a string, - * the element by that id is used. The HTML element can also be passed - * by direct reference, or as the first argument of the chart constructor, - * in which case the option is not needed. - * - * @type {String|Object} - * @sample {highcharts} highcharts/chart/reflow-true/ - * String - * @sample {highcharts} highcharts/chart/renderto-object/ - * Object reference - * @sample {highcharts} highcharts/chart/renderto-jquery/ - * Object reference through jQuery - * @sample {highstock} stock/chart/renderto-string/ - * String - * @sample {highstock} stock/chart/renderto-object/ - * Object reference - * @sample {highstock} stock/chart/renderto-jquery/ - * Object reference through jQuery - * @apioption chart.renderTo - */ - - /** - * The background color of the marker square when selecting (zooming - * in on) an area of the chart. - * - * @type {Color} - * @see In styled mode, the selection marker fill is set with the - * `.highcharts-selection-marker` class. - * @default rgba(51,92,173,0.25) - * @since 2.1.7 - * @apioption chart.selectionMarkerFill - */ - - /** - * Whether to apply a drop shadow to the outer chart area. Requires - * that backgroundColor be set. The shadow can be an object configuration - * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. - * - * @type {Boolean|Object} - * @sample {highcharts} highcharts/chart/shadow/ Shadow - * @sample {highstock} stock/chart/shadow/ - * Shadow - * @sample {highmaps} maps/chart/border/ - * Chart border and shadow - * @default false - * @apioption chart.shadow - */ - - /** - * Whether to show the axes initially. This only applies to empty charts - * where series are added dynamically, as axes are automatically added - * to cartesian series. - * - * @type {Boolean} - * @sample {highcharts} highcharts/chart/showaxes-false/ False by default - * @sample {highcharts} highcharts/chart/showaxes-true/ True - * @since 1.2.5 - * @product highcharts - * @apioption chart.showAxes - */ - - /** - * The space between the bottom edge of the chart and the content (plot - * area, axis title and labels, title, subtitle or legend in top position). - * - * @type {Number} - * @sample {highcharts} highcharts/chart/spacingbottom/ - * Spacing bottom set to 100 - * @sample {highstock} stock/chart/spacingbottom/ - * Spacing bottom set to 100 - * @sample {highmaps} maps/chart/spacing/ - * Spacing 100 all around - * @default 15 - * @since 2.1 - * @apioption chart.spacingBottom - */ - - /** - * The space between the left edge of the chart and the content (plot - * area, axis title and labels, title, subtitle or legend in top position). - * - * @type {Number} - * @sample {highcharts} highcharts/chart/spacingleft/ - * Spacing left set to 100 - * @sample {highstock} stock/chart/spacingleft/ - * Spacing left set to 100 - * @sample {highmaps} maps/chart/spacing/ - * Spacing 100 all around - * @default 10 - * @since 2.1 - * @apioption chart.spacingLeft - */ - - /** - * The space between the right edge of the chart and the content (plot - * area, axis title and labels, title, subtitle or legend in top - * position). - * - * @type {Number} - * @sample {highcharts} highcharts/chart/spacingright-100/ - * Spacing set to 100 - * @sample {highcharts} highcharts/chart/spacingright-legend/ - * Legend in right position with default spacing - * @sample {highstock} stock/chart/spacingright/ - * Spacing set to 100 - * @sample {highmaps} maps/chart/spacing/ - * Spacing 100 all around - * @default 10 - * @since 2.1 - * @apioption chart.spacingRight - */ - - /** - * The space between the top edge of the chart and the content (plot - * area, axis title and labels, title, subtitle or legend in top - * position). - * - * @type {Number} - * @sample {highcharts} highcharts/chart/spacingtop-100/ - * A top spacing of 100 - * @sample {highcharts} highcharts/chart/spacingtop-10/ - * Floating chart title makes the plot area align to the default - * spacingTop of 10. - * @sample {highstock} stock/chart/spacingtop/ - * A top spacing of 100 - * @sample {highmaps} maps/chart/spacing/ - * Spacing 100 all around - * @default 10 - * @since 2.1 - * @apioption chart.spacingTop - */ - - /** - * Additional CSS styles to apply inline to the container `div`. Note - * that since the default font styles are applied in the renderer, it - * is ignorant of the individual chart options and must be set globally. - * - * @type {CSSObject} - * @see In styled mode, general chart styles can be set with the `.highcharts-root` class. - * @sample {highcharts} highcharts/chart/style-serif-font/ - * Using a serif type font - * @sample {highcharts} highcharts/css/em/ - * Styled mode with relative font sizes - * @sample {highstock} stock/chart/style/ - * Using a serif type font - * @sample {highmaps} maps/chart/style-serif-font/ - * Using a serif type font - * @default {"fontFamily":"\"Lucida Grande\", \"Lucida Sans Unicode\", Verdana, Arial, Helvetica, sans-serif","fontSize":"12px"} - * @apioption chart.style - */ - - /** - * The default series type for the chart. Can be any of the chart types - * listed under [plotOptions](#plotOptions). - * - * @validvalue ["line", "spline", "column", "bar", "area", "areaspline", "pie", "arearange", "areasplinerange", "boxplot", "bubble", "columnrange", "errorbar", "funnel", "gauge", "heatmap", "polygon", "pyramid", "scatter", "solidgauge", "treemap", "waterfall"] - * @type {String} - * @sample {highcharts} highcharts/chart/type-bar/ Bar - * @sample {highstock} stock/chart/type/ - * Areaspline - * @sample {highmaps} maps/chart/type-mapline/ - * Mapline - * @default {highcharts} line - * @default {highstock} line - * @default {highmaps} map - * @since 2.1.0 - * @apioption chart.type - */ - - /** - * Decides in what dimensions the user can zoom by dragging the mouse. - * Can be one of `x`, `y` or `xy`. - * - * @validvalue [null, "x", "y", "xy"] - * @type {String} - * @see [panKey](#chart.panKey) - * @sample {highcharts} highcharts/chart/zoomtype-none/ None by default - * @sample {highcharts} highcharts/chart/zoomtype-x/ X - * @sample {highcharts} highcharts/chart/zoomtype-y/ Y - * @sample {highcharts} highcharts/chart/zoomtype-xy/ Xy - * @sample {highstock} stock/demo/basic-line/ None by default - * @sample {highstock} stock/chart/zoomtype-x/ X - * @sample {highstock} stock/chart/zoomtype-y/ Y - * @sample {highstock} stock/chart/zoomtype-xy/ Xy - * @product highcharts highstock - * @apioption chart.zoomType - */ - - /** - * An explicit width for the chart. By default (when `null`) the width - * is calculated from the offset width of the containing element. - * - * @type {Number} - * @sample {highcharts} highcharts/chart/width/ 800px wide - * @sample {highstock} stock/chart/width/ 800px wide - * @sample {highmaps} maps/chart/size/ Chart with explicit size - * @default null - */ - width: null, - - /** - * An explicit height for the chart. If a _number_, the height is - * given in pixels. If given a _percentage string_ (for example `'56%'`), - * the height is given as the percentage of the actual chart width. - * This allows for preserving the aspect ratio across responsive - * sizes. - * - * By default (when `null`) the height is calculated from the offset - * height of the containing element, or 400 pixels if the containing - * element's height is 0. - * - * @type {Number|String} - * @sample {highcharts} highcharts/chart/height/ - * 500px height - * @sample {highstock} stock/chart/height/ - * 300px height - * @sample {highmaps} maps/chart/size/ - * Chart with explicit size - * @sample highcharts/chart/height-percent/ - * Highcharts with percentage height - * @default null - */ - height: null, - - /*= if (build.classic) { =*/ - - /** - * The color of the outer chart border. - * - * @type {Color} - * @see In styled mode, the stroke is set with the `.highcharts-background` - * class. - * @sample {highcharts} highcharts/chart/bordercolor/ Brown border - * @sample {highstock} stock/chart/border/ Brown border - * @sample {highmaps} maps/chart/border/ Border options - * @default #335cad - */ - borderColor: '${palette.highlightColor80}', - - /** - * The pixel width of the outer chart border. - * - * @type {Number} - * @see In styled mode, the stroke is set with the `.highcharts-background` - * class. - * @sample {highcharts} highcharts/chart/borderwidth/ 5px border - * @sample {highstock} stock/chart/border/ - * 2px border - * @sample {highmaps} maps/chart/border/ - * Border options - * @default 0 - * @apioption chart.borderWidth - */ - - /** - * The background color or gradient for the outer chart area. - * - * @type {Color} - * @see In styled mode, the background is set with the `.highcharts-background` class. - * @sample {highcharts} highcharts/chart/backgroundcolor-color/ Color - * @sample {highcharts} highcharts/chart/backgroundcolor-gradient/ Gradient - * @sample {highstock} stock/chart/backgroundcolor-color/ - * Color - * @sample {highstock} stock/chart/backgroundcolor-gradient/ - * Gradient - * @sample {highmaps} maps/chart/backgroundcolor-color/ - * Color - * @sample {highmaps} maps/chart/backgroundcolor-gradient/ - * Gradient - * @default #FFFFFF - */ - backgroundColor: '${palette.backgroundColor}', - - /** - * The background color or gradient for the plot area. - * - * @type {Color} - * @see In styled mode, the plot background is set with the `.highcharts-plot-background` class. - * @sample {highcharts} highcharts/chart/plotbackgroundcolor-color/ - * Color - * @sample {highcharts} highcharts/chart/plotbackgroundcolor-gradient/ - * Gradient - * @sample {highstock} stock/chart/plotbackgroundcolor-color/ - * Color - * @sample {highstock} stock/chart/plotbackgroundcolor-gradient/ - * Gradient - * @sample {highmaps} maps/chart/plotbackgroundcolor-color/ - * Color - * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ - * Gradient - * @default null - * @apioption chart.plotBackgroundColor - */ - - - /** - * The URL for an image to use as the plot background. To set an image - * as the background for the entire chart, set a CSS background image - * to the container element. Note that for the image to be applied to - * exported charts, its URL needs to be accessible by the export server. - * - * @type {String} - * @see In styled mode, a plot background image can be set with the - * `.highcharts-plot-background` class and a [custom pattern](http://www. - * highcharts.com/docs/chart-design-and-style/gradients-shadows-and- - * patterns). - * @sample {highcharts} highcharts/chart/plotbackgroundimage/ Skies - * @sample {highstock} stock/chart/plotbackgroundimage/ Skies - * @default null - * @apioption chart.plotBackgroundImage - */ - - /** - * The color of the inner chart or plot area border. - * - * @type {Color} - * @see In styled mode, a plot border stroke can be set with the - * `.highcharts-plot-border` class. - * @sample {highcharts} highcharts/chart/plotbordercolor/ Blue border - * @sample {highstock} stock/chart/plotborder/ Blue border - * @sample {highmaps} maps/chart/plotborder/ Plot border options - * @default #cccccc - */ - plotBorderColor: '${palette.neutralColor20}' - /*= } =*/ - - }, - - /** - * The chart's main title. - * - * @sample {highmaps} maps/title/title/ Title options demonstrated - */ - title: { - - /** - * When the title is floating, the plot area will not move to make space - * for it. - * - * @type {Boolean} - * @sample {highcharts} highcharts/chart/zoomtype-none/ False by default - * @sample {highcharts} highcharts/title/floating/ - * True - title on top of the plot area - * @sample {highstock} stock/chart/title-floating/ - * True - title on top of the plot area - * @default false - * @since 2.1 - * @apioption title.floating - */ - - /** - * CSS styles for the title. Use this for font styling, but use `align`, - * `x` and `y` for text alignment. - * - * In styled mode, the title style is given in the `.highcharts-title` class. - * - * @type {CSSObject} - * @sample {highcharts} highcharts/title/style/ Custom color and weight - * @sample {highstock} stock/chart/title-style/ Custom color and weight - * @sample highcharts/css/titles/ Styled mode - * @default {highcharts|highmaps} { "color": "#333333", "fontSize": "18px" } - * @default {highstock} { "color": "#333333", "fontSize": "16px" } - * @apioption title.style - */ - - /** - * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- - * and-string-formatting#html) to render the text. - * - * @type {Boolean} - * @default false - * @apioption title.useHTML - */ - - /** - * The vertical alignment of the title. Can be one of `"top"`, `"middle"` - * and `"bottom"`. When a value is given, the title behaves as if - * [floating](#title.floating) were `true`. - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @sample {highcharts} highcharts/title/verticalalign/ - * Chart title in bottom right corner - * @sample {highstock} stock/chart/title-verticalalign/ - * Chart title in bottom right corner - * @since 2.1 - * @apioption title.verticalAlign - */ - - /** - * The x position of the title relative to the alignment within chart. - * spacingLeft and chart.spacingRight. - * - * @type {Number} - * @sample {highcharts} highcharts/title/align/ - * Aligned to the plot area (x = 70px = margin left - spacing left) - * @sample {highstock} stock/chart/title-align/ - * Aligned to the plot area (x = 50px = margin left - spacing left) - * @default 0 - * @since 2.0 - * @apioption title.x - */ - - /** - * The y position of the title relative to the alignment within [chart. - * spacingTop](#chart.spacingTop) and [chart.spacingBottom](#chart.spacingBottom). - * By default it depends on the font size. - * - * @type {Number} - * @sample {highcharts} highcharts/title/y/ - * Title inside the plot area - * @sample {highstock} stock/chart/title-verticalalign/ - * Chart title in bottom right corner - * @since 2.0 - * @apioption title.y - */ - - /** - * The title of the chart. To disable the title, set the `text` to - * `null`. - * - * @type {String} - * @sample {highcharts} highcharts/title/text/ Custom title - * @sample {highstock} stock/chart/title-text/ Custom title - * @default {highcharts|highmaps} Chart title - * @default {highstock} null - */ - text: 'Chart title', - - /** - * The horizontal alignment of the title. Can be one of "left", "center" - * and "right". - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @sample {highcharts} highcharts/title/align/ Aligned to the plot area (x = 70px = margin left - spacing left) - * @sample {highstock} stock/chart/title-align/ Aligned to the plot area (x = 50px = margin left - spacing left) - * @default center - * @since 2.0 - */ - align: 'center', - - /** - * The margin between the title and the plot area, or if a subtitle - * is present, the margin between the subtitle and the plot area. - * - * @type {Number} - * @sample {highcharts} highcharts/title/margin-50/ A chart title margin of 50 - * @sample {highcharts} highcharts/title/margin-subtitle/ The same margin applied with a subtitle - * @sample {highstock} stock/chart/title-margin/ A chart title margin of 50 - * @default 15 - * @since 2.1 - */ - margin: 15, - - /** - * Adjustment made to the title width, normally to reserve space for - * the exporting burger menu. - * - * @type {Number} - * @sample {highcharts} highcharts/title/widthadjust/ Wider menu, greater padding - * @sample {highstock} highcharts/title/widthadjust/ Wider menu, greater padding - * @sample {highmaps} highcharts/title/widthadjust/ Wider menu, greater padding - * @default -44 - * @since 4.2.5 - */ - widthAdjust: -44 - - }, - - /** - * The chart's subtitle. This can be used both to display a subtitle below - * the main title, and to display random text anywhere in the chart. The - * subtitle can be updated after chart initialization through the - * `Chart.setTitle` method. - * - * @sample {highmaps} maps/title/subtitle/ Subtitle options demonstrated - */ - subtitle: { - - /** - * When the subtitle is floating, the plot area will not move to make - * space for it. - * - * @type {Boolean} - * @sample {highcharts} highcharts/subtitle/floating/ - * Floating title and subtitle - * @sample {highstock} stock/chart/subtitle-footnote - * Footnote floating at bottom right of plot area - * @default false - * @since 2.1 - * @apioption subtitle.floating - */ - - /** - * CSS styles for the title. - * - * In styled mode, the subtitle style is given in the `.highcharts-subtitle` class. - * - * @type {CSSObject} - * @sample {highcharts} highcharts/subtitle/style/ - * Custom color and weight - * @sample {highcharts} highcharts/css/titles/ - * Styled mode - * @sample {highstock} stock/chart/subtitle-style - * Custom color and weight - * @sample {highstock} highcharts/css/titles/ - * Styled mode - * @sample {highmaps} highcharts/css/titles/ - * Styled mode - * @default { "color": "#666666" } - * @apioption subtitle.style - */ - - /** - * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- - * and-string-formatting#html) to render the text. - * - * @type {Boolean} - * @default false - * @apioption subtitle.useHTML - */ - - /** - * The vertical alignment of the title. Can be one of "top", "middle" - * and "bottom". When a value is given, the title behaves as floating. - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @sample {highcharts} highcharts/subtitle/verticalalign/ - * Footnote at the bottom right of plot area - * @sample {highstock} stock/chart/subtitle-footnote - * Footnote at the bottom right of plot area - * @default - * @since 2.1 - * @apioption subtitle.verticalAlign - */ - - /** - * The x position of the subtitle relative to the alignment within chart. - * spacingLeft and chart.spacingRight. - * - * @type {Number} - * @sample {highcharts} highcharts/subtitle/align/ - * Footnote at right of plot area - * @sample {highstock} stock/chart/subtitle-footnote - * Footnote at the bottom right of plot area - * @default 0 - * @since 2.0 - * @apioption subtitle.x - */ - - /** - * The y position of the subtitle relative to the alignment within chart. - * spacingTop and chart.spacingBottom. By default the subtitle is laid - * out below the title unless the title is floating. - * - * @type {Number} - * @sample {highcharts} highcharts/subtitle/verticalalign/ - * Footnote at the bottom right of plot area - * @sample {highstock} stock/chart/subtitle-footnote - * Footnote at the bottom right of plot area - * @default {highcharts} null - * @default {highstock} null - * @default {highmaps} - * @since 2.0 - * @apioption subtitle.y - */ - - /** - * The subtitle of the chart. - * - * @type {String} - * @sample {highcharts} highcharts/subtitle/text/ Custom subtitle - * @sample {highcharts} highcharts/subtitle/text-formatted/ Formatted and linked text. - * @sample {highstock} stock/chart/subtitle-text Custom subtitle - * @sample {highstock} stock/chart/subtitle-text-formatted Formatted and linked text. - */ - text: '', - - /** - * The horizontal alignment of the subtitle. Can be one of "left", - * "center" and "right". - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @sample {highcharts} highcharts/subtitle/align/ Footnote at right of plot area - * @sample {highstock} stock/chart/subtitle-footnote Footnote at bottom right of plot area - * @default center - * @since 2.0 - */ - align: 'center', - - /** - * Adjustment made to the subtitle width, normally to reserve space - * for the exporting burger menu. - * - * @type {Number} - * @see [title.widthAdjust](#title.widthAdjust) - * @sample {highcharts} highcharts/title/widthadjust/ Wider menu, greater padding - * @sample {highstock} highcharts/title/widthadjust/ Wider menu, greater padding - * @sample {highmaps} highcharts/title/widthadjust/ Wider menu, greater padding - * @default -44 - * @since 4.2.5 - */ - widthAdjust: -44 - }, - - /** - * The plotOptions is a wrapper object for config objects for each series - * type. The config objects for each series can also be overridden for - * each series item as given in the series array. - * - * Configuration options for the series are given in three levels. Options - * for all series in a chart are given in the [plotOptions.series]( - * #plotOptions.series) object. Then options for all series of a specific - * type are given in the plotOptions of that type, for example - * `plotOptions.line`. Next, options for one single series are given in - * [the series array](#series). - * - */ - plotOptions: {}, - - /** - * HTML labels that can be positioned anywhere in the chart area. - * - */ - labels: { - - /** - * A HTML label that can be positioned anywhere in the chart area. - * - * @type {Array} - * @apioption labels.items - */ - - /** - * Inner HTML or text for the label. - * - * @type {String} - * @apioption labels.items.html - */ - - /** - * CSS styles for each label. To position the label, use left and top - * like this: - * - *
style: {
-		 *     left: '100px',
-		 *     top: '100px'
-		 * }
- * - * @type {CSSObject} - * @apioption labels.items.style - */ - - /** - * Shared CSS styles for all labels. - * - * @type {CSSObject} - * @default { "color": "#333333" } - */ - style: { - position: 'absolute', - color: '${palette.neutralColor80}' - } - }, - - /** - * The legend is a box containing a symbol and name for each series - * item or point item in the chart. Each series (or points in case - * of pie charts) is represented by a symbol and its name in the legend. - * - * It is possible to override the symbol creator function and - * create [custom legend symbols](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/studies/legend- - * custom-symbol/). - * - * @productdesc {highmaps} - * A Highmaps legend by default contains one legend item per series, but if - * a `colorAxis` is defined, the axis will be displayed in the legend. - * Either as a gradient, or as multiple legend items for `dataClasses`. - */ - legend: { - - /** - * The background color of the legend. - * - * @type {Color} - * @see In styled mode, the legend background fill can be applied with - * the `.highcharts-legend-box` class. - * @sample {highcharts} highcharts/legend/backgroundcolor/ Yellowish background - * @sample {highstock} stock/legend/align/ Various legend options - * @sample {highmaps} maps/legend/border-background/ Border and background options - * @apioption legend.backgroundColor - */ - - /** - * The width of the drawn border around the legend. - * - * @type {Number} - * @see In styled mode, the legend border stroke width can be applied - * with the `.highcharts-legend-box` class. - * @sample {highcharts} highcharts/legend/borderwidth/ 2px border width - * @sample {highstock} stock/legend/align/ Various legend options - * @sample {highmaps} maps/legend/border-background/ Border and background options - * @default 0 - * @apioption legend.borderWidth - */ - - /** - * Enable or disable the legend. - * - * @type {Boolean} - * @sample {highcharts} highcharts/legend/enabled-false/ Legend disabled - * @sample {highstock} stock/legend/align/ Various legend options - * @sample {highmaps} maps/legend/enabled-false/ Legend disabled - * @default {highstock} false - * @default {highmaps} true - */ - enabled: true, - - /** - * The horizontal alignment of the legend box within the chart area. - * Valid values are `left`, `center` and `right`. - * - * In the case that the legend is aligned in a corner position, the - * `layout` option will determine whether to place it above/below - * or on the side of the plot area. - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @sample {highcharts} highcharts/legend/align/ - * Legend at the right of the chart - * @sample {highstock} stock/legend/align/ - * Various legend options - * @sample {highmaps} maps/legend/alignment/ - * Legend alignment - * @since 2.0 - */ - align: 'center', - - /** - * If the [layout](legend.layout) is `horizontal` and the legend items - * span over two lines or more, whether to align the items into vertical - * columns. Setting this to `false` makes room for more items, but will - * look more messy. - * - * @since 6.1.0 - */ - alignColumns: true, - - /** - * When the legend is floating, the plot area ignores it and is allowed - * to be placed below it. - * - * @type {Boolean} - * @sample {highcharts} highcharts/legend/floating-false/ False by default - * @sample {highcharts} highcharts/legend/floating-true/ True - * @sample {highmaps} maps/legend/alignment/ Floating legend - * @default false - * @since 2.1 - * @apioption legend.floating - */ - - /** - * The layout of the legend items. Can be one of "horizontal" or "vertical". - * - * @validvalue ["horizontal", "vertical"] - * @type {String} - * @sample {highcharts} highcharts/legend/layout-horizontal/ Horizontal by default - * @sample {highcharts} highcharts/legend/layout-vertical/ Vertical - * @sample {highstock} stock/legend/layout-horizontal/ Horizontal by default - * @sample {highmaps} maps/legend/padding-itemmargin/ Vertical with data classes - * @sample {highmaps} maps/legend/layout-vertical/ Vertical with color axis gradient - * @default horizontal - */ - layout: 'horizontal', - - /** - * In a legend with horizontal layout, the itemDistance defines the - * pixel distance between each item. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/layout-horizontal/ 50px item distance - * @sample {highstock} highcharts/legend/layout-horizontal/ 50px item distance - * @default {highcharts} 20 - * @default {highstock} 20 - * @default {highmaps} 8 - * @since 3.0.3 - * @apioption legend.itemDistance - */ - - /** - * The pixel bottom margin for each legend item. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated - * @sample {highstock} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated - * @sample {highmaps} maps/legend/padding-itemmargin/ Padding and item margins demonstrated - * @default 0 - * @since 2.2.0 - * @apioption legend.itemMarginBottom - */ - - /** - * The pixel top margin for each legend item. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated - * @sample {highstock} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated - * @sample {highmaps} maps/legend/padding-itemmargin/ Padding and item margins demonstrated - * @default 0 - * @since 2.2.0 - * @apioption legend.itemMarginTop - */ - - /** - * The width for each legend item. By default the items are laid out - * successively. In a [horizontal layout](legend.layout), if the items - * are laid out across two rows or more, they will be vertically aligned - * depending on the [legend.alignColumns](legend.alignColumns) option. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/itemwidth-default/ Null by default - * @sample {highcharts} highcharts/legend/itemwidth-80/ 80 for aligned legend items - * @default null - * @since 2.0 - * @apioption legend.itemWidth - */ - - /** - * A [format string](http://www.highcharts.com/docs/chart-concepts/labels- - * and-string-formatting) for each legend label. Available variables - * relates to properties on the series, or the point in case of pies. - * - * @type {String} - * @default {name} - * @since 1.3 - * @apioption legend.labelFormat - */ - - /** - * Callback function to format each of the series' labels. The `this` - * keyword refers to the series object, or the point object in case - * of pie charts. By default the series or point name is printed. - * - * @productdesc {highmaps} - * In Highmaps the context can also be a data class in case - * of a `colorAxis`. - * - * @type {Function} - * @sample {highcharts} highcharts/legend/labelformatter/ Add text - * @sample {highmaps} maps/legend/labelformatter/ Data classes with label formatter - * @context {Series|Point} - */ - labelFormatter: function () { - return this.name; - }, - - /** - * Line height for the legend items. Deprecated as of 2.1\. Instead, - * the line height for each item can be set using itemStyle.lineHeight, - * and the padding between items using itemMarginTop and itemMarginBottom. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/lineheight/ Setting padding - * @default 16 - * @since 2.0 - * @product highcharts - * @apioption legend.lineHeight - */ - - /** - * If the plot area sized is calculated automatically and the legend - * is not floating, the legend margin is the space between the legend - * and the axis labels or plot area. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/margin-default/ 12 pixels by default - * @sample {highcharts} highcharts/legend/margin-30/ 30 pixels - * @default 12 - * @since 2.1 - * @apioption legend.margin - */ - - /** - * Maximum pixel height for the legend. When the maximum height is extended, - * navigation will show. - * - * @type {Number} - * @default undefined - * @since 2.3.0 - * @apioption legend.maxHeight - */ - - /** - * The color of the drawn border around the legend. - * - * @type {Color} - * @see In styled mode, the legend border stroke can be applied with - * the `.highcharts-legend-box` class. - * @sample {highcharts} highcharts/legend/bordercolor/ Brown border - * @sample {highstock} stock/legend/align/ Various legend options - * @sample {highmaps} maps/legend/border-background/ Border and background options - * @default #999999 - */ - borderColor: '${palette.neutralColor40}', - - /** - * The border corner radius of the legend. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/borderradius-default/ Square by default - * @sample {highcharts} highcharts/legend/borderradius-round/ 5px rounded - * @sample {highmaps} maps/legend/border-background/ Border and background options - * @default 0 - */ - borderRadius: 0, - - /** - * Options for the paging or navigation appearing when the legend - * is overflown. Navigation works well on screen, but not in static - * exported images. One way of working around that is to [increase - * the chart height in export](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/legend/navigation- - * enabled-false/). - * - */ - navigation: { - - /** - * How to animate the pages when navigating up or down. A value of `true` - * applies the default navigation given in the chart.animation option. - * Additional options can be given as an object containing values for - * easing and duration. - * - * @type {Boolean|Object} - * @sample {highcharts} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @sample {highstock} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @default true - * @since 2.2.4 - * @apioption legend.navigation.animation - */ - - /** - * The pixel size of the up and down arrows in the legend paging - * navigation. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @sample {highstock} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @default 12 - * @since 2.2.4 - * @apioption legend.navigation.arrowSize - */ - - /** - * Whether to enable the legend navigation. In most cases, disabling - * the navigation results in an unwanted overflow. - * - * See also the [adapt chart to legend](http://www.highcharts.com/plugin- - * registry/single/8/Adapt-Chart-To-Legend) plugin for a solution to - * extend the chart height to make room for the legend, optionally in - * exported charts only. - * - * @type {Boolean} - * @default true - * @since 4.2.4 - * @apioption legend.navigation.enabled - */ - - /** - * Text styles for the legend page navigation. - * - * @type {CSSObject} - * @see In styled mode, the navigation items are styled with the - * `.highcharts-legend-navigation` class. - * @sample {highcharts} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @sample {highstock} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @since 2.2.4 - * @apioption legend.navigation.style - */ - - /*= if (build.classic) { =*/ - - /** - * The color for the active up or down arrow in the legend page navigation. - * - * @type {Color} - * @see In styled mode, the active arrow be styled with the `.highcharts-legend-nav-active` class. - * @sample {highcharts} highcharts/legend/navigation/ Legend page navigation demonstrated - * @sample {highstock} highcharts/legend/navigation/ Legend page navigation demonstrated - * @default #003399 - * @since 2.2.4 - */ - activeColor: '${palette.highlightColor100}', - - /** - * The color of the inactive up or down arrow in the legend page - * navigation. . - * - * @type {Color} - * @see In styled mode, the inactive arrow be styled with the - * `.highcharts-legend-nav-inactive` class. - * @sample {highcharts} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @sample {highstock} highcharts/legend/navigation/ - * Legend page navigation demonstrated - * @default {highcharts} #cccccc - * @default {highstock} #cccccc - * @default {highmaps} ##cccccc - * @since 2.2.4 - */ - inactiveColor: '${palette.neutralColor20}' - /*= } =*/ - }, - - /** - * The inner padding of the legend box. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/padding-itemmargin/ - * Padding and item margins demonstrated - * @sample {highstock} highcharts/legend/padding-itemmargin/ - * Padding and item margins demonstrated - * @sample {highmaps} maps/legend/padding-itemmargin/ - * Padding and item margins demonstrated - * @default 8 - * @since 2.2.0 - * @apioption legend.padding - */ - - /** - * Whether to reverse the order of the legend items compared to the - * order of the series or points as defined in the configuration object. - * - * @type {Boolean} - * @see [yAxis.reversedStacks](#yAxis.reversedStacks), - * [series.legendIndex](#series.legendIndex) - * @sample {highcharts} highcharts/legend/reversed/ - * Stacked bar with reversed legend - * @default false - * @since 1.2.5 - * @apioption legend.reversed - */ - - /** - * Whether to show the symbol on the right side of the text rather than - * the left side. This is common in Arabic and Hebraic. - * - * @type {Boolean} - * @sample {highcharts} highcharts/legend/rtl/ Symbol to the right - * @default false - * @since 2.2 - * @apioption legend.rtl - */ - - /** - * CSS styles for the legend area. In the 1.x versions the position - * of the legend area was determined by CSS. In 2.x, the position is - * determined by properties like `align`, `verticalAlign`, `x` and `y`, - * but the styles are still parsed for backwards compatibility. - * - * @type {CSSObject} - * @deprecated - * @product highcharts highstock - * @apioption legend.style - */ - - /*= if (build.classic) { =*/ - - /** - * CSS styles for each legend item. Only a subset of CSS is supported, - * notably those options related to text. The default `textOverflow` - * property makes long texts truncate. Set it to `null` to wrap text - * instead. A `width` property can be added to control the text width. - * - * @type {CSSObject} - * @see In styled mode, the legend items can be styled with the - * `.highcharts-legend-item` class. - * @sample {highcharts} highcharts/legend/itemstyle/ Bold black text - * @sample {highmaps} maps/legend/itemstyle/ Item text styles - * @default { "color": "#333333", "cursor": "pointer", "fontSize": "12px", "fontWeight": "bold", "textOverflow": "ellipsis" } - */ - itemStyle: { - color: '${palette.neutralColor80}', - fontSize: '12px', - fontWeight: 'bold', - textOverflow: 'ellipsis' - }, - - /** - * CSS styles for each legend item in hover mode. Only a subset of - * CSS is supported, notably those options related to text. Properties - * are inherited from `style` unless overridden here. - * - * @type {CSSObject} - * @see In styled mode, the hovered legend items can be styled with - * the `.highcharts-legend-item:hover` pesudo-class. - * @sample {highcharts} highcharts/legend/itemhoverstyle/ Red on hover - * @sample {highmaps} maps/legend/itemstyle/ Item text styles - * @default { "color": "#000000" } - */ - itemHoverStyle: { - color: '${palette.neutralColor100}' - }, - - /** - * CSS styles for each legend item when the corresponding series or - * point is hidden. Only a subset of CSS is supported, notably those - * options related to text. Properties are inherited from `style` - * unless overridden here. - * - * @type {CSSObject} - * @see In styled mode, the hidden legend items can be styled with - * the `.highcharts-legend-item-hidden` class. - * @sample {highcharts} highcharts/legend/itemhiddenstyle/ Darker gray color - * @default { "color": "#cccccc" } - */ - itemHiddenStyle: { - color: '${palette.neutralColor20}' - }, - - /** - * Whether to apply a drop shadow to the legend. A `backgroundColor` - * also needs to be applied for this to take effect. The shadow can be - * an object configuration containing `color`, `offsetX`, `offsetY`, - * `opacity` and `width`. - * - * @type {Boolean|Object} - * @sample {highcharts} highcharts/legend/shadow/ - * White background and drop shadow - * @sample {highstock} stock/legend/align/ - * Various legend options - * @sample {highmaps} maps/legend/border-background/ - * Border and background options - * @default false - */ - shadow: false, - /*= } =*/ - - /** - * Default styling for the checkbox next to a legend item when - * `showCheckbox` is true. - */ - itemCheckboxStyle: { - position: 'absolute', - width: '13px', // for IE precision - height: '13px' - }, - // itemWidth: undefined, - - /** - * When this is true, the legend symbol width will be the same as - * the symbol height, which in turn defaults to the font size of the - * legend items. - * - * @type {Boolean} - * @default true - * @since 5.0.0 - */ - squareSymbol: true, - - /** - * The pixel height of the symbol for series types that use a rectangle - * in the legend. Defaults to the font size of legend items. - * - * @productdesc {highmaps} - * In Highmaps, when the symbol is the gradient of a vertical color - * axis, the height defaults to 200. - * - * @type {Number} - * @sample {highmaps} maps/legend/layout-vertical-sized/ - * Sized vertical gradient - * @sample {highmaps} maps/legend/padding-itemmargin/ - * No distance between data classes - * @since 3.0.8 - * @apioption legend.symbolHeight - */ - - /** - * The border radius of the symbol for series types that use a rectangle - * in the legend. Defaults to half the `symbolHeight`. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/symbolradius/ Round symbols - * @sample {highstock} highcharts/legend/symbolradius/ Round symbols - * @sample {highmaps} highcharts/legend/symbolradius/ Round symbols - * @since 3.0.8 - * @apioption legend.symbolRadius - */ - - /** - * The pixel width of the legend item symbol. When the `squareSymbol` - * option is set, this defaults to the `symbolHeight`, otherwise 16. - * - * @productdesc {highmaps} - * In Highmaps, when the symbol is the gradient of a horizontal color - * axis, the width defaults to 200. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/symbolwidth/ - * Greater symbol width and padding - * @sample {highmaps} maps/legend/padding-itemmargin/ - * Padding and item margins demonstrated - * @sample {highmaps} maps/legend/layout-vertical-sized/ - * Sized vertical gradient - * @apioption legend.symbolWidth - */ - - /** - * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- - * and-string-formatting#html) to render the legend item texts. Prior - * to 4.1.7, when using HTML, [legend.navigation](#legend.navigation) - * was disabled. - * - * @type {Boolean} - * @default false - * @apioption legend.useHTML - */ - - /** - * The width of the legend box. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/width/ Aligned to the plot area - * @default null - * @since 2.0 - * @apioption legend.width - */ - - /** - * The pixel padding between the legend item symbol and the legend - * item text. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/symbolpadding/ Greater symbol width and padding - * @default 5 - */ - symbolPadding: 5, - - /** - * The vertical alignment of the legend box. Can be one of `top`, - * `middle` or `bottom`. Vertical position can be further determined - * by the `y` option. - * - * In the case that the legend is aligned in a corner position, the - * `layout` option will determine whether to place it above/below - * or on the side of the plot area. - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @sample {highcharts} highcharts/legend/verticalalign/ Legend 100px from the top of the chart - * @sample {highstock} stock/legend/align/ Various legend options - * @sample {highmaps} maps/legend/alignment/ Legend alignment - * @default bottom - * @since 2.0 - */ - verticalAlign: 'bottom', - // width: undefined, - - /** - * The x offset of the legend relative to its horizontal alignment - * `align` within chart.spacingLeft and chart.spacingRight. Negative - * x moves it to the left, positive x moves it to the right. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/width/ Aligned to the plot area - * @default 0 - * @since 2.0 - */ - x: 0, - - /** - * The vertical offset of the legend relative to it's vertical alignment - * `verticalAlign` within chart.spacingTop and chart.spacingBottom. - * Negative y moves it up, positive y moves it down. - * - * @type {Number} - * @sample {highcharts} highcharts/legend/verticalalign/ Legend 100px from the top of the chart - * @sample {highstock} stock/legend/align/ Various legend options - * @sample {highmaps} maps/legend/alignment/ Legend alignment - * @default 0 - * @since 2.0 - */ - y: 0, - - /** - * A title to be added on top of the legend. - * - * @sample {highcharts} highcharts/legend/title/ Legend title - * @sample {highmaps} maps/legend/alignment/ Legend with title - * @since 3.0 - */ - title: { - /** - * A text or HTML string for the title. - * - * @type {String} - * @default null - * @since 3.0 - * @apioption legend.title.text - */ - - /*= if (build.classic) { =*/ - - /** - * Generic CSS styles for the legend title. - * - * @type {CSSObject} - * @see In styled mode, the legend title is styled with the - * `.highcharts-legend-title` class. - * @default {"fontWeight":"bold"} - * @since 3.0 - */ - style: { - fontWeight: 'bold' - } - /*= } =*/ - } - }, - - - /** - * The loading options control the appearance of the loading screen - * that covers the plot area on chart operations. This screen only - * appears after an explicit call to `chart.showLoading()`. It is a - * utility for developers to communicate to the end user that something - * is going on, for example while retrieving new data via an XHR connection. - * The "Loading..." text itself is not part of this configuration - * object, but part of the `lang` object. - * - */ - loading: { - - /** - * The duration in milliseconds of the fade out effect. - * - * @type {Number} - * @sample highcharts/loading/hideduration/ Fade in and out over a second - * @default 100 - * @since 1.2.0 - * @apioption loading.hideDuration - */ - - /** - * The duration in milliseconds of the fade in effect. - * - * @type {Number} - * @sample highcharts/loading/hideduration/ Fade in and out over a second - * @default 100 - * @since 1.2.0 - * @apioption loading.showDuration - */ - /*= if (build.classic) { =*/ - - /** - * CSS styles for the loading label `span`. - * - * @type {CSSObject} - * @see In styled mode, the loading label is styled with the - * `.highcharts-legend-loading-inner` class. - * @sample {highcharts|highmaps} highcharts/loading/labelstyle/ Vertically centered - * @sample {highstock} stock/loading/general/ Label styles - * @default { "fontWeight": "bold", "position": "relative", "top": "45%" } - * @since 1.2.0 - */ - labelStyle: { - fontWeight: 'bold', - position: 'relative', - top: '45%' - }, - - /** - * CSS styles for the loading screen that covers the plot area. - * - * @type {CSSObject} - * @see In styled mode, the loading label is styled with the `.highcharts-legend-loading` class. - * @sample {highcharts|highmaps} highcharts/loading/style/ Gray plot area, white text - * @sample {highstock} stock/loading/general/ Gray plot area, white text - * @default { "position": "absolute", "backgroundColor": "#ffffff", "opacity": 0.5, "textAlign": "center" } - * @since 1.2.0 - */ - style: { - position: 'absolute', - backgroundColor: '${palette.backgroundColor}', - opacity: 0.5, - textAlign: 'center' - } - /*= } =*/ - }, - - - /** - * Options for the tooltip that appears when the user hovers over a - * series or point. - * - */ - tooltip: { - - - /** - * The color of the tooltip border. When `null`, the border takes the - * color of the corresponding series or point. - * - * @type {Color} - * @sample {highcharts} highcharts/tooltip/bordercolor-default/ - * Follow series by default - * @sample {highcharts} highcharts/tooltip/bordercolor-black/ - * Black border - * @sample {highstock} stock/tooltip/general/ - * Styled tooltip - * @sample {highmaps} maps/tooltip/background-border/ - * Background and border demo - * @default null - * @apioption tooltip.borderColor - */ - - /** - * Since 4.1, the crosshair definitions are moved to the Axis object - * in order for a better separation from the tooltip. See - * [xAxis.crosshair](#xAxis.crosshair). - * - * @type {Mixed} - * @deprecated - * @sample {highcharts} highcharts/tooltip/crosshairs-x/ - * Enable a crosshair for the x value - * @default true - * @apioption tooltip.crosshairs - */ - - /** - * Whether the tooltip should follow the mouse as it moves across columns, - * pie slices and other point types with an extent. By default it behaves - * this way for scatter, bubble and pie series by override in the `plotOptions` - * for those series types. - * - * For touch moves to behave the same way, [followTouchMove]( - * #tooltip.followTouchMove) must be `true` also. - * - * @type {Boolean} - * @default {highcharts} false - * @default {highstock} false - * @default {highmaps} true - * @since 3.0 - * @apioption tooltip.followPointer - */ - - /** - * Whether the tooltip should follow the finger as it moves on a touch - * device. If this is `true` and [chart.panning](#chart.panning) is - * set,`followTouchMove` will take over one-finger touches, so the user - * needs to use two fingers for zooming and panning. - * - * @type {Boolean} - * @default {highcharts} true - * @default {highstock} true - * @default {highmaps} false - * @since 3.0.1 - * @apioption tooltip.followTouchMove - */ - - /** - * Callback function to format the text of the tooltip from scratch. Return - * `false` to disable tooltip for a specific point on series. - * - * A subset of HTML is supported. Unless `useHTML` is true, the HTML of the - * tooltip is parsed and converted to SVG, therefore this isn't a complete HTML - * renderer. The following tags are supported: ``, ``, ``, ``, - * `
`, ``. Spans can be styled with a `style` attribute, - * but only text-related CSS that is shared with SVG is handled. - * - * Since version 2.1 the tooltip can be shared between multiple series - * through the `shared` option. The available data in the formatter - * differ a bit depending on whether the tooltip is shared or not. In - * a shared tooltip, all properties except `x`, which is common for - * all points, are kept in an array, `this.points`. - * - * Available data are: - * - *
- * - *
this.percentage (not shared) / this.points[i].percentage (shared)
- * - *
Stacked series and pies only. The point's percentage of the total. - *
- * - *
this.point (not shared) / this.points[i].point (shared)
- * - *
The point object. The point name, if defined, is available through - * `this.point.name`.
- * - *
this.points
- * - *
In a shared tooltip, this is an array containing all other properties - * for each point.
- * - *
this.series (not shared) / this.points[i].series (shared)
- * - *
The series object. The series name is available through - * `this.series.name`.
- * - *
this.total (not shared) / this.points[i].total (shared)
- * - *
Stacked series only. The total value at this point's x value. - *
- * - *
this.x
- * - *
The x value. This property is the same regardless of the tooltip - * being shared or not.
- * - *
this.y (not shared) / this.points[i].y (shared)
- * - *
The y value.
- * - *
- * - * @type {Function} - * @sample {highcharts} highcharts/tooltip/formatter-simple/ - * Simple string formatting - * @sample {highcharts} highcharts/tooltip/formatter-shared/ - * Formatting with shared tooltip - * @sample {highstock} stock/tooltip/formatter/ - * Formatting with shared tooltip - * @sample {highmaps} maps/tooltip/formatter/ - * String formatting - * @apioption tooltip.formatter - */ - - /** - * The number of milliseconds to wait until the tooltip is hidden when - * mouse out from a point or chart. - * - * @type {Number} - * @default 500 - * @since 3.0 - * @apioption tooltip.hideDelay - */ - - /** - * A callback function for formatting the HTML output for a single point - * in the tooltip. Like the `pointFormat` string, but with more flexibility. - * - * @type {Function} - * @context Point - * @since 4.1.0 - * @apioption tooltip.pointFormatter - */ - - /** - * A callback function to place the tooltip in a default position. The - * callback receives three parameters: `labelWidth`, `labelHeight` and - * `point`, where point contains values for `plotX` and `plotY` telling - * where the reference point is in the plot area. Add `chart.plotLeft` - * and `chart.plotTop` to get the full coordinates. - * - * The return should be an object containing x and y values, for example - * `{ x: 100, y: 100 }`. - * - * @type {Function} - * @sample {highcharts} highcharts/tooltip/positioner/ A fixed tooltip position - * @sample {highstock} stock/tooltip/positioner/ A fixed tooltip position on top of the chart - * @sample {highmaps} maps/tooltip/positioner/ A fixed tooltip position - * @since 2.2.4 - * @apioption tooltip.positioner - */ - - /** - * The name of a symbol to use for the border around the tooltip. - * - * @type {String} - * @default callout - * @validvalue ["callout", "square"] - * @since 4.0 - * @apioption tooltip.shape - */ - - /** - * When the tooltip is shared, the entire plot area will capture mouse - * movement or touch events. Tooltip texts for series types with ordered - * data (not pie, scatter, flags etc) will be shown in a single bubble. - * This is recommended for single series charts and for tablet/mobile - * optimized charts. - * - * See also [tooltip.split](#tooltip.split), that is better suited for - * charts with many series, especially line-type series. The - * `tooltip.split` option takes precedence over `tooltip.shared`. - * - * @type {Boolean} - * @sample {highcharts} highcharts/tooltip/shared-false/ False by default - * @sample {highcharts} highcharts/tooltip/shared-true/ True - * @sample {highcharts} highcharts/tooltip/shared-x-crosshair/ True with x axis crosshair - * @sample {highcharts} highcharts/tooltip/shared-true-mixed-types/ True with mixed series types - * @default false - * @since 2.1 - * @product highcharts highstock - * @apioption tooltip.shared - */ - - /** - * Split the tooltip into one label per series, with the header close - * to the axis. This is recommended over [shared](#tooltip.shared) tooltips - * for charts with multiple line series, generally making them easier - * to read. This option takes precedence over `tooltip.shared`. - * - * @productdesc {highstock} In Highstock, tooltips are split by default - * since v6.0.0. Stock charts typically contain multi-dimension points - * and multiple panes, making split tooltips the preferred layout over - * the previous `shared` tooltip. - * - * @type {Boolean} - * @sample {highcharts} highcharts/tooltip/split/ Split tooltip - * @sample {highstock} highcharts/tooltip/split/ Split tooltip - * @sample {highmaps} highcharts/tooltip/split/ Split tooltip - * @default {highcharts} false - * @default {highstock} true - * @product highcharts highstock - * @since 5.0.0 - * @apioption tooltip.split - */ - - /** - * Use HTML to render the contents of the tooltip instead of SVG. Using - * HTML allows advanced formatting like tables and images in the tooltip. - * It is also recommended for rtl languages as it works around rtl - * bugs in early Firefox. - * - * @type {Boolean} - * @sample {highcharts} highcharts/tooltip/footerformat/ A table for value alignment - * @sample {highcharts} highcharts/tooltip/fullhtml/ Full HTML tooltip - * @sample {highstock} highcharts/tooltip/footerformat/ A table for value alignment - * @sample {highstock} highcharts/tooltip/fullhtml/ Full HTML tooltip - * @sample {highmaps} maps/tooltip/usehtml/ Pure HTML tooltip - * @default false - * @since 2.2 - * @apioption tooltip.useHTML - */ - - /** - * How many decimals to show in each series' y value. This is overridable - * in each series' tooltip options object. The default is to preserve - * all decimals. - * - * @type {Number} - * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @since 2.2 - * @apioption tooltip.valueDecimals - */ - - /** - * A string to prepend to each series' y value. Overridable in each - * series' tooltip options object. - * - * @type {String} - * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @since 2.2 - * @apioption tooltip.valuePrefix - */ - - /** - * A string to append to each series' y value. Overridable in each series' - * tooltip options object. - * - * @type {String} - * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value - * @since 2.2 - * @apioption tooltip.valueSuffix - */ - - /** - * The format for the date in the tooltip header if the X axis is a - * datetime axis. The default is a best guess based on the smallest - * distance between points in the chart. - * - * @type {String} - * @sample {highcharts} highcharts/tooltip/xdateformat/ A different format - * @product highcharts highstock - * @apioption tooltip.xDateFormat - */ - - /** - * Enable or disable the tooltip. - * - * @type {Boolean} - * @sample {highcharts} highcharts/tooltip/enabled/ Disabled - * @sample {highcharts} highcharts/plotoptions/series-point-events-mouseover/ Disable tooltip and show values on chart instead - * @default true - */ - enabled: true, - - /** - * Enable or disable animation of the tooltip. In slow legacy IE browsers - * the animation is disabled by default. - * - * @type {Boolean} - * @default true - * @since 2.3.0 - */ - animation: svg, - - /** - * The radius of the rounded border corners. - * - * @type {Number} - * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 5px by default - * @sample {highcharts} highcharts/tooltip/borderradius-0/ Square borders - * @sample {highmaps} maps/tooltip/background-border/ Background and border demo - * @default 3 - */ - borderRadius: 3, - - /** - * For series on a datetime axes, the date format in the tooltip's - * header will by default be guessed based on the closest data points. - * This member gives the default string representations used for - * each unit. For an overview of the replacement codes, see - * [dateFormat](#Highcharts.dateFormat). - * - * Defaults to: - * - *
{
-		 *     millisecond:"%A, %b %e, %H:%M:%S.%L",
-		 *     second:"%A, %b %e, %H:%M:%S",
-		 *     minute:"%A, %b %e, %H:%M",
-		 *     hour:"%A, %b %e, %H:%M",
-		 *     day:"%A, %b %e, %Y",
-		 *     week:"Week from %A, %b %e, %Y",
-		 *     month:"%B %Y",
-		 *     year:"%Y"
-		 * }
- * - * @type {Object} - * @see [xAxis.dateTimeLabelFormats](#xAxis.dateTimeLabelFormats) - * @product highcharts highstock - */ - dateTimeLabelFormats: { - millisecond: '%A, %b %e, %H:%M:%S.%L', - second: '%A, %b %e, %H:%M:%S', - minute: '%A, %b %e, %H:%M', - hour: '%A, %b %e, %H:%M', - day: '%A, %b %e, %Y', - week: 'Week from %A, %b %e, %Y', - month: '%B %Y', - year: '%Y' - }, - - /** - * A string to append to the tooltip format. - * - * @sample {highcharts} highcharts/tooltip/footerformat/ A table for value alignment - * @sample {highmaps} maps/tooltip/format/ Format demo - * @since 2.2 - */ - footerFormat: '', - - /** - * Padding inside the tooltip, in pixels. - * - * @type {Number} - * @default 8 - * @since 5.0.0 - */ - padding: 8, - - /** - * Proximity snap for graphs or single points. It defaults to 10 for - * mouse-powered devices and 25 for touch devices. - * - * Note that in most cases the whole plot area captures the mouse - * movement, and in these cases `tooltip.snap` doesn't make sense. - * This applies when [stickyTracking](#plotOptions.series.stickyTracking) - * is `true` (default) and when the tooltip is [shared](#tooltip.shared) - * or [split](#tooltip.split). - * - * @type {Number} - * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 10 px by default - * @sample {highcharts} highcharts/tooltip/snap-50/ 50 px on graph - * @default 10/25 - * @since 1.2.0 - * @product highcharts highstock - */ - snap: isTouchDevice ? 25 : 10, - /*= if (!build.classic) { =*/ - headerFormat: '{point.key}
', - pointFormat: '' + - '\u25CF {series.name}: ' + - '{point.y}
', - /*= } else { =*/ - - /** - * The background color or gradient for the tooltip. - * - * In styled mode, the stroke width is set in the `.highcharts-tooltip-box` class. - * - * @type {Color} - * @sample {highcharts} highcharts/tooltip/backgroundcolor-solid/ Yellowish background - * @sample {highcharts} highcharts/tooltip/backgroundcolor-gradient/ Gradient - * @sample {highcharts} highcharts/css/tooltip-border-background/ Tooltip in styled mode - * @sample {highstock} stock/tooltip/general/ Custom tooltip - * @sample {highstock} highcharts/css/tooltip-border-background/ Tooltip in styled mode - * @sample {highmaps} maps/tooltip/background-border/ Background and border demo - * @sample {highmaps} highcharts/css/tooltip-border-background/ Tooltip in styled mode - * @default rgba(247,247,247,0.85) - */ - backgroundColor: color('${palette.neutralColor3}').setOpacity(0.85).get(), - - /** - * The pixel width of the tooltip border. - * - * In styled mode, the stroke width is set in the `.highcharts-tooltip-box` class. - * - * @type {Number} - * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 2px by default - * @sample {highcharts} highcharts/tooltip/borderwidth/ No border (shadow only) - * @sample {highcharts} highcharts/css/tooltip-border-background/ Tooltip in styled mode - * @sample {highstock} stock/tooltip/general/ Custom tooltip - * @sample {highstock} highcharts/css/tooltip-border-background/ Tooltip in styled mode - * @sample {highmaps} maps/tooltip/background-border/ Background and border demo - * @sample {highmaps} highcharts/css/tooltip-border-background/ Tooltip in styled mode - * @default 1 - */ - borderWidth: 1, - - /** - * The HTML of the tooltip header line. Variables are enclosed by - * curly brackets. Available variables are `point.key`, `series.name`, - * `series.color` and other members from the `point` and `series` - * objects. The `point.key` variable contains the category name, x - * value or datetime string depending on the type of axis. For datetime - * axes, the `point.key` date format can be set using tooltip.xDateFormat. - * - * @type {String} - * @sample {highcharts} highcharts/tooltip/footerformat/ - * A HTML table in the tooltip - * @sample {highstock} highcharts/tooltip/footerformat/ - * A HTML table in the tooltip - * @sample {highmaps} maps/tooltip/format/ Format demo - */ - headerFormat: '{point.key}
', - - /** - * The HTML of the point's line in the tooltip. Variables are enclosed - * by curly brackets. Available variables are point.x, point.y, series. - * name and series.color and other properties on the same form. Furthermore, - * point.y can be extended by the `tooltip.valuePrefix` and - * `tooltip.valueSuffix` variables. This can also be overridden for each - * series, which makes it a good hook for displaying units. - * - * In styled mode, the dot is colored by a class name rather - * than the point color. - * - * @type {String} - * @sample {highcharts} highcharts/tooltip/pointformat/ A different point format with value suffix - * @sample {highmaps} maps/tooltip/format/ Format demo - * @default \u25CF {series.name}: {point.y}
- * @since 2.2 - */ - pointFormat: '\u25CF {series.name}: {point.y}
', - - /** - * Whether to apply a drop shadow to the tooltip. - * - * @type {Boolean} - * @sample {highcharts} highcharts/tooltip/bordercolor-default/ True by default - * @sample {highcharts} highcharts/tooltip/shadow/ False - * @sample {highmaps} maps/tooltip/positioner/ Fixed tooltip position, border and shadow disabled - * @default true - */ - shadow: true, - - /** - * CSS styles for the tooltip. The tooltip can also be styled through - * the CSS class `.highcharts-tooltip`. - * - * @type {CSSObject} - * @sample {highcharts} highcharts/tooltip/style/ Greater padding, bold text - * @default { "color": "#333333", "cursor": "default", "fontSize": "12px", "pointerEvents": "none", "whiteSpace": "nowrap" } - */ - style: { - color: '${palette.neutralColor80}', - cursor: 'default', - fontSize: '12px', - pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events - whiteSpace: 'nowrap' - } - /*= } =*/ - }, - - - /** - * Highchart by default puts a credits label in the lower right corner - * of the chart. This can be changed using these options. - */ - credits: { - - /** - * Whether to show the credits text. - * - * @type {Boolean} - * @sample {highcharts} highcharts/credits/enabled-false/ Credits disabled - * @sample {highstock} stock/credits/enabled/ Credits disabled - * @sample {highmaps} maps/credits/enabled-false/ Credits disabled - * @default true - */ - enabled: true, - - /** - * The URL for the credits label. - * - * @type {String} - * @sample {highcharts} highcharts/credits/href/ Custom URL and text - * @sample {highmaps} maps/credits/customized/ Custom URL and text - * @default {highcharts} http://www.highcharts.com - * @default {highstock} "http://www.highcharts.com" - * @default {highmaps} http://www.highcharts.com - */ - href: 'http://www.highcharts.com', - - /** - * Position configuration for the credits label. - * - * @type {Object} - * @sample {highcharts} highcharts/credits/position-left/ Left aligned - * @sample {highcharts} highcharts/credits/position-left/ Left aligned - * @sample {highmaps} maps/credits/customized/ Left aligned - * @sample {highmaps} maps/credits/customized/ Left aligned - * @since 2.1 - */ - position: { - - /** - * Horizontal alignment of the credits. - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @default right - */ - align: 'right', - - /** - * Horizontal pixel offset of the credits. - * - * @type {Number} - * @default -10 - */ - x: -10, - - /** - * Vertical alignment of the credits. - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @default bottom - */ - verticalAlign: 'bottom', - - /** - * Vertical pixel offset of the credits. - * - * @type {Number} - * @default -5 - */ - y: -5 - }, - /*= if (build.classic) { =*/ - - /** - * CSS styles for the credits label. - * - * @type {CSSObject} - * @see In styled mode, credits styles can be set with the - * `.highcharts-credits` class. - * @default { "cursor": "pointer", "color": "#999999", "fontSize": "10px" } - */ - style: { - - cursor: 'pointer', - color: '${palette.neutralColor40}', - fontSize: '9px' - }, - /*= } =*/ - - /** - * The text for the credits label. - * - * @productdesc {highmaps} - * If a map is loaded as GeoJSON, the text defaults to - * `Highcharts @ {map-credits}`. Otherwise, it defaults to - * `Highcharts.com`. - * - * @type {String} - * @sample {highcharts} highcharts/credits/href/ Custom URL and text - * @sample {highmaps} maps/credits/customized/ Custom URL and text - * @default {highcharts|highstock} Highcharts.com - */ - text: 'Highcharts.com' - } + /*= if (build.classic) { =*/ + + /** + * An array containing the default colors for the chart's series. When + * all colors are used, new colors are pulled from the start again. + * + * Default colors can also be set on a series or series.type basis, + * see [column.colors](#plotOptions.column.colors), + * [pie.colors](#plotOptions.pie.colors). + * + * In styled mode, the colors option doesn't exist. Instead, colors + * are defined in CSS and applied either through series or point class + * names, or through the [chart.colorCount](#chart.colorCount) option. + * + * + * ### Legacy + * + * In Highcharts 3.x, the default colors were: + * + *
colors: ['#2f7ed8', '#0d233a', '#8bbc21', '#910000', '#1aadce',
+     *     '#492970', '#f28f43', '#77a1e5', '#c42525', '#a6c96a']
+ * + * In Highcharts 2.x, the default colors were: + * + *
colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE',
+     *    '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92']
+ * + * @type {Array} + * @sample {highcharts} highcharts/chart/colors/ Assign a global color theme + * @default ["#7cb5ec", "#434348", "#90ed7d", "#f7a35c", "#8085e9", + * "#f15c80", "#e4d354", "#2b908f", "#f45b5b", "#91e8e1"] + */ + colors: '${palette.colors}'.split(' '), + /*= } =*/ + + + /** + * Styled mode only. Configuration object for adding SVG definitions for + * reusable elements. See [gradients, shadows and patterns](http://www. + * highcharts.com/docs/chart-design-and-style/gradients-shadows-and- + * patterns) for more information and code examples. + * + * @type {Object} + * @since 5.0.0 + * @apioption defs + */ + + /** + * @ignore-option + */ + symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], + lang: { + + /** + * The loading text that appears when the chart is set into the loading + * state following a call to `chart.showLoading`. + * + * @type {String} + * @default Loading... + */ + loading: 'Loading...', + + /** + * An array containing the months names. Corresponds to the `%B` format + * in `Highcharts.dateFormat()`. + * + * @type {Array} + * @default [ "January" , "February" , "March" , "April" , "May" , + * "June" , "July" , "August" , "September" , "October" , + * "November" , "December"] + */ + months: [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December' + ], + + /** + * An array containing the months names in abbreviated form. Corresponds + * to the `%b` format in `Highcharts.dateFormat()`. + * + * @type {Array} + * @default [ "Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , + * "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec"] + */ + shortMonths: [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', + 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ], + + /** + * An array containing the weekday names. + * + * @type {Array} + * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", + * "Friday", "Saturday"] + */ + weekdays: [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday' + ], + + /** + * Short week days, starting Sunday. If not specified, Highcharts uses + * the first three letters of the `lang.weekdays` option. + * + * @type {Array} + * @sample highcharts/lang/shortweekdays/ + * Finnish two-letter abbreviations + * @since 4.2.4 + * @apioption lang.shortWeekdays + */ + + /** + * What to show in a date field for invalid dates. Defaults to an empty + * string. + * + * @type {String} + * @since 4.1.8 + * @product highcharts highstock + * @apioption lang.invalidDate + */ + + /** + * The default decimal point used in the `Highcharts.numberFormat` + * method unless otherwise specified in the function arguments. + * + * @type {String} + * @default . + * @since 1.2.2 + */ + decimalPoint: '.', + + /** + * [Metric prefixes](http://en.wikipedia.org/wiki/Metric_prefix) used + * to shorten high numbers in axis labels. Replacing any of the positions + * with `null` causes the full number to be written. Setting `numericSymbols` + * to `null` disables shortening altogether. + * + * @type {Array} + * @sample {highcharts} highcharts/lang/numericsymbols/ + * Replacing the symbols with text + * @sample {highstock} highcharts/lang/numericsymbols/ + * Replacing the symbols with text + * @default [ "k" , "M" , "G" , "T" , "P" , "E"] + * @since 2.3.0 + */ + numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], + + /** + * The magnitude of [numericSymbols](#lang.numericSymbol) replacements. + * Use 10000 for Japanese, Korean and various Chinese locales, which + * use symbols for 10^4, 10^8 and 10^12. + * + * @type {Number} + * @sample highcharts/lang/numericsymbolmagnitude/ + * 10000 magnitude for Japanese + * @default 1000 + * @since 5.0.3 + * @apioption lang.numericSymbolMagnitude + */ + + /** + * The text for the label appearing when a chart is zoomed. + * + * @type {String} + * @default Reset zoom + * @since 1.2.4 + */ + resetZoom: 'Reset zoom', + + /** + * The tooltip title for the label appearing when a chart is zoomed. + * + * @type {String} + * @default Reset zoom level 1:1 + * @since 1.2.4 + */ + resetZoomTitle: 'Reset zoom level 1:1', + + /** + * The default thousands separator used in the `Highcharts.numberFormat` + * method unless otherwise specified in the function arguments. Since + * Highcharts 4.1 it defaults to a single space character, which is + * compatible with ISO and works across Anglo-American and continental + * European languages. + * + * The default is a single space. + * + * @type {String} + * @default + * @since 1.2.2 + */ + thousandsSep: ' ' + }, + + /** + * Global options that don't apply to each chart. These options, like + * the `lang` options, must be set using the `Highcharts.setOptions` + * method. + * + *
Highcharts.setOptions({
+     *     global: {
+     *         useUTC: false
+     *     }
+     * });
+ * + */ + + /** + * _Canvg rendering for Android 2.x is removed as of Highcharts 5.0\. + * Use the [libURL](#exporting.libURL) option to configure exporting._ + * + * The URL to the additional file to lazy load for Android 2.x devices. + * These devices don't support SVG, so we download a helper file that + * contains [canvg](http://code.google.com/p/canvg/), its dependency + * rbcolor, and our own CanVG Renderer class. To avoid hotlinking to + * our site, you can install canvas-tools.js on your own server and + * change this option accordingly. + * + * @type {String} + * @deprecated + * @default http://code.highcharts.com/{version}/modules/canvas-tools.js + * @product highcharts highmaps + * @apioption global.canvasToolsURL + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.useUTC](#time.useUTC) that supports individual time settings + * per chart. + * + * @deprecated + * @apioption global.useUTC + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.Date](#time.Date) that supports individual time settings + * per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.Date + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.getTimezoneOffset](#time.getTimezoneOffset) that supports + * individual time settings per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.getTimezoneOffset + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.timezone](#time.timezone) that supports individual time + * settings per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.timezone + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.timezoneOffset](#time.timezoneOffset) that supports individual + * time settings per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.timezoneOffset + */ + global: {}, + + + time: H.Time.prototype.defaultOptions, + chart: { + + /** + * When using multiple axis, the ticks of two or more opposite axes + * will automatically be aligned by adding ticks to the axis or axes + * with the least ticks, as if `tickAmount` were specified. + * + * This can be prevented by setting `alignTicks` to false. If the grid + * lines look messy, it's a good idea to hide them for the secondary + * axis by setting `gridLineWidth` to 0. + * + * If `startOnTick` or `endOnTick` in an Axis options are set to false, + * then the `alignTicks ` will be disabled for the Axis. + * + * Disabled for logarithmic axes. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/alignticks-true/ + * True by default + * @sample {highcharts} highcharts/chart/alignticks-false/ + * False + * @sample {highstock} stock/chart/alignticks-true/ + * True by default + * @sample {highstock} stock/chart/alignticks-false/ + * False + * @default true + * @product highcharts highstock + * @apioption chart.alignTicks + */ + + + /** + * Set the overall animation for all chart updating. Animation can be + * disabled throughout the chart by setting it to false here. It can + * be overridden for each individual API method as a function parameter. + * The only animation not affected by this option is the initial series + * animation, see [plotOptions.series.animation]( + * #plotOptions.series.animation). + * + * The animation can either be set as a boolean or a configuration + * object. If `true`, it will use the 'swing' jQuery easing and a + * duration of 500 ms. If used as a configuration object, the following + * properties are supported: + * + *
+ * + *
duration
+ * + *
The duration of the animation in milliseconds.
+ * + *
easing
+ * + *
A string reference to an easing function set on the `Math` object. + * See [the easing demo](http://jsfiddle.net/gh/get/library/pure/ + * highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ + * series-animation-easing/).
+ * + *
+ * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/chart/animation-none/ + * Updating with no animation + * @sample {highcharts} highcharts/chart/animation-duration/ + * With a longer duration + * @sample {highcharts} highcharts/chart/animation-easing/ + * With a jQuery UI easing + * @sample {highmaps} maps/chart/animation-none/ + * Updating with no animation + * @sample {highmaps} maps/chart/animation-duration/ + * With a longer duration + * @default true + * @apioption chart.animation + */ + + /** + * A CSS class name to apply to the charts container `div`, allowing + * unique CSS styling for each chart. + * + * @type {String} + * @apioption chart.className + */ + + /** + * Event listeners for the chart. + * + * @apioption chart.events + */ + + /** + * Fires when a series is added to the chart after load time, using + * the `addSeries` method. One parameter, `event`, is passed to the + * function, containing common event information. + * Through `event.options` you can access the series options that was + * passed to the `addSeries` method. Returning false prevents the series + * from being added. + * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-addseries/ Alert on add series + * @sample {highstock} stock/chart/events-addseries/ Alert on add series + * @since 1.2.0 + * @apioption chart.events.addSeries + */ + + /** + * Fires when clicking on the plot background. One parameter, `event`, + * is passed to the function, containing common event information. + * + * Information on the clicked spot can be found through `event.xAxis` + * and `event.yAxis`, which are arrays containing the axes of each dimension + * and each axis' value at the clicked spot. The primary axes are + * `event.xAxis[0]` and `event.yAxis[0]`. Remember the unit of a + * datetime axis is milliseconds since 1970-01-01 00:00:00. + * + *
click: function(e) {
+         *     console.log(
+         *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', e.xAxis[0].value),
+         *         e.yAxis[0].value
+         *     )
+         * }
+ * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-click/ + * Alert coordinates on click + * @sample {highcharts} highcharts/chart/events-container/ + * Alternatively, attach event to container + * @sample {highstock} stock/chart/events-click/ + * Alert coordinates on click + * @sample {highstock} highcharts/chart/events-container/ + * Alternatively, attach event to container + * @sample {highmaps} maps/chart/events-click/ + * Record coordinates on click + * @sample {highmaps} highcharts/chart/events-container/ + * Alternatively, attach event to container + * @since 1.2.0 + * @apioption chart.events.click + */ + + + /** + * Fires when the chart is finished loading. Since v4.2.2, it also waits + * for images to be loaded, for example from point markers. One parameter, + * `event`, is passed to the function, containing common event information. + * + * There is also a second parameter to the chart constructor where a + * callback function can be passed to be executed on chart.load. + * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-load/ + * Alert on chart load + * @sample {highstock} stock/chart/events-load/ + * Alert on chart load + * @sample {highmaps} maps/chart/events-load/ + * Add series on chart load + * @apioption chart.events.load + */ + + /** + * Fires when the chart is redrawn, either after a call to chart.redraw() + * or after an axis, series or point is modified with the `redraw` option + * set to true. One parameter, `event`, is passed to the function, containing common event information. + * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-redraw/ + * Alert on chart redraw + * @sample {highstock} stock/chart/events-redraw/ + * Alert on chart redraw when adding a series or moving the + * zoomed range + * @sample {highmaps} maps/chart/events-redraw/ + * Set subtitle on chart redraw + * @since 1.2.0 + * @apioption chart.events.redraw + */ + + /** + * Fires after initial load of the chart (directly after the `load` + * event), and after each redraw (directly after the `redraw` event). + * + * @type {Function} + * @context Chart + * @since 5.0.7 + * @apioption chart.events.render + */ + + /** + * Fires when an area of the chart has been selected. Selection is enabled + * by setting the chart's zoomType. One parameter, `event`, is passed + * to the function, containing common event information. The default action for the selection event is to + * zoom the chart to the selected area. It can be prevented by calling + * `event.preventDefault()`. + * + * Information on the selected area can be found through `event.xAxis` + * and `event.yAxis`, which are arrays containing the axes of each dimension + * and each axis' min and max values. The primary axes are `event.xAxis[0]` + * and `event.yAxis[0]`. Remember the unit of a datetime axis is milliseconds + * since 1970-01-01 00:00:00. + * + *
selection: function(event) {
+         *     // log the min and max of the primary, datetime x-axis
+         *     console.log(
+         *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', event.xAxis[0].min),
+         *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', event.xAxis[0].max)
+         *     );
+         *     // log the min and max of the y axis
+         *     console.log(event.yAxis[0].min, event.yAxis[0].max);
+         * }
+ * + * @type {Function} + * @sample {highcharts} highcharts/chart/events-selection/ + * Report on selection and reset + * @sample {highcharts} highcharts/chart/events-selection-points/ + * Select a range of points through a drag selection + * @sample {highstock} stock/chart/events-selection/ + * Report on selection and reset + * @sample {highstock} highcharts/chart/events-selection-points/ + * Select a range of points through a drag selection (Highcharts) + * @apioption chart.events.selection + */ + + /** + * The margin between the outer edge of the chart and the plot area. + * The numbers in the array designate top, right, bottom and left + * respectively. Use the options `marginTop`, `marginRight`, + * `marginBottom` and `marginLeft` for shorthand setting of one option. + * + * By default there is no margin. The actual space is dynamically calculated + * from the offset of axis labels, axis title, title, subtitle and legend + * in addition to the `spacingTop`, `spacingRight`, `spacingBottom` + * and `spacingLeft` options. + * + * @type {Array} + * @sample {highcharts} highcharts/chart/margins-zero/ + * Zero margins + * @sample {highstock} stock/chart/margin-zero/ + * Zero margins + * + * @defaults {all} null + * @apioption chart.margin + */ + + /** + * The margin between the bottom outer edge of the chart and the plot + * area. Use this to set a fixed pixel value for the margin as opposed + * to the default dynamic margin. See also `spacingBottom`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/marginbottom/ + * 100px bottom margin + * @sample {highstock} stock/chart/marginbottom/ + * 100px bottom margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @since 2.0 + * @apioption chart.marginBottom + */ + + /** + * The margin between the left outer edge of the chart and the plot + * area. Use this to set a fixed pixel value for the margin as opposed + * to the default dynamic margin. See also `spacingLeft`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/marginleft/ + * 150px left margin + * @sample {highstock} stock/chart/marginleft/ + * 150px left margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @default null + * @since 2.0 + * @apioption chart.marginLeft + */ + + /** + * The margin between the right outer edge of the chart and the plot + * area. Use this to set a fixed pixel value for the margin as opposed + * to the default dynamic margin. See also `spacingRight`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/marginright/ + * 100px right margin + * @sample {highstock} stock/chart/marginright/ + * 100px right margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @default null + * @since 2.0 + * @apioption chart.marginRight + */ + + /** + * The margin between the top outer edge of the chart and the plot area. + * Use this to set a fixed pixel value for the margin as opposed to + * the default dynamic margin. See also `spacingTop`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/margintop/ 100px top margin + * @sample {highstock} stock/chart/margintop/ + * 100px top margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @default null + * @since 2.0 + * @apioption chart.marginTop + */ + + /** + * Allows setting a key to switch between zooming and panning. Can be + * one of `alt`, `ctrl`, `meta` (the command key on Mac and Windows + * key on Windows) or `shift`. The keys are mapped directly to the key + * properties of the click event argument (`event.altKey`, `event.ctrlKey`, + * `event.metaKey` and `event.shiftKey`). + * + * @validvalue [null, "alt", "ctrl", "meta", "shift"] + * @type {String} + * @since 4.0.3 + * @product highcharts + * @apioption chart.panKey + */ + + /** + * Allow panning in a chart. Best used with [panKey](#chart.panKey) + * to combine zooming and panning. + * + * On touch devices, when the [tooltip.followTouchMove](#tooltip.followTouchMove) + * option is `true` (default), panning requires two fingers. To allow + * panning with one finger, set `followTouchMove` to `false`. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/pankey/ Zooming and panning + * @default {highcharts} false + * @default {highstock} true + * @since 4.0.3 + * @product highcharts highstock + * @apioption chart.panning + */ + + + /** + * Equivalent to [zoomType](#chart.zoomType), but for multitouch gestures + * only. By default, the `pinchType` is the same as the `zoomType` setting. + * However, pinching can be enabled separately in some cases, for example + * in stock charts where a mouse drag pans the chart, while pinching + * is enabled. When [tooltip.followTouchMove](#tooltip.followTouchMove) + * is true, pinchType only applies to two-finger touches. + * + * @validvalue ["x", "y", "xy"] + * @type {String} + * @default {highcharts} null + * @default {highstock} x + * @since 3.0 + * @product highcharts highstock + * @apioption chart.pinchType + */ + + /** + * The corner radius of the outer chart border. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/borderradius/ 20px radius + * @sample {highstock} stock/chart/border/ 10px radius + * @sample {highmaps} maps/chart/border/ Border options + * @default 0 + */ + borderRadius: 0, + /*= if (!build.classic) { =*/ + + /** + * In styled mode, this sets how many colors the class names + * should rotate between. With ten colors, series (or points) are + * given class names like `highcharts-color-0`, `highcharts-color-0` + * [...] `highcharts-color-9`. The equivalent in non-styled mode + * is to set colors using the [colors](#colors) setting. + * + * @type {Number} + * @default 10 + * @since 5.0.0 + */ + colorCount: 10, + /*= } =*/ + + /** + * Alias of `type`. + * + * @validvalue ["line", "spline", "column", "area", "areaspline", "pie"] + * @type {String} + * @deprecated + * @sample {highcharts} highcharts/chart/defaultseriestype/ Bar + * @default line + * @product highcharts + */ + defaultSeriesType: 'line', + + /** + * If true, the axes will scale to the remaining visible series once + * one series is hidden. If false, hiding and showing a series will + * not affect the axes or the other series. For stacks, once one series + * within the stack is hidden, the rest of the stack will close in + * around it even if the axis is not affected. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/ignorehiddenseries-true/ + * True by default + * @sample {highcharts} highcharts/chart/ignorehiddenseries-false/ + * False + * @sample {highcharts} highcharts/chart/ignorehiddenseries-true-stacked/ + * True with stack + * @sample {highstock} stock/chart/ignorehiddenseries-true/ + * True by default + * @sample {highstock} stock/chart/ignorehiddenseries-false/ + * False + * @default true + * @since 1.2.0 + * @product highcharts highstock + */ + ignoreHiddenSeries: true, + + + /** + * Whether to invert the axes so that the x axis is vertical and y axis + * is horizontal. When `true`, the x axis is [reversed](#xAxis.reversed) + * by default. + * + * @productdesc {highcharts} + * If a bar series is present in the chart, it will be inverted + * automatically. Inverting the chart doesn't have an effect if there + * are no cartesian series in the chart, or if the chart is + * [polar](#chart.polar). + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/inverted/ + * Inverted line + * @sample {highstock} stock/navigator/inverted/ + * Inverted stock chart + * @default false + * @product highcharts highstock + * @apioption chart.inverted + */ + + /** + * The distance between the outer edge of the chart and the content, + * like title or legend, or axis title and labels if present. The + * numbers in the array designate top, right, bottom and left respectively. + * Use the options spacingTop, spacingRight, spacingBottom and spacingLeft + * options for shorthand setting of one option. + * + * @type {Array} + * @see [chart.margin](#chart.margin) + * @default [10, 10, 15, 10] + * @since 3.0.6 + */ + spacing: [10, 10, 15, 10], + + /** + * The button that appears after a selection zoom, allowing the user + * to reset zoom. + * + */ + resetZoomButton: { + + /** + * What frame the button should be placed related to. Can be either + * `plot` or `chart` + * + * @validvalue ["plot", "chart"] + * @type {String} + * @sample {highcharts} highcharts/chart/resetzoombutton-relativeto/ + * Relative to the chart + * @sample {highstock} highcharts/chart/resetzoombutton-relativeto/ + * Relative to the chart + * @default plot + * @since 2.2 + * @apioption chart.resetZoomButton.relativeTo + */ + + /** + * A collection of attributes for the button. The object takes SVG + * attributes like `fill`, `stroke`, `stroke-width` or `r`, the border + * radius. The theme also supports `style`, a collection of CSS properties + * for the text. Equivalent attributes for the hover state are given + * in `theme.states.hover`. + * + * @type {Object} + * @sample {highcharts} highcharts/chart/resetzoombutton-theme/ + * Theming the button + * @sample {highstock} highcharts/chart/resetzoombutton-theme/ + * Theming the button + * @since 2.2 + */ + theme: { + + /** + * The Z index for the reset zoom button. The default value + * places it below the tooltip that has Z index 7. + */ + zIndex: 6 + }, + + /** + * The position of the button. + * + * @type {Object} + * @sample {highcharts} highcharts/chart/resetzoombutton-position/ + * Above the plot area + * @sample {highstock} highcharts/chart/resetzoombutton-position/ + * Above the plot area + * @sample {highmaps} highcharts/chart/resetzoombutton-position/ + * Above the plot area + * @since 2.2 + */ + position: { + + /** + * The horizontal alignment of the button. + * + * @type {String} + */ + align: 'right', + + /** + * The horizontal offset of the button. + * + * @type {Number} + */ + x: -10, + + /** + * The vertical alignment of the button. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @default top + * @apioption chart.resetZoomButton.position.verticalAlign + */ + + /** + * The vertical offset of the button. + * + * @type {Number} + */ + y: 10 + } + }, + + /** + * The pixel width of the plot area border. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/plotborderwidth/ 1px border + * @sample {highstock} stock/chart/plotborder/ + * 2px border + * @sample {highmaps} maps/chart/plotborder/ + * Plot border options + * @default 0 + * @apioption chart.plotBorderWidth + */ + + /** + * Whether to apply a drop shadow to the plot area. Requires that + * plotBackgroundColor be set. The shadow can be an object configuration + * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/chart/plotshadow/ Plot shadow + * @sample {highstock} stock/chart/plotshadow/ + * Plot shadow + * @sample {highmaps} maps/chart/plotborder/ + * Plot border options + * @default false + * @apioption chart.plotShadow + */ + + /** + * When true, cartesian charts like line, spline, area and column are + * transformed into the polar coordinate system. Requires + * `highcharts-more.js`. + * + * @type {Boolean} + * @default false + * @since 2.3.0 + * @product highcharts + * @apioption chart.polar + */ + + /** + * Whether to reflow the chart to fit the width of the container div + * on resizing the window. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/reflow-true/ True by default + * @sample {highcharts} highcharts/chart/reflow-false/ False + * @sample {highstock} stock/chart/reflow-true/ + * True by default + * @sample {highstock} stock/chart/reflow-false/ + * False + * @sample {highmaps} maps/chart/reflow-true/ + * True by default + * @sample {highmaps} maps/chart/reflow-false/ + * False + * @default true + * @since 2.1 + * @apioption chart.reflow + */ + + /** + * The HTML element where the chart will be rendered. If it is a string, + * the element by that id is used. The HTML element can also be passed + * by direct reference, or as the first argument of the chart constructor, + * in which case the option is not needed. + * + * @type {String|Object} + * @sample {highcharts} highcharts/chart/reflow-true/ + * String + * @sample {highcharts} highcharts/chart/renderto-object/ + * Object reference + * @sample {highcharts} highcharts/chart/renderto-jquery/ + * Object reference through jQuery + * @sample {highstock} stock/chart/renderto-string/ + * String + * @sample {highstock} stock/chart/renderto-object/ + * Object reference + * @sample {highstock} stock/chart/renderto-jquery/ + * Object reference through jQuery + * @apioption chart.renderTo + */ + + /** + * The background color of the marker square when selecting (zooming + * in on) an area of the chart. + * + * @type {Color} + * @see In styled mode, the selection marker fill is set with the + * `.highcharts-selection-marker` class. + * @default rgba(51,92,173,0.25) + * @since 2.1.7 + * @apioption chart.selectionMarkerFill + */ + + /** + * Whether to apply a drop shadow to the outer chart area. Requires + * that backgroundColor be set. The shadow can be an object configuration + * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/chart/shadow/ Shadow + * @sample {highstock} stock/chart/shadow/ + * Shadow + * @sample {highmaps} maps/chart/border/ + * Chart border and shadow + * @default false + * @apioption chart.shadow + */ + + /** + * Whether to show the axes initially. This only applies to empty charts + * where series are added dynamically, as axes are automatically added + * to cartesian series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/showaxes-false/ False by default + * @sample {highcharts} highcharts/chart/showaxes-true/ True + * @since 1.2.5 + * @product highcharts + * @apioption chart.showAxes + */ + + /** + * The space between the bottom edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingbottom/ + * Spacing bottom set to 100 + * @sample {highstock} stock/chart/spacingbottom/ + * Spacing bottom set to 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 15 + * @since 2.1 + * @apioption chart.spacingBottom + */ + + /** + * The space between the left edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingleft/ + * Spacing left set to 100 + * @sample {highstock} stock/chart/spacingleft/ + * Spacing left set to 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 10 + * @since 2.1 + * @apioption chart.spacingLeft + */ + + /** + * The space between the right edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top + * position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingright-100/ + * Spacing set to 100 + * @sample {highcharts} highcharts/chart/spacingright-legend/ + * Legend in right position with default spacing + * @sample {highstock} stock/chart/spacingright/ + * Spacing set to 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 10 + * @since 2.1 + * @apioption chart.spacingRight + */ + + /** + * The space between the top edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top + * position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingtop-100/ + * A top spacing of 100 + * @sample {highcharts} highcharts/chart/spacingtop-10/ + * Floating chart title makes the plot area align to the default + * spacingTop of 10. + * @sample {highstock} stock/chart/spacingtop/ + * A top spacing of 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 10 + * @since 2.1 + * @apioption chart.spacingTop + */ + + /** + * Additional CSS styles to apply inline to the container `div`. Note + * that since the default font styles are applied in the renderer, it + * is ignorant of the individual chart options and must be set globally. + * + * @type {CSSObject} + * @see In styled mode, general chart styles can be set with the `.highcharts-root` class. + * @sample {highcharts} highcharts/chart/style-serif-font/ + * Using a serif type font + * @sample {highcharts} highcharts/css/em/ + * Styled mode with relative font sizes + * @sample {highstock} stock/chart/style/ + * Using a serif type font + * @sample {highmaps} maps/chart/style-serif-font/ + * Using a serif type font + * @default {"fontFamily":"\"Lucida Grande\", \"Lucida Sans Unicode\", Verdana, Arial, Helvetica, sans-serif","fontSize":"12px"} + * @apioption chart.style + */ + + /** + * The default series type for the chart. Can be any of the chart types + * listed under [plotOptions](#plotOptions). + * + * @validvalue ["line", "spline", "column", "bar", "area", "areaspline", "pie", "arearange", "areasplinerange", "boxplot", "bubble", "columnrange", "errorbar", "funnel", "gauge", "heatmap", "polygon", "pyramid", "scatter", "solidgauge", "treemap", "waterfall"] + * @type {String} + * @sample {highcharts} highcharts/chart/type-bar/ Bar + * @sample {highstock} stock/chart/type/ + * Areaspline + * @sample {highmaps} maps/chart/type-mapline/ + * Mapline + * @default {highcharts} line + * @default {highstock} line + * @default {highmaps} map + * @since 2.1.0 + * @apioption chart.type + */ + + /** + * Decides in what dimensions the user can zoom by dragging the mouse. + * Can be one of `x`, `y` or `xy`. + * + * @validvalue [null, "x", "y", "xy"] + * @type {String} + * @see [panKey](#chart.panKey) + * @sample {highcharts} highcharts/chart/zoomtype-none/ None by default + * @sample {highcharts} highcharts/chart/zoomtype-x/ X + * @sample {highcharts} highcharts/chart/zoomtype-y/ Y + * @sample {highcharts} highcharts/chart/zoomtype-xy/ Xy + * @sample {highstock} stock/demo/basic-line/ None by default + * @sample {highstock} stock/chart/zoomtype-x/ X + * @sample {highstock} stock/chart/zoomtype-y/ Y + * @sample {highstock} stock/chart/zoomtype-xy/ Xy + * @product highcharts highstock + * @apioption chart.zoomType + */ + + /** + * An explicit width for the chart. By default (when `null`) the width + * is calculated from the offset width of the containing element. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/width/ 800px wide + * @sample {highstock} stock/chart/width/ 800px wide + * @sample {highmaps} maps/chart/size/ Chart with explicit size + * @default null + */ + width: null, + + /** + * An explicit height for the chart. If a _number_, the height is + * given in pixels. If given a _percentage string_ (for example `'56%'`), + * the height is given as the percentage of the actual chart width. + * This allows for preserving the aspect ratio across responsive + * sizes. + * + * By default (when `null`) the height is calculated from the offset + * height of the containing element, or 400 pixels if the containing + * element's height is 0. + * + * @type {Number|String} + * @sample {highcharts} highcharts/chart/height/ + * 500px height + * @sample {highstock} stock/chart/height/ + * 300px height + * @sample {highmaps} maps/chart/size/ + * Chart with explicit size + * @sample highcharts/chart/height-percent/ + * Highcharts with percentage height + * @default null + */ + height: null, + + /*= if (build.classic) { =*/ + + /** + * The color of the outer chart border. + * + * @type {Color} + * @see In styled mode, the stroke is set with the `.highcharts-background` + * class. + * @sample {highcharts} highcharts/chart/bordercolor/ Brown border + * @sample {highstock} stock/chart/border/ Brown border + * @sample {highmaps} maps/chart/border/ Border options + * @default #335cad + */ + borderColor: '${palette.highlightColor80}', + + /** + * The pixel width of the outer chart border. + * + * @type {Number} + * @see In styled mode, the stroke is set with the `.highcharts-background` + * class. + * @sample {highcharts} highcharts/chart/borderwidth/ 5px border + * @sample {highstock} stock/chart/border/ + * 2px border + * @sample {highmaps} maps/chart/border/ + * Border options + * @default 0 + * @apioption chart.borderWidth + */ + + /** + * The background color or gradient for the outer chart area. + * + * @type {Color} + * @see In styled mode, the background is set with the `.highcharts-background` class. + * @sample {highcharts} highcharts/chart/backgroundcolor-color/ Color + * @sample {highcharts} highcharts/chart/backgroundcolor-gradient/ Gradient + * @sample {highstock} stock/chart/backgroundcolor-color/ + * Color + * @sample {highstock} stock/chart/backgroundcolor-gradient/ + * Gradient + * @sample {highmaps} maps/chart/backgroundcolor-color/ + * Color + * @sample {highmaps} maps/chart/backgroundcolor-gradient/ + * Gradient + * @default #FFFFFF + */ + backgroundColor: '${palette.backgroundColor}', + + /** + * The background color or gradient for the plot area. + * + * @type {Color} + * @see In styled mode, the plot background is set with the `.highcharts-plot-background` class. + * @sample {highcharts} highcharts/chart/plotbackgroundcolor-color/ + * Color + * @sample {highcharts} highcharts/chart/plotbackgroundcolor-gradient/ + * Gradient + * @sample {highstock} stock/chart/plotbackgroundcolor-color/ + * Color + * @sample {highstock} stock/chart/plotbackgroundcolor-gradient/ + * Gradient + * @sample {highmaps} maps/chart/plotbackgroundcolor-color/ + * Color + * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ + * Gradient + * @default null + * @apioption chart.plotBackgroundColor + */ + + + /** + * The URL for an image to use as the plot background. To set an image + * as the background for the entire chart, set a CSS background image + * to the container element. Note that for the image to be applied to + * exported charts, its URL needs to be accessible by the export server. + * + * @type {String} + * @see In styled mode, a plot background image can be set with the + * `.highcharts-plot-background` class and a [custom pattern](http://www. + * highcharts.com/docs/chart-design-and-style/gradients-shadows-and- + * patterns). + * @sample {highcharts} highcharts/chart/plotbackgroundimage/ Skies + * @sample {highstock} stock/chart/plotbackgroundimage/ Skies + * @default null + * @apioption chart.plotBackgroundImage + */ + + /** + * The color of the inner chart or plot area border. + * + * @type {Color} + * @see In styled mode, a plot border stroke can be set with the + * `.highcharts-plot-border` class. + * @sample {highcharts} highcharts/chart/plotbordercolor/ Blue border + * @sample {highstock} stock/chart/plotborder/ Blue border + * @sample {highmaps} maps/chart/plotborder/ Plot border options + * @default #cccccc + */ + plotBorderColor: '${palette.neutralColor20}' + /*= } =*/ + + }, + + /** + * The chart's main title. + * + * @sample {highmaps} maps/title/title/ Title options demonstrated + */ + title: { + + /** + * When the title is floating, the plot area will not move to make space + * for it. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/zoomtype-none/ False by default + * @sample {highcharts} highcharts/title/floating/ + * True - title on top of the plot area + * @sample {highstock} stock/chart/title-floating/ + * True - title on top of the plot area + * @default false + * @since 2.1 + * @apioption title.floating + */ + + /** + * CSS styles for the title. Use this for font styling, but use `align`, + * `x` and `y` for text alignment. + * + * In styled mode, the title style is given in the `.highcharts-title` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/title/style/ Custom color and weight + * @sample {highstock} stock/chart/title-style/ Custom color and weight + * @sample highcharts/css/titles/ Styled mode + * @default {highcharts|highmaps} { "color": "#333333", "fontSize": "18px" } + * @default {highstock} { "color": "#333333", "fontSize": "16px" } + * @apioption title.style + */ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting#html) to render the text. + * + * @type {Boolean} + * @default false + * @apioption title.useHTML + */ + + /** + * The vertical alignment of the title. Can be one of `"top"`, `"middle"` + * and `"bottom"`. When a value is given, the title behaves as if + * [floating](#title.floating) were `true`. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @sample {highcharts} highcharts/title/verticalalign/ + * Chart title in bottom right corner + * @sample {highstock} stock/chart/title-verticalalign/ + * Chart title in bottom right corner + * @since 2.1 + * @apioption title.verticalAlign + */ + + /** + * The x position of the title relative to the alignment within chart. + * spacingLeft and chart.spacingRight. + * + * @type {Number} + * @sample {highcharts} highcharts/title/align/ + * Aligned to the plot area (x = 70px = margin left - spacing left) + * @sample {highstock} stock/chart/title-align/ + * Aligned to the plot area (x = 50px = margin left - spacing left) + * @default 0 + * @since 2.0 + * @apioption title.x + */ + + /** + * The y position of the title relative to the alignment within [chart. + * spacingTop](#chart.spacingTop) and [chart.spacingBottom](#chart.spacingBottom). + * By default it depends on the font size. + * + * @type {Number} + * @sample {highcharts} highcharts/title/y/ + * Title inside the plot area + * @sample {highstock} stock/chart/title-verticalalign/ + * Chart title in bottom right corner + * @since 2.0 + * @apioption title.y + */ + + /** + * The title of the chart. To disable the title, set the `text` to + * `null`. + * + * @type {String} + * @sample {highcharts} highcharts/title/text/ Custom title + * @sample {highstock} stock/chart/title-text/ Custom title + * @default {highcharts|highmaps} Chart title + * @default {highstock} null + */ + text: 'Chart title', + + /** + * The horizontal alignment of the title. Can be one of "left", "center" + * and "right". + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/title/align/ Aligned to the plot area (x = 70px = margin left - spacing left) + * @sample {highstock} stock/chart/title-align/ Aligned to the plot area (x = 50px = margin left - spacing left) + * @default center + * @since 2.0 + */ + align: 'center', + + /** + * The margin between the title and the plot area, or if a subtitle + * is present, the margin between the subtitle and the plot area. + * + * @type {Number} + * @sample {highcharts} highcharts/title/margin-50/ A chart title margin of 50 + * @sample {highcharts} highcharts/title/margin-subtitle/ The same margin applied with a subtitle + * @sample {highstock} stock/chart/title-margin/ A chart title margin of 50 + * @default 15 + * @since 2.1 + */ + margin: 15, + + /** + * Adjustment made to the title width, normally to reserve space for + * the exporting burger menu. + * + * @type {Number} + * @sample {highcharts} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highstock} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highmaps} highcharts/title/widthadjust/ Wider menu, greater padding + * @default -44 + * @since 4.2.5 + */ + widthAdjust: -44 + + }, + + /** + * The chart's subtitle. This can be used both to display a subtitle below + * the main title, and to display random text anywhere in the chart. The + * subtitle can be updated after chart initialization through the + * `Chart.setTitle` method. + * + * @sample {highmaps} maps/title/subtitle/ Subtitle options demonstrated + */ + subtitle: { + + /** + * When the subtitle is floating, the plot area will not move to make + * space for it. + * + * @type {Boolean} + * @sample {highcharts} highcharts/subtitle/floating/ + * Floating title and subtitle + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote floating at bottom right of plot area + * @default false + * @since 2.1 + * @apioption subtitle.floating + */ + + /** + * CSS styles for the title. + * + * In styled mode, the subtitle style is given in the `.highcharts-subtitle` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/subtitle/style/ + * Custom color and weight + * @sample {highcharts} highcharts/css/titles/ + * Styled mode + * @sample {highstock} stock/chart/subtitle-style + * Custom color and weight + * @sample {highstock} highcharts/css/titles/ + * Styled mode + * @sample {highmaps} highcharts/css/titles/ + * Styled mode + * @default { "color": "#666666" } + * @apioption subtitle.style + */ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting#html) to render the text. + * + * @type {Boolean} + * @default false + * @apioption subtitle.useHTML + */ + + /** + * The vertical alignment of the title. Can be one of "top", "middle" + * and "bottom". When a value is given, the title behaves as floating. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @sample {highcharts} highcharts/subtitle/verticalalign/ + * Footnote at the bottom right of plot area + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote at the bottom right of plot area + * @default + * @since 2.1 + * @apioption subtitle.verticalAlign + */ + + /** + * The x position of the subtitle relative to the alignment within chart. + * spacingLeft and chart.spacingRight. + * + * @type {Number} + * @sample {highcharts} highcharts/subtitle/align/ + * Footnote at right of plot area + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote at the bottom right of plot area + * @default 0 + * @since 2.0 + * @apioption subtitle.x + */ + + /** + * The y position of the subtitle relative to the alignment within chart. + * spacingTop and chart.spacingBottom. By default the subtitle is laid + * out below the title unless the title is floating. + * + * @type {Number} + * @sample {highcharts} highcharts/subtitle/verticalalign/ + * Footnote at the bottom right of plot area + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote at the bottom right of plot area + * @default {highcharts} null + * @default {highstock} null + * @default {highmaps} + * @since 2.0 + * @apioption subtitle.y + */ + + /** + * The subtitle of the chart. + * + * @type {String} + * @sample {highcharts} highcharts/subtitle/text/ Custom subtitle + * @sample {highcharts} highcharts/subtitle/text-formatted/ Formatted and linked text. + * @sample {highstock} stock/chart/subtitle-text Custom subtitle + * @sample {highstock} stock/chart/subtitle-text-formatted Formatted and linked text. + */ + text: '', + + /** + * The horizontal alignment of the subtitle. Can be one of "left", + * "center" and "right". + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/subtitle/align/ Footnote at right of plot area + * @sample {highstock} stock/chart/subtitle-footnote Footnote at bottom right of plot area + * @default center + * @since 2.0 + */ + align: 'center', + + /** + * Adjustment made to the subtitle width, normally to reserve space + * for the exporting burger menu. + * + * @type {Number} + * @see [title.widthAdjust](#title.widthAdjust) + * @sample {highcharts} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highstock} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highmaps} highcharts/title/widthadjust/ Wider menu, greater padding + * @default -44 + * @since 4.2.5 + */ + widthAdjust: -44 + }, + + /** + * The plotOptions is a wrapper object for config objects for each series + * type. The config objects for each series can also be overridden for + * each series item as given in the series array. + * + * Configuration options for the series are given in three levels. Options + * for all series in a chart are given in the [plotOptions.series]( + * #plotOptions.series) object. Then options for all series of a specific + * type are given in the plotOptions of that type, for example + * `plotOptions.line`. Next, options for one single series are given in + * [the series array](#series). + * + */ + plotOptions: {}, + + /** + * HTML labels that can be positioned anywhere in the chart area. + * + */ + labels: { + + /** + * A HTML label that can be positioned anywhere in the chart area. + * + * @type {Array} + * @apioption labels.items + */ + + /** + * Inner HTML or text for the label. + * + * @type {String} + * @apioption labels.items.html + */ + + /** + * CSS styles for each label. To position the label, use left and top + * like this: + * + *
style: {
+         *     left: '100px',
+         *     top: '100px'
+         * }
+ * + * @type {CSSObject} + * @apioption labels.items.style + */ + + /** + * Shared CSS styles for all labels. + * + * @type {CSSObject} + * @default { "color": "#333333" } + */ + style: { + position: 'absolute', + color: '${palette.neutralColor80}' + } + }, + + /** + * The legend is a box containing a symbol and name for each series + * item or point item in the chart. Each series (or points in case + * of pie charts) is represented by a symbol and its name in the legend. + * + * It is possible to override the symbol creator function and + * create [custom legend symbols](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/studies/legend- + * custom-symbol/). + * + * @productdesc {highmaps} + * A Highmaps legend by default contains one legend item per series, but if + * a `colorAxis` is defined, the axis will be displayed in the legend. + * Either as a gradient, or as multiple legend items for `dataClasses`. + */ + legend: { + + /** + * The background color of the legend. + * + * @type {Color} + * @see In styled mode, the legend background fill can be applied with + * the `.highcharts-legend-box` class. + * @sample {highcharts} highcharts/legend/backgroundcolor/ Yellowish background + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @apioption legend.backgroundColor + */ + + /** + * The width of the drawn border around the legend. + * + * @type {Number} + * @see In styled mode, the legend border stroke width can be applied + * with the `.highcharts-legend-box` class. + * @sample {highcharts} highcharts/legend/borderwidth/ 2px border width + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @default 0 + * @apioption legend.borderWidth + */ + + /** + * Enable or disable the legend. + * + * @type {Boolean} + * @sample {highcharts} highcharts/legend/enabled-false/ Legend disabled + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/enabled-false/ Legend disabled + * @default {highstock} false + * @default {highmaps} true + */ + enabled: true, + + /** + * The horizontal alignment of the legend box within the chart area. + * Valid values are `left`, `center` and `right`. + * + * In the case that the legend is aligned in a corner position, the + * `layout` option will determine whether to place it above/below + * or on the side of the plot area. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/legend/align/ + * Legend at the right of the chart + * @sample {highstock} stock/legend/align/ + * Various legend options + * @sample {highmaps} maps/legend/alignment/ + * Legend alignment + * @since 2.0 + */ + align: 'center', + + /** + * If the [layout](legend.layout) is `horizontal` and the legend items + * span over two lines or more, whether to align the items into vertical + * columns. Setting this to `false` makes room for more items, but will + * look more messy. + * + * @since 6.1.0 + */ + alignColumns: true, + + /** + * When the legend is floating, the plot area ignores it and is allowed + * to be placed below it. + * + * @type {Boolean} + * @sample {highcharts} highcharts/legend/floating-false/ False by default + * @sample {highcharts} highcharts/legend/floating-true/ True + * @sample {highmaps} maps/legend/alignment/ Floating legend + * @default false + * @since 2.1 + * @apioption legend.floating + */ + + /** + * The layout of the legend items. Can be one of "horizontal" or "vertical". + * + * @validvalue ["horizontal", "vertical"] + * @type {String} + * @sample {highcharts} highcharts/legend/layout-horizontal/ Horizontal by default + * @sample {highcharts} highcharts/legend/layout-vertical/ Vertical + * @sample {highstock} stock/legend/layout-horizontal/ Horizontal by default + * @sample {highmaps} maps/legend/padding-itemmargin/ Vertical with data classes + * @sample {highmaps} maps/legend/layout-vertical/ Vertical with color axis gradient + * @default horizontal + */ + layout: 'horizontal', + + /** + * In a legend with horizontal layout, the itemDistance defines the + * pixel distance between each item. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/layout-horizontal/ 50px item distance + * @sample {highstock} highcharts/legend/layout-horizontal/ 50px item distance + * @default {highcharts} 20 + * @default {highstock} 20 + * @default {highmaps} 8 + * @since 3.0.3 + * @apioption legend.itemDistance + */ + + /** + * The pixel bottom margin for each legend item. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highstock} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highmaps} maps/legend/padding-itemmargin/ Padding and item margins demonstrated + * @default 0 + * @since 2.2.0 + * @apioption legend.itemMarginBottom + */ + + /** + * The pixel top margin for each legend item. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highstock} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highmaps} maps/legend/padding-itemmargin/ Padding and item margins demonstrated + * @default 0 + * @since 2.2.0 + * @apioption legend.itemMarginTop + */ + + /** + * The width for each legend item. By default the items are laid out + * successively. In a [horizontal layout](legend.layout), if the items + * are laid out across two rows or more, they will be vertically aligned + * depending on the [legend.alignColumns](legend.alignColumns) option. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/itemwidth-default/ Null by default + * @sample {highcharts} highcharts/legend/itemwidth-80/ 80 for aligned legend items + * @default null + * @since 2.0 + * @apioption legend.itemWidth + */ + + /** + * A [format string](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting) for each legend label. Available variables + * relates to properties on the series, or the point in case of pies. + * + * @type {String} + * @default {name} + * @since 1.3 + * @apioption legend.labelFormat + */ + + /** + * Callback function to format each of the series' labels. The `this` + * keyword refers to the series object, or the point object in case + * of pie charts. By default the series or point name is printed. + * + * @productdesc {highmaps} + * In Highmaps the context can also be a data class in case + * of a `colorAxis`. + * + * @type {Function} + * @sample {highcharts} highcharts/legend/labelformatter/ Add text + * @sample {highmaps} maps/legend/labelformatter/ Data classes with label formatter + * @context {Series|Point} + */ + labelFormatter: function () { + return this.name; + }, + + /** + * Line height for the legend items. Deprecated as of 2.1\. Instead, + * the line height for each item can be set using itemStyle.lineHeight, + * and the padding between items using itemMarginTop and itemMarginBottom. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/lineheight/ Setting padding + * @default 16 + * @since 2.0 + * @product highcharts + * @apioption legend.lineHeight + */ + + /** + * If the plot area sized is calculated automatically and the legend + * is not floating, the legend margin is the space between the legend + * and the axis labels or plot area. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/margin-default/ 12 pixels by default + * @sample {highcharts} highcharts/legend/margin-30/ 30 pixels + * @default 12 + * @since 2.1 + * @apioption legend.margin + */ + + /** + * Maximum pixel height for the legend. When the maximum height is extended, + * navigation will show. + * + * @type {Number} + * @default undefined + * @since 2.3.0 + * @apioption legend.maxHeight + */ + + /** + * The color of the drawn border around the legend. + * + * @type {Color} + * @see In styled mode, the legend border stroke can be applied with + * the `.highcharts-legend-box` class. + * @sample {highcharts} highcharts/legend/bordercolor/ Brown border + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @default #999999 + */ + borderColor: '${palette.neutralColor40}', + + /** + * The border corner radius of the legend. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/borderradius-default/ Square by default + * @sample {highcharts} highcharts/legend/borderradius-round/ 5px rounded + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @default 0 + */ + borderRadius: 0, + + /** + * Options for the paging or navigation appearing when the legend + * is overflown. Navigation works well on screen, but not in static + * exported images. One way of working around that is to [increase + * the chart height in export](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/legend/navigation- + * enabled-false/). + * + */ + navigation: { + + /** + * How to animate the pages when navigating up or down. A value of `true` + * applies the default navigation given in the chart.animation option. + * Additional options can be given as an object containing values for + * easing and duration. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @default true + * @since 2.2.4 + * @apioption legend.navigation.animation + */ + + /** + * The pixel size of the up and down arrows in the legend paging + * navigation. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @default 12 + * @since 2.2.4 + * @apioption legend.navigation.arrowSize + */ + + /** + * Whether to enable the legend navigation. In most cases, disabling + * the navigation results in an unwanted overflow. + * + * See also the [adapt chart to legend](http://www.highcharts.com/plugin- + * registry/single/8/Adapt-Chart-To-Legend) plugin for a solution to + * extend the chart height to make room for the legend, optionally in + * exported charts only. + * + * @type {Boolean} + * @default true + * @since 4.2.4 + * @apioption legend.navigation.enabled + */ + + /** + * Text styles for the legend page navigation. + * + * @type {CSSObject} + * @see In styled mode, the navigation items are styled with the + * `.highcharts-legend-navigation` class. + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @since 2.2.4 + * @apioption legend.navigation.style + */ + + /*= if (build.classic) { =*/ + + /** + * The color for the active up or down arrow in the legend page navigation. + * + * @type {Color} + * @see In styled mode, the active arrow be styled with the `.highcharts-legend-nav-active` class. + * @sample {highcharts} highcharts/legend/navigation/ Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ Legend page navigation demonstrated + * @default #003399 + * @since 2.2.4 + */ + activeColor: '${palette.highlightColor100}', + + /** + * The color of the inactive up or down arrow in the legend page + * navigation. . + * + * @type {Color} + * @see In styled mode, the inactive arrow be styled with the + * `.highcharts-legend-nav-inactive` class. + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @default {highcharts} #cccccc + * @default {highstock} #cccccc + * @default {highmaps} ##cccccc + * @since 2.2.4 + */ + inactiveColor: '${palette.neutralColor20}' + /*= } =*/ + }, + + /** + * The inner padding of the legend box. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @sample {highstock} highcharts/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @sample {highmaps} maps/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @default 8 + * @since 2.2.0 + * @apioption legend.padding + */ + + /** + * Whether to reverse the order of the legend items compared to the + * order of the series or points as defined in the configuration object. + * + * @type {Boolean} + * @see [yAxis.reversedStacks](#yAxis.reversedStacks), + * [series.legendIndex](#series.legendIndex) + * @sample {highcharts} highcharts/legend/reversed/ + * Stacked bar with reversed legend + * @default false + * @since 1.2.5 + * @apioption legend.reversed + */ + + /** + * Whether to show the symbol on the right side of the text rather than + * the left side. This is common in Arabic and Hebraic. + * + * @type {Boolean} + * @sample {highcharts} highcharts/legend/rtl/ Symbol to the right + * @default false + * @since 2.2 + * @apioption legend.rtl + */ + + /** + * CSS styles for the legend area. In the 1.x versions the position + * of the legend area was determined by CSS. In 2.x, the position is + * determined by properties like `align`, `verticalAlign`, `x` and `y`, + * but the styles are still parsed for backwards compatibility. + * + * @type {CSSObject} + * @deprecated + * @product highcharts highstock + * @apioption legend.style + */ + + /*= if (build.classic) { =*/ + + /** + * CSS styles for each legend item. Only a subset of CSS is supported, + * notably those options related to text. The default `textOverflow` + * property makes long texts truncate. Set it to `null` to wrap text + * instead. A `width` property can be added to control the text width. + * + * @type {CSSObject} + * @see In styled mode, the legend items can be styled with the + * `.highcharts-legend-item` class. + * @sample {highcharts} highcharts/legend/itemstyle/ Bold black text + * @sample {highmaps} maps/legend/itemstyle/ Item text styles + * @default { "color": "#333333", "cursor": "pointer", "fontSize": "12px", "fontWeight": "bold", "textOverflow": "ellipsis" } + */ + itemStyle: { + color: '${palette.neutralColor80}', + fontSize: '12px', + fontWeight: 'bold', + textOverflow: 'ellipsis' + }, + + /** + * CSS styles for each legend item in hover mode. Only a subset of + * CSS is supported, notably those options related to text. Properties + * are inherited from `style` unless overridden here. + * + * @type {CSSObject} + * @see In styled mode, the hovered legend items can be styled with + * the `.highcharts-legend-item:hover` pesudo-class. + * @sample {highcharts} highcharts/legend/itemhoverstyle/ Red on hover + * @sample {highmaps} maps/legend/itemstyle/ Item text styles + * @default { "color": "#000000" } + */ + itemHoverStyle: { + color: '${palette.neutralColor100}' + }, + + /** + * CSS styles for each legend item when the corresponding series or + * point is hidden. Only a subset of CSS is supported, notably those + * options related to text. Properties are inherited from `style` + * unless overridden here. + * + * @type {CSSObject} + * @see In styled mode, the hidden legend items can be styled with + * the `.highcharts-legend-item-hidden` class. + * @sample {highcharts} highcharts/legend/itemhiddenstyle/ Darker gray color + * @default { "color": "#cccccc" } + */ + itemHiddenStyle: { + color: '${palette.neutralColor20}' + }, + + /** + * Whether to apply a drop shadow to the legend. A `backgroundColor` + * also needs to be applied for this to take effect. The shadow can be + * an object configuration containing `color`, `offsetX`, `offsetY`, + * `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/legend/shadow/ + * White background and drop shadow + * @sample {highstock} stock/legend/align/ + * Various legend options + * @sample {highmaps} maps/legend/border-background/ + * Border and background options + * @default false + */ + shadow: false, + /*= } =*/ + + /** + * Default styling for the checkbox next to a legend item when + * `showCheckbox` is true. + */ + itemCheckboxStyle: { + position: 'absolute', + width: '13px', // for IE precision + height: '13px' + }, + // itemWidth: undefined, + + /** + * When this is true, the legend symbol width will be the same as + * the symbol height, which in turn defaults to the font size of the + * legend items. + * + * @type {Boolean} + * @default true + * @since 5.0.0 + */ + squareSymbol: true, + + /** + * The pixel height of the symbol for series types that use a rectangle + * in the legend. Defaults to the font size of legend items. + * + * @productdesc {highmaps} + * In Highmaps, when the symbol is the gradient of a vertical color + * axis, the height defaults to 200. + * + * @type {Number} + * @sample {highmaps} maps/legend/layout-vertical-sized/ + * Sized vertical gradient + * @sample {highmaps} maps/legend/padding-itemmargin/ + * No distance between data classes + * @since 3.0.8 + * @apioption legend.symbolHeight + */ + + /** + * The border radius of the symbol for series types that use a rectangle + * in the legend. Defaults to half the `symbolHeight`. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/symbolradius/ Round symbols + * @sample {highstock} highcharts/legend/symbolradius/ Round symbols + * @sample {highmaps} highcharts/legend/symbolradius/ Round symbols + * @since 3.0.8 + * @apioption legend.symbolRadius + */ + + /** + * The pixel width of the legend item symbol. When the `squareSymbol` + * option is set, this defaults to the `symbolHeight`, otherwise 16. + * + * @productdesc {highmaps} + * In Highmaps, when the symbol is the gradient of a horizontal color + * axis, the width defaults to 200. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/symbolwidth/ + * Greater symbol width and padding + * @sample {highmaps} maps/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @sample {highmaps} maps/legend/layout-vertical-sized/ + * Sized vertical gradient + * @apioption legend.symbolWidth + */ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting#html) to render the legend item texts. Prior + * to 4.1.7, when using HTML, [legend.navigation](#legend.navigation) + * was disabled. + * + * @type {Boolean} + * @default false + * @apioption legend.useHTML + */ + + /** + * The width of the legend box. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/width/ Aligned to the plot area + * @default null + * @since 2.0 + * @apioption legend.width + */ + + /** + * The pixel padding between the legend item symbol and the legend + * item text. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/symbolpadding/ Greater symbol width and padding + * @default 5 + */ + symbolPadding: 5, + + /** + * The vertical alignment of the legend box. Can be one of `top`, + * `middle` or `bottom`. Vertical position can be further determined + * by the `y` option. + * + * In the case that the legend is aligned in a corner position, the + * `layout` option will determine whether to place it above/below + * or on the side of the plot area. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @sample {highcharts} highcharts/legend/verticalalign/ Legend 100px from the top of the chart + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/alignment/ Legend alignment + * @default bottom + * @since 2.0 + */ + verticalAlign: 'bottom', + // width: undefined, + + /** + * The x offset of the legend relative to its horizontal alignment + * `align` within chart.spacingLeft and chart.spacingRight. Negative + * x moves it to the left, positive x moves it to the right. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/width/ Aligned to the plot area + * @default 0 + * @since 2.0 + */ + x: 0, + + /** + * The vertical offset of the legend relative to it's vertical alignment + * `verticalAlign` within chart.spacingTop and chart.spacingBottom. + * Negative y moves it up, positive y moves it down. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/verticalalign/ Legend 100px from the top of the chart + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/alignment/ Legend alignment + * @default 0 + * @since 2.0 + */ + y: 0, + + /** + * A title to be added on top of the legend. + * + * @sample {highcharts} highcharts/legend/title/ Legend title + * @sample {highmaps} maps/legend/alignment/ Legend with title + * @since 3.0 + */ + title: { + /** + * A text or HTML string for the title. + * + * @type {String} + * @default null + * @since 3.0 + * @apioption legend.title.text + */ + + /*= if (build.classic) { =*/ + + /** + * Generic CSS styles for the legend title. + * + * @type {CSSObject} + * @see In styled mode, the legend title is styled with the + * `.highcharts-legend-title` class. + * @default {"fontWeight":"bold"} + * @since 3.0 + */ + style: { + fontWeight: 'bold' + } + /*= } =*/ + } + }, + + + /** + * The loading options control the appearance of the loading screen + * that covers the plot area on chart operations. This screen only + * appears after an explicit call to `chart.showLoading()`. It is a + * utility for developers to communicate to the end user that something + * is going on, for example while retrieving new data via an XHR connection. + * The "Loading..." text itself is not part of this configuration + * object, but part of the `lang` object. + * + */ + loading: { + + /** + * The duration in milliseconds of the fade out effect. + * + * @type {Number} + * @sample highcharts/loading/hideduration/ Fade in and out over a second + * @default 100 + * @since 1.2.0 + * @apioption loading.hideDuration + */ + + /** + * The duration in milliseconds of the fade in effect. + * + * @type {Number} + * @sample highcharts/loading/hideduration/ Fade in and out over a second + * @default 100 + * @since 1.2.0 + * @apioption loading.showDuration + */ + /*= if (build.classic) { =*/ + + /** + * CSS styles for the loading label `span`. + * + * @type {CSSObject} + * @see In styled mode, the loading label is styled with the + * `.highcharts-legend-loading-inner` class. + * @sample {highcharts|highmaps} highcharts/loading/labelstyle/ Vertically centered + * @sample {highstock} stock/loading/general/ Label styles + * @default { "fontWeight": "bold", "position": "relative", "top": "45%" } + * @since 1.2.0 + */ + labelStyle: { + fontWeight: 'bold', + position: 'relative', + top: '45%' + }, + + /** + * CSS styles for the loading screen that covers the plot area. + * + * @type {CSSObject} + * @see In styled mode, the loading label is styled with the `.highcharts-legend-loading` class. + * @sample {highcharts|highmaps} highcharts/loading/style/ Gray plot area, white text + * @sample {highstock} stock/loading/general/ Gray plot area, white text + * @default { "position": "absolute", "backgroundColor": "#ffffff", "opacity": 0.5, "textAlign": "center" } + * @since 1.2.0 + */ + style: { + position: 'absolute', + backgroundColor: '${palette.backgroundColor}', + opacity: 0.5, + textAlign: 'center' + } + /*= } =*/ + }, + + + /** + * Options for the tooltip that appears when the user hovers over a + * series or point. + * + */ + tooltip: { + + + /** + * The color of the tooltip border. When `null`, the border takes the + * color of the corresponding series or point. + * + * @type {Color} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ + * Follow series by default + * @sample {highcharts} highcharts/tooltip/bordercolor-black/ + * Black border + * @sample {highstock} stock/tooltip/general/ + * Styled tooltip + * @sample {highmaps} maps/tooltip/background-border/ + * Background and border demo + * @default null + * @apioption tooltip.borderColor + */ + + /** + * Since 4.1, the crosshair definitions are moved to the Axis object + * in order for a better separation from the tooltip. See + * [xAxis.crosshair](#xAxis.crosshair). + * + * @type {Mixed} + * @deprecated + * @sample {highcharts} highcharts/tooltip/crosshairs-x/ + * Enable a crosshair for the x value + * @default true + * @apioption tooltip.crosshairs + */ + + /** + * Whether the tooltip should follow the mouse as it moves across columns, + * pie slices and other point types with an extent. By default it behaves + * this way for scatter, bubble and pie series by override in the `plotOptions` + * for those series types. + * + * For touch moves to behave the same way, [followTouchMove]( + * #tooltip.followTouchMove) must be `true` also. + * + * @type {Boolean} + * @default {highcharts} false + * @default {highstock} false + * @default {highmaps} true + * @since 3.0 + * @apioption tooltip.followPointer + */ + + /** + * Whether the tooltip should follow the finger as it moves on a touch + * device. If this is `true` and [chart.panning](#chart.panning) is + * set,`followTouchMove` will take over one-finger touches, so the user + * needs to use two fingers for zooming and panning. + * + * @type {Boolean} + * @default {highcharts} true + * @default {highstock} true + * @default {highmaps} false + * @since 3.0.1 + * @apioption tooltip.followTouchMove + */ + + /** + * Callback function to format the text of the tooltip from scratch. Return + * `false` to disable tooltip for a specific point on series. + * + * A subset of HTML is supported. Unless `useHTML` is true, the HTML of the + * tooltip is parsed and converted to SVG, therefore this isn't a complete HTML + * renderer. The following tags are supported: ``, ``, ``, ``, + * `
`, ``. Spans can be styled with a `style` attribute, + * but only text-related CSS that is shared with SVG is handled. + * + * Since version 2.1 the tooltip can be shared between multiple series + * through the `shared` option. The available data in the formatter + * differ a bit depending on whether the tooltip is shared or not. In + * a shared tooltip, all properties except `x`, which is common for + * all points, are kept in an array, `this.points`. + * + * Available data are: + * + *
+ * + *
this.percentage (not shared) / this.points[i].percentage (shared)
+ * + *
Stacked series and pies only. The point's percentage of the total. + *
+ * + *
this.point (not shared) / this.points[i].point (shared)
+ * + *
The point object. The point name, if defined, is available through + * `this.point.name`.
+ * + *
this.points
+ * + *
In a shared tooltip, this is an array containing all other properties + * for each point.
+ * + *
this.series (not shared) / this.points[i].series (shared)
+ * + *
The series object. The series name is available through + * `this.series.name`.
+ * + *
this.total (not shared) / this.points[i].total (shared)
+ * + *
Stacked series only. The total value at this point's x value. + *
+ * + *
this.x
+ * + *
The x value. This property is the same regardless of the tooltip + * being shared or not.
+ * + *
this.y (not shared) / this.points[i].y (shared)
+ * + *
The y value.
+ * + *
+ * + * @type {Function} + * @sample {highcharts} highcharts/tooltip/formatter-simple/ + * Simple string formatting + * @sample {highcharts} highcharts/tooltip/formatter-shared/ + * Formatting with shared tooltip + * @sample {highstock} stock/tooltip/formatter/ + * Formatting with shared tooltip + * @sample {highmaps} maps/tooltip/formatter/ + * String formatting + * @apioption tooltip.formatter + */ + + /** + * The number of milliseconds to wait until the tooltip is hidden when + * mouse out from a point or chart. + * + * @type {Number} + * @default 500 + * @since 3.0 + * @apioption tooltip.hideDelay + */ + + /** + * A callback function for formatting the HTML output for a single point + * in the tooltip. Like the `pointFormat` string, but with more flexibility. + * + * @type {Function} + * @context Point + * @since 4.1.0 + * @apioption tooltip.pointFormatter + */ + + /** + * A callback function to place the tooltip in a default position. The + * callback receives three parameters: `labelWidth`, `labelHeight` and + * `point`, where point contains values for `plotX` and `plotY` telling + * where the reference point is in the plot area. Add `chart.plotLeft` + * and `chart.plotTop` to get the full coordinates. + * + * The return should be an object containing x and y values, for example + * `{ x: 100, y: 100 }`. + * + * @type {Function} + * @sample {highcharts} highcharts/tooltip/positioner/ A fixed tooltip position + * @sample {highstock} stock/tooltip/positioner/ A fixed tooltip position on top of the chart + * @sample {highmaps} maps/tooltip/positioner/ A fixed tooltip position + * @since 2.2.4 + * @apioption tooltip.positioner + */ + + /** + * The name of a symbol to use for the border around the tooltip. + * + * @type {String} + * @default callout + * @validvalue ["callout", "square"] + * @since 4.0 + * @apioption tooltip.shape + */ + + /** + * When the tooltip is shared, the entire plot area will capture mouse + * movement or touch events. Tooltip texts for series types with ordered + * data (not pie, scatter, flags etc) will be shown in a single bubble. + * This is recommended for single series charts and for tablet/mobile + * optimized charts. + * + * See also [tooltip.split](#tooltip.split), that is better suited for + * charts with many series, especially line-type series. The + * `tooltip.split` option takes precedence over `tooltip.shared`. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/shared-false/ False by default + * @sample {highcharts} highcharts/tooltip/shared-true/ True + * @sample {highcharts} highcharts/tooltip/shared-x-crosshair/ True with x axis crosshair + * @sample {highcharts} highcharts/tooltip/shared-true-mixed-types/ True with mixed series types + * @default false + * @since 2.1 + * @product highcharts highstock + * @apioption tooltip.shared + */ + + /** + * Split the tooltip into one label per series, with the header close + * to the axis. This is recommended over [shared](#tooltip.shared) tooltips + * for charts with multiple line series, generally making them easier + * to read. This option takes precedence over `tooltip.shared`. + * + * @productdesc {highstock} In Highstock, tooltips are split by default + * since v6.0.0. Stock charts typically contain multi-dimension points + * and multiple panes, making split tooltips the preferred layout over + * the previous `shared` tooltip. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/split/ Split tooltip + * @sample {highstock} highcharts/tooltip/split/ Split tooltip + * @sample {highmaps} highcharts/tooltip/split/ Split tooltip + * @default {highcharts} false + * @default {highstock} true + * @product highcharts highstock + * @since 5.0.0 + * @apioption tooltip.split + */ + + /** + * Use HTML to render the contents of the tooltip instead of SVG. Using + * HTML allows advanced formatting like tables and images in the tooltip. + * It is also recommended for rtl languages as it works around rtl + * bugs in early Firefox. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/footerformat/ A table for value alignment + * @sample {highcharts} highcharts/tooltip/fullhtml/ Full HTML tooltip + * @sample {highstock} highcharts/tooltip/footerformat/ A table for value alignment + * @sample {highstock} highcharts/tooltip/fullhtml/ Full HTML tooltip + * @sample {highmaps} maps/tooltip/usehtml/ Pure HTML tooltip + * @default false + * @since 2.2 + * @apioption tooltip.useHTML + */ + + /** + * How many decimals to show in each series' y value. This is overridable + * in each series' tooltip options object. The default is to preserve + * all decimals. + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @since 2.2 + * @apioption tooltip.valueDecimals + */ + + /** + * A string to prepend to each series' y value. Overridable in each + * series' tooltip options object. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @since 2.2 + * @apioption tooltip.valuePrefix + */ + + /** + * A string to append to each series' y value. Overridable in each series' + * tooltip options object. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @since 2.2 + * @apioption tooltip.valueSuffix + */ + + /** + * The format for the date in the tooltip header if the X axis is a + * datetime axis. The default is a best guess based on the smallest + * distance between points in the chart. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/xdateformat/ A different format + * @product highcharts highstock + * @apioption tooltip.xDateFormat + */ + + /** + * Enable or disable the tooltip. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/enabled/ Disabled + * @sample {highcharts} highcharts/plotoptions/series-point-events-mouseover/ Disable tooltip and show values on chart instead + * @default true + */ + enabled: true, + + /** + * Enable or disable animation of the tooltip. In slow legacy IE browsers + * the animation is disabled by default. + * + * @type {Boolean} + * @default true + * @since 2.3.0 + */ + animation: svg, + + /** + * The radius of the rounded border corners. + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 5px by default + * @sample {highcharts} highcharts/tooltip/borderradius-0/ Square borders + * @sample {highmaps} maps/tooltip/background-border/ Background and border demo + * @default 3 + */ + borderRadius: 3, + + /** + * For series on a datetime axes, the date format in the tooltip's + * header will by default be guessed based on the closest data points. + * This member gives the default string representations used for + * each unit. For an overview of the replacement codes, see + * [dateFormat](#Highcharts.dateFormat). + * + * Defaults to: + * + *
{
+         *     millisecond:"%A, %b %e, %H:%M:%S.%L",
+         *     second:"%A, %b %e, %H:%M:%S",
+         *     minute:"%A, %b %e, %H:%M",
+         *     hour:"%A, %b %e, %H:%M",
+         *     day:"%A, %b %e, %Y",
+         *     week:"Week from %A, %b %e, %Y",
+         *     month:"%B %Y",
+         *     year:"%Y"
+         * }
+ * + * @type {Object} + * @see [xAxis.dateTimeLabelFormats](#xAxis.dateTimeLabelFormats) + * @product highcharts highstock + */ + dateTimeLabelFormats: { + millisecond: '%A, %b %e, %H:%M:%S.%L', + second: '%A, %b %e, %H:%M:%S', + minute: '%A, %b %e, %H:%M', + hour: '%A, %b %e, %H:%M', + day: '%A, %b %e, %Y', + week: 'Week from %A, %b %e, %Y', + month: '%B %Y', + year: '%Y' + }, + + /** + * A string to append to the tooltip format. + * + * @sample {highcharts} highcharts/tooltip/footerformat/ A table for value alignment + * @sample {highmaps} maps/tooltip/format/ Format demo + * @since 2.2 + */ + footerFormat: '', + + /** + * Padding inside the tooltip, in pixels. + * + * @type {Number} + * @default 8 + * @since 5.0.0 + */ + padding: 8, + + /** + * Proximity snap for graphs or single points. It defaults to 10 for + * mouse-powered devices and 25 for touch devices. + * + * Note that in most cases the whole plot area captures the mouse + * movement, and in these cases `tooltip.snap` doesn't make sense. + * This applies when [stickyTracking](#plotOptions.series.stickyTracking) + * is `true` (default) and when the tooltip is [shared](#tooltip.shared) + * or [split](#tooltip.split). + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 10 px by default + * @sample {highcharts} highcharts/tooltip/snap-50/ 50 px on graph + * @default 10/25 + * @since 1.2.0 + * @product highcharts highstock + */ + snap: isTouchDevice ? 25 : 10, + /*= if (!build.classic) { =*/ + headerFormat: '{point.key}
', + pointFormat: '' + + '\u25CF {series.name}: ' + + '{point.y}
', + /*= } else { =*/ + + /** + * The background color or gradient for the tooltip. + * + * In styled mode, the stroke width is set in the `.highcharts-tooltip-box` class. + * + * @type {Color} + * @sample {highcharts} highcharts/tooltip/backgroundcolor-solid/ Yellowish background + * @sample {highcharts} highcharts/tooltip/backgroundcolor-gradient/ Gradient + * @sample {highcharts} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highstock} stock/tooltip/general/ Custom tooltip + * @sample {highstock} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highmaps} maps/tooltip/background-border/ Background and border demo + * @sample {highmaps} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @default rgba(247,247,247,0.85) + */ + backgroundColor: color('${palette.neutralColor3}').setOpacity(0.85).get(), + + /** + * The pixel width of the tooltip border. + * + * In styled mode, the stroke width is set in the `.highcharts-tooltip-box` class. + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 2px by default + * @sample {highcharts} highcharts/tooltip/borderwidth/ No border (shadow only) + * @sample {highcharts} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highstock} stock/tooltip/general/ Custom tooltip + * @sample {highstock} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highmaps} maps/tooltip/background-border/ Background and border demo + * @sample {highmaps} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @default 1 + */ + borderWidth: 1, + + /** + * The HTML of the tooltip header line. Variables are enclosed by + * curly brackets. Available variables are `point.key`, `series.name`, + * `series.color` and other members from the `point` and `series` + * objects. The `point.key` variable contains the category name, x + * value or datetime string depending on the type of axis. For datetime + * axes, the `point.key` date format can be set using tooltip.xDateFormat. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/footerformat/ + * A HTML table in the tooltip + * @sample {highstock} highcharts/tooltip/footerformat/ + * A HTML table in the tooltip + * @sample {highmaps} maps/tooltip/format/ Format demo + */ + headerFormat: '{point.key}
', + + /** + * The HTML of the point's line in the tooltip. Variables are enclosed + * by curly brackets. Available variables are point.x, point.y, series. + * name and series.color and other properties on the same form. Furthermore, + * point.y can be extended by the `tooltip.valuePrefix` and + * `tooltip.valueSuffix` variables. This can also be overridden for each + * series, which makes it a good hook for displaying units. + * + * In styled mode, the dot is colored by a class name rather + * than the point color. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/pointformat/ A different point format with value suffix + * @sample {highmaps} maps/tooltip/format/ Format demo + * @default \u25CF {series.name}: {point.y}
+ * @since 2.2 + */ + pointFormat: '\u25CF {series.name}: {point.y}
', + + /** + * Whether to apply a drop shadow to the tooltip. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ True by default + * @sample {highcharts} highcharts/tooltip/shadow/ False + * @sample {highmaps} maps/tooltip/positioner/ Fixed tooltip position, border and shadow disabled + * @default true + */ + shadow: true, + + /** + * CSS styles for the tooltip. The tooltip can also be styled through + * the CSS class `.highcharts-tooltip`. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/tooltip/style/ Greater padding, bold text + * @default { "color": "#333333", "cursor": "default", "fontSize": "12px", "pointerEvents": "none", "whiteSpace": "nowrap" } + */ + style: { + color: '${palette.neutralColor80}', + cursor: 'default', + fontSize: '12px', + pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events + whiteSpace: 'nowrap' + } + /*= } =*/ + }, + + + /** + * Highchart by default puts a credits label in the lower right corner + * of the chart. This can be changed using these options. + */ + credits: { + + /** + * Whether to show the credits text. + * + * @type {Boolean} + * @sample {highcharts} highcharts/credits/enabled-false/ Credits disabled + * @sample {highstock} stock/credits/enabled/ Credits disabled + * @sample {highmaps} maps/credits/enabled-false/ Credits disabled + * @default true + */ + enabled: true, + + /** + * The URL for the credits label. + * + * @type {String} + * @sample {highcharts} highcharts/credits/href/ Custom URL and text + * @sample {highmaps} maps/credits/customized/ Custom URL and text + * @default {highcharts} http://www.highcharts.com + * @default {highstock} "http://www.highcharts.com" + * @default {highmaps} http://www.highcharts.com + */ + href: 'http://www.highcharts.com', + + /** + * Position configuration for the credits label. + * + * @type {Object} + * @sample {highcharts} highcharts/credits/position-left/ Left aligned + * @sample {highcharts} highcharts/credits/position-left/ Left aligned + * @sample {highmaps} maps/credits/customized/ Left aligned + * @sample {highmaps} maps/credits/customized/ Left aligned + * @since 2.1 + */ + position: { + + /** + * Horizontal alignment of the credits. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @default right + */ + align: 'right', + + /** + * Horizontal pixel offset of the credits. + * + * @type {Number} + * @default -10 + */ + x: -10, + + /** + * Vertical alignment of the credits. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @default bottom + */ + verticalAlign: 'bottom', + + /** + * Vertical pixel offset of the credits. + * + * @type {Number} + * @default -5 + */ + y: -5 + }, + /*= if (build.classic) { =*/ + + /** + * CSS styles for the credits label. + * + * @type {CSSObject} + * @see In styled mode, credits styles can be set with the + * `.highcharts-credits` class. + * @default { "cursor": "pointer", "color": "#999999", "fontSize": "10px" } + */ + style: { + + cursor: 'pointer', + color: '${palette.neutralColor40}', + fontSize: '9px' + }, + /*= } =*/ + + /** + * The text for the credits label. + * + * @productdesc {highmaps} + * If a map is loaded as GeoJSON, the text defaults to + * `Highcharts @ {map-credits}`. Otherwise, it defaults to + * `Highcharts.com`. + * + * @type {String} + * @sample {highcharts} highcharts/credits/href/ Custom URL and text + * @sample {highmaps} maps/credits/customized/ Custom URL and text + * @default {highcharts|highstock} Highcharts.com + */ + text: 'Highcharts.com' + } }; /** @@ -2932,16 +2932,16 @@ H.defaultOptions = { */ H.setOptions = function (options) { - // Copy in the default options - H.defaultOptions = merge(true, H.defaultOptions, options); + // Copy in the default options + H.defaultOptions = merge(true, H.defaultOptions, options); - // Update the time object - H.time.update( - merge(H.defaultOptions.global, H.defaultOptions.time), - false - ); + // Update the time object + H.time.update( + merge(H.defaultOptions.global, H.defaultOptions.time), + false + ); - return H.defaultOptions; + return H.defaultOptions; }; /** @@ -2949,7 +2949,7 @@ H.setOptions = function (options) { * wasn't enough because the setOptions method created a new object. */ H.getOptions = function () { - return H.defaultOptions; + return H.defaultOptions; }; @@ -2981,5 +2981,5 @@ H.time = new H.Time(merge(H.defaultOptions.global, H.defaultOptions.time)); * @returns {String} The formatted date. */ H.dateFormat = function (format, timestamp, capitalize) { - return H.time.dateFormat(format, timestamp, capitalize); + return H.time.dateFormat(format, timestamp, capitalize); }; diff --git a/js/parts/OrdinalAxis.js b/js/parts/OrdinalAxis.js index e38ade7cb50..b4ffc0763e4 100644 --- a/js/parts/OrdinalAxis.js +++ b/js/parts/OrdinalAxis.js @@ -11,17 +11,17 @@ import './Utilities.js'; import './Chart.js'; import './Series.js'; var addEvent = H.addEvent, - Axis = H.Axis, - Chart = H.Chart, - css = H.css, - defined = H.defined, - each = H.each, - extend = H.extend, - noop = H.noop, - pick = H.pick, - Series = H.Series, - timeUnits = H.timeUnits, - wrap = H.wrap; + Axis = H.Axis, + Chart = H.Chart, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + noop = H.noop, + pick = H.pick, + Series = H.Series, + timeUnits = H.timeUnits, + wrap = H.wrap; /* **************************************************************************** * Start ordinal axis logic * @@ -29,20 +29,20 @@ var addEvent = H.addEvent, wrap(Series.prototype, 'init', function (proceed) { - var series = this, - xAxis; + var series = this, + xAxis; - // call the original function - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + // call the original function + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); - xAxis = series.xAxis; + xAxis = series.xAxis; - // Destroy the extended ordinal index on updated data - if (xAxis && xAxis.options.ordinal) { - addEvent(series, 'updatedData', function () { - delete xAxis.ordinalIndex; - }); - } + // Destroy the extended ordinal index on updated data + if (xAxis && xAxis.options.ordinal) { + addEvent(series, 'updatedData', function () { + delete xAxis.ordinalIndex; + }); + } }); /** @@ -54,629 +54,629 @@ wrap(Series.prototype, 'init', function (proceed) { */ wrap(Axis.prototype, 'getTimeTicks', function (proceed, normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) { - var start = 0, - end, - segmentPositions, - higherRanks = {}, - hasCrossedHigherRank, - info, - posLength, - outsideMax, - groupPositions = [], - lastGroupPosition = -Number.MAX_VALUE, - tickPixelIntervalOption = this.options.tickPixelInterval, - time = this.chart.time; - - // The positions are not always defined, for example for ordinal positions when data - // has regular interval (#1557, #2090) - if ((!this.options.ordinal && !this.options.breaks) || !positions || positions.length < 3 || min === undefined) { - return proceed.call(this, normalizedInterval, min, max, startOfWeek); - } - - // Analyze the positions array to split it into segments on gaps larger than 5 times - // the closest distance. The closest distance is already found at this point, so - // we reuse that instead of computing it again. - posLength = positions.length; - - for (end = 0; end < posLength; end++) { - - outsideMax = end && positions[end - 1] > max; - - if (positions[end] < min) { // Set the last position before min - start = end; - } - - if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) { - - // For each segment, calculate the tick positions from the getTimeTicks utility - // function. The interval will be the same regardless of how long the segment is. - if (positions[end] > lastGroupPosition) { // #1475 - - segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek); - - // Prevent duplicate groups, for example for multiple segments within one larger time frame (#1475) - while (segmentPositions.length && segmentPositions[0] <= lastGroupPosition) { - segmentPositions.shift(); - } - if (segmentPositions.length) { - lastGroupPosition = segmentPositions[segmentPositions.length - 1]; - } - - groupPositions = groupPositions.concat(segmentPositions); - } - // Set start of next segment - start = end + 1; - } - - if (outsideMax) { - break; - } - } - - // Get the grouping info from the last of the segments. The info is the same for - // all segments. - info = segmentPositions.info; - - // Optionally identify ticks with higher rank, for example when the ticks - // have crossed midnight. - if (findHigherRanks && info.unitRange <= timeUnits.hour) { - end = groupPositions.length - 1; - - // Compare points two by two - for (start = 1; start < end; start++) { - if ( - time.dateFormat('%d', groupPositions[start]) !== - time.dateFormat('%d', groupPositions[start - 1]) - ) { - higherRanks[groupPositions[start]] = 'day'; - hasCrossedHigherRank = true; - } - } - - // If the complete array has crossed midnight, we want to mark the first - // positions also as higher rank - if (hasCrossedHigherRank) { - higherRanks[groupPositions[0]] = 'day'; - } - info.higherRanks = higherRanks; - } - - // Save the info - groupPositions.info = info; - - - - // Don't show ticks within a gap in the ordinal axis, where the space between - // two points is greater than a portion of the tick pixel interval - if (findHigherRanks && defined(tickPixelIntervalOption)) { // check for squashed ticks - - var length = groupPositions.length, - i = length, - itemToRemove, - translated, - translatedArr = [], - lastTranslated, - medianDistance, - distance, - distances = []; - - // Find median pixel distance in order to keep a reasonably even distance between - // ticks (#748) - while (i--) { - translated = this.translate(groupPositions[i]); - if (lastTranslated) { - distances[i] = lastTranslated - translated; - } - translatedArr[i] = lastTranslated = translated; - } - distances.sort(); - medianDistance = distances[Math.floor(distances.length / 2)]; - if (medianDistance < tickPixelIntervalOption * 0.6) { - medianDistance = null; - } - - // Now loop over again and remove ticks where needed - i = groupPositions[length - 1] > max ? length - 1 : length; // #817 - lastTranslated = undefined; - while (i--) { - translated = translatedArr[i]; - distance = Math.abs(lastTranslated - translated); - // #4175 - when axis is reversed, the distance, is negative but - // tickPixelIntervalOption positive, so we need to compare the same values - - // Remove ticks that are closer than 0.6 times the pixel interval from the one to the right, - // but not if it is close to the median distance (#748). - if (lastTranslated && distance < tickPixelIntervalOption * 0.8 && - (medianDistance === null || distance < medianDistance * 0.8)) { - - // Is this a higher ranked position with a normal position to the right? - if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) { - - // Yes: remove the lower ranked neighbour to the right - itemToRemove = i + 1; - lastTranslated = translated; // #709 - - } else { - - // No: remove this one - itemToRemove = i; - } - - groupPositions.splice(itemToRemove, 1); - - } else { - lastTranslated = translated; - } - } - } - return groupPositions; + var start = 0, + end, + segmentPositions, + higherRanks = {}, + hasCrossedHigherRank, + info, + posLength, + outsideMax, + groupPositions = [], + lastGroupPosition = -Number.MAX_VALUE, + tickPixelIntervalOption = this.options.tickPixelInterval, + time = this.chart.time; + + // The positions are not always defined, for example for ordinal positions when data + // has regular interval (#1557, #2090) + if ((!this.options.ordinal && !this.options.breaks) || !positions || positions.length < 3 || min === undefined) { + return proceed.call(this, normalizedInterval, min, max, startOfWeek); + } + + // Analyze the positions array to split it into segments on gaps larger than 5 times + // the closest distance. The closest distance is already found at this point, so + // we reuse that instead of computing it again. + posLength = positions.length; + + for (end = 0; end < posLength; end++) { + + outsideMax = end && positions[end - 1] > max; + + if (positions[end] < min) { // Set the last position before min + start = end; + } + + if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) { + + // For each segment, calculate the tick positions from the getTimeTicks utility + // function. The interval will be the same regardless of how long the segment is. + if (positions[end] > lastGroupPosition) { // #1475 + + segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek); + + // Prevent duplicate groups, for example for multiple segments within one larger time frame (#1475) + while (segmentPositions.length && segmentPositions[0] <= lastGroupPosition) { + segmentPositions.shift(); + } + if (segmentPositions.length) { + lastGroupPosition = segmentPositions[segmentPositions.length - 1]; + } + + groupPositions = groupPositions.concat(segmentPositions); + } + // Set start of next segment + start = end + 1; + } + + if (outsideMax) { + break; + } + } + + // Get the grouping info from the last of the segments. The info is the same for + // all segments. + info = segmentPositions.info; + + // Optionally identify ticks with higher rank, for example when the ticks + // have crossed midnight. + if (findHigherRanks && info.unitRange <= timeUnits.hour) { + end = groupPositions.length - 1; + + // Compare points two by two + for (start = 1; start < end; start++) { + if ( + time.dateFormat('%d', groupPositions[start]) !== + time.dateFormat('%d', groupPositions[start - 1]) + ) { + higherRanks[groupPositions[start]] = 'day'; + hasCrossedHigherRank = true; + } + } + + // If the complete array has crossed midnight, we want to mark the first + // positions also as higher rank + if (hasCrossedHigherRank) { + higherRanks[groupPositions[0]] = 'day'; + } + info.higherRanks = higherRanks; + } + + // Save the info + groupPositions.info = info; + + + + // Don't show ticks within a gap in the ordinal axis, where the space between + // two points is greater than a portion of the tick pixel interval + if (findHigherRanks && defined(tickPixelIntervalOption)) { // check for squashed ticks + + var length = groupPositions.length, + i = length, + itemToRemove, + translated, + translatedArr = [], + lastTranslated, + medianDistance, + distance, + distances = []; + + // Find median pixel distance in order to keep a reasonably even distance between + // ticks (#748) + while (i--) { + translated = this.translate(groupPositions[i]); + if (lastTranslated) { + distances[i] = lastTranslated - translated; + } + translatedArr[i] = lastTranslated = translated; + } + distances.sort(); + medianDistance = distances[Math.floor(distances.length / 2)]; + if (medianDistance < tickPixelIntervalOption * 0.6) { + medianDistance = null; + } + + // Now loop over again and remove ticks where needed + i = groupPositions[length - 1] > max ? length - 1 : length; // #817 + lastTranslated = undefined; + while (i--) { + translated = translatedArr[i]; + distance = Math.abs(lastTranslated - translated); + // #4175 - when axis is reversed, the distance, is negative but + // tickPixelIntervalOption positive, so we need to compare the same values + + // Remove ticks that are closer than 0.6 times the pixel interval from the one to the right, + // but not if it is close to the median distance (#748). + if (lastTranslated && distance < tickPixelIntervalOption * 0.8 && + (medianDistance === null || distance < medianDistance * 0.8)) { + + // Is this a higher ranked position with a normal position to the right? + if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) { + + // Yes: remove the lower ranked neighbour to the right + itemToRemove = i + 1; + lastTranslated = translated; // #709 + + } else { + + // No: remove this one + itemToRemove = i; + } + + groupPositions.splice(itemToRemove, 1); + + } else { + lastTranslated = translated; + } + } + } + return groupPositions; }); // Extend the Axis prototype extend(Axis.prototype, /** @lends Axis.prototype */ { - /** - * Calculate the ordinal positions before tick positions are calculated. - */ - beforeSetTickPositions: function () { - var axis = this, - len, - ordinalPositions = [], - useOrdinal = false, - dist, - extremes = axis.getExtremes(), - min = extremes.min, - max = extremes.max, - minIndex, - maxIndex, - slope, - hasBreaks = axis.isXAxis && !!axis.options.breaks, - isOrdinal = axis.options.ordinal, - overscrollPointsRange = Number.MAX_VALUE, - ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries, - isNavigatorAxis = axis.options.className === 'highcharts-navigator-xaxis', - i; - - 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) - ) { - - // 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); - } - } - } - } - - }); - - // cache the length - len = ordinalPositions.length; - - // Check if we really need the overhead of mapping axis data against the ordinal positions. - // If the series consist of evenly spaced data any way, we don't need any ordinal logic. - if (len > 2) { // two points have equal distance by default - dist = ordinalPositions[1] - ordinalPositions[0]; - i = len - 1; - while (i-- && !useOrdinal) { - if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) { - useOrdinal = true; - } - } - - // 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 - ) - ) { - 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. - minIndex = axis.ordinal2lin( // #5979 - Math.max( - min, - ordinalPositions[0] - ), - true - ); - maxIndex = Math.max(axis.ordinal2lin( - Math.min( - max, - ordinalPositions[ordinalPositions.length - 1] - ), - true - ), 1); // #3339 - - // 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 - }, - /** - * Translate from a linear axis value to the corresponding ordinal axis position. If there - * are no gaps in the ordinal axis this will be the same. The translated value is the value - * that the point would have if the axis were linear, using the same min and max. - * - * @param Number val The axis value - * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value - */ - val2lin: function (val, toIndex) { - var axis = this, - ordinalPositions = axis.ordinalPositions, - ret; - - if (!ordinalPositions) { - ret = val; - - } else { - - var ordinalLength = ordinalPositions.length, - i, - distance, - ordinalIndex; - - // first look for an exact match in the ordinalpositions array - i = ordinalLength; - while (i--) { - if (ordinalPositions[i] === val) { - ordinalIndex = i; - break; - } - } - - // if that failed, find the intermediate position between the two nearest values - i = ordinalLength - 1; - while (i--) { - if (val > ordinalPositions[i] || i === 0) { // interpolate - distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1 - ordinalIndex = i + distance; - break; - } - } - ret = toIndex ? - ordinalIndex : - axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset; - } - return ret; - }, - /** - * Translate from linear (internal) to axis value - * - * @param Number val The linear abstracted value - * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value - */ - lin2val: function (val, fromIndex) { - var axis = this, - ordinalPositions = axis.ordinalPositions, - ret; - - if (!ordinalPositions) { // the visible range contains only equally spaced values - ret = val; - - } else { - - var ordinalSlope = axis.ordinalSlope, - ordinalOffset = axis.ordinalOffset, - i = ordinalPositions.length - 1, - linearEquivalentLeft, - linearEquivalentRight, - distance; - - - // Handle the case where we translate from the index directly, used only - // when panning an ordinal axis - if (fromIndex) { - - if (val < 0) { // out of range, in effect panning to the left - val = ordinalPositions[0]; - } else if (val > i) { // out of range, panning to the right - val = ordinalPositions[i]; - } else { // split it up - i = Math.floor(val); - distance = val - i; // the decimal - } - - // Loop down along the ordinal positions. When the linear equivalent of i matches - // an ordinal position, interpolate between the left and right values. - } else { - while (i--) { - linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset; - if (val >= linearEquivalentLeft) { - linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset; - distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1 - break; - } - } - } - - // If the index is within the range of the ordinal positions, return the associated - // or interpolated value. If not, just return the value - return distance !== undefined && ordinalPositions[i] !== undefined ? - ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) : - val; - } - return ret; - }, - /** - * Get the ordinal positions for the entire data set. This is necessary in chart panning - * because we need to find out what points or data groups are available outside the - * visible range. When a panning operation starts, if an index for the given grouping - * does not exists, it is created and cached. This index is deleted on updated data, so - * it will be regenerated the next time a panning operation starts. - */ - getExtendedPositions: function () { - 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, - // create it. - if (!ordinalIndex) { - ordinalIndex = axis.ordinalIndex = {}; - } - - - if (!ordinalIndex[key]) { - - // Create a fake axis object where the extended ordinal positions are emulated - fakeAxis = { - series: [], - chart: chart, - getExtremes: function () { - return { - min: extremes.dataMin, - max: extremes.dataMax + overscroll - }; - }, - options: { - ordinal: true - }, - val2lin: Axis.prototype.val2lin, // #2590 - ordinal2lin: Axis.prototype.ordinal2lin // #6276 - }; - - // 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.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 - units: [[grouping.unitName, [grouping.count]]] - } : { - enabled: false - } - }; - series.processData.apply(fakeSeries); - - - fakeAxis.series.push(fakeSeries); - }); - - // Run beforeSetTickPositions to compute the ordinalPositions - axis.beforeSetTickPositions.apply(fakeAxis); - - // Cache it - ordinalIndex[key] = fakeAxis.ordinalPositions; - } - 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 - * 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 - * example. Without this logic, it would show the correct data grouping when viewing - * a range within each day, but once moving the range to include the gap between two - * days, the interval would include the cut-away night hours and the data grouping - * would be wrong. So the below method tries to compensate by identifying the most - * common point interval, in this case days. - * - * An opposite case is presented in issue #718. We have a long array of daily data, - * then one point is appended one hour after the last point. We expect the data grouping - * not to change. - * - * In the future, if we find cases where this estimation doesn't work optimally, we - * might need to add a second pass to the data grouping logic, where we do another run - * with a greater interval if the number of data groups is more than a certain fraction - * of the desired group count. - */ - getGroupIntervalFactor: function (xMin, xMax, series) { - var i, - processedXData = series.processedXData, - len = processedXData.length, - distances = [], - median, - groupIntervalFactor = this.groupIntervalFactor; - - // Only do this computation for the first series, let the other inherit it (#2416) - if (!groupIntervalFactor) { - - // Register all the distances in an array - for (i = 0; i < len - 1; i++) { - distances[i] = processedXData[i + 1] - processedXData[i]; - } - - // Sort them and find the median - distances.sort(function (a, b) { - return a - b; - }); - median = distances[Math.floor(len / 2)]; - - // Compensate for series that don't extend through the entire axis extent. #1675. - xMin = Math.max(xMin, processedXData[0]); - xMax = Math.min(xMax, processedXData[len - 1]); - - this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin); - } - - // Return the factor needed for data grouping - return groupIntervalFactor; - }, - - /** - * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster - */ - postProcessTickInterval: function (tickInterval) { - // Problem: http://jsfiddle.net/highcharts/FQm4E/1/ - // This is a case where this algorithm doesn't work optimally. In this case, the - // tick labels are spread out per week, but all the gaps reside within weeks. So - // we have a situation where the labels are courser than the ordinal gaps, and - // thus the tick interval should not be altered - var ordinalSlope = this.ordinalSlope, - ret; - - - if (ordinalSlope) { - if (!this.options.breaks) { - ret = tickInterval / (ordinalSlope / this.closestPointRange); - } else { - ret = this.closestPointRange || tickInterval; // #7275 - } - } else { - ret = tickInterval; - } - return ret; - } + /** + * Calculate the ordinal positions before tick positions are calculated. + */ + beforeSetTickPositions: function () { + var axis = this, + len, + ordinalPositions = [], + useOrdinal = false, + dist, + extremes = axis.getExtremes(), + min = extremes.min, + max = extremes.max, + minIndex, + maxIndex, + slope, + hasBreaks = axis.isXAxis && !!axis.options.breaks, + isOrdinal = axis.options.ordinal, + overscrollPointsRange = Number.MAX_VALUE, + ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries, + isNavigatorAxis = axis.options.className === 'highcharts-navigator-xaxis', + i; + + 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) + ) { + + // 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); + } + } + } + } + + }); + + // cache the length + len = ordinalPositions.length; + + // Check if we really need the overhead of mapping axis data against the ordinal positions. + // If the series consist of evenly spaced data any way, we don't need any ordinal logic. + if (len > 2) { // two points have equal distance by default + dist = ordinalPositions[1] - ordinalPositions[0]; + i = len - 1; + while (i-- && !useOrdinal) { + if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) { + useOrdinal = true; + } + } + + // 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 + ) + ) { + 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. + minIndex = axis.ordinal2lin( // #5979 + Math.max( + min, + ordinalPositions[0] + ), + true + ); + maxIndex = Math.max(axis.ordinal2lin( + Math.min( + max, + ordinalPositions[ordinalPositions.length - 1] + ), + true + ), 1); // #3339 + + // 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 + }, + /** + * Translate from a linear axis value to the corresponding ordinal axis position. If there + * are no gaps in the ordinal axis this will be the same. The translated value is the value + * that the point would have if the axis were linear, using the same min and max. + * + * @param Number val The axis value + * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value + */ + val2lin: function (val, toIndex) { + var axis = this, + ordinalPositions = axis.ordinalPositions, + ret; + + if (!ordinalPositions) { + ret = val; + + } else { + + var ordinalLength = ordinalPositions.length, + i, + distance, + ordinalIndex; + + // first look for an exact match in the ordinalpositions array + i = ordinalLength; + while (i--) { + if (ordinalPositions[i] === val) { + ordinalIndex = i; + break; + } + } + + // if that failed, find the intermediate position between the two nearest values + i = ordinalLength - 1; + while (i--) { + if (val > ordinalPositions[i] || i === 0) { // interpolate + distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1 + ordinalIndex = i + distance; + break; + } + } + ret = toIndex ? + ordinalIndex : + axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset; + } + return ret; + }, + /** + * Translate from linear (internal) to axis value + * + * @param Number val The linear abstracted value + * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value + */ + lin2val: function (val, fromIndex) { + var axis = this, + ordinalPositions = axis.ordinalPositions, + ret; + + if (!ordinalPositions) { // the visible range contains only equally spaced values + ret = val; + + } else { + + var ordinalSlope = axis.ordinalSlope, + ordinalOffset = axis.ordinalOffset, + i = ordinalPositions.length - 1, + linearEquivalentLeft, + linearEquivalentRight, + distance; + + + // Handle the case where we translate from the index directly, used only + // when panning an ordinal axis + if (fromIndex) { + + if (val < 0) { // out of range, in effect panning to the left + val = ordinalPositions[0]; + } else if (val > i) { // out of range, panning to the right + val = ordinalPositions[i]; + } else { // split it up + i = Math.floor(val); + distance = val - i; // the decimal + } + + // Loop down along the ordinal positions. When the linear equivalent of i matches + // an ordinal position, interpolate between the left and right values. + } else { + while (i--) { + linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset; + if (val >= linearEquivalentLeft) { + linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset; + distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1 + break; + } + } + } + + // If the index is within the range of the ordinal positions, return the associated + // or interpolated value. If not, just return the value + return distance !== undefined && ordinalPositions[i] !== undefined ? + ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) : + val; + } + return ret; + }, + /** + * Get the ordinal positions for the entire data set. This is necessary in chart panning + * because we need to find out what points or data groups are available outside the + * visible range. When a panning operation starts, if an index for the given grouping + * does not exists, it is created and cached. This index is deleted on updated data, so + * it will be regenerated the next time a panning operation starts. + */ + getExtendedPositions: function () { + 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, + // create it. + if (!ordinalIndex) { + ordinalIndex = axis.ordinalIndex = {}; + } + + + if (!ordinalIndex[key]) { + + // Create a fake axis object where the extended ordinal positions are emulated + fakeAxis = { + series: [], + chart: chart, + getExtremes: function () { + return { + min: extremes.dataMin, + max: extremes.dataMax + overscroll + }; + }, + options: { + ordinal: true + }, + val2lin: Axis.prototype.val2lin, // #2590 + ordinal2lin: Axis.prototype.ordinal2lin // #6276 + }; + + // 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.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 + units: [[grouping.unitName, [grouping.count]]] + } : { + enabled: false + } + }; + series.processData.apply(fakeSeries); + + + fakeAxis.series.push(fakeSeries); + }); + + // Run beforeSetTickPositions to compute the ordinalPositions + axis.beforeSetTickPositions.apply(fakeAxis); + + // Cache it + ordinalIndex[key] = fakeAxis.ordinalPositions; + } + 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 + * 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 + * example. Without this logic, it would show the correct data grouping when viewing + * a range within each day, but once moving the range to include the gap between two + * days, the interval would include the cut-away night hours and the data grouping + * would be wrong. So the below method tries to compensate by identifying the most + * common point interval, in this case days. + * + * An opposite case is presented in issue #718. We have a long array of daily data, + * then one point is appended one hour after the last point. We expect the data grouping + * not to change. + * + * In the future, if we find cases where this estimation doesn't work optimally, we + * might need to add a second pass to the data grouping logic, where we do another run + * with a greater interval if the number of data groups is more than a certain fraction + * of the desired group count. + */ + getGroupIntervalFactor: function (xMin, xMax, series) { + var i, + processedXData = series.processedXData, + len = processedXData.length, + distances = [], + median, + groupIntervalFactor = this.groupIntervalFactor; + + // Only do this computation for the first series, let the other inherit it (#2416) + if (!groupIntervalFactor) { + + // Register all the distances in an array + for (i = 0; i < len - 1; i++) { + distances[i] = processedXData[i + 1] - processedXData[i]; + } + + // Sort them and find the median + distances.sort(function (a, b) { + return a - b; + }); + median = distances[Math.floor(len / 2)]; + + // Compensate for series that don't extend through the entire axis extent. #1675. + xMin = Math.max(xMin, processedXData[0]); + xMax = Math.min(xMax, processedXData[len - 1]); + + this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin); + } + + // Return the factor needed for data grouping + return groupIntervalFactor; + }, + + /** + * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster + */ + postProcessTickInterval: function (tickInterval) { + // Problem: http://jsfiddle.net/highcharts/FQm4E/1/ + // This is a case where this algorithm doesn't work optimally. In this case, the + // tick labels are spread out per week, but all the gaps reside within weeks. So + // we have a situation where the labels are courser than the ordinal gaps, and + // thus the tick interval should not be altered + var ordinalSlope = this.ordinalSlope, + ret; + + + if (ordinalSlope) { + if (!this.options.breaks) { + ret = tickInterval / (ordinalSlope / this.closestPointRange); + } else { + ret = this.closestPointRange || tickInterval; // #7275 + } + } else { + ret = tickInterval; + } + return ret; + } }); // Record this to prevent overwriting by broken-axis module (#5979) @@ -684,99 +684,99 @@ Axis.prototype.ordinal2lin = Axis.prototype.val2lin; // 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) { - - var mouseDownX = chart.mouseDownX, - extremes = xAxis.getExtremes(), - dataMax = extremes.dataMax, - min = extremes.min, - max = extremes.max, - trimmedRange, - hoverPoints = chart.hoverPoints, - 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 - ordinalPositions, - searchAxisLeft, - lin2val = xAxis.lin2val, - val2lin = xAxis.val2lin, - searchAxisRight; - - if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced - runBase = true; - - } else if (Math.abs(movedUnits) > 1) { - - // Remove active points for shared tooltip - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - if (movedUnits < 0) { - searchAxisLeft = extendedAxis; - searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis; - } else { - searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis; - searchAxisRight = extendedAxis; - } - - // In grouped data series, the last ordinal position represents the grouped data, which is - // to the left of the real data max. If we don't compensate for this, we will be allowed - // to pan grouped data series passed the right of the plot area. - ordinalPositions = searchAxisRight.ordinalPositions; - if (dataMax > ordinalPositions[ordinalPositions.length - 1]) { - ordinalPositions.push(dataMax); - } - - // Get the new min and max values by getting the ordinal index for the current extreme, - // then add the moved units and translate back to values. This happens on the - // extended ordinal positions if the new position is out of range, else it happens - // on the current x axis which is smaller and faster. - chart.fixedRange = max - min; - trimmedRange = xAxis.toFixedRange(null, null, - lin2val.apply(searchAxisLeft, [ - val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index - true // translate from index - ]), - lin2val.apply(searchAxisRight, [ - val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index - 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) + overscroll - ) { - xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' }); - } - - chart.mouseDownX = chartX; // set new reference for next run - css(chart.container, { cursor: 'move' }); - } - - } else { - 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)); - } + var chart = this, + xAxis = chart.xAxis[0], + overscroll = xAxis.options.overscroll, + chartX = e.chartX, + runBase = false; + + if (xAxis.options.ordinal && xAxis.series.length) { + + var mouseDownX = chart.mouseDownX, + extremes = xAxis.getExtremes(), + dataMax = extremes.dataMax, + min = extremes.min, + max = extremes.max, + trimmedRange, + hoverPoints = chart.hoverPoints, + 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 + ordinalPositions, + searchAxisLeft, + lin2val = xAxis.lin2val, + val2lin = xAxis.val2lin, + searchAxisRight; + + if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced + runBase = true; + + } else if (Math.abs(movedUnits) > 1) { + + // Remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + if (movedUnits < 0) { + searchAxisLeft = extendedAxis; + searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis; + } else { + searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis; + searchAxisRight = extendedAxis; + } + + // In grouped data series, the last ordinal position represents the grouped data, which is + // to the left of the real data max. If we don't compensate for this, we will be allowed + // to pan grouped data series passed the right of the plot area. + ordinalPositions = searchAxisRight.ordinalPositions; + if (dataMax > ordinalPositions[ordinalPositions.length - 1]) { + ordinalPositions.push(dataMax); + } + + // Get the new min and max values by getting the ordinal index for the current extreme, + // then add the moved units and translate back to values. This happens on the + // extended ordinal positions if the new position is out of range, else it happens + // on the current x axis which is smaller and faster. + chart.fixedRange = max - min; + trimmedRange = xAxis.toFixedRange(null, null, + lin2val.apply(searchAxisLeft, [ + val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index + true // translate from index + ]), + lin2val.apply(searchAxisRight, [ + val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index + 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) + overscroll + ) { + xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' }); + } + + chart.mouseDownX = chartX; // set new reference for next run + css(chart.container, { cursor: 'move' }); + } + + } else { + 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)); + } }); /* **************************************************************************** diff --git a/js/parts/PieSeries.js b/js/parts/PieSeries.js index 8d53cb01601..5b605bf6db6 100644 --- a/js/parts/PieSeries.js +++ b/js/parts/PieSeries.js @@ -14,20 +14,20 @@ import './Options.js'; import './Point.js'; import './Series.js'; var addEvent = H.addEvent, - CenteredSeriesMixin = H.CenteredSeriesMixin, - defined = H.defined, - each = H.each, - extend = H.extend, - getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians, - inArray = H.inArray, - LegendSymbolMixin = H.LegendSymbolMixin, - noop = H.noop, - pick = H.pick, - Point = H.Point, - Series = H.Series, - seriesType = H.seriesType, - seriesTypes = H.seriesTypes, - setAnimation = H.setAnimation; + CenteredSeriesMixin = H.CenteredSeriesMixin, + defined = H.defined, + each = H.each, + extend = H.extend, + getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians, + inArray = H.inArray, + LegendSymbolMixin = H.LegendSymbolMixin, + noop = H.noop, + pick = H.pick, + Point = H.Point, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes, + setAnimation = H.setAnimation; /** * The pie series type. @@ -41,7 +41,7 @@ var addEvent = H.addEvent, * numerical proportion. * * @sample highcharts/demo/pie-basic/ Pie chart - * + * * @extends {plotOptions.line} * @excluding animationLimit,boostThreshold,connectEnds,connectNulls, * cropThreshold,dashStyle,findNearestPointBy,getExtremesFromAll, @@ -53,783 +53,783 @@ var addEvent = H.addEvent, */ seriesType('pie', 'line', { - /** - * The center of the pie chart relative to the plot area. Can be percentages - * or pixel values. The default behaviour (as of 3.0) is to center - * the pie so that all slices and data labels are within the plot area. - * As a consequence, the pie may actually jump around in a chart with - * dynamic values, as the data labels move. In that case, the center - * should be explicitly set, for example to `["50%", "50%"]`. - * - * @type {Array} - * @sample {highcharts} highcharts/plotoptions/pie-center/ Centered at 100, 100 - * @default [null, null] - * @product highcharts - */ - center: [null, null], - - clip: false, - - /** - * @ignore - */ - colorByPoint: true, // always true for pies - - /** - * A series specific or series type specific color set to use instead - * of the global [colors](#colors). - * - * @type {Array} - * @sample {highcharts} highcharts/demo/pie-monochrome/ Set default colors for all pies - * @since 3.0 - * @product highcharts - * @apioption plotOptions.pie.colors - */ - - /** - * @extends plotOptions.series.dataLabels - * @excluding align,allowOverlap,staggerLines,step - * @product highcharts - */ - dataLabels: { - /** - * The color of the line connecting the data label to the pie slice. - * The default color is the same as the point's color. - * - * In styled mode, the connector stroke is given in the - * `.highcharts-data-label-connector` class. - * - * @type {String} - * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorcolor/ Blue connectors - * @sample {highcharts} highcharts/css/pie-point/ Styled connectors - * @default {point.color} - * @since 2.1 - * @product highcharts - * @apioption plotOptions.pie.dataLabels.connectorColor - */ - - /** - * The distance from the data label to the connector. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorpadding/ No padding - * @default 5 - * @since 2.1 - * @product highcharts - * @apioption plotOptions.pie.dataLabels.connectorPadding - */ - - /** - * The width of the line connecting the data label to the pie slice. - * - * - * In styled mode, the connector stroke width is given in the - * `.highcharts-data-label-connector` class. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorwidth-disabled/ Disable the connector - * @sample {highcharts} highcharts/css/pie-point/ Styled connectors - * @default 1 - * @since 2.1 - * @product highcharts - * @apioption plotOptions.pie.dataLabels.connectorWidth - */ - - /** - * - * @sample {highcharts} - * highcharts/plotOptions/pie-datalabels-overflow - * Long labels truncated with an ellipsis - * @sample {highcharts} - * highcharts/plotOptions/pie-datalabels-overflow-wrap - * Long labels are wrapped - * @apioption plotOptions.pie.dataLabels.style - */ - - /** - * The distance of the data label from the pie's edge. Negative numbers - * put the data label on top of the pie slices. Connectors are only - * shown for data labels outside the pie. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/pie-datalabels-distance/ Data labels on top of the pie - * @default 30 - * @since 2.1 - * @product highcharts - */ - distance: 30, - - /** - * Enable or disable the data labels. - * - * @type {Boolean} - * @since 2.1 - * @product highcharts - */ - enabled: true, - - formatter: function () { // #2945 - return this.point.isNull ? undefined : this.point.name; - }, - - /** - * Whether to render the connector as a soft arc or a line with sharp - * break. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-true/ Soft - * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-false/ Non soft - * @since 2.1.7 - * @product highcharts - * @apioption plotOptions.pie.dataLabels.softConnector - */ - - x: 0 - }, - - /** - * The end angle of the pie in degrees where 0 is top and 90 is right. - * Defaults to `startAngle` plus 360. - * - * @type {Number} - * @sample {highcharts} highcharts/demo/pie-semi-circle/ Semi-circle donut - * @default null - * @since 1.3.6 - * @product highcharts - * @apioption plotOptions.pie.endAngle - */ - - /** - * Equivalent to [chart.ignoreHiddenSeries](#chart.ignoreHiddenSeries), - * this option tells whether the series shall be redrawn as if the - * hidden point were `null`. - * - * The default value changed from `false` to `true` with Highcharts - * 3.0. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/pie-ignorehiddenpoint/ True, the hiddden point is ignored - * @default true - * @since 2.3.0 - * @product highcharts - */ - ignoreHiddenPoint: true, - - /** - * The size of the inner diameter for the pie. A size greater than 0 - * renders a donut chart. Can be a percentage or pixel value. Percentages - * are relative to the pie size. Pixel values are given as integers. - * - * - * Note: in Highcharts < 4.1.2, the percentage was relative to the plot - * area, not the pie size. - * - * @type {String|Number} - * @sample {highcharts} highcharts/plotoptions/pie-innersize-80px/ 80px inner size - * @sample {highcharts} highcharts/plotoptions/pie-innersize-50percent/ 50% of the plot area - * @sample {highcharts} highcharts/demo/3d-pie-donut/ 3D donut - * @default 0 - * @since 2.0 - * @product highcharts - * @apioption plotOptions.pie.innerSize - */ - - /** - * @ignore - */ - legendType: 'point', - - /** @ignore */ - marker: null, // point options are specified in the base options - - /** - * The minimum size for a pie in response to auto margins. The pie will - * try to shrink to make room for data labels in side the plot area, - * but only to this size. - * - * @type {Number} - * @default 80 - * @since 3.0 - * @product highcharts - * @apioption plotOptions.pie.minSize - */ - - /** - * The diameter of the pie relative to the plot area. Can be a percentage - * or pixel value. Pixel values are given as integers. The default - * behaviour (as of 3.0) is to scale to the plot area and give room - * for data labels within the plot area. - * [slicedOffset](#plotOptions.pie.slicedOffset) is also included - * in the default size calculation. As a consequence, the size - * of the pie may vary when points are updated and data labels more - * around. In that case it is best to set a fixed value, for example - * `"75%"`. - * - * @type {String|Number} - * @sample {highcharts} highcharts/plotoptions/pie-size/ - * Smaller pie - * @product highcharts - */ - size: null, - - /** - * Whether to display this particular series or series type in the - * legend. Since 2.1, pies are not shown in the legend by default. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-showinlegend/ One series in the legend, one hidden - * @product highcharts - */ - showInLegend: false, - - /** - * If a point is sliced, moved out from the center, how many pixels - * should it be moved?. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/pie-slicedoffset-20/ 20px offset - * @default 10 - * @product highcharts - */ - slicedOffset: 10, - - /** - * The start angle of the pie slices in degrees where 0 is top and 90 - * right. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/pie-startangle-90/ Start from right - * @default 0 - * @since 2.3.4 - * @product highcharts - * @apioption plotOptions.pie.startAngle - */ - - /** - * Sticky tracking of mouse events. When true, the `mouseOut` event - * on a series isn't triggered until the mouse moves over another series, - * or out of the plot area. When false, the `mouseOut` event on a - * series is triggered when the mouse leaves the area around the series' - * graph or markers. This also implies the tooltip. When `stickyTracking` - * is false and `tooltip.shared` is false, the tooltip will be hidden - * when moving the mouse between series. - * - * @product highcharts - */ - stickyTracking: false, - - tooltip: { - followPointer: true - }, - /*= if (build.classic) { =*/ - - /** - * The color of the border surrounding each slice. When `null`, the - * border takes the same color as the slice fill. This can be used - * together with a `borderWidth` to fill drawing gaps created by antialiazing - * artefacts in borderless pies. - * - * In styled mode, the border stroke is given in the `.highcharts-point` class. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/pie-bordercolor-black/ Black border - * @default #ffffff - * @product highcharts - */ - borderColor: '${palette.backgroundColor}', - - /** - * The width of the border surrounding each slice. - * - * When setting the border width to 0, there may be small gaps between - * the slices due to SVG antialiasing artefacts. To work around this, - * keep the border width at 0.5 or 1, but set the `borderColor` to - * `null` instead. - * - * In styled mode, the border stroke width is given in the `.highcharts-point` class. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/pie-borderwidth/ 3px border - * @default 1 - * @product highcharts - */ - borderWidth: 1, - - states: { - - /** - * @extends plotOptions.series.states.hover - * @excluding marker,lineWidth,lineWidthPlus - * @product highcharts - */ - hover: { - - /** - * How much to brighten the point on interaction. Requires the main - * color to be defined in hex or rgb(a) format. - * - * In styled mode, the hover brightness is by default replaced - * by a fill-opacity given in the `.highcharts-point-hover` class. - * - * @sample {highcharts} - * highcharts/plotoptions/pie-states-hover-brightness/ - * Brightened by 0.5 - * @product highcharts - */ - brightness: 0.1 - } - } - /*= } =*/ + /** + * The center of the pie chart relative to the plot area. Can be percentages + * or pixel values. The default behaviour (as of 3.0) is to center + * the pie so that all slices and data labels are within the plot area. + * As a consequence, the pie may actually jump around in a chart with + * dynamic values, as the data labels move. In that case, the center + * should be explicitly set, for example to `["50%", "50%"]`. + * + * @type {Array} + * @sample {highcharts} highcharts/plotoptions/pie-center/ Centered at 100, 100 + * @default [null, null] + * @product highcharts + */ + center: [null, null], + + clip: false, + + /** + * @ignore + */ + colorByPoint: true, // always true for pies + + /** + * A series specific or series type specific color set to use instead + * of the global [colors](#colors). + * + * @type {Array} + * @sample {highcharts} highcharts/demo/pie-monochrome/ Set default colors for all pies + * @since 3.0 + * @product highcharts + * @apioption plotOptions.pie.colors + */ + + /** + * @extends plotOptions.series.dataLabels + * @excluding align,allowOverlap,staggerLines,step + * @product highcharts + */ + dataLabels: { + /** + * The color of the line connecting the data label to the pie slice. + * The default color is the same as the point's color. + * + * In styled mode, the connector stroke is given in the + * `.highcharts-data-label-connector` class. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorcolor/ Blue connectors + * @sample {highcharts} highcharts/css/pie-point/ Styled connectors + * @default {point.color} + * @since 2.1 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.connectorColor + */ + + /** + * The distance from the data label to the connector. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorpadding/ No padding + * @default 5 + * @since 2.1 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.connectorPadding + */ + + /** + * The width of the line connecting the data label to the pie slice. + * + * + * In styled mode, the connector stroke width is given in the + * `.highcharts-data-label-connector` class. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorwidth-disabled/ Disable the connector + * @sample {highcharts} highcharts/css/pie-point/ Styled connectors + * @default 1 + * @since 2.1 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.connectorWidth + */ + + /** + * + * @sample {highcharts} + * highcharts/plotOptions/pie-datalabels-overflow + * Long labels truncated with an ellipsis + * @sample {highcharts} + * highcharts/plotOptions/pie-datalabels-overflow-wrap + * Long labels are wrapped + * @apioption plotOptions.pie.dataLabels.style + */ + + /** + * The distance of the data label from the pie's edge. Negative numbers + * put the data label on top of the pie slices. Connectors are only + * shown for data labels outside the pie. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-distance/ Data labels on top of the pie + * @default 30 + * @since 2.1 + * @product highcharts + */ + distance: 30, + + /** + * Enable or disable the data labels. + * + * @type {Boolean} + * @since 2.1 + * @product highcharts + */ + enabled: true, + + formatter: function () { // #2945 + return this.point.isNull ? undefined : this.point.name; + }, + + /** + * Whether to render the connector as a soft arc or a line with sharp + * break. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-true/ Soft + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-false/ Non soft + * @since 2.1.7 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.softConnector + */ + + x: 0 + }, + + /** + * The end angle of the pie in degrees where 0 is top and 90 is right. + * Defaults to `startAngle` plus 360. + * + * @type {Number} + * @sample {highcharts} highcharts/demo/pie-semi-circle/ Semi-circle donut + * @default null + * @since 1.3.6 + * @product highcharts + * @apioption plotOptions.pie.endAngle + */ + + /** + * Equivalent to [chart.ignoreHiddenSeries](#chart.ignoreHiddenSeries), + * this option tells whether the series shall be redrawn as if the + * hidden point were `null`. + * + * The default value changed from `false` to `true` with Highcharts + * 3.0. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/pie-ignorehiddenpoint/ True, the hiddden point is ignored + * @default true + * @since 2.3.0 + * @product highcharts + */ + ignoreHiddenPoint: true, + + /** + * The size of the inner diameter for the pie. A size greater than 0 + * renders a donut chart. Can be a percentage or pixel value. Percentages + * are relative to the pie size. Pixel values are given as integers. + * + * + * Note: in Highcharts < 4.1.2, the percentage was relative to the plot + * area, not the pie size. + * + * @type {String|Number} + * @sample {highcharts} highcharts/plotoptions/pie-innersize-80px/ 80px inner size + * @sample {highcharts} highcharts/plotoptions/pie-innersize-50percent/ 50% of the plot area + * @sample {highcharts} highcharts/demo/3d-pie-donut/ 3D donut + * @default 0 + * @since 2.0 + * @product highcharts + * @apioption plotOptions.pie.innerSize + */ + + /** + * @ignore + */ + legendType: 'point', + + /** @ignore */ + marker: null, // point options are specified in the base options + + /** + * The minimum size for a pie in response to auto margins. The pie will + * try to shrink to make room for data labels in side the plot area, + * but only to this size. + * + * @type {Number} + * @default 80 + * @since 3.0 + * @product highcharts + * @apioption plotOptions.pie.minSize + */ + + /** + * The diameter of the pie relative to the plot area. Can be a percentage + * or pixel value. Pixel values are given as integers. The default + * behaviour (as of 3.0) is to scale to the plot area and give room + * for data labels within the plot area. + * [slicedOffset](#plotOptions.pie.slicedOffset) is also included + * in the default size calculation. As a consequence, the size + * of the pie may vary when points are updated and data labels more + * around. In that case it is best to set a fixed value, for example + * `"75%"`. + * + * @type {String|Number} + * @sample {highcharts} highcharts/plotoptions/pie-size/ + * Smaller pie + * @product highcharts + */ + size: null, + + /** + * Whether to display this particular series or series type in the + * legend. Since 2.1, pies are not shown in the legend by default. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-showinlegend/ One series in the legend, one hidden + * @product highcharts + */ + showInLegend: false, + + /** + * If a point is sliced, moved out from the center, how many pixels + * should it be moved?. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-slicedoffset-20/ 20px offset + * @default 10 + * @product highcharts + */ + slicedOffset: 10, + + /** + * The start angle of the pie slices in degrees where 0 is top and 90 + * right. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-startangle-90/ Start from right + * @default 0 + * @since 2.3.4 + * @product highcharts + * @apioption plotOptions.pie.startAngle + */ + + /** + * Sticky tracking of mouse events. When true, the `mouseOut` event + * on a series isn't triggered until the mouse moves over another series, + * or out of the plot area. When false, the `mouseOut` event on a + * series is triggered when the mouse leaves the area around the series' + * graph or markers. This also implies the tooltip. When `stickyTracking` + * is false and `tooltip.shared` is false, the tooltip will be hidden + * when moving the mouse between series. + * + * @product highcharts + */ + stickyTracking: false, + + tooltip: { + followPointer: true + }, + /*= if (build.classic) { =*/ + + /** + * The color of the border surrounding each slice. When `null`, the + * border takes the same color as the slice fill. This can be used + * together with a `borderWidth` to fill drawing gaps created by antialiazing + * artefacts in borderless pies. + * + * In styled mode, the border stroke is given in the `.highcharts-point` class. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/pie-bordercolor-black/ Black border + * @default #ffffff + * @product highcharts + */ + borderColor: '${palette.backgroundColor}', + + /** + * The width of the border surrounding each slice. + * + * When setting the border width to 0, there may be small gaps between + * the slices due to SVG antialiasing artefacts. To work around this, + * keep the border width at 0.5 or 1, but set the `borderColor` to + * `null` instead. + * + * In styled mode, the border stroke width is given in the `.highcharts-point` class. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-borderwidth/ 3px border + * @default 1 + * @product highcharts + */ + borderWidth: 1, + + states: { + + /** + * @extends plotOptions.series.states.hover + * @excluding marker,lineWidth,lineWidthPlus + * @product highcharts + */ + hover: { + + /** + * How much to brighten the point on interaction. Requires the main + * color to be defined in hex or rgb(a) format. + * + * In styled mode, the hover brightness is by default replaced + * by a fill-opacity given in the `.highcharts-point-hover` class. + * + * @sample {highcharts} + * highcharts/plotoptions/pie-states-hover-brightness/ + * Brightened by 0.5 + * @product highcharts + */ + brightness: 0.1 + } + } + /*= } =*/ }, /** @lends seriesTypes.pie.prototype */ { - isCartesian: false, - requireSorting: false, - directTouch: true, - noSharedTooltip: true, - trackerGroups: ['group', 'dataLabelsGroup'], - axisTypes: [], - pointAttribs: seriesTypes.column.prototype.pointAttribs, - /** - * Animate the pies in - */ - animate: function (init) { - var series = this, - points = series.points, - startAngleRad = series.startAngleRad; - - if (!init) { - each(points, function (point) { - var graphic = point.graphic, - args = point.shapeArgs; - - if (graphic) { - // start values - graphic.attr({ - r: point.startR || (series.center[3] / 2), // animate from inner radius (#779) - start: startAngleRad, - end: startAngleRad - }); - - // animate - graphic.animate({ - r: args.r, - start: args.start, - end: args.end - }, series.options.animation); - } - }); - - // delete this function to allow it only once - series.animate = null; - } - }, - - /** - * Recompute total chart sum and update percentages of points. - */ - updateTotals: function () { - var i, - total = 0, - points = this.points, - len = points.length, - point, - ignoreHiddenPoint = this.options.ignoreHiddenPoint; - - // Get the total sum - for (i = 0; i < len; i++) { - point = points[i]; - total += (ignoreHiddenPoint && !point.visible) ? - 0 : - point.isNull ? 0 : point.y; - } - this.total = total; - - // Set each point's properties - for (i = 0; i < len; i++) { - point = points[i]; - point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; - point.total = total; - } - }, - - /** - * Extend the generatePoints method by adding total and percentage properties to each point - */ - generatePoints: function () { - Series.prototype.generatePoints.call(this); - this.updateTotals(); - }, - - /** - * Do translation for pie slices - */ - translate: function (positions) { - this.generatePoints(); - - var series = this, - cumulative = 0, - precision = 1000, // issue #172 - options = series.options, - slicedOffset = options.slicedOffset, - connectorOffset = slicedOffset + (options.borderWidth || 0), - finalConnectorOffset, - start, - end, - angle, - radians = getStartAndEndRadians(options.startAngle, options.endAngle), - startAngleRad = series.startAngleRad = radians.start, - endAngleRad = series.endAngleRad = radians.end, - circ = endAngleRad - startAngleRad, // 2 * Math.PI, - points = series.points, - radiusX, // the x component of the radius vector for a given point - radiusY, - labelDistance = options.dataLabels.distance, - ignoreHiddenPoint = options.ignoreHiddenPoint, - i, - len = points.length, - point; - - // Get positions - either an integer or a percentage string must be given. - // If positions are passed as a parameter, we're in a recursive loop for adjusting - // space for data labels. - if (!positions) { - series.center = positions = series.getCenter(); - } - - // Utility for getting the x value from a given y, used for anticollision - // logic in data labels. - // Added point for using specific points' label distance. - series.getX = function (y, left, point) { - angle = Math.asin(Math.min((y - positions[1]) / (positions[2] / 2 + point.labelDistance), 1)); - return positions[0] + - (left ? -1 : 1) * - (Math.cos(angle) * (positions[2] / 2 + point.labelDistance)); - }; - - // Calculate the geometry for each point - for (i = 0; i < len; i++) { - - point = points[i]; - - // Used for distance calculation for specific point. - point.labelDistance = pick( - point.options.dataLabels && point.options.dataLabels.distance, - labelDistance - ); - - // Saved for later dataLabels distance calculation. - series.maxLabelDistance = Math.max(series.maxLabelDistance || 0, point.labelDistance); - - // set start and end angle - start = startAngleRad + (cumulative * circ); - if (!ignoreHiddenPoint || point.visible) { - cumulative += point.percentage / 100; - } - end = startAngleRad + (cumulative * circ); - - // set the shape - point.shapeType = 'arc'; - point.shapeArgs = { - x: positions[0], - y: positions[1], - r: positions[2] / 2, - innerR: positions[3] / 2, - start: Math.round(start * precision) / precision, - end: Math.round(end * precision) / precision - }; - - // The angle must stay within -90 and 270 (#2645) - angle = (end + start) / 2; - if (angle > 1.5 * Math.PI) { - angle -= 2 * Math.PI; - } else if (angle < -Math.PI / 2) { - angle += 2 * Math.PI; - } - - // Center for the sliced out slice - point.slicedTranslation = { - translateX: Math.round(Math.cos(angle) * slicedOffset), - translateY: Math.round(Math.sin(angle) * slicedOffset) - }; - - // set the anchor point for tooltips - radiusX = Math.cos(angle) * positions[2] / 2; - radiusY = Math.sin(angle) * positions[2] / 2; - point.tooltipPos = [ - positions[0] + radiusX * 0.7, - positions[1] + radiusY * 0.7 - ]; - - point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? 1 : 0; - point.angle = angle; - - // Set the anchor point for data labels. Use point.labelDistance - // instead of labelDistance // #1174 - // finalConnectorOffset - not override connectorOffset value. - finalConnectorOffset = Math.min(connectorOffset, point.labelDistance / 5); // #1678 - point.labelPos = [ - positions[0] + radiusX + Math.cos(angle) * point.labelDistance, // first break of connector - positions[1] + radiusY + Math.sin(angle) * point.labelDistance, // a/a - positions[0] + radiusX + Math.cos(angle) * finalConnectorOffset, // second break, right outside pie - positions[1] + radiusY + Math.sin(angle) * finalConnectorOffset, // a/a - positions[0] + radiusX, // landing point for connector - positions[1] + radiusY, // a/a - point.labelDistance < 0 ? // alignment - 'center' : - point.half ? 'right' : 'left', // alignment - angle // center angle - ]; - - } - }, - - drawGraph: null, - - /** - * Draw the data points - */ - drawPoints: function () { - var series = this, - chart = series.chart, - renderer = chart.renderer, - groupTranslation, - graphic, - pointAttr, - shapeArgs; - - /*= if (build.classic) { =*/ - var shadow = series.options.shadow; - if (shadow && !series.shadowGroup) { - series.shadowGroup = renderer.g('shadow') - .add(series.group); - } - /*= } =*/ - - // draw the slices - each(series.points, function (point) { - graphic = point.graphic; - if (!point.isNull) { - shapeArgs = point.shapeArgs; - - - // If the point is sliced, use special translation, else use - // plot area traslation - groupTranslation = point.getTranslate(); - - /*= if (build.classic) { =*/ - // Put the shadow behind all points - var shadowGroup = point.shadowGroup; - if (shadow && !shadowGroup) { - shadowGroup = point.shadowGroup = renderer.g('shadow') - .add(series.shadowGroup); - } - - if (shadowGroup) { - shadowGroup.attr(groupTranslation); - } - pointAttr = series.pointAttribs(point, point.selected && 'select'); - /*= } =*/ - - // Draw the slice - if (graphic) { - graphic - .setRadialReference(series.center) - /*= if (build.classic) { =*/ - .attr(pointAttr) - /*= } =*/ - .animate(extend(shapeArgs, groupTranslation)); - } else { - - point.graphic = graphic = renderer[point.shapeType](shapeArgs) - .setRadialReference(series.center) - .attr(groupTranslation) - .add(series.group); - - if (!point.visible) { - graphic.attr({ visibility: 'hidden' }); - } - - /*= if (build.classic) { =*/ - graphic - .attr(pointAttr) - .attr({ 'stroke-linejoin': 'round' }) - .shadow(shadow, shadowGroup); - /*= } =*/ - } - - graphic.addClass(point.getClassName()); - - } else if (graphic) { - point.graphic = graphic.destroy(); - } - }); - - }, - - - searchPoint: noop, - - /** - * Utility for sorting data labels - */ - sortByAngle: function (points, sign) { - points.sort(function (a, b) { - return a.angle !== undefined && (b.angle - a.angle) * sign; - }); - }, - - /** - * Use a simple symbol from LegendSymbolMixin - */ - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - - /** - * Use the getCenter method from drawLegendSymbol - */ - getCenter: CenteredSeriesMixin.getCenter, - - /** - * Pies don't have point marker symbols - */ - getSymbol: noop + isCartesian: false, + requireSorting: false, + directTouch: true, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], + axisTypes: [], + pointAttribs: seriesTypes.column.prototype.pointAttribs, + /** + * Animate the pies in + */ + animate: function (init) { + var series = this, + points = series.points, + startAngleRad = series.startAngleRad; + + if (!init) { + each(points, function (point) { + var graphic = point.graphic, + args = point.shapeArgs; + + if (graphic) { + // start values + graphic.attr({ + r: point.startR || (series.center[3] / 2), // animate from inner radius (#779) + start: startAngleRad, + end: startAngleRad + }); + + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + } + }, + + /** + * Recompute total chart sum and update percentages of points. + */ + updateTotals: function () { + var i, + total = 0, + points = this.points, + len = points.length, + point, + ignoreHiddenPoint = this.options.ignoreHiddenPoint; + + // Get the total sum + for (i = 0; i < len; i++) { + point = points[i]; + total += (ignoreHiddenPoint && !point.visible) ? + 0 : + point.isNull ? 0 : point.y; + } + this.total = total; + + // Set each point's properties + for (i = 0; i < len; i++) { + point = points[i]; + point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; + point.total = total; + } + }, + + /** + * Extend the generatePoints method by adding total and percentage properties to each point + */ + generatePoints: function () { + Series.prototype.generatePoints.call(this); + this.updateTotals(); + }, + + /** + * Do translation for pie slices + */ + translate: function (positions) { + this.generatePoints(); + + var series = this, + cumulative = 0, + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + (options.borderWidth || 0), + finalConnectorOffset, + start, + end, + angle, + radians = getStartAndEndRadians(options.startAngle, options.endAngle), + startAngleRad = series.startAngleRad = radians.start, + endAngleRad = series.endAngleRad = radians.end, + circ = endAngleRad - startAngleRad, // 2 * Math.PI, + points = series.points, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance, + ignoreHiddenPoint = options.ignoreHiddenPoint, + i, + len = points.length, + point; + + // Get positions - either an integer or a percentage string must be given. + // If positions are passed as a parameter, we're in a recursive loop for adjusting + // space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } + + // Utility for getting the x value from a given y, used for anticollision + // logic in data labels. + // Added point for using specific points' label distance. + series.getX = function (y, left, point) { + angle = Math.asin(Math.min((y - positions[1]) / (positions[2] / 2 + point.labelDistance), 1)); + return positions[0] + + (left ? -1 : 1) * + (Math.cos(angle) * (positions[2] / 2 + point.labelDistance)); + }; + + // Calculate the geometry for each point + for (i = 0; i < len; i++) { + + point = points[i]; + + // Used for distance calculation for specific point. + point.labelDistance = pick( + point.options.dataLabels && point.options.dataLabels.distance, + labelDistance + ); + + // Saved for later dataLabels distance calculation. + series.maxLabelDistance = Math.max(series.maxLabelDistance || 0, point.labelDistance); + + // set start and end angle + start = startAngleRad + (cumulative * circ); + if (!ignoreHiddenPoint || point.visible) { + cumulative += point.percentage / 100; + } + end = startAngleRad + (cumulative * circ); + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: Math.round(start * precision) / precision, + end: Math.round(end * precision) / precision + }; + + // The angle must stay within -90 and 270 (#2645) + angle = (end + start) / 2; + if (angle > 1.5 * Math.PI) { + angle -= 2 * Math.PI; + } else if (angle < -Math.PI / 2) { + angle += 2 * Math.PI; + } + + // Center for the sliced out slice + point.slicedTranslation = { + translateX: Math.round(Math.cos(angle) * slicedOffset), + translateY: Math.round(Math.sin(angle) * slicedOffset) + }; + + // set the anchor point for tooltips + radiusX = Math.cos(angle) * positions[2] / 2; + radiusY = Math.sin(angle) * positions[2] / 2; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; + + point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? 1 : 0; + point.angle = angle; + + // Set the anchor point for data labels. Use point.labelDistance + // instead of labelDistance // #1174 + // finalConnectorOffset - not override connectorOffset value. + finalConnectorOffset = Math.min(connectorOffset, point.labelDistance / 5); // #1678 + point.labelPos = [ + positions[0] + radiusX + Math.cos(angle) * point.labelDistance, // first break of connector + positions[1] + radiusY + Math.sin(angle) * point.labelDistance, // a/a + positions[0] + radiusX + Math.cos(angle) * finalConnectorOffset, // second break, right outside pie + positions[1] + radiusY + Math.sin(angle) * finalConnectorOffset, // a/a + positions[0] + radiusX, // landing point for connector + positions[1] + radiusY, // a/a + point.labelDistance < 0 ? // alignment + 'center' : + point.half ? 'right' : 'left', // alignment + angle // center angle + ]; + + } + }, + + drawGraph: null, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + graphic, + pointAttr, + shapeArgs; + + /*= if (build.classic) { =*/ + var shadow = series.options.shadow; + if (shadow && !series.shadowGroup) { + series.shadowGroup = renderer.g('shadow') + .add(series.group); + } + /*= } =*/ + + // draw the slices + each(series.points, function (point) { + graphic = point.graphic; + if (!point.isNull) { + shapeArgs = point.shapeArgs; + + + // If the point is sliced, use special translation, else use + // plot area traslation + groupTranslation = point.getTranslate(); + + /*= if (build.classic) { =*/ + // Put the shadow behind all points + var shadowGroup = point.shadowGroup; + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .add(series.shadowGroup); + } + + if (shadowGroup) { + shadowGroup.attr(groupTranslation); + } + pointAttr = series.pointAttribs(point, point.selected && 'select'); + /*= } =*/ + + // Draw the slice + if (graphic) { + graphic + .setRadialReference(series.center) + /*= if (build.classic) { =*/ + .attr(pointAttr) + /*= } =*/ + .animate(extend(shapeArgs, groupTranslation)); + } else { + + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .setRadialReference(series.center) + .attr(groupTranslation) + .add(series.group); + + if (!point.visible) { + graphic.attr({ visibility: 'hidden' }); + } + + /*= if (build.classic) { =*/ + graphic + .attr(pointAttr) + .attr({ 'stroke-linejoin': 'round' }) + .shadow(shadow, shadowGroup); + /*= } =*/ + } + + graphic.addClass(point.getClassName()); + + } else if (graphic) { + point.graphic = graphic.destroy(); + } + }); + + }, + + + searchPoint: noop, + + /** + * Utility for sorting data labels + */ + sortByAngle: function (points, sign) { + points.sort(function (a, b) { + return a.angle !== undefined && (b.angle - a.angle) * sign; + }); + }, + + /** + * Use a simple symbol from LegendSymbolMixin + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + /** + * Use the getCenter method from drawLegendSymbol + */ + getCenter: CenteredSeriesMixin.getCenter, + + /** + * Pies don't have point marker symbols + */ + getSymbol: noop }, /** @lends seriesTypes.pie.prototype.pointClass.prototype */ { - /** - * Initiate the pie slice - */ - init: function () { - - Point.prototype.init.apply(this, arguments); - - var point = this, - toggleSlice; - - point.name = pick(point.name, 'Slice'); - - // add event listener for select - toggleSlice = function (e) { - point.slice(e.type === 'select'); - }; - addEvent(point, 'select', toggleSlice); - addEvent(point, 'unselect', toggleSlice); - - return point; - }, - - /** - * Negative points are not valid (#1530, #3623, #5322) - */ - isValid: function () { - return H.isNumber(this.y, true) && this.y >= 0; - }, - - /** - * Toggle the visibility of the pie slice - * @param {Boolean} vis Whether to show the slice or not. If undefined, the - * visibility is toggled - */ - setVisible: function (vis, redraw) { - var point = this, - series = point.series, - chart = series.chart, - ignoreHiddenPoint = series.options.ignoreHiddenPoint; - - redraw = pick(redraw, ignoreHiddenPoint); - - if (vis !== point.visible) { - - // If called without an argument, toggle visibility - point.visible = point.options.visible = vis = vis === undefined ? !point.visible : vis; - series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data - - // Show and hide associated elements. This is performed regardless of redraw or not, - // because chart.redraw only handles full series. - each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { - if (point[key]) { - point[key][vis ? 'show' : 'hide'](true); - } - }); - - if (point.legendItem) { - chart.legend.colorizeItem(point, vis); - } - - // #4170, hide halo after hiding point - if (!vis && point.state === 'hover') { - point.setState(''); - } - - // Handle ignore hidden slices - if (ignoreHiddenPoint) { - series.isDirty = true; - } - - if (redraw) { - chart.redraw(); - } - } - }, - - /** - * Set or toggle whether the slice is cut out from the pie - * @param {Boolean} sliced When undefined, the slice state is toggled - * @param {Boolean} redraw Whether to redraw the chart. True by default. - */ - slice: function (sliced, redraw, animation) { - var point = this, - series = point.series, - chart = series.chart; - - setAnimation(animation, chart); - - // redraw is true by default - redraw = pick(redraw, true); - - // if called without an argument, toggle - point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; - series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data - - point.graphic.animate(this.getTranslate()); - - /*= if (build.classic) { =*/ - if (point.shadowGroup) { - point.shadowGroup.animate(this.getTranslate()); - } - /*= } =*/ - }, - - getTranslate: function () { - return this.sliced ? this.slicedTranslation : { - translateX: 0, - translateY: 0 - }; - }, - - haloPath: function (size) { - var shapeArgs = this.shapeArgs; - - return this.sliced || !this.visible ? - [] : - this.series.chart.renderer.symbols.arc( - shapeArgs.x, - shapeArgs.y, - shapeArgs.r + size, - shapeArgs.r + size, { - // Substract 1px to ensure the background is not bleeding - // through between the halo and the slice (#7495). - innerR: this.shapeArgs.r - 1, - start: shapeArgs.start, - end: shapeArgs.end - } - ); - } + /** + * Initiate the pie slice + */ + init: function () { + + Point.prototype.init.apply(this, arguments); + + var point = this, + toggleSlice; + + point.name = pick(point.name, 'Slice'); + + // add event listener for select + toggleSlice = function (e) { + point.slice(e.type === 'select'); + }; + addEvent(point, 'select', toggleSlice); + addEvent(point, 'unselect', toggleSlice); + + return point; + }, + + /** + * Negative points are not valid (#1530, #3623, #5322) + */ + isValid: function () { + return H.isNumber(this.y, true) && this.y >= 0; + }, + + /** + * Toggle the visibility of the pie slice + * @param {Boolean} vis Whether to show the slice or not. If undefined, the + * visibility is toggled + */ + setVisible: function (vis, redraw) { + var point = this, + series = point.series, + chart = series.chart, + ignoreHiddenPoint = series.options.ignoreHiddenPoint; + + redraw = pick(redraw, ignoreHiddenPoint); + + if (vis !== point.visible) { + + // If called without an argument, toggle visibility + point.visible = point.options.visible = vis = vis === undefined ? !point.visible : vis; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + // Show and hide associated elements. This is performed regardless of redraw or not, + // because chart.redraw only handles full series. + each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { + if (point[key]) { + point[key][vis ? 'show' : 'hide'](true); + } + }); + + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } + + // #4170, hide halo after hiding point + if (!vis && point.state === 'hover') { + point.setState(''); + } + + // Handle ignore hidden slices + if (ignoreHiddenPoint) { + series.isDirty = true; + } + + if (redraw) { + chart.redraw(); + } + } + }, + + /** + * Set or toggle whether the slice is cut out from the pie + * @param {Boolean} sliced When undefined, the slice state is toggled + * @param {Boolean} redraw Whether to redraw the chart. True by default. + */ + slice: function (sliced, redraw, animation) { + var point = this, + series = point.series, + chart = series.chart; + + setAnimation(animation, chart); + + // redraw is true by default + redraw = pick(redraw, true); + + // if called without an argument, toggle + point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + point.graphic.animate(this.getTranslate()); + + /*= if (build.classic) { =*/ + if (point.shadowGroup) { + point.shadowGroup.animate(this.getTranslate()); + } + /*= } =*/ + }, + + getTranslate: function () { + return this.sliced ? this.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + }, + + haloPath: function (size) { + var shapeArgs = this.shapeArgs; + + return this.sliced || !this.visible ? + [] : + this.series.chart.renderer.symbols.arc( + shapeArgs.x, + shapeArgs.y, + shapeArgs.r + size, + shapeArgs.r + size, { + // Substract 1px to ensure the background is not bleeding + // through between the halo and the slice (#7495). + innerR: this.shapeArgs.r - 1, + start: shapeArgs.start, + end: shapeArgs.end + } + ); + } }); /** * A `pie` series. If the [type](#series.pie.type) option is not specified, * it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.pie * @excluding dataParser,dataURL,stack,xAxis,yAxis @@ -840,19 +840,19 @@ seriesType('pie', 'line', { /** * An array of data points for the series. For the `pie` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 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.pie.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * y: 1, @@ -863,7 +863,7 @@ seriesType('pie', 'line', { * name: "Point1", * color: "#FF00FF" * }] - * + * * @type {Array} * @extends series.line.data * @excluding marker,x @@ -876,14 +876,14 @@ seriesType('pie', 'line', { * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @product highcharts * @apioption series.pie.data */ /** * The sequential index of the data point in the legend. - * + * * @type {Number} * @product highcharts * @apioption series.pie.data.legendIndex @@ -891,7 +891,7 @@ seriesType('pie', 'line', { /** * Whether to display a slice offset from the center. - * + * * @type {Boolean} * @sample {highcharts} highcharts/point/sliced/ One sliced point * @product highcharts @@ -904,7 +904,7 @@ seriesType('pie', 'line', { * checkbox is found by event.checked. The checked item is found by * event.item. Return false to prevent the default action which is to * toggle the select state of the series. - * + * * @type {Function} * @context Point * @sample {highcharts} highcharts/plotoptions/series-events-checkboxclick/ @@ -917,7 +917,7 @@ seriesType('pie', 'line', { /** * Not applicable to pies, as the legend item is per point. See point. * events. - * + * * @type {Function} * @since 1.2.0 * @product highcharts @@ -930,7 +930,7 @@ seriesType('pie', 'line', { * `event`, is passed to the function, containing common event information. The * default action is to toggle the visibility of the point. This can be * prevented by calling `event.preventDefault()`. - * + * * @type {Function} * @sample {highcharts} highcharts/plotoptions/pie-point-events-legenditemclick/ * Confirm toggle visibility diff --git a/js/parts/PlotBandSeries.experimental.js b/js/parts/PlotBandSeries.experimental.js index ed0f7d8277c..4397a125aea 100644 --- a/js/parts/PlotBandSeries.experimental.js +++ b/js/parts/PlotBandSeries.experimental.js @@ -4,7 +4,7 @@ * License: www.highcharts.com/license */ /* **************************************************************************** - * Start PlotBand series code * + * Start PlotBand series code * *****************************************************************************/ /** * This is an experiment of implementing plotBands and plotLines as a series. @@ -18,57 +18,57 @@ import './Utilities.js'; import './Series.js'; import './Options.js'; var seriesType = H.seriesType, - each = H.each, - Series = H.Series; + each = H.each, + Series = H.Series; seriesType('plotband', 'column', { - lineWidth: 0, - threshold: null + lineWidth: 0, + threshold: null }, { - /*= if (build.classic) { =*/ - // mapping between SVG attributes and the corresponding options - pointAttrToOptions: { - fill: 'color', - stroke: 'lineColor', - 'stroke-width': 'lineWidth' - }, - /*= } =*/ - animate: function () {}, + /*= if (build.classic) { =*/ + // mapping between SVG attributes and the corresponding options + pointAttrToOptions: { + fill: 'color', + stroke: 'lineColor', + 'stroke-width': 'lineWidth' + }, + /*= } =*/ + animate: function () {}, - translate: function () { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis; + translate: function () { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis; - Series.prototype.translate.apply(series); + Series.prototype.translate.apply(series); - each(series.points, function (point) { - var onXAxis = point.onXAxis, - ownAxis = onXAxis ? xAxis : yAxis, - otherAxis = onXAxis ? yAxis : xAxis, - from = ownAxis.toPixels(point.from, true), - to = ownAxis.toPixels(point.to, true), - start = Math.min(from, to), - width = Math.abs(to - from); + each(series.points, function (point) { + var onXAxis = point.onXAxis, + ownAxis = onXAxis ? xAxis : yAxis, + otherAxis = onXAxis ? yAxis : xAxis, + from = ownAxis.toPixels(point.from, true), + to = ownAxis.toPixels(point.to, true), + start = Math.min(from, to), + width = Math.abs(to - from); - point.plotY = 1; // lure ColumnSeries.drawPoints - point.shapeType = 'rect'; - point.shapeArgs = ownAxis.horiz ? { - x: start, - y: 0, - width: width, - height: otherAxis.len - } : { - x: 0, - y: start, - width: otherAxis.len, - height: width - }; - }); - } + point.plotY = 1; // lure ColumnSeries.drawPoints + point.shapeType = 'rect'; + point.shapeArgs = ownAxis.horiz ? { + x: start, + y: 0, + width: width, + height: otherAxis.len + } : { + x: 0, + y: start, + width: otherAxis.len, + height: width + }; + }); + } }); /* **************************************************************************** - * End PlotBand series code * + * End PlotBand series code * *****************************************************************************/ diff --git a/js/parts/PlotLineOrBand.js b/js/parts/PlotLineOrBand.js index de777feec26..318899e990e 100644 --- a/js/parts/PlotLineOrBand.js +++ b/js/parts/PlotLineOrBand.js @@ -8,239 +8,239 @@ import H from './Globals.js'; import Axis from './Axis.js'; import './Utilities.js'; var arrayMax = H.arrayMax, - arrayMin = H.arrayMin, - defined = H.defined, - destroyObjectProperties = H.destroyObjectProperties, - each = H.each, - erase = H.erase, - merge = H.merge, - pick = H.pick; + arrayMin = H.arrayMin, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + erase = H.erase, + merge = H.merge, + pick = H.pick; /* * The object wrapper for plot lines and plot bands * @param {Object} options */ H.PlotLineOrBand = function (axis, options) { - this.axis = axis; + this.axis = axis; - if (options) { - this.options = options; - this.id = options.id; - } + if (options) { + this.options = options; + this.id = options.id; + } }; H.PlotLineOrBand.prototype = { - - /** - * Render the plot line or plot band. If it is already existing, - * move it. - */ - render: function () { - var plotLine = this, - axis = plotLine.axis, - horiz = axis.horiz, - options = plotLine.options, - optionsLabel = options.label, - label = plotLine.label, - to = options.to, - from = options.from, - value = options.value, - isBand = defined(from) && defined(to), - isLine = defined(value), - svgElem = plotLine.svgElem, - isNew = !svgElem, - path = [], - color = options.color, - zIndex = pick(options.zIndex, 0), - events = options.events, - attribs = { - 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + - (options.className || '') - }, - groupAttribs = {}, - renderer = axis.chart.renderer, - groupName = isBand ? 'bands' : 'lines', - group, - log2lin = axis.log2lin; - - // logarithmic conversion - if (axis.isLog) { - from = log2lin(from); - to = log2lin(to); - value = log2lin(value); - } - - /*= if (build.classic) { =*/ - // Set the presentational attributes - if (isLine) { - attribs = { - stroke: color, - 'stroke-width': options.width - }; - if (options.dashStyle) { - attribs.dashstyle = options.dashStyle; - } - - } else if (isBand) { // plot band - if (color) { - attribs.fill = color; - } - if (options.borderWidth) { - attribs.stroke = options.borderColor; - attribs['stroke-width'] = options.borderWidth; - } - } - /*= } =*/ - - // Grouping and zIndex - groupAttribs.zIndex = zIndex; - groupName += '-' + zIndex; - - group = axis.plotLinesAndBandsGroups[groupName]; - if (!group) { - axis.plotLinesAndBandsGroups[groupName] = group = - renderer.g('plot-' + groupName) - .attr(groupAttribs).add(); - } - - // Create the path - if (isNew) { - plotLine.svgElem = svgElem = - renderer - .path() - .attr(attribs).add(group); - } - - - // Set the path or return - if (isLine) { - path = axis.getPlotLinePath(value, svgElem.strokeWidth()); - } else if (isBand) { // plot band - path = axis.getPlotBandPath(from, to, options); - } else { - return; - } - - - // common for lines and bands - if (isNew && path && path.length) { - svgElem.attr({ d: path }); - - // events - if (events) { - H.objectEach(events, function (event, eventType) { - svgElem.on(eventType, function (e) { - events[eventType].apply(plotLine, [e]); - }); - }); - } - } else if (svgElem) { - if (path) { - svgElem.show(); - svgElem.animate({ d: path }); - } else { - svgElem.hide(); - if (label) { - plotLine.label = label = label.destroy(); - } - } - } - - // the plot band/line label - if ( - optionsLabel && - defined(optionsLabel.text) && - path && - path.length && - axis.width > 0 && - axis.height > 0 && - !path.flat - ) { - // apply defaults - optionsLabel = merge({ - align: horiz && isBand && 'center', - x: horiz ? !isBand && 4 : 10, - verticalAlign: !horiz && isBand && 'middle', - y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, - rotation: horiz && !isBand && 90 - }, optionsLabel); - - this.renderLabel(optionsLabel, path, isBand, zIndex); - - } else if (label) { // move out of sight - label.hide(); - } - - // chainable - return plotLine; - }, - - /** - * Render and align label for plot line or band. - */ - renderLabel: function (optionsLabel, path, isBand, zIndex) { - var plotLine = this, - label = plotLine.label, - renderer = plotLine.axis.chart.renderer, - attribs, - xBounds, - yBounds, - x, - y; - - // add the SVG element - if (!label) { - attribs = { - align: optionsLabel.textAlign || optionsLabel.align, - rotation: optionsLabel.rotation, - 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + - '-label ' + (optionsLabel.className || '') - }; - - attribs.zIndex = zIndex; - - plotLine.label = label = renderer.text( - optionsLabel.text, - 0, - 0, - optionsLabel.useHTML - ) - .attr(attribs) - .add(); - - /*= if (build.classic) { =*/ - label.css(optionsLabel.style); - /*= } =*/ - } - - // get the bounding box and align the label - // #3000 changed to better handle choice between plotband or plotline - xBounds = path.xBounds || - [path[1], path[4], (isBand ? path[6] : path[1])]; - yBounds = path.yBounds || - [path[2], path[5], (isBand ? path[7] : path[2])]; - - x = arrayMin(xBounds); - y = arrayMin(yBounds); - - label.align(optionsLabel, false, { - x: x, - y: y, - width: arrayMax(xBounds) - x, - height: arrayMax(yBounds) - y - }); - label.show(); - }, - - /** - * Remove the plot line or band - */ - destroy: function () { - // remove it from the lookup - erase(this.axis.plotLinesAndBands, this); - - delete this.axis; - destroyObjectProperties(this); - } + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + axis = plotLine.axis, + horiz = axis.horiz, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + to = options.to, + from = options.from, + value = options.value, + isBand = defined(from) && defined(to), + isLine = defined(value), + svgElem = plotLine.svgElem, + isNew = !svgElem, + path = [], + color = options.color, + zIndex = pick(options.zIndex, 0), + events = options.events, + attribs = { + 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + + (options.className || '') + }, + groupAttribs = {}, + renderer = axis.chart.renderer, + groupName = isBand ? 'bands' : 'lines', + group, + log2lin = axis.log2lin; + + // logarithmic conversion + if (axis.isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } + + /*= if (build.classic) { =*/ + // Set the presentational attributes + if (isLine) { + attribs = { + stroke: color, + 'stroke-width': options.width + }; + if (options.dashStyle) { + attribs.dashstyle = options.dashStyle; + } + + } else if (isBand) { // plot band + if (color) { + attribs.fill = color; + } + if (options.borderWidth) { + attribs.stroke = options.borderColor; + attribs['stroke-width'] = options.borderWidth; + } + } + /*= } =*/ + + // Grouping and zIndex + groupAttribs.zIndex = zIndex; + groupName += '-' + zIndex; + + group = axis.plotLinesAndBandsGroups[groupName]; + if (!group) { + axis.plotLinesAndBandsGroups[groupName] = group = + renderer.g('plot-' + groupName) + .attr(groupAttribs).add(); + } + + // Create the path + if (isNew) { + plotLine.svgElem = svgElem = + renderer + .path() + .attr(attribs).add(group); + } + + + // Set the path or return + if (isLine) { + path = axis.getPlotLinePath(value, svgElem.strokeWidth()); + } else if (isBand) { // plot band + path = axis.getPlotBandPath(from, to, options); + } else { + return; + } + + + // common for lines and bands + if (isNew && path && path.length) { + svgElem.attr({ d: path }); + + // events + if (events) { + H.objectEach(events, function (event, eventType) { + svgElem.on(eventType, function (e) { + events[eventType].apply(plotLine, [e]); + }); + }); + } + } else if (svgElem) { + if (path) { + svgElem.show(); + svgElem.animate({ d: path }); + } else { + svgElem.hide(); + if (label) { + plotLine.label = label = label.destroy(); + } + } + } + + // the plot band/line label + if ( + optionsLabel && + defined(optionsLabel.text) && + path && + path.length && + axis.width > 0 && + axis.height > 0 && + !path.flat + ) { + // apply defaults + optionsLabel = merge({ + align: horiz && isBand && 'center', + x: horiz ? !isBand && 4 : 10, + verticalAlign: !horiz && isBand && 'middle', + y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, + rotation: horiz && !isBand && 90 + }, optionsLabel); + + this.renderLabel(optionsLabel, path, isBand, zIndex); + + } else if (label) { // move out of sight + label.hide(); + } + + // chainable + return plotLine; + }, + + /** + * Render and align label for plot line or band. + */ + renderLabel: function (optionsLabel, path, isBand, zIndex) { + var plotLine = this, + label = plotLine.label, + renderer = plotLine.axis.chart.renderer, + attribs, + xBounds, + yBounds, + x, + y; + + // add the SVG element + if (!label) { + attribs = { + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation, + 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + + '-label ' + (optionsLabel.className || '') + }; + + attribs.zIndex = zIndex; + + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0, + optionsLabel.useHTML + ) + .attr(attribs) + .add(); + + /*= if (build.classic) { =*/ + label.css(optionsLabel.style); + /*= } =*/ + } + + // get the bounding box and align the label + // #3000 changed to better handle choice between plotband or plotline + xBounds = path.xBounds || + [path[1], path[4], (isBand ? path[6] : path[1])]; + yBounds = path.yBounds || + [path[2], path[5], (isBand ? path[7] : path[2])]; + + x = arrayMin(xBounds); + y = arrayMin(yBounds); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xBounds) - x, + height: arrayMax(yBounds) - y + }); + label.show(); + }, + + /** + * Remove the plot line or band + */ + destroy: function () { + // remove it from the lookup + erase(this.axis.plotLinesAndBands, this); + + delete this.axis; + destroyObjectProperties(this); + } }; /** @@ -250,187 +250,187 @@ H.PlotLineOrBand.prototype = { H.extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ { - /** - * Internal function to create the SVG path definition for a plot band. - * - * @param {Number} from - * The axis value to start from. - * @param {Number} to - * The axis value to end on. - * - * @return {Array.} - * The SVG path definition in array form. - */ - getPlotBandPath: function (from, to) { - var toPath = this.getPlotLinePath(to, null, null, true), - path = this.getPlotLinePath(from, null, null, true), - result = [], - i, - // #4964 check if chart is inverted or plotband is on yAxis - horiz = this.horiz, - plus = 1, - flat, - outside = - (from < this.min && to < this.min) || - (from > this.max && to > this.max); - - if (path && toPath) { - - // Flat paths don't need labels (#3836) - if (outside) { - flat = path.toString() === toPath.toString(); - plus = 0; - } - - // Go over each subpath - for panes in Highstock - for (i = 0; i < path.length; i += 6) { - - // Add 1 pixel when coordinates are the same - if (horiz && toPath[i + 1] === path[i + 1]) { - toPath[i + 1] += plus; - toPath[i + 4] += plus; - } else if (!horiz && toPath[i + 2] === path[i + 2]) { - toPath[i + 2] += plus; - toPath[i + 5] += plus; - } - - result.push( - 'M', - path[i + 1], - path[i + 2], - 'L', - path[i + 4], - path[i + 5], - toPath[i + 4], - toPath[i + 5], - toPath[i + 1], - toPath[i + 2], - 'z' - ); - result.flat = flat; - } - - } else { // outside the axis area - path = null; - } - - return result; - }, - - /** - * Add a plot band after render time. - * - * @param {AxisPlotBandsOptions} options - * A configuration object for the plot band, as defined in {@link - * https://api.highcharts.com/highcharts/xAxis.plotBands| - * xAxis.plotBands}. - * @return {Object} - * The added plot band. - * @sample highcharts/members/axis-addplotband/ - * Toggle the plot band from a button - */ - addPlotBand: function (options) { - return this.addPlotBandOrLine(options, 'plotBands'); - }, - - /** - * Add a plot line after render time. - * - * @param {AxisPlotLinesOptions} options - * A configuration object for the plot line, as defined in {@link - * https://api.highcharts.com/highcharts/xAxis.plotLines| - * xAxis.plotLines}. - * @return {Object} - * The added plot line. - * @sample highcharts/members/axis-addplotline/ - * Toggle the plot line from a button - */ - addPlotLine: function (options) { - return this.addPlotBandOrLine(options, 'plotLines'); - }, - - /** - * Add a plot band or plot line after render time. Called from addPlotBand - * and addPlotLine internally. - * - * @private - * @param options {AxisPlotLinesOptions|AxisPlotBandsOptions} - * The plotBand or plotLine configuration object. - */ - addPlotBandOrLine: function (options, coll) { - var obj = new H.PlotLineOrBand(this, options).render(), - userOptions = this.userOptions; - - if (obj) { // #2189 - // Add it to the user options for exporting and Axis.update - if (coll) { - userOptions[coll] = userOptions[coll] || []; - userOptions[coll].push(options); - } - this.plotLinesAndBands.push(obj); - } - - return obj; - }, - - /** - * Remove a plot band or plot line from the chart by id. Called internally - * from `removePlotBand` and `removePlotLine`. - * - * @private - * @param {String} id - */ - removePlotBandOrLine: function (id) { - var plotLinesAndBands = this.plotLinesAndBands, - options = this.options, - userOptions = this.userOptions, - i = plotLinesAndBands.length; - while (i--) { - if (plotLinesAndBands[i].id === id) { - plotLinesAndBands[i].destroy(); - } - } - each([ - options.plotLines || [], - userOptions.plotLines || [], - options.plotBands || [], - userOptions.plotBands || [] - ], function (arr) { - i = arr.length; - while (i--) { - if (arr[i].id === id) { - erase(arr, arr[i]); - } - } - }); - }, - - /** - * Remove a plot band by its id. - * - * @param {String} id - * The plot band's `id` as given in the original configuration - * object or in the `addPlotBand` option. - * @sample highcharts/members/axis-removeplotband/ - * Remove plot band by id - * @sample highcharts/members/axis-addplotband/ - * Toggle the plot band from a button - */ - removePlotBand: function (id) { - this.removePlotBandOrLine(id); - }, - - /** - * Remove a plot line by its id. - * @param {String} id - * The plot line's `id` as given in the original configuration - * object or in the `addPlotLine` option. - * @sample highcharts/xaxis/plotlines-id/ - * Remove plot line by id - * @sample highcharts/members/axis-addplotline/ - * Toggle the plot line from a button - */ - removePlotLine: function (id) { - this.removePlotBandOrLine(id); - } + /** + * Internal function to create the SVG path definition for a plot band. + * + * @param {Number} from + * The axis value to start from. + * @param {Number} to + * The axis value to end on. + * + * @return {Array.} + * The SVG path definition in array form. + */ + getPlotBandPath: function (from, to) { + var toPath = this.getPlotLinePath(to, null, null, true), + path = this.getPlotLinePath(from, null, null, true), + result = [], + i, + // #4964 check if chart is inverted or plotband is on yAxis + horiz = this.horiz, + plus = 1, + flat, + outside = + (from < this.min && to < this.min) || + (from > this.max && to > this.max); + + if (path && toPath) { + + // Flat paths don't need labels (#3836) + if (outside) { + flat = path.toString() === toPath.toString(); + plus = 0; + } + + // Go over each subpath - for panes in Highstock + for (i = 0; i < path.length; i += 6) { + + // Add 1 pixel when coordinates are the same + if (horiz && toPath[i + 1] === path[i + 1]) { + toPath[i + 1] += plus; + toPath[i + 4] += plus; + } else if (!horiz && toPath[i + 2] === path[i + 2]) { + toPath[i + 2] += plus; + toPath[i + 5] += plus; + } + + result.push( + 'M', + path[i + 1], + path[i + 2], + 'L', + path[i + 4], + path[i + 5], + toPath[i + 4], + toPath[i + 5], + toPath[i + 1], + toPath[i + 2], + 'z' + ); + result.flat = flat; + } + + } else { // outside the axis area + path = null; + } + + return result; + }, + + /** + * Add a plot band after render time. + * + * @param {AxisPlotBandsOptions} options + * A configuration object for the plot band, as defined in {@link + * https://api.highcharts.com/highcharts/xAxis.plotBands| + * xAxis.plotBands}. + * @return {Object} + * The added plot band. + * @sample highcharts/members/axis-addplotband/ + * Toggle the plot band from a button + */ + addPlotBand: function (options) { + return this.addPlotBandOrLine(options, 'plotBands'); + }, + + /** + * Add a plot line after render time. + * + * @param {AxisPlotLinesOptions} options + * A configuration object for the plot line, as defined in {@link + * https://api.highcharts.com/highcharts/xAxis.plotLines| + * xAxis.plotLines}. + * @return {Object} + * The added plot line. + * @sample highcharts/members/axis-addplotline/ + * Toggle the plot line from a button + */ + addPlotLine: function (options) { + return this.addPlotBandOrLine(options, 'plotLines'); + }, + + /** + * Add a plot band or plot line after render time. Called from addPlotBand + * and addPlotLine internally. + * + * @private + * @param options {AxisPlotLinesOptions|AxisPlotBandsOptions} + * The plotBand or plotLine configuration object. + */ + addPlotBandOrLine: function (options, coll) { + var obj = new H.PlotLineOrBand(this, options).render(), + userOptions = this.userOptions; + + if (obj) { // #2189 + // Add it to the user options for exporting and Axis.update + if (coll) { + userOptions[coll] = userOptions[coll] || []; + userOptions[coll].push(options); + } + this.plotLinesAndBands.push(obj); + } + + return obj; + }, + + /** + * Remove a plot band or plot line from the chart by id. Called internally + * from `removePlotBand` and `removePlotLine`. + * + * @private + * @param {String} id + */ + removePlotBandOrLine: function (id) { + var plotLinesAndBands = this.plotLinesAndBands, + options = this.options, + userOptions = this.userOptions, + i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id === id) { + plotLinesAndBands[i].destroy(); + } + } + each([ + options.plotLines || [], + userOptions.plotLines || [], + options.plotBands || [], + userOptions.plotBands || [] + ], function (arr) { + i = arr.length; + while (i--) { + if (arr[i].id === id) { + erase(arr, arr[i]); + } + } + }); + }, + + /** + * Remove a plot band by its id. + * + * @param {String} id + * The plot band's `id` as given in the original configuration + * object or in the `addPlotBand` option. + * @sample highcharts/members/axis-removeplotband/ + * Remove plot band by id + * @sample highcharts/members/axis-addplotband/ + * Toggle the plot band from a button + */ + removePlotBand: function (id) { + this.removePlotBandOrLine(id); + }, + + /** + * Remove a plot line by its id. + * @param {String} id + * The plot line's `id` as given in the original configuration + * object or in the `addPlotLine` option. + * @sample highcharts/xaxis/plotlines-id/ + * Remove plot line by id + * @sample highcharts/members/axis-addplotline/ + * Toggle the plot line from a button + */ + removePlotLine: function (id) { + this.removePlotBandOrLine(id); + } }); diff --git a/js/parts/Point.js b/js/parts/Point.js index 934b0fffc3a..eaa606c20f4 100644 --- a/js/parts/Point.js +++ b/js/parts/Point.js @@ -3,25 +3,25 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import Highcharts from './Globals.js'; import './Utilities.js'; var Point, - H = Highcharts, - - each = H.each, - extend = H.extend, - erase = H.erase, - fireEvent = H.fireEvent, - format = H.format, - isArray = H.isArray, - isNumber = H.isNumber, - pick = H.pick, - removeEvent = H.removeEvent; + H = Highcharts, + + each = H.each, + extend = H.extend, + erase = H.erase, + fireEvent = H.fireEvent, + format = H.format, + isArray = H.isArray, + isNumber = H.isNumber, + pick = H.pick, + removeEvent = H.removeEvent; /** - * The Point object. The point objects are generated from the `series.data` + * The Point object. The point objects are generated from the `series.data` * configuration objects or raw numbers. They can be accessed from the * `Series.points` array. Other ways to instantiate points are through {@link * Highcharts.Series#addPoint} or {@link Highcharts.Series#setData}. @@ -32,459 +32,459 @@ var Point, Highcharts.Point = Point = function () {}; Highcharts.Point.prototype = { - /** - * Initialize the point. Called internally based on the `series.data` - * option. - * @param {Series} series - * The series object containing this point. - * @param {Number|Array|Object} options - * The data in either number, array or object format. - * @param {Number} x Optionally, the X value of the point. - * @return {Point} The Point instance. - */ - init: function (series, options, x) { - - var point = this, - colors, - colorCount = series.chart.options.chart.colorCount, - colorIndex; - - /** - * The series object associated with the point. - * - * @name series - * @memberof Highcharts.Point - * @type Highcharts.Series - */ - point.series = series; - - /*= if (build.classic) { =*/ - /** - * The point's current color. - * @name color - * @memberof Highcharts.Point - * @type {Color} - */ - point.color = series.color; // #3445 - /*= } =*/ - point.applyOptions(options, x); - - if (series.options.colorByPoint) { - /*= if (build.classic) { =*/ - colors = series.options.colors || series.chart.options.colors; - point.color = point.color || colors[series.colorCounter]; - colorCount = colors.length; - /*= } =*/ - colorIndex = series.colorCounter; - series.colorCounter++; - // loop back to zero - if (series.colorCounter === colorCount) { - series.colorCounter = 0; - } - } else { - colorIndex = series.colorIndex; - } - - /** - * The point's current color index, used in styled mode instead of - * `color`. The color index is inserted in class names used for styling. - * @name colorIndex - * @memberof Highcharts.Point - * @type {Number} - */ - point.colorIndex = pick(point.colorIndex, colorIndex); - - series.chart.pointCount++; - - fireEvent(point, 'afterInit'); - - return point; - }, - /** - * Apply the options containing the x and y data and possible some extra - * properties. Called on point init or from point.update. - * - * @private - * @param {Object} options The point options as defined in series.data. - * @param {Number} x Optionally, the X value. - * @returns {Object} The Point instance. - */ - applyOptions: function (options, x) { - var point = this, - series = point.series, - pointValKey = series.options.pointValKey || series.pointValKey; - - options = Point.prototype.optionsToObject.call(this, options); - - // copy options directly to point - extend(point, options); - point.options = point.options ? - extend(point.options, options) : - options; - - // Since options are copied into the Point instance, some accidental - // options must be shielded (#5681) - if (options.group) { - delete point.group; - } - - // For higher dimension series types. For instance, for ranges, point.y - // is mapped to point.low. - if (pointValKey) { - point.y = point[pointValKey]; - } - point.isNull = pick( - point.isValid && !point.isValid(), - point.x === null || !isNumber(point.y, true) - ); // #3571, check for NaN - - // The point is initially selected by options (#5777) - if (point.selected) { - point.state = 'select'; - } - - // If no x is set by now, get auto incremented value. All points must - // have an x value, however the y value can be null to create a gap in - // the series - if ( - 'name' in point && - x === undefined && - series.xAxis && - series.xAxis.hasNames - ) { - point.x = series.xAxis.nameToX(point); - } - if (point.x === undefined && series) { - if (x === undefined) { - point.x = series.autoIncrement(point); - } else { - point.x = x; - } - } - - return point; - }, - - /** - * Set a value in an object, on the property defined by key. The key - * supports nested properties using dot notation. The function modifies the - * input object and does not make a copy. - * - * @param {Object} object The object to set the value on. - * @param {Mixed} value The value to set. - * @param {String} key Key to the property to set. - * - * @return {Object} The modified object. - */ - setNestedProperty: function (object, value, key) { - var nestedKeys = key.split('.'); - H.reduce(nestedKeys, function (result, key, i, arr) { - var isLastKey = arr.length - 1 === i; - result[key] = ( - isLastKey ? - value : - (H.isObject(result[key], true) ? result[key] : {}) - ); - return result[key]; - }, object); - return object; - }, - - /** - * Transform number or array configs into objects. Used internally to unify - * the different configuration formats for points. For example, a simple - * number `10` in a line series will be transformed to `{ y: 10 }`, and an - * array config like `[1, 10]` in a scatter series will be transformed to - * `{ x: 1, y: 10 }`. - * - * @param {Number|Array|Object} options - * The input options - * @return {Object} Transformed options. - */ - optionsToObject: function (options) { - var ret = {}, - series = this.series, - keys = series.options.keys, - pointArrayMap = keys || series.pointArrayMap || ['y'], - valueCount = pointArrayMap.length, - firstItemType, - i = 0, - j = 0; - - if (isNumber(options) || options === null) { - ret[pointArrayMap[0]] = options; - - } else if (isArray(options)) { - // with leading x value - if (!keys && options.length > valueCount) { - firstItemType = typeof options[0]; - if (firstItemType === 'string') { - ret.name = options[0]; - } else if (firstItemType === 'number') { - ret.x = options[0]; - } - i++; - } - while (j < valueCount) { - // Skip undefined positions for keys - if (!keys || options[i] !== undefined) { - if (pointArrayMap[j].indexOf('.') > 0) { - // Handle nested keys, e.g. ['color.pattern.image'] - // Avoid function call unless necessary. - H.Point.prototype.setNestedProperty( - ret, options[i], pointArrayMap[j] - ); - } else { - ret[pointArrayMap[j]] = options[i]; - } - } - i++; - j++; - } - } else if (typeof options === 'object') { - ret = options; - - // This is the fastest way to detect if there are individual point - // dataLabels that need to be considered in drawDataLabels. These - // can only occur in object configs. - if (options.dataLabels) { - series._hasPointLabels = true; - } - - // Same approach as above for markers - if (options.marker) { - series._hasPointMarkers = true; - } - } - return ret; - }, - - /** - * Get the CSS class names for individual points. Used internally where the - * returned value is set on every point. - * - * @returns {String} The class names. - */ - getClassName: function () { - return 'highcharts-point' + - (this.selected ? ' highcharts-point-select' : '') + - (this.negative ? ' highcharts-negative' : '') + - (this.isNull ? ' highcharts-null-point' : '') + - (this.colorIndex !== undefined ? ' highcharts-color-' + - this.colorIndex : '') + - (this.options.className ? ' ' + this.options.className : '') + - (this.zone && this.zone.className ? ' ' + - this.zone.className.replace('highcharts-negative', '') : ''); - }, - - /** - * In a series with `zones`, return the zone that the point belongs to. - * - * @return {Object} - * The zone item. - */ - getZone: function () { - var series = this.series, - zones = series.zones, - zoneAxis = series.zoneAxis || 'y', - i = 0, - zone; - - zone = zones[i]; - while (this[zoneAxis] >= zone.value) { - zone = zones[++i]; - } - - if (zone && zone.color && !this.options.color) { - this.color = zone.color; - } - - return zone; - }, - - /** - * Destroy a point to clear memory. Its reference still stays in - * `series.data`. - * - * @private - */ - destroy: function () { - var point = this, - series = point.series, - chart = series.chart, - hoverPoints = chart.hoverPoints, - prop; - - chart.pointCount--; - - if (hoverPoints) { - point.setState(); - erase(hoverPoints, point); - if (!hoverPoints.length) { - chart.hoverPoints = null; - } - - } - if (point === chart.hoverPoint) { - point.onMouseOut(); - } - - // Remove all events - if (point.graphic || point.dataLabel) { - removeEvent(point); - point.destroyElements(); - } - - if (point.legendItem) { // pies have legend items - chart.legend.destroyItem(point); - } - - for (prop in point) { - point[prop] = null; - } - - - }, - - /** - * Destroy SVG elements associated with the point. - * - * @private - */ - destroyElements: function () { - var point = this, - props = [ - 'graphic', - 'dataLabel', - 'dataLabelUpper', - 'connector', - 'shadowGroup' - ], - prop, - i = 6; - while (i--) { - prop = props[i]; - if (point[prop]) { - point[prop] = point[prop].destroy(); - } - } - }, - - /** - * Return the configuration hash needed for the data label and tooltip - * formatters. - * - * @returns {Object} - * Abstract object used in formatters and formats. - */ - getLabelConfig: function () { - return { - x: this.category, - y: this.y, - color: this.color, - colorIndex: this.colorIndex, - key: this.name || this.category, - series: this.series, - point: this, - percentage: this.percentage, - total: this.total || this.stackTotal - }; - }, - - /** - * Extendable method for formatting each point's tooltip line. - * - * @param {String} pointFormat - * The point format. - * @return {String} - * A string to be concatenated in to the common tooltip text. - */ - tooltipFormatter: function (pointFormat) { - - // Insert options for valueDecimals, valuePrefix, and valueSuffix - var series = this.series, - seriesTooltipOptions = series.tooltipOptions, - valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), - valuePrefix = seriesTooltipOptions.valuePrefix || '', - valueSuffix = seriesTooltipOptions.valueSuffix || ''; - - // Loop over the point array map and replace unformatted values with - // sprintf formatting markup - each(series.pointArrayMap || ['y'], function (key) { - key = '{point.' + key; // without the closing bracket - if (valuePrefix || valueSuffix) { - pointFormat = pointFormat.replace( - key + '}', - valuePrefix + key + '}' + valueSuffix - ); - } - pointFormat = pointFormat.replace( - key + '}', - key + ':,.' + valueDecimals + 'f}' - ); - }); - - return format(pointFormat, { - point: this, - series: this.series - }, series.chart.time); - }, - - /** - * Fire an event on the Point object. - * - * @private - * @param {String} eventType - * @param {Object} eventArgs Additional event arguments - * @param {Function} defaultFunction Default event handler - */ - firePointEvent: function (eventType, eventArgs, defaultFunction) { - var point = this, - series = this.series, - seriesOptions = series.options; - - // load event handlers on demand to save time on mouseover/out - if ( - seriesOptions.point.events[eventType] || - ( - point.options && - point.options.events && - point.options.events[eventType] - ) - ) { - this.importEvents(); - } - - // add default handler if in selection mode - if (eventType === 'click' && seriesOptions.allowPointSelect) { - defaultFunction = function (event) { - // Control key is for Windows, meta (= Cmd key) for Mac, Shift - // for Opera. - if (point.select) { // #2911 - point.select( - null, - event.ctrlKey || event.metaKey || event.shiftKey - ); - } - }; - } - - fireEvent(this, eventType, eventArgs, defaultFunction); - }, - - /** - * For certain series types, like pie charts, where individual points can - * be shown or hidden. - * - * @name visible - * @memberOf Highcharts.Point - * @type {Boolean} - */ - visible: true + /** + * Initialize the point. Called internally based on the `series.data` + * option. + * @param {Series} series + * The series object containing this point. + * @param {Number|Array|Object} options + * The data in either number, array or object format. + * @param {Number} x Optionally, the X value of the point. + * @return {Point} The Point instance. + */ + init: function (series, options, x) { + + var point = this, + colors, + colorCount = series.chart.options.chart.colorCount, + colorIndex; + + /** + * The series object associated with the point. + * + * @name series + * @memberof Highcharts.Point + * @type Highcharts.Series + */ + point.series = series; + + /*= if (build.classic) { =*/ + /** + * The point's current color. + * @name color + * @memberof Highcharts.Point + * @type {Color} + */ + point.color = series.color; // #3445 + /*= } =*/ + point.applyOptions(options, x); + + if (series.options.colorByPoint) { + /*= if (build.classic) { =*/ + colors = series.options.colors || series.chart.options.colors; + point.color = point.color || colors[series.colorCounter]; + colorCount = colors.length; + /*= } =*/ + colorIndex = series.colorCounter; + series.colorCounter++; + // loop back to zero + if (series.colorCounter === colorCount) { + series.colorCounter = 0; + } + } else { + colorIndex = series.colorIndex; + } + + /** + * The point's current color index, used in styled mode instead of + * `color`. The color index is inserted in class names used for styling. + * @name colorIndex + * @memberof Highcharts.Point + * @type {Number} + */ + point.colorIndex = pick(point.colorIndex, colorIndex); + + series.chart.pointCount++; + + fireEvent(point, 'afterInit'); + + return point; + }, + /** + * Apply the options containing the x and y data and possible some extra + * properties. Called on point init or from point.update. + * + * @private + * @param {Object} options The point options as defined in series.data. + * @param {Number} x Optionally, the X value. + * @returns {Object} The Point instance. + */ + applyOptions: function (options, x) { + var point = this, + series = point.series, + pointValKey = series.options.pointValKey || series.pointValKey; + + options = Point.prototype.optionsToObject.call(this, options); + + // copy options directly to point + extend(point, options); + point.options = point.options ? + extend(point.options, options) : + options; + + // Since options are copied into the Point instance, some accidental + // options must be shielded (#5681) + if (options.group) { + delete point.group; + } + + // For higher dimension series types. For instance, for ranges, point.y + // is mapped to point.low. + if (pointValKey) { + point.y = point[pointValKey]; + } + point.isNull = pick( + point.isValid && !point.isValid(), + point.x === null || !isNumber(point.y, true) + ); // #3571, check for NaN + + // The point is initially selected by options (#5777) + if (point.selected) { + point.state = 'select'; + } + + // If no x is set by now, get auto incremented value. All points must + // have an x value, however the y value can be null to create a gap in + // the series + if ( + 'name' in point && + x === undefined && + series.xAxis && + series.xAxis.hasNames + ) { + point.x = series.xAxis.nameToX(point); + } + if (point.x === undefined && series) { + if (x === undefined) { + point.x = series.autoIncrement(point); + } else { + point.x = x; + } + } + + return point; + }, + + /** + * Set a value in an object, on the property defined by key. The key + * supports nested properties using dot notation. The function modifies the + * input object and does not make a copy. + * + * @param {Object} object The object to set the value on. + * @param {Mixed} value The value to set. + * @param {String} key Key to the property to set. + * + * @return {Object} The modified object. + */ + setNestedProperty: function (object, value, key) { + var nestedKeys = key.split('.'); + H.reduce(nestedKeys, function (result, key, i, arr) { + var isLastKey = arr.length - 1 === i; + result[key] = ( + isLastKey ? + value : + (H.isObject(result[key], true) ? result[key] : {}) + ); + return result[key]; + }, object); + return object; + }, + + /** + * Transform number or array configs into objects. Used internally to unify + * the different configuration formats for points. For example, a simple + * number `10` in a line series will be transformed to `{ y: 10 }`, and an + * array config like `[1, 10]` in a scatter series will be transformed to + * `{ x: 1, y: 10 }`. + * + * @param {Number|Array|Object} options + * The input options + * @return {Object} Transformed options. + */ + optionsToObject: function (options) { + var ret = {}, + series = this.series, + keys = series.options.keys, + pointArrayMap = keys || series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + firstItemType, + i = 0, + j = 0; + + if (isNumber(options) || options === null) { + ret[pointArrayMap[0]] = options; + + } else if (isArray(options)) { + // with leading x value + if (!keys && options.length > valueCount) { + firstItemType = typeof options[0]; + if (firstItemType === 'string') { + ret.name = options[0]; + } else if (firstItemType === 'number') { + ret.x = options[0]; + } + i++; + } + while (j < valueCount) { + // Skip undefined positions for keys + if (!keys || options[i] !== undefined) { + if (pointArrayMap[j].indexOf('.') > 0) { + // Handle nested keys, e.g. ['color.pattern.image'] + // Avoid function call unless necessary. + H.Point.prototype.setNestedProperty( + ret, options[i], pointArrayMap[j] + ); + } else { + ret[pointArrayMap[j]] = options[i]; + } + } + i++; + j++; + } + } else if (typeof options === 'object') { + ret = options; + + // This is the fastest way to detect if there are individual point + // dataLabels that need to be considered in drawDataLabels. These + // can only occur in object configs. + if (options.dataLabels) { + series._hasPointLabels = true; + } + + // Same approach as above for markers + if (options.marker) { + series._hasPointMarkers = true; + } + } + return ret; + }, + + /** + * Get the CSS class names for individual points. Used internally where the + * returned value is set on every point. + * + * @returns {String} The class names. + */ + getClassName: function () { + return 'highcharts-point' + + (this.selected ? ' highcharts-point-select' : '') + + (this.negative ? ' highcharts-negative' : '') + + (this.isNull ? ' highcharts-null-point' : '') + + (this.colorIndex !== undefined ? ' highcharts-color-' + + this.colorIndex : '') + + (this.options.className ? ' ' + this.options.className : '') + + (this.zone && this.zone.className ? ' ' + + this.zone.className.replace('highcharts-negative', '') : ''); + }, + + /** + * In a series with `zones`, return the zone that the point belongs to. + * + * @return {Object} + * The zone item. + */ + getZone: function () { + var series = this.series, + zones = series.zones, + zoneAxis = series.zoneAxis || 'y', + i = 0, + zone; + + zone = zones[i]; + while (this[zoneAxis] >= zone.value) { + zone = zones[++i]; + } + + if (zone && zone.color && !this.options.color) { + this.color = zone.color; + } + + return zone; + }, + + /** + * Destroy a point to clear memory. Its reference still stays in + * `series.data`. + * + * @private + */ + destroy: function () { + var point = this, + series = point.series, + chart = series.chart, + hoverPoints = chart.hoverPoints, + prop; + + chart.pointCount--; + + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + if (!hoverPoints.length) { + chart.hoverPoints = null; + } + + } + if (point === chart.hoverPoint) { + point.onMouseOut(); + } + + // Remove all events + if (point.graphic || point.dataLabel) { + removeEvent(point); + point.destroyElements(); + } + + if (point.legendItem) { // pies have legend items + chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Destroy SVG elements associated with the point. + * + * @private + */ + destroyElements: function () { + var point = this, + props = [ + 'graphic', + 'dataLabel', + 'dataLabelUpper', + 'connector', + 'shadowGroup' + ], + prop, + i = 6; + while (i--) { + prop = props[i]; + if (point[prop]) { + point[prop] = point[prop].destroy(); + } + } + }, + + /** + * Return the configuration hash needed for the data label and tooltip + * formatters. + * + * @returns {Object} + * Abstract object used in formatters and formats. + */ + getLabelConfig: function () { + return { + x: this.category, + y: this.y, + color: this.color, + colorIndex: this.colorIndex, + key: this.name || this.category, + series: this.series, + point: this, + percentage: this.percentage, + total: this.total || this.stackTotal + }; + }, + + /** + * Extendable method for formatting each point's tooltip line. + * + * @param {String} pointFormat + * The point format. + * @return {String} + * A string to be concatenated in to the common tooltip text. + */ + tooltipFormatter: function (pointFormat) { + + // Insert options for valueDecimals, valuePrefix, and valueSuffix + var series = this.series, + seriesTooltipOptions = series.tooltipOptions, + valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), + valuePrefix = seriesTooltipOptions.valuePrefix || '', + valueSuffix = seriesTooltipOptions.valueSuffix || ''; + + // Loop over the point array map and replace unformatted values with + // sprintf formatting markup + each(series.pointArrayMap || ['y'], function (key) { + key = '{point.' + key; // without the closing bracket + if (valuePrefix || valueSuffix) { + pointFormat = pointFormat.replace( + key + '}', + valuePrefix + key + '}' + valueSuffix + ); + } + pointFormat = pointFormat.replace( + key + '}', + key + ':,.' + valueDecimals + 'f}' + ); + }); + + return format(pointFormat, { + point: this, + series: this.series + }, series.chart.time); + }, + + /** + * Fire an event on the Point object. + * + * @private + * @param {String} eventType + * @param {Object} eventArgs Additional event arguments + * @param {Function} defaultFunction Default event handler + */ + firePointEvent: function (eventType, eventArgs, defaultFunction) { + var point = this, + series = this.series, + seriesOptions = series.options; + + // load event handlers on demand to save time on mouseover/out + if ( + seriesOptions.point.events[eventType] || + ( + point.options && + point.options.events && + point.options.events[eventType] + ) + ) { + this.importEvents(); + } + + // add default handler if in selection mode + if (eventType === 'click' && seriesOptions.allowPointSelect) { + defaultFunction = function (event) { + // Control key is for Windows, meta (= Cmd key) for Mac, Shift + // for Opera. + if (point.select) { // #2911 + point.select( + null, + event.ctrlKey || event.metaKey || event.shiftKey + ); + } + }; + } + + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + + /** + * For certain series types, like pie charts, where individual points can + * be shown or hidden. + * + * @name visible + * @memberOf Highcharts.Point + * @type {Boolean} + */ + visible: true }; /** - * For categorized axes this property holds the category name for the + * For categorized axes this property holds the category name for the * point. For other axes it holds the X value. * * @name category @@ -493,7 +493,7 @@ Highcharts.Point.prototype = { */ /** - * The name of the point. The name can be given as the first position of the + * The name of the point. The name can be given as the first position of the * point configuration array, or as a `name` property in the configuration: * * @example @@ -505,8 +505,8 @@ Highcharts.Point.prototype = { * * // Object config * data: [{ - * name: 'John', - * y: 1 + * name: 'John', + * y: 1 * }, { * name: 'Jane', * y: 2 diff --git a/js/parts/Pointer.js b/js/parts/Pointer.js index 7c377a314b3..f6995bbb006 100644 --- a/js/parts/Pointer.js +++ b/js/parts/Pointer.js @@ -3,29 +3,29 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import Highcharts from './Globals.js'; import './Utilities.js'; import './Tooltip.js'; import './Color.js'; var H = Highcharts, - addEvent = H.addEvent, - attr = H.attr, - charts = H.charts, - color = H.color, - css = H.css, - defined = H.defined, - each = H.each, - extend = H.extend, - find = H.find, - fireEvent = H.fireEvent, - isNumber = H.isNumber, - isObject = H.isObject, - offset = H.offset, - pick = H.pick, - splat = H.splat, - Tooltip = H.Tooltip; + addEvent = H.addEvent, + attr = H.attr, + charts = H.charts, + color = H.color, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + find = H.find, + fireEvent = H.fireEvent, + isNumber = H.isNumber, + isObject = H.isObject, + offset = H.offset, + pick = H.pick, + splat = H.splat, + Tooltip = H.Tooltip; /** * The mouse and touch tracker object. Each {@link Chart} item has one @@ -40,1059 +40,1059 @@ var H = Highcharts, * tooltip structures. */ Highcharts.Pointer = function (chart, options) { - this.init(chart, options); + this.init(chart, options); }; Highcharts.Pointer.prototype = { - /** - * Initialize the Pointer. - * - * @private - */ - init: function (chart, options) { - - // Store references - this.options = options; - this.chart = chart; - - // Do we need to handle click on a touch device? - this.runChartClick = - options.chart.events && !!options.chart.events.click; - - this.pinchDown = []; - this.lastValidTouch = {}; - - if (Tooltip) { - chart.tooltip = new Tooltip(chart, options.tooltip); - this.followTouchMove = pick(options.tooltip.followTouchMove, true); - } - - this.setDOMEvents(); - }, - - /** - * Resolve the zoomType option, this is reset on all touch start and mouse - * down events. - * - * @private - */ - zoomOption: function (e) { - var chart = this.chart, - options = chart.options.chart, - zoomType = options.zoomType || '', - inverted = chart.inverted, - zoomX, - zoomY; - - // Look for the pinchType option - if (/touch/.test(e.type)) { - zoomType = pick(options.pinchType, zoomType); - } - - this.zoomX = zoomX = /x/.test(zoomType); - this.zoomY = zoomY = /y/.test(zoomType); - this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); - this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); - this.hasZoom = zoomX || zoomY; - }, - - /** - * @typedef {Object} PointerEvent - * A native browser mouse or touch event, extended with position - * information relative to the {@link Chart.container}. - * @property {Number} chartX - * The X coordinate of the pointer interaction relative to the - * chart. - * @property {Number} chartY - * The Y coordinate of the pointer interaction relative to the - * chart. - * - */ - /** - * Takes a browser event object and extends it with custom Highcharts - * properties `chartX` and `chartY` in order to work on the internal - * coordinate system. - * - * @param {Object} e - * The event object in standard browsers. - * - * @return {PointerEvent} - * A browser event with extended properties `chartX` and `chartY`. - */ - normalize: function (e, chartPosition) { - var ePos; - - // iOS (#2757) - ePos = e.touches ? - (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : - e; - - // Get mouse position - if (!chartPosition) { - this.chartPosition = chartPosition = offset(this.chart.container); - } - - return extend(e, { - chartX: Math.round(ePos.pageX - chartPosition.left), - chartY: Math.round(ePos.pageY - chartPosition.top) - }); - }, - - /** - * Get the click position in terms of axis values. - * - * @param {PointerEvent} e - * A pointer event, extended with `chartX` and `chartY` - * properties. - */ - getCoordinates: function (e) { - var coordinates = { - xAxis: [], - yAxis: [] - }; - - each(this.chart.axes, function (axis) { - coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) - }); - }); - return coordinates; - }, - /** - * Finds the closest point to a set of coordinates, using the k-d-tree - * algorithm. - * - * @param {Array.} series - * All the series to search in. - * @param {boolean} shared - * Whether it is a shared tooltip or not. - * @param {object} coordinates - * Chart coordinates of the pointer. - * @param {number} coordinates.chartX - * @param {number} coordinates.chartY - * - * @return {Point|undefined} The point closest to given coordinates. - */ - findNearestKDPoint: function (series, shared, coordinates) { - var closest, - sort = function (p1, p2) { - var isCloserX = p1.distX - p2.distX, - isCloser = p1.dist - p2.dist, - isAbove = - (p2.series.group && p2.series.group.zIndex) - - (p1.series.group && p1.series.group.zIndex), - result; - - // We have two points which are not in the same place on xAxis - // and shared tooltip: - if (isCloserX !== 0 && shared) { // #5721 - result = isCloserX; - // Points are not exactly in the same place on x/yAxis: - } else if (isCloser !== 0) { - result = isCloser; - // The same xAxis and yAxis position, sort by z-index: - } else if (isAbove !== 0) { - result = isAbove; - // The same zIndex, sort by array index: - } else { - result = p1.series.index > p2.series.index ? -1 : 1; - } - return result; - }; - each(series, function (s) { - var noSharedTooltip = s.noSharedTooltip && shared, - compareX = ( - !noSharedTooltip && - s.options.findNearestPointBy.indexOf('y') < 0 - ), - point = s.searchPoint( - coordinates, - compareX - ); - if ( - // Check that we actually found a point on the series. - isObject(point, true) && - // Use the new point if it is closer. - (!isObject(closest, true) || (sort(closest, point) > 0)) - ) { - closest = point; - } - }); - return closest; - }, - getPointFromEvent: function (e) { - var target = e.target, - point; - - while (target && !point) { - point = target.point; - target = target.parentNode; - } - return point; - }, - - getChartCoordinatesFromPoint: function (point, inverted) { - var series = point.series, - xAxis = series.xAxis, - yAxis = series.yAxis, - plotX = pick(point.clientX, point.plotX), - shapeArgs = point.shapeArgs; - - if (xAxis && yAxis) { - return inverted ? { - chartX: xAxis.len + xAxis.pos - plotX, - chartY: yAxis.len + yAxis.pos - point.plotY - } : { - chartX: plotX + xAxis.pos, - chartY: point.plotY + yAxis.pos - }; - } else if (shapeArgs && shapeArgs.x && shapeArgs.y) { - // E.g. pies do not have axes - return { - chartX: shapeArgs.x, - chartY: shapeArgs.y - }; - } - }, - - /** - * Calculates what is the current hovered point/points and series. - * - * @private - * - * @param {undefined|Point} existingHoverPoint - * The point currrently beeing hovered. - * @param {undefined|Series} existingHoverSeries - * The series currently beeing hovered. - * @param {Array.} series - * All the series in the chart. - * @param {boolean} isDirectTouch - * Is the pointer directly hovering the point. - * @param {boolean} shared - * Whether it is a shared tooltip or not. - * @param {object} coordinates - * Chart coordinates of the pointer. - * @param {number} coordinates.chartX - * @param {number} coordinates.chartY - * - * @return {object} - * Object containing resulting hover data. - */ - getHoverData: function ( - existingHoverPoint, - existingHoverSeries, - series, - isDirectTouch, - shared, - coordinates, - params - ) { - var hoverPoint, - hoverPoints = [], - hoverSeries = existingHoverSeries, - isBoosting = params && params.isBoosting, - useExisting = !!(isDirectTouch && existingHoverPoint), - notSticky = hoverSeries && !hoverSeries.stickyTracking, - filter = function (s) { - return ( - s.visible && - !(!shared && s.directTouch) && // #3821 - pick(s.options.enableMouseTracking, true) - ); - }, - // Which series to look in for the hover point - searchSeries = notSticky ? - // Only search on hovered series if it has stickyTracking false - [hoverSeries] : - // Filter what series to look in. - H.grep(series, function (s) { - return filter(s) && s.stickyTracking; - }); - - // Use existing hovered point or find the one closest to coordinates. - hoverPoint = useExisting ? - existingHoverPoint : - this.findNearestKDPoint(searchSeries, shared, coordinates); - - // Assign hover series - hoverSeries = hoverPoint && hoverPoint.series; - - // If we have a hoverPoint, assign hoverPoints. - if (hoverPoint) { - // When tooltip is shared, it displays more than one point - if (shared && !hoverSeries.noSharedTooltip) { - searchSeries = H.grep(series, function (s) { - return filter(s) && !s.noSharedTooltip; - }); - - // Get all points with the same x value as the hoverPoint - each(searchSeries, function (s) { - var point = find(s.points, function (p) { - return p.x === hoverPoint.x && !p.isNull; - }); - if (isObject(point)) { - /* - * Boost returns a minimal point. Convert it to a usable - * point for tooltip and states. - */ - if (isBoosting) { - point = s.getPoint(point); - } - hoverPoints.push(point); - } - }); - } else { - hoverPoints.push(hoverPoint); - } - } - return { - hoverPoint: hoverPoint, - hoverSeries: hoverSeries, - hoverPoints: hoverPoints - }; - }, - /** - * With line type charts with a single tracker, get the point closest to the - * mouse. Run Point.onMouseOver and display tooltip for the point or points. - * - * @private - */ - runPointActions: function (e, p) { - var pointer = this, - chart = pointer.chart, - series = chart.series, - tooltip = chart.tooltip && chart.tooltip.options.enabled ? - chart.tooltip : - undefined, - shared = tooltip ? tooltip.shared : false, - hoverPoint = p || chart.hoverPoint, - hoverSeries = hoverPoint && hoverPoint.series || chart.hoverSeries, - // onMouseOver or already hovering a series with directTouch - isDirectTouch = !!p || ( - (hoverSeries && hoverSeries.directTouch) && - pointer.isDirectTouch - ), - hoverData = this.getHoverData( - hoverPoint, - hoverSeries, - series, - isDirectTouch, - shared, - e, - { isBoosting: chart.isBoosting } - ), - useSharedTooltip, - followPointer, - anchor, - points; - - // Update variables from hoverData. - hoverPoint = hoverData.hoverPoint; - points = hoverData.hoverPoints; - hoverSeries = hoverData.hoverSeries; - followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; - useSharedTooltip = ( - shared && - hoverSeries && - !hoverSeries.noSharedTooltip - ); - - // Refresh tooltip for kdpoint if new hover point or tooltip was hidden - // #3926, #4200 - if ( - hoverPoint && - // !(hoverSeries && hoverSeries.directTouch) && - (hoverPoint !== chart.hoverPoint || (tooltip && tooltip.isHidden)) - ) { - each(chart.hoverPoints || [], function (p) { - if (H.inArray(p, points) === -1) { - p.setState(); - } - }); - // Do mouseover on all points (#3919, #3985, #4410, #5622) - each(points || [], function (p) { - p.setState('hover'); - }); - // set normal state to previous series - if (chart.hoverSeries !== hoverSeries) { - hoverSeries.onMouseOver(); - } - - // If tracking is on series in stead of on each point, - // fire mouseOver on hover point. // #4448 - if (chart.hoverPoint) { - chart.hoverPoint.firePointEvent('mouseOut'); - } - - // Hover point may have been destroyed in the event handlers (#7127) - if (!hoverPoint.series) { - return; - } - - hoverPoint.firePointEvent('mouseOver'); - chart.hoverPoints = points; - chart.hoverPoint = hoverPoint; - // Draw tooltip if necessary - if (tooltip) { - tooltip.refresh(useSharedTooltip ? points : hoverPoint, e); - } - // Update positions (regardless of kdpoint or hoverPoint) - } else if (followPointer && tooltip && !tooltip.isHidden) { - anchor = tooltip.getAnchor([{}], e); - tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); - } - - // Start the event listener to pick up the tooltip and crosshairs - if (!pointer.unDocMouseMove) { - pointer.unDocMouseMove = addEvent( - chart.container.ownerDocument, - 'mousemove', - function (e) { - var chart = charts[H.hoverChartIndex]; - if (chart) { - chart.pointer.onDocumentMouseMove(e); - } - } - ); - } - - // Issues related to crosshair #4927, #5269 #5066, #5658 - each(chart.axes, function drawAxisCrosshair(axis) { - var snap = pick(axis.crosshair.snap, true), - point = !snap ? - undefined : - H.find(points, function (p) { - return p.series[axis.coll] === axis; - }); - - // Axis has snapping crosshairs, and one of the hover points belongs - // to axis. Always call drawCrosshair when it is not snap. - if (point || !snap) { - axis.drawCrosshair(e, point); - // Axis has snapping crosshairs, but no hover point belongs to axis - } else { - axis.hideCrosshair(); - } - }); - }, - - /** - * Reset the tracking by hiding the tooltip, the hover series state and the - * hover point - * - * @param allowMove {Boolean} - * Instead of destroying the tooltip altogether, allow moving it if - * possible. - */ - reset: function (allowMove, delay) { - var pointer = this, - chart = pointer.chart, - hoverSeries = chart.hoverSeries, - hoverPoint = chart.hoverPoint, - hoverPoints = chart.hoverPoints, - tooltip = chart.tooltip, - tooltipPoints = tooltip && tooltip.shared ? - hoverPoints : - hoverPoint; - - // Check if the points have moved outside the plot area (#1003, #4736, - // #5101) - if (allowMove && tooltipPoints) { - each(splat(tooltipPoints), function (point) { - if (point.series.isCartesian && point.plotX === undefined) { - allowMove = false; - } - }); - } - - // Just move the tooltip, #349 - if (allowMove) { - if (tooltip && tooltipPoints) { - tooltip.refresh(tooltipPoints); - if (hoverPoint) { // #2500 - hoverPoint.setState(hoverPoint.state, true); - each(chart.axes, function (axis) { - if (axis.crosshair) { - axis.drawCrosshair(null, hoverPoint); - } - }); - } - } - - // Full reset - } else { - - if (hoverPoint) { - hoverPoint.onMouseOut(); - } - - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } - - if (hoverSeries) { - hoverSeries.onMouseOut(); - } - - if (tooltip) { - tooltip.hide(delay); - } - - if (pointer.unDocMouseMove) { - pointer.unDocMouseMove = pointer.unDocMouseMove(); - } - - // Remove crosshairs - each(chart.axes, function (axis) { - axis.hideCrosshair(); - }); - - pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; - } - }, - - /** - * Scale series groups to a certain scale and translation. - * - * @private - */ - scaleGroups: function (attribs, clip) { - - var chart = this.chart, - seriesAttribs; - - // Scale each series - each(chart.series, function (series) { - seriesAttribs = attribs || series.getPlotBox(); // #1701 - if (series.xAxis && series.xAxis.zoomEnabled && series.group) { - series.group.attr(seriesAttribs); - if (series.markerGroup) { - series.markerGroup.attr(seriesAttribs); - series.markerGroup.clip(clip ? chart.clipRect : null); - } - if (series.dataLabelsGroup) { - series.dataLabelsGroup.attr(seriesAttribs); - } - } - }); - - // Clip - chart.clipRect.attr(clip || chart.clipBox); - }, - - /** - * Start a drag operation. - * - * @private - */ - dragStart: function (e) { - var chart = this.chart; - - // Record the start position - chart.mouseIsDown = e.type; - chart.cancelClick = false; - chart.mouseDownX = this.mouseDownX = e.chartX; - chart.mouseDownY = this.mouseDownY = e.chartY; - }, - - /** - * Perform a drag operation in response to a mousemove event while the mouse - * is down. - * - * @private - */ - drag: function (e) { - - var chart = this.chart, - chartOptions = chart.options.chart, - chartX = e.chartX, - chartY = e.chartY, - zoomHor = this.zoomHor, - zoomVert = this.zoomVert, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - clickedInside, - size, - selectionMarker = this.selectionMarker, - mouseDownX = this.mouseDownX, - mouseDownY = this.mouseDownY, - panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; - - // If the device supports both touch and mouse (like IE11), and we are - // touch-dragging inside the plot area, don't handle the mouse event. - // #4339. - if (selectionMarker && selectionMarker.touch) { - return; - } - - // If the mouse is outside the plot area, adjust to cooordinates - // inside to prevent the selection marker from going outside - if (chartX < plotLeft) { - chartX = plotLeft; - } else if (chartX > plotLeft + plotWidth) { - chartX = plotLeft + plotWidth; - } - - if (chartY < plotTop) { - chartY = plotTop; - } else if (chartY > plotTop + plotHeight) { - chartY = plotTop + plotHeight; - } - - // determine if the mouse has moved more than 10px - this.hasDragged = Math.sqrt( - Math.pow(mouseDownX - chartX, 2) + - Math.pow(mouseDownY - chartY, 2) - ); - - if (this.hasDragged > 10) { - clickedInside = chart.isInsidePlot( - mouseDownX - plotLeft, - mouseDownY - plotTop - ); - - // make a selection - if ( - chart.hasCartesianSeries && - (this.zoomX || this.zoomY) && - clickedInside && - !panKey - ) { - if (!selectionMarker) { - this.selectionMarker = selectionMarker = - chart.renderer.rect( - plotLeft, - plotTop, - zoomHor ? 1 : plotWidth, - zoomVert ? 1 : plotHeight, - 0 - ) - .attr({ - /*= if (build.classic) { =*/ - fill: ( - chartOptions.selectionMarkerFill || - color('${palette.highlightColor80}') - .setOpacity(0.25).get() - ), - /*= } =*/ - 'class': 'highcharts-selection-marker', - 'zIndex': 7 - }) - .add(); - } - } - - // adjust the width of the selection marker - if (selectionMarker && zoomHor) { - size = chartX - mouseDownX; - selectionMarker.attr({ - width: Math.abs(size), - x: (size > 0 ? 0 : size) + mouseDownX - }); - } - // adjust the height of the selection marker - if (selectionMarker && zoomVert) { - size = chartY - mouseDownY; - selectionMarker.attr({ - height: Math.abs(size), - y: (size > 0 ? 0 : size) + mouseDownY - }); - } - - // panning - if (clickedInside && !selectionMarker && chartOptions.panning) { - chart.pan(e, chartOptions.panning); - } - } - }, - - /** - * On mouse up or touch end across the entire document, drop the selection. - * - * @private - */ - drop: function (e) { - var pointer = this, - chart = this.chart, - hasPinched = this.hasPinched; - - if (this.selectionMarker) { - var selectionData = { - originalEvent: e, // #4890 - xAxis: [], - yAxis: [] - }, - selectionBox = this.selectionMarker, - selectionLeft = selectionBox.attr ? - selectionBox.attr('x') : - selectionBox.x, - selectionTop = selectionBox.attr ? - selectionBox.attr('y') : - selectionBox.y, - selectionWidth = selectionBox.attr ? - selectionBox.attr('width') : - selectionBox.width, - selectionHeight = selectionBox.attr ? - selectionBox.attr('height') : - selectionBox.height, - runZoom; - - // a selection has been made - if (this.hasDragged || hasPinched) { - - // record each axis' min and max - each(chart.axes, function (axis) { - if ( - axis.zoomEnabled && - defined(axis.min) && - ( - hasPinched || - pointer[{ - xAxis: 'zoomX', - yAxis: 'zoomY' - }[axis.coll]] - ) - ) { // #859, #3569 - var horiz = axis.horiz, - minPixelPadding = e.type === 'touchend' ? - axis.minPixelPadding : - 0, // #1207, #3075 - selectionMin = axis.toValue( - (horiz ? selectionLeft : selectionTop) + - minPixelPadding - ), - selectionMax = axis.toValue( - ( - horiz ? - selectionLeft + selectionWidth : - selectionTop + selectionHeight - ) - minPixelPadding - ); - - selectionData[axis.coll].push({ - axis: axis, - // Min/max for reversed axes - min: Math.min(selectionMin, selectionMax), - max: Math.max(selectionMin, selectionMax) - }); - runZoom = true; - } - }); - if (runZoom) { - fireEvent( - chart, - 'selection', - selectionData, - function (args) { - chart.zoom( - extend( - args, - hasPinched ? { animation: false } : null - ) - ); - } - ); - } - - } - - if (isNumber(chart.index)) { - this.selectionMarker = this.selectionMarker.destroy(); - } - - // Reset scaling preview - if (hasPinched) { - this.scaleGroups(); - } - } - - // Reset all. Check isNumber because it may be destroyed on mouse up - // (#877) - if (chart && isNumber(chart.index)) { - css(chart.container, { cursor: chart._cursor }); - chart.cancelClick = this.hasDragged > 10; // #370 - chart.mouseIsDown = this.hasDragged = this.hasPinched = false; - this.pinchDown = []; - } - }, - - onContainerMouseDown: function (e) { - // Normalize before the 'if' for the legacy IE (#7850) - e = this.normalize(e); - - if (e.button !== 2) { - - this.zoomOption(e); - - // issue #295, dragging not always working in Firefox - if (e.preventDefault) { - e.preventDefault(); - } - - this.dragStart(e); - } - }, - - - - onDocumentMouseUp: function (e) { - if (charts[H.hoverChartIndex]) { - charts[H.hoverChartIndex].pointer.drop(e); - } - }, - - /** - * Special handler for mouse move that will hide the tooltip when the mouse - * leaves the plotarea. Issue #149 workaround. The mouseleave event does not - * always fire. - * - * @private - */ - onDocumentMouseMove: function (e) { - var chart = this.chart, - chartPosition = this.chartPosition; - - e = this.normalize(e, chartPosition); - - // If we're outside, hide the tooltip - if ( - chartPosition && - !this.inClass(e.target, 'highcharts-tracker') && - !chart.isInsidePlot( - e.chartX - chart.plotLeft, - e.chartY - chart.plotTop - ) - ) { - this.reset(); - } - }, - - /** - * When mouse leaves the container, hide the tooltip. - * - * @private - */ - onContainerMouseLeave: function (e) { - var chart = charts[H.hoverChartIndex]; - // #4886, MS Touch end fires mouseleave but with no related target - if (chart && (e.relatedTarget || e.toElement)) { - chart.pointer.reset(); - // Also reset the chart position, used in #149 fix - chart.pointer.chartPosition = null; - } - }, - - // The mousemove, touchmove and touchstart event handler - onContainerMouseMove: function (e) { - - var chart = this.chart; - - if ( - !defined(H.hoverChartIndex) || - !charts[H.hoverChartIndex] || - !charts[H.hoverChartIndex].mouseIsDown - ) { - H.hoverChartIndex = chart.index; - } - - e = this.normalize(e); - e.returnValue = false; // #2251, #3224 - - if (chart.mouseIsDown === 'mousedown') { - this.drag(e); - } - - // Show the tooltip and run mouse over events (#977) - if ( - ( - this.inClass(e.target, 'highcharts-tracker') || - chart.isInsidePlot( - e.chartX - chart.plotLeft, - e.chartY - chart.plotTop - ) - ) && - !chart.openMenu - ) { - this.runPointActions(e); - } - }, - - /** - * Utility to detect whether an element has, or has a parent with, a - * specificclass name. Used on detection of tracker objects and on deciding - * whether hovering the tooltip should cause the active series to mouse out. - * - * @param {SVGDOMElement|HTMLDOMElement} element - * The element to investigate. - * @param {String} className - * The class name to look for. - * - * @return {Boolean} - * True if either the element or one of its parents has the given - * class name. - */ - inClass: function (element, className) { - var elemClassName; - while (element) { - elemClassName = attr(element, 'class'); - if (elemClassName) { - if (elemClassName.indexOf(className) !== -1) { - return true; - } - if (elemClassName.indexOf('highcharts-container') !== -1) { - return false; - } - } - element = element.parentNode; - } - }, - - onTrackerMouseOut: function (e) { - var series = this.chart.hoverSeries, - relatedTarget = e.relatedTarget || e.toElement; - - this.isDirectTouch = false; - - if ( - series && - relatedTarget && - !series.stickyTracking && - !this.inClass(relatedTarget, 'highcharts-tooltip') && - ( - !this.inClass( - relatedTarget, - 'highcharts-series-' + series.index - ) || // #2499, #4465 - !this.inClass(relatedTarget, 'highcharts-tracker') // #5553 - ) - ) { - series.onMouseOut(); - } - }, - - onContainerClick: function (e) { - var chart = this.chart, - hoverPoint = chart.hoverPoint, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop; - - e = this.normalize(e); - - if (!chart.cancelClick) { - - // On tracker click, fire the series and point events. #783, #1583 - if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) { - - // the series click event - fireEvent(hoverPoint.series, 'click', extend(e, { - point: hoverPoint - })); - - // the point click event - if (chart.hoverPoint) { // it may be destroyed (#1844) - hoverPoint.firePointEvent('click', e); - } - - // When clicking outside a tracker, fire a chart event - } else { - extend(e, this.getCoordinates(e)); - - // fire a click event in the chart - if ( - chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop) - ) { - fireEvent(chart, 'click', e); - } - } - - - } - }, - - /** - * Set the JS DOM events on the container and document. This method should - * contain a one-to-one assignment between methods and their handlers. Any - * advanced logic should be moved to the handler reflecting the event's - * name. - * - * @private - */ - setDOMEvents: function () { - - var pointer = this, - container = pointer.chart.container, - ownerDoc = container.ownerDocument; - - container.onmousedown = function (e) { - pointer.onContainerMouseDown(e); - }; - container.onmousemove = function (e) { - pointer.onContainerMouseMove(e); - }; - container.onclick = function (e) { - pointer.onContainerClick(e); - }; - this.unbindContainerMouseLeave = addEvent( - container, - 'mouseleave', - pointer.onContainerMouseLeave - ); - if (!H.unbindDocumentMouseUp) { - H.unbindDocumentMouseUp = addEvent( - ownerDoc, - 'mouseup', - pointer.onDocumentMouseUp - ); - } - if (H.hasTouch) { - container.ontouchstart = function (e) { - pointer.onContainerTouchStart(e); - }; - container.ontouchmove = function (e) { - pointer.onContainerTouchMove(e); - }; - if (!H.unbindDocumentTouchEnd) { - H.unbindDocumentTouchEnd = addEvent( - ownerDoc, - 'touchend', - pointer.onDocumentTouchEnd - ); - } - } - - }, - - /** - * Destroys the Pointer object and disconnects DOM events. - */ - destroy: function () { - var pointer = this; - - if (pointer.unDocMouseMove) { - pointer.unDocMouseMove(); - } - - this.unbindContainerMouseLeave(); - - if (!H.chartCount) { - if (H.unbindDocumentMouseUp) { - H.unbindDocumentMouseUp = H.unbindDocumentMouseUp(); - } - if (H.unbindDocumentTouchEnd) { - H.unbindDocumentTouchEnd = H.unbindDocumentTouchEnd(); - } - } - - // memory and CPU leak - clearInterval(pointer.tooltipTimeout); - - H.objectEach(pointer, function (val, prop) { - pointer[prop] = null; - }); - } + /** + * Initialize the Pointer. + * + * @private + */ + init: function (chart, options) { + + // Store references + this.options = options; + this.chart = chart; + + // Do we need to handle click on a touch device? + this.runChartClick = + options.chart.events && !!options.chart.events.click; + + this.pinchDown = []; + this.lastValidTouch = {}; + + if (Tooltip) { + chart.tooltip = new Tooltip(chart, options.tooltip); + this.followTouchMove = pick(options.tooltip.followTouchMove, true); + } + + this.setDOMEvents(); + }, + + /** + * Resolve the zoomType option, this is reset on all touch start and mouse + * down events. + * + * @private + */ + zoomOption: function (e) { + var chart = this.chart, + options = chart.options.chart, + zoomType = options.zoomType || '', + inverted = chart.inverted, + zoomX, + zoomY; + + // Look for the pinchType option + if (/touch/.test(e.type)) { + zoomType = pick(options.pinchType, zoomType); + } + + this.zoomX = zoomX = /x/.test(zoomType); + this.zoomY = zoomY = /y/.test(zoomType); + this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); + this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); + this.hasZoom = zoomX || zoomY; + }, + + /** + * @typedef {Object} PointerEvent + * A native browser mouse or touch event, extended with position + * information relative to the {@link Chart.container}. + * @property {Number} chartX + * The X coordinate of the pointer interaction relative to the + * chart. + * @property {Number} chartY + * The Y coordinate of the pointer interaction relative to the + * chart. + * + */ + /** + * Takes a browser event object and extends it with custom Highcharts + * properties `chartX` and `chartY` in order to work on the internal + * coordinate system. + * + * @param {Object} e + * The event object in standard browsers. + * + * @return {PointerEvent} + * A browser event with extended properties `chartX` and `chartY`. + */ + normalize: function (e, chartPosition) { + var ePos; + + // iOS (#2757) + ePos = e.touches ? + (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : + e; + + // Get mouse position + if (!chartPosition) { + this.chartPosition = chartPosition = offset(this.chart.container); + } + + return extend(e, { + chartX: Math.round(ePos.pageX - chartPosition.left), + chartY: Math.round(ePos.pageY - chartPosition.top) + }); + }, + + /** + * Get the click position in terms of axis values. + * + * @param {PointerEvent} e + * A pointer event, extended with `chartX` and `chartY` + * properties. + */ + getCoordinates: function (e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; + + each(this.chart.axes, function (axis) { + coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) + }); + }); + return coordinates; + }, + /** + * Finds the closest point to a set of coordinates, using the k-d-tree + * algorithm. + * + * @param {Array.} series + * All the series to search in. + * @param {boolean} shared + * Whether it is a shared tooltip or not. + * @param {object} coordinates + * Chart coordinates of the pointer. + * @param {number} coordinates.chartX + * @param {number} coordinates.chartY + * + * @return {Point|undefined} The point closest to given coordinates. + */ + findNearestKDPoint: function (series, shared, coordinates) { + var closest, + sort = function (p1, p2) { + var isCloserX = p1.distX - p2.distX, + isCloser = p1.dist - p2.dist, + isAbove = + (p2.series.group && p2.series.group.zIndex) - + (p1.series.group && p1.series.group.zIndex), + result; + + // We have two points which are not in the same place on xAxis + // and shared tooltip: + if (isCloserX !== 0 && shared) { // #5721 + result = isCloserX; + // Points are not exactly in the same place on x/yAxis: + } else if (isCloser !== 0) { + result = isCloser; + // The same xAxis and yAxis position, sort by z-index: + } else if (isAbove !== 0) { + result = isAbove; + // The same zIndex, sort by array index: + } else { + result = p1.series.index > p2.series.index ? -1 : 1; + } + return result; + }; + each(series, function (s) { + var noSharedTooltip = s.noSharedTooltip && shared, + compareX = ( + !noSharedTooltip && + s.options.findNearestPointBy.indexOf('y') < 0 + ), + point = s.searchPoint( + coordinates, + compareX + ); + if ( + // Check that we actually found a point on the series. + isObject(point, true) && + // Use the new point if it is closer. + (!isObject(closest, true) || (sort(closest, point) > 0)) + ) { + closest = point; + } + }); + return closest; + }, + getPointFromEvent: function (e) { + var target = e.target, + point; + + while (target && !point) { + point = target.point; + target = target.parentNode; + } + return point; + }, + + getChartCoordinatesFromPoint: function (point, inverted) { + var series = point.series, + xAxis = series.xAxis, + yAxis = series.yAxis, + plotX = pick(point.clientX, point.plotX), + shapeArgs = point.shapeArgs; + + if (xAxis && yAxis) { + return inverted ? { + chartX: xAxis.len + xAxis.pos - plotX, + chartY: yAxis.len + yAxis.pos - point.plotY + } : { + chartX: plotX + xAxis.pos, + chartY: point.plotY + yAxis.pos + }; + } else if (shapeArgs && shapeArgs.x && shapeArgs.y) { + // E.g. pies do not have axes + return { + chartX: shapeArgs.x, + chartY: shapeArgs.y + }; + } + }, + + /** + * Calculates what is the current hovered point/points and series. + * + * @private + * + * @param {undefined|Point} existingHoverPoint + * The point currrently beeing hovered. + * @param {undefined|Series} existingHoverSeries + * The series currently beeing hovered. + * @param {Array.} series + * All the series in the chart. + * @param {boolean} isDirectTouch + * Is the pointer directly hovering the point. + * @param {boolean} shared + * Whether it is a shared tooltip or not. + * @param {object} coordinates + * Chart coordinates of the pointer. + * @param {number} coordinates.chartX + * @param {number} coordinates.chartY + * + * @return {object} + * Object containing resulting hover data. + */ + getHoverData: function ( + existingHoverPoint, + existingHoverSeries, + series, + isDirectTouch, + shared, + coordinates, + params + ) { + var hoverPoint, + hoverPoints = [], + hoverSeries = existingHoverSeries, + isBoosting = params && params.isBoosting, + useExisting = !!(isDirectTouch && existingHoverPoint), + notSticky = hoverSeries && !hoverSeries.stickyTracking, + filter = function (s) { + return ( + s.visible && + !(!shared && s.directTouch) && // #3821 + pick(s.options.enableMouseTracking, true) + ); + }, + // Which series to look in for the hover point + searchSeries = notSticky ? + // Only search on hovered series if it has stickyTracking false + [hoverSeries] : + // Filter what series to look in. + H.grep(series, function (s) { + return filter(s) && s.stickyTracking; + }); + + // Use existing hovered point or find the one closest to coordinates. + hoverPoint = useExisting ? + existingHoverPoint : + this.findNearestKDPoint(searchSeries, shared, coordinates); + + // Assign hover series + hoverSeries = hoverPoint && hoverPoint.series; + + // If we have a hoverPoint, assign hoverPoints. + if (hoverPoint) { + // When tooltip is shared, it displays more than one point + if (shared && !hoverSeries.noSharedTooltip) { + searchSeries = H.grep(series, function (s) { + return filter(s) && !s.noSharedTooltip; + }); + + // Get all points with the same x value as the hoverPoint + each(searchSeries, function (s) { + var point = find(s.points, function (p) { + return p.x === hoverPoint.x && !p.isNull; + }); + if (isObject(point)) { + /* + * Boost returns a minimal point. Convert it to a usable + * point for tooltip and states. + */ + if (isBoosting) { + point = s.getPoint(point); + } + hoverPoints.push(point); + } + }); + } else { + hoverPoints.push(hoverPoint); + } + } + return { + hoverPoint: hoverPoint, + hoverSeries: hoverSeries, + hoverPoints: hoverPoints + }; + }, + /** + * With line type charts with a single tracker, get the point closest to the + * mouse. Run Point.onMouseOver and display tooltip for the point or points. + * + * @private + */ + runPointActions: function (e, p) { + var pointer = this, + chart = pointer.chart, + series = chart.series, + tooltip = chart.tooltip && chart.tooltip.options.enabled ? + chart.tooltip : + undefined, + shared = tooltip ? tooltip.shared : false, + hoverPoint = p || chart.hoverPoint, + hoverSeries = hoverPoint && hoverPoint.series || chart.hoverSeries, + // onMouseOver or already hovering a series with directTouch + isDirectTouch = !!p || ( + (hoverSeries && hoverSeries.directTouch) && + pointer.isDirectTouch + ), + hoverData = this.getHoverData( + hoverPoint, + hoverSeries, + series, + isDirectTouch, + shared, + e, + { isBoosting: chart.isBoosting } + ), + useSharedTooltip, + followPointer, + anchor, + points; + + // Update variables from hoverData. + hoverPoint = hoverData.hoverPoint; + points = hoverData.hoverPoints; + hoverSeries = hoverData.hoverSeries; + followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; + useSharedTooltip = ( + shared && + hoverSeries && + !hoverSeries.noSharedTooltip + ); + + // Refresh tooltip for kdpoint if new hover point or tooltip was hidden + // #3926, #4200 + if ( + hoverPoint && + // !(hoverSeries && hoverSeries.directTouch) && + (hoverPoint !== chart.hoverPoint || (tooltip && tooltip.isHidden)) + ) { + each(chart.hoverPoints || [], function (p) { + if (H.inArray(p, points) === -1) { + p.setState(); + } + }); + // Do mouseover on all points (#3919, #3985, #4410, #5622) + each(points || [], function (p) { + p.setState('hover'); + }); + // set normal state to previous series + if (chart.hoverSeries !== hoverSeries) { + hoverSeries.onMouseOver(); + } + + // If tracking is on series in stead of on each point, + // fire mouseOver on hover point. // #4448 + if (chart.hoverPoint) { + chart.hoverPoint.firePointEvent('mouseOut'); + } + + // Hover point may have been destroyed in the event handlers (#7127) + if (!hoverPoint.series) { + return; + } + + hoverPoint.firePointEvent('mouseOver'); + chart.hoverPoints = points; + chart.hoverPoint = hoverPoint; + // Draw tooltip if necessary + if (tooltip) { + tooltip.refresh(useSharedTooltip ? points : hoverPoint, e); + } + // Update positions (regardless of kdpoint or hoverPoint) + } else if (followPointer && tooltip && !tooltip.isHidden) { + anchor = tooltip.getAnchor([{}], e); + tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); + } + + // Start the event listener to pick up the tooltip and crosshairs + if (!pointer.unDocMouseMove) { + pointer.unDocMouseMove = addEvent( + chart.container.ownerDocument, + 'mousemove', + function (e) { + var chart = charts[H.hoverChartIndex]; + if (chart) { + chart.pointer.onDocumentMouseMove(e); + } + } + ); + } + + // Issues related to crosshair #4927, #5269 #5066, #5658 + each(chart.axes, function drawAxisCrosshair(axis) { + var snap = pick(axis.crosshair.snap, true), + point = !snap ? + undefined : + H.find(points, function (p) { + return p.series[axis.coll] === axis; + }); + + // Axis has snapping crosshairs, and one of the hover points belongs + // to axis. Always call drawCrosshair when it is not snap. + if (point || !snap) { + axis.drawCrosshair(e, point); + // Axis has snapping crosshairs, but no hover point belongs to axis + } else { + axis.hideCrosshair(); + } + }); + }, + + /** + * Reset the tracking by hiding the tooltip, the hover series state and the + * hover point + * + * @param allowMove {Boolean} + * Instead of destroying the tooltip altogether, allow moving it if + * possible. + */ + reset: function (allowMove, delay) { + var pointer = this, + chart = pointer.chart, + hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint, + hoverPoints = chart.hoverPoints, + tooltip = chart.tooltip, + tooltipPoints = tooltip && tooltip.shared ? + hoverPoints : + hoverPoint; + + // Check if the points have moved outside the plot area (#1003, #4736, + // #5101) + if (allowMove && tooltipPoints) { + each(splat(tooltipPoints), function (point) { + if (point.series.isCartesian && point.plotX === undefined) { + allowMove = false; + } + }); + } + + // Just move the tooltip, #349 + if (allowMove) { + if (tooltip && tooltipPoints) { + tooltip.refresh(tooltipPoints); + if (hoverPoint) { // #2500 + hoverPoint.setState(hoverPoint.state, true); + each(chart.axes, function (axis) { + if (axis.crosshair) { + axis.drawCrosshair(null, hoverPoint); + } + }); + } + } + + // Full reset + } else { + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(delay); + } + + if (pointer.unDocMouseMove) { + pointer.unDocMouseMove = pointer.unDocMouseMove(); + } + + // Remove crosshairs + each(chart.axes, function (axis) { + axis.hideCrosshair(); + }); + + pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; + } + }, + + /** + * Scale series groups to a certain scale and translation. + * + * @private + */ + scaleGroups: function (attribs, clip) { + + var chart = this.chart, + seriesAttribs; + + // Scale each series + each(chart.series, function (series) { + seriesAttribs = attribs || series.getPlotBox(); // #1701 + if (series.xAxis && series.xAxis.zoomEnabled && series.group) { + series.group.attr(seriesAttribs); + if (series.markerGroup) { + series.markerGroup.attr(seriesAttribs); + series.markerGroup.clip(clip ? chart.clipRect : null); + } + if (series.dataLabelsGroup) { + series.dataLabelsGroup.attr(seriesAttribs); + } + } + }); + + // Clip + chart.clipRect.attr(clip || chart.clipBox); + }, + + /** + * Start a drag operation. + * + * @private + */ + dragStart: function (e) { + var chart = this.chart; + + // Record the start position + chart.mouseIsDown = e.type; + chart.cancelClick = false; + chart.mouseDownX = this.mouseDownX = e.chartX; + chart.mouseDownY = this.mouseDownY = e.chartY; + }, + + /** + * Perform a drag operation in response to a mousemove event while the mouse + * is down. + * + * @private + */ + drag: function (e) { + + var chart = this.chart, + chartOptions = chart.options.chart, + chartX = e.chartX, + chartY = e.chartY, + zoomHor = this.zoomHor, + zoomVert = this.zoomVert, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + clickedInside, + size, + selectionMarker = this.selectionMarker, + mouseDownX = this.mouseDownX, + mouseDownY = this.mouseDownY, + panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; + + // If the device supports both touch and mouse (like IE11), and we are + // touch-dragging inside the plot area, don't handle the mouse event. + // #4339. + if (selectionMarker && selectionMarker.touch) { + return; + } + + // If the mouse is outside the plot area, adjust to cooordinates + // inside to prevent the selection marker from going outside + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } + + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } + + // determine if the mouse has moved more than 10px + this.hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); + + if (this.hasDragged > 10) { + clickedInside = chart.isInsidePlot( + mouseDownX - plotLeft, + mouseDownY - plotTop + ); + + // make a selection + if ( + chart.hasCartesianSeries && + (this.zoomX || this.zoomY) && + clickedInside && + !panKey + ) { + if (!selectionMarker) { + this.selectionMarker = selectionMarker = + chart.renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + /*= if (build.classic) { =*/ + fill: ( + chartOptions.selectionMarkerFill || + color('${palette.highlightColor80}') + .setOpacity(0.25).get() + ), + /*= } =*/ + 'class': 'highcharts-selection-marker', + 'zIndex': 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (selectionMarker && zoomHor) { + size = chartX - mouseDownX; + selectionMarker.attr({ + width: Math.abs(size), + x: (size > 0 ? 0 : size) + mouseDownX + }); + } + // adjust the height of the selection marker + if (selectionMarker && zoomVert) { + size = chartY - mouseDownY; + selectionMarker.attr({ + height: Math.abs(size), + y: (size > 0 ? 0 : size) + mouseDownY + }); + } + + // panning + if (clickedInside && !selectionMarker && chartOptions.panning) { + chart.pan(e, chartOptions.panning); + } + } + }, + + /** + * On mouse up or touch end across the entire document, drop the selection. + * + * @private + */ + drop: function (e) { + var pointer = this, + chart = this.chart, + hasPinched = this.hasPinched; + + if (this.selectionMarker) { + var selectionData = { + originalEvent: e, // #4890 + xAxis: [], + yAxis: [] + }, + selectionBox = this.selectionMarker, + selectionLeft = selectionBox.attr ? + selectionBox.attr('x') : + selectionBox.x, + selectionTop = selectionBox.attr ? + selectionBox.attr('y') : + selectionBox.y, + selectionWidth = selectionBox.attr ? + selectionBox.attr('width') : + selectionBox.width, + selectionHeight = selectionBox.attr ? + selectionBox.attr('height') : + selectionBox.height, + runZoom; + + // a selection has been made + if (this.hasDragged || hasPinched) { + + // record each axis' min and max + each(chart.axes, function (axis) { + if ( + axis.zoomEnabled && + defined(axis.min) && + ( + hasPinched || + pointer[{ + xAxis: 'zoomX', + yAxis: 'zoomY' + }[axis.coll]] + ) + ) { // #859, #3569 + var horiz = axis.horiz, + minPixelPadding = e.type === 'touchend' ? + axis.minPixelPadding : + 0, // #1207, #3075 + selectionMin = axis.toValue( + (horiz ? selectionLeft : selectionTop) + + minPixelPadding + ), + selectionMax = axis.toValue( + ( + horiz ? + selectionLeft + selectionWidth : + selectionTop + selectionHeight + ) - minPixelPadding + ); + + selectionData[axis.coll].push({ + axis: axis, + // Min/max for reversed axes + min: Math.min(selectionMin, selectionMax), + max: Math.max(selectionMin, selectionMax) + }); + runZoom = true; + } + }); + if (runZoom) { + fireEvent( + chart, + 'selection', + selectionData, + function (args) { + chart.zoom( + extend( + args, + hasPinched ? { animation: false } : null + ) + ); + } + ); + } + + } + + if (isNumber(chart.index)) { + this.selectionMarker = this.selectionMarker.destroy(); + } + + // Reset scaling preview + if (hasPinched) { + this.scaleGroups(); + } + } + + // Reset all. Check isNumber because it may be destroyed on mouse up + // (#877) + if (chart && isNumber(chart.index)) { + css(chart.container, { cursor: chart._cursor }); + chart.cancelClick = this.hasDragged > 10; // #370 + chart.mouseIsDown = this.hasDragged = this.hasPinched = false; + this.pinchDown = []; + } + }, + + onContainerMouseDown: function (e) { + // Normalize before the 'if' for the legacy IE (#7850) + e = this.normalize(e); + + if (e.button !== 2) { + + this.zoomOption(e); + + // issue #295, dragging not always working in Firefox + if (e.preventDefault) { + e.preventDefault(); + } + + this.dragStart(e); + } + }, + + + + onDocumentMouseUp: function (e) { + if (charts[H.hoverChartIndex]) { + charts[H.hoverChartIndex].pointer.drop(e); + } + }, + + /** + * Special handler for mouse move that will hide the tooltip when the mouse + * leaves the plotarea. Issue #149 workaround. The mouseleave event does not + * always fire. + * + * @private + */ + onDocumentMouseMove: function (e) { + var chart = this.chart, + chartPosition = this.chartPosition; + + e = this.normalize(e, chartPosition); + + // If we're outside, hide the tooltip + if ( + chartPosition && + !this.inClass(e.target, 'highcharts-tracker') && + !chart.isInsidePlot( + e.chartX - chart.plotLeft, + e.chartY - chart.plotTop + ) + ) { + this.reset(); + } + }, + + /** + * When mouse leaves the container, hide the tooltip. + * + * @private + */ + onContainerMouseLeave: function (e) { + var chart = charts[H.hoverChartIndex]; + // #4886, MS Touch end fires mouseleave but with no related target + if (chart && (e.relatedTarget || e.toElement)) { + chart.pointer.reset(); + // Also reset the chart position, used in #149 fix + chart.pointer.chartPosition = null; + } + }, + + // The mousemove, touchmove and touchstart event handler + onContainerMouseMove: function (e) { + + var chart = this.chart; + + if ( + !defined(H.hoverChartIndex) || + !charts[H.hoverChartIndex] || + !charts[H.hoverChartIndex].mouseIsDown + ) { + H.hoverChartIndex = chart.index; + } + + e = this.normalize(e); + e.returnValue = false; // #2251, #3224 + + if (chart.mouseIsDown === 'mousedown') { + this.drag(e); + } + + // Show the tooltip and run mouse over events (#977) + if ( + ( + this.inClass(e.target, 'highcharts-tracker') || + chart.isInsidePlot( + e.chartX - chart.plotLeft, + e.chartY - chart.plotTop + ) + ) && + !chart.openMenu + ) { + this.runPointActions(e); + } + }, + + /** + * Utility to detect whether an element has, or has a parent with, a + * specificclass name. Used on detection of tracker objects and on deciding + * whether hovering the tooltip should cause the active series to mouse out. + * + * @param {SVGDOMElement|HTMLDOMElement} element + * The element to investigate. + * @param {String} className + * The class name to look for. + * + * @return {Boolean} + * True if either the element or one of its parents has the given + * class name. + */ + inClass: function (element, className) { + var elemClassName; + while (element) { + elemClassName = attr(element, 'class'); + if (elemClassName) { + if (elemClassName.indexOf(className) !== -1) { + return true; + } + if (elemClassName.indexOf('highcharts-container') !== -1) { + return false; + } + } + element = element.parentNode; + } + }, + + onTrackerMouseOut: function (e) { + var series = this.chart.hoverSeries, + relatedTarget = e.relatedTarget || e.toElement; + + this.isDirectTouch = false; + + if ( + series && + relatedTarget && + !series.stickyTracking && + !this.inClass(relatedTarget, 'highcharts-tooltip') && + ( + !this.inClass( + relatedTarget, + 'highcharts-series-' + series.index + ) || // #2499, #4465 + !this.inClass(relatedTarget, 'highcharts-tracker') // #5553 + ) + ) { + series.onMouseOut(); + } + }, + + onContainerClick: function (e) { + var chart = this.chart, + hoverPoint = chart.hoverPoint, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop; + + e = this.normalize(e); + + if (!chart.cancelClick) { + + // On tracker click, fire the series and point events. #783, #1583 + if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) { + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + if (chart.hoverPoint) { // it may be destroyed (#1844) + hoverPoint.firePointEvent('click', e); + } + + // When clicking outside a tracker, fire a chart event + } else { + extend(e, this.getCoordinates(e)); + + // fire a click event in the chart + if ( + chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop) + ) { + fireEvent(chart, 'click', e); + } + } + + + } + }, + + /** + * Set the JS DOM events on the container and document. This method should + * contain a one-to-one assignment between methods and their handlers. Any + * advanced logic should be moved to the handler reflecting the event's + * name. + * + * @private + */ + setDOMEvents: function () { + + var pointer = this, + container = pointer.chart.container, + ownerDoc = container.ownerDocument; + + container.onmousedown = function (e) { + pointer.onContainerMouseDown(e); + }; + container.onmousemove = function (e) { + pointer.onContainerMouseMove(e); + }; + container.onclick = function (e) { + pointer.onContainerClick(e); + }; + this.unbindContainerMouseLeave = addEvent( + container, + 'mouseleave', + pointer.onContainerMouseLeave + ); + if (!H.unbindDocumentMouseUp) { + H.unbindDocumentMouseUp = addEvent( + ownerDoc, + 'mouseup', + pointer.onDocumentMouseUp + ); + } + if (H.hasTouch) { + container.ontouchstart = function (e) { + pointer.onContainerTouchStart(e); + }; + container.ontouchmove = function (e) { + pointer.onContainerTouchMove(e); + }; + if (!H.unbindDocumentTouchEnd) { + H.unbindDocumentTouchEnd = addEvent( + ownerDoc, + 'touchend', + pointer.onDocumentTouchEnd + ); + } + } + + }, + + /** + * Destroys the Pointer object and disconnects DOM events. + */ + destroy: function () { + var pointer = this; + + if (pointer.unDocMouseMove) { + pointer.unDocMouseMove(); + } + + this.unbindContainerMouseLeave(); + + if (!H.chartCount) { + if (H.unbindDocumentMouseUp) { + H.unbindDocumentMouseUp = H.unbindDocumentMouseUp(); + } + if (H.unbindDocumentTouchEnd) { + H.unbindDocumentTouchEnd = H.unbindDocumentTouchEnd(); + } + } + + // memory and CPU leak + clearInterval(pointer.tooltipTimeout); + + H.objectEach(pointer, function (val, prop) { + pointer[prop] = null; + }); + } }; diff --git a/js/parts/RangeSelector.js b/js/parts/RangeSelector.js index d3efee9dc72..ca4e8c1d580 100644 --- a/js/parts/RangeSelector.js +++ b/js/parts/RangeSelector.js @@ -9,250 +9,250 @@ import H from './Globals.js'; import './Axis.js'; import './Chart.js'; var addEvent = H.addEvent, - Axis = H.Axis, - Chart = H.Chart, - css = H.css, - createElement = H.createElement, - defaultOptions = H.defaultOptions, - defined = H.defined, - destroyObjectProperties = H.destroyObjectProperties, - discardElement = H.discardElement, - each = H.each, - extend = H.extend, - fireEvent = H.fireEvent, - isNumber = H.isNumber, - merge = H.merge, - pick = H.pick, - pInt = H.pInt, - splat = H.splat, - wrap = H.wrap; - + Axis = H.Axis, + Chart = H.Chart, + css = H.css, + createElement = H.createElement, + defaultOptions = H.defaultOptions, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + discardElement = H.discardElement, + each = H.each, + extend = H.extend, + fireEvent = H.fireEvent, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + pInt = H.pInt, + splat = H.splat, + wrap = H.wrap; + /* **************************************************************************** - * Start Range Selector code * + * Start Range Selector code * *****************************************************************************/ extend(defaultOptions, { - /** - * 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. - * - * @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, 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 - }, - - /** - * 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 undefined - * @since 2.1.9 - * @product highstock - * @deprecated true - */ - height: undefined, // reserved space for buttons and input - - /** - * Positioning for the input boxes. Allowed properties are `align`, - * `x` and `y`. - * - * @type {Object} - * @default { align: "right" } - * @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: '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, - /*= if (build.classic) { =*/ - // inputStyle: {}, - - /** - * CSS styles for the labels - the Zoom, From and To texts. - * - * 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: '${palette.neutralColor60}' - } - /*= } =*/ - } + /** + * 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. + * + * @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, 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 + }, + + /** + * 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 undefined + * @since 2.1.9 + * @product highstock + * @deprecated true + */ + height: undefined, // reserved space for buttons and input + + /** + * Positioning for the input boxes. Allowed properties are `align`, + * `x` and `y`. + * + * @type {Object} + * @default { align: "right" } + * @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: '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, + /*= if (build.classic) { =*/ + // inputStyle: {}, + + /** + * CSS styles for the labels - the Zoom, From and To texts. + * + * 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: '${palette.neutralColor60}' + } + /*= } =*/ + } }); defaultOptions.lang = merge( - defaultOptions.lang, - /** - * 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. - * - *
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'
-	 *         ]
-	 *     }
-	 * });
- * - * @optionparent lang - * @product highstock - */ - { - - /** - * The text for the label for the range selector buttons. - * - * @type {String} - * @default Zoom - * @product highstock - */ - rangeSelectorZoom: 'Zoom', - - /** - * The text for the label for the "from" input box in the range - * selector. - * - * @type {String} - * @default From - * @product highstock - */ - rangeSelectorFrom: 'From', - - /** - * The text for the label for the "to" input box in the range selector. - * - * @type {String} - * @default To - * @product highstock - */ - rangeSelectorTo: 'To' - } + defaultOptions.lang, + /** + * 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. + * + *
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'
+     *         ]
+     *     }
+     * });
+ * + * @optionparent lang + * @product highstock + */ + { + + /** + * The text for the label for the range selector buttons. + * + * @type {String} + * @default Zoom + * @product highstock + */ + rangeSelectorZoom: 'Zoom', + + /** + * The text for the label for the "from" input box in the range + * selector. + * + * @type {String} + * @default From + * @product highstock + */ + rangeSelectorFrom: 'From', + + /** + * The text for the label for the "to" input box in the range selector. + * + * @type {String} + * @default To + * @product highstock + */ + rangeSelectorTo: 'To' + } ); /** @@ -262,1026 +262,1026 @@ defaultOptions.lang = merge( */ function RangeSelector(chart) { - // Run RangeSelector - this.init(chart); + // Run RangeSelector + this.init(chart); } RangeSelector.prototype = { - /** - * The method to run when one of the buttons in the range selectors is clicked - * @param {Number} i The index of the button - * @param {Object} rangeOptions - * @param {Boolean} redraw - */ - clickButton: function (i, redraw) { - var rangeSelector = this, - chart = rangeSelector.chart, - rangeOptions = rangeSelector.buttonOptions[i], - baseAxis = chart.xAxis[0], - unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {}, - dataMin = unionExtremes.dataMin, - dataMax = unionExtremes.dataMax, - newMin, - newMax = baseAxis && Math.round(Math.min(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568 - type = rangeOptions.type, - baseXAxisOptions, - range = rangeOptions._range, - rangeMin, - minSetting, - rangeSetting, - ctx, - ytdExtremes, - dataGrouping = rangeOptions.dataGrouping; - - if (dataMin === null || dataMax === null) { // chart has no data, base series is removed - return; - } - - // Set the fixed range before range is altered - chart.fixedRange = range; - - // Apply dataGrouping associated to button - if (dataGrouping) { - this.forcedDataGrouping = true; - Axis.prototype.setDataGrouping.call(baseAxis || { chart: this.chart }, dataGrouping, false); - } - - // Apply range - if (type === 'month' || type === 'year') { - if (!baseAxis) { - // This is set to the user options and picked up later when the axis is instantiated - // so that we know the min and max. - range = rangeOptions; - } else { - ctx = { - range: rangeOptions, - max: newMax, - chart: chart, - dataMin: dataMin, - dataMax: dataMax - }; - newMin = baseAxis.minFromRange.call(ctx); - if (isNumber(ctx.newMax)) { - newMax = ctx.newMax; - } - } - - // Fixed times like minutes, hours, days - } else if (range) { - newMin = Math.max(newMax - range, dataMin); - newMax = Math.min(newMin + range, dataMax); - - } else if (type === 'ytd') { - - // On user clicks on the buttons, or a delayed action running from the beforeRender - // event (below), the baseAxis is defined. - if (baseAxis) { - // When "ytd" is the pre-selected button for the initial view, its calculation - // is delayed and rerun in the beforeRender event (below). When the series - // are initialized, but before the chart is rendered, we have access to the xData - // array (#942). - if (dataMax === undefined) { - dataMin = Number.MAX_VALUE; - dataMax = Number.MIN_VALUE; - each(chart.series, function (series) { - var xData = series.xData; // reassign it to the last item - dataMin = Math.min(xData[0], dataMin); - dataMax = Math.max(xData[xData.length - 1], dataMax); - }); - redraw = false; - } - ytdExtremes = rangeSelector.getYTDExtremes( - dataMax, - dataMin, - chart.time.useUTC - ); - newMin = rangeMin = ytdExtremes.min; - newMax = ytdExtremes.max; - - // "ytd" is pre-selected. We don't yet have access to processed point and extremes data - // (things like pointStart and pointInterval are missing), so we delay the process (#942) - } else { - addEvent(chart, 'beforeRender', function () { - rangeSelector.clickButton(i); - }); - return; - } - } 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 - // options and remove them on chart load (#4317). - baseXAxisOptions = splat(chart.options.xAxis)[0]; - rangeSetting = baseXAxisOptions.range; - baseXAxisOptions.range = range; - minSetting = baseXAxisOptions.min; - baseXAxisOptions.min = rangeMin; - addEvent(chart, 'load', function resetMinAndRange() { - baseXAxisOptions.range = rangeSetting; - baseXAxisOptions.min = minSetting; - }); - } else { - // Existing axis object. Set extremes after render time. - baseAxis.setExtremes( - newMin, - newMax, - pick(redraw, 1), - null, // auto animation - { - trigger: 'rangeSelectorButton', - rangeSelectorButton: rangeOptions - } - ); - } - }, - - /** - * Set the selected option. This method only sets the internal flag, it - * doesn't update the buttons or the actual zoomed range. - */ - setSelected: function (selected) { - this.selected = this.options.selected = selected; - }, - - /** - * The default buttons for pre-selecting time frames - */ - defaultButtons: [{ - type: 'month', - count: 1, - text: '1m' - }, { - type: 'month', - count: 3, - text: '3m' - }, { - type: 'month', - count: 6, - text: '6m' - }, { - type: 'ytd', - text: 'YTD' - }, { - type: 'year', - count: 1, - text: '1y' - }, { - type: 'all', - text: 'All' - }], - - /** - * Initialize the range selector - */ - init: function (chart) { - var rangeSelector = this, - options = chart.options.rangeSelector, - buttonOptions = options.buttons || - [].concat(rangeSelector.defaultButtons), - selectedOption = options.selected, - blurInputs = function () { - var minInput = rangeSelector.minInput, - maxInput = rangeSelector.maxInput; - - // #3274 in some case blur is not defined - if (minInput && minInput.blur) { - fireEvent(minInput, 'blur'); - } - if (maxInput && maxInput.blur) { - fireEvent(maxInput, 'blur'); - } - }; - - rangeSelector.chart = chart; - rangeSelector.options = options; - rangeSelector.buttons = []; - - chart.extraTopMargin = options.height; - rangeSelector.buttonOptions = buttonOptions; - - this.unMouseDown = addEvent(chart.container, 'mousedown', blurInputs); - this.unResize = addEvent(chart, 'resize', blurInputs); - - // Extend the buttonOptions with actual range - each(buttonOptions, rangeSelector.computeButtonRange); - - // zoomed range based on a pre-selected button index - if (selectedOption !== undefined && buttonOptions[selectedOption]) { - this.clickButton(selectedOption, false); - } - - - addEvent(chart, 'load', function () { - // If a data grouping is applied to the current button, release it - // when extremes change - if (chart.xAxis && chart.xAxis[0]) { - addEvent(chart.xAxis[0], 'setExtremes', function (e) { - if ( - this.max - this.min !== chart.fixedRange && - e.trigger !== 'rangeSelectorButton' && - e.trigger !== 'updatedData' && - rangeSelector.forcedDataGrouping - ) { - this.setDataGrouping(false, false); - } - }); - } - }); - }, - - /** - * Dynamically update the range selector buttons after a new range has been - * set - */ - updateButtonStates: function () { - var rangeSelector = this, - chart = this.chart, - baseAxis = chart.xAxis[0], - actualRange = Math.round(baseAxis.max - baseAxis.min), - hasNoData = !baseAxis.hasVisibleSeries, - day = 24 * 36e5, // A single day in milliseconds - unionExtremes = ( - chart.scroller && - chart.scroller.getUnionExtremes() - ) || baseAxis, - dataMin = unionExtremes.dataMin, - dataMax = unionExtremes.dataMax, - ytdExtremes = rangeSelector.getYTDExtremes( - dataMax, - dataMin, - chart.time.useUTC - ), - ytdMin = ytdExtremes.min, - ytdMax = ytdExtremes.max, - selected = rangeSelector.selected, - selectedExists = isNumber(selected), - allButtonsEnabled = rangeSelector.options.allButtonsEnabled, - buttons = rangeSelector.buttons; - - each(rangeSelector.buttonOptions, function (rangeOptions, i) { - var range = rangeOptions._range, - type = rangeOptions.type, - 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, - // Do not select the YTD button if not explicitly told so - isYTDButNotSelected = false, - // Disable the All button if we're already showing all - isAllButAlreadyShowingAll = false, - isSameRange = range === actualRange; - // Months and years have a variable range so we check the extremes - if ( - (type === 'month' || type === 'year') && - ( - actualRange + 36e5 >= - { month: 28, year: 365 }[type] * day * count - offsetRange - ) && - ( - actualRange - 36e5 <= - { month: 31, year: 366 }[type] * day * count + offsetRange - ) - ) { - isSameRange = true; - } else if (type === 'ytd') { - isSameRange = (ytdMax - ytdMin + offsetRange) === actualRange; - isYTDButNotSelected = !isSelected; - } else if (type === 'all') { - isSameRange = baseAxis.max - baseAxis.min >= dataMax - dataMin; - isAllButAlreadyShowingAll = ( - !isSelected && - selectedExists && - isSameRange - ); - } - - // The new zoom area happens to match the range for a button - mark - // it selected. This happens when scrolling across an ordinal gap. - // It can be seen in the intraday demos when selecting 1h and scroll - // across the night gap. - disable = ( - !allButtonsEnabled && - ( - isTooGreatRange || - isTooSmallRange || - isAllButAlreadyShowingAll || - hasNoData - ) - ); - select = ( - (isSelected && isSameRange) || - (isSameRange && !selectedExists && !isYTDButNotSelected) - ); - - if (disable) { - state = 3; - } else if (select) { - selectedExists = true; // Only one button can be selected - state = 2; - } - - // If state has changed, update the button - if (button.state !== state) { - button.setState(state); - } - }); - }, - - /** - * Compute and cache the range for an individual button - */ - computeButtonRange: function (rangeOptions) { - var type = rangeOptions.type, - count = rangeOptions.count || 1, - - // these time intervals have a fixed number of milliseconds, as - // opposed to month, ytd and year - fixedTimes = { - millisecond: 1, - second: 1000, - minute: 60 * 1000, - hour: 3600 * 1000, - day: 24 * 3600 * 1000, - week: 7 * 24 * 3600 * 1000 - }; - - // Store the range on the button object - if (fixedTimes[type]) { - rangeOptions._range = fixedTimes[type] * count; - } else if (type === 'month' || type === 'year') { - 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 - * @param {Number} inputTime - */ - setInputValue: function (name, inputTime) { - var options = this.chart.options.rangeSelector, - time = this.chart.time, - input = this[name + 'Input']; - - if (defined(inputTime)) { - input.previousValue = input.HCTime; - input.HCTime = inputTime; - } - - input.value = time.dateFormat( - options.inputEditDateFormat || '%Y-%m-%d', - input.HCTime - ); - this[name + 'DateBox'].attr({ - text: time.dateFormat( - options.inputDateFormat || '%b %e, %Y', - input.HCTime - ) - }); - }, - - showInput: function (name) { - var inputGroup = this.inputGroup, - dateBox = this[name + 'DateBox']; - - css(this[name + 'Input'], { - left: (inputGroup.translateX + dateBox.x) + 'px', - top: inputGroup.translateY + 'px', - width: (dateBox.width - 2) + 'px', - height: (dateBox.height - 2) + 'px', - border: '2px solid silver' - }); - }, - - hideInput: function (name) { - css(this[name + 'Input'], { - border: 0, - width: '1px', - height: '1px' - }); - this.setInputValue(name); - }, - - /** - * Draw either the 'from' or the 'to' HTML input box of the range selector - * @param {Object} name - */ - drawInput: function (name) { - var rangeSelector = this, - chart = rangeSelector.chart, - chartStyle = chart.renderer.style || {}, - renderer = chart.renderer, - options = chart.options.rangeSelector, - lang = defaultOptions.lang, - div = rangeSelector.div, - isMin = name === 'min', - input, - label, - dateBox, - inputGroup = this.inputGroup; - - function updateExtremes() { - var inputValue = input.value, - value = (options.inputDateParser || Date.parse)(inputValue), - chartAxis = chart.xAxis[0], - dataAxis = chart.scroller && chart.scroller.xAxis ? chart.scroller.xAxis : chartAxis, - dataMin = dataAxis.dataMin, - dataMax = dataAxis.dataMax; - if (value !== input.previousValue) { - input.previousValue = value; - // If the value isn't parsed directly to a value by the browser's Date.parse method, - // like YYYY-MM-DD in IE, try parsing it a different way - if (!isNumber(value)) { - value = inputValue.split('-'); - value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2])); - } - - if (isNumber(value)) { - - // Correct for timezone offset (#433) - if (!chart.time.useUTC) { - value = value + new Date().getTimezoneOffset() * 60 * 1000; - } - - // Validate the extremes. If it goes beyound the data min or max, use the - // actual data extreme (#2438). - if (isMin) { - if (value > rangeSelector.maxInput.HCTime) { - value = undefined; - } else if (value < dataMin) { - value = dataMin; - } - } else { - if (value < rangeSelector.minInput.HCTime) { - value = undefined; - } else if (value > dataMax) { - value = dataMax; - } - } - - // Set the extremes - if (value !== undefined) { - chartAxis.setExtremes( - isMin ? value : chartAxis.min, - isMin ? chartAxis.max : value, - undefined, - undefined, - { trigger: 'rangeSelectorInput' } - ); - } - } - } - } - - // Create the text label - this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset) - .addClass('highcharts-range-label') - .attr({ - padding: 2 - }) - .add(inputGroup); - inputGroup.offset += label.width + 5; - - // Create an SVG label that shows updated date ranges and and records click events that - // bring in the HTML input. - this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset) - .addClass('highcharts-range-input') - .attr({ - padding: 2, - width: options.inputBoxWidth || 90, - height: options.inputBoxHeight || 17, - stroke: options.inputBoxBorderColor || '${palette.neutralColor20}', - 'stroke-width': 1, - 'text-align': 'center' - }) - .on('click', function () { - rangeSelector.showInput(name); // If it is already focused, the onfocus event doesn't fire (#3713) - rangeSelector[name + 'Input'].focus(); - }) - .add(inputGroup); - inputGroup.offset += dateBox.width + (isMin ? 10 : 0); - - - // Create the HTML input element. This is rendered as 1x1 pixel then set to the right size - // when focused. - this[name + 'Input'] = input = createElement('input', { - name: name, - className: 'highcharts-range-selector', - type: 'text' - }, { - top: chart.plotTop + 'px' // prevent jump on focus in Firefox - }, div); - - /*= if (build.classic) { =*/ - // Styles - label.css(merge(chartStyle, options.labelStyle)); - - dateBox.css(merge({ - color: '${palette.neutralColor80}' - }, chartStyle, options.inputStyle)); - - css(input, extend({ - position: 'absolute', - border: 0, - width: '1px', // Chrome needs a pixel to see it - height: '1px', - padding: 0, - textAlign: 'center', - fontSize: chartStyle.fontSize, - fontFamily: chartStyle.fontFamily, - top: '-9999em' // #4798 - }, options.inputStyle)); - /*= } =*/ - - // Blow up the input box - input.onfocus = function () { - rangeSelector.showInput(name); - }; - // Hide away the input box - input.onblur = function () { - rangeSelector.hideInput(name); - }; - - // handle changes in the input boxes - input.onchange = updateExtremes; - - input.onkeypress = function (event) { - // IE does not fire onchange on enter - if (event.keyCode === 13) { - updateExtremes(); - } - }; - }, - - /** - * 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, - top = (options.verticalAlign) === 'top' ? chart.plotTop - chart.axisOffset[0] : 0; // set offset only for varticalAlign top - - return { - 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. - * Will choose dataMin if its value is higher than the timestamp for - * the start of current year. - * @param {number} dataMax - * @param {number} dataMin - * @return {object} Returns min and max for the YTD - */ - getYTDExtremes: function (dataMax, dataMin, useUTC) { - var time = this.chart.time, - min, - now = new time.Date(dataMax), - year = time.get('FullYear', now), - startOfYear = useUTC ? time.Date.UTC(year, 0, 1) : +new time.Date(year, 0, 1); // eslint-disable-line new-cap - min = Math.max(dataMin || 0, startOfYear); - now = now.getTime(); - return { - max: Math.min(dataMax || now, now), - min: min - }; - }, - - /** - * Render the range selector including the buttons and the inputs. The first time render - * is called, the elements are created and positioned. On subsequent calls, they are - * moved and updated. - * @param {Number} min X axis minimum - * @param {Number} max X axis maximum - */ - render: function (min, max) { - - var rangeSelector = this, - chart = rangeSelector.chart, - renderer = chart.renderer, - container = chart.container, - chartOptions = chart.options, - navButtonOptions = chartOptions.exporting && chartOptions.exporting.enabled !== false && - chartOptions.navigation && chartOptions.navigation.buttonOptions, - lang = defaultOptions.lang, - div = rangeSelector.div, - options = chartOptions.rangeSelector, - floating = options.floating, - buttons = rangeSelector.buttons, - inputGroup = rangeSelector.inputGroup, - buttonTheme = options.buttonTheme, - buttonPosition = options.buttonPosition, - inputPosition = options.inputPosition, - inputEnabled = options.inputEnabled, - states = buttonTheme && buttonTheme.states, - plotLeft = chart.plotLeft, - buttonLeft, - 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, - animate = rendered || false, - exportingX = 0, - alignTranslateY, - legendHeight, - minPosition, - translateY = 0, - translateX; - - if (options.enabled === false) { - return; - } - - // create the elements - if (!rendered) { - - rangeSelector.group = group = renderer.g('range-selector-group') - .attr({ - zIndex: 7 - }) - .add(); - - 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 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 () { - - // 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, - states && states.disabled - ) - .attr({ - 'text-align': 'center' - }) - .add(buttonGroup); - - // increase button position for the next button - buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5); - }); - - // first create a wrapper outside the container in order to make - // the inputs work and make export correct - if (inputEnabled !== false) { - rangeSelector.div = div = createElement('div', null, { - position: 'relative', - height: 0, - zIndex: 1 // above container - }); - - container.parentNode.insertBefore(div, container); - - // Create the group to keep the inputs - rangeSelector.inputGroup = inputGroup = renderer.g('input-group') - .add(group); - inputGroup.offset = 0; - - rangeSelector.drawInput('min'); - rangeSelector.drawInput('max'); - } - } - - plotLeft = chart.plotLeft - chart.spacing[3]; - rangeSelector.updateButtonStates(); - - // 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) - ) - ) { - exportingX = -40; - } - - if (buttonPosition.align === 'left') { - translateX = buttonPosition.x - chart.spacing[3]; - } else if (buttonPosition.align === 'right') { - translateX = buttonPosition.x + exportingX - chart.spacing[1]; - } - - // align button group - buttonGroup.align({ - y: buttonPosition.y, - width: buttonGroup.getBBox().width, - align: buttonPosition.align, - x: translateX - }, true, chart.spacingBox); - - // skip animation - rangeSelector.group.placed = animate; - rangeSelector.buttonGroup.placed = animate; - - if (inputEnabled !== false) { - - var inputGroupX, - inputGroupWidth, - buttonGroupX, - buttonGroupWidth; - - // detect collision with exporting - if - ( - navButtonOptions && - this.titleCollision(chart) && - verticalAlign === 'top' && - inputPosition.align === 'right' && - ( - (inputPosition.y - inputGroup.getBBox().height - 12) < - ((navButtonOptions.y || 0) + navButtonOptions.height + chart.spacing[0]) - ) - ) { - exportingX = -40; - } else { - exportingX = 0; - } - - if (inputPosition.align === 'left') { - translateX = plotLeft; - } else if (inputPosition.align === 'right') { - translateX = -Math.max(chart.axisOffset[1], -exportingX); // yAxis offset - } - - // Update the alignment to the updated spacing box - inputGroup.align({ - y: inputPosition.y, - width: inputGroup.getBBox().width, - align: inputPosition.align, - x: inputPosition.x + translateX - 2 // fix wrong getBBox() value on right align - }, true, chart.spacingBox); - - // detect collision - inputGroupX = inputGroup.alignAttr.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.alignAttr.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)) - ) - ) { - - inputGroup.attr({ - translateX: inputGroup.alignAttr.translateX + (chart.axisOffset[1] >= -exportingX ? 0 : -exportingX), - translateY: inputGroup.alignAttr.translateY + buttonGroup.getBBox().height + 10 - }); - - } - - // Set or reset the input values - rangeSelector.setInputValue('min', min); - rangeSelector.setInputValue('max', max); - - // skip animation - rangeSelector.inputGroup.placed = animate; - } - - // vertical align - rangeSelector.group.align({ - verticalAlign: verticalAlign - }, true, chart.spacingBox); - - // set position - groupHeight = rangeSelector.group.getBBox().height + 20; // # 20 padding - alignTranslateY = rangeSelector.group.alignAttr.translateY; - - // 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; - translateY = alignTranslateY - groupHeight - (floating ? 0 : options.y) - 10; // 10 spacing - - } - - if (verticalAlign === 'top') { - if (floating) { - translateY = 0; - } - - if (chart.titleOffset) { - translateY = chart.titleOffset + chart.options.title.margin; - } - - translateY += ((chart.margin[0] - chart.spacing[0]) || 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; - } - } - } - - rangeSelector.group.translate( - options.x, - options.y + Math.floor(translateY) - ); - - // 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, - rangeSelectorGroup = rangeSelector.group, - inputPosition = options.inputPosition, - buttonPosition = options.buttonPosition, - yPosition = options.y, - 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) { - return !(chart.options.title.text || chart.options.subtitle.text); - }, - - /** - * 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. - */ - destroy: function () { - var rSelector = this, - minInput = rSelector.minInput, - maxInput = rSelector.maxInput; - - rSelector.unMouseDown(); - rSelector.unResize(); - - // Destroy elements in collections - destroyObjectProperties(rSelector.buttons); - - // Clear input element events - if (minInput) { - minInput.onfocus = minInput.onblur = minInput.onchange = null; - } - if (maxInput) { - maxInput.onfocus = maxInput.onblur = maxInput.onchange = null; - } - - // Destroy HTML and SVG elements - H.objectEach(rSelector, function (val, key) { - if (val && key !== 'chart') { - if (val.destroy) { // SVGElement - val.destroy(); - } else if (val.nodeType) { // HTML element - discardElement(this[key]); - } - } - if (val !== RangeSelector.prototype[key]) { - rSelector[key] = null; - } - }, this); - } + /** + * The method to run when one of the buttons in the range selectors is clicked + * @param {Number} i The index of the button + * @param {Object} rangeOptions + * @param {Boolean} redraw + */ + clickButton: function (i, redraw) { + var rangeSelector = this, + chart = rangeSelector.chart, + rangeOptions = rangeSelector.buttonOptions[i], + baseAxis = chart.xAxis[0], + unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {}, + dataMin = unionExtremes.dataMin, + dataMax = unionExtremes.dataMax, + newMin, + newMax = baseAxis && Math.round(Math.min(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568 + type = rangeOptions.type, + baseXAxisOptions, + range = rangeOptions._range, + rangeMin, + minSetting, + rangeSetting, + ctx, + ytdExtremes, + dataGrouping = rangeOptions.dataGrouping; + + if (dataMin === null || dataMax === null) { // chart has no data, base series is removed + return; + } + + // Set the fixed range before range is altered + chart.fixedRange = range; + + // Apply dataGrouping associated to button + if (dataGrouping) { + this.forcedDataGrouping = true; + Axis.prototype.setDataGrouping.call(baseAxis || { chart: this.chart }, dataGrouping, false); + } + + // Apply range + if (type === 'month' || type === 'year') { + if (!baseAxis) { + // This is set to the user options and picked up later when the axis is instantiated + // so that we know the min and max. + range = rangeOptions; + } else { + ctx = { + range: rangeOptions, + max: newMax, + chart: chart, + dataMin: dataMin, + dataMax: dataMax + }; + newMin = baseAxis.minFromRange.call(ctx); + if (isNumber(ctx.newMax)) { + newMax = ctx.newMax; + } + } + + // Fixed times like minutes, hours, days + } else if (range) { + newMin = Math.max(newMax - range, dataMin); + newMax = Math.min(newMin + range, dataMax); + + } else if (type === 'ytd') { + + // On user clicks on the buttons, or a delayed action running from the beforeRender + // event (below), the baseAxis is defined. + if (baseAxis) { + // When "ytd" is the pre-selected button for the initial view, its calculation + // is delayed and rerun in the beforeRender event (below). When the series + // are initialized, but before the chart is rendered, we have access to the xData + // array (#942). + if (dataMax === undefined) { + dataMin = Number.MAX_VALUE; + dataMax = Number.MIN_VALUE; + each(chart.series, function (series) { + var xData = series.xData; // reassign it to the last item + dataMin = Math.min(xData[0], dataMin); + dataMax = Math.max(xData[xData.length - 1], dataMax); + }); + redraw = false; + } + ytdExtremes = rangeSelector.getYTDExtremes( + dataMax, + dataMin, + chart.time.useUTC + ); + newMin = rangeMin = ytdExtremes.min; + newMax = ytdExtremes.max; + + // "ytd" is pre-selected. We don't yet have access to processed point and extremes data + // (things like pointStart and pointInterval are missing), so we delay the process (#942) + } else { + addEvent(chart, 'beforeRender', function () { + rangeSelector.clickButton(i); + }); + return; + } + } 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 + // options and remove them on chart load (#4317). + baseXAxisOptions = splat(chart.options.xAxis)[0]; + rangeSetting = baseXAxisOptions.range; + baseXAxisOptions.range = range; + minSetting = baseXAxisOptions.min; + baseXAxisOptions.min = rangeMin; + addEvent(chart, 'load', function resetMinAndRange() { + baseXAxisOptions.range = rangeSetting; + baseXAxisOptions.min = minSetting; + }); + } else { + // Existing axis object. Set extremes after render time. + baseAxis.setExtremes( + newMin, + newMax, + pick(redraw, 1), + null, // auto animation + { + trigger: 'rangeSelectorButton', + rangeSelectorButton: rangeOptions + } + ); + } + }, + + /** + * Set the selected option. This method only sets the internal flag, it + * doesn't update the buttons or the actual zoomed range. + */ + setSelected: function (selected) { + this.selected = this.options.selected = selected; + }, + + /** + * The default buttons for pre-selecting time frames + */ + defaultButtons: [{ + type: 'month', + count: 1, + text: '1m' + }, { + type: 'month', + count: 3, + text: '3m' + }, { + type: 'month', + count: 6, + text: '6m' + }, { + type: 'ytd', + text: 'YTD' + }, { + type: 'year', + count: 1, + text: '1y' + }, { + type: 'all', + text: 'All' + }], + + /** + * Initialize the range selector + */ + init: function (chart) { + var rangeSelector = this, + options = chart.options.rangeSelector, + buttonOptions = options.buttons || + [].concat(rangeSelector.defaultButtons), + selectedOption = options.selected, + blurInputs = function () { + var minInput = rangeSelector.minInput, + maxInput = rangeSelector.maxInput; + + // #3274 in some case blur is not defined + if (minInput && minInput.blur) { + fireEvent(minInput, 'blur'); + } + if (maxInput && maxInput.blur) { + fireEvent(maxInput, 'blur'); + } + }; + + rangeSelector.chart = chart; + rangeSelector.options = options; + rangeSelector.buttons = []; + + chart.extraTopMargin = options.height; + rangeSelector.buttonOptions = buttonOptions; + + this.unMouseDown = addEvent(chart.container, 'mousedown', blurInputs); + this.unResize = addEvent(chart, 'resize', blurInputs); + + // Extend the buttonOptions with actual range + each(buttonOptions, rangeSelector.computeButtonRange); + + // zoomed range based on a pre-selected button index + if (selectedOption !== undefined && buttonOptions[selectedOption]) { + this.clickButton(selectedOption, false); + } + + + addEvent(chart, 'load', function () { + // If a data grouping is applied to the current button, release it + // when extremes change + if (chart.xAxis && chart.xAxis[0]) { + addEvent(chart.xAxis[0], 'setExtremes', function (e) { + if ( + this.max - this.min !== chart.fixedRange && + e.trigger !== 'rangeSelectorButton' && + e.trigger !== 'updatedData' && + rangeSelector.forcedDataGrouping + ) { + this.setDataGrouping(false, false); + } + }); + } + }); + }, + + /** + * Dynamically update the range selector buttons after a new range has been + * set + */ + updateButtonStates: function () { + var rangeSelector = this, + chart = this.chart, + baseAxis = chart.xAxis[0], + actualRange = Math.round(baseAxis.max - baseAxis.min), + hasNoData = !baseAxis.hasVisibleSeries, + day = 24 * 36e5, // A single day in milliseconds + unionExtremes = ( + chart.scroller && + chart.scroller.getUnionExtremes() + ) || baseAxis, + dataMin = unionExtremes.dataMin, + dataMax = unionExtremes.dataMax, + ytdExtremes = rangeSelector.getYTDExtremes( + dataMax, + dataMin, + chart.time.useUTC + ), + ytdMin = ytdExtremes.min, + ytdMax = ytdExtremes.max, + selected = rangeSelector.selected, + selectedExists = isNumber(selected), + allButtonsEnabled = rangeSelector.options.allButtonsEnabled, + buttons = rangeSelector.buttons; + + each(rangeSelector.buttonOptions, function (rangeOptions, i) { + var range = rangeOptions._range, + type = rangeOptions.type, + 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, + // Do not select the YTD button if not explicitly told so + isYTDButNotSelected = false, + // Disable the All button if we're already showing all + isAllButAlreadyShowingAll = false, + isSameRange = range === actualRange; + // Months and years have a variable range so we check the extremes + if ( + (type === 'month' || type === 'year') && + ( + actualRange + 36e5 >= + { month: 28, year: 365 }[type] * day * count - offsetRange + ) && + ( + actualRange - 36e5 <= + { month: 31, year: 366 }[type] * day * count + offsetRange + ) + ) { + isSameRange = true; + } else if (type === 'ytd') { + isSameRange = (ytdMax - ytdMin + offsetRange) === actualRange; + isYTDButNotSelected = !isSelected; + } else if (type === 'all') { + isSameRange = baseAxis.max - baseAxis.min >= dataMax - dataMin; + isAllButAlreadyShowingAll = ( + !isSelected && + selectedExists && + isSameRange + ); + } + + // The new zoom area happens to match the range for a button - mark + // it selected. This happens when scrolling across an ordinal gap. + // It can be seen in the intraday demos when selecting 1h and scroll + // across the night gap. + disable = ( + !allButtonsEnabled && + ( + isTooGreatRange || + isTooSmallRange || + isAllButAlreadyShowingAll || + hasNoData + ) + ); + select = ( + (isSelected && isSameRange) || + (isSameRange && !selectedExists && !isYTDButNotSelected) + ); + + if (disable) { + state = 3; + } else if (select) { + selectedExists = true; // Only one button can be selected + state = 2; + } + + // If state has changed, update the button + if (button.state !== state) { + button.setState(state); + } + }); + }, + + /** + * Compute and cache the range for an individual button + */ + computeButtonRange: function (rangeOptions) { + var type = rangeOptions.type, + count = rangeOptions.count || 1, + + // these time intervals have a fixed number of milliseconds, as + // opposed to month, ytd and year + fixedTimes = { + millisecond: 1, + second: 1000, + minute: 60 * 1000, + hour: 3600 * 1000, + day: 24 * 3600 * 1000, + week: 7 * 24 * 3600 * 1000 + }; + + // Store the range on the button object + if (fixedTimes[type]) { + rangeOptions._range = fixedTimes[type] * count; + } else if (type === 'month' || type === 'year') { + 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 + * @param {Number} inputTime + */ + setInputValue: function (name, inputTime) { + var options = this.chart.options.rangeSelector, + time = this.chart.time, + input = this[name + 'Input']; + + if (defined(inputTime)) { + input.previousValue = input.HCTime; + input.HCTime = inputTime; + } + + input.value = time.dateFormat( + options.inputEditDateFormat || '%Y-%m-%d', + input.HCTime + ); + this[name + 'DateBox'].attr({ + text: time.dateFormat( + options.inputDateFormat || '%b %e, %Y', + input.HCTime + ) + }); + }, + + showInput: function (name) { + var inputGroup = this.inputGroup, + dateBox = this[name + 'DateBox']; + + css(this[name + 'Input'], { + left: (inputGroup.translateX + dateBox.x) + 'px', + top: inputGroup.translateY + 'px', + width: (dateBox.width - 2) + 'px', + height: (dateBox.height - 2) + 'px', + border: '2px solid silver' + }); + }, + + hideInput: function (name) { + css(this[name + 'Input'], { + border: 0, + width: '1px', + height: '1px' + }); + this.setInputValue(name); + }, + + /** + * Draw either the 'from' or the 'to' HTML input box of the range selector + * @param {Object} name + */ + drawInput: function (name) { + var rangeSelector = this, + chart = rangeSelector.chart, + chartStyle = chart.renderer.style || {}, + renderer = chart.renderer, + options = chart.options.rangeSelector, + lang = defaultOptions.lang, + div = rangeSelector.div, + isMin = name === 'min', + input, + label, + dateBox, + inputGroup = this.inputGroup; + + function updateExtremes() { + var inputValue = input.value, + value = (options.inputDateParser || Date.parse)(inputValue), + chartAxis = chart.xAxis[0], + dataAxis = chart.scroller && chart.scroller.xAxis ? chart.scroller.xAxis : chartAxis, + dataMin = dataAxis.dataMin, + dataMax = dataAxis.dataMax; + if (value !== input.previousValue) { + input.previousValue = value; + // If the value isn't parsed directly to a value by the browser's Date.parse method, + // like YYYY-MM-DD in IE, try parsing it a different way + if (!isNumber(value)) { + value = inputValue.split('-'); + value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2])); + } + + if (isNumber(value)) { + + // Correct for timezone offset (#433) + if (!chart.time.useUTC) { + value = value + new Date().getTimezoneOffset() * 60 * 1000; + } + + // Validate the extremes. If it goes beyound the data min or max, use the + // actual data extreme (#2438). + if (isMin) { + if (value > rangeSelector.maxInput.HCTime) { + value = undefined; + } else if (value < dataMin) { + value = dataMin; + } + } else { + if (value < rangeSelector.minInput.HCTime) { + value = undefined; + } else if (value > dataMax) { + value = dataMax; + } + } + + // Set the extremes + if (value !== undefined) { + chartAxis.setExtremes( + isMin ? value : chartAxis.min, + isMin ? chartAxis.max : value, + undefined, + undefined, + { trigger: 'rangeSelectorInput' } + ); + } + } + } + } + + // Create the text label + this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset) + .addClass('highcharts-range-label') + .attr({ + padding: 2 + }) + .add(inputGroup); + inputGroup.offset += label.width + 5; + + // Create an SVG label that shows updated date ranges and and records click events that + // bring in the HTML input. + this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset) + .addClass('highcharts-range-input') + .attr({ + padding: 2, + width: options.inputBoxWidth || 90, + height: options.inputBoxHeight || 17, + stroke: options.inputBoxBorderColor || '${palette.neutralColor20}', + 'stroke-width': 1, + 'text-align': 'center' + }) + .on('click', function () { + rangeSelector.showInput(name); // If it is already focused, the onfocus event doesn't fire (#3713) + rangeSelector[name + 'Input'].focus(); + }) + .add(inputGroup); + inputGroup.offset += dateBox.width + (isMin ? 10 : 0); + + + // Create the HTML input element. This is rendered as 1x1 pixel then set to the right size + // when focused. + this[name + 'Input'] = input = createElement('input', { + name: name, + className: 'highcharts-range-selector', + type: 'text' + }, { + top: chart.plotTop + 'px' // prevent jump on focus in Firefox + }, div); + + /*= if (build.classic) { =*/ + // Styles + label.css(merge(chartStyle, options.labelStyle)); + + dateBox.css(merge({ + color: '${palette.neutralColor80}' + }, chartStyle, options.inputStyle)); + + css(input, extend({ + position: 'absolute', + border: 0, + width: '1px', // Chrome needs a pixel to see it + height: '1px', + padding: 0, + textAlign: 'center', + fontSize: chartStyle.fontSize, + fontFamily: chartStyle.fontFamily, + top: '-9999em' // #4798 + }, options.inputStyle)); + /*= } =*/ + + // Blow up the input box + input.onfocus = function () { + rangeSelector.showInput(name); + }; + // Hide away the input box + input.onblur = function () { + rangeSelector.hideInput(name); + }; + + // handle changes in the input boxes + input.onchange = updateExtremes; + + input.onkeypress = function (event) { + // IE does not fire onchange on enter + if (event.keyCode === 13) { + updateExtremes(); + } + }; + }, + + /** + * 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, + top = (options.verticalAlign) === 'top' ? chart.plotTop - chart.axisOffset[0] : 0; // set offset only for varticalAlign top + + return { + 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. + * Will choose dataMin if its value is higher than the timestamp for + * the start of current year. + * @param {number} dataMax + * @param {number} dataMin + * @return {object} Returns min and max for the YTD + */ + getYTDExtremes: function (dataMax, dataMin, useUTC) { + var time = this.chart.time, + min, + now = new time.Date(dataMax), + year = time.get('FullYear', now), + startOfYear = useUTC ? time.Date.UTC(year, 0, 1) : +new time.Date(year, 0, 1); // eslint-disable-line new-cap + min = Math.max(dataMin || 0, startOfYear); + now = now.getTime(); + return { + max: Math.min(dataMax || now, now), + min: min + }; + }, + + /** + * Render the range selector including the buttons and the inputs. The first time render + * is called, the elements are created and positioned. On subsequent calls, they are + * moved and updated. + * @param {Number} min X axis minimum + * @param {Number} max X axis maximum + */ + render: function (min, max) { + + var rangeSelector = this, + chart = rangeSelector.chart, + renderer = chart.renderer, + container = chart.container, + chartOptions = chart.options, + navButtonOptions = chartOptions.exporting && chartOptions.exporting.enabled !== false && + chartOptions.navigation && chartOptions.navigation.buttonOptions, + lang = defaultOptions.lang, + div = rangeSelector.div, + options = chartOptions.rangeSelector, + floating = options.floating, + buttons = rangeSelector.buttons, + inputGroup = rangeSelector.inputGroup, + buttonTheme = options.buttonTheme, + buttonPosition = options.buttonPosition, + inputPosition = options.inputPosition, + inputEnabled = options.inputEnabled, + states = buttonTheme && buttonTheme.states, + plotLeft = chart.plotLeft, + buttonLeft, + 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, + animate = rendered || false, + exportingX = 0, + alignTranslateY, + legendHeight, + minPosition, + translateY = 0, + translateX; + + if (options.enabled === false) { + return; + } + + // create the elements + if (!rendered) { + + rangeSelector.group = group = renderer.g('range-selector-group') + .attr({ + zIndex: 7 + }) + .add(); + + 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 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 () { + + // 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, + states && states.disabled + ) + .attr({ + 'text-align': 'center' + }) + .add(buttonGroup); + + // increase button position for the next button + buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5); + }); + + // first create a wrapper outside the container in order to make + // the inputs work and make export correct + if (inputEnabled !== false) { + rangeSelector.div = div = createElement('div', null, { + position: 'relative', + height: 0, + zIndex: 1 // above container + }); + + container.parentNode.insertBefore(div, container); + + // Create the group to keep the inputs + rangeSelector.inputGroup = inputGroup = renderer.g('input-group') + .add(group); + inputGroup.offset = 0; + + rangeSelector.drawInput('min'); + rangeSelector.drawInput('max'); + } + } + + plotLeft = chart.plotLeft - chart.spacing[3]; + rangeSelector.updateButtonStates(); + + // 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) + ) + ) { + exportingX = -40; + } + + if (buttonPosition.align === 'left') { + translateX = buttonPosition.x - chart.spacing[3]; + } else if (buttonPosition.align === 'right') { + translateX = buttonPosition.x + exportingX - chart.spacing[1]; + } + + // align button group + buttonGroup.align({ + y: buttonPosition.y, + width: buttonGroup.getBBox().width, + align: buttonPosition.align, + x: translateX + }, true, chart.spacingBox); + + // skip animation + rangeSelector.group.placed = animate; + rangeSelector.buttonGroup.placed = animate; + + if (inputEnabled !== false) { + + var inputGroupX, + inputGroupWidth, + buttonGroupX, + buttonGroupWidth; + + // detect collision with exporting + if + ( + navButtonOptions && + this.titleCollision(chart) && + verticalAlign === 'top' && + inputPosition.align === 'right' && + ( + (inputPosition.y - inputGroup.getBBox().height - 12) < + ((navButtonOptions.y || 0) + navButtonOptions.height + chart.spacing[0]) + ) + ) { + exportingX = -40; + } else { + exportingX = 0; + } + + if (inputPosition.align === 'left') { + translateX = plotLeft; + } else if (inputPosition.align === 'right') { + translateX = -Math.max(chart.axisOffset[1], -exportingX); // yAxis offset + } + + // Update the alignment to the updated spacing box + inputGroup.align({ + y: inputPosition.y, + width: inputGroup.getBBox().width, + align: inputPosition.align, + x: inputPosition.x + translateX - 2 // fix wrong getBBox() value on right align + }, true, chart.spacingBox); + + // detect collision + inputGroupX = inputGroup.alignAttr.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.alignAttr.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)) + ) + ) { + + inputGroup.attr({ + translateX: inputGroup.alignAttr.translateX + (chart.axisOffset[1] >= -exportingX ? 0 : -exportingX), + translateY: inputGroup.alignAttr.translateY + buttonGroup.getBBox().height + 10 + }); + + } + + // Set or reset the input values + rangeSelector.setInputValue('min', min); + rangeSelector.setInputValue('max', max); + + // skip animation + rangeSelector.inputGroup.placed = animate; + } + + // vertical align + rangeSelector.group.align({ + verticalAlign: verticalAlign + }, true, chart.spacingBox); + + // set position + groupHeight = rangeSelector.group.getBBox().height + 20; // # 20 padding + alignTranslateY = rangeSelector.group.alignAttr.translateY; + + // 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; + translateY = alignTranslateY - groupHeight - (floating ? 0 : options.y) - 10; // 10 spacing + + } + + if (verticalAlign === 'top') { + if (floating) { + translateY = 0; + } + + if (chart.titleOffset) { + translateY = chart.titleOffset + chart.options.title.margin; + } + + translateY += ((chart.margin[0] - chart.spacing[0]) || 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; + } + } + } + + rangeSelector.group.translate( + options.x, + options.y + Math.floor(translateY) + ); + + // 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, + rangeSelectorGroup = rangeSelector.group, + inputPosition = options.inputPosition, + buttonPosition = options.buttonPosition, + yPosition = options.y, + 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) { + return !(chart.options.title.text || chart.options.subtitle.text); + }, + + /** + * 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. + */ + destroy: function () { + var rSelector = this, + minInput = rSelector.minInput, + maxInput = rSelector.maxInput; + + rSelector.unMouseDown(); + rSelector.unResize(); + + // Destroy elements in collections + destroyObjectProperties(rSelector.buttons); + + // Clear input element events + if (minInput) { + minInput.onfocus = minInput.onblur = minInput.onchange = null; + } + if (maxInput) { + maxInput.onfocus = maxInput.onblur = maxInput.onchange = null; + } + + // Destroy HTML and SVG elements + H.objectEach(rSelector, function (val, key) { + if (val && key !== 'chart') { + if (val.destroy) { // SVGElement + val.destroy(); + } else if (val.nodeType) { // HTML element + discardElement(this[key]); + } + } + if (val !== RangeSelector.prototype[key]) { + rSelector[key] = null; + } + }, this); + } }; /** * Add logic to normalize the zoomed range in order to preserve the pressed state of range selector buttons */ Axis.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) { - var fixedRange = this.chart && this.chart.fixedRange, - newMin = pick(fixedMin, this.translate(pxMin, true, !this.horiz)), - newMax = pick(fixedMax, this.translate(pxMax, true, !this.horiz)), - changeRatio = fixedRange && (newMax - newMin) / fixedRange; - - // If the difference between the fixed range and the actual requested range is - // too great, the user is dragging across an ordinal gap, and we need to release - // the range selector button. - if (changeRatio > 0.7 && changeRatio < 1.3) { - if (fixedMax) { - newMin = newMax - fixedRange; - } else { - newMax = newMin + fixedRange; - } - } - if (!isNumber(newMin) || !isNumber(newMax)) { // #1195, #7411 - newMin = newMax = undefined; - } - - return { - min: newMin, - max: newMax - }; + var fixedRange = this.chart && this.chart.fixedRange, + newMin = pick(fixedMin, this.translate(pxMin, true, !this.horiz)), + newMax = pick(fixedMax, this.translate(pxMax, true, !this.horiz)), + changeRatio = fixedRange && (newMax - newMin) / fixedRange; + + // If the difference between the fixed range and the actual requested range is + // too great, the user is dragging across an ordinal gap, and we need to release + // the range selector button. + if (changeRatio > 0.7 && changeRatio < 1.3) { + if (fixedMax) { + newMin = newMax - fixedRange; + } else { + newMax = newMin + fixedRange; + } + } + if (!isNumber(newMin) || !isNumber(newMax)) { // #1195, #7411 + newMin = newMax = undefined; + } + + return { + min: newMin, + max: newMax + }; }; /** @@ -1289,210 +1289,210 @@ Axis.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) { * stock charts this is extended via the {@link RangeSelector} so that if the * selected range is a multiple of months or years, it is compensated for * various month lengths. - * + * * @return {number} The new minimum value. */ Axis.prototype.minFromRange = function () { - var rangeOptions = this.range, - type = rangeOptions.type, - timeName = { month: 'Month', year: 'FullYear' }[type], - min, - max = this.max, - dataMin, - range, - // Get the true range from a start date - getTrueRange = function (base, count) { - var date = new Date(base), - basePeriod = date['get' + timeName](); - - date['set' + timeName](basePeriod + count); - - if (basePeriod === date['get' + timeName]()) { - date.setDate(0); // #6537 - } - - return date.getTime() - base; - }; - - if (isNumber(rangeOptions)) { - min = max - rangeOptions; - range = rangeOptions; - } else { - min = max + getTrueRange(max, -rangeOptions.count); - - // Let the fixedRange reflect initial settings (#5930) - if (this.chart) { - this.chart.fixedRange = max - min; - } - } - - dataMin = pick(this.dataMin, Number.MIN_VALUE); - if (!isNumber(min)) { - min = dataMin; - } - if (min <= dataMin) { - min = dataMin; - if (range === undefined) { // #4501 - range = getTrueRange(min, rangeOptions.count); - } - this.newMax = Math.min(min + range, this.dataMax); - } - if (!isNumber(max)) { - min = undefined; - } - return min; + var rangeOptions = this.range, + type = rangeOptions.type, + timeName = { month: 'Month', year: 'FullYear' }[type], + min, + max = this.max, + dataMin, + range, + // Get the true range from a start date + getTrueRange = function (base, count) { + var date = new Date(base), + basePeriod = date['get' + timeName](); + + date['set' + timeName](basePeriod + count); + + if (basePeriod === date['get' + timeName]()) { + date.setDate(0); // #6537 + } + + return date.getTime() - base; + }; + + if (isNumber(rangeOptions)) { + min = max - rangeOptions; + range = rangeOptions; + } else { + min = max + getTrueRange(max, -rangeOptions.count); + + // Let the fixedRange reflect initial settings (#5930) + if (this.chart) { + this.chart.fixedRange = max - min; + } + } + + dataMin = pick(this.dataMin, Number.MIN_VALUE); + if (!isNumber(min)) { + min = dataMin; + } + if (min <= dataMin) { + min = dataMin; + if (range === undefined) { // #4501 + range = getTrueRange(min, rangeOptions.count); + } + this.newMax = Math.min(min + range, this.dataMax); + } + if (!isNumber(max)) { + min = undefined; + } + return min; }; // Initialize rangeselector for stock charts addEvent(Chart, 'afterGetContainer', function () { - if (this.options.rangeSelector.enabled) { - this.rangeSelector = new RangeSelector(this); - } + if (this.options.rangeSelector.enabled) { + this.rangeSelector = new RangeSelector(this); + } }); wrap(Chart.prototype, 'render', function (proceed, options, callback) { - var chart = this, - axes = chart.axes, - rangeSelector = chart.rangeSelector, - verticalAlign; + var chart = this, + axes = chart.axes, + rangeSelector = chart.rangeSelector, + verticalAlign; - if (rangeSelector) { + if (rangeSelector) { - each(axes, function (axis) { - axis.updateNames(); - axis.setScale(); - }); + each(axes, function (axis) { + axis.updateNames(); + axis.setScale(); + }); - chart.getAxisMargins(); + chart.getAxisMargins(); - rangeSelector.render(); - verticalAlign = rangeSelector.options.verticalAlign; + rangeSelector.render(); + verticalAlign = rangeSelector.options.verticalAlign; - if (!rangeSelector.options.floating) { - if (verticalAlign === 'bottom') { - this.extraBottomMargin = true; - } else if (verticalAlign !== 'middle') { - this.extraTopMargin = true; - } - } - } + if (!rangeSelector.options.floating) { + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } + } - proceed.call(this, options, callback); + proceed.call(this, options, callback); }); addEvent(Chart, 'update', function (e) { - var chart = this, - options = e.options, - rangeSelector = chart.rangeSelector, - verticalAlign; + var chart = this, + options = e.options, + rangeSelector = chart.rangeSelector, + verticalAlign; - this.extraBottomMargin = false; - this.extraTopMargin = false; - this.isDirtyBox = true; // #7684 - ignored spacingBottom after update + this.extraBottomMargin = false; + this.extraTopMargin = false; + this.isDirtyBox = true; // #7684 - ignored spacingBottom after update - if (rangeSelector) { + if (rangeSelector) { - rangeSelector.render(); + rangeSelector.render(); - verticalAlign = (options.rangeSelector && options.rangeSelector.verticalAlign) || - (rangeSelector.options && rangeSelector.options.verticalAlign); + 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; - } - } + if (!rangeSelector.options.floating) { + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } - } + } }); wrap(Chart.prototype, 'redraw', function (proceed, options, callback) { - var chart = this, - rangeSelector = chart.rangeSelector, - verticalAlign; + var chart = this, + rangeSelector = chart.rangeSelector, + verticalAlign; - if (rangeSelector && !rangeSelector.options.floating) { + if (rangeSelector && !rangeSelector.options.floating) { - rangeSelector.render(); - verticalAlign = rangeSelector.options.verticalAlign; + rangeSelector.render(); + verticalAlign = rangeSelector.options.verticalAlign; - if (verticalAlign === 'bottom') { - this.extraBottomMargin = true; - } else if (verticalAlign !== 'middle') { - this.extraTopMargin = true; - } - } + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } - proceed.call(this, options, callback); + 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; - } - } + 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; - - function renderRangeSelector() { - extremes = chart.xAxis[0].getExtremes(); - if (isNumber(extremes.min)) { - rangeSelector.render(extremes.min, extremes.max); - } - } - - if (rangeSelector) { - // redraw the scroller on setExtremes - unbindSetExtremes = addEvent( - chart.xAxis[0], - 'afterSetExtremes', - function (e) { - rangeSelector.render(e.min, e.max); - } - ); - - // redraw the scroller chart resize - unbindRender = addEvent(chart, 'redraw', renderRangeSelector); - - // do it now - renderRangeSelector(); - } - - // Remove resize/afterSetExtremes at chart destroy - addEvent(chart, 'destroy', function destroyEvents() { - if (rangeSelector) { - unbindRender(); - unbindSetExtremes(); - } - }); + var extremes, + rangeSelector = chart.rangeSelector, + unbindRender, + unbindSetExtremes; + + function renderRangeSelector() { + extremes = chart.xAxis[0].getExtremes(); + if (isNumber(extremes.min)) { + rangeSelector.render(extremes.min, extremes.max); + } + } + + if (rangeSelector) { + // redraw the scroller on setExtremes + unbindSetExtremes = addEvent( + chart.xAxis[0], + 'afterSetExtremes', + function (e) { + rangeSelector.render(e.min, e.max); + } + ); + + // redraw the scroller chart resize + unbindRender = addEvent(chart, 'redraw', renderRangeSelector); + + // do it now + renderRangeSelector(); + } + + // Remove resize/afterSetExtremes at chart destroy + addEvent(chart, 'destroy', function destroyEvents() { + if (rangeSelector) { + unbindRender(); + unbindSetExtremes(); + } + }); }); H.RangeSelector = RangeSelector; /* **************************************************************************** - * End Range Selector code * + * End Range Selector code * *****************************************************************************/ diff --git a/js/parts/Responsive.js b/js/parts/Responsive.js index 67cc0a07f4c..f6adc70e930 100644 --- a/js/parts/Responsive.js +++ b/js/parts/Responsive.js @@ -8,18 +8,18 @@ import H from './Globals.js'; import './Chart.js'; import './Utilities.js'; var Chart = H.Chart, - each = H.each, - inArray = H.inArray, - isArray = H.isArray, - isObject = H.isObject, - pick = H.pick, - splat = H.splat; + each = H.each, + inArray = H.inArray, + isArray = H.isArray, + isObject = H.isObject, + pick = H.pick, + splat = H.splat; /** * Allows setting a set of rules to apply for different screen or chart * sizes. Each rule specifies additional chart options. - * + * * @sample {highstock} stock/demo/responsive/ Stock chart * @sample highcharts/responsive/axis/ Axis * @sample highcharts/responsive/legend/ Legend @@ -31,7 +31,7 @@ var Chart = H.Chart, /** * A set of rules for responsive settings. The rules are executed from * the top down. - * + * * @type {Array} * @sample {highcharts} highcharts/responsive/axis/ Axis changes * @sample {highstock} highcharts/responsive/axis/ Axis changes @@ -44,7 +44,7 @@ var Chart = H.Chart, * A full set of chart options to apply as overrides to the general * chart options. The chart options are applied when the given rule * is active. - * + * * A special case is configuration objects that take arrays, for example * [xAxis](#xAxis), [yAxis](#yAxis) or [series](#series). For these * collections, an `id` option is used to map the new option set to @@ -52,7 +52,7 @@ var Chart = H.Chart, * the item of the same indexupdated. So for example, setting `chartOptions` * with two series items without an `id`, will cause the existing chart's * two series to be updated with respective options. - * + * * @type {Object} * @sample {highstock} stock/demo/responsive/ Stock chart * @sample highcharts/responsive/axis/ Axis @@ -64,7 +64,7 @@ var Chart = H.Chart, /** * Under which conditions the rule applies. - * + * * @type {Object} * @since 5.0.0 * @apioption responsive.rules.condition @@ -75,7 +75,7 @@ var Chart = H.Chart, * rule applies. Return `true` if it applies. This opens for checking * against other metrics than the chart size, or example the document * size or other elements. - * + * * @type {Function} * @context Chart * @since 5.0.0 @@ -84,7 +84,7 @@ var Chart = H.Chart, /** * The responsive rule applies if the chart height is less than this. - * + * * @type {Number} * @since 5.0.0 * @apioption responsive.rules.condition.maxHeight @@ -92,7 +92,7 @@ var Chart = H.Chart, /** * The responsive rule applies if the chart width is less than this. - * + * * @type {Number} * @sample highcharts/responsive/axis/ Max width is 500 * @since 5.0.0 @@ -101,7 +101,7 @@ var Chart = H.Chart, /** * The responsive rule applies if the chart height is greater than this. - * + * * @type {Number} * @default 0 * @since 5.0.0 @@ -110,7 +110,7 @@ var Chart = H.Chart, /** * The responsive rule applies if the chart width is greater than this. - * + * * @type {Number} * @default 0 * @since 5.0.0 @@ -122,76 +122,76 @@ var Chart = H.Chart, * responsiveness. */ Chart.prototype.setResponsive = function (redraw) { - var options = this.options.responsive, - ruleIds = [], - currentResponsive = this.currentResponsive, - currentRuleIds; - - if (options && options.rules) { - each(options.rules, function (rule) { - if (rule._id === undefined) { - rule._id = H.uniqueKey(); - } - - this.matchResponsiveRule(rule, ruleIds, redraw); - }, this); - } - - // Merge matching rules - var mergedOptions = H.merge.apply(0, H.map(ruleIds, function (ruleId) { - return H.find(options.rules, function (rule) { - return rule._id === ruleId; - }).chartOptions; - })); - - // Stringified key for the rules that currently apply. - ruleIds = ruleIds.toString() || undefined; - currentRuleIds = currentResponsive && currentResponsive.ruleIds; - - - // Changes in what rules apply - if (ruleIds !== currentRuleIds) { - - // Undo previous rules. Before we apply a new set of rules, we need to - // roll back completely to base options (#6291). - if (currentResponsive) { - this.update(currentResponsive.undoOptions, redraw); - } - - if (ruleIds) { - // Get undo-options for matching rules - this.currentResponsive = { - ruleIds: ruleIds, - mergedOptions: mergedOptions, - undoOptions: this.currentOptions(mergedOptions) - }; - - this.update(mergedOptions, redraw); - - } else { - this.currentResponsive = undefined; - } - } + var options = this.options.responsive, + ruleIds = [], + currentResponsive = this.currentResponsive, + currentRuleIds; + + if (options && options.rules) { + each(options.rules, function (rule) { + if (rule._id === undefined) { + rule._id = H.uniqueKey(); + } + + this.matchResponsiveRule(rule, ruleIds, redraw); + }, this); + } + + // Merge matching rules + var mergedOptions = H.merge.apply(0, H.map(ruleIds, function (ruleId) { + return H.find(options.rules, function (rule) { + return rule._id === ruleId; + }).chartOptions; + })); + + // Stringified key for the rules that currently apply. + ruleIds = ruleIds.toString() || undefined; + currentRuleIds = currentResponsive && currentResponsive.ruleIds; + + + // Changes in what rules apply + if (ruleIds !== currentRuleIds) { + + // Undo previous rules. Before we apply a new set of rules, we need to + // roll back completely to base options (#6291). + if (currentResponsive) { + this.update(currentResponsive.undoOptions, redraw); + } + + if (ruleIds) { + // Get undo-options for matching rules + this.currentResponsive = { + ruleIds: ruleIds, + mergedOptions: mergedOptions, + undoOptions: this.currentOptions(mergedOptions) + }; + + this.update(mergedOptions, redraw); + + } else { + this.currentResponsive = undefined; + } + } }; /** * Handle a single responsiveness rule */ Chart.prototype.matchResponsiveRule = function (rule, matches) { - var condition = rule.condition, - fn = condition.callback || function () { - return ( - this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) && - this.chartHeight <= - pick(condition.maxHeight, Number.MAX_VALUE) && - this.chartWidth >= pick(condition.minWidth, 0) && - this.chartHeight >= pick(condition.minHeight, 0) - ); - }; - - if (fn.call(this)) { - matches.push(rule._id); - } + var condition = rule.condition, + fn = condition.callback || function () { + return ( + this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) && + this.chartHeight <= + pick(condition.maxHeight, Number.MAX_VALUE) && + this.chartWidth >= pick(condition.minWidth, 0) && + this.chartHeight >= pick(condition.minHeight, 0) + ); + }; + + if (fn.call(this)) { + matches.push(rule._id); + } }; @@ -202,42 +202,42 @@ Chart.prototype.matchResponsiveRule = function (rule, matches) { */ Chart.prototype.currentOptions = function (options) { - var ret = {}; - - /** - * Recurse over a set of options and its current values, - * and store the current values in the ret object. - */ - function getCurrent(options, curr, ret, depth) { - var i; - H.objectEach(options, function (val, key) { - if (!depth && inArray(key, ['series', 'xAxis', 'yAxis']) > -1) { - val = splat(val); - - ret[key] = []; - - // Iterate over collections like series, xAxis or yAxis and map - // the items by index. - for (i = 0; i < val.length; i++) { - if (curr[key][i]) { // Item exists in current data (#6347) - ret[key][i] = {}; - getCurrent( - val[i], - curr[key][i], - ret[key][i], - depth + 1 - ); - } - } - } else if (isObject(val)) { - ret[key] = isArray(val) ? [] : {}; - getCurrent(val, curr[key] || {}, ret[key], depth + 1); - } else { - ret[key] = curr[key] || null; - } - }); - } - - getCurrent(options, this.options, ret, 0); - return ret; + var ret = {}; + + /** + * Recurse over a set of options and its current values, + * and store the current values in the ret object. + */ + function getCurrent(options, curr, ret, depth) { + var i; + H.objectEach(options, function (val, key) { + if (!depth && inArray(key, ['series', 'xAxis', 'yAxis']) > -1) { + val = splat(val); + + ret[key] = []; + + // Iterate over collections like series, xAxis or yAxis and map + // the items by index. + for (i = 0; i < val.length; i++) { + if (curr[key][i]) { // Item exists in current data (#6347) + ret[key][i] = {}; + getCurrent( + val[i], + curr[key][i], + ret[key][i], + depth + 1 + ); + } + } + } else if (isObject(val)) { + ret[key] = isArray(val) ? [] : {}; + getCurrent(val, curr[key] || {}, ret[key], depth + 1); + } else { + ret[key] = curr[key] || null; + } + }); + } + + getCurrent(options, this.options, ret, 0); + return ret; }; diff --git a/js/parts/ScatterSeries.js b/js/parts/ScatterSeries.js index 43663724d0f..4627f9ba884 100644 --- a/js/parts/ScatterSeries.js +++ b/js/parts/ScatterSeries.js @@ -9,7 +9,7 @@ import './Utilities.js'; import './Options.js'; import './Series.js'; var Series = H.Series, - seriesType = H.seriesType; + seriesType = H.seriesType; /** * A scatter plot uses cartesian coordinates to display values for two variables @@ -23,80 +23,80 @@ var Series = H.Series, */ seriesType('scatter', 'line', { - /** - * The width of the line connecting the data points. - * - * @sample {highcharts} highcharts/plotoptions/scatter-linewidth-none/ - * 0 by default - * @sample {highcharts} highcharts/plotoptions/scatter-linewidth-1/ - * 1px - * @product highcharts highstock - */ - lineWidth: 0, + /** + * The width of the line connecting the data points. + * + * @sample {highcharts} highcharts/plotoptions/scatter-linewidth-none/ + * 0 by default + * @sample {highcharts} highcharts/plotoptions/scatter-linewidth-1/ + * 1px + * @product highcharts highstock + */ + lineWidth: 0, - findNearestPointBy: 'xy', - marker: { - enabled: true // Overrides auto-enabling in line series (#3647) - }, + findNearestPointBy: 'xy', + marker: { + enabled: true // Overrides auto-enabling in line series (#3647) + }, - /** - * Sticky tracking of mouse events. When true, the `mouseOut` event - * on a series isn't triggered until the mouse moves over another series, - * or out of the plot area. When false, the `mouseOut` event on a series - * is triggered when the mouse leaves the area around the series' graph - * or markers. This also implies the tooltip. When `stickyTracking` - * is false and `tooltip.shared` is false, the tooltip will be hidden - * when moving the mouse between series. - * - * @type {Boolean} - * @default false - * @product highcharts highstock - * @apioption plotOptions.scatter.stickyTracking - */ + /** + * Sticky tracking of mouse events. When true, the `mouseOut` event + * on a series isn't triggered until the mouse moves over another series, + * or out of the plot area. When false, the `mouseOut` event on a series + * is triggered when the mouse leaves the area around the series' graph + * or markers. This also implies the tooltip. When `stickyTracking` + * is false and `tooltip.shared` is false, the tooltip will be hidden + * when moving the mouse between series. + * + * @type {Boolean} + * @default false + * @product highcharts highstock + * @apioption plotOptions.scatter.stickyTracking + */ - /** - * A configuration object for the tooltip rendering of each single - * series. Properties are inherited from #tooltip. - * Overridable properties are `headerFormat`, `pointFormat`, `yDecimals`, - * `xDateFormat`, `yPrefix` and `ySuffix`. Unlike other series, in - * a scatter plot the series.name by default shows in the headerFormat - * and point.x and point.y in the pointFormat. - * - * @product highcharts highstock - */ - tooltip: { - /*= if (build.classic) { =*/ - headerFormat: - '\u25CF ' + - ' {series.name}
', - /*= } else { =*/ + /** + * A configuration object for the tooltip rendering of each single + * series. Properties are inherited from #tooltip. + * Overridable properties are `headerFormat`, `pointFormat`, `yDecimals`, + * `xDateFormat`, `yPrefix` and `ySuffix`. Unlike other series, in + * a scatter plot the series.name by default shows in the headerFormat + * and point.x and point.y in the pointFormat. + * + * @product highcharts highstock + */ + tooltip: { + /*= if (build.classic) { =*/ + headerFormat: + '\u25CF ' + + ' {series.name}
', + /*= } else { =*/ - headerFormat: - '\u25CF ' + - ' {series.name}
', - /*= } =*/ + headerFormat: + '\u25CF ' + + ' {series.name}
', + /*= } =*/ - pointFormat: 'x: {point.x}
y: {point.y}
' - } + pointFormat: 'x: {point.x}
y: {point.y}
' + } // Prototype members }, { - sorted: false, - requireSorting: false, - noSharedTooltip: true, - trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], - takeOrdinalPosition: false, // #2342 - drawGraph: function () { - if (this.options.lineWidth) { - Series.prototype.drawGraph.call(this); - } - } + sorted: false, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], + takeOrdinalPosition: false, // #2342 + drawGraph: function () { + if (this.options.lineWidth) { + Series.prototype.drawGraph.call(this); + } + } }); /** * A `scatter` series. If the [type](#series.scatter.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.scatter * @excluding dataParser,dataURL @@ -107,21 +107,21 @@ seriesType('scatter', 'line', { /** * An array of data points for the series. For the `scatter` series * type, points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 0], @@ -129,12 +129,12 @@ seriesType('scatter', 'line', { * [2, 9] * ] * ``` - * + * * 3. 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.scatter.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -148,7 +148,7 @@ seriesType('scatter', 'line', { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ diff --git a/js/parts/Scrollbar.js b/js/parts/Scrollbar.js index 6534ae170d2..f1746168933 100644 --- a/js/parts/Scrollbar.js +++ b/js/parts/Scrollbar.js @@ -10,210 +10,210 @@ import './Utilities.js'; import './Axis.js'; import './Options.js'; var addEvent = H.addEvent, - Axis = H.Axis, - correctFloat = H.correctFloat, - defaultOptions = H.defaultOptions, - defined = H.defined, - destroyObjectProperties = H.destroyObjectProperties, - each = H.each, - fireEvent = H.fireEvent, - hasTouch = H.hasTouch, - isTouchDevice = H.isTouchDevice, - merge = H.merge, - pick = H.pick, - removeEvent = H.removeEvent, - svg = H.svg, - wrap = H.wrap, - swapXY; + Axis = H.Axis, + correctFloat = H.correctFloat, + defaultOptions = H.defaultOptions, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + fireEvent = H.fireEvent, + hasTouch = H.hasTouch, + isTouchDevice = H.isTouchDevice, + merge = H.merge, + pick = H.pick, + removeEvent = H.removeEvent, + svg = H.svg, + wrap = H.wrap, + swapXY; /** - * + * * The scrollbar is a means of panning over the X axis of a stock chart. - * + * * 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 = { - /** - * 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. - * - * @type {Number} - * @sample {highstock} stock/scrollbar/height/ A 30px scrollbar - * @product highstock - */ - height: isTouchDevice ? 20 : 14, - - /** - * The border rounding radius of the bar. - * - * @type {Number} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default 0 - * @product highstock - */ - barBorderRadius: 0, - - /** - * The corner radius of the scrollbar buttons. - * - * @type {Number} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default 0 - * @product highstock - */ - buttonBorderRadius: 0, - - /** - * Whether to redraw the main chart as the scrollbar or the navigator - * zoomed window is moved. Defaults to `true` for modern browsers and - * `false` for legacy IE browsers as well as mobile devices. - * - * @type {Boolean} - * @since 1.3 - * @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. - * - * @type {Number} - * @default 6 - * @since 1.2.5 - * @product highstock - */ - minWidth: 6, - - step: 0.2, - - /** - * The z index of the scrollbar group. - */ - zIndex: 3, - /*= if (build.classic) { =*/ - - /** - * The background color of the scrollbar itself. - * - * @type {Color} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default #cccccc - * @product highstock - */ - barBackgroundColor: '${palette.neutralColor20}', - - /** - * The width of the bar's border. - * - * @type {Number} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default 1 - * @product highstock - */ - barBorderWidth: 1, - - /** - * The color of the scrollbar's border. - * - * @type {Color} - * @default #cccccc - * @product highstock - */ - barBorderColor: '${palette.neutralColor20}', - - /** - * The color of the small arrow inside the scrollbar buttons. - * - * @type {Color} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default #333333 - * @product highstock - */ - buttonArrowColor: '${palette.neutralColor80}', - - /** - * The color of scrollbar buttons. - * - * @type {Color} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default #e6e6e6 - * @product highstock - */ - buttonBackgroundColor: '${palette.neutralColor10}', - - /** - * The color of the border of the scrollbar buttons. - * - * @type {Color} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default #cccccc - * @product highstock - */ - buttonBorderColor: '${palette.neutralColor20}', - - /** - * The border width of the scrollbar buttons. - * - * @type {Number} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default 1 - * @product highstock - */ - buttonBorderWidth: 1, - - /** - * The color of the small rifles in the middle of the scrollbar. - * - * @type {Color} - * @default #333333 - * @product highstock - */ - rifleColor: '${palette.neutralColor80}', - - /** - * The color of the track background. - * - * @type {Color} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default #f2f2f2 - * @product highstock - */ - trackBackgroundColor: '${palette.neutralColor5}', - - /** - * The color of the border of the scrollbar track. - * - * @type {Color} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default #f2f2f2 - * @product highstock - */ - trackBorderColor: '${palette.neutralColor5}', - - /** - * The width of the border of the scrollbar track. - * - * @type {Number} - * @sample {highstock} stock/scrollbar/style/ Scrollbar styling - * @default 1 - * @product highstock - */ - trackBorderWidth: 1 - /*= } =*/ + /** + * 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. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/height/ A 30px scrollbar + * @product highstock + */ + height: isTouchDevice ? 20 : 14, + + /** + * The border rounding radius of the bar. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 0 + * @product highstock + */ + barBorderRadius: 0, + + /** + * The corner radius of the scrollbar buttons. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 0 + * @product highstock + */ + buttonBorderRadius: 0, + + /** + * Whether to redraw the main chart as the scrollbar or the navigator + * zoomed window is moved. Defaults to `true` for modern browsers and + * `false` for legacy IE browsers as well as mobile devices. + * + * @type {Boolean} + * @since 1.3 + * @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. + * + * @type {Number} + * @default 6 + * @since 1.2.5 + * @product highstock + */ + minWidth: 6, + + step: 0.2, + + /** + * The z index of the scrollbar group. + */ + zIndex: 3, + /*= if (build.classic) { =*/ + + /** + * The background color of the scrollbar itself. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #cccccc + * @product highstock + */ + barBackgroundColor: '${palette.neutralColor20}', + + /** + * The width of the bar's border. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 1 + * @product highstock + */ + barBorderWidth: 1, + + /** + * The color of the scrollbar's border. + * + * @type {Color} + * @default #cccccc + * @product highstock + */ + barBorderColor: '${palette.neutralColor20}', + + /** + * The color of the small arrow inside the scrollbar buttons. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #333333 + * @product highstock + */ + buttonArrowColor: '${palette.neutralColor80}', + + /** + * The color of scrollbar buttons. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #e6e6e6 + * @product highstock + */ + buttonBackgroundColor: '${palette.neutralColor10}', + + /** + * The color of the border of the scrollbar buttons. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #cccccc + * @product highstock + */ + buttonBorderColor: '${palette.neutralColor20}', + + /** + * The border width of the scrollbar buttons. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 1 + * @product highstock + */ + buttonBorderWidth: 1, + + /** + * The color of the small rifles in the middle of the scrollbar. + * + * @type {Color} + * @default #333333 + * @product highstock + */ + rifleColor: '${palette.neutralColor80}', + + /** + * The color of the track background. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #f2f2f2 + * @product highstock + */ + trackBackgroundColor: '${palette.neutralColor5}', + + /** + * The color of the border of the scrollbar track. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #f2f2f2 + * @product highstock + */ + trackBorderColor: '${palette.neutralColor5}', + + /** + * The width of the border of the scrollbar track. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 1 + * @product highstock + */ + trackBorderWidth: 1 + /*= } =*/ }; defaultOptions.scrollbar = merge(true, defaultScrollbarOptions, defaultOptions.scrollbar); @@ -225,19 +225,19 @@ defaultOptions.scrollbar = merge(true, defaultScrollbarOptions, defaultOptions.s * @param {Boolean} vertical - if vertical scrollbar, swap x-y values */ H.swapXY = swapXY = function (path, vertical) { - var i, - len = path.length, - temp; - - if (vertical) { - for (i = 0; i < len; i += 3) { - temp = path[i + 1]; - path[i + 1] = path[i + 2]; - path[i + 2] = temp; - } - } - - return path; + var i, + len = path.length, + temp; + + if (vertical) { + for (i = 0; i < len; i += 3) { + temp = path[i + 1]; + path[i + 1] = path[i + 2]; + path[i + 2] = temp; + } + } + + return path; }; /** @@ -250,681 +250,681 @@ H.swapXY = swapXY = function (path, vertical) { * @param {Object} chart */ function Scrollbar(renderer, options, chart) { // docs - this.init(renderer, options, chart); + this.init(renderer, options, chart); } Scrollbar.prototype = { - init: function (renderer, options, chart) { - - this.scrollbarButtons = []; - - this.renderer = renderer; - - this.userOptions = options; - this.options = merge(defaultScrollbarOptions, options); - - this.chart = chart; - - this.size = pick(this.options.size, this.options.height); // backward compatibility - - // Init - if (options.enabled) { - this.render(); - this.initEvents(); - this.addEvents(); - } - }, - - /** - * Render scrollbar with all required items. - */ - render: function () { - var scroller = this, - renderer = scroller.renderer, - options = scroller.options, - size = scroller.size, - group; - - // Draw the scrollbar group - scroller.group = group = renderer.g('scrollbar').attr({ - zIndex: options.zIndex, - translateY: -99999 - }).add(); - - // Draw the scrollbar track: - scroller.track = renderer.rect() - .addClass('highcharts-scrollbar-track') - .attr({ - x: 0, - r: options.trackBorderRadius || 0, - height: size, - width: size - }).add(group); - - /*= if (build.classic) { =*/ - scroller.track.attr({ - fill: options.trackBackgroundColor, - stroke: options.trackBorderColor, - 'stroke-width': options.trackBorderWidth - }); - /*= } =*/ - this.trackBorderWidth = scroller.track.strokeWidth(); - scroller.track.attr({ - y: -this.trackBorderWidth % 2 / 2 - }); - - - // Draw the scrollbar itself - scroller.scrollbarGroup = renderer.g().add(group); - - scroller.scrollbar = renderer.rect() - .addClass('highcharts-scrollbar-thumb') - .attr({ - height: size, - width: size, - r: options.barBorderRadius || 0 - }).add(scroller.scrollbarGroup); - - scroller.scrollbarRifles = renderer.path( - swapXY([ - 'M', - -3, size / 4, - 'L', - -3, 2 * size / 3, - 'M', - 0, size / 4, - 'L', - 0, 2 * size / 3, - 'M', - 3, size / 4, - 'L', - 3, 2 * size / 3 - ], options.vertical)) - .addClass('highcharts-scrollbar-rifles') - .add(scroller.scrollbarGroup); - - /*= if (build.classic) { =*/ - scroller.scrollbar.attr({ - fill: options.barBackgroundColor, - stroke: options.barBorderColor, - 'stroke-width': options.barBorderWidth - }); - scroller.scrollbarRifles.attr({ - stroke: options.rifleColor, - 'stroke-width': 1 - }); - /*= } =*/ - scroller.scrollbarStrokeWidth = scroller.scrollbar.strokeWidth(); - scroller.scrollbarGroup.translate( - -scroller.scrollbarStrokeWidth % 2 / 2, - -scroller.scrollbarStrokeWidth % 2 / 2 - ); - - // Draw the buttons: - scroller.drawScrollbarButton(0); - scroller.drawScrollbarButton(1); - }, - - /** - * Position the scrollbar, method called from a parent with defined dimensions - * @param {Number} x - x-position on the chart - * @param {Number} y - y-position on the chart - * @param {Number} width - width of the scrollbar - * @param {Number} height - height of the scorllbar - */ - position: function (x, y, width, height) { - var scroller = this, - options = scroller.options, - vertical = options.vertical, - xOffset = height, - yOffset = 0, - method = scroller.rendered ? 'animate' : 'attr'; - - scroller.x = x; - scroller.y = y + this.trackBorderWidth; - scroller.width = width; // width with buttons - scroller.height = height; - scroller.xOffset = xOffset; - scroller.yOffset = yOffset; - - // If Scrollbar is a vertical type, swap options: - if (vertical) { - scroller.width = scroller.yOffset = width = yOffset = scroller.size; - scroller.xOffset = xOffset = 0; - scroller.barWidth = height - width * 2; // width without buttons - scroller.x = x = x + scroller.options.margin; - } else { - scroller.height = scroller.xOffset = height = xOffset = scroller.size; - scroller.barWidth = width - height * 2; // width without buttons - scroller.y = scroller.y + scroller.options.margin; - } - - // Set general position for a group: - scroller.group[method]({ - translateX: x, - translateY: scroller.y - }); - - // Resize background/track: - scroller.track[method]({ - width: width, - height: height - }); - - // Move right/bottom button ot it's place: - scroller.scrollbarButtons[1][method]({ - translateX: vertical ? 0 : width - xOffset, - translateY: vertical ? height - yOffset : 0 - }); - }, - - /** - * Draw the scrollbar buttons with arrows - * @param {Number} index 0 is left, 1 is right - */ - drawScrollbarButton: function (index) { - var scroller = this, - renderer = scroller.renderer, - scrollbarButtons = scroller.scrollbarButtons, - options = scroller.options, - size = scroller.size, - group, - tempElem; - - group = renderer.g().add(scroller.group); - scrollbarButtons.push(group); - - // Create a rectangle for the scrollbar button - tempElem = renderer.rect() - .addClass('highcharts-scrollbar-button') - .add(group); - - /*= if (build.classic) { =*/ - // Presentational attributes - tempElem.attr({ - stroke: options.buttonBorderColor, - 'stroke-width': options.buttonBorderWidth, - fill: options.buttonBackgroundColor - }); - /*= } =*/ - - // Place the rectangle based on the rendered stroke width - tempElem.attr(tempElem.crisp({ - x: -0.5, - y: -0.5, - width: size + 1, // +1 to compensate for crispifying in rect method - height: size + 1, - r: options.buttonBorderRadius - }, tempElem.strokeWidth())); - - // Button arrow - tempElem = renderer - .path(swapXY([ - 'M', - size / 2 + (index ? -1 : 1), - size / 2 - 3, - 'L', - size / 2 + (index ? -1 : 1), - size / 2 + 3, - 'L', - size / 2 + (index ? 2 : -2), - size / 2 - ], options.vertical)) - .addClass('highcharts-scrollbar-arrow') - .add(scrollbarButtons[index]); - - /*= if (build.classic) { =*/ - tempElem.attr({ - fill: options.buttonArrowColor - }); - /*= } =*/ - }, - - /** - * Set scrollbar size, with a given scale. - * @param {Number} from - scale (0-1) where bar should start - * @param {Number} to - scale (0-1) where bar should end - */ - setRange: function (from, to) { - var scroller = this, - options = scroller.options, - vertical = options.vertical, - minWidth = options.minWidth, - fullWidth = scroller.barWidth, - fromPX, - toPX, - newPos, - newSize, - newRiflesPos, - method = this.rendered && !this.hasDragged ? 'animate' : 'attr'; - - if (!defined(fullWidth)) { - return; - } - - from = Math.max(from, 0); - fromPX = Math.ceil(fullWidth * from); - toPX = fullWidth * Math.min(to, 1); - scroller.calculatedWidth = newSize = correctFloat(toPX - fromPX); - - // We need to recalculate position, if minWidth is used - if (newSize < minWidth) { - fromPX = (fullWidth - minWidth + newSize) * from; - newSize = minWidth; - } - newPos = Math.floor(fromPX + scroller.xOffset + scroller.yOffset); - newRiflesPos = newSize / 2 - 0.5; // -0.5 -> rifle line width / 2 - - // Store current position: - scroller.from = from; - scroller.to = to; - - if (!vertical) { - scroller.scrollbarGroup[method]({ - translateX: newPos - }); - scroller.scrollbar[method]({ - width: newSize - }); - scroller.scrollbarRifles[method]({ - translateX: newRiflesPos - }); - scroller.scrollbarLeft = newPos; - scroller.scrollbarTop = 0; - } else { - scroller.scrollbarGroup[method]({ - translateY: newPos - }); - scroller.scrollbar[method]({ - height: newSize - }); - scroller.scrollbarRifles[method]({ - translateY: newRiflesPos - }); - scroller.scrollbarTop = newPos; - scroller.scrollbarLeft = 0; - } - - if (newSize <= 12) { - scroller.scrollbarRifles.hide(); - } else { - scroller.scrollbarRifles.show(true); - } - - // Show or hide the scrollbar based on the showFull setting - if (options.showFull === false) { - if (from <= 0 && to >= 1) { - scroller.group.hide(); - } else { - scroller.group.show(); - } - } - - scroller.rendered = true; - }, - - /** - * Init events methods, so we have an access to the Scrollbar itself - */ - initEvents: function () { - var scroller = this; - /** - * Event handler for the mouse move event. - */ - scroller.mouseMoveHandler = function (e) { - var normalizedEvent = scroller.chart.pointer.normalize(e), - options = scroller.options, - direction = options.vertical ? 'chartY' : 'chartX', - initPositions = scroller.initPositions, - scrollPosition, - chartPosition, - change; - - // 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 (scroller.grabbedCenter && (!e.touches || e.touches[0][direction] !== 0)) { // #4696, scrollbar failed on Android - chartPosition = scroller.cursorToScrollbarPosition(normalizedEvent)[direction]; - scrollPosition = scroller[direction]; - - change = chartPosition - scrollPosition; - - scroller.hasDragged = true; - scroller.updatePosition(initPositions[0] + change, initPositions[1] + change); - - if (scroller.hasDragged) { - fireEvent(scroller, 'changed', { - from: scroller.from, - to: scroller.to, - trigger: 'scrollbar', - DOMType: e.type, - DOMEvent: e - }); - } - } - }; - - /** - * Event handler for the mouse up event. - */ - scroller.mouseUpHandler = function (e) { - if (scroller.hasDragged) { - fireEvent(scroller, 'changed', { - from: scroller.from, - to: scroller.to, - trigger: 'scrollbar', - DOMType: e.type, - DOMEvent: e - }); - } - scroller.grabbedCenter = scroller.hasDragged = scroller.chartX = scroller.chartY = null; - }; - - scroller.mouseDownHandler = function (e) { - var normalizedEvent = scroller.chart.pointer.normalize(e), - mousePosition = scroller.cursorToScrollbarPosition(normalizedEvent); - - scroller.chartX = mousePosition.chartX; - scroller.chartY = mousePosition.chartY; - scroller.initPositions = [scroller.from, scroller.to]; - - scroller.grabbedCenter = true; - }; - - scroller.buttonToMinClick = function (e) { - var range = correctFloat(scroller.to - scroller.from) * scroller.options.step; - scroller.updatePosition(correctFloat(scroller.from - range), correctFloat(scroller.to - range)); - fireEvent(scroller, 'changed', { - from: scroller.from, - to: scroller.to, - trigger: 'scrollbar', - DOMEvent: e - }); - }; - - scroller.buttonToMaxClick = function (e) { - var range = (scroller.to - scroller.from) * scroller.options.step; - scroller.updatePosition(scroller.from + range, scroller.to + range); - fireEvent(scroller, 'changed', { - from: scroller.from, - to: scroller.to, - trigger: 'scrollbar', - DOMEvent: e - }); - }; - - scroller.trackClick = function (e) { - var normalizedEvent = scroller.chart.pointer.normalize(e), - range = scroller.to - scroller.from, - top = scroller.y + scroller.scrollbarTop, - left = scroller.x + scroller.scrollbarLeft; - - if ((scroller.options.vertical && normalizedEvent.chartY > top) || - (!scroller.options.vertical && normalizedEvent.chartX > left)) { - // On the top or on the left side of the track: - scroller.updatePosition(scroller.from + range, scroller.to + range); - } else { - // On the bottom or the right side of the track: - scroller.updatePosition(scroller.from - range, scroller.to - range); - } - - fireEvent(scroller, 'changed', { - from: scroller.from, - to: scroller.to, - trigger: 'scrollbar', - DOMEvent: e - }); - }; - }, - - /** - * Get normalized (0-1) cursor position over the scrollbar - * @param {Event} normalizedEvent - normalized event, with chartX and chartY values - * @return {Object} Local position {chartX, chartY} - */ - cursorToScrollbarPosition: function (normalizedEvent) { - var scroller = this, - options = scroller.options, - minWidthDifference = options.minWidth > scroller.calculatedWidth ? options.minWidth : 0; // minWidth distorts translation - - return { - chartX: (normalizedEvent.chartX - scroller.x - scroller.xOffset) / (scroller.barWidth - minWidthDifference), - chartY: (normalizedEvent.chartY - scroller.y - scroller.yOffset) / (scroller.barWidth - minWidthDifference) - }; - }, - - /** - * Update position option in the Scrollbar, with normalized 0-1 scale - */ - updatePosition: function (from, to) { - if (to > 1) { - from = correctFloat(1 - correctFloat(to - from)); - to = 1; - } - - if (from < 0) { - to = correctFloat(to - from); - from = 0; - } - - this.from = from; - this.to = to; - }, - - /** - * Update the scrollbar with new options - */ - update: function (options) { - this.destroy(); - this.init(this.chart.renderer, merge(true, this.options, options), this.chart); - }, - - /** - * Set up the mouse and touch events for the Scrollbar - */ - addEvents: function () { - var buttonsOrder = this.options.inverted ? [1, 0] : [0, 1], - buttons = this.scrollbarButtons, - bar = this.scrollbarGroup.element, - track = this.track.element, - mouseDownHandler = this.mouseDownHandler, - mouseMoveHandler = this.mouseMoveHandler, - mouseUpHandler = this.mouseUpHandler, - _events; - - // Mouse events - _events = [ - [buttons[buttonsOrder[0]].element, 'click', this.buttonToMinClick], - [buttons[buttonsOrder[1]].element, 'click', this.buttonToMaxClick], - [track, 'click', this.trackClick], - [bar, 'mousedown', mouseDownHandler], - [bar.ownerDocument, 'mousemove', mouseMoveHandler], - [bar.ownerDocument, 'mouseup', mouseUpHandler] - ]; - - // Touch events - if (hasTouch) { - _events.push( - [bar, 'touchstart', mouseDownHandler], - [bar.ownerDocument, 'touchmove', mouseMoveHandler], - [bar.ownerDocument, 'touchend', mouseUpHandler] - ); - } - - // Add them all - each(_events, function (args) { - addEvent.apply(null, args); - }); - this._events = _events; - }, - - /** - * Removes the event handlers attached previously with addEvents. - */ - removeEvents: function () { - each(this._events, function (args) { - removeEvent.apply(null, args); - }); - this._events.length = 0; - }, - - /** - * Destroys allocated elements. - */ - destroy: function () { - - var scroller = this.chart.scroller; - - // Disconnect events added in addEvents - this.removeEvents(); - - // Destroy properties - each(['track', 'scrollbarRifles', 'scrollbar', 'scrollbarGroup', 'group'], function (prop) { - if (this[prop] && this[prop].destroy) { - this[prop] = this[prop].destroy(); - } - }, this); - - if (scroller && this === scroller.scrollbar) { // #6421, chart may have more scrollbars - scroller.scrollbar = null; - - // Destroy elements in collection - destroyObjectProperties(scroller.scrollbarButtons); - } - } + init: function (renderer, options, chart) { + + this.scrollbarButtons = []; + + this.renderer = renderer; + + this.userOptions = options; + this.options = merge(defaultScrollbarOptions, options); + + this.chart = chart; + + this.size = pick(this.options.size, this.options.height); // backward compatibility + + // Init + if (options.enabled) { + this.render(); + this.initEvents(); + this.addEvents(); + } + }, + + /** + * Render scrollbar with all required items. + */ + render: function () { + var scroller = this, + renderer = scroller.renderer, + options = scroller.options, + size = scroller.size, + group; + + // Draw the scrollbar group + scroller.group = group = renderer.g('scrollbar').attr({ + zIndex: options.zIndex, + translateY: -99999 + }).add(); + + // Draw the scrollbar track: + scroller.track = renderer.rect() + .addClass('highcharts-scrollbar-track') + .attr({ + x: 0, + r: options.trackBorderRadius || 0, + height: size, + width: size + }).add(group); + + /*= if (build.classic) { =*/ + scroller.track.attr({ + fill: options.trackBackgroundColor, + stroke: options.trackBorderColor, + 'stroke-width': options.trackBorderWidth + }); + /*= } =*/ + this.trackBorderWidth = scroller.track.strokeWidth(); + scroller.track.attr({ + y: -this.trackBorderWidth % 2 / 2 + }); + + + // Draw the scrollbar itself + scroller.scrollbarGroup = renderer.g().add(group); + + scroller.scrollbar = renderer.rect() + .addClass('highcharts-scrollbar-thumb') + .attr({ + height: size, + width: size, + r: options.barBorderRadius || 0 + }).add(scroller.scrollbarGroup); + + scroller.scrollbarRifles = renderer.path( + swapXY([ + 'M', + -3, size / 4, + 'L', + -3, 2 * size / 3, + 'M', + 0, size / 4, + 'L', + 0, 2 * size / 3, + 'M', + 3, size / 4, + 'L', + 3, 2 * size / 3 + ], options.vertical)) + .addClass('highcharts-scrollbar-rifles') + .add(scroller.scrollbarGroup); + + /*= if (build.classic) { =*/ + scroller.scrollbar.attr({ + fill: options.barBackgroundColor, + stroke: options.barBorderColor, + 'stroke-width': options.barBorderWidth + }); + scroller.scrollbarRifles.attr({ + stroke: options.rifleColor, + 'stroke-width': 1 + }); + /*= } =*/ + scroller.scrollbarStrokeWidth = scroller.scrollbar.strokeWidth(); + scroller.scrollbarGroup.translate( + -scroller.scrollbarStrokeWidth % 2 / 2, + -scroller.scrollbarStrokeWidth % 2 / 2 + ); + + // Draw the buttons: + scroller.drawScrollbarButton(0); + scroller.drawScrollbarButton(1); + }, + + /** + * Position the scrollbar, method called from a parent with defined dimensions + * @param {Number} x - x-position on the chart + * @param {Number} y - y-position on the chart + * @param {Number} width - width of the scrollbar + * @param {Number} height - height of the scorllbar + */ + position: function (x, y, width, height) { + var scroller = this, + options = scroller.options, + vertical = options.vertical, + xOffset = height, + yOffset = 0, + method = scroller.rendered ? 'animate' : 'attr'; + + scroller.x = x; + scroller.y = y + this.trackBorderWidth; + scroller.width = width; // width with buttons + scroller.height = height; + scroller.xOffset = xOffset; + scroller.yOffset = yOffset; + + // If Scrollbar is a vertical type, swap options: + if (vertical) { + scroller.width = scroller.yOffset = width = yOffset = scroller.size; + scroller.xOffset = xOffset = 0; + scroller.barWidth = height - width * 2; // width without buttons + scroller.x = x = x + scroller.options.margin; + } else { + scroller.height = scroller.xOffset = height = xOffset = scroller.size; + scroller.barWidth = width - height * 2; // width without buttons + scroller.y = scroller.y + scroller.options.margin; + } + + // Set general position for a group: + scroller.group[method]({ + translateX: x, + translateY: scroller.y + }); + + // Resize background/track: + scroller.track[method]({ + width: width, + height: height + }); + + // Move right/bottom button ot it's place: + scroller.scrollbarButtons[1][method]({ + translateX: vertical ? 0 : width - xOffset, + translateY: vertical ? height - yOffset : 0 + }); + }, + + /** + * Draw the scrollbar buttons with arrows + * @param {Number} index 0 is left, 1 is right + */ + drawScrollbarButton: function (index) { + var scroller = this, + renderer = scroller.renderer, + scrollbarButtons = scroller.scrollbarButtons, + options = scroller.options, + size = scroller.size, + group, + tempElem; + + group = renderer.g().add(scroller.group); + scrollbarButtons.push(group); + + // Create a rectangle for the scrollbar button + tempElem = renderer.rect() + .addClass('highcharts-scrollbar-button') + .add(group); + + /*= if (build.classic) { =*/ + // Presentational attributes + tempElem.attr({ + stroke: options.buttonBorderColor, + 'stroke-width': options.buttonBorderWidth, + fill: options.buttonBackgroundColor + }); + /*= } =*/ + + // Place the rectangle based on the rendered stroke width + tempElem.attr(tempElem.crisp({ + x: -0.5, + y: -0.5, + width: size + 1, // +1 to compensate for crispifying in rect method + height: size + 1, + r: options.buttonBorderRadius + }, tempElem.strokeWidth())); + + // Button arrow + tempElem = renderer + .path(swapXY([ + 'M', + size / 2 + (index ? -1 : 1), + size / 2 - 3, + 'L', + size / 2 + (index ? -1 : 1), + size / 2 + 3, + 'L', + size / 2 + (index ? 2 : -2), + size / 2 + ], options.vertical)) + .addClass('highcharts-scrollbar-arrow') + .add(scrollbarButtons[index]); + + /*= if (build.classic) { =*/ + tempElem.attr({ + fill: options.buttonArrowColor + }); + /*= } =*/ + }, + + /** + * Set scrollbar size, with a given scale. + * @param {Number} from - scale (0-1) where bar should start + * @param {Number} to - scale (0-1) where bar should end + */ + setRange: function (from, to) { + var scroller = this, + options = scroller.options, + vertical = options.vertical, + minWidth = options.minWidth, + fullWidth = scroller.barWidth, + fromPX, + toPX, + newPos, + newSize, + newRiflesPos, + method = this.rendered && !this.hasDragged ? 'animate' : 'attr'; + + if (!defined(fullWidth)) { + return; + } + + from = Math.max(from, 0); + fromPX = Math.ceil(fullWidth * from); + toPX = fullWidth * Math.min(to, 1); + scroller.calculatedWidth = newSize = correctFloat(toPX - fromPX); + + // We need to recalculate position, if minWidth is used + if (newSize < minWidth) { + fromPX = (fullWidth - minWidth + newSize) * from; + newSize = minWidth; + } + newPos = Math.floor(fromPX + scroller.xOffset + scroller.yOffset); + newRiflesPos = newSize / 2 - 0.5; // -0.5 -> rifle line width / 2 + + // Store current position: + scroller.from = from; + scroller.to = to; + + if (!vertical) { + scroller.scrollbarGroup[method]({ + translateX: newPos + }); + scroller.scrollbar[method]({ + width: newSize + }); + scroller.scrollbarRifles[method]({ + translateX: newRiflesPos + }); + scroller.scrollbarLeft = newPos; + scroller.scrollbarTop = 0; + } else { + scroller.scrollbarGroup[method]({ + translateY: newPos + }); + scroller.scrollbar[method]({ + height: newSize + }); + scroller.scrollbarRifles[method]({ + translateY: newRiflesPos + }); + scroller.scrollbarTop = newPos; + scroller.scrollbarLeft = 0; + } + + if (newSize <= 12) { + scroller.scrollbarRifles.hide(); + } else { + scroller.scrollbarRifles.show(true); + } + + // Show or hide the scrollbar based on the showFull setting + if (options.showFull === false) { + if (from <= 0 && to >= 1) { + scroller.group.hide(); + } else { + scroller.group.show(); + } + } + + scroller.rendered = true; + }, + + /** + * Init events methods, so we have an access to the Scrollbar itself + */ + initEvents: function () { + var scroller = this; + /** + * Event handler for the mouse move event. + */ + scroller.mouseMoveHandler = function (e) { + var normalizedEvent = scroller.chart.pointer.normalize(e), + options = scroller.options, + direction = options.vertical ? 'chartY' : 'chartX', + initPositions = scroller.initPositions, + scrollPosition, + chartPosition, + change; + + // 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 (scroller.grabbedCenter && (!e.touches || e.touches[0][direction] !== 0)) { // #4696, scrollbar failed on Android + chartPosition = scroller.cursorToScrollbarPosition(normalizedEvent)[direction]; + scrollPosition = scroller[direction]; + + change = chartPosition - scrollPosition; + + scroller.hasDragged = true; + scroller.updatePosition(initPositions[0] + change, initPositions[1] + change); + + if (scroller.hasDragged) { + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMType: e.type, + DOMEvent: e + }); + } + } + }; + + /** + * Event handler for the mouse up event. + */ + scroller.mouseUpHandler = function (e) { + if (scroller.hasDragged) { + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMType: e.type, + DOMEvent: e + }); + } + scroller.grabbedCenter = scroller.hasDragged = scroller.chartX = scroller.chartY = null; + }; + + scroller.mouseDownHandler = function (e) { + var normalizedEvent = scroller.chart.pointer.normalize(e), + mousePosition = scroller.cursorToScrollbarPosition(normalizedEvent); + + scroller.chartX = mousePosition.chartX; + scroller.chartY = mousePosition.chartY; + scroller.initPositions = [scroller.from, scroller.to]; + + scroller.grabbedCenter = true; + }; + + scroller.buttonToMinClick = function (e) { + var range = correctFloat(scroller.to - scroller.from) * scroller.options.step; + scroller.updatePosition(correctFloat(scroller.from - range), correctFloat(scroller.to - range)); + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMEvent: e + }); + }; + + scroller.buttonToMaxClick = function (e) { + var range = (scroller.to - scroller.from) * scroller.options.step; + scroller.updatePosition(scroller.from + range, scroller.to + range); + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMEvent: e + }); + }; + + scroller.trackClick = function (e) { + var normalizedEvent = scroller.chart.pointer.normalize(e), + range = scroller.to - scroller.from, + top = scroller.y + scroller.scrollbarTop, + left = scroller.x + scroller.scrollbarLeft; + + if ((scroller.options.vertical && normalizedEvent.chartY > top) || + (!scroller.options.vertical && normalizedEvent.chartX > left)) { + // On the top or on the left side of the track: + scroller.updatePosition(scroller.from + range, scroller.to + range); + } else { + // On the bottom or the right side of the track: + scroller.updatePosition(scroller.from - range, scroller.to - range); + } + + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMEvent: e + }); + }; + }, + + /** + * Get normalized (0-1) cursor position over the scrollbar + * @param {Event} normalizedEvent - normalized event, with chartX and chartY values + * @return {Object} Local position {chartX, chartY} + */ + cursorToScrollbarPosition: function (normalizedEvent) { + var scroller = this, + options = scroller.options, + minWidthDifference = options.minWidth > scroller.calculatedWidth ? options.minWidth : 0; // minWidth distorts translation + + return { + chartX: (normalizedEvent.chartX - scroller.x - scroller.xOffset) / (scroller.barWidth - minWidthDifference), + chartY: (normalizedEvent.chartY - scroller.y - scroller.yOffset) / (scroller.barWidth - minWidthDifference) + }; + }, + + /** + * Update position option in the Scrollbar, with normalized 0-1 scale + */ + updatePosition: function (from, to) { + if (to > 1) { + from = correctFloat(1 - correctFloat(to - from)); + to = 1; + } + + if (from < 0) { + to = correctFloat(to - from); + from = 0; + } + + this.from = from; + this.to = to; + }, + + /** + * Update the scrollbar with new options + */ + update: function (options) { + this.destroy(); + this.init(this.chart.renderer, merge(true, this.options, options), this.chart); + }, + + /** + * Set up the mouse and touch events for the Scrollbar + */ + addEvents: function () { + var buttonsOrder = this.options.inverted ? [1, 0] : [0, 1], + buttons = this.scrollbarButtons, + bar = this.scrollbarGroup.element, + track = this.track.element, + mouseDownHandler = this.mouseDownHandler, + mouseMoveHandler = this.mouseMoveHandler, + mouseUpHandler = this.mouseUpHandler, + _events; + + // Mouse events + _events = [ + [buttons[buttonsOrder[0]].element, 'click', this.buttonToMinClick], + [buttons[buttonsOrder[1]].element, 'click', this.buttonToMaxClick], + [track, 'click', this.trackClick], + [bar, 'mousedown', mouseDownHandler], + [bar.ownerDocument, 'mousemove', mouseMoveHandler], + [bar.ownerDocument, 'mouseup', mouseUpHandler] + ]; + + // Touch events + if (hasTouch) { + _events.push( + [bar, 'touchstart', mouseDownHandler], + [bar.ownerDocument, 'touchmove', mouseMoveHandler], + [bar.ownerDocument, 'touchend', mouseUpHandler] + ); + } + + // Add them all + each(_events, function (args) { + addEvent.apply(null, args); + }); + this._events = _events; + }, + + /** + * Removes the event handlers attached previously with addEvents. + */ + removeEvents: function () { + each(this._events, function (args) { + removeEvent.apply(null, args); + }); + this._events.length = 0; + }, + + /** + * Destroys allocated elements. + */ + destroy: function () { + + var scroller = this.chart.scroller; + + // Disconnect events added in addEvents + this.removeEvents(); + + // Destroy properties + each(['track', 'scrollbarRifles', 'scrollbar', 'scrollbarGroup', 'group'], function (prop) { + if (this[prop] && this[prop].destroy) { + this[prop] = this[prop].destroy(); + } + }, this); + + if (scroller && this === scroller.scrollbar) { // #6421, chart may have more scrollbars + scroller.scrollbar = null; + + // Destroy elements in collection + destroyObjectProperties(scroller.scrollbarButtons); + } + } }; /** * Wrap axis initialization and create scrollbar if enabled: */ wrap(Axis.prototype, 'init', function (proceed) { - var axis = this; - proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); - - if (axis.options.scrollbar && axis.options.scrollbar.enabled) { - // Predefined options: - axis.options.scrollbar.vertical = !axis.horiz; - axis.options.startOnTick = axis.options.endOnTick = false; - - axis.scrollbar = new Scrollbar(axis.chart.renderer, axis.options.scrollbar, axis.chart); - - addEvent(axis.scrollbar, 'changed', function (e) { - var unitedMin = Math.min(pick(axis.options.min, axis.min), axis.min, axis.dataMin), - unitedMax = Math.max(pick(axis.options.max, axis.max), axis.max, axis.dataMax), - range = unitedMax - unitedMin, - to, - from; - - if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { - to = unitedMin + range * this.to; - from = unitedMin + range * this.from; - } else { - // y-values in browser are reversed, but this also applies for reversed horizontal axis: - to = unitedMin + range * (1 - this.from); - from = unitedMin + range * (1 - this.to); - } - - axis.setExtremes(from, to, true, false, e); - }); - } + var axis = this; + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + + if (axis.options.scrollbar && axis.options.scrollbar.enabled) { + // Predefined options: + axis.options.scrollbar.vertical = !axis.horiz; + axis.options.startOnTick = axis.options.endOnTick = false; + + axis.scrollbar = new Scrollbar(axis.chart.renderer, axis.options.scrollbar, axis.chart); + + addEvent(axis.scrollbar, 'changed', function (e) { + var unitedMin = Math.min(pick(axis.options.min, axis.min), axis.min, axis.dataMin), + unitedMax = Math.max(pick(axis.options.max, axis.max), axis.max, axis.dataMax), + range = unitedMax - unitedMin, + to, + from; + + if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { + to = unitedMin + range * this.to; + from = unitedMin + range * this.from; + } else { + // y-values in browser are reversed, but this also applies for reversed horizontal axis: + to = unitedMin + range * (1 - this.from); + from = unitedMin + range * (1 - this.to); + } + + axis.setExtremes(from, to, true, false, e); + }); + } }); /** * Wrap rendering axis, and update scrollbar if one is created: */ wrap(Axis.prototype, 'render', function (proceed) { - var axis = this, - scrollMin = Math.min( - pick(axis.options.min, axis.min), - axis.min, - pick(axis.dataMin, axis.min) // #6930 - ), - scrollMax = Math.max( - pick(axis.options.max, axis.max), - axis.max, - pick(axis.dataMax, axis.max) // #6930 - ), - scrollbar = axis.scrollbar, - titleOffset = axis.titleOffset || 0, - offsetsIndex, - from, - to; - - proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); - - if (scrollbar) { - - if (axis.horiz) { - scrollbar.position( - axis.left, - axis.top + axis.height + 2 + axis.chart.scrollbarsOffsets[1] + - (axis.opposite ? - 0 : - titleOffset + axis.axisTitleMargin + axis.offset - ), - axis.width, - axis.height - ); - offsetsIndex = 1; - } else { - scrollbar.position( - axis.left + axis.width + 2 + axis.chart.scrollbarsOffsets[0] + - (axis.opposite ? - titleOffset + axis.axisTitleMargin + axis.offset : - 0 - ), - axis.top, - axis.width, - axis.height - ); - offsetsIndex = 0; - } - - if ((!axis.opposite && !axis.horiz) || (axis.opposite && axis.horiz)) { - axis.chart.scrollbarsOffsets[offsetsIndex] += - axis.scrollbar.size + axis.scrollbar.options.margin; - } - - if (isNaN(scrollMin) || isNaN(scrollMax) || !defined(axis.min) || !defined(axis.max)) { - scrollbar.setRange(0, 0); // default action: when there is not extremes on the axis, but scrollbar exists, make it full size - } else { - from = (axis.min - scrollMin) / (scrollMax - scrollMin); - to = (axis.max - scrollMin) / (scrollMax - scrollMin); - - if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { - scrollbar.setRange(from, to); - } else { - scrollbar.setRange(1 - to, 1 - from); // inverse vertical axis - } - } - } + var axis = this, + scrollMin = Math.min( + pick(axis.options.min, axis.min), + axis.min, + pick(axis.dataMin, axis.min) // #6930 + ), + scrollMax = Math.max( + pick(axis.options.max, axis.max), + axis.max, + pick(axis.dataMax, axis.max) // #6930 + ), + scrollbar = axis.scrollbar, + titleOffset = axis.titleOffset || 0, + offsetsIndex, + from, + to; + + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + + if (scrollbar) { + + if (axis.horiz) { + scrollbar.position( + axis.left, + axis.top + axis.height + 2 + axis.chart.scrollbarsOffsets[1] + + (axis.opposite ? + 0 : + titleOffset + axis.axisTitleMargin + axis.offset + ), + axis.width, + axis.height + ); + offsetsIndex = 1; + } else { + scrollbar.position( + axis.left + axis.width + 2 + axis.chart.scrollbarsOffsets[0] + + (axis.opposite ? + titleOffset + axis.axisTitleMargin + axis.offset : + 0 + ), + axis.top, + axis.width, + axis.height + ); + offsetsIndex = 0; + } + + if ((!axis.opposite && !axis.horiz) || (axis.opposite && axis.horiz)) { + axis.chart.scrollbarsOffsets[offsetsIndex] += + axis.scrollbar.size + axis.scrollbar.options.margin; + } + + if (isNaN(scrollMin) || isNaN(scrollMax) || !defined(axis.min) || !defined(axis.max)) { + scrollbar.setRange(0, 0); // default action: when there is not extremes on the axis, but scrollbar exists, make it full size + } else { + from = (axis.min - scrollMin) / (scrollMax - scrollMin); + to = (axis.max - scrollMin) / (scrollMax - scrollMin); + + if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { + scrollbar.setRange(from, to); + } else { + scrollbar.setRange(1 - to, 1 - from); // inverse vertical axis + } + } + } }); /** * Make space for a scrollbar */ wrap(Axis.prototype, 'getOffset', function (proceed) { - var axis = this, - index = axis.horiz ? 2 : 1, - scrollbar = axis.scrollbar; + var axis = this, + index = axis.horiz ? 2 : 1, + scrollbar = axis.scrollbar; - proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); - if (scrollbar) { - axis.chart.scrollbarsOffsets = [0, 0]; // reset scrollbars offsets - axis.chart.axisOffset[index] += scrollbar.size + scrollbar.options.margin; - } + if (scrollbar) { + axis.chart.scrollbarsOffsets = [0, 0]; // reset scrollbars offsets + axis.chart.axisOffset[index] += scrollbar.size + scrollbar.options.margin; + } }); /** * Destroy scrollbar when connected to the specific axis */ wrap(Axis.prototype, 'destroy', function (proceed) { - if (this.scrollbar) { - this.scrollbar = this.scrollbar.destroy(); - } + if (this.scrollbar) { + this.scrollbar = this.scrollbar.destroy(); + } - proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); }); H.Scrollbar = Scrollbar; diff --git a/js/parts/Series.js b/js/parts/Series.js index 7dd2e446c60..5b28a74c40d 100644 --- a/js/parts/Series.js +++ b/js/parts/Series.js @@ -11,31 +11,31 @@ import './Legend.js'; import './Point.js'; import './SvgRenderer.js'; var addEvent = H.addEvent, - animObject = H.animObject, - arrayMax = H.arrayMax, - arrayMin = H.arrayMin, - correctFloat = H.correctFloat, - defaultOptions = H.defaultOptions, - defaultPlotOptions = H.defaultPlotOptions, - defined = H.defined, - each = H.each, - erase = H.erase, - extend = H.extend, - fireEvent = H.fireEvent, - grep = H.grep, - isArray = H.isArray, - isNumber = H.isNumber, - isString = H.isString, - LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement - merge = H.merge, - objectEach = H.objectEach, - pick = H.pick, - Point = H.Point, // @todo add as a requirement - removeEvent = H.removeEvent, - splat = H.splat, - SVGElement = H.SVGElement, - syncTimeout = H.syncTimeout, - win = H.win; + animObject = H.animObject, + arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + correctFloat = H.correctFloat, + defaultOptions = H.defaultOptions, + defaultPlotOptions = H.defaultPlotOptions, + defined = H.defined, + each = H.each, + erase = H.erase, + extend = H.extend, + fireEvent = H.fireEvent, + grep = H.grep, + isArray = H.isArray, + isNumber = H.isNumber, + isString = H.isString, + LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement + merge = H.merge, + objectEach = H.objectEach, + pick = H.pick, + Point = H.Point, // @todo add as a requirement + removeEvent = H.removeEvent, + splat = H.splat, + SVGElement = H.SVGElement, + syncTimeout = H.syncTimeout, + win = H.win; /** * This is the base series prototype that all other series types inherit from. @@ -93,4853 +93,4853 @@ var addEvent = H.addEvent, * @optionparent plotOptions.series */ H.Series = H.seriesType('line', null, { // base series options - /*= if (build.classic) { =*/ - /** - * The SVG value used for the `stroke-linecap` and `stroke-linejoin` - * of a line graph. Round means that lines are rounded in the ends and - * bends. - * - * @validvalue ["round", "butt", "square"] - * @type {String} - * @default round - * @since 3.0.7 - * @apioption plotOptions.line.linecap - */ - - /** - * Pixel width of the graph line. - * - * @type {Number} - * @see In styled mode, the line stroke-width can be set with the - * `.highcharts-graph` class name. - * @sample {highcharts} highcharts/plotoptions/series-linewidth-general/ - * On all series - * @sample {highcharts} highcharts/plotoptions/series-linewidth-specific/ - * On one single series - * @default 2 - * @product highcharts highstock - */ - lineWidth: 2, - /*= } =*/ - - /** - * For some series, there is a limit that shuts down initial animation - * by default when the total number of points in the chart is too high. - * For example, for a column chart and its derivatives, animation doesn't - * run if there is more than 250 points totally. To disable this cap, set - * `animationLimit` to `Infinity`. - * - * @type {Number} - * @apioption plotOptions.series.animationLimit - */ - - /** - * Allow this series' points to be selected by clicking on the graphic - * (columns, point markers, pie slices, map areas etc). - * - * @see [Chart#getSelectedPoints] - * (../class-reference/Highcharts.Chart#getSelectedPoints). - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-allowpointselect-line/ - * Line - * @sample {highcharts} - * highcharts/plotoptions/series-allowpointselect-column/ - * Column - * @sample {highcharts} highcharts/plotoptions/series-allowpointselect-pie/ - * Pie - * @sample {highmaps} maps/plotoptions/series-allowpointselect/ - * Map area - * @sample {highmaps} maps/plotoptions/mapbubble-allowpointselect/ - * Map bubble - * @default false - * @since 1.2.0 - */ - allowPointSelect: false, - - - - /** - * If true, a checkbox is displayed next to the legend item to allow - * selecting the series. The state of the checkbox is determined by - * the `selected` option. - * - * @productdesc {highmaps} - * Note that if a `colorAxis` is defined, the color axis is represented in - * the legend, not the series. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-showcheckbox-true/ - * Show select box - * @default false - * @since 1.2.0 - */ - showCheckbox: false, - - - - /** - * Enable or disable the initial animation when a series is displayed. - * The animation can also be set as a configuration object. Please - * note that this option only applies to the initial animation of the - * series itself. For other animations, see [chart.animation]( - * #chart.animation) and the animation parameter under the API methods. The - * following properties are supported: - * - *
- * - *
duration
- * - *
The duration of the animation in milliseconds.
- * - *
easing
- * - *
A string reference to an easing function set on the `Math` object. - * See the _Custom easing function_ demo below.
- * - *
- * - * Due to poor performance, animation is disabled in old IE browsers - * for several chart types. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-animation-disabled/ - * Animation disabled - * @sample {highcharts} highcharts/plotoptions/series-animation-slower/ - * Slower animation - * @sample {highcharts} highcharts/plotoptions/series-animation-easing/ - * Custom easing function - * @sample {highstock} stock/plotoptions/animation-slower/ - * Slower animation - * @sample {highstock} stock/plotoptions/animation-easing/ - * Custom easing function - * @sample {highmaps} maps/plotoptions/series-animation-true/ - * Animation enabled on map series - * @sample {highmaps} maps/plotoptions/mapbubble-animation-false/ - * Disabled on mapbubble series - * @default {highcharts} true - * @default {highstock} true - * @default {highmaps} false - */ - animation: { - duration: 1000 - }, - - /** - * A class name to apply to the series' graphical elements. - * - * @type {String} - * @since 5.0.0 - * @apioption plotOptions.series.className - */ - - /** - * The main color of the series. In line type series it applies to the - * line and the point markers unless otherwise specified. In bar type - * series it applies to the bars unless a color is specified per point. - * The default value is pulled from the `options.colors` array. - * - * In styled mode, the color can be defined by the - * [colorIndex](#plotOptions.series.colorIndex) option. Also, the series - * color can be set with the `.highcharts-series`, `.highcharts-color-{n}`, - * `.highcharts-{type}-series` or `.highcharts-series-{n}` class, or - * individual classes given by the `className` option. - * - * @productdesc {highmaps} - * In maps, the series color is rarely used, as most choropleth maps use the - * color to denote the value of each point. The series color can however be - * used in a map with multiple series holding categorized data. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/series-color-general/ - * General plot option - * @sample {highcharts} highcharts/plotoptions/series-color-specific/ - * One specific series - * @sample {highcharts} highcharts/plotoptions/series-color-area/ - * Area color - * @sample {highmaps} maps/demo/category-map/ - * Category map by multiple series - * @apioption plotOptions.series.color - */ - - /** - * Styled mode only. A specific color index to use for the series, so its - * graphic representations are given the class name `highcharts-color-{n}`. - * - * @type {Number} - * @since 5.0.0 - * @apioption plotOptions.series.colorIndex - */ - - - /** - * Whether to connect a graph line across null points, or render a gap - * between the two points on either side of the null. - * - * @type {Boolean} - * @default false - * @sample {highcharts} highcharts/plotoptions/series-connectnulls-false/ - * False by default - * @sample {highcharts} highcharts/plotoptions/series-connectnulls-true/ - * True - * @product highcharts highstock - * @apioption plotOptions.series.connectNulls - */ - - - /** - * You can set the cursor to "pointer" if you have click events attached - * to the series, to signal to the user that the points and lines can - * be clicked. - * - * @validvalue [null, "default", "none", "help", "pointer", "crosshair"] - * @type {String} - * @see In styled mode, the series cursor can be set with the same classes - * as listed under [series.color](#plotOptions.series.color). - * @sample {highcharts} highcharts/plotoptions/series-cursor-line/ - * On line graph - * @sample {highcharts} highcharts/plotoptions/series-cursor-column/ - * On columns - * @sample {highcharts} highcharts/plotoptions/series-cursor-scatter/ - * On scatter markers - * @sample {highstock} stock/plotoptions/cursor/ - * Pointer on a line graph - * @sample {highmaps} maps/plotoptions/series-allowpointselect/ - * Map area - * @sample {highmaps} maps/plotoptions/mapbubble-allowpointselect/ - * Map bubble - * @apioption plotOptions.series.cursor - */ - - - /** - * A name for the dash style to use for the graph, or for some series types - * the outline of each shape. The value for the `dashStyle` include: - * - * * Solid - * * ShortDash - * * ShortDot - * * ShortDashDot - * * ShortDashDotDot - * * Dot - * * Dash - * * LongDash - * * DashDot - * * LongDashDot - * * LongDashDotDot - * - * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", - * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", "DashDot", - * "LongDashDot", "LongDashDotDot"] - * @type {String} - * @see In styled mode, the [stroke dash-array](http://jsfiddle.net/gh/get/ - * library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/ - * series-dashstyle/) can be set with the same classes as listed under - * [series.color](#plotOptions.series.color). - * - * @sample {highcharts} highcharts/plotoptions/series-dashstyle-all/ - * Possible values demonstrated - * @sample {highcharts} highcharts/plotoptions/series-dashstyle/ - * Chart suitable for printing in black and white - * @sample {highstock} highcharts/plotoptions/series-dashstyle-all/ - * Possible values demonstrated - * @sample {highmaps} highcharts/plotoptions/series-dashstyle-all/ - * Possible values demonstrated - * @sample {highmaps} maps/plotoptions/series-dashstyle/ - * Dotted borders on a map - * @default Solid - * @since 2.1 - * @apioption plotOptions.series.dashStyle - */ - - /** - * Requires the Accessibility module. - * - * A description of the series to add to the screen reader information - * about the series. - * - * @type {String} - * @default undefined - * @since 5.0.0 - * @apioption plotOptions.series.description - */ - - - - - - /** - * Enable or disable the mouse tracking for a specific series. This - * includes point tooltips and click events on graphs and points. For - * large datasets it improves performance. - * - * @type {Boolean} - * @sample {highcharts} - * highcharts/plotoptions/series-enablemousetracking-false/ - * No mouse tracking - * @sample {highmaps} - * maps/plotoptions/series-enablemousetracking-false/ - * No mouse tracking - * @default true - * @apioption plotOptions.series.enableMouseTracking - */ - - /** - * By default, series are exposed to screen readers as regions. By enabling - * this option, the series element itself will be exposed in the same - * way as the data points. This is useful if the series is not used - * as a grouping entity in the chart, but you still want to attach a - * description to the series. - * - * Requires the Accessibility module. - * - * @type {Boolean} - * @sample highcharts/accessibility/art-grants/ - * Accessible data visualization - * @default undefined - * @since 5.0.12 - * @apioption plotOptions.series.exposeElementToA11y - */ - - /** - * Whether to use the Y extremes of the total chart width or only the - * zoomed area when zooming in on parts of the X axis. By default, the - * Y axis adjusts to the min and max of the visible data. Cartesian - * series only. - * - * @type {Boolean} - * @default false - * @since 4.1.6 - * @product highcharts highstock - * @apioption plotOptions.series.getExtremesFromAll - */ - - /** - * An id for the series. This can be used after render time to get a - * pointer to the series object through `chart.get()`. - * - * @type {String} - * @sample {highcharts} highcharts/plotoptions/series-id/ Get series by id - * @since 1.2.0 - * @apioption series.id - */ - - /** - * The index of the series in the chart, affecting the internal index - * in the `chart.series` array, the visible Z index as well as the order - * in the legend. - * - * @type {Number} - * @default undefined - * @since 2.3.0 - * @apioption series.index - */ - - /** - * An array specifying which option maps to which key in the data point - * array. This makes it convenient to work with unstructured data arrays - * from different sources. - * - * @type {Array} - * @see [series.data](#series.line.data) - * @sample {highcharts|highstock} highcharts/series/data-keys/ - * An extended data array with keys - * @sample {highcharts|highstock} highcharts/series/data-nested-keys/ - * Nested keys used to access object properties - * @since 4.1.6 - * @product highcharts highstock - * @apioption plotOptions.series.keys - */ - - /** - * The sequential index of the series in the legend. - * - * @sample {highcharts|highstock} highcharts/series/legendindex/ - * Legend in opposite order - * @type {Number} - * @see [legend.reversed](#legend.reversed), - * [yAxis.reversedStacks](#yAxis.reversedStacks) - * @apioption series.legendIndex - */ - - /** - * The line cap used for line ends and line joins on the graph. - * - * @validvalue ["round", "square"] - * @type {String} - * @default round - * @product highcharts highstock - * @apioption plotOptions.series.linecap - */ - - /** - * The [id](#series.id) of another series to link to. Additionally, - * the value can be ":previous" to link to the previous series. When - * two series are linked, only the first one appears in the legend. - * Toggling the visibility of this also toggles the linked series. - * - * @type {String} - * @sample {highcharts} highcharts/demo/arearange-line/ Linked series - * @sample {highstock} highcharts/demo/arearange-line/ Linked series - * @since 3.0 - * @product highcharts highstock - * @apioption plotOptions.series.linkedTo - */ - - /** - * The name of the series as shown in the legend, tooltip etc. - * - * @type {String} - * @sample {highcharts} highcharts/series/name/ Series name - * @sample {highmaps} maps/demo/category-map/ Series name - * @apioption series.name - */ - - /** - * The color for the parts of the graph or points that are below the - * [threshold](#plotOptions.series.threshold). - * - * @type {Color} - * @see In styled mode, a negative color is applied by setting this - * option to `true` combined with the `.highcharts-negative` class name. - * - * @sample {highcharts} highcharts/plotoptions/series-negative-color/ - * Spline, area and column - * @sample {highcharts} highcharts/plotoptions/arearange-negativecolor/ - * Arearange - * @sample {highcharts} highcharts/css/series-negative-color/ - * Styled mode - * @sample {highstock} highcharts/plotoptions/series-negative-color/ - * Spline, area and column - * @sample {highstock} highcharts/plotoptions/arearange-negativecolor/ - * Arearange - * @sample {highmaps} highcharts/plotoptions/series-negative-color/ - * Spline, area and column - * @sample {highmaps} highcharts/plotoptions/arearange-negativecolor/ - * Arearange - * @default null - * @since 3.0 - * @apioption plotOptions.series.negativeColor - */ - - /** - * Same as [accessibility.pointDescriptionFormatter]( - * #accessibility.pointDescriptionFormatter), but for an individual series. - * Overrides the chart wide configuration. - * - * @type {Function} - * @since 5.0.12 - * @apioption plotOptions.series.pointDescriptionFormatter - */ - - /** - * If no x values are given for the points in a series, `pointInterval` - * defines the interval of the x values. For example, if a series contains - * one value every decade starting from year 0, set `pointInterval` to - * `10`. In true `datetime` axes, the `pointInterval` is set in - * milliseconds. - * - * It can be also be combined with `pointIntervalUnit` to draw irregular - * time intervals. - * - * Please note that this options applies to the _series data_, not the - * interval of the axis ticks, which is independent. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/series-pointstart-datetime/ - * Datetime X axis - * @sample {highstock} stock/plotoptions/pointinterval-pointstart/ - * Using pointStart and pointInterval - * @default 1 - * @product highcharts highstock - * @apioption plotOptions.series.pointInterval - */ - - /** - * On datetime series, this allows for setting the - * [pointInterval](#plotOptions.series.pointInterval) to irregular time - * units, `day`, `month` and `year`. A day is usually the same as 24 hours, - * but `pointIntervalUnit` also takes the DST crossover into consideration - * when dealing with local time. Combine this option with `pointInterval` - * to draw weeks, quarters, 6 months, 10 years etc. - * - * Please note that this options applies to the _series data_, not the - * interval of the axis ticks, which is independent. - * - * @validvalue [null, "day", "month", "year"] - * @type {String} - * @sample {highcharts} highcharts/plotoptions/series-pointintervalunit/ - * One point a month - * @sample {highstock} highcharts/plotoptions/series-pointintervalunit/ - * One point a month - * @since 4.1.0 - * @product highcharts highstock - * @apioption plotOptions.series.pointIntervalUnit - */ - - /** - * Possible values: `null`, `"on"`, `"between"`. - * - * In a column chart, when pointPlacement is `"on"`, the point will - * not create any padding of the X axis. In a polar column chart this - * means that the first column points directly north. If the pointPlacement - * is `"between"`, the columns will be laid out between ticks. This - * is useful for example for visualising an amount between two points - * in time or in a certain sector of a polar chart. - * - * Since Highcharts 3.0.2, the point placement can also be numeric, - * where 0 is on the axis value, -0.5 is between this value and the - * previous, and 0.5 is between this value and the next. Unlike the - * textual options, numeric point placement options won't affect axis - * padding. - * - * Note that pointPlacement needs a [pointRange]( - * #plotOptions.series.pointRange) to work. For column series this is - * computed, but for line-type series it needs to be set. - * - * Defaults to `null` in cartesian charts, `"between"` in polar charts. - * - * @validvalue [null, "on", "between"] - * @type {String|Number} - * @see [xAxis.tickmarkPlacement](#xAxis.tickmarkPlacement) - * @sample {highcharts|highstock} - * highcharts/plotoptions/series-pointplacement-between/ - * Between in a column chart - * @sample {highcharts|highstock} - * highcharts/plotoptions/series-pointplacement-numeric/ - * Numeric placement for custom layout - * @default null - * @since 2.3.0 - * @product highcharts highstock - * @apioption plotOptions.series.pointPlacement - */ - - /** - * If no x values are given for the points in a series, pointStart defines - * on what value to start. For example, if a series contains one yearly - * value starting from 1945, set pointStart to 1945. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/series-pointstart-linear/ - * Linear - * @sample {highcharts} highcharts/plotoptions/series-pointstart-datetime/ - * Datetime - * @sample {highstock} stock/plotoptions/pointinterval-pointstart/ - * Using pointStart and pointInterval - * @default 0 - * @product highcharts highstock - * @apioption plotOptions.series.pointStart - */ - - /** - * Whether to select the series initially. If `showCheckbox` is true, - * the checkbox next to the series name in the legend will be checked for a - * selected series. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-selected/ - * One out of two series selected - * @default false - * @since 1.2.0 - * @apioption plotOptions.series.selected - */ - - /** - * Whether to apply a drop shadow to the graph line. Since 2.3 the shadow - * can be an object configuration containing `color`, `offsetX`, `offsetY`, - * `opacity` and `width`. - * - * @type {Boolean|Object} - * @sample {highcharts} highcharts/plotoptions/series-shadow/ Shadow enabled - * @default false - * @apioption plotOptions.series.shadow - */ - - /** - * Whether to display this particular series or series type in the legend. - * The default value is `true` for standalone series, `false` for linked - * series. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-showinlegend/ - * One series in the legend, one hidden - * @default true - * @apioption plotOptions.series.showInLegend - */ - - /** - * If set to `True`, the accessibility module will skip past the points - * in this series for keyboard navigation. - * - * @type {Boolean} - * @since 5.0.12 - * @apioption plotOptions.series.skipKeyboardNavigation - */ - - /** - * This option allows grouping series in a stacked chart. The stack - * option can be a string or a number or anything else, as long as the - * grouped series' stack options match each other. - * - * @type {String} - * @sample {highcharts} highcharts/series/stack/ Stacked and grouped columns - * @default null - * @since 2.1 - * @product highcharts highstock - * @apioption series.stack - */ - - /** - * Whether to stack the values of each series on top of each other. - * Possible values are `null` to disable, `"normal"` to stack by value or - * `"percent"`. When stacking is enabled, data must be sorted in ascending - * X order. A special stacking option is with the streamgraph series type, - * where the stacking option is set to `"stream"`. - * - * @validvalue [null, "normal", "percent"] - * @type {String} - * @see [yAxis.reversedStacks](#yAxis.reversedStacks) - * @sample {highcharts} highcharts/plotoptions/series-stacking-line/ - * Line - * @sample {highcharts} highcharts/plotoptions/series-stacking-column/ - * Column - * @sample {highcharts} highcharts/plotoptions/series-stacking-bar/ - * Bar - * @sample {highcharts} highcharts/plotoptions/series-stacking-area/ - * Area - * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-line/ - * Line - * @sample {highcharts} - * highcharts/plotoptions/series-stacking-percent-column/ - * Column - * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-bar/ - * Bar - * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-area/ - * Area - * @sample {highstock} stock/plotoptions/stacking/ - * Area - * @default null - * @product highcharts highstock - * @apioption plotOptions.series.stacking - */ - - /** - * Whether to apply steps to the line. Possible values are `left`, `center` - * and `right`. - * - * @validvalue [null, "left", "center", "right"] - * @type {String} - * @sample {highcharts} highcharts/plotoptions/line-step/ - * Different step line options - * @sample {highcharts} highcharts/plotoptions/area-step/ - * Stepped, stacked area - * @sample {highstock} stock/plotoptions/line-step/ - * Step line - * @default {highcharts} null - * @default {highstock} false - * @since 1.2.5 - * @product highcharts highstock - * @apioption plotOptions.series.step - */ - - /** - * The threshold, also called zero level or base level. For line type - * series this is only used in conjunction with - * [negativeColor](#plotOptions.series.negativeColor). - * - * @type {Number} - * @see [softThreshold](#plotOptions.series.softThreshold). - * @default 0 - * @since 3.0 - * @product highcharts highstock - * @apioption plotOptions.series.threshold - */ - - /** - * The type of series, for example `line` or `column`. By default, the - * series type is inherited from [chart.type](#chart.type), so unless the - * chart is a combination of series types, there is no need to set it on the - * series level. - * - * @validvalue [null, "line", "spline", "column", "area", "areaspline", - * "pie", "arearange", "areasplinerange", "boxplot", "bubble", - * "columnrange", "errorbar", "funnel", "gauge", "scatter", - * "waterfall"] - * @type {String} - * @sample {highcharts} highcharts/series/type/ - * Line and column in the same chart - * @sample {highmaps} maps/demo/mapline-mappoint/ - * Multiple types in the same map - * @apioption series.type - */ - - /** - * Set the initial visibility of the series. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-visible/ - * Two series, one hidden and one visible - * @sample {highstock} stock/plotoptions/series-visibility/ - * Hidden series - * @default true - * @apioption plotOptions.series.visible - */ - - /** - * When using dual or multiple x axes, this number defines which xAxis - * the particular series is connected to. It refers to either the [axis - * id](#xAxis.id) or the index of the axis in the xAxis array, with - * 0 being the first. - * - * @type {Number|String} - * @default 0 - * @product highcharts highstock - * @apioption series.xAxis - */ - - /** - * When using dual or multiple y axes, this number defines which yAxis - * the particular series is connected to. It refers to either the [axis - * id](#yAxis.id) or the index of the axis in the yAxis array, with - * 0 being the first. - * - * @type {Number|String} - * @sample {highcharts} highcharts/series/yaxis/ - * Apply the column series to the secondary Y axis - * @default 0 - * @product highcharts highstock - * @apioption series.yAxis - */ - - /** - * Defines the Axis on which the zones are applied. - * - * @type {String} - * @see [zones](#plotOptions.series.zones) - * @sample {highcharts} highcharts/series/color-zones-zoneaxis-x/ - * Zones on the X-Axis - * @sample {highstock} highcharts/series/color-zones-zoneaxis-x/ - * Zones on the X-Axis - * @default y - * @since 4.1.0 - * @product highcharts highstock - * @apioption plotOptions.series.zoneAxis - */ - - /** - * Define the visual z index of the series. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/series-zindex-default/ - * With no z index, the series defined last are on top - * @sample {highcharts} highcharts/plotoptions/series-zindex/ - * With a z index, the series with the highest z index is on top - * @sample {highstock} highcharts/plotoptions/series-zindex-default/ - * With no z index, the series defined last are on top - * @sample {highstock} highcharts/plotoptions/series-zindex/ - * With a z index, the series with the highest z index is on top - * @product highcharts highstock - * @apioption series.zIndex - */ - - /** - * General event handlers for the series items. These event hooks can also - * be attached to the series at run time using the `Highcharts.addEvent` - * function. - */ - - /** - * Fires after the series has finished its initial animation, or in - * case animation is disabled, immediately as the series is displayed. - * - * @type {Function} - * @context Series - * @sample {highcharts} - * highcharts/plotoptions/series-events-afteranimate/ - * Show label after animate - * @sample {highstock} - * highcharts/plotoptions/series-events-afteranimate/ - * Show label after animate - * @since 4.0 - * @product highcharts highstock - * @apioption plotOptions.series.events.afterAnimate - */ - - /** - * Fires when the checkbox next to the series' name in the legend is - * clicked. One parameter, `event`, is passed to the function. The state - * of the checkbox is found by `event.checked`. The checked item is - * found by `event.item`. Return `false` to prevent the default action - * which is to toggle the select state of the series. - * - * @type {Function} - * @context Series - * @sample {highcharts} - * highcharts/plotoptions/series-events-checkboxclick/ - * Alert checkbox status - * @since 1.2.0 - * @apioption plotOptions.series.events.checkboxClick - */ - - /** - * Fires when the series is clicked. One parameter, `event`, is passed - * to the function, containing common event information. Additionally, - * `event.point` holds a pointer to the nearest point on the graph. - * - * @type {Function} - * @context Series - * @sample {highcharts} highcharts/plotoptions/series-events-click/ - * Alert click info - * @sample {highstock} stock/plotoptions/series-events-click/ - * Alert click info - * @sample {highmaps} maps/plotoptions/series-events-click/ - * Display click info in subtitle - * @apioption plotOptions.series.events.click - */ - - /** - * Fires when the series is hidden after chart generation time, either - * by clicking the legend item or by calling `.hide()`. - * - * @type {Function} - * @context Series - * @sample {highcharts} highcharts/plotoptions/series-events-hide/ - * Alert when the series is hidden by clicking the legend item - * @since 1.2.0 - * @apioption plotOptions.series.events.hide - */ - - /** - * Fires when the legend item belonging to the series is clicked. One - * parameter, `event`, is passed to the function. The default action - * is to toggle the visibility of the series. This can be prevented - * by returning `false` or calling `event.preventDefault()`. - * - * @type {Function} - * @context Series - * @sample {highcharts} - * highcharts/plotoptions/series-events-legenditemclick/ - * Confirm hiding and showing - * @apioption plotOptions.series.events.legendItemClick - */ - - /** - * Fires when the mouse leaves the graph. One parameter, `event`, is - * passed to the function, containing common event information. If the - * [stickyTracking](#plotOptions.series) option is true, `mouseOut` - * doesn't happen before the mouse enters another graph or leaves the - * plot area. - * - * @type {Function} - * @context Series - * @sample {highcharts} - * highcharts/plotoptions/series-events-mouseover-sticky/ - * With sticky tracking by default - * @sample {highcharts} - * highcharts/plotoptions/series-events-mouseover-no-sticky/ - * Without sticky tracking - * @apioption plotOptions.series.events.mouseOut - */ - - /** - * Fires when the mouse enters the graph. One parameter, `event`, is - * passed to the function, containing common event information. - * - * @type {Function} - * @context Series - * @sample {highcharts} - * highcharts/plotoptions/series-events-mouseover-sticky/ - * With sticky tracking by default - * @sample {highcharts} - * highcharts/plotoptions/series-events-mouseover-no-sticky/ - * Without sticky tracking - * @apioption plotOptions.series.events.mouseOver - */ - - /** - * Fires when the series is shown after chart generation time, either - * by clicking the legend item or by calling `.show()`. - * - * @type {Function} - * @context Series - * @sample {highcharts} highcharts/plotoptions/series-events-show/ - * Alert when the series is shown by clicking the legend item. - * @since 1.2.0 - * @apioption plotOptions.series.events.show - */ - events: {}, - - - - /** - * Options for the point markers of line-like series. Properties like - * `fillColor`, `lineColor` and `lineWidth` define the visual appearance - * of the markers. Other series types, like column series, don't have - * markers, but have visual options on the series level instead. - * - * In styled mode, the markers can be styled with the `.highcharts-point`, - * `.highcharts-point-hover` and `.highcharts-point-select` - * class names. - */ - marker: { - /*= if (build.classic) { =*/ - - - /** - * The width of the point marker's outline. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ - * 2px blue marker - * @default 0 - */ - lineWidth: 0, - - - /** - * The color of the point marker's outline. When `null`, the series' - * or point's color is used. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ - * Inherit from series color (null) - */ - lineColor: '${palette.backgroundColor}', - - /** - * The fill color of the point marker. When `null`, the series' or - * point's color is used. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ - * White fill - * @default null - * @apioption plotOptions.series.marker.fillColor - */ - - /*= } =*/ - - /** - * Enable or disable the point marker. If `null`, the markers are hidden - * when the data is dense, and shown for more widespread data points. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-marker-enabled/ - * Disabled markers - * @sample {highcharts} - * highcharts/plotoptions/series-marker-enabled-false/ - * Disabled in normal state but enabled on hover - * @sample {highstock} stock/plotoptions/series-marker/ - * Enabled markers - * @default {highcharts} null - * @default {highstock} false - * @apioption plotOptions.series.marker.enabled - */ - - /** - * Image markers only. Set the image width explicitly. When using this - * option, a `width` must also be set. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-width-height/ - * Fixed width and height - * @sample {highstock} - * highcharts/plotoptions/series-marker-width-height/ - * Fixed width and height - * @default null - * @since 4.0.4 - * @apioption plotOptions.series.marker.height - */ - - /** - * A predefined shape or symbol for the marker. When null, the symbol - * is pulled from options.symbols. Other possible values are "circle", - * "square", "diamond", "triangle" and "triangle-down". - * - * 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. - * - * @validvalue [null, "circle", "square", "diamond", "triangle", - * "triangle-down"] - * @type {String} - * @sample {highcharts} highcharts/plotoptions/series-marker-symbol/ - * Predefined, graphic and custom markers - * @sample {highstock} highcharts/plotoptions/series-marker-symbol/ - * Predefined, graphic and custom markers - * @default null - * @apioption plotOptions.series.marker.symbol - */ - - /** - * The threshold for how dense the point markers should be before they - * are hidden, given that `enabled` is not defined. The number indicates - * the horizontal distance between the two closest points in the series, - * as multiples of the `marker.radius`. In other words, the default - * value of 2 means points are hidden if overlapping horizontally. - * - * @since 6.0.5 - * @sample highcharts/plotoptions/series-marker-enabledthreshold - * A higher threshold - */ - enabledThreshold: 2, - - /** - * The radius of the point marker. - * - * @sample {highcharts} highcharts/plotoptions/series-marker-radius/ - * Bigger markers - */ - radius: 4, - - /** - * Image markers only. Set the image width explicitly. When using this - * option, a `height` must also be set. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-width-height/ - * Fixed width and height - * @sample {highstock} - * highcharts/plotoptions/series-marker-width-height/ - * Fixed width and height - * @default null - * @since 4.0.4 - * @apioption plotOptions.series.marker.width - */ - - - /** - * States for a single point marker. - */ - states: { - - /** - * The normal state of a single point marker. Currently only used - * for setting animation when returning to normal state from hover. - * - * @type {Object} - */ - normal: { - /** - * Animation when returning to normal state after hovering. - * - * @type {Boolean|Object} - */ - animation: true - }, - - /** - * The hover state for a single point marker. - * - * @type {Object} - */ - hover: { - - /** - * Animation when hovering over the marker. - * - * @type {Boolean|Object} - */ - animation: { - duration: 50 - }, - - /** - * Enable or disable the point marker. - * - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-hover-enabled/ - * Disabled hover state - */ - enabled: true, - - /** - * The fill color of the marker in hover state. When `null`, the - * series' or point's fillColor for normal state is used. - * - * @type {Color} - * @default null - * @apioption plotOptions.series.marker.states.hover.fillColor - */ - - /** - * The color of the point marker's outline. When `null`, the - * series' or point's lineColor for normal state is used. - * - * @type {Color} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-hover-linecolor/ - * White fill color, black line color - * @default null - * @apioption plotOptions.series.marker.states.hover.lineColor - */ - - /** - * The width of the point marker's outline. When `null`, the - * series' or point's lineWidth for normal state is used. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-hover-linewidth/ - * 3px line width - * @default null - * @apioption plotOptions.series.marker.states.hover.lineWidth - */ - - /** - * The radius of the point marker. In hover state, it defaults - * to the normal state's radius + 2 as per the [radiusPlus]( - * #plotOptions.series.marker.states.hover.radiusPlus) - * option. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-hover-radius/ - * 10px radius - * @apioption plotOptions.series.marker.states.hover.radius - */ - - /** - * The number of pixels to increase the radius of the hovered - * point. - * - * @sample {highcharts} - * highcharts/plotoptions/series-states-hover-linewidthplus/ - * 5 pixels greater radius on hover - * @sample {highstock} - * highcharts/plotoptions/series-states-hover-linewidthplus/ - * 5 pixels greater radius on hover - * @since 4.0.3 - */ - radiusPlus: 2, - - /*= if (build.classic) { =*/ - - /** - * The additional line width for a hovered point. - * - * @sample {highcharts} - * highcharts/plotoptions/series-states-hover-linewidthplus/ - * 2 pixels wider on hover - * @sample {highstock} - * highcharts/plotoptions/series-states-hover-linewidthplus/ - * 2 pixels wider on hover - * @since 4.0.3 - */ - lineWidthPlus: 1 - /*= } =*/ - }, - /*= if (build.classic) { =*/ - - - - /** - * The appearance of the point marker when selected. In order to - * allow a point to be selected, set the `series.allowPointSelect` - * option to true. - */ - select: { - - /** - * The radius of the point marker. In hover state, it defaults - * to the normal state's radius + 2. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-select-radius/ - * 10px radius for selected points - * @apioption plotOptions.series.marker.states.select.radius - */ - - /** - * Enable or disable visible feedback for selection. - * - * @type {Boolean} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-select-enabled/ - * Disabled select state - * @default true - * @apioption plotOptions.series.marker.states.select.enabled - */ - - /** - * The fill color of the point marker. - * - * @type {Color} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-select-fillcolor/ - * Solid red discs for selected points - * @default #cccccc - */ - fillColor: '${palette.neutralColor20}', - - /** - * The color of the point marker's outline. When `null`, the - * series' or point's color is used. - * - * @type {Color} - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-select-linecolor/ - * Red line color for selected points - * @default #000000 - */ - lineColor: '${palette.neutralColor100}', - - /** - * The width of the point marker's outline. - * - * @sample {highcharts} - * highcharts/plotoptions/series-marker-states-select-linewidth/ - * 3px line width for selected points - */ - lineWidth: 2 - } - /*= } =*/ - } - }, - - - - /** - * Properties for each single point. - */ - point: { - - - /** - * Fires when a point is clicked. One parameter, `event`, is passed - * to the function, containing common event information. - * - * If the `series.allowPointSelect` option is true, the default - * action for the point's click event is to toggle the point's - * select state. Returning `false` cancels this action. - * - * @type {Function} - * @context Point - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-click/ - * Click marker to alert values - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-click-column/ - * Click column - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-click-url/ - * Go to URL - * @sample {highmaps} - * maps/plotoptions/series-point-events-click/ - * Click marker to display values - * @sample {highmaps} - * maps/plotoptions/series-point-events-click-url/ - * Go to URL - * @apioption plotOptions.series.point.events.click - */ - - /** - * Fires when the mouse leaves the area close to the point. One - * parameter, `event`, is passed to the function, containing common - * event information. - * - * @type {Function} - * @context Point - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-mouseover/ - * Show values in the chart's corner on mouse over - * @apioption plotOptions.series.point.events.mouseOut - */ - - /** - * Fires when the mouse enters the area close to the point. One - * parameter, `event`, is passed to the function, containing common - * event information. - * - * @type {Function} - * @context Point - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-mouseover/ - * Show values in the chart's corner on mouse over - * @apioption plotOptions.series.point.events.mouseOver - */ - - /** - * Fires when the point is removed using the `.remove()` method. One - * parameter, `event`, is passed to the function. Returning `false` - * cancels the operation. - * - * @type {Function} - * @context Point - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-remove/ - * Remove point and confirm - * @since 1.2.0 - * @apioption plotOptions.series.point.events.remove - */ - - /** - * Fires when the point is selected either programmatically or - * following a click on the point. One parameter, `event`, is passed - * to the function. Returning `false` cancels the operation. - * - * @type {Function} - * @context Point - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-select/ - * Report the last selected point - * @sample {highmaps} - * maps/plotoptions/series-allowpointselect/ - * Report select and unselect - * @since 1.2.0 - * @apioption plotOptions.series.point.events.select - */ - - /** - * Fires when the point is unselected either programmatically or - * following a click on the point. One parameter, `event`, is passed - * to the function. - * Returning `false` cancels the operation. - * - * @type {Function} - * @context Point - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-unselect/ - * Report the last unselected point - * @sample {highmaps} - * maps/plotoptions/series-allowpointselect/ - * Report select and unselect - * @since 1.2.0 - * @apioption plotOptions.series.point.events.unselect - */ - - /** - * Fires when the point is updated programmatically through the - * `.update()` method. One parameter, `event`, is passed to the - * function. The new point options can be accessed through - * `event.options`. Returning `false` cancels the operation. - * - * @type {Function} - * @context Point - * @sample {highcharts} - * highcharts/plotoptions/series-point-events-update/ - * Confirm point updating - * @since 1.2.0 - * @apioption plotOptions.series.point.events.update - */ - - /** - * Events for each single point. - */ - events: {} - }, - - - - /** - * Options for the series data labels, appearing next to each data - * point. - * - * In styled mode, the data labels can be styled wtih the - * `.highcharts-data-label-box` and `.highcharts-data-label` class names - * ([see example](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/series-datalabels)). - */ - dataLabels: { - - - /** - * The alignment of the data label compared to the point. If `right`, - * the right side of the label should be touching the point. For - * points with an extent, like columns, the alignments also dictates - * how to align it inside the box, as given with the - * [inside](#plotOptions.column.dataLabels.inside) option. Can be one of - * `left`, `center` or `right`. - * - * @validvalue ["left", "center", "right"] - * @type {String} - * @sample {highcharts} - * highcharts/plotoptions/series-datalabels-align-left/ - * Left aligned - * @default center - */ - align: 'center', - - - /** - * Whether to allow data labels to overlap. To make the labels less - * sensitive for overlapping, the [dataLabels.padding]( - * #plotOptions.series.dataLabels.padding) can be set to 0. - * - * @type {Boolean} - * @sample highcharts/plotoptions/series-datalabels-allowoverlap-false/ - * Don't allow overlap - * @default false - * @since 4.1.0 - * @apioption plotOptions.series.dataLabels.allowOverlap - */ - - - /** - * The border radius in pixels for the data label. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @sample {highmaps} maps/plotoptions/series-datalabels-box/ - * Data labels box options - * @default 0 - * @since 2.2.1 - * @apioption plotOptions.series.dataLabels.borderRadius - */ - - - /** - * The border width in pixels for the data label. - * - * @type {Number} - * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @default 0 - * @since 2.2.1 - * @apioption plotOptions.series.dataLabels.borderWidth - */ - - /** - * A class name for the data label. Particularly in styled mode, this - * can be used to give each series' or point's data label unique - * styling. In addition to this option, a default color class name is - * added so that we can give the labels a - * [contrast text shadow](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/data-label-contrast/). - * - * @type {String} - * @sample {highcharts} highcharts/css/series-datalabels/ Styling by CSS - * @sample {highstock} highcharts/css/series-datalabels/ Styling by CSS - * @sample {highmaps} highcharts/css/series-datalabels/ Styling by CSS - * @since 5.0.0 - * @apioption plotOptions.series.dataLabels.className - */ - - /** - * The text color for the data labels. Defaults to `null`. For certain - * series types, like column or map, the data labels can be drawn inside - * the points. In this case the data label will be drawn with maximum - * contrast by default. Additionally, it will be given a `text-outline` - * style with the opposite color, to further increase the contrast. This - * can be overridden by setting the `text-outline` style to `none` in - * the `dataLabels.style` option. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/series-datalabels-color/ - * Red data labels - * @sample {highmaps} maps/demo/color-axis/ - * White data labels - * @apioption plotOptions.series.dataLabels.color - */ - - /** - * Whether to hide data labels that are outside the plot area. By - * default, the data label is moved inside the plot area according to - * the [overflow](#plotOptions.series.dataLabels.overflow) option. - * - * @type {Boolean} - * @default true - * @since 2.3.3 - * @apioption plotOptions.series.dataLabels.crop - */ - - /** - * Whether to defer displaying the data labels until the initial series - * animation has finished. - * - * @type {Boolean} - * @default true - * @since 4.0 - * @product highcharts highstock - * @apioption plotOptions.series.dataLabels.defer - */ - - /** - * Enable or disable the data labels. - * - * @type {Boolean} - * @sample {highcharts} - * highcharts/plotoptions/series-datalabels-enabled/ - * Data labels enabled - * @sample {highmaps} maps/demo/color-axis/ Data labels enabled - * @default false - * @apioption plotOptions.series.dataLabels.enabled - */ - - /** - * A [format string](http://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting) - * for the data label. Available variables are the same as for - * `formatter`. - * - * @type {String} - * @sample {highcharts|highstock} - * highcharts/plotoptions/series-datalabels-format/ - * Add a unit - * @sample {highmaps} - * maps/plotoptions/series-datalabels-format/ - * Formatted value in the data label - * @default {highcharts} {y} - * @default {highstock} {y} - * @default {highmaps} {point.value} - * @since 3.0 - * @apioption plotOptions.series.dataLabels.format - */ - - /** - * Callback JavaScript function to format the data label. Note that if a - * `format` is defined, the format takes precedence and the formatter is - * ignored. Available data are: - * - *
- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
`this.percentage`Stacked series and pies only. The point's percentage of the - * total.
`this.point`The point object. The point name, if defined, is available - * through `this.point.name`.
`this.series`:The series object. The series name is available through - * `this.series.name`.
`this.total`Stacked series only. The total value at this point's x value. - *
`this.x`:The x value.
`this.y`:The y value.
- * - * @type {Function} - * @sample {highmaps} maps/plotoptions/series-datalabels-format/ - * Formatted value - */ - formatter: function () { - return this.y === null ? '' : H.numberFormat(this.y, -1); - }, - /*= if (build.classic) { =*/ - - - /** - * Styles for the label. The default `color` setting is `"contrast"`, - * which is a pseudo color that Highcharts picks up and applies the - * maximum contrast to the underlying point item, for example the - * bar in a bar chart. - * - * The `textOutline` is a pseudo property that - * applies an outline of the given width with the given color, which - * by default is the maximum contrast to the text. So a bright text - * color will result in a black text outline for maximum readability - * on a mixed background. In some cases, especially with grayscale - * text, the text outline doesn't work well, in which cases it can - * be disabled by setting it to `"none"`. When `useHTML` is true, the - * `textOutline` will not be picked up. In this, case, the same effect - * can be acheived through the `text-shadow` CSS property. - * - * @type {CSSObject} - * @sample {highcharts} highcharts/plotoptions/series-datalabels-style/ - * Bold labels - * @sample {highmaps} maps/demo/color-axis/ Bold labels - * @default {"color": "contrast", "fontSize": "11px", "fontWeight": "bold", "textOutline": "1px contrast" } - * @since 4.1.0 - */ - style: { - fontSize: '11px', - fontWeight: 'bold', - color: 'contrast', - textOutline: '1px contrast' - }, - - /** - * The name of a symbol to use for the border around the label. Symbols - * are predefined functions on the Renderer object. - * - * @type {String} - * @sample highcharts/plotoptions/series-datalabels-shape/ - * A callout for annotations - * @default square - * @since 4.1.2 - * @apioption plotOptions.series.dataLabels.shape - */ - - /** - * The Z index of the data labels. The default Z index puts it above - * the series. Use a Z index of 2 to display it behind the series. - * - * @type {Number} - * @default 6 - * @since 2.3.5 - * @apioption plotOptions.series.dataLabels.zIndex - */ - - /** - * A declarative filter for which data labels to display. The - * declarative filter is designed for use when callback functions are - * not available, like when the chart options require a pure JSON - * structure or for use with graphical editors. For programmatic - * control, use the `formatter` instead, and return `false` to disable - * a single data label. - * - * @example - * filter: { + /*= if (build.classic) { =*/ + /** + * The SVG value used for the `stroke-linecap` and `stroke-linejoin` + * of a line graph. Round means that lines are rounded in the ends and + * bends. + * + * @validvalue ["round", "butt", "square"] + * @type {String} + * @default round + * @since 3.0.7 + * @apioption plotOptions.line.linecap + */ + + /** + * Pixel width of the graph line. + * + * @type {Number} + * @see In styled mode, the line stroke-width can be set with the + * `.highcharts-graph` class name. + * @sample {highcharts} highcharts/plotoptions/series-linewidth-general/ + * On all series + * @sample {highcharts} highcharts/plotoptions/series-linewidth-specific/ + * On one single series + * @default 2 + * @product highcharts highstock + */ + lineWidth: 2, + /*= } =*/ + + /** + * For some series, there is a limit that shuts down initial animation + * by default when the total number of points in the chart is too high. + * For example, for a column chart and its derivatives, animation doesn't + * run if there is more than 250 points totally. To disable this cap, set + * `animationLimit` to `Infinity`. + * + * @type {Number} + * @apioption plotOptions.series.animationLimit + */ + + /** + * Allow this series' points to be selected by clicking on the graphic + * (columns, point markers, pie slices, map areas etc). + * + * @see [Chart#getSelectedPoints] + * (../class-reference/Highcharts.Chart#getSelectedPoints). + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-allowpointselect-line/ + * Line + * @sample {highcharts} + * highcharts/plotoptions/series-allowpointselect-column/ + * Column + * @sample {highcharts} highcharts/plotoptions/series-allowpointselect-pie/ + * Pie + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ + * Map area + * @sample {highmaps} maps/plotoptions/mapbubble-allowpointselect/ + * Map bubble + * @default false + * @since 1.2.0 + */ + allowPointSelect: false, + + + + /** + * If true, a checkbox is displayed next to the legend item to allow + * selecting the series. The state of the checkbox is determined by + * the `selected` option. + * + * @productdesc {highmaps} + * Note that if a `colorAxis` is defined, the color axis is represented in + * the legend, not the series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-showcheckbox-true/ + * Show select box + * @default false + * @since 1.2.0 + */ + showCheckbox: false, + + + + /** + * Enable or disable the initial animation when a series is displayed. + * The animation can also be set as a configuration object. Please + * note that this option only applies to the initial animation of the + * series itself. For other animations, see [chart.animation]( + * #chart.animation) and the animation parameter under the API methods. The + * following properties are supported: + * + *
+ * + *
duration
+ * + *
The duration of the animation in milliseconds.
+ * + *
easing
+ * + *
A string reference to an easing function set on the `Math` object. + * See the _Custom easing function_ demo below.
+ * + *
+ * + * Due to poor performance, animation is disabled in old IE browsers + * for several chart types. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-animation-disabled/ + * Animation disabled + * @sample {highcharts} highcharts/plotoptions/series-animation-slower/ + * Slower animation + * @sample {highcharts} highcharts/plotoptions/series-animation-easing/ + * Custom easing function + * @sample {highstock} stock/plotoptions/animation-slower/ + * Slower animation + * @sample {highstock} stock/plotoptions/animation-easing/ + * Custom easing function + * @sample {highmaps} maps/plotoptions/series-animation-true/ + * Animation enabled on map series + * @sample {highmaps} maps/plotoptions/mapbubble-animation-false/ + * Disabled on mapbubble series + * @default {highcharts} true + * @default {highstock} true + * @default {highmaps} false + */ + animation: { + duration: 1000 + }, + + /** + * A class name to apply to the series' graphical elements. + * + * @type {String} + * @since 5.0.0 + * @apioption plotOptions.series.className + */ + + /** + * The main color of the series. In line type series it applies to the + * line and the point markers unless otherwise specified. In bar type + * series it applies to the bars unless a color is specified per point. + * The default value is pulled from the `options.colors` array. + * + * In styled mode, the color can be defined by the + * [colorIndex](#plotOptions.series.colorIndex) option. Also, the series + * color can be set with the `.highcharts-series`, `.highcharts-color-{n}`, + * `.highcharts-{type}-series` or `.highcharts-series-{n}` class, or + * individual classes given by the `className` option. + * + * @productdesc {highmaps} + * In maps, the series color is rarely used, as most choropleth maps use the + * color to denote the value of each point. The series color can however be + * used in a map with multiple series holding categorized data. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-color-general/ + * General plot option + * @sample {highcharts} highcharts/plotoptions/series-color-specific/ + * One specific series + * @sample {highcharts} highcharts/plotoptions/series-color-area/ + * Area color + * @sample {highmaps} maps/demo/category-map/ + * Category map by multiple series + * @apioption plotOptions.series.color + */ + + /** + * Styled mode only. A specific color index to use for the series, so its + * graphic representations are given the class name `highcharts-color-{n}`. + * + * @type {Number} + * @since 5.0.0 + * @apioption plotOptions.series.colorIndex + */ + + + /** + * Whether to connect a graph line across null points, or render a gap + * between the two points on either side of the null. + * + * @type {Boolean} + * @default false + * @sample {highcharts} highcharts/plotoptions/series-connectnulls-false/ + * False by default + * @sample {highcharts} highcharts/plotoptions/series-connectnulls-true/ + * True + * @product highcharts highstock + * @apioption plotOptions.series.connectNulls + */ + + + /** + * You can set the cursor to "pointer" if you have click events attached + * to the series, to signal to the user that the points and lines can + * be clicked. + * + * @validvalue [null, "default", "none", "help", "pointer", "crosshair"] + * @type {String} + * @see In styled mode, the series cursor can be set with the same classes + * as listed under [series.color](#plotOptions.series.color). + * @sample {highcharts} highcharts/plotoptions/series-cursor-line/ + * On line graph + * @sample {highcharts} highcharts/plotoptions/series-cursor-column/ + * On columns + * @sample {highcharts} highcharts/plotoptions/series-cursor-scatter/ + * On scatter markers + * @sample {highstock} stock/plotoptions/cursor/ + * Pointer on a line graph + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ + * Map area + * @sample {highmaps} maps/plotoptions/mapbubble-allowpointselect/ + * Map bubble + * @apioption plotOptions.series.cursor + */ + + + /** + * A name for the dash style to use for the graph, or for some series types + * the outline of each shape. The value for the `dashStyle` include: + * + * * Solid + * * ShortDash + * * ShortDot + * * ShortDashDot + * * ShortDashDotDot + * * Dot + * * Dash + * * LongDash + * * DashDot + * * LongDashDot + * * LongDashDotDot + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", "DashDot", + * "LongDashDot", "LongDashDotDot"] + * @type {String} + * @see In styled mode, the [stroke dash-array](http://jsfiddle.net/gh/get/ + * library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/ + * series-dashstyle/) can be set with the same classes as listed under + * [series.color](#plotOptions.series.color). + * + * @sample {highcharts} highcharts/plotoptions/series-dashstyle-all/ + * Possible values demonstrated + * @sample {highcharts} highcharts/plotoptions/series-dashstyle/ + * Chart suitable for printing in black and white + * @sample {highstock} highcharts/plotoptions/series-dashstyle-all/ + * Possible values demonstrated + * @sample {highmaps} highcharts/plotoptions/series-dashstyle-all/ + * Possible values demonstrated + * @sample {highmaps} maps/plotoptions/series-dashstyle/ + * Dotted borders on a map + * @default Solid + * @since 2.1 + * @apioption plotOptions.series.dashStyle + */ + + /** + * Requires the Accessibility module. + * + * A description of the series to add to the screen reader information + * about the series. + * + * @type {String} + * @default undefined + * @since 5.0.0 + * @apioption plotOptions.series.description + */ + + + + + + /** + * Enable or disable the mouse tracking for a specific series. This + * includes point tooltips and click events on graphs and points. For + * large datasets it improves performance. + * + * @type {Boolean} + * @sample {highcharts} + * highcharts/plotoptions/series-enablemousetracking-false/ + * No mouse tracking + * @sample {highmaps} + * maps/plotoptions/series-enablemousetracking-false/ + * No mouse tracking + * @default true + * @apioption plotOptions.series.enableMouseTracking + */ + + /** + * By default, series are exposed to screen readers as regions. By enabling + * this option, the series element itself will be exposed in the same + * way as the data points. This is useful if the series is not used + * as a grouping entity in the chart, but you still want to attach a + * description to the series. + * + * Requires the Accessibility module. + * + * @type {Boolean} + * @sample highcharts/accessibility/art-grants/ + * Accessible data visualization + * @default undefined + * @since 5.0.12 + * @apioption plotOptions.series.exposeElementToA11y + */ + + /** + * Whether to use the Y extremes of the total chart width or only the + * zoomed area when zooming in on parts of the X axis. By default, the + * Y axis adjusts to the min and max of the visible data. Cartesian + * series only. + * + * @type {Boolean} + * @default false + * @since 4.1.6 + * @product highcharts highstock + * @apioption plotOptions.series.getExtremesFromAll + */ + + /** + * An id for the series. This can be used after render time to get a + * pointer to the series object through `chart.get()`. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-id/ Get series by id + * @since 1.2.0 + * @apioption series.id + */ + + /** + * The index of the series in the chart, affecting the internal index + * in the `chart.series` array, the visible Z index as well as the order + * in the legend. + * + * @type {Number} + * @default undefined + * @since 2.3.0 + * @apioption series.index + */ + + /** + * An array specifying which option maps to which key in the data point + * array. This makes it convenient to work with unstructured data arrays + * from different sources. + * + * @type {Array} + * @see [series.data](#series.line.data) + * @sample {highcharts|highstock} highcharts/series/data-keys/ + * An extended data array with keys + * @sample {highcharts|highstock} highcharts/series/data-nested-keys/ + * Nested keys used to access object properties + * @since 4.1.6 + * @product highcharts highstock + * @apioption plotOptions.series.keys + */ + + /** + * The sequential index of the series in the legend. + * + * @sample {highcharts|highstock} highcharts/series/legendindex/ + * Legend in opposite order + * @type {Number} + * @see [legend.reversed](#legend.reversed), + * [yAxis.reversedStacks](#yAxis.reversedStacks) + * @apioption series.legendIndex + */ + + /** + * The line cap used for line ends and line joins on the graph. + * + * @validvalue ["round", "square"] + * @type {String} + * @default round + * @product highcharts highstock + * @apioption plotOptions.series.linecap + */ + + /** + * The [id](#series.id) of another series to link to. Additionally, + * the value can be ":previous" to link to the previous series. When + * two series are linked, only the first one appears in the legend. + * Toggling the visibility of this also toggles the linked series. + * + * @type {String} + * @sample {highcharts} highcharts/demo/arearange-line/ Linked series + * @sample {highstock} highcharts/demo/arearange-line/ Linked series + * @since 3.0 + * @product highcharts highstock + * @apioption plotOptions.series.linkedTo + */ + + /** + * The name of the series as shown in the legend, tooltip etc. + * + * @type {String} + * @sample {highcharts} highcharts/series/name/ Series name + * @sample {highmaps} maps/demo/category-map/ Series name + * @apioption series.name + */ + + /** + * The color for the parts of the graph or points that are below the + * [threshold](#plotOptions.series.threshold). + * + * @type {Color} + * @see In styled mode, a negative color is applied by setting this + * option to `true` combined with the `.highcharts-negative` class name. + * + * @sample {highcharts} highcharts/plotoptions/series-negative-color/ + * Spline, area and column + * @sample {highcharts} highcharts/plotoptions/arearange-negativecolor/ + * Arearange + * @sample {highcharts} highcharts/css/series-negative-color/ + * Styled mode + * @sample {highstock} highcharts/plotoptions/series-negative-color/ + * Spline, area and column + * @sample {highstock} highcharts/plotoptions/arearange-negativecolor/ + * Arearange + * @sample {highmaps} highcharts/plotoptions/series-negative-color/ + * Spline, area and column + * @sample {highmaps} highcharts/plotoptions/arearange-negativecolor/ + * Arearange + * @default null + * @since 3.0 + * @apioption plotOptions.series.negativeColor + */ + + /** + * Same as [accessibility.pointDescriptionFormatter]( + * #accessibility.pointDescriptionFormatter), but for an individual series. + * Overrides the chart wide configuration. + * + * @type {Function} + * @since 5.0.12 + * @apioption plotOptions.series.pointDescriptionFormatter + */ + + /** + * If no x values are given for the points in a series, `pointInterval` + * defines the interval of the x values. For example, if a series contains + * one value every decade starting from year 0, set `pointInterval` to + * `10`. In true `datetime` axes, the `pointInterval` is set in + * milliseconds. + * + * It can be also be combined with `pointIntervalUnit` to draw irregular + * time intervals. + * + * Please note that this options applies to the _series data_, not the + * interval of the axis ticks, which is independent. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-pointstart-datetime/ + * Datetime X axis + * @sample {highstock} stock/plotoptions/pointinterval-pointstart/ + * Using pointStart and pointInterval + * @default 1 + * @product highcharts highstock + * @apioption plotOptions.series.pointInterval + */ + + /** + * On datetime series, this allows for setting the + * [pointInterval](#plotOptions.series.pointInterval) to irregular time + * units, `day`, `month` and `year`. A day is usually the same as 24 hours, + * but `pointIntervalUnit` also takes the DST crossover into consideration + * when dealing with local time. Combine this option with `pointInterval` + * to draw weeks, quarters, 6 months, 10 years etc. + * + * Please note that this options applies to the _series data_, not the + * interval of the axis ticks, which is independent. + * + * @validvalue [null, "day", "month", "year"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-pointintervalunit/ + * One point a month + * @sample {highstock} highcharts/plotoptions/series-pointintervalunit/ + * One point a month + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.pointIntervalUnit + */ + + /** + * Possible values: `null`, `"on"`, `"between"`. + * + * In a column chart, when pointPlacement is `"on"`, the point will + * not create any padding of the X axis. In a polar column chart this + * means that the first column points directly north. If the pointPlacement + * is `"between"`, the columns will be laid out between ticks. This + * is useful for example for visualising an amount between two points + * in time or in a certain sector of a polar chart. + * + * Since Highcharts 3.0.2, the point placement can also be numeric, + * where 0 is on the axis value, -0.5 is between this value and the + * previous, and 0.5 is between this value and the next. Unlike the + * textual options, numeric point placement options won't affect axis + * padding. + * + * Note that pointPlacement needs a [pointRange]( + * #plotOptions.series.pointRange) to work. For column series this is + * computed, but for line-type series it needs to be set. + * + * Defaults to `null` in cartesian charts, `"between"` in polar charts. + * + * @validvalue [null, "on", "between"] + * @type {String|Number} + * @see [xAxis.tickmarkPlacement](#xAxis.tickmarkPlacement) + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-pointplacement-between/ + * Between in a column chart + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-pointplacement-numeric/ + * Numeric placement for custom layout + * @default null + * @since 2.3.0 + * @product highcharts highstock + * @apioption plotOptions.series.pointPlacement + */ + + /** + * If no x values are given for the points in a series, pointStart defines + * on what value to start. For example, if a series contains one yearly + * value starting from 1945, set pointStart to 1945. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-pointstart-linear/ + * Linear + * @sample {highcharts} highcharts/plotoptions/series-pointstart-datetime/ + * Datetime + * @sample {highstock} stock/plotoptions/pointinterval-pointstart/ + * Using pointStart and pointInterval + * @default 0 + * @product highcharts highstock + * @apioption plotOptions.series.pointStart + */ + + /** + * Whether to select the series initially. If `showCheckbox` is true, + * the checkbox next to the series name in the legend will be checked for a + * selected series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-selected/ + * One out of two series selected + * @default false + * @since 1.2.0 + * @apioption plotOptions.series.selected + */ + + /** + * Whether to apply a drop shadow to the graph line. Since 2.3 the shadow + * can be an object configuration containing `color`, `offsetX`, `offsetY`, + * `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/plotoptions/series-shadow/ Shadow enabled + * @default false + * @apioption plotOptions.series.shadow + */ + + /** + * Whether to display this particular series or series type in the legend. + * The default value is `true` for standalone series, `false` for linked + * series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-showinlegend/ + * One series in the legend, one hidden + * @default true + * @apioption plotOptions.series.showInLegend + */ + + /** + * If set to `True`, the accessibility module will skip past the points + * in this series for keyboard navigation. + * + * @type {Boolean} + * @since 5.0.12 + * @apioption plotOptions.series.skipKeyboardNavigation + */ + + /** + * This option allows grouping series in a stacked chart. The stack + * option can be a string or a number or anything else, as long as the + * grouped series' stack options match each other. + * + * @type {String} + * @sample {highcharts} highcharts/series/stack/ Stacked and grouped columns + * @default null + * @since 2.1 + * @product highcharts highstock + * @apioption series.stack + */ + + /** + * Whether to stack the values of each series on top of each other. + * Possible values are `null` to disable, `"normal"` to stack by value or + * `"percent"`. When stacking is enabled, data must be sorted in ascending + * X order. A special stacking option is with the streamgraph series type, + * where the stacking option is set to `"stream"`. + * + * @validvalue [null, "normal", "percent"] + * @type {String} + * @see [yAxis.reversedStacks](#yAxis.reversedStacks) + * @sample {highcharts} highcharts/plotoptions/series-stacking-line/ + * Line + * @sample {highcharts} highcharts/plotoptions/series-stacking-column/ + * Column + * @sample {highcharts} highcharts/plotoptions/series-stacking-bar/ + * Bar + * @sample {highcharts} highcharts/plotoptions/series-stacking-area/ + * Area + * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-line/ + * Line + * @sample {highcharts} + * highcharts/plotoptions/series-stacking-percent-column/ + * Column + * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-bar/ + * Bar + * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-area/ + * Area + * @sample {highstock} stock/plotoptions/stacking/ + * Area + * @default null + * @product highcharts highstock + * @apioption plotOptions.series.stacking + */ + + /** + * Whether to apply steps to the line. Possible values are `left`, `center` + * and `right`. + * + * @validvalue [null, "left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/line-step/ + * Different step line options + * @sample {highcharts} highcharts/plotoptions/area-step/ + * Stepped, stacked area + * @sample {highstock} stock/plotoptions/line-step/ + * Step line + * @default {highcharts} null + * @default {highstock} false + * @since 1.2.5 + * @product highcharts highstock + * @apioption plotOptions.series.step + */ + + /** + * The threshold, also called zero level or base level. For line type + * series this is only used in conjunction with + * [negativeColor](#plotOptions.series.negativeColor). + * + * @type {Number} + * @see [softThreshold](#plotOptions.series.softThreshold). + * @default 0 + * @since 3.0 + * @product highcharts highstock + * @apioption plotOptions.series.threshold + */ + + /** + * The type of series, for example `line` or `column`. By default, the + * series type is inherited from [chart.type](#chart.type), so unless the + * chart is a combination of series types, there is no need to set it on the + * series level. + * + * @validvalue [null, "line", "spline", "column", "area", "areaspline", + * "pie", "arearange", "areasplinerange", "boxplot", "bubble", + * "columnrange", "errorbar", "funnel", "gauge", "scatter", + * "waterfall"] + * @type {String} + * @sample {highcharts} highcharts/series/type/ + * Line and column in the same chart + * @sample {highmaps} maps/demo/mapline-mappoint/ + * Multiple types in the same map + * @apioption series.type + */ + + /** + * Set the initial visibility of the series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-visible/ + * Two series, one hidden and one visible + * @sample {highstock} stock/plotoptions/series-visibility/ + * Hidden series + * @default true + * @apioption plotOptions.series.visible + */ + + /** + * When using dual or multiple x axes, this number defines which xAxis + * the particular series is connected to. It refers to either the [axis + * id](#xAxis.id) or the index of the axis in the xAxis array, with + * 0 being the first. + * + * @type {Number|String} + * @default 0 + * @product highcharts highstock + * @apioption series.xAxis + */ + + /** + * When using dual or multiple y axes, this number defines which yAxis + * the particular series is connected to. It refers to either the [axis + * id](#yAxis.id) or the index of the axis in the yAxis array, with + * 0 being the first. + * + * @type {Number|String} + * @sample {highcharts} highcharts/series/yaxis/ + * Apply the column series to the secondary Y axis + * @default 0 + * @product highcharts highstock + * @apioption series.yAxis + */ + + /** + * Defines the Axis on which the zones are applied. + * + * @type {String} + * @see [zones](#plotOptions.series.zones) + * @sample {highcharts} highcharts/series/color-zones-zoneaxis-x/ + * Zones on the X-Axis + * @sample {highstock} highcharts/series/color-zones-zoneaxis-x/ + * Zones on the X-Axis + * @default y + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zoneAxis + */ + + /** + * Define the visual z index of the series. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-zindex-default/ + * With no z index, the series defined last are on top + * @sample {highcharts} highcharts/plotoptions/series-zindex/ + * With a z index, the series with the highest z index is on top + * @sample {highstock} highcharts/plotoptions/series-zindex-default/ + * With no z index, the series defined last are on top + * @sample {highstock} highcharts/plotoptions/series-zindex/ + * With a z index, the series with the highest z index is on top + * @product highcharts highstock + * @apioption series.zIndex + */ + + /** + * General event handlers for the series items. These event hooks can also + * be attached to the series at run time using the `Highcharts.addEvent` + * function. + */ + + /** + * Fires after the series has finished its initial animation, or in + * case animation is disabled, immediately as the series is displayed. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-afteranimate/ + * Show label after animate + * @sample {highstock} + * highcharts/plotoptions/series-events-afteranimate/ + * Show label after animate + * @since 4.0 + * @product highcharts highstock + * @apioption plotOptions.series.events.afterAnimate + */ + + /** + * Fires when the checkbox next to the series' name in the legend is + * clicked. One parameter, `event`, is passed to the function. The state + * of the checkbox is found by `event.checked`. The checked item is + * found by `event.item`. Return `false` to prevent the default action + * which is to toggle the select state of the series. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-checkboxclick/ + * Alert checkbox status + * @since 1.2.0 + * @apioption plotOptions.series.events.checkboxClick + */ + + /** + * Fires when the series is clicked. One parameter, `event`, is passed + * to the function, containing common event information. Additionally, + * `event.point` holds a pointer to the nearest point on the graph. + * + * @type {Function} + * @context Series + * @sample {highcharts} highcharts/plotoptions/series-events-click/ + * Alert click info + * @sample {highstock} stock/plotoptions/series-events-click/ + * Alert click info + * @sample {highmaps} maps/plotoptions/series-events-click/ + * Display click info in subtitle + * @apioption plotOptions.series.events.click + */ + + /** + * Fires when the series is hidden after chart generation time, either + * by clicking the legend item or by calling `.hide()`. + * + * @type {Function} + * @context Series + * @sample {highcharts} highcharts/plotoptions/series-events-hide/ + * Alert when the series is hidden by clicking the legend item + * @since 1.2.0 + * @apioption plotOptions.series.events.hide + */ + + /** + * Fires when the legend item belonging to the series is clicked. One + * parameter, `event`, is passed to the function. The default action + * is to toggle the visibility of the series. This can be prevented + * by returning `false` or calling `event.preventDefault()`. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-legenditemclick/ + * Confirm hiding and showing + * @apioption plotOptions.series.events.legendItemClick + */ + + /** + * Fires when the mouse leaves the graph. One parameter, `event`, is + * passed to the function, containing common event information. If the + * [stickyTracking](#plotOptions.series) option is true, `mouseOut` + * doesn't happen before the mouse enters another graph or leaves the + * plot area. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-sticky/ + * With sticky tracking by default + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-no-sticky/ + * Without sticky tracking + * @apioption plotOptions.series.events.mouseOut + */ + + /** + * Fires when the mouse enters the graph. One parameter, `event`, is + * passed to the function, containing common event information. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-sticky/ + * With sticky tracking by default + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-no-sticky/ + * Without sticky tracking + * @apioption plotOptions.series.events.mouseOver + */ + + /** + * Fires when the series is shown after chart generation time, either + * by clicking the legend item or by calling `.show()`. + * + * @type {Function} + * @context Series + * @sample {highcharts} highcharts/plotoptions/series-events-show/ + * Alert when the series is shown by clicking the legend item. + * @since 1.2.0 + * @apioption plotOptions.series.events.show + */ + events: {}, + + + + /** + * Options for the point markers of line-like series. Properties like + * `fillColor`, `lineColor` and `lineWidth` define the visual appearance + * of the markers. Other series types, like column series, don't have + * markers, but have visual options on the series level instead. + * + * In styled mode, the markers can be styled with the `.highcharts-point`, + * `.highcharts-point-hover` and `.highcharts-point-select` + * class names. + */ + marker: { + /*= if (build.classic) { =*/ + + + /** + * The width of the point marker's outline. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ + * 2px blue marker + * @default 0 + */ + lineWidth: 0, + + + /** + * The color of the point marker's outline. When `null`, the series' + * or point's color is used. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ + * Inherit from series color (null) + */ + lineColor: '${palette.backgroundColor}', + + /** + * The fill color of the point marker. When `null`, the series' or + * point's color is used. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ + * White fill + * @default null + * @apioption plotOptions.series.marker.fillColor + */ + + /*= } =*/ + + /** + * Enable or disable the point marker. If `null`, the markers are hidden + * when the data is dense, and shown for more widespread data points. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-marker-enabled/ + * Disabled markers + * @sample {highcharts} + * highcharts/plotoptions/series-marker-enabled-false/ + * Disabled in normal state but enabled on hover + * @sample {highstock} stock/plotoptions/series-marker/ + * Enabled markers + * @default {highcharts} null + * @default {highstock} false + * @apioption plotOptions.series.marker.enabled + */ + + /** + * Image markers only. Set the image width explicitly. When using this + * option, a `width` must also be set. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @sample {highstock} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @default null + * @since 4.0.4 + * @apioption plotOptions.series.marker.height + */ + + /** + * A predefined shape or symbol for the marker. When null, the symbol + * is pulled from options.symbols. Other possible values are "circle", + * "square", "diamond", "triangle" and "triangle-down". + * + * 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. + * + * @validvalue [null, "circle", "square", "diamond", "triangle", + * "triangle-down"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-marker-symbol/ + * Predefined, graphic and custom markers + * @sample {highstock} highcharts/plotoptions/series-marker-symbol/ + * Predefined, graphic and custom markers + * @default null + * @apioption plotOptions.series.marker.symbol + */ + + /** + * The threshold for how dense the point markers should be before they + * are hidden, given that `enabled` is not defined. The number indicates + * the horizontal distance between the two closest points in the series, + * as multiples of the `marker.radius`. In other words, the default + * value of 2 means points are hidden if overlapping horizontally. + * + * @since 6.0.5 + * @sample highcharts/plotoptions/series-marker-enabledthreshold + * A higher threshold + */ + enabledThreshold: 2, + + /** + * The radius of the point marker. + * + * @sample {highcharts} highcharts/plotoptions/series-marker-radius/ + * Bigger markers + */ + radius: 4, + + /** + * Image markers only. Set the image width explicitly. When using this + * option, a `height` must also be set. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @sample {highstock} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @default null + * @since 4.0.4 + * @apioption plotOptions.series.marker.width + */ + + + /** + * States for a single point marker. + */ + states: { + + /** + * The normal state of a single point marker. Currently only used + * for setting animation when returning to normal state from hover. + * + * @type {Object} + */ + normal: { + /** + * Animation when returning to normal state after hovering. + * + * @type {Boolean|Object} + */ + animation: true + }, + + /** + * The hover state for a single point marker. + * + * @type {Object} + */ + hover: { + + /** + * Animation when hovering over the marker. + * + * @type {Boolean|Object} + */ + animation: { + duration: 50 + }, + + /** + * Enable or disable the point marker. + * + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-hover-enabled/ + * Disabled hover state + */ + enabled: true, + + /** + * The fill color of the marker in hover state. When `null`, the + * series' or point's fillColor for normal state is used. + * + * @type {Color} + * @default null + * @apioption plotOptions.series.marker.states.hover.fillColor + */ + + /** + * The color of the point marker's outline. When `null`, the + * series' or point's lineColor for normal state is used. + * + * @type {Color} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-hover-linecolor/ + * White fill color, black line color + * @default null + * @apioption plotOptions.series.marker.states.hover.lineColor + */ + + /** + * The width of the point marker's outline. When `null`, the + * series' or point's lineWidth for normal state is used. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-hover-linewidth/ + * 3px line width + * @default null + * @apioption plotOptions.series.marker.states.hover.lineWidth + */ + + /** + * The radius of the point marker. In hover state, it defaults + * to the normal state's radius + 2 as per the [radiusPlus]( + * #plotOptions.series.marker.states.hover.radiusPlus) + * option. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-hover-radius/ + * 10px radius + * @apioption plotOptions.series.marker.states.hover.radius + */ + + /** + * The number of pixels to increase the radius of the hovered + * point. + * + * @sample {highcharts} + * highcharts/plotoptions/series-states-hover-linewidthplus/ + * 5 pixels greater radius on hover + * @sample {highstock} + * highcharts/plotoptions/series-states-hover-linewidthplus/ + * 5 pixels greater radius on hover + * @since 4.0.3 + */ + radiusPlus: 2, + + /*= if (build.classic) { =*/ + + /** + * The additional line width for a hovered point. + * + * @sample {highcharts} + * highcharts/plotoptions/series-states-hover-linewidthplus/ + * 2 pixels wider on hover + * @sample {highstock} + * highcharts/plotoptions/series-states-hover-linewidthplus/ + * 2 pixels wider on hover + * @since 4.0.3 + */ + lineWidthPlus: 1 + /*= } =*/ + }, + /*= if (build.classic) { =*/ + + + + /** + * The appearance of the point marker when selected. In order to + * allow a point to be selected, set the `series.allowPointSelect` + * option to true. + */ + select: { + + /** + * The radius of the point marker. In hover state, it defaults + * to the normal state's radius + 2. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-select-radius/ + * 10px radius for selected points + * @apioption plotOptions.series.marker.states.select.radius + */ + + /** + * Enable or disable visible feedback for selection. + * + * @type {Boolean} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-select-enabled/ + * Disabled select state + * @default true + * @apioption plotOptions.series.marker.states.select.enabled + */ + + /** + * The fill color of the point marker. + * + * @type {Color} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-select-fillcolor/ + * Solid red discs for selected points + * @default #cccccc + */ + fillColor: '${palette.neutralColor20}', + + /** + * The color of the point marker's outline. When `null`, the + * series' or point's color is used. + * + * @type {Color} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-select-linecolor/ + * Red line color for selected points + * @default #000000 + */ + lineColor: '${palette.neutralColor100}', + + /** + * The width of the point marker's outline. + * + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-select-linewidth/ + * 3px line width for selected points + */ + lineWidth: 2 + } + /*= } =*/ + } + }, + + + + /** + * Properties for each single point. + */ + point: { + + + /** + * Fires when a point is clicked. One parameter, `event`, is passed + * to the function, containing common event information. + * + * If the `series.allowPointSelect` option is true, the default + * action for the point's click event is to toggle the point's + * select state. Returning `false` cancels this action. + * + * @type {Function} + * @context Point + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-click/ + * Click marker to alert values + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-click-column/ + * Click column + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-click-url/ + * Go to URL + * @sample {highmaps} + * maps/plotoptions/series-point-events-click/ + * Click marker to display values + * @sample {highmaps} + * maps/plotoptions/series-point-events-click-url/ + * Go to URL + * @apioption plotOptions.series.point.events.click + */ + + /** + * Fires when the mouse leaves the area close to the point. One + * parameter, `event`, is passed to the function, containing common + * event information. + * + * @type {Function} + * @context Point + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-mouseover/ + * Show values in the chart's corner on mouse over + * @apioption plotOptions.series.point.events.mouseOut + */ + + /** + * Fires when the mouse enters the area close to the point. One + * parameter, `event`, is passed to the function, containing common + * event information. + * + * @type {Function} + * @context Point + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-mouseover/ + * Show values in the chart's corner on mouse over + * @apioption plotOptions.series.point.events.mouseOver + */ + + /** + * Fires when the point is removed using the `.remove()` method. One + * parameter, `event`, is passed to the function. Returning `false` + * cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-remove/ + * Remove point and confirm + * @since 1.2.0 + * @apioption plotOptions.series.point.events.remove + */ + + /** + * Fires when the point is selected either programmatically or + * following a click on the point. One parameter, `event`, is passed + * to the function. Returning `false` cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-select/ + * Report the last selected point + * @sample {highmaps} + * maps/plotoptions/series-allowpointselect/ + * Report select and unselect + * @since 1.2.0 + * @apioption plotOptions.series.point.events.select + */ + + /** + * Fires when the point is unselected either programmatically or + * following a click on the point. One parameter, `event`, is passed + * to the function. + * Returning `false` cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-unselect/ + * Report the last unselected point + * @sample {highmaps} + * maps/plotoptions/series-allowpointselect/ + * Report select and unselect + * @since 1.2.0 + * @apioption plotOptions.series.point.events.unselect + */ + + /** + * Fires when the point is updated programmatically through the + * `.update()` method. One parameter, `event`, is passed to the + * function. The new point options can be accessed through + * `event.options`. Returning `false` cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} + * highcharts/plotoptions/series-point-events-update/ + * Confirm point updating + * @since 1.2.0 + * @apioption plotOptions.series.point.events.update + */ + + /** + * Events for each single point. + */ + events: {} + }, + + + + /** + * Options for the series data labels, appearing next to each data + * point. + * + * In styled mode, the data labels can be styled wtih the + * `.highcharts-data-label-box` and `.highcharts-data-label` class names + * ([see example](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/series-datalabels)). + */ + dataLabels: { + + + /** + * The alignment of the data label compared to the point. If `right`, + * the right side of the label should be touching the point. For + * points with an extent, like columns, the alignments also dictates + * how to align it inside the box, as given with the + * [inside](#plotOptions.column.dataLabels.inside) option. Can be one of + * `left`, `center` or `right`. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} + * highcharts/plotoptions/series-datalabels-align-left/ + * Left aligned + * @default center + */ + align: 'center', + + + /** + * Whether to allow data labels to overlap. To make the labels less + * sensitive for overlapping, the [dataLabels.padding]( + * #plotOptions.series.dataLabels.padding) can be set to 0. + * + * @type {Boolean} + * @sample highcharts/plotoptions/series-datalabels-allowoverlap-false/ + * Don't allow overlap + * @default false + * @since 4.1.0 + * @apioption plotOptions.series.dataLabels.allowOverlap + */ + + + /** + * The border radius in pixels for the data label. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @sample {highmaps} maps/plotoptions/series-datalabels-box/ + * Data labels box options + * @default 0 + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.borderRadius + */ + + + /** + * The border width in pixels for the data label. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @default 0 + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.borderWidth + */ + + /** + * A class name for the data label. Particularly in styled mode, this + * can be used to give each series' or point's data label unique + * styling. In addition to this option, a default color class name is + * added so that we can give the labels a + * [contrast text shadow](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/data-label-contrast/). + * + * @type {String} + * @sample {highcharts} highcharts/css/series-datalabels/ Styling by CSS + * @sample {highstock} highcharts/css/series-datalabels/ Styling by CSS + * @sample {highmaps} highcharts/css/series-datalabels/ Styling by CSS + * @since 5.0.0 + * @apioption plotOptions.series.dataLabels.className + */ + + /** + * The text color for the data labels. Defaults to `null`. For certain + * series types, like column or map, the data labels can be drawn inside + * the points. In this case the data label will be drawn with maximum + * contrast by default. Additionally, it will be given a `text-outline` + * style with the opposite color, to further increase the contrast. This + * can be overridden by setting the `text-outline` style to `none` in + * the `dataLabels.style` option. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-color/ + * Red data labels + * @sample {highmaps} maps/demo/color-axis/ + * White data labels + * @apioption plotOptions.series.dataLabels.color + */ + + /** + * Whether to hide data labels that are outside the plot area. By + * default, the data label is moved inside the plot area according to + * the [overflow](#plotOptions.series.dataLabels.overflow) option. + * + * @type {Boolean} + * @default true + * @since 2.3.3 + * @apioption plotOptions.series.dataLabels.crop + */ + + /** + * Whether to defer displaying the data labels until the initial series + * animation has finished. + * + * @type {Boolean} + * @default true + * @since 4.0 + * @product highcharts highstock + * @apioption plotOptions.series.dataLabels.defer + */ + + /** + * Enable or disable the data labels. + * + * @type {Boolean} + * @sample {highcharts} + * highcharts/plotoptions/series-datalabels-enabled/ + * Data labels enabled + * @sample {highmaps} maps/demo/color-axis/ Data labels enabled + * @default false + * @apioption plotOptions.series.dataLabels.enabled + */ + + /** + * A [format string](http://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting) + * for the data label. Available variables are the same as for + * `formatter`. + * + * @type {String} + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-datalabels-format/ + * Add a unit + * @sample {highmaps} + * maps/plotoptions/series-datalabels-format/ + * Formatted value in the data label + * @default {highcharts} {y} + * @default {highstock} {y} + * @default {highmaps} {point.value} + * @since 3.0 + * @apioption plotOptions.series.dataLabels.format + */ + + /** + * Callback JavaScript function to format the data label. Note that if a + * `format` is defined, the format takes precedence and the formatter is + * ignored. Available data are: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
`this.percentage`Stacked series and pies only. The point's percentage of the + * total.
`this.point`The point object. The point name, if defined, is available + * through `this.point.name`.
`this.series`:The series object. The series name is available through + * `this.series.name`.
`this.total`Stacked series only. The total value at this point's x value. + *
`this.x`:The x value.
`this.y`:The y value.
+ * + * @type {Function} + * @sample {highmaps} maps/plotoptions/series-datalabels-format/ + * Formatted value + */ + formatter: function () { + return this.y === null ? '' : H.numberFormat(this.y, -1); + }, + /*= if (build.classic) { =*/ + + + /** + * Styles for the label. The default `color` setting is `"contrast"`, + * which is a pseudo color that Highcharts picks up and applies the + * maximum contrast to the underlying point item, for example the + * bar in a bar chart. + * + * The `textOutline` is a pseudo property that + * applies an outline of the given width with the given color, which + * by default is the maximum contrast to the text. So a bright text + * color will result in a black text outline for maximum readability + * on a mixed background. In some cases, especially with grayscale + * text, the text outline doesn't work well, in which cases it can + * be disabled by setting it to `"none"`. When `useHTML` is true, the + * `textOutline` will not be picked up. In this, case, the same effect + * can be acheived through the `text-shadow` CSS property. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-style/ + * Bold labels + * @sample {highmaps} maps/demo/color-axis/ Bold labels + * @default {"color": "contrast", "fontSize": "11px", "fontWeight": "bold", "textOutline": "1px contrast" } + * @since 4.1.0 + */ + style: { + fontSize: '11px', + fontWeight: 'bold', + color: 'contrast', + textOutline: '1px contrast' + }, + + /** + * The name of a symbol to use for the border around the label. Symbols + * are predefined functions on the Renderer object. + * + * @type {String} + * @sample highcharts/plotoptions/series-datalabels-shape/ + * A callout for annotations + * @default square + * @since 4.1.2 + * @apioption plotOptions.series.dataLabels.shape + */ + + /** + * The Z index of the data labels. The default Z index puts it above + * the series. Use a Z index of 2 to display it behind the series. + * + * @type {Number} + * @default 6 + * @since 2.3.5 + * @apioption plotOptions.series.dataLabels.zIndex + */ + + /** + * A declarative filter for which data labels to display. The + * declarative filter is designed for use when callback functions are + * not available, like when the chart options require a pure JSON + * structure or for use with graphical editors. For programmatic + * control, use the `formatter` instead, and return `false` to disable + * a single data label. + * + * @example + * filter: { * property: 'percentage', * operator: '>', * value: 4 * } - * - * @sample highcharts/demo/pie-monochrome - * Data labels filtered by percentage - * - * @type {Object} - * @since 6.0.3 - * @apioption plotOptions.series.dataLabels.filter - */ - - /** - * The point property to filter by. Point options are passed directly to - * properties, additionally there are `y` value, `percentage` and others - * listed under [Point](https://api.highcharts.com/class-reference/Highcharts.Point) - * members. - * - * @type {String} - * @apioption plotOptions.series.dataLabels.filter.property - */ - - /** - * The operator to compare by. Can be one of `>`, `<`, `>=`, `<=`, `==`, - * and `===`. - * - * @type {String} - * @validvalue [">", "<", ">=", "<=", "==", "===""] - * @apioption plotOptions.series.dataLabels.filter.operator - */ - - /** - * The value to compare against. - * - * @type {Mixed} - * @apioption plotOptions.series.dataLabels.filter.value - */ - - /** - * The background color or gradient for the data label. - * - * @type {Color} - * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @sample {highmaps} maps/plotoptions/series-datalabels-box/ - * Data labels box options - * @since 2.2.1 - * @apioption plotOptions.series.dataLabels.backgroundColor - */ - - /** - * The border color for the data label. Defaults to `undefined`. - * - * @type {Color} - * @sample {highcharts|highstock} - * highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @default undefined - * @since 2.2.1 - * @apioption plotOptions.series.dataLabels.borderColor - */ - - /** - * The shadow of the box. Works best with `borderWidth` or - * `backgroundColor`. Since 2.3 the shadow can be an object - * configuration containing `color`, `offsetX`, `offsetY`, `opacity` and - * `width`. - * - * @type {Boolean|Object} - * @sample {highcharts|highstock} - * highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @default false - * @since 2.2.1 - * @apioption plotOptions.series.dataLabels.shadow - */ - /*= } =*/ - - /** - * For points with an extent, like columns or map areas, whether to - * align the data label inside the box or to the actual value point. - * Defaults to `false` in most cases, `true` in stacked columns. - * - * @type {Boolean} - * @since 3.0 - * @apioption plotOptions.series.dataLabels.inside - */ - - /** - * How to handle data labels that flow outside the plot area. The - * default is `justify`, which aligns them inside the plot area. For - * columns and bars, this means it will be moved inside the bar. To - * display data labels outside the plot area, set `crop` to `false` and - * `overflow` to `"none"`. - * - * @validvalue ["justify", "none"] - * @type {String} - * @default justify - * @since 3.0.6 - * @apioption plotOptions.series.dataLabels.overflow - */ - - /** - * Text rotation in degrees. Note that due to a more complex structure, - * backgrounds, borders and padding will be lost on a rotated data - * label. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-datalabels-rotation/ - * Vertical labels - * @default 0 - * @apioption plotOptions.series.dataLabels.rotation - */ - - /** - * Whether to - * [use HTML](http://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting#html) - * to render the labels. - * - * @type {Boolean} - * @default false - * @apioption plotOptions.series.dataLabels.useHTML - */ - - /** - * The vertical alignment of a data label. Can be one of `top`, `middle` - * or `bottom`. The default value depends on the data, for instance - * in a column chart, the label is above positive values and below - * negative values. - * - * @validvalue ["top", "middle", "bottom"] - * @type {String} - * @since 2.3.3 - */ - verticalAlign: 'bottom', // above singular point - - - /** - * The x position offset of the label relative to the point. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-datalabels-rotation/ - * Vertical and positioned - * @default 0 - */ - x: 0, - - - /** - * The y position offset of the label relative to the point. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-datalabels-rotation/ - * Vertical and positioned - * @default -6 - */ - y: 0, - - - /** - * When either the `borderWidth` or the `backgroundColor` is set, - * this is the padding within the box. - * - * @type {Number} - * @sample {highcharts|highstock} - * highcharts/plotoptions/series-datalabels-box/ - * Data labels box options - * @sample {highmaps} - * maps/plotoptions/series-datalabels-box/ - * Data labels box options - * @default {highcharts} 5 - * @default {highstock} 5 - * @default {highmaps} 0 - * @since 2.2.1 - */ - padding: 5 - }, - - /** - * When the series contains less points than the crop threshold, all - * points are drawn, even if the points fall outside the visible plot - * area at the current zoom. The advantage of drawing all points (including - * markers and columns), is that animation is performed on updates. - * On the other hand, when the series contains more points than the - * crop threshold, the series data is cropped to only contain points - * that fall within the plot area. The advantage of cropping away invisible - * points is to increase performance on large series. - * - * @type {Number} - * @default 300 - * @since 2.2 - * @product highcharts highstock - */ - cropThreshold: 300, - - - - /** - * The width of each point on the x axis. For example in a column chart - * with one value each day, the pointRange would be 1 day (= 24 * 3600 - * * 1000 milliseconds). This is normally computed automatically, but - * this option can be used to override the automatic value. - * - * @type {Number} - * @default 0 - * @product highstock - */ - pointRange: 0, - - /** - * When this is true, the series will not cause the Y axis to cross - * the zero plane (or [threshold](#plotOptions.series.threshold) option) - * unless the data actually crosses the plane. - * - * For example, if `softThreshold` is `false`, a series of 0, 1, 2, - * 3 will make the Y axis show negative values according to the `minPadding` - * option. If `softThreshold` is `true`, the Y axis starts at 0. - * - * @type {Boolean} - * @default true - * @since 4.1.9 - * @product highcharts highstock - */ - softThreshold: true, - - - - /** - * A wrapper object for all the series options in specific states. - * - * @type {plotOptions.series.states} - */ - states: { - - /** - * The normal state of a series, or for point items in column, pie and - * similar series. Currently only used for setting animation when - * returning to normal state from hover. - * @type {Object} - */ - normal: { - /** - * Animation when returning to normal state after hovering. - * @type {Boolean|Object} - */ - animation: true - }, - - /** - * Options for the hovered series. These settings override the normal - * state options when a series is moused over or touched. - * - */ - hover: { - - /** - * Enable separate styles for the hovered series to visualize that - * the user hovers either the series itself or the legend. . - * - * @type {Boolean} - * @sample {highcharts} - * highcharts/plotoptions/series-states-hover-enabled/ - * Line - * @sample {highcharts} - * highcharts/plotoptions/series-states-hover-enabled-column/ - * Column - * @sample {highcharts} - * highcharts/plotoptions/series-states-hover-enabled-pie/ - * Pie - * @default true - * @since 1.2 - * @apioption plotOptions.series.states.hover.enabled - */ - - - /** - * Animation setting for hovering the graph in line-type series. - * - * @type {Boolean|Object} - * @default { "duration": 50 } - * @since 5.0.8 - * @product highcharts - */ - animation: { - /** - * The duration of the hover animation in milliseconds. By - * default the hover state animates quickly in, and slowly back - * to normal. - */ - duration: 50 - }, - - /** - * Pixel width of the graph line. By default this property is - * undefined, and the `lineWidthPlus` property dictates how much - * to increase the linewidth from normal state. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-states-hover-linewidth/ - * 5px line on hover - * @default undefined - * @product highcharts highstock - * @apioption plotOptions.series.states.hover.lineWidth - */ - - - /** - * The additional line width for the graph of a hovered series. - * - * @type {Number} - * @sample {highcharts} - * highcharts/plotoptions/series-states-hover-linewidthplus/ - * 5 pixels wider - * @sample {highstock} - * highcharts/plotoptions/series-states-hover-linewidthplus/ - * 5 pixels wider - * @default 1 - * @since 4.0.3 - * @product highcharts highstock - */ - lineWidthPlus: 1, - - - - /** - * In Highcharts 1.0, the appearance of all markers belonging to the - * hovered series. For settings on the hover state of the individual - * point, see - * [marker.states.hover](#plotOptions.series.marker.states.hover). - * - * @extends plotOptions.series.marker - * @deprecated - * @product highcharts highstock - */ - marker: { - // lineWidth: base + 1, - // radius: base + 1 - }, - - - - /** - * Options for the halo appearing around the hovered point in line- - * type series as well as outside the hovered slice in pie charts. - * By default the halo is filled by the current point or series - * color with an opacity of 0.25\. The halo can be disabled by - * setting the `halo` option to `false`. - * - * In styled mode, the halo is styled with the `.highcharts-halo` - * class, with colors inherited from `.highcharts-color-{n}`. - * - * @type {Object} - * @sample {highcharts} highcharts/plotoptions/halo/ Halo options - * @sample {highstock} highcharts/plotoptions/halo/ Halo options - * @since 4.0 - * @product highcharts highstock - */ - halo: { - - /** - * A collection of SVG attributes to override the appearance of - * the halo, for example `fill`, `stroke` and `stroke-width`. - * - * @type {Object} - * @since 4.0 - * @product highcharts highstock - * @apioption plotOptions.series.states.hover.halo.attributes - */ - - - /** - * The pixel size of the halo. For point markers this is the - * radius of the halo. For pie slices it is the width of the - * halo outside the slice. For bubbles it defaults to 5 and is - * the width of the halo outside the bubble. - * - * @type {Number} - * @default 10 - * @since 4.0 - * @product highcharts highstock - */ - size: 10, - /*= if (build.classic) { =*/ - - - - /** - * Opacity for the halo unless a specific fill is overridden - * using the `attributes` setting. Note that Highcharts is only - * able to apply opacity to colors of hex or rgb(a) formats. - * - * @type {Number} - * @default 0.25 - * @since 4.0 - * @product highcharts highstock - */ - opacity: 0.25 - /*= } =*/ - } - }, - - - /** - * Specific options for point in selected states, after being selected - * by [allowPointSelect](#plotOptions.series.allowPointSelect) or - * programmatically. - * - * @type {Object} - * @extends plotOptions.series.states.hover - * @excluding brightness - * @sample {highmaps} maps/plotoptions/series-allowpointselect/ - * Allow point select demo - * @product highmaps - */ - select: { - marker: {} - } - }, - - - - /** - * Sticky tracking of mouse events. When true, the `mouseOut` event - * on a series isn't triggered until the mouse moves over another series, - * or out of the plot area. When false, the `mouseOut` event on a - * series is triggered when the mouse leaves the area around the series' - * graph or markers. This also implies the tooltip when not shared. When - * `stickyTracking` is false and `tooltip.shared` is false, the tooltip will - * be hidden when moving the mouse between series. Defaults to true for line - * and area type series, but to false for columns, pies etc. - * - * @type {Boolean} - * @sample {highcharts} highcharts/plotoptions/series-stickytracking-true/ - * True by default - * @sample {highcharts} highcharts/plotoptions/series-stickytracking-false/ - * False - * @default {highcharts} true - * @default {highstock} true - * @default {highmaps} false - * @since 2.0 - */ - stickyTracking: true, - - /** - * A configuration object for the tooltip rendering of each single series. - * Properties are inherited from [tooltip](#tooltip), but only the - * following properties can be defined on a series level. - * - * @type {Object} - * @extends tooltip - * @excluding animation,backgroundColor,borderColor,borderRadius, - * borderWidth,crosshairs,enabled,formatter,positioner,shadow, - * shared,shape,snap,style,useHTML - * @since 2.3 - * @apioption plotOptions.series.tooltip - */ - - /** - * When a series contains a data array that is longer than this, only - * one dimensional arrays of numbers, or two dimensional arrays with - * x and y values are allowed. Also, only the first point is tested, - * and the rest are assumed to be the same format. This saves expensive - * data checking and indexing in long series. Set it to `0` disable. - * - * @type {Number} - * @default 1000 - * @since 2.2 - * @product highcharts highstock - */ - turboThreshold: 1000, - - /** - * An array defining zones within a series. Zones can be applied to - * the X axis, Y axis or Z axis for bubbles, according to the `zoneAxis` - * option. - * - * In styled mode, the color zones are styled with the - * `.highcharts-zone-{n}` class, or custom classed from the `className` - * option - * ([view live demo](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/color-zones/)). - * - * @type {Array} - * @see [zoneAxis](#plotOptions.series.zoneAxis) - * @sample {highcharts} highcharts/series/color-zones-simple/ Color zones - * @sample {highstock} highcharts/series/color-zones-simple/ Color zones - * @since 4.1.0 - * @product highcharts highstock - * @apioption plotOptions.series.zones - */ - - /** - * Styled mode only. A custom class name for the zone. - * - * @type {String} - * @sample highcharts/css/color-zones/ Zones styled by class name - * @since 5.0.0 - * @apioption plotOptions.series.zones.className - */ - - /** - * Defines the color of the series. - * - * @type {Color} - * @see [series color](#plotOptions.series.color) - * @since 4.1.0 - * @product highcharts highstock - * @apioption plotOptions.series.zones.color - */ - - /** - * A name for the dash style to use for the graph. - * - * @type {String} - * @see [series.dashStyle](#plotOptions.series.dashStyle) - * @sample {highcharts|highstock} - * highcharts/series/color-zones-dashstyle-dot/ - * Dashed line indicates prognosis - * @since 4.1.0 - * @product highcharts highstock - * @apioption plotOptions.series.zones.dashStyle - */ - - /** - * Defines the fill color for the series (in area type series) - * - * @type {Color} - * @see [fillColor](#plotOptions.area.fillColor) - * @since 4.1.0 - * @product highcharts highstock - * @apioption plotOptions.series.zones.fillColor - */ - - /** - * The value up to where the zone extends, if undefined the zones stretches - * to the last value in the series. - * - * @type {Number} - * @default undefined - * @since 4.1.0 - * @product highcharts highstock - * @apioption plotOptions.series.zones.value - */ - - - - /** - * Determines whether the series should look for the nearest point - * in both dimensions or just the x-dimension when hovering the series. - * Defaults to `'xy'` for scatter series and `'x'` for most other - * series. If the data has duplicate x-values, it is recommended to - * set this to `'xy'` to allow hovering over all points. - * - * Applies only to series types using nearest neighbor search (not - * direct hover) for tooltip. - * - * @validvalue ['x', 'xy'] - * @type {String} - * @sample {highcharts} highcharts/series/findnearestpointby/ - * Different hover behaviors - * @sample {highstock} highcharts/series/findnearestpointby/ - * Different hover behaviors - * @sample {highmaps} highcharts/series/findnearestpointby/ - * Different hover behaviors - * @since 5.0.10 - */ - findNearestPointBy: 'x' + * + * @sample highcharts/demo/pie-monochrome + * Data labels filtered by percentage + * + * @type {Object} + * @since 6.0.3 + * @apioption plotOptions.series.dataLabels.filter + */ + + /** + * The point property to filter by. Point options are passed directly to + * properties, additionally there are `y` value, `percentage` and others + * listed under [Point](https://api.highcharts.com/class-reference/Highcharts.Point) + * members. + * + * @type {String} + * @apioption plotOptions.series.dataLabels.filter.property + */ + + /** + * The operator to compare by. Can be one of `>`, `<`, `>=`, `<=`, `==`, + * and `===`. + * + * @type {String} + * @validvalue [">", "<", ">=", "<=", "==", "===""] + * @apioption plotOptions.series.dataLabels.filter.operator + */ + + /** + * The value to compare against. + * + * @type {Mixed} + * @apioption plotOptions.series.dataLabels.filter.value + */ + + /** + * The background color or gradient for the data label. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @sample {highmaps} maps/plotoptions/series-datalabels-box/ + * Data labels box options + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.backgroundColor + */ + + /** + * The border color for the data label. Defaults to `undefined`. + * + * @type {Color} + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @default undefined + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.borderColor + */ + + /** + * The shadow of the box. Works best with `borderWidth` or + * `backgroundColor`. Since 2.3 the shadow can be an object + * configuration containing `color`, `offsetX`, `offsetY`, `opacity` and + * `width`. + * + * @type {Boolean|Object} + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @default false + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.shadow + */ + /*= } =*/ + + /** + * For points with an extent, like columns or map areas, whether to + * align the data label inside the box or to the actual value point. + * Defaults to `false` in most cases, `true` in stacked columns. + * + * @type {Boolean} + * @since 3.0 + * @apioption plotOptions.series.dataLabels.inside + */ + + /** + * How to handle data labels that flow outside the plot area. The + * default is `justify`, which aligns them inside the plot area. For + * columns and bars, this means it will be moved inside the bar. To + * display data labels outside the plot area, set `crop` to `false` and + * `overflow` to `"none"`. + * + * @validvalue ["justify", "none"] + * @type {String} + * @default justify + * @since 3.0.6 + * @apioption plotOptions.series.dataLabels.overflow + */ + + /** + * Text rotation in degrees. Note that due to a more complex structure, + * backgrounds, borders and padding will be lost on a rotated data + * label. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-datalabels-rotation/ + * Vertical labels + * @default 0 + * @apioption plotOptions.series.dataLabels.rotation + */ + + /** + * Whether to + * [use HTML](http://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting#html) + * to render the labels. + * + * @type {Boolean} + * @default false + * @apioption plotOptions.series.dataLabels.useHTML + */ + + /** + * The vertical alignment of a data label. Can be one of `top`, `middle` + * or `bottom`. The default value depends on the data, for instance + * in a column chart, the label is above positive values and below + * negative values. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @since 2.3.3 + */ + verticalAlign: 'bottom', // above singular point + + + /** + * The x position offset of the label relative to the point. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-datalabels-rotation/ + * Vertical and positioned + * @default 0 + */ + x: 0, + + + /** + * The y position offset of the label relative to the point. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-datalabels-rotation/ + * Vertical and positioned + * @default -6 + */ + y: 0, + + + /** + * When either the `borderWidth` or the `backgroundColor` is set, + * this is the padding within the box. + * + * @type {Number} + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-datalabels-box/ + * Data labels box options + * @sample {highmaps} + * maps/plotoptions/series-datalabels-box/ + * Data labels box options + * @default {highcharts} 5 + * @default {highstock} 5 + * @default {highmaps} 0 + * @since 2.2.1 + */ + padding: 5 + }, + + /** + * When the series contains less points than the crop threshold, all + * points are drawn, even if the points fall outside the visible plot + * area at the current zoom. The advantage of drawing all points (including + * markers and columns), is that animation is performed on updates. + * On the other hand, when the series contains more points than the + * crop threshold, the series data is cropped to only contain points + * that fall within the plot area. The advantage of cropping away invisible + * points is to increase performance on large series. + * + * @type {Number} + * @default 300 + * @since 2.2 + * @product highcharts highstock + */ + cropThreshold: 300, + + + + /** + * The width of each point on the x axis. For example in a column chart + * with one value each day, the pointRange would be 1 day (= 24 * 3600 + * * 1000 milliseconds). This is normally computed automatically, but + * this option can be used to override the automatic value. + * + * @type {Number} + * @default 0 + * @product highstock + */ + pointRange: 0, + + /** + * When this is true, the series will not cause the Y axis to cross + * the zero plane (or [threshold](#plotOptions.series.threshold) option) + * unless the data actually crosses the plane. + * + * For example, if `softThreshold` is `false`, a series of 0, 1, 2, + * 3 will make the Y axis show negative values according to the `minPadding` + * option. If `softThreshold` is `true`, the Y axis starts at 0. + * + * @type {Boolean} + * @default true + * @since 4.1.9 + * @product highcharts highstock + */ + softThreshold: true, + + + + /** + * A wrapper object for all the series options in specific states. + * + * @type {plotOptions.series.states} + */ + states: { + + /** + * The normal state of a series, or for point items in column, pie and + * similar series. Currently only used for setting animation when + * returning to normal state from hover. + * @type {Object} + */ + normal: { + /** + * Animation when returning to normal state after hovering. + * @type {Boolean|Object} + */ + animation: true + }, + + /** + * Options for the hovered series. These settings override the normal + * state options when a series is moused over or touched. + * + */ + hover: { + + /** + * Enable separate styles for the hovered series to visualize that + * the user hovers either the series itself or the legend. . + * + * @type {Boolean} + * @sample {highcharts} + * highcharts/plotoptions/series-states-hover-enabled/ + * Line + * @sample {highcharts} + * highcharts/plotoptions/series-states-hover-enabled-column/ + * Column + * @sample {highcharts} + * highcharts/plotoptions/series-states-hover-enabled-pie/ + * Pie + * @default true + * @since 1.2 + * @apioption plotOptions.series.states.hover.enabled + */ + + + /** + * Animation setting for hovering the graph in line-type series. + * + * @type {Boolean|Object} + * @default { "duration": 50 } + * @since 5.0.8 + * @product highcharts + */ + animation: { + /** + * The duration of the hover animation in milliseconds. By + * default the hover state animates quickly in, and slowly back + * to normal. + */ + duration: 50 + }, + + /** + * Pixel width of the graph line. By default this property is + * undefined, and the `lineWidthPlus` property dictates how much + * to increase the linewidth from normal state. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-states-hover-linewidth/ + * 5px line on hover + * @default undefined + * @product highcharts highstock + * @apioption plotOptions.series.states.hover.lineWidth + */ + + + /** + * The additional line width for the graph of a hovered series. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-states-hover-linewidthplus/ + * 5 pixels wider + * @sample {highstock} + * highcharts/plotoptions/series-states-hover-linewidthplus/ + * 5 pixels wider + * @default 1 + * @since 4.0.3 + * @product highcharts highstock + */ + lineWidthPlus: 1, + + + + /** + * In Highcharts 1.0, the appearance of all markers belonging to the + * hovered series. For settings on the hover state of the individual + * point, see + * [marker.states.hover](#plotOptions.series.marker.states.hover). + * + * @extends plotOptions.series.marker + * @deprecated + * @product highcharts highstock + */ + marker: { + // lineWidth: base + 1, + // radius: base + 1 + }, + + + + /** + * Options for the halo appearing around the hovered point in line- + * type series as well as outside the hovered slice in pie charts. + * By default the halo is filled by the current point or series + * color with an opacity of 0.25\. The halo can be disabled by + * setting the `halo` option to `false`. + * + * In styled mode, the halo is styled with the `.highcharts-halo` + * class, with colors inherited from `.highcharts-color-{n}`. + * + * @type {Object} + * @sample {highcharts} highcharts/plotoptions/halo/ Halo options + * @sample {highstock} highcharts/plotoptions/halo/ Halo options + * @since 4.0 + * @product highcharts highstock + */ + halo: { + + /** + * A collection of SVG attributes to override the appearance of + * the halo, for example `fill`, `stroke` and `stroke-width`. + * + * @type {Object} + * @since 4.0 + * @product highcharts highstock + * @apioption plotOptions.series.states.hover.halo.attributes + */ + + + /** + * The pixel size of the halo. For point markers this is the + * radius of the halo. For pie slices it is the width of the + * halo outside the slice. For bubbles it defaults to 5 and is + * the width of the halo outside the bubble. + * + * @type {Number} + * @default 10 + * @since 4.0 + * @product highcharts highstock + */ + size: 10, + /*= if (build.classic) { =*/ + + + + /** + * Opacity for the halo unless a specific fill is overridden + * using the `attributes` setting. Note that Highcharts is only + * able to apply opacity to colors of hex or rgb(a) formats. + * + * @type {Number} + * @default 0.25 + * @since 4.0 + * @product highcharts highstock + */ + opacity: 0.25 + /*= } =*/ + } + }, + + + /** + * Specific options for point in selected states, after being selected + * by [allowPointSelect](#plotOptions.series.allowPointSelect) or + * programmatically. + * + * @type {Object} + * @extends plotOptions.series.states.hover + * @excluding brightness + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ + * Allow point select demo + * @product highmaps + */ + select: { + marker: {} + } + }, + + + + /** + * Sticky tracking of mouse events. When true, the `mouseOut` event + * on a series isn't triggered until the mouse moves over another series, + * or out of the plot area. When false, the `mouseOut` event on a + * series is triggered when the mouse leaves the area around the series' + * graph or markers. This also implies the tooltip when not shared. When + * `stickyTracking` is false and `tooltip.shared` is false, the tooltip will + * be hidden when moving the mouse between series. Defaults to true for line + * and area type series, but to false for columns, pies etc. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-stickytracking-true/ + * True by default + * @sample {highcharts} highcharts/plotoptions/series-stickytracking-false/ + * False + * @default {highcharts} true + * @default {highstock} true + * @default {highmaps} false + * @since 2.0 + */ + stickyTracking: true, + + /** + * A configuration object for the tooltip rendering of each single series. + * Properties are inherited from [tooltip](#tooltip), but only the + * following properties can be defined on a series level. + * + * @type {Object} + * @extends tooltip + * @excluding animation,backgroundColor,borderColor,borderRadius, + * borderWidth,crosshairs,enabled,formatter,positioner,shadow, + * shared,shape,snap,style,useHTML + * @since 2.3 + * @apioption plotOptions.series.tooltip + */ + + /** + * When a series contains a data array that is longer than this, only + * one dimensional arrays of numbers, or two dimensional arrays with + * x and y values are allowed. Also, only the first point is tested, + * and the rest are assumed to be the same format. This saves expensive + * data checking and indexing in long series. Set it to `0` disable. + * + * @type {Number} + * @default 1000 + * @since 2.2 + * @product highcharts highstock + */ + turboThreshold: 1000, + + /** + * An array defining zones within a series. Zones can be applied to + * the X axis, Y axis or Z axis for bubbles, according to the `zoneAxis` + * option. + * + * In styled mode, the color zones are styled with the + * `.highcharts-zone-{n}` class, or custom classed from the `className` + * option + * ([view live demo](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/color-zones/)). + * + * @type {Array} + * @see [zoneAxis](#plotOptions.series.zoneAxis) + * @sample {highcharts} highcharts/series/color-zones-simple/ Color zones + * @sample {highstock} highcharts/series/color-zones-simple/ Color zones + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones + */ + + /** + * Styled mode only. A custom class name for the zone. + * + * @type {String} + * @sample highcharts/css/color-zones/ Zones styled by class name + * @since 5.0.0 + * @apioption plotOptions.series.zones.className + */ + + /** + * Defines the color of the series. + * + * @type {Color} + * @see [series color](#plotOptions.series.color) + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.color + */ + + /** + * A name for the dash style to use for the graph. + * + * @type {String} + * @see [series.dashStyle](#plotOptions.series.dashStyle) + * @sample {highcharts|highstock} + * highcharts/series/color-zones-dashstyle-dot/ + * Dashed line indicates prognosis + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.dashStyle + */ + + /** + * Defines the fill color for the series (in area type series) + * + * @type {Color} + * @see [fillColor](#plotOptions.area.fillColor) + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.fillColor + */ + + /** + * The value up to where the zone extends, if undefined the zones stretches + * to the last value in the series. + * + * @type {Number} + * @default undefined + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.value + */ + + + + /** + * Determines whether the series should look for the nearest point + * in both dimensions or just the x-dimension when hovering the series. + * Defaults to `'xy'` for scatter series and `'x'` for most other + * series. If the data has duplicate x-values, it is recommended to + * set this to `'xy'` to allow hovering over all points. + * + * Applies only to series types using nearest neighbor search (not + * direct hover) for tooltip. + * + * @validvalue ['x', 'xy'] + * @type {String} + * @sample {highcharts} highcharts/series/findnearestpointby/ + * Different hover behaviors + * @sample {highstock} highcharts/series/findnearestpointby/ + * Different hover behaviors + * @sample {highmaps} highcharts/series/findnearestpointby/ + * Different hover behaviors + * @since 5.0.10 + */ + findNearestPointBy: 'x' }, /** @lends Highcharts.Series.prototype */ { - isCartesian: true, - pointClass: Point, - sorted: true, // requires the data to be sorted - requireSorting: true, - directTouch: false, - axisTypes: ['xAxis', 'yAxis'], - colorCounter: 0, - // each point's x and y values are stored in this.xData and this.yData - parallelArrays: ['x', 'y'], - coll: 'series', - init: function (chart, options) { - var series = this, - events, - chartSeries = chart.series, - lastSeries; - - /** - * Read only. The chart that the series belongs to. - * - * @name chart - * @memberOf Series - * @type {Chart} - */ - series.chart = chart; - - /** - * Read only. The series' type, like "line", "area", "column" etc. The - * type in the series options anc can be altered using {@link - * Series#update}. - * - * @name type - * @memberOf Series - * @type String - */ - - /** - * Read only. The series' current options. To update, use {@link - * Series#update}. - * - * @name options - * @memberOf Series - * @type SeriesOptions - */ - series.options = options = series.setOptions(options); - series.linkedSeries = []; - - // bind the axes - series.bindAxes(); - - // set some variables - extend(series, { - /** - * The series name as given in the options. Defaults to - * "Series {n}". - * - * @name name - * @memberOf Series - * @type {String} - */ - name: options.name, - state: '', - /** - * Read only. The series' visibility state as set by {@link - * Series#show}, {@link Series#hide}, or in the initial - * configuration. - * - * @name visible - * @memberOf Series - * @type {Boolean} - */ - visible: options.visible !== false, // true by default - /** - * Read only. The series' selected state as set by {@link - * Highcharts.Series#select}. - * - * @name selected - * @memberOf Series - * @type {Boolean} - */ - selected: options.selected === true // false by default - }); - - // register event listeners - events = options.events; - - objectEach(events, function (event, eventType) { - addEvent(series, eventType, event); - }); - if ( - (events && events.click) || - ( - options.point && - options.point.events && - options.point.events.click - ) || - options.allowPointSelect - ) { - chart.runTrackerClick = true; - } - - series.getColor(); - series.getSymbol(); - - // Set the data - each(series.parallelArrays, function (key) { - series[key + 'Data'] = []; - }); - series.setData(options.data, false); - - // Mark cartesian - if (series.isCartesian) { - chart.hasCartesianSeries = true; - } - - // Get the index and register the series in the chart. The index is one - // more than the current latest series index (#5960). - if (chartSeries.length) { - lastSeries = chartSeries[chartSeries.length - 1]; - } - series._i = pick(lastSeries && lastSeries._i, -1) + 1; - - // Insert the series and re-order all series above the insertion point. - chart.orderSeries(this.insert(chartSeries)); - - fireEvent(this, 'afterInit'); - }, - - /** - * Insert the series in a collection with other series, either the chart - * series or yAxis series, in the correct order according to the index - * option. Used internally when adding series. - * - * @private - * @param {Array.} collection - * A collection of series, like `chart.series` or `xAxis.series`. - * @returns {Number} The index of the series in the collection. - */ - insert: function (collection) { - var indexOption = this.options.index, - i; - - // Insert by index option - if (isNumber(indexOption)) { - i = collection.length; - while (i--) { - // Loop down until the interted element has higher index - if (indexOption >= - pick(collection[i].options.index, collection[i]._i)) { - collection.splice(i + 1, 0, this); - break; - } - } - if (i === -1) { - collection.unshift(this); - } - i = i + 1; - - // Or just push it to the end - } else { - collection.push(this); - } - return pick(i, collection.length - 1); - }, - - /** - * Set the xAxis and yAxis properties of cartesian series, and register the - * series in the `axis.series` array. - * - * @private - */ - bindAxes: function () { - var series = this, - seriesOptions = series.options, - chart = series.chart, - axisOptions; - - // repeat for xAxis and yAxis - each(series.axisTypes || [], function (AXIS) { - - // loop through the chart's axis objects - each(chart[AXIS], function (axis) { - axisOptions = axis.options; - - // apply if the series xAxis or yAxis option mathches the number - // of the axis, or if undefined, use the first axis - if ( - seriesOptions[AXIS] === axisOptions.index || - ( - seriesOptions[AXIS] !== undefined && - seriesOptions[AXIS] === axisOptions.id - ) || - ( - seriesOptions[AXIS] === undefined && - axisOptions.index === 0 - ) - ) { - - // register this series in the axis.series lookup - series.insert(axis.series); - - // set this series.xAxis or series.yAxis reference - /** - * Read only. The unique xAxis object associated with the - * series. - * - * @name xAxis - * @memberOf Series - * @type Axis - */ - /** - * Read only. The unique yAxis object associated with the - * series. - * - * @name yAxis - * @memberOf Series - * @type Axis - */ - series[AXIS] = axis; - - // mark dirty for redraw - axis.isDirty = true; - } - }); - - // The series needs an X and an Y axis - if (!series[AXIS] && series.optionalAxis !== AXIS) { - H.error(18, true); - } - - }); - }, - - /** - * For simple series types like line and column, the data values are held in - * arrays like xData and yData for quick lookup to find extremes and more. - * For multidimensional series like bubble and map, this can be extended - * with arrays like zData and valueData by adding to the - * `series.parallelArrays` array. - * - * @private - */ - updateParallelArrays: function (point, i) { - var series = point.series, - args = arguments, - fn = isNumber(i) ? - // Insert the value in the given position - function (key) { - var val = key === 'y' && series.toYData ? - series.toYData(point) : - point[key]; - series[key + 'Data'][i] = val; - } : - // Apply the method specified in i with the following arguments - // as arguments - function (key) { - Array.prototype[i].apply( - series[key + 'Data'], - Array.prototype.slice.call(args, 2) - ); - }; - - each(series.parallelArrays, fn); - }, - - /** - * Return an auto incremented x value based on the pointStart and - * pointInterval options. This is only used if an x value is not given for - * the point that calls autoIncrement. - * - * @private - */ - autoIncrement: function () { - - var options = this.options, - xIncrement = this.xIncrement, - date, - pointInterval, - pointIntervalUnit = options.pointIntervalUnit, - time = this.chart.time; - - xIncrement = pick(xIncrement, options.pointStart, 0); - - this.pointInterval = pointInterval = pick( - this.pointInterval, - options.pointInterval, - 1 - ); - - // Added code for pointInterval strings - if (pointIntervalUnit) { - date = new time.Date(xIncrement); - - if (pointIntervalUnit === 'day') { - time.set( - 'Date', - date, - time.get('Date', date) + pointInterval - ); - } else if (pointIntervalUnit === 'month') { - time.set( - 'Month', - date, - time.get('Month', date) + pointInterval - ); - } else if (pointIntervalUnit === 'year') { - time.set( - 'FullYear', - date, - time.get('FullYear', date) + pointInterval - ); - } - - pointInterval = date.getTime() - xIncrement; - - } - - this.xIncrement = xIncrement + pointInterval; - return xIncrement; - }, - - /** - * Set the series options by merging from the options tree. Called - * internally on initiating and updating series. This function will not - * redraw the series. For API usage, use {@link Series#update}. - * - * @param {Options.plotOptions.series} itemOptions - * The series options. - */ - setOptions: function (itemOptions) { - var chart = this.chart, - chartOptions = chart.options, - plotOptions = chartOptions.plotOptions, - userOptions = chart.userOptions || {}, - userPlotOptions = userOptions.plotOptions || {}, - typeOptions = plotOptions[this.type], - options, - zones; - - this.userOptions = itemOptions; - - // General series options take precedence over type options because - // otherwise, default type options like column.animation would be - // overwritten by the general option. But issues have been raised here - // (#3881), and the solution may be to distinguish between default - // option and userOptions like in the tooltip below. - options = merge( - typeOptions, - plotOptions.series, - itemOptions - ); - - // The tooltip options are merged between global and series specific - // options. Importance order asscendingly: - // globals: (1)tooltip, (2)plotOptions.series, (3)plotOptions[this.type] - // init userOptions with possible later updates: 4-6 like 1-3 and - // (7)this series options - this.tooltipOptions = merge( - defaultOptions.tooltip, // 1 - defaultOptions.plotOptions.series && - defaultOptions.plotOptions.series.tooltip, // 2 - defaultOptions.plotOptions[this.type].tooltip, // 3 - chartOptions.tooltip.userOptions, // 4 - plotOptions.series && plotOptions.series.tooltip, // 5 - plotOptions[this.type].tooltip, // 6 - itemOptions.tooltip // 7 - ); - - // When shared tooltip, stickyTracking is true by default, - // unless user says otherwise. - this.stickyTracking = pick( - itemOptions.stickyTracking, - userPlotOptions[this.type] && - userPlotOptions[this.type].stickyTracking, - userPlotOptions.series && userPlotOptions.series.stickyTracking, - ( - this.tooltipOptions.shared && !this.noSharedTooltip ? - true : - options.stickyTracking - ) - ); - - // Delete marker object if not allowed (#1125) - if (typeOptions.marker === null) { - delete options.marker; - } - - // Handle color zones - this.zoneAxis = options.zoneAxis; - zones = this.zones = (options.zones || []).slice(); - if ( - (options.negativeColor || options.negativeFillColor) && - !options.zones - ) { - zones.push({ - value: - options[this.zoneAxis + 'Threshold'] || - options.threshold || - 0, - className: 'highcharts-negative', - /*= if (build.classic) { =*/ - color: options.negativeColor, - fillColor: options.negativeFillColor - /*= } =*/ - }); - } - if (zones.length) { // Push one extra zone for the rest - if (defined(zones[zones.length - 1].value)) { - zones.push({ - /*= if (build.classic) { =*/ - color: this.color, - fillColor: this.fillColor - /*= } =*/ - }); - } - } - - fireEvent(this, 'afterSetOptions', { options: options }); - - return options; - }, - - /** - * Return series name in "Series {Number}" format or the one defined by a - * user. This method can be simply overridden as series name format can - * vary (e.g. technical indicators). - * - * @return {String} The series name. - */ - getName: function () { - return this.name || 'Series ' + (this.index + 1); - }, - - getCyclic: function (prop, value, defaults) { - var i, - chart = this.chart, - userOptions = this.userOptions, - indexName = prop + 'Index', - counterName = prop + 'Counter', - len = defaults ? defaults.length : pick( - chart.options.chart[prop + 'Count'], - chart[prop + 'Count'] - ), - setting; - - if (!value) { - // Pick up either the colorIndex option, or the _colorIndex after - // Series.update() - setting = pick( - userOptions[indexName], - userOptions['_' + indexName] - ); - if (defined(setting)) { // after Series.update() - i = setting; - } else { - // #6138 - if (!chart.series.length) { - chart[counterName] = 0; - } - userOptions['_' + indexName] = i = chart[counterName] % len; - chart[counterName] += 1; - } - if (defaults) { - value = defaults[i]; - } - } - // Set the colorIndex - if (i !== undefined) { - this[indexName] = i; - } - this[prop] = value; - }, - - /** - * Get the series' color based on either the options or pulled from global - * options. - * - * @return {Color} The series color. - */ - /*= if (!build.classic) { =*/ - getColor: function () { - this.getCyclic('color'); - }, - - /*= } else { =*/ - getColor: function () { - if (this.options.colorByPoint) { - // #4359, selected slice got series.color even when colorByPoint was - // set. - this.options.color = null; - } else { - this.getCyclic( - 'color', - this.options.color || defaultPlotOptions[this.type].color, - this.chart.options.colors - ); - } - }, - /*= } =*/ - /** - * Get the series' symbol based on either the options or pulled from global - * options. - */ - getSymbol: function () { - var seriesMarkerOption = this.options.marker; - - this.getCyclic( - 'symbol', - seriesMarkerOption.symbol, - this.chart.options.symbols - ); - }, - - drawLegendSymbol: LegendSymbolMixin.drawLineMarker, - - /** - * Internal function called from setData. If the point count is the same as - * is was, or if there are overlapping X values, just run Point.update which - * is cheaper, allows animation, and keeps references to points. This also - * allows adding or removing points if the X-es don't match. - * - * @private - */ - updateData: function (data) { - var options = this.options, - oldData = this.points, - pointsToAdd = [], - hasUpdatedByKey, - i, - point, - lastIndex, - requireSorting = this.requireSorting; - - // Iterate the new data - each(data, function (pointOptions) { - var x, - pointIndex; - - // Get the x of the new data point - x = ( - H.defined(pointOptions) && - this.pointClass.prototype.optionsToObject.call( - { series: this }, - pointOptions - ).x - ); - - if (isNumber(x)) { - // Search for the same X in the existing data set - pointIndex = H.inArray(x, this.xData, lastIndex); - - // Matching X not found, add point (but later) - if (pointIndex === -1) { - pointsToAdd.push(pointOptions); - - // Matching X found, update - } else if (pointOptions !== options.data[pointIndex]) { - oldData[pointIndex].update( - pointOptions, - false, - null, - false - ); - - // Mark it touched, below we will remove all points that - // are not touched. - oldData[pointIndex].touched = true; - - // Speed optimize by only searching from last known index. - // Performs ~20% bettor on large data sets. - if (requireSorting) { - lastIndex = pointIndex; - } - } - hasUpdatedByKey = true; - } - }, this); - - // Remove points that don't exist in the updated data set - if (hasUpdatedByKey) { - i = oldData.length; - while (i--) { - point = oldData[i]; - if (!point.touched) { - point.remove(false); - } - point.touched = false; - } - - // If we did not find keys (x-values), and the length is the same, - // update one-to-one - } else if (data.length === oldData.length) { - each(data, function (point, i) { - // .update doesn't exist on a linked, hidden series (#3709) - if (oldData[i].update && point !== options.data[i]) { - oldData[i].update(point, false, null, false); - } - }); - - // Did not succeed in updating data - } else { - return false; - } - - // Add new points - each(pointsToAdd, function (point) { - this.addPoint(point, false); - }, this); - - return true; - }, - - /** - * Apply a new set of data to the series and optionally redraw it. The new - * data array is passed by reference (except in case of `updatePoints`), and - * may later be mutated when updating the chart data. - * - * Note the difference in behaviour when setting the same amount of points, - * or a different amount of points, as handled by the `updatePoints` - * parameter. - * - * @param {SeriesDataOptions} data - * Takes an array of data in the same format as described under - * `series.typedata` for the given series type. - * @param {Boolean} [redraw=true] - * Whether to redraw the chart after the series is altered. If doing - * more operations on the chart, it is a good idea to set redraw to - * false and call {@link Chart#redraw} after. - * @param {AnimationOptions} [animation] - * When the updated data is the same length as the existing data, - * points will be updated by default, and animation visualizes how - * the points are changed. Set false to disable animation, or a - * configuration object to set duration or easing. - * @param {Boolean} [updatePoints=true] - * When the updated data is the same length as the existing data, or - * points can be matched by X values, points will be updated instead - * of replaced. This allows updating with animation and performs - * better. In this case, the original array is not passed by - * reference. Set `false` to prevent. - * - * @sample highcharts/members/series-setdata/ - * Set new data from a button - * @sample highcharts/members/series-setdata-pie/ - * Set data in a pie - * @sample stock/members/series-setdata/ - * Set new data in Highstock - * @sample maps/members/series-setdata/ - * Set new data in Highmaps - */ - setData: function (data, redraw, animation, updatePoints) { - var series = this, - oldData = series.points, - oldDataLength = (oldData && oldData.length) || 0, - dataLength, - options = series.options, - chart = series.chart, - firstPoint = null, - xAxis = series.xAxis, - i, - turboThreshold = options.turboThreshold, - pt, - xData = this.xData, - yData = this.yData, - pointArrayMap = series.pointArrayMap, - valueCount = pointArrayMap && pointArrayMap.length, - updatedData; - - data = data || []; - dataLength = data.length; - redraw = pick(redraw, true); - - // If the point count is the same as is was, just run Point.update which - // is cheaper, allows animation, and keeps references to points. - if ( - updatePoints !== false && - dataLength && - oldDataLength && - !series.cropped && - !series.hasGroupedData && - series.visible - ) { - updatedData = this.updateData(data); - } - - if (!updatedData) { - - // Reset properties - series.xIncrement = null; - - series.colorCounter = 0; // for series with colorByPoint (#1547) - - // Update parallel arrays - each(this.parallelArrays, function (key) { - series[key + 'Data'].length = 0; - }); - - // In turbo mode, only one- or twodimensional arrays of numbers are - // allowed. The first value is tested, and we assume that all the - // rest are defined the same way. Although the 'for' loops are - // similar, they are repeated inside each if-else conditional for - // max performance. - if (turboThreshold && dataLength > turboThreshold) { - - // find the first non-null point - i = 0; - while (firstPoint === null && i < dataLength) { - firstPoint = data[i]; - i++; - } - - - if (isNumber(firstPoint)) { // assume all points are numbers - for (i = 0; i < dataLength; i++) { - xData[i] = this.autoIncrement(); - yData[i] = data[i]; - } - - // Assume all points are arrays when first point is - } else if (isArray(firstPoint)) { - if (valueCount) { // [x, low, high] or [x, o, h, l, c] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt.slice(1, valueCount + 1); - } - } else { // [x, y] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt[1]; - } - } - } else { - // Highcharts expects configs to be numbers or arrays in - // turbo mode - H.error(12); - } - } else { - for (i = 0; i < dataLength; i++) { - if (data[i] !== undefined) { // stray commas in oldIE - pt = { series: series }; - series.pointClass.prototype.applyOptions.apply( - pt, - [data[i]] - ); - series.updateParallelArrays(pt, i); - } - } - } - - // Forgetting to cast strings to numbers is a common caveat when - // handling CSV or JSON - if (yData && isString(yData[0])) { - H.error(14, true); - } - - series.data = []; - series.options.data = series.userOptions.data = data; - - // destroy old points - i = oldDataLength; - while (i--) { - if (oldData[i] && oldData[i].destroy) { - oldData[i].destroy(); - } - } - - // reset minRange (#878) - if (xAxis) { - xAxis.minRange = xAxis.userMinRange; - } - - // redraw - series.isDirty = chart.isDirtyBox = true; - series.isDirtyData = !!oldData; - animation = false; - } - - // Typically for pie series, points need to be processed and generated - // prior to rendering the legend - if (options.legendType === 'point') { - this.processData(); - this.generatePoints(); - } - - if (redraw) { - chart.redraw(animation); - } - }, - - /** - * Internal function to process the data by cropping away unused data points - * if the series is longer than the crop threshold. This saves computing - * time for large series. In Highstock, this function is extended to - * provide data grouping. - * - * @private - * @param {Boolean} force - * Force data grouping. - */ - processData: function (force) { - var series = this, - processedXData = series.xData, // copied during slice operation - processedYData = series.yData, - dataLength = processedXData.length, - croppedData, - cropStart = 0, - cropped, - distance, - closestPointRange, - xAxis = series.xAxis, - i, // loop variable - options = series.options, - cropThreshold = options.cropThreshold, - getExtremesFromAll = - series.getExtremesFromAll || - options.getExtremesFromAll, // #4599 - isCartesian = series.isCartesian, - xExtremes, - val2lin = xAxis && xAxis.val2lin, - isLog = xAxis && xAxis.isLog, - throwOnUnsorted = series.requireSorting, - min, - max; - - // If the series data or axes haven't changed, don't go through this. - // Return false to pass the message on to override methods like in data - // grouping. - if ( - isCartesian && - !series.isDirty && - !xAxis.isDirty && - !series.yAxis.isDirty && - !force - ) { - return false; - } - - if (xAxis) { - xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) - min = xExtremes.min; - max = xExtremes.max; - } - - // optionally filter out points outside the plot area - if ( - isCartesian && - series.sorted && - !getExtremesFromAll && - (!cropThreshold || dataLength > cropThreshold || series.forceCrop) - ) { - - // it's outside current extremes - if ( - processedXData[dataLength - 1] < min || - processedXData[0] > max - ) { - processedXData = []; - processedYData = []; - - // only crop if it's actually spilling out - } else if ( - processedXData[0] < min || - processedXData[dataLength - 1] > max - ) { - croppedData = this.cropData( - series.xData, - series.yData, - min, - max - ); - processedXData = croppedData.xData; - processedYData = croppedData.yData; - cropStart = croppedData.start; - cropped = true; - } - } - - - // Find the closest distance between processed points - i = processedXData.length || 1; - while (--i) { - distance = isLog ? - val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : - processedXData[i] - processedXData[i - 1]; - - if ( - distance > 0 && - ( - closestPointRange === undefined || - distance < closestPointRange - ) - ) { - closestPointRange = distance; - - // Unsorted data is not supported by the line tooltip, as well as - // data grouping and navigation in Stock charts (#725) and width - // calculation of columns (#1900) - } else if (distance < 0 && throwOnUnsorted) { - H.error(15); - throwOnUnsorted = false; // Only once - } - } - - // Record the properties - series.cropped = cropped; // undefined or true - series.cropStart = cropStart; - series.processedXData = processedXData; - series.processedYData = processedYData; - - series.closestPointRange = closestPointRange; - - }, - - /** - * Iterate over xData and crop values between min and max. Returns object - * containing crop start/end cropped xData with corresponding part of yData, - * dataMin and dataMax within the cropped range. - * - * @private - */ - cropData: function (xData, yData, min, max, cropShoulder) { - var dataLength = xData.length, - cropStart = 0, - cropEnd = dataLength, - i, - j; - - // line-type series need one point outside - cropShoulder = pick(cropShoulder, this.cropShoulder, 1); - - // iterate up to find slice start - for (i = 0; i < dataLength; i++) { - if (xData[i] >= min) { - cropStart = Math.max(0, i - cropShoulder); - break; - } - } - - // proceed to find slice end - for (j = i; j < dataLength; j++) { - if (xData[j] > max) { - cropEnd = j + cropShoulder; - break; - } - } - - return { - xData: xData.slice(cropStart, cropEnd), - yData: yData.slice(cropStart, cropEnd), - start: cropStart, - end: cropEnd - }; - }, - - - /** - * Generate the data point after the data has been processed by cropping - * away unused points and optionally grouped in Highcharts Stock. - * - * @private - */ - generatePoints: function () { - var series = this, - options = series.options, - dataOptions = options.data, - data = series.data, - dataLength, - processedXData = series.processedXData, - processedYData = series.processedYData, - PointClass = series.pointClass, - processedDataLength = processedXData.length, - cropStart = series.cropStart || 0, - cursor, - hasGroupedData = series.hasGroupedData, - keys = options.keys, - point, - points = [], - i; - - if (!data && !hasGroupedData) { - var arr = []; - arr.length = dataOptions.length; - data = series.data = arr; - } - - if (keys && hasGroupedData) { - // grouped data has already applied keys (#6590) - series.options.keys = false; - } - - for (i = 0; i < processedDataLength; i++) { - cursor = cropStart + i; - if (!hasGroupedData) { - point = data[cursor]; - if (!point && dataOptions[cursor] !== undefined) { // #970 - data[cursor] = point = (new PointClass()).init( - series, - dataOptions[cursor], - processedXData[i] - ); - } - } else { - // splat the y data in case of ohlc data array - point = (new PointClass()).init( - series, - [processedXData[i]].concat(splat(processedYData[i])) - ); - - /** - * Highstock only. If a point object is created by data - * grouping, it doesn't reflect actual points in the raw data. - * In this case, the `dataGroup` property holds information - * that points back to the raw data. - * - * - `dataGroup.start` is the index of the first raw data point - * in the group. - * - `dataGroup.length` is the amount of points in the group. - * - * @name dataGroup - * @memberOf Point - * @type {Object} - * - */ - point.dataGroup = series.groupMap[i]; - } - if (point) { // #6279 - point.index = cursor; // For faster access in Point.update - points[i] = point; - } - } - - // restore keys options (#6590) - series.options.keys = keys; - - // Hide cropped-away points - this only runs when the number of points - // is above cropThreshold, or when swithching view from non-grouped - // data to grouped data (#637) - if ( - data && - ( - processedDataLength !== (dataLength = data.length) || - hasGroupedData - ) - ) { - for (i = 0; i < dataLength; i++) { - // when has grouped data, clear all points - if (i === cropStart && !hasGroupedData) { - i += processedDataLength; - } - if (data[i]) { - data[i].destroyElements(); - data[i].plotX = undefined; // #1003 - } - } - } - - /** - * Read only. An array containing those values converted to points. - * In case the series data length exceeds the `cropThreshold`, or if the - * data is grouped, `series.data` doesn't contain all the points. Also, - * in case a series is hidden, the `data` array may be empty. To access - * raw values, `series.options.data` will always be up to date. - * `Series.data` only contains the points that have been created on - * demand. To modify the data, use {@link Highcharts.Series#setData} or - * {@link Highcharts.Point#update}. - * - * @name data - * @memberOf Highcharts.Series - * @see Series.points - * @type {Array.} - */ - series.data = data; - - /** - * An array containing all currently visible point objects. In case of - * cropping, the cropped-away points are not part of this array. The - * `series.points` array starts at `series.cropStart` compared to - * `series.data` and `series.options.data`. If however the series data - * is grouped, these can't be correlated one to one. To - * modify the data, use {@link Highcharts.Series#setData} or {@link - * Highcharts.Point#update}. - * @name points - * @memberof Series - * @type {Array.} - */ - series.points = points; - }, - - /** - * Calculate Y extremes for the visible data. The result is set as - * `dataMin` and `dataMax` on the Series item. - * - * @param {Array.} [yData] - * The data to inspect. Defaults to the current data within the - * visible range. - * - */ - getExtremes: function (yData) { - var xAxis = this.xAxis, - yAxis = this.yAxis, - xData = this.processedXData, - yDataLength, - activeYData = [], - activeCounter = 0, - // #2117, need to compensate for log X axis - xExtremes = xAxis.getExtremes(), - xMin = xExtremes.min, - xMax = xExtremes.max, - validValue, - withinRange, - // Handle X outside the viewed area. This does not work with non- - // sorted data like scatter (#7639). - shoulder = this.requireSorting ? 1 : 0, - x, - y, - i, - j; - - yData = yData || this.stackedYData || this.processedYData || []; - yDataLength = yData.length; - - for (i = 0; i < yDataLength; i++) { - - x = xData[i]; - y = yData[i]; - - // For points within the visible range, including the first point - // outside the visible range (#7061), consider y extremes. - validValue = ( - (isNumber(y, true) || isArray(y)) && - (!yAxis.positiveValuesOnly || (y.length || y > 0)) - ); - withinRange = ( - this.getExtremesFromAll || - this.options.getExtremesFromAll || - this.cropped || - ( - (xData[i + shoulder] || x) >= xMin && - (xData[i - shoulder] || x) <= xMax - ) - ); - - if (validValue && withinRange) { - - j = y.length; - if (j) { // array, like ohlc or range data - while (j--) { - if (typeof y[j] === 'number') { // #7380 - activeYData[activeCounter++] = y[j]; - } - } - } else { - activeYData[activeCounter++] = y; - } - } - } - - this.dataMin = arrayMin(activeYData); - this.dataMax = arrayMax(activeYData); - }, - - /** - * Translate data points from raw data values to chart specific positioning - * data needed later in the `drawPoints` and `drawGraph` functions. This - * function can be overridden in plugins and custom series type - * implementations. - */ - translate: function () { - if (!this.processedXData) { // hidden series - this.processData(); - } - this.generatePoints(); - var series = this, - options = series.options, - stacking = options.stacking, - xAxis = series.xAxis, - categories = xAxis.categories, - yAxis = series.yAxis, - points = series.points, - dataLength = points.length, - hasModifyValue = !!series.modifyValue, - i, - pointPlacement = options.pointPlacement, - dynamicallyPlaced = - pointPlacement === 'between' || - isNumber(pointPlacement), - threshold = options.threshold, - stackThreshold = options.startFromThreshold ? threshold : 0, - plotX, - plotY, - lastPlotX, - stackIndicator, - closestPointRangePx = Number.MAX_VALUE; - - /* - * Plotted coordinates need to be within a limited range. Drawing too - * far outside the viewport causes various rendering issues (#3201, - * #3923, #7555). - */ - function limitedRange(val) { - return Math.min(Math.max(-1e5, val), 1e5); - } - - // Point placement is relative to each series pointRange (#5889) - if (pointPlacement === 'between') { - pointPlacement = 0.5; - } - if (isNumber(pointPlacement)) { - pointPlacement *= pick(options.pointRange || xAxis.pointRange); - } - - // Translate each point - for (i = 0; i < dataLength; i++) { - var point = points[i], - xValue = point.x, - yValue = point.y, - yBottom = point.low, - stack = stacking && yAxis.stacks[( - series.negStacks && - yValue < (stackThreshold ? 0 : threshold) ? '-' : '' - ) + series.stackKey], - pointStack, - stackValues; - - // Discard disallowed y values for log axes (#3434) - if (yAxis.positiveValuesOnly && yValue !== null && yValue <= 0) { - point.isNull = true; - } - - // Get the plotX translation - point.plotX = plotX = correctFloat( // #5236 - limitedRange(xAxis.translate( // #3923 - xValue, - 0, - 0, - 0, - 1, - pointPlacement, - this.type === 'flags' - )) // #3923 - ); - - // Calculate the bottom y value for stacked series - if ( - stacking && - series.visible && - !point.isNull && - stack && - stack[xValue] - ) { - stackIndicator = series.getStackIndicator( - stackIndicator, - xValue, - series.index - ); - pointStack = stack[xValue]; - stackValues = pointStack.points[stackIndicator.key]; - yBottom = stackValues[0]; - yValue = stackValues[1]; - - if ( - yBottom === stackThreshold && - stackIndicator.key === stack[xValue].base - ) { - yBottom = pick(threshold, yAxis.min); - } - if (yAxis.positiveValuesOnly && yBottom <= 0) { // #1200, #1232 - yBottom = null; - } - - point.total = point.stackTotal = pointStack.total; - point.percentage = - pointStack.total && - (point.y / pointStack.total * 100); - point.stackY = yValue; - - // Place the stack label - pointStack.setOffset( - series.pointXOffset || 0, - series.barW || 0 - ); - - } - - // Set translated yBottom or remove it - point.yBottom = defined(yBottom) ? - limitedRange(yAxis.translate(yBottom, 0, 1, 0, 1)) : - null; - - // general hook, used for Highstock compare mode - if (hasModifyValue) { - yValue = series.modifyValue(yValue, point); - } - - // Set the the plotY value, reset it for redraws - point.plotY = plotY = - (typeof yValue === 'number' && yValue !== Infinity) ? - limitedRange(yAxis.translate(yValue, 0, 1, 0, 1)) : // #3201 - undefined; - - point.isInside = - plotY !== undefined && - plotY >= 0 && - plotY <= yAxis.len && // #3519 - plotX >= 0 && - plotX <= xAxis.len; - - - // Set client related positions for mouse tracking - point.clientX = dynamicallyPlaced ? - correctFloat( - xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement) - ) : - plotX; // #1514, #5383, #5518 - - point.negative = point.y < (threshold || 0); - - // some API data - point.category = categories && categories[point.x] !== undefined ? - categories[point.x] : point.x; - - // Determine auto enabling of markers (#3635, #5099) - if (!point.isNull) { - if (lastPlotX !== undefined) { - closestPointRangePx = Math.min( - closestPointRangePx, - Math.abs(plotX - lastPlotX) - ); - } - lastPlotX = plotX; - } - - // Find point zone - point.zone = this.zones.length && point.getZone(); - } - series.closestPointRangePx = closestPointRangePx; - - fireEvent(this, 'afterTranslate'); - }, - - /** - * Return the series points with null points filtered out. - * - * @param {Array.} [points] - * The points to inspect, defaults to {@link Series.points}. - * @param {Boolean} [insideOnly=false] - * Whether to inspect only the points that are inside the visible - * view. - * - * @return {Array.} - * The valid points. - */ - getValidPoints: function (points, insideOnly) { - var chart = this.chart; - // #3916, #5029, #5085 - return grep(points || this.points || [], function isValidPoint(point) { - if (insideOnly && !chart.isInsidePlot( - point.plotX, - point.plotY, - chart.inverted - )) { - return false; - } - return !point.isNull; - }); - }, - - /** - * Set the clipping for the series. For animated series it is called twice, - * first to initiate animating the clip then the second time without the - * animation to set the final clip. - * - * @private - */ - setClip: function (animation) { - var chart = this.chart, - options = this.options, - renderer = chart.renderer, - inverted = chart.inverted, - seriesClipBox = this.clipBox, - clipBox = seriesClipBox || chart.clipBox, - sharedClipKey = - this.sharedClipKey || - [ - '_sharedClip', - animation && animation.duration, - animation && animation.easing, - clipBox.height, - options.xAxis, - options.yAxis - ].join(','), // #4526 - clipRect = chart[sharedClipKey], - markerClipRect = chart[sharedClipKey + 'm']; - - // If a clipping rectangle with the same properties is currently present - // in the chart, use that. - if (!clipRect) { - - // When animation is set, prepare the initial positions - if (animation) { - clipBox.width = 0; - if (inverted) { - clipBox.x = chart.plotSizeX; - } - - chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect( - // include the width of the first marker - inverted ? chart.plotSizeX + 99 : -99, - inverted ? -chart.plotLeft : -chart.plotTop, - 99, - inverted ? chart.chartWidth : chart.chartHeight - ); - } - chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); - // Create hashmap for series indexes - clipRect.count = { length: 0 }; - - } - if (animation) { - if (!clipRect.count[this.index]) { - clipRect.count[this.index] = true; - clipRect.count.length += 1; - } - } - - if (options.clip !== false) { - this.group.clip( - animation || seriesClipBox ? clipRect : chart.clipRect - ); - this.markerGroup.clip(markerClipRect); - this.sharedClipKey = sharedClipKey; - } - - // Remove the shared clipping rectangle when all series are shown - if (!animation) { - if (clipRect.count[this.index]) { - delete clipRect.count[this.index]; - clipRect.count.length -= 1; - } - - if ( - clipRect.count.length === 0 && - sharedClipKey && - chart[sharedClipKey] - ) { - if (!seriesClipBox) { - chart[sharedClipKey] = chart[sharedClipKey].destroy(); - } - if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'] = - chart[sharedClipKey + 'm'].destroy(); - } - } - } - }, - - /** - * Animate in the series. Called internally twice. First with the `init` - * parameter set to true, which sets up the initial state of the animation. - * Then when ready, it is called with the `init` parameter undefined, in - * order to perform the actual animation. After the second run, the function - * is removed. - * - * @param {Boolean} init - * Initialize the animation. - */ - animate: function (init) { - var series = this, - chart = series.chart, - clipRect, - animation = animObject(series.options.animation), - sharedClipKey; - - // Initialize the animation. Set up the clipping rectangle. - if (init) { - - series.setClip(animation); - - // Run the animation - } else { - sharedClipKey = this.sharedClipKey; - clipRect = chart[sharedClipKey]; - if (clipRect) { - clipRect.animate({ - width: chart.plotSizeX, - x: 0 - }, animation); - } - if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'].animate({ - width: chart.plotSizeX + 99, - x: 0 - }, animation); - } - - // Delete this function to allow it only once - series.animate = null; - - } - }, - - /** - * This runs after animation to land on the final plot clipping. - * - * @private - */ - afterAnimate: function () { - this.setClip(); - fireEvent(this, 'afterAnimate'); - this.finishedAnimating = true; - }, - - /** - * Draw the markers for line-like series types, and columns or other - * graphical representation for {@link Point} objects for other series - * types. The resulting element is typically stored as {@link - * Point.graphic}, and is created on the first call and updated and moved on - * subsequent calls. - */ - drawPoints: function () { - var series = this, - points = series.points, - chart = series.chart, - i, - point, - symbol, - graphic, - options = series.options, - seriesMarkerOptions = options.marker, - pointMarkerOptions, - hasPointMarker, - enabled, - isInside, - markerGroup = series[series.specialGroup] || series.markerGroup, - xAxis = series.xAxis, - markerAttribs, - globallyEnabled = pick( - seriesMarkerOptions.enabled, - xAxis.isRadial ? true : null, - // Use larger or equal as radius is null in bubbles (#6321) - series.closestPointRangePx >= ( - seriesMarkerOptions.enabledThreshold * - seriesMarkerOptions.radius - ) - ); - - if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { - - for (i = 0; i < points.length; i++) { - point = points[i]; - graphic = point.graphic; - pointMarkerOptions = point.marker || {}; - hasPointMarker = !!point.marker; - enabled = ( - globallyEnabled && - pointMarkerOptions.enabled === undefined - ) || pointMarkerOptions.enabled; - isInside = point.isInside; - - // only draw the point if y is defined - if (enabled && !point.isNull) { - - // Shortcuts - symbol = pick(pointMarkerOptions.symbol, series.symbol); - - markerAttribs = series.markerAttribs( - point, - point.selected && 'select' - ); - - if (graphic) { // update - // Since the marker group isn't clipped, each individual - // marker must be toggled - graphic[isInside ? 'show' : 'hide'](true) - .animate(markerAttribs); - } else if ( - isInside && - (markerAttribs.width > 0 || point.hasImage) - ) { - - /** - * The graphic representation of the point. Typically - * this is a simple shape, like a `rect` for column - * charts or `path` for line markers, but for some - * complex series types like boxplot or 3D charts, the - * graphic may be a `g` element containing other shapes. - * The graphic is generated the first time {@link - * Series#drawPoints} runs, and updated and moved on - * subsequent runs. - * - * @memberof Point - * @name graphic - * @type {SVGElement} - */ - point.graphic = graphic = chart.renderer.symbol( - symbol, - markerAttribs.x, - markerAttribs.y, - markerAttribs.width, - markerAttribs.height, - hasPointMarker ? - pointMarkerOptions : - seriesMarkerOptions - ) - .add(markerGroup); - } - - /*= if (build.classic) { =*/ - // Presentational attributes - if (graphic) { - graphic.attr( - series.pointAttribs( - point, - point.selected && 'select' - ) - ); - } - /*= } =*/ - - if (graphic) { - graphic.addClass(point.getClassName(), true); - } - - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 - } - } - } - - }, - - /** - * Get non-presentational attributes for a point. Used internally for both - * styled mode and classic. Can be overridden for different series types. - * - * @see Series#pointAttribs - * - * @param {Point} point - * The Point to inspect. - * @param {String} [state] - * The state, can be either `hover`, `select` or undefined. - * - * @return {SVGAttributes} - * A hash containing those attributes that are not settable from - * CSS. - */ - markerAttribs: function (point, state) { - var seriesMarkerOptions = this.options.marker, - seriesStateOptions, - pointMarkerOptions = point.marker || {}, - symbol = pointMarkerOptions.symbol || seriesMarkerOptions.symbol, - pointStateOptions, - radius = pick( - pointMarkerOptions.radius, - seriesMarkerOptions.radius - ), - attribs; - - // Handle hover and select states - if (state) { - seriesStateOptions = seriesMarkerOptions.states[state]; - pointStateOptions = pointMarkerOptions.states && - pointMarkerOptions.states[state]; - - radius = pick( - pointStateOptions && pointStateOptions.radius, - seriesStateOptions && seriesStateOptions.radius, - radius + ( - seriesStateOptions && seriesStateOptions.radiusPlus || - 0 - ) - ); - } - - point.hasImage = symbol && symbol.indexOf('url') === 0; - - if (point.hasImage) { - radius = 0; // and subsequently width and height is not set - } - - attribs = { - x: Math.floor(point.plotX) - radius, // Math.floor for #1843 - y: point.plotY - radius - }; - - if (radius) { - attribs.width = attribs.height = 2 * radius; - } - - return attribs; - - }, - - /*= if (build.classic) { =*/ - /** - * Internal function to get presentational attributes for each point. Unlike - * {@link Series#markerAttribs}, this function should return those - * attributes that can also be set in CSS. In styled mode, `pointAttribs` - * won't be called. - * - * @param {Point} point - * The point instance to inspect. - * @param {String} [state] - * The point state, can be either `hover`, `select` or undefined for - * normal state. - * - * @return {SVGAttributes} - * The presentational attributes to be set on the point. - */ - pointAttribs: function (point, state) { - var seriesMarkerOptions = this.options.marker, - seriesStateOptions, - pointOptions = point && point.options, - pointMarkerOptions = (pointOptions && pointOptions.marker) || {}, - pointStateOptions, - color = this.color, - pointColorOption = pointOptions && pointOptions.color, - pointColor = point && point.color, - strokeWidth = pick( - pointMarkerOptions.lineWidth, - seriesMarkerOptions.lineWidth - ), - zoneColor = point && point.zone && point.zone.color, - fill, - stroke; - - color = ( - pointColorOption || - zoneColor || - pointColor || - color - ); - fill = ( - pointMarkerOptions.fillColor || - seriesMarkerOptions.fillColor || - color - ); - stroke = ( - pointMarkerOptions.lineColor || - seriesMarkerOptions.lineColor || - color - ); - - // Handle hover and select states - if (state) { - seriesStateOptions = seriesMarkerOptions.states[state]; - pointStateOptions = ( - pointMarkerOptions.states && pointMarkerOptions.states[state] - ) || {}; - strokeWidth = pick( - pointStateOptions.lineWidth, - seriesStateOptions.lineWidth, - strokeWidth + pick( - pointStateOptions.lineWidthPlus, - seriesStateOptions.lineWidthPlus, - 0 - ) - ); - fill = ( - pointStateOptions.fillColor || - seriesStateOptions.fillColor || - fill - ); - stroke = ( - pointStateOptions.lineColor || - seriesStateOptions.lineColor || - stroke - ); - } - - return { - 'stroke': stroke, - 'stroke-width': strokeWidth, - 'fill': fill - }; - }, - /*= } =*/ - /** - * Clear DOM objects and free up memory. - * - * @private - */ - destroy: function () { - var series = this, - chart = series.chart, - issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent), - destroy, - i, - data = series.data || [], - point, - axis; - - // add event hook - fireEvent(series, 'destroy'); - - // remove all events - removeEvent(series); - - // erase from axes - each(series.axisTypes || [], function (AXIS) { - axis = series[AXIS]; - if (axis && axis.series) { - erase(axis.series, series); - axis.isDirty = axis.forceRedraw = true; - } - }); - - // remove legend items - if (series.legendItem) { - series.chart.legend.destroyItem(series); - } - - // destroy all points with their elements - i = data.length; - while (i--) { - point = data[i]; - if (point && point.destroy) { - point.destroy(); - } - } - series.points = null; - - // Clear the animation timeout if we are destroying the series during - // initial animation - H.clearTimeout(series.animationTimeout); - - // Destroy all SVGElements associated to the series - objectEach(series, function (val, prop) { - // Survive provides a hook for not destroying - if (val instanceof SVGElement && !val.survive) { - - // issue 134 workaround - destroy = issue134 && prop === 'group' ? - 'hide' : - 'destroy'; - - val[destroy](); - } - }); - - // remove from hoverSeries - if (chart.hoverSeries === series) { - chart.hoverSeries = null; - } - erase(chart.series, series); - chart.orderSeries(); - - // clear all members - objectEach(series, function (val, prop) { - delete series[prop]; - }); - }, - - /** - * Get the graph path. - * - * @private - */ - getGraphPath: function (points, nullsAsZeroes, connectCliffs) { - var series = this, - options = series.options, - step = options.step, - reversed, - graphPath = [], - xMap = [], - gap; - - points = points || series.points; - - // Bottom of a stack is reversed - reversed = points.reversed; - if (reversed) { - points.reverse(); - } - // Reverse the steps (#5004) - step = { right: 1, center: 2 }[step] || (step && 3); - if (step && reversed) { - step = 4 - step; - } - - // Remove invalid points, especially in spline (#5015) - if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { - points = this.getValidPoints(points); - } - - // Build the line - each(points, function (point, i) { - - var plotX = point.plotX, - plotY = point.plotY, - lastPoint = points[i - 1], - pathToPoint; // the path to this point from the previous - - if ( - (point.leftCliff || (lastPoint && lastPoint.rightCliff)) && - !connectCliffs - ) { - gap = true; // ... and continue - } - - // Line series, nullsAsZeroes is not handled - if (point.isNull && !defined(nullsAsZeroes) && i > 0) { - gap = !options.connectNulls; - - // Area series, nullsAsZeroes is set - } else if (point.isNull && !nullsAsZeroes) { - gap = true; - - } else { - - if (i === 0 || gap) { - pathToPoint = ['M', point.plotX, point.plotY]; - - // Generate the spline as defined in the SplineSeries object - } else if (series.getPointSpline) { - - pathToPoint = series.getPointSpline(points, point, i); - - } else if (step) { - - if (step === 1) { // right - pathToPoint = [ - 'L', - lastPoint.plotX, - plotY - ]; - - } else if (step === 2) { // center - pathToPoint = [ - 'L', - (lastPoint.plotX + plotX) / 2, - lastPoint.plotY, - 'L', - (lastPoint.plotX + plotX) / 2, - plotY - ]; - - } else { - pathToPoint = [ - 'L', - plotX, - lastPoint.plotY - ]; - } - pathToPoint.push('L', plotX, plotY); - - } else { - // normal line to next point - pathToPoint = [ - 'L', - plotX, - plotY - ]; - } - - // Prepare for animation. When step is enabled, there are two - // path nodes for each x value. - xMap.push(point.x); - if (step) { - xMap.push(point.x); - } - - graphPath.push.apply(graphPath, pathToPoint); - gap = false; - } - }); - - graphPath.xMap = xMap; - series.graphPath = graphPath; - - return graphPath; - - }, - - /** - * Draw the graph. Called internally when rendering line-like series types. - * The first time it generates the `series.graph` item and optionally other - * series-wide items like `series.area` for area charts. On subsequent calls - * these items are updated with new positions and attributes. - */ - drawGraph: function () { - var series = this, - options = this.options, - graphPath = (this.gappedPath || this.getGraphPath).call(this), - props = [[ - 'graph', - 'highcharts-graph', - /*= if (build.classic) { =*/ - options.lineColor || this.color, - options.dashStyle - /*= } =*/ - ]]; - - props = series.getZonesGraphs(props); - - // Draw the graph - each(props, function (prop, i) { - var graphKey = prop[0], - graph = series[graphKey], - attribs; - - if (graph) { - graph.endX = series.preventGraphAnimation ? - null : - graphPath.xMap; - graph.animate({ d: graphPath }); - - } else if (graphPath.length) { // #1487 - - series[graphKey] = series.chart.renderer.path(graphPath) - .addClass(prop[1]) - .attr({ zIndex: 1 }) // #1069 - .add(series.group); - - /*= if (build.classic) { =*/ - attribs = { - 'stroke': prop[2], - 'stroke-width': options.lineWidth, - // Polygon series use filled graph - 'fill': (series.fillGraph && series.color) || 'none' - }; - - if (prop[3]) { - attribs.dashstyle = prop[3]; - } else if (options.linecap !== 'square') { - attribs['stroke-linecap'] = attribs['stroke-linejoin'] = - 'round'; - } - - graph = series[graphKey] - .attr(attribs) - // Add shadow to normal series (0) or to first zone (1) - // #3932 - .shadow((i < 2) && options.shadow); - /*= } =*/ - } - - // Helpers for animation - if (graph) { - graph.startX = graphPath.xMap; - graph.isArea = graphPath.isArea; // For arearange animation - } - }); - }, - - /** - * Get zones properties for building graphs. - * Extendable by series with multiple lines within one series. - * - * @private - */ - getZonesGraphs: function (props) { - // Add the zone properties if any - each(this.zones, function (zone, i) { - props.push([ - 'zone-graph-' + i, - 'highcharts-graph highcharts-zone-graph-' + i + ' ' + - (zone.className || ''), - /*= if (build.classic) { =*/ - zone.color || this.color, - zone.dashStyle || this.options.dashStyle - /*= } =*/ - ]); - }, this); - - return props; - }, - - /** - * Clip the graphs into zones for colors and styling. - * - * @private - */ - applyZones: function () { - var series = this, - chart = this.chart, - renderer = chart.renderer, - zones = this.zones, - translatedFrom, - translatedTo, - clips = this.clips || [], - clipAttr, - graph = this.graph, - area = this.area, - chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight), - axis = this[(this.zoneAxis || 'y') + 'Axis'], - extremes, - reversed, - inverted = chart.inverted, - horiz, - pxRange, - pxPosMin, - pxPosMax, - ignoreZones = false; - - if (zones.length && (graph || area) && axis && axis.min !== undefined) { - reversed = axis.reversed; - horiz = axis.horiz; - // The use of the Color Threshold assumes there are no gaps - // so it is safe to hide the original graph and area - // unless it is not waterfall series, then use showLine property to - // set lines between columns to be visible (#7862) - if (graph && !this.showLine) { - graph.hide(); - } - if (area) { - area.hide(); - } - - // Create the clips - extremes = axis.getExtremes(); - each(zones, function (threshold, i) { - - translatedFrom = reversed ? - (horiz ? chart.plotWidth : 0) : - (horiz ? 0 : axis.toPixels(extremes.min)); - translatedFrom = Math.min( - Math.max( - pick(translatedTo, translatedFrom), 0 - ), - chartSizeMax - ); - translatedTo = Math.min( - Math.max( - Math.round( - axis.toPixels( - pick(threshold.value, extremes.max), - true - ) - ), - 0 - ), - chartSizeMax - ); - - if (ignoreZones) { - translatedFrom = translatedTo = axis.toPixels(extremes.max); - } - - pxRange = Math.abs(translatedFrom - translatedTo); - pxPosMin = Math.min(translatedFrom, translatedTo); - pxPosMax = Math.max(translatedFrom, translatedTo); - if (axis.isXAxis) { - clipAttr = { - x: inverted ? pxPosMax : pxPosMin, - y: 0, - width: pxRange, - height: chartSizeMax - }; - if (!horiz) { - clipAttr.x = chart.plotHeight - clipAttr.x; - } - } else { - clipAttr = { - x: 0, - y: inverted ? pxPosMax : pxPosMin, - width: chartSizeMax, - height: pxRange - }; - if (horiz) { - clipAttr.y = chart.plotWidth - clipAttr.y; - } - } - - /*= if (build.classic) { =*/ - // VML SUPPPORT - if (inverted && renderer.isVML) { - if (axis.isXAxis) { - clipAttr = { - x: 0, - y: reversed ? pxPosMin : pxPosMax, - height: clipAttr.width, - width: chart.chartWidth - }; - } else { - clipAttr = { - x: clipAttr.y - chart.plotLeft - chart.spacingBox.x, - y: 0, - width: clipAttr.height, - height: chart.chartHeight - }; - } - } - // END OF VML SUPPORT - /*= } =*/ - - if (clips[i]) { - clips[i].animate(clipAttr); - } else { - clips[i] = renderer.clipRect(clipAttr); - - if (graph) { - series['zone-graph-' + i].clip(clips[i]); - } - - if (area) { - series['zone-area-' + i].clip(clips[i]); - } - } - // if this zone extends out of the axis, ignore the others - ignoreZones = threshold.value > extremes.max; - - // Clear translatedTo for indicators - if (series.resetZones && translatedTo === 0) { - translatedTo = undefined; - } - }); - this.clips = clips; - } - }, - - /** - * Initialize and perform group inversion on series.group and - * series.markerGroup. - * - * @private - */ - invertGroups: function (inverted) { - var series = this, - chart = series.chart, - remover; - - function setInvert() { - each(['group', 'markerGroup'], function (groupName) { - if (series[groupName]) { - - // VML/HTML needs explicit attributes for flipping - if (chart.renderer.isVML) { - series[groupName].attr({ - width: series.yAxis.len, - height: series.xAxis.len - }); - } - - series[groupName].width = series.yAxis.len; - series[groupName].height = series.xAxis.len; - series[groupName].invert(inverted); - } - }); - } - - // Pie, go away (#1736) - if (!series.xAxis) { - return; - } - - // A fixed size is needed for inversion to work - remover = addEvent(chart, 'resize', setInvert); - addEvent(series, 'destroy', remover); - - // Do it now - setInvert(inverted); // do it now - - // On subsequent render and redraw, just do setInvert without setting up - // events again - series.invertGroups = setInvert; - }, - - /** - * General abstraction for creating plot groups like series.group, - * series.dataLabelsGroup and series.markerGroup. On subsequent calls, the - * group will only be adjusted to the updated plot size. - * - * @private - */ - plotGroup: function (prop, name, visibility, zIndex, parent) { - var group = this[prop], - isNew = !group; - - // Generate it on first call - if (isNew) { - this[prop] = group = this.chart.renderer.g() - .attr({ - zIndex: zIndex || 0.1 // IE8 and pointer logic use this - }) - .add(parent); - - } - - // Add the class names, and replace existing ones as response to - // Series.update (#6660) - group.addClass( - ( - 'highcharts-' + name + - ' highcharts-series-' + this.index + - ' highcharts-' + this.type + '-series ' + - ( - defined(this.colorIndex) ? - 'highcharts-color-' + this.colorIndex + ' ' : - '' - ) + - (this.options.className || '') + - ( - group.hasClass('highcharts-tracker') ? - ' highcharts-tracker' : - '' - ) - ), - true - ); - - // Place it on first and subsequent (redraw) calls - group.attr({ visibility: visibility })[isNew ? 'attr' : 'animate']( - this.getPlotBox() - ); - return group; - }, - - /** - * Get the translation and scale for the plot area of this series. - */ - getPlotBox: function () { - var chart = this.chart, - xAxis = this.xAxis, - yAxis = this.yAxis; - - // Swap axes for inverted (#2339) - if (chart.inverted) { - xAxis = yAxis; - yAxis = this.xAxis; - } - return { - translateX: xAxis ? xAxis.left : chart.plotLeft, - translateY: yAxis ? yAxis.top : chart.plotTop, - scaleX: 1, // #1623 - scaleY: 1 - }; - }, - - /** - * Render the graph and markers. Called internally when first rendering and - * later when redrawing the chart. This function can be extended in plugins, - * but normally shouldn't be called directly. - */ - render: function () { - var series = this, - chart = series.chart, - group, - options = series.options, - // Animation doesn't work in IE8 quirks when the group div is - // hidden, and looks bad in other oldIE - animDuration = ( - !!series.animate && - chart.renderer.isSVG && - animObject(options.animation).duration - ), - visibility = series.visible ? 'inherit' : 'hidden', // #2597 - zIndex = options.zIndex, - hasRendered = series.hasRendered, - chartSeriesGroup = chart.seriesGroup, - inverted = chart.inverted; - - // the group - group = series.plotGroup( - 'group', - 'series', - visibility, - zIndex, - chartSeriesGroup - ); - - series.markerGroup = series.plotGroup( - 'markerGroup', - 'markers', - visibility, - zIndex, - chartSeriesGroup - ); - - // initiate the animation - if (animDuration) { - series.animate(true); - } - - // SVGRenderer needs to know this before drawing elements (#1089, #1795) - group.inverted = series.isCartesian ? inverted : false; - - // draw the graph if any - if (series.drawGraph) { - series.drawGraph(); - series.applyZones(); - } - -/* each(series.points, function (point) { - if (point.redraw) { - point.redraw(); - } - });*/ - - // draw the data labels (inn pies they go before the points) - if (series.drawDataLabels) { - series.drawDataLabels(); - } - - // draw the points - if (series.visible) { - series.drawPoints(); - } - - - // draw the mouse tracking area - if ( - series.drawTracker && - series.options.enableMouseTracking !== false - ) { - series.drawTracker(); - } - - // Handle inverted series and tracker groups - series.invertGroups(inverted); - - // Initial clipping, must be defined after inverting groups for VML. - // Applies to columns etc. (#3839). - if (options.clip !== false && !series.sharedClipKey && !hasRendered) { - group.clip(chart.clipRect); - } - - // Run the animation - if (animDuration) { - series.animate(); - } - - // Call the afterAnimate function on animation complete (but don't - // overwrite the animation.complete option which should be available to - // the user). - if (!hasRendered) { - series.animationTimeout = syncTimeout(function () { - series.afterAnimate(); - }, animDuration); - } - - series.isDirty = false; // means data is in accordance with what you see - // (See #322) series.isDirty = series.isDirtyData = false; // means - // data is in accordance with what you see - series.hasRendered = true; - - fireEvent(series, 'afterRender'); - }, - - /** - * Redraw the series. This function is called internally from `chart.redraw` - * and normally shouldn't be called directly. - * - * @private - */ - redraw: function () { - var series = this, - chart = series.chart, - // cache it here as it is set to false in render, but used after - wasDirty = series.isDirty || series.isDirtyData, - group = series.group, - xAxis = series.xAxis, - yAxis = series.yAxis; - - // reposition on resize - if (group) { - if (chart.inverted) { - group.attr({ - width: chart.plotWidth, - height: chart.plotHeight - }); - } - - group.animate({ - translateX: pick(xAxis && xAxis.left, chart.plotLeft), - translateY: pick(yAxis && yAxis.top, chart.plotTop) - }); - } - - series.translate(); - series.render(); - if (wasDirty) { // #3868, #3945 - delete this.kdTree; - } - }, - - kdAxisArray: ['clientX', 'plotY'], - - searchPoint: function (e, compareX) { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis, - inverted = series.chart.inverted; - - return this.searchKDTree({ - clientX: inverted ? - xAxis.len - e.chartY + xAxis.pos : - e.chartX - xAxis.pos, - plotY: inverted ? - yAxis.len - e.chartX + yAxis.pos : - e.chartY - yAxis.pos - }, compareX); - }, - - /** - * Build the k-d-tree that is used by mouse and touch interaction to get the - * closest point. Line-like series typically have a one-dimensional tree - * where points are searched along the X axis, while scatter-like series - * typically search in two dimensions, X and Y. - * - * @private - */ - buildKDTree: function () { - - // Prevent multiple k-d-trees from being built simultaneously (#6235) - this.buildingKdTree = true; - - var series = this, - dimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? - 2 : 1; - - // Internal function - function _kdtree(points, depth, dimensions) { - var axis, - median, - length = points && points.length; - - if (length) { - - // alternate between the axis - axis = series.kdAxisArray[depth % dimensions]; - - // sort point array - points.sort(function (a, b) { - return a[axis] - b[axis]; - }); - - median = Math.floor(length / 2); - - // build and return nod - return { - point: points[median], - left: _kdtree( - points.slice(0, median), depth + 1, dimensions - ), - right: _kdtree( - points.slice(median + 1), depth + 1, dimensions - ) - }; - - } - } - - // Start the recursive build process with a clone of the points array - // and null points filtered out (#3873) - function startRecursive() { - series.kdTree = _kdtree( - series.getValidPoints( - null, - // For line-type series restrict to plot area, but - // column-type series not (#3916, #4511) - !series.directTouch - ), - dimensions, - dimensions - ); - series.buildingKdTree = false; - } - delete series.kdTree; - - // For testing tooltips, don't build async - syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); - }, - - searchKDTree: function (point, compareX) { - var series = this, - kdX = this.kdAxisArray[0], - kdY = this.kdAxisArray[1], - kdComparer = compareX ? 'distX' : 'dist', - kdDimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? - 2 : 1; - - // Set the one and two dimensional distance on the point object - function setDistance(p1, p2) { - var x = (defined(p1[kdX]) && defined(p2[kdX])) ? - Math.pow(p1[kdX] - p2[kdX], 2) : - null, - y = (defined(p1[kdY]) && defined(p2[kdY])) ? - Math.pow(p1[kdY] - p2[kdY], 2) : - null, - r = (x || 0) + (y || 0); - - p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; - p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; - } - function _search(search, tree, depth, dimensions) { - var point = tree.point, - axis = series.kdAxisArray[depth % dimensions], - tdist, - sideA, - sideB, - ret = point, - nPoint1, - nPoint2; - - setDistance(search, point); - - // Pick side based on distance to splitting point - tdist = search[axis] - point[axis]; - sideA = tdist < 0 ? 'left' : 'right'; - sideB = tdist < 0 ? 'right' : 'left'; - - // End of tree - if (tree[sideA]) { - nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); - - ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); - } - if (tree[sideB]) { - // compare distance to current best to splitting point to decide - // wether to check side B or not - if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { - nPoint2 = _search( - search, - tree[sideB], - depth + 1, - dimensions - ); - ret = nPoint2[kdComparer] < ret[kdComparer] ? - nPoint2 : - ret; - } - } - - return ret; - } - - if (!this.kdTree && !this.buildingKdTree) { - this.buildKDTree(); - } - - if (this.kdTree) { - return _search(point, this.kdTree, kdDimensions, kdDimensions); - } - } + isCartesian: true, + pointClass: Point, + sorted: true, // requires the data to be sorted + requireSorting: true, + directTouch: false, + axisTypes: ['xAxis', 'yAxis'], + colorCounter: 0, + // each point's x and y values are stored in this.xData and this.yData + parallelArrays: ['x', 'y'], + coll: 'series', + init: function (chart, options) { + var series = this, + events, + chartSeries = chart.series, + lastSeries; + + /** + * Read only. The chart that the series belongs to. + * + * @name chart + * @memberOf Series + * @type {Chart} + */ + series.chart = chart; + + /** + * Read only. The series' type, like "line", "area", "column" etc. The + * type in the series options anc can be altered using {@link + * Series#update}. + * + * @name type + * @memberOf Series + * @type String + */ + + /** + * Read only. The series' current options. To update, use {@link + * Series#update}. + * + * @name options + * @memberOf Series + * @type SeriesOptions + */ + series.options = options = series.setOptions(options); + series.linkedSeries = []; + + // bind the axes + series.bindAxes(); + + // set some variables + extend(series, { + /** + * The series name as given in the options. Defaults to + * "Series {n}". + * + * @name name + * @memberOf Series + * @type {String} + */ + name: options.name, + state: '', + /** + * Read only. The series' visibility state as set by {@link + * Series#show}, {@link Series#hide}, or in the initial + * configuration. + * + * @name visible + * @memberOf Series + * @type {Boolean} + */ + visible: options.visible !== false, // true by default + /** + * Read only. The series' selected state as set by {@link + * Highcharts.Series#select}. + * + * @name selected + * @memberOf Series + * @type {Boolean} + */ + selected: options.selected === true // false by default + }); + + // register event listeners + events = options.events; + + objectEach(events, function (event, eventType) { + addEvent(series, eventType, event); + }); + if ( + (events && events.click) || + ( + options.point && + options.point.events && + options.point.events.click + ) || + options.allowPointSelect + ) { + chart.runTrackerClick = true; + } + + series.getColor(); + series.getSymbol(); + + // Set the data + each(series.parallelArrays, function (key) { + series[key + 'Data'] = []; + }); + series.setData(options.data, false); + + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } + + // Get the index and register the series in the chart. The index is one + // more than the current latest series index (#5960). + if (chartSeries.length) { + lastSeries = chartSeries[chartSeries.length - 1]; + } + series._i = pick(lastSeries && lastSeries._i, -1) + 1; + + // Insert the series and re-order all series above the insertion point. + chart.orderSeries(this.insert(chartSeries)); + + fireEvent(this, 'afterInit'); + }, + + /** + * Insert the series in a collection with other series, either the chart + * series or yAxis series, in the correct order according to the index + * option. Used internally when adding series. + * + * @private + * @param {Array.} collection + * A collection of series, like `chart.series` or `xAxis.series`. + * @returns {Number} The index of the series in the collection. + */ + insert: function (collection) { + var indexOption = this.options.index, + i; + + // Insert by index option + if (isNumber(indexOption)) { + i = collection.length; + while (i--) { + // Loop down until the interted element has higher index + if (indexOption >= + pick(collection[i].options.index, collection[i]._i)) { + collection.splice(i + 1, 0, this); + break; + } + } + if (i === -1) { + collection.unshift(this); + } + i = i + 1; + + // Or just push it to the end + } else { + collection.push(this); + } + return pick(i, collection.length - 1); + }, + + /** + * Set the xAxis and yAxis properties of cartesian series, and register the + * series in the `axis.series` array. + * + * @private + */ + bindAxes: function () { + var series = this, + seriesOptions = series.options, + chart = series.chart, + axisOptions; + + // repeat for xAxis and yAxis + each(series.axisTypes || [], function (AXIS) { + + // loop through the chart's axis objects + each(chart[AXIS], function (axis) { + axisOptions = axis.options; + + // apply if the series xAxis or yAxis option mathches the number + // of the axis, or if undefined, use the first axis + if ( + seriesOptions[AXIS] === axisOptions.index || + ( + seriesOptions[AXIS] !== undefined && + seriesOptions[AXIS] === axisOptions.id + ) || + ( + seriesOptions[AXIS] === undefined && + axisOptions.index === 0 + ) + ) { + + // register this series in the axis.series lookup + series.insert(axis.series); + + // set this series.xAxis or series.yAxis reference + /** + * Read only. The unique xAxis object associated with the + * series. + * + * @name xAxis + * @memberOf Series + * @type Axis + */ + /** + * Read only. The unique yAxis object associated with the + * series. + * + * @name yAxis + * @memberOf Series + * @type Axis + */ + series[AXIS] = axis; + + // mark dirty for redraw + axis.isDirty = true; + } + }); + + // The series needs an X and an Y axis + if (!series[AXIS] && series.optionalAxis !== AXIS) { + H.error(18, true); + } + + }); + }, + + /** + * For simple series types like line and column, the data values are held in + * arrays like xData and yData for quick lookup to find extremes and more. + * For multidimensional series like bubble and map, this can be extended + * with arrays like zData and valueData by adding to the + * `series.parallelArrays` array. + * + * @private + */ + updateParallelArrays: function (point, i) { + var series = point.series, + args = arguments, + fn = isNumber(i) ? + // Insert the value in the given position + function (key) { + var val = key === 'y' && series.toYData ? + series.toYData(point) : + point[key]; + series[key + 'Data'][i] = val; + } : + // Apply the method specified in i with the following arguments + // as arguments + function (key) { + Array.prototype[i].apply( + series[key + 'Data'], + Array.prototype.slice.call(args, 2) + ); + }; + + each(series.parallelArrays, fn); + }, + + /** + * Return an auto incremented x value based on the pointStart and + * pointInterval options. This is only used if an x value is not given for + * the point that calls autoIncrement. + * + * @private + */ + autoIncrement: function () { + + var options = this.options, + xIncrement = this.xIncrement, + date, + pointInterval, + pointIntervalUnit = options.pointIntervalUnit, + time = this.chart.time; + + xIncrement = pick(xIncrement, options.pointStart, 0); + + this.pointInterval = pointInterval = pick( + this.pointInterval, + options.pointInterval, + 1 + ); + + // Added code for pointInterval strings + if (pointIntervalUnit) { + date = new time.Date(xIncrement); + + if (pointIntervalUnit === 'day') { + time.set( + 'Date', + date, + time.get('Date', date) + pointInterval + ); + } else if (pointIntervalUnit === 'month') { + time.set( + 'Month', + date, + time.get('Month', date) + pointInterval + ); + } else if (pointIntervalUnit === 'year') { + time.set( + 'FullYear', + date, + time.get('FullYear', date) + pointInterval + ); + } + + pointInterval = date.getTime() - xIncrement; + + } + + this.xIncrement = xIncrement + pointInterval; + return xIncrement; + }, + + /** + * Set the series options by merging from the options tree. Called + * internally on initiating and updating series. This function will not + * redraw the series. For API usage, use {@link Series#update}. + * + * @param {Options.plotOptions.series} itemOptions + * The series options. + */ + setOptions: function (itemOptions) { + var chart = this.chart, + chartOptions = chart.options, + plotOptions = chartOptions.plotOptions, + userOptions = chart.userOptions || {}, + userPlotOptions = userOptions.plotOptions || {}, + typeOptions = plotOptions[this.type], + options, + zones; + + this.userOptions = itemOptions; + + // General series options take precedence over type options because + // otherwise, default type options like column.animation would be + // overwritten by the general option. But issues have been raised here + // (#3881), and the solution may be to distinguish between default + // option and userOptions like in the tooltip below. + options = merge( + typeOptions, + plotOptions.series, + itemOptions + ); + + // The tooltip options are merged between global and series specific + // options. Importance order asscendingly: + // globals: (1)tooltip, (2)plotOptions.series, (3)plotOptions[this.type] + // init userOptions with possible later updates: 4-6 like 1-3 and + // (7)this series options + this.tooltipOptions = merge( + defaultOptions.tooltip, // 1 + defaultOptions.plotOptions.series && + defaultOptions.plotOptions.series.tooltip, // 2 + defaultOptions.plotOptions[this.type].tooltip, // 3 + chartOptions.tooltip.userOptions, // 4 + plotOptions.series && plotOptions.series.tooltip, // 5 + plotOptions[this.type].tooltip, // 6 + itemOptions.tooltip // 7 + ); + + // When shared tooltip, stickyTracking is true by default, + // unless user says otherwise. + this.stickyTracking = pick( + itemOptions.stickyTracking, + userPlotOptions[this.type] && + userPlotOptions[this.type].stickyTracking, + userPlotOptions.series && userPlotOptions.series.stickyTracking, + ( + this.tooltipOptions.shared && !this.noSharedTooltip ? + true : + options.stickyTracking + ) + ); + + // Delete marker object if not allowed (#1125) + if (typeOptions.marker === null) { + delete options.marker; + } + + // Handle color zones + this.zoneAxis = options.zoneAxis; + zones = this.zones = (options.zones || []).slice(); + if ( + (options.negativeColor || options.negativeFillColor) && + !options.zones + ) { + zones.push({ + value: + options[this.zoneAxis + 'Threshold'] || + options.threshold || + 0, + className: 'highcharts-negative', + /*= if (build.classic) { =*/ + color: options.negativeColor, + fillColor: options.negativeFillColor + /*= } =*/ + }); + } + if (zones.length) { // Push one extra zone for the rest + if (defined(zones[zones.length - 1].value)) { + zones.push({ + /*= if (build.classic) { =*/ + color: this.color, + fillColor: this.fillColor + /*= } =*/ + }); + } + } + + fireEvent(this, 'afterSetOptions', { options: options }); + + return options; + }, + + /** + * Return series name in "Series {Number}" format or the one defined by a + * user. This method can be simply overridden as series name format can + * vary (e.g. technical indicators). + * + * @return {String} The series name. + */ + getName: function () { + return this.name || 'Series ' + (this.index + 1); + }, + + getCyclic: function (prop, value, defaults) { + var i, + chart = this.chart, + userOptions = this.userOptions, + indexName = prop + 'Index', + counterName = prop + 'Counter', + len = defaults ? defaults.length : pick( + chart.options.chart[prop + 'Count'], + chart[prop + 'Count'] + ), + setting; + + if (!value) { + // Pick up either the colorIndex option, or the _colorIndex after + // Series.update() + setting = pick( + userOptions[indexName], + userOptions['_' + indexName] + ); + if (defined(setting)) { // after Series.update() + i = setting; + } else { + // #6138 + if (!chart.series.length) { + chart[counterName] = 0; + } + userOptions['_' + indexName] = i = chart[counterName] % len; + chart[counterName] += 1; + } + if (defaults) { + value = defaults[i]; + } + } + // Set the colorIndex + if (i !== undefined) { + this[indexName] = i; + } + this[prop] = value; + }, + + /** + * Get the series' color based on either the options or pulled from global + * options. + * + * @return {Color} The series color. + */ + /*= if (!build.classic) { =*/ + getColor: function () { + this.getCyclic('color'); + }, + + /*= } else { =*/ + getColor: function () { + if (this.options.colorByPoint) { + // #4359, selected slice got series.color even when colorByPoint was + // set. + this.options.color = null; + } else { + this.getCyclic( + 'color', + this.options.color || defaultPlotOptions[this.type].color, + this.chart.options.colors + ); + } + }, + /*= } =*/ + /** + * Get the series' symbol based on either the options or pulled from global + * options. + */ + getSymbol: function () { + var seriesMarkerOption = this.options.marker; + + this.getCyclic( + 'symbol', + seriesMarkerOption.symbol, + this.chart.options.symbols + ); + }, + + drawLegendSymbol: LegendSymbolMixin.drawLineMarker, + + /** + * Internal function called from setData. If the point count is the same as + * is was, or if there are overlapping X values, just run Point.update which + * is cheaper, allows animation, and keeps references to points. This also + * allows adding or removing points if the X-es don't match. + * + * @private + */ + updateData: function (data) { + var options = this.options, + oldData = this.points, + pointsToAdd = [], + hasUpdatedByKey, + i, + point, + lastIndex, + requireSorting = this.requireSorting; + + // Iterate the new data + each(data, function (pointOptions) { + var x, + pointIndex; + + // Get the x of the new data point + x = ( + H.defined(pointOptions) && + this.pointClass.prototype.optionsToObject.call( + { series: this }, + pointOptions + ).x + ); + + if (isNumber(x)) { + // Search for the same X in the existing data set + pointIndex = H.inArray(x, this.xData, lastIndex); + + // Matching X not found, add point (but later) + if (pointIndex === -1) { + pointsToAdd.push(pointOptions); + + // Matching X found, update + } else if (pointOptions !== options.data[pointIndex]) { + oldData[pointIndex].update( + pointOptions, + false, + null, + false + ); + + // Mark it touched, below we will remove all points that + // are not touched. + oldData[pointIndex].touched = true; + + // Speed optimize by only searching from last known index. + // Performs ~20% bettor on large data sets. + if (requireSorting) { + lastIndex = pointIndex; + } + } + hasUpdatedByKey = true; + } + }, this); + + // Remove points that don't exist in the updated data set + if (hasUpdatedByKey) { + i = oldData.length; + while (i--) { + point = oldData[i]; + if (!point.touched) { + point.remove(false); + } + point.touched = false; + } + + // If we did not find keys (x-values), and the length is the same, + // update one-to-one + } else if (data.length === oldData.length) { + each(data, function (point, i) { + // .update doesn't exist on a linked, hidden series (#3709) + if (oldData[i].update && point !== options.data[i]) { + oldData[i].update(point, false, null, false); + } + }); + + // Did not succeed in updating data + } else { + return false; + } + + // Add new points + each(pointsToAdd, function (point) { + this.addPoint(point, false); + }, this); + + return true; + }, + + /** + * Apply a new set of data to the series and optionally redraw it. The new + * data array is passed by reference (except in case of `updatePoints`), and + * may later be mutated when updating the chart data. + * + * Note the difference in behaviour when setting the same amount of points, + * or a different amount of points, as handled by the `updatePoints` + * parameter. + * + * @param {SeriesDataOptions} data + * Takes an array of data in the same format as described under + * `series.typedata` for the given series type. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the series is altered. If doing + * more operations on the chart, it is a good idea to set redraw to + * false and call {@link Chart#redraw} after. + * @param {AnimationOptions} [animation] + * When the updated data is the same length as the existing data, + * points will be updated by default, and animation visualizes how + * the points are changed. Set false to disable animation, or a + * configuration object to set duration or easing. + * @param {Boolean} [updatePoints=true] + * When the updated data is the same length as the existing data, or + * points can be matched by X values, points will be updated instead + * of replaced. This allows updating with animation and performs + * better. In this case, the original array is not passed by + * reference. Set `false` to prevent. + * + * @sample highcharts/members/series-setdata/ + * Set new data from a button + * @sample highcharts/members/series-setdata-pie/ + * Set data in a pie + * @sample stock/members/series-setdata/ + * Set new data in Highstock + * @sample maps/members/series-setdata/ + * Set new data in Highmaps + */ + setData: function (data, redraw, animation, updatePoints) { + var series = this, + oldData = series.points, + oldDataLength = (oldData && oldData.length) || 0, + dataLength, + options = series.options, + chart = series.chart, + firstPoint = null, + xAxis = series.xAxis, + i, + turboThreshold = options.turboThreshold, + pt, + xData = this.xData, + yData = this.yData, + pointArrayMap = series.pointArrayMap, + valueCount = pointArrayMap && pointArrayMap.length, + updatedData; + + data = data || []; + dataLength = data.length; + redraw = pick(redraw, true); + + // If the point count is the same as is was, just run Point.update which + // is cheaper, allows animation, and keeps references to points. + if ( + updatePoints !== false && + dataLength && + oldDataLength && + !series.cropped && + !series.hasGroupedData && + series.visible + ) { + updatedData = this.updateData(data); + } + + if (!updatedData) { + + // Reset properties + series.xIncrement = null; + + series.colorCounter = 0; // for series with colorByPoint (#1547) + + // Update parallel arrays + each(this.parallelArrays, function (key) { + series[key + 'Data'].length = 0; + }); + + // In turbo mode, only one- or twodimensional arrays of numbers are + // allowed. The first value is tested, and we assume that all the + // rest are defined the same way. Although the 'for' loops are + // similar, they are repeated inside each if-else conditional for + // max performance. + if (turboThreshold && dataLength > turboThreshold) { + + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } + + + if (isNumber(firstPoint)) { // assume all points are numbers + for (i = 0; i < dataLength; i++) { + xData[i] = this.autoIncrement(); + yData[i] = data[i]; + } + + // Assume all points are arrays when first point is + } else if (isArray(firstPoint)) { + if (valueCount) { // [x, low, high] or [x, o, h, l, c] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt.slice(1, valueCount + 1); + } + } else { // [x, y] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt[1]; + } + } + } else { + // Highcharts expects configs to be numbers or arrays in + // turbo mode + H.error(12); + } + } else { + for (i = 0; i < dataLength; i++) { + if (data[i] !== undefined) { // stray commas in oldIE + pt = { series: series }; + series.pointClass.prototype.applyOptions.apply( + pt, + [data[i]] + ); + series.updateParallelArrays(pt, i); + } + } + } + + // Forgetting to cast strings to numbers is a common caveat when + // handling CSV or JSON + if (yData && isString(yData[0])) { + H.error(14, true); + } + + series.data = []; + series.options.data = series.userOptions.data = data; + + // destroy old points + i = oldDataLength; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); + } + } + + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; + } + + // redraw + series.isDirty = chart.isDirtyBox = true; + series.isDirtyData = !!oldData; + animation = false; + } + + // Typically for pie series, points need to be processed and generated + // prior to rendering the legend + if (options.legendType === 'point') { + this.processData(); + this.generatePoints(); + } + + if (redraw) { + chart.redraw(animation); + } + }, + + /** + * Internal function to process the data by cropping away unused data points + * if the series is longer than the crop threshold. This saves computing + * time for large series. In Highstock, this function is extended to + * provide data grouping. + * + * @private + * @param {Boolean} force + * Force data grouping. + */ + processData: function (force) { + var series = this, + processedXData = series.xData, // copied during slice operation + processedYData = series.yData, + dataLength = processedXData.length, + croppedData, + cropStart = 0, + cropped, + distance, + closestPointRange, + xAxis = series.xAxis, + i, // loop variable + options = series.options, + cropThreshold = options.cropThreshold, + getExtremesFromAll = + series.getExtremesFromAll || + options.getExtremesFromAll, // #4599 + isCartesian = series.isCartesian, + xExtremes, + val2lin = xAxis && xAxis.val2lin, + isLog = xAxis && xAxis.isLog, + throwOnUnsorted = series.requireSorting, + min, + max; + + // If the series data or axes haven't changed, don't go through this. + // Return false to pass the message on to override methods like in data + // grouping. + if ( + isCartesian && + !series.isDirty && + !xAxis.isDirty && + !series.yAxis.isDirty && + !force + ) { + return false; + } + + if (xAxis) { + xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) + min = xExtremes.min; + max = xExtremes.max; + } + + // optionally filter out points outside the plot area + if ( + isCartesian && + series.sorted && + !getExtremesFromAll && + (!cropThreshold || dataLength > cropThreshold || series.forceCrop) + ) { + + // it's outside current extremes + if ( + processedXData[dataLength - 1] < min || + processedXData[0] > max + ) { + processedXData = []; + processedYData = []; + + // only crop if it's actually spilling out + } else if ( + processedXData[0] < min || + processedXData[dataLength - 1] > max + ) { + croppedData = this.cropData( + series.xData, + series.yData, + min, + max + ); + processedXData = croppedData.xData; + processedYData = croppedData.yData; + cropStart = croppedData.start; + cropped = true; + } + } + + + // Find the closest distance between processed points + i = processedXData.length || 1; + while (--i) { + distance = isLog ? + val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : + processedXData[i] - processedXData[i - 1]; + + if ( + distance > 0 && + ( + closestPointRange === undefined || + distance < closestPointRange + ) + ) { + closestPointRange = distance; + + // Unsorted data is not supported by the line tooltip, as well as + // data grouping and navigation in Stock charts (#725) and width + // calculation of columns (#1900) + } else if (distance < 0 && throwOnUnsorted) { + H.error(15); + throwOnUnsorted = false; // Only once + } + } + + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; + + series.closestPointRange = closestPointRange; + + }, + + /** + * Iterate over xData and crop values between min and max. Returns object + * containing crop start/end cropped xData with corresponding part of yData, + * dataMin and dataMax within the cropped range. + * + * @private + */ + cropData: function (xData, yData, min, max, cropShoulder) { + var dataLength = xData.length, + cropStart = 0, + cropEnd = dataLength, + i, + j; + + // line-type series need one point outside + cropShoulder = pick(cropShoulder, this.cropShoulder, 1); + + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = Math.max(0, i - cropShoulder); + break; + } + } + + // proceed to find slice end + for (j = i; j < dataLength; j++) { + if (xData[j] > max) { + cropEnd = j + cropShoulder; + break; + } + } + + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, + + + /** + * Generate the data point after the data has been processed by cropping + * away unused points and optionally grouped in Highcharts Stock. + * + * @private + */ + generatePoints: function () { + var series = this, + options = series.options, + dataOptions = options.data, + data = series.data, + dataLength, + processedXData = series.processedXData, + processedYData = series.processedYData, + PointClass = series.pointClass, + processedDataLength = processedXData.length, + cropStart = series.cropStart || 0, + cursor, + hasGroupedData = series.hasGroupedData, + keys = options.keys, + point, + points = [], + i; + + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } + + if (keys && hasGroupedData) { + // grouped data has already applied keys (#6590) + series.options.keys = false; + } + + for (i = 0; i < processedDataLength; i++) { + cursor = cropStart + i; + if (!hasGroupedData) { + point = data[cursor]; + if (!point && dataOptions[cursor] !== undefined) { // #970 + data[cursor] = point = (new PointClass()).init( + series, + dataOptions[cursor], + processedXData[i] + ); + } + } else { + // splat the y data in case of ohlc data array + point = (new PointClass()).init( + series, + [processedXData[i]].concat(splat(processedYData[i])) + ); + + /** + * Highstock only. If a point object is created by data + * grouping, it doesn't reflect actual points in the raw data. + * In this case, the `dataGroup` property holds information + * that points back to the raw data. + * + * - `dataGroup.start` is the index of the first raw data point + * in the group. + * - `dataGroup.length` is the amount of points in the group. + * + * @name dataGroup + * @memberOf Point + * @type {Object} + * + */ + point.dataGroup = series.groupMap[i]; + } + if (point) { // #6279 + point.index = cursor; // For faster access in Point.update + points[i] = point; + } + } + + // restore keys options (#6590) + series.options.keys = keys; + + // Hide cropped-away points - this only runs when the number of points + // is above cropThreshold, or when swithching view from non-grouped + // data to grouped data (#637) + if ( + data && + ( + processedDataLength !== (dataLength = data.length) || + hasGroupedData + ) + ) { + for (i = 0; i < dataLength; i++) { + // when has grouped data, clear all points + if (i === cropStart && !hasGroupedData) { + i += processedDataLength; + } + if (data[i]) { + data[i].destroyElements(); + data[i].plotX = undefined; // #1003 + } + } + } + + /** + * Read only. An array containing those values converted to points. + * In case the series data length exceeds the `cropThreshold`, or if the + * data is grouped, `series.data` doesn't contain all the points. Also, + * in case a series is hidden, the `data` array may be empty. To access + * raw values, `series.options.data` will always be up to date. + * `Series.data` only contains the points that have been created on + * demand. To modify the data, use {@link Highcharts.Series#setData} or + * {@link Highcharts.Point#update}. + * + * @name data + * @memberOf Highcharts.Series + * @see Series.points + * @type {Array.} + */ + series.data = data; + + /** + * An array containing all currently visible point objects. In case of + * cropping, the cropped-away points are not part of this array. The + * `series.points` array starts at `series.cropStart` compared to + * `series.data` and `series.options.data`. If however the series data + * is grouped, these can't be correlated one to one. To + * modify the data, use {@link Highcharts.Series#setData} or {@link + * Highcharts.Point#update}. + * @name points + * @memberof Series + * @type {Array.} + */ + series.points = points; + }, + + /** + * Calculate Y extremes for the visible data. The result is set as + * `dataMin` and `dataMax` on the Series item. + * + * @param {Array.} [yData] + * The data to inspect. Defaults to the current data within the + * visible range. + * + */ + getExtremes: function (yData) { + var xAxis = this.xAxis, + yAxis = this.yAxis, + xData = this.processedXData, + yDataLength, + activeYData = [], + activeCounter = 0, + // #2117, need to compensate for log X axis + xExtremes = xAxis.getExtremes(), + xMin = xExtremes.min, + xMax = xExtremes.max, + validValue, + withinRange, + // Handle X outside the viewed area. This does not work with non- + // sorted data like scatter (#7639). + shoulder = this.requireSorting ? 1 : 0, + x, + y, + i, + j; + + yData = yData || this.stackedYData || this.processedYData || []; + yDataLength = yData.length; + + for (i = 0; i < yDataLength; i++) { + + x = xData[i]; + y = yData[i]; + + // For points within the visible range, including the first point + // outside the visible range (#7061), consider y extremes. + validValue = ( + (isNumber(y, true) || isArray(y)) && + (!yAxis.positiveValuesOnly || (y.length || y > 0)) + ); + withinRange = ( + this.getExtremesFromAll || + this.options.getExtremesFromAll || + this.cropped || + ( + (xData[i + shoulder] || x) >= xMin && + (xData[i - shoulder] || x) <= xMax + ) + ); + + if (validValue && withinRange) { + + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (typeof y[j] === 'number') { // #7380 + activeYData[activeCounter++] = y[j]; + } + } + } else { + activeYData[activeCounter++] = y; + } + } + } + + this.dataMin = arrayMin(activeYData); + this.dataMax = arrayMax(activeYData); + }, + + /** + * Translate data points from raw data values to chart specific positioning + * data needed later in the `drawPoints` and `drawGraph` functions. This + * function can be overridden in plugins and custom series type + * implementations. + */ + translate: function () { + if (!this.processedXData) { // hidden series + this.processData(); + } + this.generatePoints(); + var series = this, + options = series.options, + stacking = options.stacking, + xAxis = series.xAxis, + categories = xAxis.categories, + yAxis = series.yAxis, + points = series.points, + dataLength = points.length, + hasModifyValue = !!series.modifyValue, + i, + pointPlacement = options.pointPlacement, + dynamicallyPlaced = + pointPlacement === 'between' || + isNumber(pointPlacement), + threshold = options.threshold, + stackThreshold = options.startFromThreshold ? threshold : 0, + plotX, + plotY, + lastPlotX, + stackIndicator, + closestPointRangePx = Number.MAX_VALUE; + + /* + * Plotted coordinates need to be within a limited range. Drawing too + * far outside the viewport causes various rendering issues (#3201, + * #3923, #7555). + */ + function limitedRange(val) { + return Math.min(Math.max(-1e5, val), 1e5); + } + + // Point placement is relative to each series pointRange (#5889) + if (pointPlacement === 'between') { + pointPlacement = 0.5; + } + if (isNumber(pointPlacement)) { + pointPlacement *= pick(options.pointRange || xAxis.pointRange); + } + + // Translate each point + for (i = 0; i < dataLength; i++) { + var point = points[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = stacking && yAxis.stacks[( + series.negStacks && + yValue < (stackThreshold ? 0 : threshold) ? '-' : '' + ) + series.stackKey], + pointStack, + stackValues; + + // Discard disallowed y values for log axes (#3434) + if (yAxis.positiveValuesOnly && yValue !== null && yValue <= 0) { + point.isNull = true; + } + + // Get the plotX translation + point.plotX = plotX = correctFloat( // #5236 + limitedRange(xAxis.translate( // #3923 + xValue, + 0, + 0, + 0, + 1, + pointPlacement, + this.type === 'flags' + )) // #3923 + ); + + // Calculate the bottom y value for stacked series + if ( + stacking && + series.visible && + !point.isNull && + stack && + stack[xValue] + ) { + stackIndicator = series.getStackIndicator( + stackIndicator, + xValue, + series.index + ); + pointStack = stack[xValue]; + stackValues = pointStack.points[stackIndicator.key]; + yBottom = stackValues[0]; + yValue = stackValues[1]; + + if ( + yBottom === stackThreshold && + stackIndicator.key === stack[xValue].base + ) { + yBottom = pick(threshold, yAxis.min); + } + if (yAxis.positiveValuesOnly && yBottom <= 0) { // #1200, #1232 + yBottom = null; + } + + point.total = point.stackTotal = pointStack.total; + point.percentage = + pointStack.total && + (point.y / pointStack.total * 100); + point.stackY = yValue; + + // Place the stack label + pointStack.setOffset( + series.pointXOffset || 0, + series.barW || 0 + ); + + } + + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + limitedRange(yAxis.translate(yBottom, 0, 1, 0, 1)) : + null; + + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } + + // Set the the plotY value, reset it for redraws + point.plotY = plotY = + (typeof yValue === 'number' && yValue !== Infinity) ? + limitedRange(yAxis.translate(yValue, 0, 1, 0, 1)) : // #3201 + undefined; + + point.isInside = + plotY !== undefined && + plotY >= 0 && + plotY <= yAxis.len && // #3519 + plotX >= 0 && + plotX <= xAxis.len; + + + // Set client related positions for mouse tracking + point.clientX = dynamicallyPlaced ? + correctFloat( + xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement) + ) : + plotX; // #1514, #5383, #5518 + + point.negative = point.y < (threshold || 0); + + // some API data + point.category = categories && categories[point.x] !== undefined ? + categories[point.x] : point.x; + + // Determine auto enabling of markers (#3635, #5099) + if (!point.isNull) { + if (lastPlotX !== undefined) { + closestPointRangePx = Math.min( + closestPointRangePx, + Math.abs(plotX - lastPlotX) + ); + } + lastPlotX = plotX; + } + + // Find point zone + point.zone = this.zones.length && point.getZone(); + } + series.closestPointRangePx = closestPointRangePx; + + fireEvent(this, 'afterTranslate'); + }, + + /** + * Return the series points with null points filtered out. + * + * @param {Array.} [points] + * The points to inspect, defaults to {@link Series.points}. + * @param {Boolean} [insideOnly=false] + * Whether to inspect only the points that are inside the visible + * view. + * + * @return {Array.} + * The valid points. + */ + getValidPoints: function (points, insideOnly) { + var chart = this.chart; + // #3916, #5029, #5085 + return grep(points || this.points || [], function isValidPoint(point) { + if (insideOnly && !chart.isInsidePlot( + point.plotX, + point.plotY, + chart.inverted + )) { + return false; + } + return !point.isNull; + }); + }, + + /** + * Set the clipping for the series. For animated series it is called twice, + * first to initiate animating the clip then the second time without the + * animation to set the final clip. + * + * @private + */ + setClip: function (animation) { + var chart = this.chart, + options = this.options, + renderer = chart.renderer, + inverted = chart.inverted, + seriesClipBox = this.clipBox, + clipBox = seriesClipBox || chart.clipBox, + sharedClipKey = + this.sharedClipKey || + [ + '_sharedClip', + animation && animation.duration, + animation && animation.easing, + clipBox.height, + options.xAxis, + options.yAxis + ].join(','), // #4526 + clipRect = chart[sharedClipKey], + markerClipRect = chart[sharedClipKey + 'm']; + + // If a clipping rectangle with the same properties is currently present + // in the chart, use that. + if (!clipRect) { + + // When animation is set, prepare the initial positions + if (animation) { + clipBox.width = 0; + if (inverted) { + clipBox.x = chart.plotSizeX; + } + + chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect( + // include the width of the first marker + inverted ? chart.plotSizeX + 99 : -99, + inverted ? -chart.plotLeft : -chart.plotTop, + 99, + inverted ? chart.chartWidth : chart.chartHeight + ); + } + chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); + // Create hashmap for series indexes + clipRect.count = { length: 0 }; + + } + if (animation) { + if (!clipRect.count[this.index]) { + clipRect.count[this.index] = true; + clipRect.count.length += 1; + } + } + + if (options.clip !== false) { + this.group.clip( + animation || seriesClipBox ? clipRect : chart.clipRect + ); + this.markerGroup.clip(markerClipRect); + this.sharedClipKey = sharedClipKey; + } + + // Remove the shared clipping rectangle when all series are shown + if (!animation) { + if (clipRect.count[this.index]) { + delete clipRect.count[this.index]; + clipRect.count.length -= 1; + } + + if ( + clipRect.count.length === 0 && + sharedClipKey && + chart[sharedClipKey] + ) { + if (!seriesClipBox) { + chart[sharedClipKey] = chart[sharedClipKey].destroy(); + } + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'] = + chart[sharedClipKey + 'm'].destroy(); + } + } + } + }, + + /** + * Animate in the series. Called internally twice. First with the `init` + * parameter set to true, which sets up the initial state of the animation. + * Then when ready, it is called with the `init` parameter undefined, in + * order to perform the actual animation. After the second run, the function + * is removed. + * + * @param {Boolean} init + * Initialize the animation. + */ + animate: function (init) { + var series = this, + chart = series.chart, + clipRect, + animation = animObject(series.options.animation), + sharedClipKey; + + // Initialize the animation. Set up the clipping rectangle. + if (init) { + + series.setClip(animation); + + // Run the animation + } else { + sharedClipKey = this.sharedClipKey; + clipRect = chart[sharedClipKey]; + if (clipRect) { + clipRect.animate({ + width: chart.plotSizeX, + x: 0 + }, animation); + } + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'].animate({ + width: chart.plotSizeX + 99, + x: 0 + }, animation); + } + + // Delete this function to allow it only once + series.animate = null; + + } + }, + + /** + * This runs after animation to land on the final plot clipping. + * + * @private + */ + afterAnimate: function () { + this.setClip(); + fireEvent(this, 'afterAnimate'); + this.finishedAnimating = true; + }, + + /** + * Draw the markers for line-like series types, and columns or other + * graphical representation for {@link Point} objects for other series + * types. The resulting element is typically stored as {@link + * Point.graphic}, and is created on the first call and updated and moved on + * subsequent calls. + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart, + i, + point, + symbol, + graphic, + options = series.options, + seriesMarkerOptions = options.marker, + pointMarkerOptions, + hasPointMarker, + enabled, + isInside, + markerGroup = series[series.specialGroup] || series.markerGroup, + xAxis = series.xAxis, + markerAttribs, + globallyEnabled = pick( + seriesMarkerOptions.enabled, + xAxis.isRadial ? true : null, + // Use larger or equal as radius is null in bubbles (#6321) + series.closestPointRangePx >= ( + seriesMarkerOptions.enabledThreshold * + seriesMarkerOptions.radius + ) + ); + + if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { + + for (i = 0; i < points.length; i++) { + point = points[i]; + graphic = point.graphic; + pointMarkerOptions = point.marker || {}; + hasPointMarker = !!point.marker; + enabled = ( + globallyEnabled && + pointMarkerOptions.enabled === undefined + ) || pointMarkerOptions.enabled; + isInside = point.isInside; + + // only draw the point if y is defined + if (enabled && !point.isNull) { + + // Shortcuts + symbol = pick(pointMarkerOptions.symbol, series.symbol); + + markerAttribs = series.markerAttribs( + point, + point.selected && 'select' + ); + + if (graphic) { // update + // Since the marker group isn't clipped, each individual + // marker must be toggled + graphic[isInside ? 'show' : 'hide'](true) + .animate(markerAttribs); + } else if ( + isInside && + (markerAttribs.width > 0 || point.hasImage) + ) { + + /** + * The graphic representation of the point. Typically + * this is a simple shape, like a `rect` for column + * charts or `path` for line markers, but for some + * complex series types like boxplot or 3D charts, the + * graphic may be a `g` element containing other shapes. + * The graphic is generated the first time {@link + * Series#drawPoints} runs, and updated and moved on + * subsequent runs. + * + * @memberof Point + * @name graphic + * @type {SVGElement} + */ + point.graphic = graphic = chart.renderer.symbol( + symbol, + markerAttribs.x, + markerAttribs.y, + markerAttribs.width, + markerAttribs.height, + hasPointMarker ? + pointMarkerOptions : + seriesMarkerOptions + ) + .add(markerGroup); + } + + /*= if (build.classic) { =*/ + // Presentational attributes + if (graphic) { + graphic.attr( + series.pointAttribs( + point, + point.selected && 'select' + ) + ); + } + /*= } =*/ + + if (graphic) { + graphic.addClass(point.getClassName(), true); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + } + } + + }, + + /** + * Get non-presentational attributes for a point. Used internally for both + * styled mode and classic. Can be overridden for different series types. + * + * @see Series#pointAttribs + * + * @param {Point} point + * The Point to inspect. + * @param {String} [state] + * The state, can be either `hover`, `select` or undefined. + * + * @return {SVGAttributes} + * A hash containing those attributes that are not settable from + * CSS. + */ + markerAttribs: function (point, state) { + var seriesMarkerOptions = this.options.marker, + seriesStateOptions, + pointMarkerOptions = point.marker || {}, + symbol = pointMarkerOptions.symbol || seriesMarkerOptions.symbol, + pointStateOptions, + radius = pick( + pointMarkerOptions.radius, + seriesMarkerOptions.radius + ), + attribs; + + // Handle hover and select states + if (state) { + seriesStateOptions = seriesMarkerOptions.states[state]; + pointStateOptions = pointMarkerOptions.states && + pointMarkerOptions.states[state]; + + radius = pick( + pointStateOptions && pointStateOptions.radius, + seriesStateOptions && seriesStateOptions.radius, + radius + ( + seriesStateOptions && seriesStateOptions.radiusPlus || + 0 + ) + ); + } + + point.hasImage = symbol && symbol.indexOf('url') === 0; + + if (point.hasImage) { + radius = 0; // and subsequently width and height is not set + } + + attribs = { + x: Math.floor(point.plotX) - radius, // Math.floor for #1843 + y: point.plotY - radius + }; + + if (radius) { + attribs.width = attribs.height = 2 * radius; + } + + return attribs; + + }, + + /*= if (build.classic) { =*/ + /** + * Internal function to get presentational attributes for each point. Unlike + * {@link Series#markerAttribs}, this function should return those + * attributes that can also be set in CSS. In styled mode, `pointAttribs` + * won't be called. + * + * @param {Point} point + * The point instance to inspect. + * @param {String} [state] + * The point state, can be either `hover`, `select` or undefined for + * normal state. + * + * @return {SVGAttributes} + * The presentational attributes to be set on the point. + */ + pointAttribs: function (point, state) { + var seriesMarkerOptions = this.options.marker, + seriesStateOptions, + pointOptions = point && point.options, + pointMarkerOptions = (pointOptions && pointOptions.marker) || {}, + pointStateOptions, + color = this.color, + pointColorOption = pointOptions && pointOptions.color, + pointColor = point && point.color, + strokeWidth = pick( + pointMarkerOptions.lineWidth, + seriesMarkerOptions.lineWidth + ), + zoneColor = point && point.zone && point.zone.color, + fill, + stroke; + + color = ( + pointColorOption || + zoneColor || + pointColor || + color + ); + fill = ( + pointMarkerOptions.fillColor || + seriesMarkerOptions.fillColor || + color + ); + stroke = ( + pointMarkerOptions.lineColor || + seriesMarkerOptions.lineColor || + color + ); + + // Handle hover and select states + if (state) { + seriesStateOptions = seriesMarkerOptions.states[state]; + pointStateOptions = ( + pointMarkerOptions.states && pointMarkerOptions.states[state] + ) || {}; + strokeWidth = pick( + pointStateOptions.lineWidth, + seriesStateOptions.lineWidth, + strokeWidth + pick( + pointStateOptions.lineWidthPlus, + seriesStateOptions.lineWidthPlus, + 0 + ) + ); + fill = ( + pointStateOptions.fillColor || + seriesStateOptions.fillColor || + fill + ); + stroke = ( + pointStateOptions.lineColor || + seriesStateOptions.lineColor || + stroke + ); + } + + return { + 'stroke': stroke, + 'stroke-width': strokeWidth, + 'fill': fill + }; + }, + /*= } =*/ + /** + * Clear DOM objects and free up memory. + * + * @private + */ + destroy: function () { + var series = this, + chart = series.chart, + issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent), + destroy, + i, + data = series.data || [], + point, + axis; + + // add event hook + fireEvent(series, 'destroy'); + + // remove all events + removeEvent(series); + + // erase from axes + each(series.axisTypes || [], function (AXIS) { + axis = series[AXIS]; + if (axis && axis.series) { + erase(axis.series, series); + axis.isDirty = axis.forceRedraw = true; + } + }); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + i = data.length; + while (i--) { + point = data[i]; + if (point && point.destroy) { + point.destroy(); + } + } + series.points = null; + + // Clear the animation timeout if we are destroying the series during + // initial animation + H.clearTimeout(series.animationTimeout); + + // Destroy all SVGElements associated to the series + objectEach(series, function (val, prop) { + // Survive provides a hook for not destroying + if (val instanceof SVGElement && !val.survive) { + + // issue 134 workaround + destroy = issue134 && prop === 'group' ? + 'hide' : + 'destroy'; + + val[destroy](); + } + }); + + // remove from hoverSeries + if (chart.hoverSeries === series) { + chart.hoverSeries = null; + } + erase(chart.series, series); + chart.orderSeries(); + + // clear all members + objectEach(series, function (val, prop) { + delete series[prop]; + }); + }, + + /** + * Get the graph path. + * + * @private + */ + getGraphPath: function (points, nullsAsZeroes, connectCliffs) { + var series = this, + options = series.options, + step = options.step, + reversed, + graphPath = [], + xMap = [], + gap; + + points = points || series.points; + + // Bottom of a stack is reversed + reversed = points.reversed; + if (reversed) { + points.reverse(); + } + // Reverse the steps (#5004) + step = { right: 1, center: 2 }[step] || (step && 3); + if (step && reversed) { + step = 4 - step; + } + + // Remove invalid points, especially in spline (#5015) + if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { + points = this.getValidPoints(points); + } + + // Build the line + each(points, function (point, i) { + + var plotX = point.plotX, + plotY = point.plotY, + lastPoint = points[i - 1], + pathToPoint; // the path to this point from the previous + + if ( + (point.leftCliff || (lastPoint && lastPoint.rightCliff)) && + !connectCliffs + ) { + gap = true; // ... and continue + } + + // Line series, nullsAsZeroes is not handled + if (point.isNull && !defined(nullsAsZeroes) && i > 0) { + gap = !options.connectNulls; + + // Area series, nullsAsZeroes is set + } else if (point.isNull && !nullsAsZeroes) { + gap = true; + + } else { + + if (i === 0 || gap) { + pathToPoint = ['M', point.plotX, point.plotY]; + + // Generate the spline as defined in the SplineSeries object + } else if (series.getPointSpline) { + + pathToPoint = series.getPointSpline(points, point, i); + + } else if (step) { + + if (step === 1) { // right + pathToPoint = [ + 'L', + lastPoint.plotX, + plotY + ]; + + } else if (step === 2) { // center + pathToPoint = [ + 'L', + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + 'L', + (lastPoint.plotX + plotX) / 2, + plotY + ]; + + } else { + pathToPoint = [ + 'L', + plotX, + lastPoint.plotY + ]; + } + pathToPoint.push('L', plotX, plotY); + + } else { + // normal line to next point + pathToPoint = [ + 'L', + plotX, + plotY + ]; + } + + // Prepare for animation. When step is enabled, there are two + // path nodes for each x value. + xMap.push(point.x); + if (step) { + xMap.push(point.x); + } + + graphPath.push.apply(graphPath, pathToPoint); + gap = false; + } + }); + + graphPath.xMap = xMap; + series.graphPath = graphPath; + + return graphPath; + + }, + + /** + * Draw the graph. Called internally when rendering line-like series types. + * The first time it generates the `series.graph` item and optionally other + * series-wide items like `series.area` for area charts. On subsequent calls + * these items are updated with new positions and attributes. + */ + drawGraph: function () { + var series = this, + options = this.options, + graphPath = (this.gappedPath || this.getGraphPath).call(this), + props = [[ + 'graph', + 'highcharts-graph', + /*= if (build.classic) { =*/ + options.lineColor || this.color, + options.dashStyle + /*= } =*/ + ]]; + + props = series.getZonesGraphs(props); + + // Draw the graph + each(props, function (prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; + + if (graph) { + graph.endX = series.preventGraphAnimation ? + null : + graphPath.xMap; + graph.animate({ d: graphPath }); + + } else if (graphPath.length) { // #1487 + + series[graphKey] = series.chart.renderer.path(graphPath) + .addClass(prop[1]) + .attr({ zIndex: 1 }) // #1069 + .add(series.group); + + /*= if (build.classic) { =*/ + attribs = { + 'stroke': prop[2], + 'stroke-width': options.lineWidth, + // Polygon series use filled graph + 'fill': (series.fillGraph && series.color) || 'none' + }; + + if (prop[3]) { + attribs.dashstyle = prop[3]; + } else if (options.linecap !== 'square') { + attribs['stroke-linecap'] = attribs['stroke-linejoin'] = + 'round'; + } + + graph = series[graphKey] + .attr(attribs) + // Add shadow to normal series (0) or to first zone (1) + // #3932 + .shadow((i < 2) && options.shadow); + /*= } =*/ + } + + // Helpers for animation + if (graph) { + graph.startX = graphPath.xMap; + graph.isArea = graphPath.isArea; // For arearange animation + } + }); + }, + + /** + * Get zones properties for building graphs. + * Extendable by series with multiple lines within one series. + * + * @private + */ + getZonesGraphs: function (props) { + // Add the zone properties if any + each(this.zones, function (zone, i) { + props.push([ + 'zone-graph-' + i, + 'highcharts-graph highcharts-zone-graph-' + i + ' ' + + (zone.className || ''), + /*= if (build.classic) { =*/ + zone.color || this.color, + zone.dashStyle || this.options.dashStyle + /*= } =*/ + ]); + }, this); + + return props; + }, + + /** + * Clip the graphs into zones for colors and styling. + * + * @private + */ + applyZones: function () { + var series = this, + chart = this.chart, + renderer = chart.renderer, + zones = this.zones, + translatedFrom, + translatedTo, + clips = this.clips || [], + clipAttr, + graph = this.graph, + area = this.area, + chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight), + axis = this[(this.zoneAxis || 'y') + 'Axis'], + extremes, + reversed, + inverted = chart.inverted, + horiz, + pxRange, + pxPosMin, + pxPosMax, + ignoreZones = false; + + if (zones.length && (graph || area) && axis && axis.min !== undefined) { + reversed = axis.reversed; + horiz = axis.horiz; + // The use of the Color Threshold assumes there are no gaps + // so it is safe to hide the original graph and area + // unless it is not waterfall series, then use showLine property to + // set lines between columns to be visible (#7862) + if (graph && !this.showLine) { + graph.hide(); + } + if (area) { + area.hide(); + } + + // Create the clips + extremes = axis.getExtremes(); + each(zones, function (threshold, i) { + + translatedFrom = reversed ? + (horiz ? chart.plotWidth : 0) : + (horiz ? 0 : axis.toPixels(extremes.min)); + translatedFrom = Math.min( + Math.max( + pick(translatedTo, translatedFrom), 0 + ), + chartSizeMax + ); + translatedTo = Math.min( + Math.max( + Math.round( + axis.toPixels( + pick(threshold.value, extremes.max), + true + ) + ), + 0 + ), + chartSizeMax + ); + + if (ignoreZones) { + translatedFrom = translatedTo = axis.toPixels(extremes.max); + } + + pxRange = Math.abs(translatedFrom - translatedTo); + pxPosMin = Math.min(translatedFrom, translatedTo); + pxPosMax = Math.max(translatedFrom, translatedTo); + if (axis.isXAxis) { + clipAttr = { + x: inverted ? pxPosMax : pxPosMin, + y: 0, + width: pxRange, + height: chartSizeMax + }; + if (!horiz) { + clipAttr.x = chart.plotHeight - clipAttr.x; + } + } else { + clipAttr = { + x: 0, + y: inverted ? pxPosMax : pxPosMin, + width: chartSizeMax, + height: pxRange + }; + if (horiz) { + clipAttr.y = chart.plotWidth - clipAttr.y; + } + } + + /*= if (build.classic) { =*/ + // VML SUPPPORT + if (inverted && renderer.isVML) { + if (axis.isXAxis) { + clipAttr = { + x: 0, + y: reversed ? pxPosMin : pxPosMax, + height: clipAttr.width, + width: chart.chartWidth + }; + } else { + clipAttr = { + x: clipAttr.y - chart.plotLeft - chart.spacingBox.x, + y: 0, + width: clipAttr.height, + height: chart.chartHeight + }; + } + } + // END OF VML SUPPORT + /*= } =*/ + + if (clips[i]) { + clips[i].animate(clipAttr); + } else { + clips[i] = renderer.clipRect(clipAttr); + + if (graph) { + series['zone-graph-' + i].clip(clips[i]); + } + + if (area) { + series['zone-area-' + i].clip(clips[i]); + } + } + // if this zone extends out of the axis, ignore the others + ignoreZones = threshold.value > extremes.max; + + // Clear translatedTo for indicators + if (series.resetZones && translatedTo === 0) { + translatedTo = undefined; + } + }); + this.clips = clips; + } + }, + + /** + * Initialize and perform group inversion on series.group and + * series.markerGroup. + * + * @private + */ + invertGroups: function (inverted) { + var series = this, + chart = series.chart, + remover; + + function setInvert() { + each(['group', 'markerGroup'], function (groupName) { + if (series[groupName]) { + + // VML/HTML needs explicit attributes for flipping + if (chart.renderer.isVML) { + series[groupName].attr({ + width: series.yAxis.len, + height: series.xAxis.len + }); + } + + series[groupName].width = series.yAxis.len; + series[groupName].height = series.xAxis.len; + series[groupName].invert(inverted); + } + }); + } + + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } + + // A fixed size is needed for inversion to work + remover = addEvent(chart, 'resize', setInvert); + addEvent(series, 'destroy', remover); + + // Do it now + setInvert(inverted); // do it now + + // On subsequent render and redraw, just do setInvert without setting up + // events again + series.invertGroups = setInvert; + }, + + /** + * General abstraction for creating plot groups like series.group, + * series.dataLabelsGroup and series.markerGroup. On subsequent calls, the + * group will only be adjusted to the updated plot size. + * + * @private + */ + plotGroup: function (prop, name, visibility, zIndex, parent) { + var group = this[prop], + isNew = !group; + + // Generate it on first call + if (isNew) { + this[prop] = group = this.chart.renderer.g() + .attr({ + zIndex: zIndex || 0.1 // IE8 and pointer logic use this + }) + .add(parent); + + } + + // Add the class names, and replace existing ones as response to + // Series.update (#6660) + group.addClass( + ( + 'highcharts-' + name + + ' highcharts-series-' + this.index + + ' highcharts-' + this.type + '-series ' + + ( + defined(this.colorIndex) ? + 'highcharts-color-' + this.colorIndex + ' ' : + '' + ) + + (this.options.className || '') + + ( + group.hasClass('highcharts-tracker') ? + ' highcharts-tracker' : + '' + ) + ), + true + ); + + // Place it on first and subsequent (redraw) calls + group.attr({ visibility: visibility })[isNew ? 'attr' : 'animate']( + this.getPlotBox() + ); + return group; + }, + + /** + * Get the translation and scale for the plot area of this series. + */ + getPlotBox: function () { + var chart = this.chart, + xAxis = this.xAxis, + yAxis = this.yAxis; + + // Swap axes for inverted (#2339) + if (chart.inverted) { + xAxis = yAxis; + yAxis = this.xAxis; + } + return { + translateX: xAxis ? xAxis.left : chart.plotLeft, + translateY: yAxis ? yAxis.top : chart.plotTop, + scaleX: 1, // #1623 + scaleY: 1 + }; + }, + + /** + * Render the graph and markers. Called internally when first rendering and + * later when redrawing the chart. This function can be extended in plugins, + * but normally shouldn't be called directly. + */ + render: function () { + var series = this, + chart = series.chart, + group, + options = series.options, + // Animation doesn't work in IE8 quirks when the group div is + // hidden, and looks bad in other oldIE + animDuration = ( + !!series.animate && + chart.renderer.isSVG && + animObject(options.animation).duration + ), + visibility = series.visible ? 'inherit' : 'hidden', // #2597 + zIndex = options.zIndex, + hasRendered = series.hasRendered, + chartSeriesGroup = chart.seriesGroup, + inverted = chart.inverted; + + // the group + group = series.plotGroup( + 'group', + 'series', + visibility, + zIndex, + chartSeriesGroup + ); + + series.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + visibility, + zIndex, + chartSeriesGroup + ); + + // initiate the animation + if (animDuration) { + series.animate(true); + } + + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? inverted : false; + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + series.applyZones(); + } + +/* each(series.points, function (point) { + if (point.redraw) { + point.redraw(); + } + });*/ + + // draw the data labels (inn pies they go before the points) + if (series.drawDataLabels) { + series.drawDataLabels(); + } + + // draw the points + if (series.visible) { + series.drawPoints(); + } + + + // draw the mouse tracking area + if ( + series.drawTracker && + series.options.enableMouseTracking !== false + ) { + series.drawTracker(); + } + + // Handle inverted series and tracker groups + series.invertGroups(inverted); + + // Initial clipping, must be defined after inverting groups for VML. + // Applies to columns etc. (#3839). + if (options.clip !== false && !series.sharedClipKey && !hasRendered) { + group.clip(chart.clipRect); + } + + // Run the animation + if (animDuration) { + series.animate(); + } + + // Call the afterAnimate function on animation complete (but don't + // overwrite the animation.complete option which should be available to + // the user). + if (!hasRendered) { + series.animationTimeout = syncTimeout(function () { + series.afterAnimate(); + }, animDuration); + } + + series.isDirty = false; // means data is in accordance with what you see + // (See #322) series.isDirty = series.isDirtyData = false; // means + // data is in accordance with what you see + series.hasRendered = true; + + fireEvent(series, 'afterRender'); + }, + + /** + * Redraw the series. This function is called internally from `chart.redraw` + * and normally shouldn't be called directly. + * + * @private + */ + redraw: function () { + var series = this, + chart = series.chart, + // cache it here as it is set to false in render, but used after + wasDirty = series.isDirty || series.isDirtyData, + group = series.group, + xAxis = series.xAxis, + yAxis = series.yAxis; + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) + }); + } + + series.translate(); + series.render(); + if (wasDirty) { // #3868, #3945 + delete this.kdTree; + } + }, + + kdAxisArray: ['clientX', 'plotY'], + + searchPoint: function (e, compareX) { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + inverted = series.chart.inverted; + + return this.searchKDTree({ + clientX: inverted ? + xAxis.len - e.chartY + xAxis.pos : + e.chartX - xAxis.pos, + plotY: inverted ? + yAxis.len - e.chartX + yAxis.pos : + e.chartY - yAxis.pos + }, compareX); + }, + + /** + * Build the k-d-tree that is used by mouse and touch interaction to get the + * closest point. Line-like series typically have a one-dimensional tree + * where points are searched along the X axis, while scatter-like series + * typically search in two dimensions, X and Y. + * + * @private + */ + buildKDTree: function () { + + // Prevent multiple k-d-trees from being built simultaneously (#6235) + this.buildingKdTree = true; + + var series = this, + dimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? + 2 : 1; + + // Internal function + function _kdtree(points, depth, dimensions) { + var axis, + median, + length = points && points.length; + + if (length) { + + // alternate between the axis + axis = series.kdAxisArray[depth % dimensions]; + + // sort point array + points.sort(function (a, b) { + return a[axis] - b[axis]; + }); + + median = Math.floor(length / 2); + + // build and return nod + return { + point: points[median], + left: _kdtree( + points.slice(0, median), depth + 1, dimensions + ), + right: _kdtree( + points.slice(median + 1), depth + 1, dimensions + ) + }; + + } + } + + // Start the recursive build process with a clone of the points array + // and null points filtered out (#3873) + function startRecursive() { + series.kdTree = _kdtree( + series.getValidPoints( + null, + // For line-type series restrict to plot area, but + // column-type series not (#3916, #4511) + !series.directTouch + ), + dimensions, + dimensions + ); + series.buildingKdTree = false; + } + delete series.kdTree; + + // For testing tooltips, don't build async + syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); + }, + + searchKDTree: function (point, compareX) { + var series = this, + kdX = this.kdAxisArray[0], + kdY = this.kdAxisArray[1], + kdComparer = compareX ? 'distX' : 'dist', + kdDimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? + 2 : 1; + + // Set the one and two dimensional distance on the point object + function setDistance(p1, p2) { + var x = (defined(p1[kdX]) && defined(p2[kdX])) ? + Math.pow(p1[kdX] - p2[kdX], 2) : + null, + y = (defined(p1[kdY]) && defined(p2[kdY])) ? + Math.pow(p1[kdY] - p2[kdY], 2) : + null, + r = (x || 0) + (y || 0); + + p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; + p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; + } + function _search(search, tree, depth, dimensions) { + var point = tree.point, + axis = series.kdAxisArray[depth % dimensions], + tdist, + sideA, + sideB, + ret = point, + nPoint1, + nPoint2; + + setDistance(search, point); + + // Pick side based on distance to splitting point + tdist = search[axis] - point[axis]; + sideA = tdist < 0 ? 'left' : 'right'; + sideB = tdist < 0 ? 'right' : 'left'; + + // End of tree + if (tree[sideA]) { + nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); + + ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); + } + if (tree[sideB]) { + // compare distance to current best to splitting point to decide + // wether to check side B or not + if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { + nPoint2 = _search( + search, + tree[sideB], + depth + 1, + dimensions + ); + ret = nPoint2[kdComparer] < ret[kdComparer] ? + nPoint2 : + ret; + } + } + + return ret; + } + + if (!this.kdTree && !this.buildingKdTree) { + this.buildKDTree(); + } + + if (this.kdTree) { + return _search(point, this.kdTree, kdDimensions, kdDimensions); + } + } }); // end Series prototype @@ -4949,7 +4949,7 @@ H.Series = H.seriesType('line', null, { // base series options * * @sample {highcharts} highcharts/demo/line-basic/ Line chart * @sample {highstock} stock/demo/basic-line/ Line chart - * + * * @extends plotOptions.series * @product highcharts highstock * @apioption plotOptions.line @@ -4958,7 +4958,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * A `line` series. If the [type](#series.line.type) option is not * specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.line * @excluding dataParser,dataURL @@ -4969,21 +4969,21 @@ H.Series = H.seriesType('line', null, { // base series options /** * An array of data points for the series. For the `line` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 1], @@ -4991,12 +4991,12 @@ H.Series = H.seriesType('line', null, { // base series options * [2, 8] * ] * ``` - * + * * 3. 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.line.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -5010,7 +5010,7 @@ H.Series = H.seriesType('line', null, { // base series options * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @sample {highcharts} highcharts/chart/reflow-true/ * Numerical values @@ -5021,14 +5021,14 @@ H.Series = H.seriesType('line', null, { // base series options * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ - * Config objects + * Config objects * @apioption series.line.data */ /** * An additional, individual class name for the data point's graphic * representation. - * + * * @type {String} * @since 5.0.0 * @product highcharts @@ -5039,9 +5039,9 @@ H.Series = H.seriesType('line', null, { // base series options * Individual color for the point. By default the color is pulled from * the global `colors` array. * - * In styled mode, the `color` option doesn't take effect. Instead, use + * In styled mode, the `color` option doesn't take effect. Instead, use * `colorIndex`. - * + * * @type {Color} * @sample {highcharts} highcharts/point/color/ Mark the highest point * @default undefined @@ -5055,7 +5055,7 @@ H.Series = H.seriesType('line', null, { // base series options * change the color of the graphic. In non-styled mode, the color by is set by * the `fill` attribute, so the change in class name won't have a visual effect * by default. - * + * * @type {Number} * @since 5.0.0 * @product highcharts @@ -5066,7 +5066,7 @@ H.Series = H.seriesType('line', null, { // base series options * Individual data label for each point. The options are the same as * the ones for [plotOptions.series.dataLabels]( * #plotOptions.series.dataLabels). - * + * * @type {Object} * @sample highcharts/point/datalabels/ * Show a label for the last value @@ -5077,7 +5077,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * A description of the point to add to the screen reader information * about the point. Requires the Accessibility module. - * + * * @type {String} * @default undefined * @since 5.0.0 @@ -5087,7 +5087,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * An id for the point. This can be used after render time to get a * pointer to the point object through `chart.get()`. - * + * * @type {String} * @sample {highcharts} highcharts/point/id/ Remove an id'd point * @default null @@ -5100,7 +5100,7 @@ H.Series = H.seriesType('line', null, { // base series options * The rank for this point's data label in case of collision. If two * data labels are about to overlap, only the one with the highest `labelrank` * will be drawn. - * + * * @type {Number} * @apioption series.line.data.labelrank */ @@ -5108,7 +5108,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * The name of the point as shown in the legend, tooltip, dataLabel * etc. - * + * * @type {String} * @sample {highcharts} highcharts/series/data-array-of-objects/ Point names * @see [xAxis.uniqueNames](#xAxis.uniqueNames) @@ -5117,7 +5117,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * Whether the data point is selected initially. - * + * * @type {Boolean} * @default false * @product highcharts highstock @@ -5127,7 +5127,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * The x value of the point. For datetime axes, the X value is the timestamp * in milliseconds since 1970. - * + * * @type {Number} * @product highcharts highstock * @apioption series.line.data.x @@ -5135,7 +5135,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * The y value of the point. - * + * * @type {Number} * @default null * @product highcharts highstock @@ -5144,7 +5144,7 @@ H.Series = H.seriesType('line', null, { // base series options /** * Individual point events - * + * * @extends plotOptions.series.point.events * @product highcharts highstock * @apioption series.line.data.events diff --git a/js/parts/SplineSeries.js b/js/parts/SplineSeries.js index 502e2462d0a..f8249c3d6dd 100644 --- a/js/parts/SplineSeries.js +++ b/js/parts/SplineSeries.js @@ -9,7 +9,7 @@ import './Utilities.js'; import './Options.js'; import './Series.js'; var pick = H.pick, - seriesType = H.seriesType; + seriesType = H.seriesType; /** * A spline series is a special type of line series, where the segments between @@ -32,149 +32,149 @@ var pick = H.pick, * @extends {Series} */ seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ { - /** - * Get the spline segment from a given point's previous neighbour to the - * given point - */ - getPointSpline: function (points, point, i) { - var - // 1 means control points midway between points, 2 means 1/3 from - // the point, 3 is 1/4 etc - smoothing = 1.5, - denom = smoothing + 1, - plotX = point.plotX, - plotY = point.plotY, - lastPoint = points[i - 1], - nextPoint = points[i + 1], - leftContX, - leftContY, - rightContX, - rightContY, - ret; + /** + * Get the spline segment from a given point's previous neighbour to the + * given point + */ + getPointSpline: function (points, point, i) { + var + // 1 means control points midway between points, 2 means 1/3 from + // the point, 3 is 1/4 etc + smoothing = 1.5, + denom = smoothing + 1, + plotX = point.plotX, + plotY = point.plotY, + lastPoint = points[i - 1], + nextPoint = points[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; - function doCurve(otherPoint) { - return otherPoint && - !otherPoint.isNull && - otherPoint.doCurve !== false && - !point.isCliff; // #6387, area splines next to null - } + function doCurve(otherPoint) { + return otherPoint && + !otherPoint.isNull && + otherPoint.doCurve !== false && + !point.isCliff; // #6387, area splines next to null + } - // Find control points - if (doCurve(lastPoint) && doCurve(nextPoint)) { - var lastX = lastPoint.plotX, - lastY = lastPoint.plotY, - nextX = nextPoint.plotX, - nextY = nextPoint.plotY, - correction = 0; + // Find control points + if (doCurve(lastPoint) && doCurve(nextPoint)) { + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction = 0; - leftContX = (smoothing * plotX + lastX) / denom; - leftContY = (smoothing * plotY + lastY) / denom; - rightContX = (smoothing * plotX + nextX) / denom; - rightContY = (smoothing * plotY + nextY) / denom; + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; - // Have the two control points make a straight line through main - // point - if (rightContX !== leftContX) { // #5016, division by zero - correction = ((rightContY - leftContY) * (rightContX - plotX)) / - (rightContX - leftContX) + plotY - rightContY; - } + // Have the two control points make a straight line through main + // point + if (rightContX !== leftContX) { // #5016, division by zero + correction = ((rightContY - leftContY) * (rightContX - plotX)) / + (rightContX - leftContX) + plotY - rightContY; + } - leftContY += correction; - rightContY += correction; + leftContY += correction; + rightContY += correction; - // to prevent false extremes, check that control points are between - // neighbouring points' y values - if (leftContY > lastY && leftContY > plotY) { - leftContY = Math.max(lastY, plotY); - // mirror of left control point - rightContY = 2 * plotY - leftContY; - } else if (leftContY < lastY && leftContY < plotY) { - leftContY = Math.min(lastY, plotY); - rightContY = 2 * plotY - leftContY; - } - if (rightContY > nextY && rightContY > plotY) { - rightContY = Math.max(nextY, plotY); - leftContY = 2 * plotY - rightContY; - } else if (rightContY < nextY && rightContY < plotY) { - rightContY = Math.min(nextY, plotY); - leftContY = 2 * plotY - rightContY; - } + // to prevent false extremes, check that control points are between + // neighbouring points' y values + if (leftContY > lastY && leftContY > plotY) { + leftContY = Math.max(lastY, plotY); + // mirror of left control point + rightContY = 2 * plotY - leftContY; + } else if (leftContY < lastY && leftContY < plotY) { + leftContY = Math.min(lastY, plotY); + rightContY = 2 * plotY - leftContY; + } + if (rightContY > nextY && rightContY > plotY) { + rightContY = Math.max(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } else if (rightContY < nextY && rightContY < plotY) { + rightContY = Math.min(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } - // record for drawing in next point - point.rightContX = rightContX; - point.rightContY = rightContY; + // record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; - - } - // Visualize control points for debugging - /* - if (leftContX) { - this.chart.renderer.circle( - leftContX + this.chart.plotLeft, - leftContY + this.chart.plotTop, - 2 - ) - .attr({ - stroke: 'red', - 'stroke-width': 2, - fill: 'none', - zIndex: 9 - }) - .add(); - this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, - leftContY + this.chart.plotTop, - 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) - .attr({ - stroke: 'red', - 'stroke-width': 2, - zIndex: 9 - }) - .add(); - } - if (rightContX) { - this.chart.renderer.circle( - rightContX + this.chart.plotLeft, - rightContY + this.chart.plotTop, - 2 - ) - .attr({ - stroke: 'green', - 'stroke-width': 2, - fill: 'none', - zIndex: 9 - }) - .add(); - this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, - rightContY + this.chart.plotTop, - 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) - .attr({ - stroke: 'green', - 'stroke-width': 2, - zIndex: 9 - }) - .add(); - } - // */ - ret = [ - 'C', - pick(lastPoint.rightContX, lastPoint.plotX), - pick(lastPoint.rightContY, lastPoint.plotY), - pick(leftContX, plotX), - pick(leftContY, plotY), - plotX, - plotY - ]; - // reset for updating series later - lastPoint.rightContX = lastPoint.rightContY = null; - return ret; - } + } + + // Visualize control points for debugging + /* + if (leftContX) { + this.chart.renderer.circle( + leftContX + this.chart.plotLeft, + leftContY + this.chart.plotTop, + 2 + ) + .attr({ + stroke: 'red', + 'stroke-width': 2, + fill: 'none', + zIndex: 9 + }) + .add(); + this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, + leftContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'red', + 'stroke-width': 2, + zIndex: 9 + }) + .add(); + } + if (rightContX) { + this.chart.renderer.circle( + rightContX + this.chart.plotLeft, + rightContY + this.chart.plotTop, + 2 + ) + .attr({ + stroke: 'green', + 'stroke-width': 2, + fill: 'none', + zIndex: 9 + }) + .add(); + this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, + rightContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'green', + 'stroke-width': 2, + zIndex: 9 + }) + .add(); + } + // */ + ret = [ + 'C', + pick(lastPoint.rightContX, lastPoint.plotX), + pick(lastPoint.rightContY, lastPoint.plotY), + pick(leftContX, plotX), + pick(leftContY, plotY), + plotX, + plotY + ]; + // reset for updating series later + lastPoint.rightContX = lastPoint.rightContY = null; + return ret; + } }); /** * A `spline` series. If the [type](#series.spline.type) option is * not specified, it is inherited from [chart.type](#chart.type). - * + * * @type {Object} * @extends series,plotOptions.spline * @excluding dataParser,dataURL,step @@ -185,21 +185,21 @@ seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ { /** * An array of data points for the series. For the `spline` series type, * points can be given in the following ways: - * + * * 1. An array of numerical values. In this case, the numerical values * will be interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from `pointStart` * and `pointInterval` given in the series options. If the axis has * categories, these will be used. Example: - * + * * ```js * data: [0, 5, 3, 5] * ``` - * + * * 2. An array of arrays with 2 values. In this case, the values correspond * to `x,y`. If the first value is a string, it is applied as the name * of the point, and the `x` value is inferred. - * + * * ```js * data: [ * [0, 9], @@ -207,12 +207,12 @@ seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ { * [2, 8] * ] * ``` - * + * * 3. 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.spline.turboThreshold), * this option is not available. - * + * * ```js * data: [{ * x: 1, @@ -226,7 +226,7 @@ seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ { * color: "#FF00FF" * }] * ``` - * + * * @type {Array} * @extends series.line.data * @sample {highcharts} highcharts/chart/reflow-true/ diff --git a/js/parts/Stacking.js b/js/parts/Stacking.js index b5f99626c57..e135d04c36d 100644 --- a/js/parts/Stacking.js +++ b/js/parts/Stacking.js @@ -10,15 +10,15 @@ import './Axis.js'; import './Chart.js'; import './Series.js'; var Axis = H.Axis, - Chart = H.Chart, - correctFloat = H.correctFloat, - defined = H.defined, - destroyObjectProperties = H.destroyObjectProperties, - each = H.each, - format = H.format, - objectEach = H.objectEach, - pick = H.pick, - Series = H.Series; + Chart = H.Chart, + correctFloat = H.correctFloat, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + format = H.format, + objectEach = H.objectEach, + pick = H.pick, + Series = H.Series; /** * The class for stacks. Each stack, on a specific X value and either negative @@ -28,166 +28,166 @@ var Axis = H.Axis, */ H.StackItem = function (axis, options, isNegative, x, stackOption) { - var inverted = axis.chart.inverted; + var inverted = axis.chart.inverted; - this.axis = axis; + this.axis = axis; - // Tells if the stack is negative - this.isNegative = isNegative; + // Tells if the stack is negative + this.isNegative = isNegative; - // Save the options to be able to style the label - this.options = options; + // Save the options to be able to style the label + this.options = options; - // Save the x value to be able to position the label later - this.x = x; + // Save the x value to be able to position the label later + this.x = x; - // Initialize total value - this.total = null; + // Initialize total value + this.total = null; - // This will keep each points' extremes stored by series.index and point - // index - this.points = {}; + // This will keep each points' extremes stored by series.index and point + // index + this.points = {}; - // Save the stack option on the series configuration object, and whether to - // treat it as percent - this.stack = stackOption; - this.leftCliff = 0; - this.rightCliff = 0; + // Save the stack option on the series configuration object, and whether to + // treat it as percent + this.stack = stackOption; + this.leftCliff = 0; + this.rightCliff = 0; - // The align options and text align varies on whether the stack is negative - // and if the chart is inverted or not. - // First test the user supplied value, then use the dynamic. - this.alignOptions = { - align: options.align || - (inverted ? (isNegative ? 'left' : 'right') : 'center'), - verticalAlign: options.verticalAlign || - (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), - y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), - x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) - }; + // The align options and text align varies on whether the stack is negative + // and if the chart is inverted or not. + // First test the user supplied value, then use the dynamic. + this.alignOptions = { + align: options.align || + (inverted ? (isNegative ? 'left' : 'right') : 'center'), + verticalAlign: options.verticalAlign || + (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), + y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), + x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) + }; - this.textAlign = options.textAlign || - (inverted ? (isNegative ? 'right' : 'left') : 'center'); + this.textAlign = options.textAlign || + (inverted ? (isNegative ? 'right' : 'left') : 'center'); }; H.StackItem.prototype = { - destroy: function () { - destroyObjectProperties(this, this.axis); - }, - - /** - * Renders the stack total label and adds it to the stack label group. - */ - render: function (group) { - var chart = this.axis.chart, - options = this.options, - formatOption = options.format, - str = formatOption ? - format(formatOption, this, chart.time) : - options.formatter.call(this); // format the text in the label - - // Change the text to reflect the new total and set visibility to hidden - // in case the serie is hidden - if (this.label) { - this.label.attr({ text: str, visibility: 'hidden' }); - // Create new label - } else { - this.label = - chart.renderer.text(str, null, null, options.useHTML) - .css(options.style) - .attr({ - align: this.textAlign, - rotation: options.rotation, - visibility: 'hidden' // hidden until setOffset is called - }) - .add(group); // add to the labels-group - } - }, - - /** - * Sets the offset that the stack has from the x value and repositions the - * label. - */ - setOffset: function (xOffset, xWidth) { - var stackItem = this, - axis = stackItem.axis, - chart = axis.chart, - // stack value translated mapped to chart coordinates - y = axis.translate( - axis.usePercentage ? 100 : stackItem.total, - 0, - 0, - 0, - 1 - ), - yZero = axis.translate(0), // stack origin - h = Math.abs(y - yZero), // stack height - x = chart.xAxis[0].translate(stackItem.x) + xOffset, // x position - stackBox = stackItem.getStackBox( - chart, - stackItem, - x, - y, - xWidth, - h, - axis - ), - label = stackItem.label, - alignAttr; - - if (label) { - // Align the label to the box - label.align(stackItem.alignOptions, null, stackBox); - - // Set visibility (#678) - alignAttr = label.alignAttr; - label[ - stackItem.options.crop === false || chart.isInsidePlot( - alignAttr.x, - alignAttr.y - ) ? 'show' : 'hide'](true); - } - }, - getStackBox: function (chart, stackItem, x, y, xWidth, h, axis) { - var reversed = stackItem.axis.reversed, - inverted = chart.inverted, - axisPos = axis.height + axis.pos - chart.plotTop, - neg = (stackItem.isNegative && !reversed) || - (!stackItem.isNegative && reversed); // #4056 - - return { // this is the box for the complete stack - x: inverted ? (neg ? y : y - h) : x, - y: inverted ? - axisPos - x - xWidth : - (neg ? - (axisPos - y - h) : - axisPos - y - ), - width: inverted ? h : xWidth, - height: inverted ? xWidth : h - }; - } + destroy: function () { + destroyObjectProperties(this, this.axis); + }, + + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var chart = this.axis.chart, + options = this.options, + formatOption = options.format, + str = formatOption ? + format(formatOption, this, chart.time) : + options.formatter.call(this); // format the text in the label + + // Change the text to reflect the new total and set visibility to hidden + // in case the serie is hidden + if (this.label) { + this.label.attr({ text: str, visibility: 'hidden' }); + // Create new label + } else { + this.label = + chart.renderer.text(str, null, null, options.useHTML) + .css(options.style) + .attr({ + align: this.textAlign, + rotation: options.rotation, + visibility: 'hidden' // hidden until setOffset is called + }) + .add(group); // add to the labels-group + } + }, + + /** + * Sets the offset that the stack has from the x value and repositions the + * label. + */ + setOffset: function (xOffset, xWidth) { + var stackItem = this, + axis = stackItem.axis, + chart = axis.chart, + // stack value translated mapped to chart coordinates + y = axis.translate( + axis.usePercentage ? 100 : stackItem.total, + 0, + 0, + 0, + 1 + ), + yZero = axis.translate(0), // stack origin + h = Math.abs(y - yZero), // stack height + x = chart.xAxis[0].translate(stackItem.x) + xOffset, // x position + stackBox = stackItem.getStackBox( + chart, + stackItem, + x, + y, + xWidth, + h, + axis + ), + label = stackItem.label, + alignAttr; + + if (label) { + // Align the label to the box + label.align(stackItem.alignOptions, null, stackBox); + + // Set visibility (#678) + alignAttr = label.alignAttr; + label[ + stackItem.options.crop === false || chart.isInsidePlot( + alignAttr.x, + alignAttr.y + ) ? 'show' : 'hide'](true); + } + }, + getStackBox: function (chart, stackItem, x, y, xWidth, h, axis) { + var reversed = stackItem.axis.reversed, + inverted = chart.inverted, + axisPos = axis.height + axis.pos - chart.plotTop, + neg = (stackItem.isNegative && !reversed) || + (!stackItem.isNegative && reversed); // #4056 + + return { // this is the box for the complete stack + x: inverted ? (neg ? y : y - h) : x, + y: inverted ? + axisPos - x - xWidth : + (neg ? + (axisPos - y - h) : + axisPos - y + ), + width: inverted ? h : xWidth, + height: inverted ? xWidth : h + }; + } }; /** * Generate stacks for each series and calculate stacks total values */ Chart.prototype.getStacks = function () { - var chart = this; - - // reset stacks for each yAxis - each(chart.yAxis, function (axis) { - if (axis.stacks && axis.hasVisibleSeries) { - axis.oldStacks = axis.stacks; - } - }); - - each(chart.series, function (series) { - if (series.options.stacking && (series.visible === true || - chart.options.chart.ignoreHiddenSeries === false)) { - series.stackKey = series.type + pick(series.options.stack, ''); - } - }); + var chart = this; + + // reset stacks for each yAxis + each(chart.yAxis, function (axis) { + if (axis.stacks && axis.hasVisibleSeries) { + axis.oldStacks = axis.stacks; + } + }); + + each(chart.series, function (series) { + if (series.options.stacking && (series.visible === true || + chart.options.chart.ignoreHiddenSeries === false)) { + series.stackKey = series.type + pick(series.options.stack, ''); + } + }); }; @@ -197,93 +197,93 @@ Chart.prototype.getStacks = function () { * Build the stacks from top down */ Axis.prototype.buildStacks = function () { - var axisSeries = this.series, - reversedStacks = pick(this.options.reversedStacks, true), - len = axisSeries.length, - i; - if (!this.isXAxis) { - this.usePercentage = false; - i = len; - while (i--) { - axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints(); - } - - // Loop up again to compute percent and stream stack - for (i = 0; i < len; i++) { - axisSeries[i].modifyStacks(); - } - } + var axisSeries = this.series, + reversedStacks = pick(this.options.reversedStacks, true), + len = axisSeries.length, + i; + if (!this.isXAxis) { + this.usePercentage = false; + i = len; + while (i--) { + axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints(); + } + + // Loop up again to compute percent and stream stack + for (i = 0; i < len; i++) { + axisSeries[i].modifyStacks(); + } + } }; Axis.prototype.renderStackTotals = function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - stacks = axis.stacks, - stackTotalGroup = axis.stackTotalGroup; - - // Create a separate group for the stack total labels - if (!stackTotalGroup) { - axis.stackTotalGroup = stackTotalGroup = - renderer.g('stack-labels') - .attr({ - visibility: 'visible', - zIndex: 6 - }) - .add(); - } - - // plotLeft/Top will change when y axis gets wider so we need to translate - // the stackTotalGroup at every render call. See bug #506 and #516 - stackTotalGroup.translate(chart.plotLeft, chart.plotTop); - - // Render each stack total - objectEach(stacks, function (type) { - objectEach(type, function (stack) { - stack.render(stackTotalGroup); - }); - }); + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + stacks = axis.stacks, + stackTotalGroup = axis.stackTotalGroup; + + // Create a separate group for the stack total labels + if (!stackTotalGroup) { + axis.stackTotalGroup = stackTotalGroup = + renderer.g('stack-labels') + .attr({ + visibility: 'visible', + zIndex: 6 + }) + .add(); + } + + // plotLeft/Top will change when y axis gets wider so we need to translate + // the stackTotalGroup at every render call. See bug #506 and #516 + stackTotalGroup.translate(chart.plotLeft, chart.plotTop); + + // Render each stack total + objectEach(stacks, function (type) { + objectEach(type, function (stack) { + stack.render(stackTotalGroup); + }); + }); }; /** * Set all the stacks to initial states and destroy unused ones. */ Axis.prototype.resetStacks = function () { - var axis = this, - stacks = axis.stacks; - if (!axis.isXAxis) { - objectEach(stacks, function (type) { - objectEach(type, function (stack, key) { - // Clean up memory after point deletion (#1044, #4320) - if (stack.touched < axis.stacksTouched) { - stack.destroy(); - delete type[key]; - - // Reset stacks - } else { - stack.total = null; - stack.cumulative = null; - } - }); - }); - } + var axis = this, + stacks = axis.stacks; + if (!axis.isXAxis) { + objectEach(stacks, function (type) { + objectEach(type, function (stack, key) { + // Clean up memory after point deletion (#1044, #4320) + if (stack.touched < axis.stacksTouched) { + stack.destroy(); + delete type[key]; + + // Reset stacks + } else { + stack.total = null; + stack.cumulative = null; + } + }); + }); + } }; Axis.prototype.cleanStacks = function () { - var stacks; - - if (!this.isXAxis) { - if (this.oldStacks) { - stacks = this.stacks = this.oldStacks; - } - - // reset stacks - objectEach(stacks, function (type) { - objectEach(type, function (stack) { - stack.cumulative = stack.total; - }); - }); - } + var stacks; + + if (!this.isXAxis) { + if (this.oldStacks) { + stacks = this.stacks = this.oldStacks; + } + + // reset stacks + objectEach(stacks, function (type) { + objectEach(type, function (stack) { + stack.cumulative = stack.total; + }); + }); + } }; @@ -293,186 +293,186 @@ Axis.prototype.cleanStacks = function () { * Adds series' points value to corresponding stack */ Series.prototype.setStackedPoints = function () { - if (!this.options.stacking || (this.visible !== true && - this.chart.options.chart.ignoreHiddenSeries !== false)) { - return; - } - - var series = this, - xData = series.processedXData, - yData = series.processedYData, - stackedYData = [], - yDataLength = yData.length, - seriesOptions = series.options, - threshold = seriesOptions.threshold, - stackThreshold = pick(seriesOptions.startFromThreshold && threshold, 0), - stackOption = seriesOptions.stack, - stacking = seriesOptions.stacking, - stackKey = series.stackKey, - negKey = '-' + stackKey, - negStacks = series.negStacks, - yAxis = series.yAxis, - stacks = yAxis.stacks, - oldStacks = yAxis.oldStacks, - stackIndicator, - isNegative, - stack, - other, - key, - pointKey, - i, - x, - y; - - - yAxis.stacksTouched += 1; - - // loop over the non-null y values and read them into a local array - for (i = 0; i < yDataLength; i++) { - x = xData[i]; - y = yData[i]; - stackIndicator = series.getStackIndicator( - stackIndicator, - x, - series.index - ); - pointKey = stackIndicator.key; - // Read stacked values into a stack based on the x value, - // the sign of y and the stack key. Stacking is also handled for null - // values (#739) - isNegative = negStacks && y < (stackThreshold ? 0 : threshold); - key = isNegative ? negKey : stackKey; - - // Create empty object for this stack if it doesn't exist yet - if (!stacks[key]) { - stacks[key] = {}; - } - - // Initialize StackItem for this x - if (!stacks[key][x]) { - if (oldStacks[key] && oldStacks[key][x]) { - stacks[key][x] = oldStacks[key][x]; - stacks[key][x].total = null; - } else { - stacks[key][x] = new H.StackItem( - yAxis, - yAxis.options.stackLabels, - isNegative, - x, - stackOption - ); - } - } - - // If the StackItem doesn't exist, create it first - stack = stacks[key][x]; - if (y !== null) { - stack.points[pointKey] = stack.points[series.index] = - [pick(stack.cumulative, stackThreshold)]; - - // Record the base of the stack - if (!defined(stack.cumulative)) { - stack.base = pointKey; - } - stack.touched = yAxis.stacksTouched; - - - // In area charts, if there are multiple points on the same X value, - // let the area fill the full span of those points - if (stackIndicator.index > 0 && series.singleStacks === false) { - stack.points[pointKey][0] = - stack.points[series.index + ',' + x + ',0'][0]; - } - - // When updating to null, reset the point stack (#7493) - } else { - stack.points[pointKey] = stack.points[series.index] = null; - } - - // Add value to the stack total - if (stacking === 'percent') { - - // Percent stacked column, totals are the same for the positive and - // negative stacks - other = isNegative ? stackKey : negKey; - if (negStacks && stacks[other] && stacks[other][x]) { - other = stacks[other][x]; - stack.total = other.total = - Math.max(other.total, stack.total) + Math.abs(y) || 0; - - // Percent stacked areas - } else { - stack.total = correctFloat(stack.total + (Math.abs(y) || 0)); - } - } else { - stack.total = correctFloat(stack.total + (y || 0)); - } - - stack.cumulative = pick(stack.cumulative, stackThreshold) + (y || 0); - - if (y !== null) { - stack.points[pointKey].push(stack.cumulative); - stackedYData[i] = stack.cumulative; - } - - } - - if (stacking === 'percent') { - yAxis.usePercentage = true; - } - - this.stackedYData = stackedYData; // To be used in getExtremes - - // Reset old stacks - yAxis.oldStacks = {}; + if (!this.options.stacking || (this.visible !== true && + this.chart.options.chart.ignoreHiddenSeries !== false)) { + return; + } + + var series = this, + xData = series.processedXData, + yData = series.processedYData, + stackedYData = [], + yDataLength = yData.length, + seriesOptions = series.options, + threshold = seriesOptions.threshold, + stackThreshold = pick(seriesOptions.startFromThreshold && threshold, 0), + stackOption = seriesOptions.stack, + stacking = seriesOptions.stacking, + stackKey = series.stackKey, + negKey = '-' + stackKey, + negStacks = series.negStacks, + yAxis = series.yAxis, + stacks = yAxis.stacks, + oldStacks = yAxis.oldStacks, + stackIndicator, + isNegative, + stack, + other, + key, + pointKey, + i, + x, + y; + + + yAxis.stacksTouched += 1; + + // loop over the non-null y values and read them into a local array + for (i = 0; i < yDataLength; i++) { + x = xData[i]; + y = yData[i]; + stackIndicator = series.getStackIndicator( + stackIndicator, + x, + series.index + ); + pointKey = stackIndicator.key; + // Read stacked values into a stack based on the x value, + // the sign of y and the stack key. Stacking is also handled for null + // values (#739) + isNegative = negStacks && y < (stackThreshold ? 0 : threshold); + key = isNegative ? negKey : stackKey; + + // Create empty object for this stack if it doesn't exist yet + if (!stacks[key]) { + stacks[key] = {}; + } + + // Initialize StackItem for this x + if (!stacks[key][x]) { + if (oldStacks[key] && oldStacks[key][x]) { + stacks[key][x] = oldStacks[key][x]; + stacks[key][x].total = null; + } else { + stacks[key][x] = new H.StackItem( + yAxis, + yAxis.options.stackLabels, + isNegative, + x, + stackOption + ); + } + } + + // If the StackItem doesn't exist, create it first + stack = stacks[key][x]; + if (y !== null) { + stack.points[pointKey] = stack.points[series.index] = + [pick(stack.cumulative, stackThreshold)]; + + // Record the base of the stack + if (!defined(stack.cumulative)) { + stack.base = pointKey; + } + stack.touched = yAxis.stacksTouched; + + + // In area charts, if there are multiple points on the same X value, + // let the area fill the full span of those points + if (stackIndicator.index > 0 && series.singleStacks === false) { + stack.points[pointKey][0] = + stack.points[series.index + ',' + x + ',0'][0]; + } + + // When updating to null, reset the point stack (#7493) + } else { + stack.points[pointKey] = stack.points[series.index] = null; + } + + // Add value to the stack total + if (stacking === 'percent') { + + // Percent stacked column, totals are the same for the positive and + // negative stacks + other = isNegative ? stackKey : negKey; + if (negStacks && stacks[other] && stacks[other][x]) { + other = stacks[other][x]; + stack.total = other.total = + Math.max(other.total, stack.total) + Math.abs(y) || 0; + + // Percent stacked areas + } else { + stack.total = correctFloat(stack.total + (Math.abs(y) || 0)); + } + } else { + stack.total = correctFloat(stack.total + (y || 0)); + } + + stack.cumulative = pick(stack.cumulative, stackThreshold) + (y || 0); + + if (y !== null) { + stack.points[pointKey].push(stack.cumulative); + stackedYData[i] = stack.cumulative; + } + + } + + if (stacking === 'percent') { + yAxis.usePercentage = true; + } + + this.stackedYData = stackedYData; // To be used in getExtremes + + // Reset old stacks + yAxis.oldStacks = {}; }; /** * Iterate over all stacks and compute the absolute values to percent */ Series.prototype.modifyStacks = function () { - var series = this, - stackKey = series.stackKey, - stacks = series.yAxis.stacks, - processedXData = series.processedXData, - stackIndicator, - stacking = series.options.stacking; - - if (series[stacking + 'Stacker']) { // Modifier function exists - each([stackKey, '-' + stackKey], function (key) { - var i = processedXData.length, - x, - stack, - pointExtremes; - - while (i--) { - x = processedXData[i]; - stackIndicator = series.getStackIndicator( - stackIndicator, - x, - series.index, - key - ); - stack = stacks[key] && stacks[key][x]; - pointExtremes = stack && stack.points[stackIndicator.key]; - if (pointExtremes) { - series[stacking + 'Stacker'](pointExtremes, stack, i); - } - } - }); - } + var series = this, + stackKey = series.stackKey, + stacks = series.yAxis.stacks, + processedXData = series.processedXData, + stackIndicator, + stacking = series.options.stacking; + + if (series[stacking + 'Stacker']) { // Modifier function exists + each([stackKey, '-' + stackKey], function (key) { + var i = processedXData.length, + x, + stack, + pointExtremes; + + while (i--) { + x = processedXData[i]; + stackIndicator = series.getStackIndicator( + stackIndicator, + x, + series.index, + key + ); + stack = stacks[key] && stacks[key][x]; + pointExtremes = stack && stack.points[stackIndicator.key]; + if (pointExtremes) { + series[stacking + 'Stacker'](pointExtremes, stack, i); + } + } + }); + } }; /** * Modifier function for percent stacks. Blows up the stack to 100%. */ Series.prototype.percentStacker = function (pointExtremes, stack, i) { - var totalFactor = stack.total ? 100 / stack.total : 0; - // Y bottom value - pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); - // Y value - pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); - this.stackedYData[i] = pointExtremes[1]; + var totalFactor = stack.total ? 100 / stack.total : 0; + // Y bottom value + pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); + // Y value + pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); + this.stackedYData[i] = pointExtremes[1]; }; /** @@ -480,21 +480,21 @@ Series.prototype.percentStacker = function (pointExtremes, stack, i) { * same x-value */ Series.prototype.getStackIndicator = function (stackIndicator, x, index, key) { - // Update stack indicator, when: - // first point in a stack || x changed || stack type (negative vs positive) - // changed: - if (!defined(stackIndicator) || stackIndicator.x !== x || - (key && stackIndicator.key !== key)) { - stackIndicator = { - x: x, - index: 0, - key: key - }; - } else { - stackIndicator.index++; - } - - stackIndicator.key = [index, x, stackIndicator.index].join(','); - - return stackIndicator; + // Update stack indicator, when: + // first point in a stack || x changed || stack type (negative vs positive) + // changed: + if (!defined(stackIndicator) || stackIndicator.x !== x || + (key && stackIndicator.key !== key)) { + stackIndicator = { + x: x, + index: 0, + key: key + }; + } else { + stackIndicator.index++; + } + + stackIndicator.key = [index, x, stackIndicator.index].join(','); + + return stackIndicator; }; diff --git a/js/parts/StockChart.js b/js/parts/StockChart.js index bcc3c947054..3ba7d01eef5 100644 --- a/js/parts/StockChart.js +++ b/js/parts/StockChart.js @@ -13,34 +13,34 @@ import './Pointer.js'; import './Series.js'; import './SvgRenderer.js'; var addEvent = H.addEvent, - arrayMax = H.arrayMax, - arrayMin = H.arrayMin, - Axis = H.Axis, - Chart = H.Chart, - defined = H.defined, - each = H.each, - extend = H.extend, - format = H.format, - grep = H.grep, - inArray = H.inArray, - isNumber = H.isNumber, - isString = H.isString, - map = H.map, - merge = H.merge, - pick = H.pick, - Point = H.Point, - Renderer = H.Renderer, - Series = H.Series, - splat = H.splat, - SVGRenderer = H.SVGRenderer, - VMLRenderer = H.VMLRenderer, - wrap = H.wrap, - - - seriesProto = Series.prototype, - seriesInit = seriesProto.init, - seriesProcessData = seriesProto.processData, - pointTooltipFormatter = Point.prototype.tooltipFormatter; + arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + Axis = H.Axis, + Chart = H.Chart, + defined = H.defined, + each = H.each, + extend = H.extend, + format = H.format, + grep = H.grep, + inArray = H.inArray, + isNumber = H.isNumber, + isString = H.isString, + map = H.map, + merge = H.merge, + pick = H.pick, + Point = H.Point, + Renderer = H.Renderer, + Series = H.Series, + splat = H.splat, + SVGRenderer = H.SVGRenderer, + VMLRenderer = H.VMLRenderer, + wrap = H.wrap, + + + seriesProto = Series.prototype, + seriesInit = seriesProto.init, + seriesProcessData = seriesProto.processData, + pointTooltipFormatter = Point.prototype.tooltipFormatter; /** @@ -49,7 +49,7 @@ var addEvent = H.addEvent, * 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()) @@ -80,7 +80,7 @@ var addEvent = H.addEvent, /** * 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 @@ -93,7 +93,7 @@ var addEvent = H.addEvent, /** * 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 * @@ -124,542 +124,542 @@ var addEvent = H.addEvent, * }); */ H.StockChart = H.stockChart = function (a, b, c) { - var hasRenderToArg = isString(a) || a.nodeName, - options = arguments[hasRenderToArg ? 1 : 0], - // to increase performance, don't merge the data - seriesOptions = options.series, - defaultOptions = H.getOptions(), - opposite, - - // Always disable startOnTick:true on the main axis when the navigator - // is enabled (#1090) - navigatorEnabled = pick( - options.navigator && options.navigator.enabled, - defaultOptions.navigator.enabled, - true - ), - disableStartOnTick = navigatorEnabled ? { - startOnTick: false, - endOnTick: false - } : null, - - lineOptions = { - - marker: { - enabled: false, - radius: 2 - } - // gapSize: 0 - }, - columnOptions = { - shadow: false, - borderWidth: 0 - }; - - // apply X axis options to both single and multi y axes - options.xAxis = map(splat(options.xAxis || {}), function (xAxisOptions, i) { - return merge( - { // defaults - minPadding: 0, - maxPadding: 0, - overscroll: 0, - ordinal: true, - title: { - text: null - }, - labels: { - overflow: 'justify' - }, - showLastLabel: true - }, - defaultOptions.xAxis, // #3802 - defaultOptions.xAxis && defaultOptions.xAxis[i], // #7690 - xAxisOptions, // user options - { // forced options - type: 'datetime', - categories: null - }, - disableStartOnTick - ); - }); - - // apply Y axis options to both single and multi y axes - options.yAxis = map(splat(options.yAxis || {}), function (yAxisOptions, i) { - opposite = pick(yAxisOptions.opposite, true); - return merge({ // defaults - labels: { - y: -2 - }, - opposite: opposite, - - /** - * @default {highcharts} true - * @default {highstock} false - * @apioption yAxis.showLastLabel - */ - showLastLabel: !!( - // #6104, show last label by default for category axes - yAxisOptions.categories || - yAxisOptions.type === 'category' - ), - - title: { - text: null - } - }, - defaultOptions.yAxis, // #3802 - defaultOptions.yAxis && defaultOptions.yAxis[i], // #7690 - yAxisOptions // user options - ); - }); - - options.series = null; - - options = merge( - { - chart: { - panning: true, - pinchType: 'x' - }, - navigator: { - enabled: navigatorEnabled - }, - scrollbar: { - // #4988 - check if setOptions was called - enabled: pick(defaultOptions.scrollbar.enabled, true) - }, - rangeSelector: { - // #4988 - check if setOptions was called - enabled: pick(defaultOptions.rangeSelector.enabled, true) - }, - title: { - text: null - }, - tooltip: { - split: pick(defaultOptions.tooltip.split, true), - crosshairs: true - }, - legend: { - enabled: false - }, - - plotOptions: { - line: lineOptions, - spline: lineOptions, - area: lineOptions, - areaspline: lineOptions, - arearange: lineOptions, - areasplinerange: lineOptions, - column: columnOptions, - columnrange: columnOptions, - candlestick: columnOptions, - ohlc: columnOptions - } - - }, - - options, // user's options - - { // forced options - isStock: true // internal flag - } - ); - - options.series = seriesOptions; - - return hasRenderToArg ? - new Chart(a, options, c) : - new Chart(options, b); + var hasRenderToArg = isString(a) || a.nodeName, + options = arguments[hasRenderToArg ? 1 : 0], + // to increase performance, don't merge the data + seriesOptions = options.series, + defaultOptions = H.getOptions(), + opposite, + + // Always disable startOnTick:true on the main axis when the navigator + // is enabled (#1090) + navigatorEnabled = pick( + options.navigator && options.navigator.enabled, + defaultOptions.navigator.enabled, + true + ), + disableStartOnTick = navigatorEnabled ? { + startOnTick: false, + endOnTick: false + } : null, + + lineOptions = { + + marker: { + enabled: false, + radius: 2 + } + // gapSize: 0 + }, + columnOptions = { + shadow: false, + borderWidth: 0 + }; + + // apply X axis options to both single and multi y axes + options.xAxis = map(splat(options.xAxis || {}), function (xAxisOptions, i) { + return merge( + { // defaults + minPadding: 0, + maxPadding: 0, + overscroll: 0, + ordinal: true, + title: { + text: null + }, + labels: { + overflow: 'justify' + }, + showLastLabel: true + }, + defaultOptions.xAxis, // #3802 + defaultOptions.xAxis && defaultOptions.xAxis[i], // #7690 + xAxisOptions, // user options + { // forced options + type: 'datetime', + categories: null + }, + disableStartOnTick + ); + }); + + // apply Y axis options to both single and multi y axes + options.yAxis = map(splat(options.yAxis || {}), function (yAxisOptions, i) { + opposite = pick(yAxisOptions.opposite, true); + return merge({ // defaults + labels: { + y: -2 + }, + opposite: opposite, + + /** + * @default {highcharts} true + * @default {highstock} false + * @apioption yAxis.showLastLabel + */ + showLastLabel: !!( + // #6104, show last label by default for category axes + yAxisOptions.categories || + yAxisOptions.type === 'category' + ), + + title: { + text: null + } + }, + defaultOptions.yAxis, // #3802 + defaultOptions.yAxis && defaultOptions.yAxis[i], // #7690 + yAxisOptions // user options + ); + }); + + options.series = null; + + options = merge( + { + chart: { + panning: true, + pinchType: 'x' + }, + navigator: { + enabled: navigatorEnabled + }, + scrollbar: { + // #4988 - check if setOptions was called + enabled: pick(defaultOptions.scrollbar.enabled, true) + }, + rangeSelector: { + // #4988 - check if setOptions was called + enabled: pick(defaultOptions.rangeSelector.enabled, true) + }, + title: { + text: null + }, + tooltip: { + split: pick(defaultOptions.tooltip.split, true), + crosshairs: true + }, + legend: { + enabled: false + }, + + plotOptions: { + line: lineOptions, + spline: lineOptions, + area: lineOptions, + areaspline: lineOptions, + arearange: lineOptions, + areasplinerange: lineOptions, + column: columnOptions, + columnrange: columnOptions, + candlestick: columnOptions, + ohlc: columnOptions + } + + }, + + options, // user's options + + { // forced options + isStock: true // internal flag + } + ); + + options.series = seriesOptions; + + return hasRenderToArg ? + new Chart(a, options, c) : + new Chart(options, b); }; // Override the automatic label alignment so that the first Y axis' labels // are drawn on top of the grid line, and subsequent axes are drawn outside wrap(Axis.prototype, 'autoLabelAlign', function (proceed) { - var chart = this.chart, - options = this.options, - panes = chart._labelPanes = chart._labelPanes || {}, - key, - labelOptions = this.options.labels; - if (this.chart.options.isStock && this.coll === 'yAxis') { - key = options.top + ',' + options.height; - // do it only for the first Y axis of each pane - if (!panes[key] && labelOptions.enabled) { - if (labelOptions.x === 15) { // default - labelOptions.x = 0; - } - if (labelOptions.align === undefined) { - labelOptions.align = 'right'; - } - panes[key] = this; - return 'right'; - } - } - return proceed.apply(this, [].slice.call(arguments, 1)); + var chart = this.chart, + options = this.options, + panes = chart._labelPanes = chart._labelPanes || {}, + key, + labelOptions = this.options.labels; + if (this.chart.options.isStock && this.coll === 'yAxis') { + key = options.top + ',' + options.height; + // do it only for the first Y axis of each pane + if (!panes[key] && labelOptions.enabled) { + if (labelOptions.x === 15) { // default + labelOptions.x = 0; + } + if (labelOptions.align === undefined) { + labelOptions.align = 'right'; + } + panes[key] = this; + return 'right'; + } + } + return proceed.apply(this, [].slice.call(arguments, 1)); }); // Clear axis from label panes (#6071) addEvent(Axis, 'destroy', function () { - var chart = this.chart, - key = this.options && (this.options.top + ',' + this.options.height); + var chart = this.chart, + key = this.options && (this.options.top + ',' + this.options.height); - if (key && chart._labelPanes && chart._labelPanes[key] === this) { - delete chart._labelPanes[key]; - } + if (key && chart._labelPanes && chart._labelPanes[key] === this) { + delete chart._labelPanes[key]; + } }); // Override getPlotLinePath to allow for multipane charts wrap(Axis.prototype, 'getPlotLinePath', function ( - proceed, - value, - lineWidth, - old, - force, - translatedValue + proceed, + value, + lineWidth, + old, + force, + translatedValue ) { - var axis = this, - series = ( - this.isLinked && !this.series ? - this.linkedParent.series : - this.series - ), - chart = axis.chart, - renderer = chart.renderer, - axisLeft = axis.left, - axisTop = axis.top, - x1, - y1, - x2, - y2, - result = [], - axes = [], // #3416 need a default array - axes2, - uniqueAxes, - transVal; - - /** - * Return the other axis based on either the axis option or on related - * series. - */ - function getAxis(coll) { - var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis', - opt = axis.options[otherColl]; - - // Other axis indexed by number - if (isNumber(opt)) { - return [chart[otherColl][opt]]; - } - - // Other axis indexed by id (like navigator) - if (isString(opt)) { - return [chart.get(opt)]; - } - - // Auto detect based on existing series - return map(series, function (s) { - return s[otherColl]; - }); - } - - // Ignore in case of colorAxis or zAxis. #3360, #3524, #6720 - if (axis.coll !== 'xAxis' && axis.coll !== 'yAxis') { - return proceed.apply(this, [].slice.call(arguments, 1)); - } - - // Get the related axes based on series - axes = getAxis(axis.coll); - - // Get the related axes based options.*Axis setting #2810 - axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis); - each(axes2, function (A) { - if ( - defined(A.options.id) ? - A.options.id.indexOf('navigator') === -1 : - true - ) { - var a = (A.isXAxis ? 'yAxis' : 'xAxis'), - rax = ( - defined(A.options[a]) ? - chart[a][A.options[a]] : - chart[a][0] - ); - - if (axis === rax) { - axes.push(A); - } - } - }); - - - // Remove duplicates in the axes array. If there are no axes in the axes - // array, we are adding an axis without data, so we need to populate this - // with grid lines (#2796). - uniqueAxes = axes.length ? - [] : - [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742 - each(axes, function (axis2) { - if ( - inArray(axis2, uniqueAxes) === -1 && - // Do not draw on axis which overlap completely. #5424 - !H.find(uniqueAxes, function (unique) { - return unique.pos === axis2.pos && unique.len && axis2.len; - }) - ) { - uniqueAxes.push(axis2); - } - }); - - transVal = pick(translatedValue, axis.translate(value, null, null, old)); - if (isNumber(transVal)) { - if (axis.horiz) { - each(uniqueAxes, function (axis2) { - var skip; - - y1 = axis2.pos; - y2 = y1 + axis2.len; - x1 = x2 = Math.round(transVal + axis.transB); - - // outside plot area - if (x1 < axisLeft || x1 > axisLeft + axis.width) { - if (force) { - x1 = x2 = Math.min( - Math.max(axisLeft, x1), - axisLeft + axis.width - ); - } else { - skip = true; - } - } - if (!skip) { - result.push('M', x1, y1, 'L', x2, y2); - } - }); - } else { - each(uniqueAxes, function (axis2) { - var skip; - - x1 = axis2.pos; - x2 = x1 + axis2.len; - y1 = y2 = Math.round(axisTop + axis.height - transVal); - - // outside plot area - if (y1 < axisTop || y1 > axisTop + axis.height) { - if (force) { - y1 = y2 = Math.min( - Math.max(axisTop, y1), - axis.top + axis.height - ); - } else { - skip = true; - } - } - if (!skip) { - result.push('M', x1, y1, 'L', x2, y2); - } - }); - } - } - return result.length > 0 ? - renderer.crispPolyLine(result, lineWidth || 1) : - null; // #3557 getPlotLinePath in regular Highcharts also returns null + var axis = this, + series = ( + this.isLinked && !this.series ? + this.linkedParent.series : + this.series + ), + chart = axis.chart, + renderer = chart.renderer, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + result = [], + axes = [], // #3416 need a default array + axes2, + uniqueAxes, + transVal; + + /** + * Return the other axis based on either the axis option or on related + * series. + */ + function getAxis(coll) { + var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis', + opt = axis.options[otherColl]; + + // Other axis indexed by number + if (isNumber(opt)) { + return [chart[otherColl][opt]]; + } + + // Other axis indexed by id (like navigator) + if (isString(opt)) { + return [chart.get(opt)]; + } + + // Auto detect based on existing series + return map(series, function (s) { + return s[otherColl]; + }); + } + + // Ignore in case of colorAxis or zAxis. #3360, #3524, #6720 + if (axis.coll !== 'xAxis' && axis.coll !== 'yAxis') { + return proceed.apply(this, [].slice.call(arguments, 1)); + } + + // Get the related axes based on series + axes = getAxis(axis.coll); + + // Get the related axes based options.*Axis setting #2810 + axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis); + each(axes2, function (A) { + if ( + defined(A.options.id) ? + A.options.id.indexOf('navigator') === -1 : + true + ) { + var a = (A.isXAxis ? 'yAxis' : 'xAxis'), + rax = ( + defined(A.options[a]) ? + chart[a][A.options[a]] : + chart[a][0] + ); + + if (axis === rax) { + axes.push(A); + } + } + }); + + + // Remove duplicates in the axes array. If there are no axes in the axes + // array, we are adding an axis without data, so we need to populate this + // with grid lines (#2796). + uniqueAxes = axes.length ? + [] : + [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742 + each(axes, function (axis2) { + if ( + inArray(axis2, uniqueAxes) === -1 && + // Do not draw on axis which overlap completely. #5424 + !H.find(uniqueAxes, function (unique) { + return unique.pos === axis2.pos && unique.len && axis2.len; + }) + ) { + uniqueAxes.push(axis2); + } + }); + + transVal = pick(translatedValue, axis.translate(value, null, null, old)); + if (isNumber(transVal)) { + if (axis.horiz) { + each(uniqueAxes, function (axis2) { + var skip; + + y1 = axis2.pos; + y2 = y1 + axis2.len; + x1 = x2 = Math.round(transVal + axis.transB); + + // outside plot area + if (x1 < axisLeft || x1 > axisLeft + axis.width) { + if (force) { + x1 = x2 = Math.min( + Math.max(axisLeft, x1), + axisLeft + axis.width + ); + } else { + skip = true; + } + } + if (!skip) { + result.push('M', x1, y1, 'L', x2, y2); + } + }); + } else { + each(uniqueAxes, function (axis2) { + var skip; + + x1 = axis2.pos; + x2 = x1 + axis2.len; + y1 = y2 = Math.round(axisTop + axis.height - transVal); + + // outside plot area + if (y1 < axisTop || y1 > axisTop + axis.height) { + if (force) { + y1 = y2 = Math.min( + Math.max(axisTop, y1), + axis.top + axis.height + ); + } else { + skip = true; + } + } + if (!skip) { + result.push('M', x1, y1, 'L', x2, y2); + } + }); + } + } + return result.length > 0 ? + renderer.crispPolyLine(result, lineWidth || 1) : + null; // #3557 getPlotLinePath in regular Highcharts also returns null }); // Function to crisp a line with multiple segments SVGRenderer.prototype.crispPolyLine = function (points, width) { - // points format: ['M', 0, 0, 'L', 100, 0] - // normalize to a crisp line - var i; - for (i = 0; i < points.length; i = i + 6) { - if (points[i + 1] === points[i + 4]) { - // Substract due to #1129. Now bottom and left axis gridlines behave - // the same. - points[i + 1] = points[i + 4] = - Math.round(points[i + 1]) - (width % 2 / 2); - } - if (points[i + 2] === points[i + 5]) { - points[i + 2] = points[i + 5] = - Math.round(points[i + 2]) + (width % 2 / 2); - } - } - return points; + // points format: ['M', 0, 0, 'L', 100, 0] + // normalize to a crisp line + var i; + for (i = 0; i < points.length; i = i + 6) { + if (points[i + 1] === points[i + 4]) { + // Substract due to #1129. Now bottom and left axis gridlines behave + // the same. + points[i + 1] = points[i + 4] = + Math.round(points[i + 1]) - (width % 2 / 2); + } + if (points[i + 2] === points[i + 5]) { + points[i + 2] = points[i + 5] = + Math.round(points[i + 2]) + (width % 2 / 2); + } + } + return points; }; /*= if (build.classic) { =*/ if (Renderer === VMLRenderer) { - VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine; + VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine; } /*= } =*/ // Wrapper to hide the label wrap(Axis.prototype, 'hideCrosshair', function (proceed, i) { - - proceed.call(this, i); - if (this.crossLabel) { - this.crossLabel = this.crossLabel.hide(); - } + proceed.call(this, i); + + if (this.crossLabel) { + this.crossLabel = this.crossLabel.hide(); + } }); // Extend crosshairs to also draw the label addEvent(Axis, 'afterDrawCrosshair', function (event) { - - // Check if the label has to be drawn - if ( - !defined(this.crosshair.label) || - !this.crosshair.label.enabled || - !this.cross - ) { - return; - } - - var chart = this.chart, - options = this.options.crosshair.label, // the label's options - horiz = this.horiz, // axis orientation - opposite = this.opposite, // axis position - left = this.left, // left position - top = this.top, // top position - crossLabel = this.crossLabel, // the svgElement - posx, - posy, - crossBox, - formatOption = options.format, - formatFormat = '', - limit, - align, - tickInside = this.options.tickPosition === 'inside', - snap = this.crosshair.snap !== false, - value, - offset = 0, - // Use last available event (#5287) - e = event.e || (this.cross && this.cross.e), - point = event.point; - - align = (horiz ? 'center' : opposite ? - (this.labelAlign === 'right' ? 'right' : 'left') : - (this.labelAlign === 'left' ? 'left' : 'center')); - - // If the label does not exist yet, create it. - if (!crossLabel) { - crossLabel = this.crossLabel = chart.renderer.label( - null, - null, - null, - options.shape || 'callout' - ) - .addClass('highcharts-crosshair-label' + ( - this.series[0] && - ' highcharts-color-' + this.series[0].colorIndex) - ) - .attr({ - align: options.align || align, - padding: pick(options.padding, 8), - r: pick(options.borderRadius, 3), - zIndex: 2 - }) - .add(this.labelGroup); - - /*= if (build.classic) { =*/ - // Presentational - crossLabel - .attr({ - fill: options.backgroundColor || - (this.series[0] && this.series[0].color) || - '${palette.neutralColor60}', - stroke: options.borderColor || '', - 'stroke-width': options.borderWidth || 0 - }) - .css(extend({ - color: '${palette.backgroundColor}', - fontWeight: 'normal', - fontSize: '11px', - textAlign: 'center' - }, options.style)); - /*= } =*/ - } - - if (horiz) { - posx = snap ? point.plotX + left : e.chartX; - posy = top + (opposite ? 0 : this.height); - } else { - posx = opposite ? this.width + left : 0; - posy = snap ? point.plotY + top : e.chartY; - } - - if (!formatOption && !options.formatter) { - if (this.isDatetimeAxis) { - formatFormat = '%b %d, %Y'; - } - formatOption = - '{value' + (formatFormat ? ':' + formatFormat : '') + '}'; - } - - // Show the label - value = snap ? - point[this.isXAxis ? 'x' : 'y'] : - this.toValue(horiz ? e.chartX : e.chartY); - - crossLabel.attr({ - text: formatOption ? - format(formatOption, { value: value }, chart.time) : - options.formatter.call(this, value), - x: posx, - y: posy, - // Crosshair should be rendered within Axis range (#7219) - visibility: value < this.min || value > this.max ? 'hidden' : 'visible' - }); - - crossBox = crossLabel.getBBox(); - - // now it is placed we can correct its position - if (horiz) { - if ((tickInside && !opposite) || (!tickInside && opposite)) { - posy = crossLabel.y - crossBox.height; - } - } else { - posy = crossLabel.y - (crossBox.height / 2); - } - - // check the edges - if (horiz) { - limit = { - left: left - crossBox.x, - right: left + this.width - crossBox.x - }; - } else { - limit = { - left: this.labelAlign === 'left' ? left : 0, - right: this.labelAlign === 'right' ? - left + this.width : - chart.chartWidth - }; - } - - // left edge - if (crossLabel.translateX < limit.left) { - offset = limit.left - crossLabel.translateX; - } - // right edge - if (crossLabel.translateX + crossBox.width >= limit.right) { - offset = -(crossLabel.translateX + crossBox.width - limit.right); - } - - // show the crosslabel - crossLabel.attr({ - x: posx + offset, - y: posy, - // First set x and y, then anchorX and anchorY, when box is actually - // calculated, #5702 - anchorX: horiz ? - posx : - (this.opposite ? 0 : chart.chartWidth), - anchorY: horiz ? - (this.opposite ? chart.chartHeight : 0) : - posy + crossBox.height / 2 - }); + + // Check if the label has to be drawn + if ( + !defined(this.crosshair.label) || + !this.crosshair.label.enabled || + !this.cross + ) { + return; + } + + var chart = this.chart, + options = this.options.crosshair.label, // the label's options + horiz = this.horiz, // axis orientation + opposite = this.opposite, // axis position + left = this.left, // left position + top = this.top, // top position + crossLabel = this.crossLabel, // the svgElement + posx, + posy, + crossBox, + formatOption = options.format, + formatFormat = '', + limit, + align, + tickInside = this.options.tickPosition === 'inside', + snap = this.crosshair.snap !== false, + value, + offset = 0, + // Use last available event (#5287) + e = event.e || (this.cross && this.cross.e), + point = event.point; + + align = (horiz ? 'center' : opposite ? + (this.labelAlign === 'right' ? 'right' : 'left') : + (this.labelAlign === 'left' ? 'left' : 'center')); + + // If the label does not exist yet, create it. + if (!crossLabel) { + crossLabel = this.crossLabel = chart.renderer.label( + null, + null, + null, + options.shape || 'callout' + ) + .addClass('highcharts-crosshair-label' + ( + this.series[0] && + ' highcharts-color-' + this.series[0].colorIndex) + ) + .attr({ + align: options.align || align, + padding: pick(options.padding, 8), + r: pick(options.borderRadius, 3), + zIndex: 2 + }) + .add(this.labelGroup); + + /*= if (build.classic) { =*/ + // Presentational + crossLabel + .attr({ + fill: options.backgroundColor || + (this.series[0] && this.series[0].color) || + '${palette.neutralColor60}', + stroke: options.borderColor || '', + 'stroke-width': options.borderWidth || 0 + }) + .css(extend({ + color: '${palette.backgroundColor}', + fontWeight: 'normal', + fontSize: '11px', + textAlign: 'center' + }, options.style)); + /*= } =*/ + } + + if (horiz) { + posx = snap ? point.plotX + left : e.chartX; + posy = top + (opposite ? 0 : this.height); + } else { + posx = opposite ? this.width + left : 0; + posy = snap ? point.plotY + top : e.chartY; + } + + if (!formatOption && !options.formatter) { + if (this.isDatetimeAxis) { + formatFormat = '%b %d, %Y'; + } + formatOption = + '{value' + (formatFormat ? ':' + formatFormat : '') + '}'; + } + + // Show the label + value = snap ? + point[this.isXAxis ? 'x' : 'y'] : + this.toValue(horiz ? e.chartX : e.chartY); + + crossLabel.attr({ + text: formatOption ? + format(formatOption, { value: value }, chart.time) : + options.formatter.call(this, value), + x: posx, + y: posy, + // Crosshair should be rendered within Axis range (#7219) + visibility: value < this.min || value > this.max ? 'hidden' : 'visible' + }); + + crossBox = crossLabel.getBBox(); + + // now it is placed we can correct its position + if (horiz) { + if ((tickInside && !opposite) || (!tickInside && opposite)) { + posy = crossLabel.y - crossBox.height; + } + } else { + posy = crossLabel.y - (crossBox.height / 2); + } + + // check the edges + if (horiz) { + limit = { + left: left - crossBox.x, + right: left + this.width - crossBox.x + }; + } else { + limit = { + left: this.labelAlign === 'left' ? left : 0, + right: this.labelAlign === 'right' ? + left + this.width : + chart.chartWidth + }; + } + + // left edge + if (crossLabel.translateX < limit.left) { + offset = limit.left - crossLabel.translateX; + } + // right edge + if (crossLabel.translateX + crossBox.width >= limit.right) { + offset = -(crossLabel.translateX + crossBox.width - limit.right); + } + + // show the crosslabel + crossLabel.attr({ + x: posx + offset, + y: posy, + // First set x and y, then anchorX and anchorY, when box is actually + // calculated, #5702 + anchorX: horiz ? + posx : + (this.opposite ? 0 : chart.chartWidth), + anchorY: horiz ? + (this.opposite ? chart.chartHeight : 0) : + posy + crossBox.height / 2 + }); }); /* **************************************************************************** - * Start value compare logic * + * Start value compare logic * *****************************************************************************/ - + /** * Extend series.init by adding a method to modify the y value used for plotting * on the y axis. This method is called both from the axis when finding dataMin @@ -667,11 +667,11 @@ addEvent(Axis, 'afterDrawCrosshair', function (event) { */ seriesProto.init = function () { - // Call base method - seriesInit.apply(this, arguments); + // Call base method + seriesInit.apply(this, arguments); - // Set comparison mode - this.setCompare(this.options.compare); + // Set comparison mode + this.setCompare(this.options.compare); }; /** @@ -689,43 +689,43 @@ seriesProto.init = function () { */ seriesProto.setCompare = function (compare) { - // Set or unset the modifyValue method - this.modifyValue = (compare === 'value' || compare === 'percent') ? - function (value, point) { - var compareValue = this.compareValue; - - if ( - value !== undefined && - compareValue !== undefined - ) { // #2601, #5814 - - // Get the modified value - if (compare === 'value') { - value -= compareValue; - - // Compare percent - } else { - value = 100 * (value / compareValue) - - (this.options.compareBase === 100 ? 0 : 100); - } - - // record for tooltip etc. - if (point) { - point.change = value; - } - - return value; - } - } : - null; - - // Survive to export, #5485 - this.userOptions.compare = compare; - - // Mark dirty - if (this.chart.hasRendered) { - this.isDirty = true; - } + // Set or unset the modifyValue method + this.modifyValue = (compare === 'value' || compare === 'percent') ? + function (value, point) { + var compareValue = this.compareValue; + + if ( + value !== undefined && + compareValue !== undefined + ) { // #2601, #5814 + + // Get the modified value + if (compare === 'value') { + value -= compareValue; + + // Compare percent + } else { + value = 100 * (value / compareValue) - + (this.options.compareBase === 100 ? 0 : 100); + } + + // record for tooltip etc. + if (point) { + point.change = value; + } + + return value; + } + } : + null; + + // Survive to export, #5485 + this.userOptions.compare = compare; + + // Mark dirty + if (this.chart.hasRendered) { + this.isDirty = true; + } }; @@ -734,71 +734,71 @@ seriesProto.setCompare = function (compare) { * used for comparing the following values */ seriesProto.processData = function () { - var series = this, - i, - keyIndex = -1, - processedXData, - processedYData, - compareStart = series.options.compareStart === true ? 0 : 1, - length, - compareValue; - - // call base method - seriesProcessData.apply(this, arguments); - - if (series.xAxis && series.processedYData) { // not pies - - // local variables - processedXData = series.processedXData; - processedYData = series.processedYData; - length = processedYData.length; - - // For series with more than one value (range, OHLC etc), compare - // against close or the pointValKey (#4922, #3112) - if (series.pointArrayMap) { - // Use close if present (#3112) - keyIndex = inArray('close', series.pointArrayMap); - if (keyIndex === -1) { - keyIndex = inArray( - series.pointValKey || 'y', - series.pointArrayMap - ); - } - } - - // find the first value for comparison - for (i = 0; i < length - compareStart; i++) { - compareValue = processedYData[i] && keyIndex > -1 ? - processedYData[i][keyIndex] : - processedYData[i]; - if ( - isNumber(compareValue) && - processedXData[i + compareStart] >= series.xAxis.min && - compareValue !== 0 - ) { - series.compareValue = compareValue; - break; - } - } - } + var series = this, + i, + keyIndex = -1, + processedXData, + processedYData, + compareStart = series.options.compareStart === true ? 0 : 1, + length, + compareValue; + + // call base method + seriesProcessData.apply(this, arguments); + + if (series.xAxis && series.processedYData) { // not pies + + // local variables + processedXData = series.processedXData; + processedYData = series.processedYData; + length = processedYData.length; + + // For series with more than one value (range, OHLC etc), compare + // against close or the pointValKey (#4922, #3112) + if (series.pointArrayMap) { + // Use close if present (#3112) + keyIndex = inArray('close', series.pointArrayMap); + if (keyIndex === -1) { + keyIndex = inArray( + series.pointValKey || 'y', + series.pointArrayMap + ); + } + } + + // find the first value for comparison + for (i = 0; i < length - compareStart; i++) { + compareValue = processedYData[i] && keyIndex > -1 ? + processedYData[i][keyIndex] : + processedYData[i]; + if ( + isNumber(compareValue) && + processedXData[i + compareStart] >= series.xAxis.min && + compareValue !== 0 + ) { + series.compareValue = compareValue; + break; + } + } + } }; /** * Modify series extremes */ wrap(seriesProto, 'getExtremes', function (proceed) { - var extremes; - - proceed.apply(this, [].slice.call(arguments, 1)); - - if (this.modifyValue) { - extremes = [ - this.modifyValue(this.dataMin), - this.modifyValue(this.dataMax) - ]; - this.dataMin = arrayMin(extremes); - this.dataMax = arrayMax(extremes); - } + var extremes; + + proceed.apply(this, [].slice.call(arguments, 1)); + + if (this.modifyValue) { + extremes = [ + this.modifyValue(this.dataMin), + this.modifyValue(this.dataMax) + ]; + this.dataMin = arrayMin(extremes); + this.dataMax = arrayMax(extremes); + } }); /** @@ -821,14 +821,14 @@ wrap(seriesProto, 'getExtremes', function (proceed) { * Set compoare */ Axis.prototype.setCompare = function (compare, redraw) { - if (!this.isXAxis) { - each(this.series, function (series) { - series.setCompare(compare); - }); - if (pick(redraw, true)) { - this.chart.redraw(); - } - } + if (!this.isXAxis) { + each(this.series, function (series) { + series.setCompare(compare); + }); + if (pick(redraw, true)) { + this.chart.redraw(); + } + } }; /** @@ -836,21 +836,21 @@ Axis.prototype.setCompare = function (compare, redraw) { * as well as the changeDecimals option */ Point.prototype.tooltipFormatter = function (pointFormat) { - var point = this; - - pointFormat = pointFormat.replace( - '{point.change}', - (point.change > 0 ? '+' : '') + H.numberFormat( - point.change, - pick(point.series.tooltipOptions.changeDecimals, 2) - ) - ); - - return pointTooltipFormatter.apply(this, [pointFormat]); + var point = this; + + pointFormat = pointFormat.replace( + '{point.change}', + (point.change > 0 ? '+' : '') + H.numberFormat( + point.change, + pick(point.series.tooltipOptions.changeDecimals, 2) + ) + ); + + return pointTooltipFormatter.apply(this, [pointFormat]); }; /* **************************************************************************** - * End value compare logic * + * End value compare logic * *****************************************************************************/ @@ -860,58 +860,58 @@ Point.prototype.tooltipFormatter = function (pointFormat) { * this feature (#2754). */ wrap(Series.prototype, 'render', function (proceed) { - // Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only - // if the series type handles clipping in the animate method (#2975). - if ( - !(this.chart.is3d && this.chart.is3d()) && - !this.chart.polar && - this.xAxis && - !this.xAxis.isRadial // Gauge, #6192 - ) { - - // First render, initial clip box - if (!this.clipBox && this.animate) { - this.clipBox = merge(this.chart.clipBox); - this.clipBox.width = this.xAxis.len; - this.clipBox.height = this.yAxis.len; - - // On redrawing, resizing etc, update the clip rectangle - } else if (this.chart[this.sharedClipKey]) { - this.chart[this.sharedClipKey].attr({ - width: this.xAxis.len, - height: this.yAxis.len - }); - // #3111 - } else if (this.clipBox) { - this.clipBox.width = this.xAxis.len; - this.clipBox.height = this.yAxis.len; - } - } - proceed.call(this); + // Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only + // if the series type handles clipping in the animate method (#2975). + if ( + !(this.chart.is3d && this.chart.is3d()) && + !this.chart.polar && + this.xAxis && + !this.xAxis.isRadial // Gauge, #6192 + ) { + + // First render, initial clip box + if (!this.clipBox && this.animate) { + this.clipBox = merge(this.chart.clipBox); + this.clipBox.width = this.xAxis.len; + this.clipBox.height = this.yAxis.len; + + // On redrawing, resizing etc, update the clip rectangle + } else if (this.chart[this.sharedClipKey]) { + this.chart[this.sharedClipKey].attr({ + width: this.xAxis.len, + height: this.yAxis.len + }); + // #3111 + } else if (this.clipBox) { + this.clipBox.width = this.xAxis.len; + this.clipBox.height = this.yAxis.len; + } + } + proceed.call(this); }); wrap(Chart.prototype, 'getSelectedPoints', function (proceed) { - var points = proceed.call(this); - - each(this.series, function (serie) { - // series.points - for grouped points (#6445) - if (serie.hasGroupedData) { - points = points.concat(grep(serie.points || [], function (point) { - return point.selected; - })); - } - }); - return points; + var points = proceed.call(this); + + each(this.series, function (serie) { + // series.points - for grouped points (#6445) + if (serie.hasGroupedData) { + points = points.concat(grep(serie.points || [], function (point) { + return point.selected; + })); + } + }); + return points; }); addEvent(Chart, 'update', function (e) { - var options = e.options; - // Use case: enabling scrollbar from a disabled state. - // Scrollbar needs to be initialized from a controller, Navigator in this - // case (#6615) - if ('scrollbar' in options && this.navigator) { - merge(true, this.options.scrollbar, options.scrollbar); - this.navigator.update({}, false); - delete options.scrollbar; - } + var options = e.options; + // Use case: enabling scrollbar from a disabled state. + // Scrollbar needs to be initialized from a controller, Navigator in this + // case (#6615) + if ('scrollbar' in options && this.navigator) { + merge(true, this.options.scrollbar, options.scrollbar); + this.navigator.update({}, false); + delete options.scrollbar; + } }); diff --git a/js/parts/SvgRenderer.js b/js/parts/SvgRenderer.js index a55ff24bc00..3ebd59a2f4f 100644 --- a/js/parts/SvgRenderer.js +++ b/js/parts/SvgRenderer.js @@ -9,43 +9,43 @@ import H from './Globals.js'; import './Utilities.js'; import './Color.js'; var SVGElement, - SVGRenderer, - - addEvent = H.addEvent, - animate = H.animate, - attr = H.attr, - charts = H.charts, - color = H.color, - css = H.css, - createElement = H.createElement, - defined = H.defined, - deg2rad = H.deg2rad, - destroyObjectProperties = H.destroyObjectProperties, - doc = H.doc, - each = H.each, - extend = H.extend, - erase = H.erase, - grep = H.grep, - hasTouch = H.hasTouch, - inArray = H.inArray, - isArray = H.isArray, - isFirefox = H.isFirefox, - isMS = H.isMS, - isObject = H.isObject, - isString = H.isString, - isWebKit = H.isWebKit, - merge = H.merge, - noop = H.noop, - objectEach = H.objectEach, - pick = H.pick, - pInt = H.pInt, - removeEvent = H.removeEvent, - splat = H.splat, - stop = H.stop, - svg = H.svg, - SVG_NS = H.SVG_NS, - symbolSizes = H.symbolSizes, - win = H.win; + SVGRenderer, + + addEvent = H.addEvent, + animate = H.animate, + attr = H.attr, + charts = H.charts, + color = H.color, + css = H.css, + createElement = H.createElement, + defined = H.defined, + deg2rad = H.deg2rad, + destroyObjectProperties = H.destroyObjectProperties, + doc = H.doc, + each = H.each, + extend = H.extend, + erase = H.erase, + grep = H.grep, + hasTouch = H.hasTouch, + inArray = H.inArray, + isArray = H.isArray, + isFirefox = H.isFirefox, + isMS = H.isMS, + isObject = H.isObject, + isString = H.isString, + isWebKit = H.isWebKit, + merge = H.merge, + noop = H.noop, + objectEach = H.objectEach, + pick = H.pick, + pInt = H.pInt, + removeEvent = H.removeEvent, + splat = H.splat, + stop = H.stop, + svg = H.svg, + SVG_NS = H.SVG_NS, + symbolSizes = H.symbolSizes, + win = H.win; /** * @typedef {Object} SVGDOMElement - An SVG DOM element. @@ -58,7 +58,7 @@ var SVGElement, * SVGElement can also wrap HTML labels, when `text` or `label` elements are * created with the `useHTML` parameter. * - * The SVGElement instances are created through factory functions on the + * The SVGElement instances are created through factory functions on the * {@link Highcharts.SVGRenderer} object, like * [rect]{@link Highcharts.SVGRenderer#rect}, [path]{@link * Highcharts.SVGRenderer#path}, [text]{@link Highcharts.SVGRenderer#text}, @@ -68,1835 +68,1835 @@ var SVGElement, * @class Highcharts.SVGElement */ SVGElement = H.SVGElement = function () { - return this; + return this; }; extend(SVGElement.prototype, /** @lends Highcharts.SVGElement.prototype */ { - // Default base for animation - opacity: 1, - SVG_NS: SVG_NS, - - /** - * For labels, these CSS properties are applied to the `text` node directly. - * - * @private - * @type {Array.} - */ - textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', - 'fontStyle', 'color', 'lineHeight', 'width', 'textAlign', - 'textDecoration', 'textOverflow', 'textOutline'], - - /** - * Initialize the SVG element. This function only exists to make the - * initiation process overridable. It should not be called directly. - * - * @param {SVGRenderer} renderer - * The SVGRenderer instance to initialize to. - * @param {String} nodeName - * The SVG node name. - * - */ - init: function (renderer, nodeName) { - - /** - * The primary DOM node. Each `SVGElement` instance wraps a main DOM - * node, but may also represent more nodes. - * - * @name element - * @memberOf SVGElement - * @type {SVGDOMNode|HTMLDOMNode} - */ - this.element = nodeName === 'span' ? - createElement(nodeName) : - doc.createElementNS(this.SVG_NS, nodeName); - - /** - * The renderer that the SVGElement belongs to. - * - * @name renderer - * @memberOf SVGElement - * @type {SVGRenderer} - */ - this.renderer = renderer; - }, - - /** - * Animate to given attributes or CSS properties. - * - * @param {SVGAttributes} params SVG attributes or CSS to animate. - * @param {AnimationOptions} [options] Animation options. - * @param {Function} [complete] Function to perform at the end of animation. - * - * @sample highcharts/members/element-on/ - * Setting some attributes by animation - * - * @returns {SVGElement} Returns the SVGElement for chaining. - */ - animate: function (params, options, complete) { - var animOptions = H.animObject( - pick(options, this.renderer.globalAnimation, true) - ); - if (animOptions.duration !== 0) { - // allows using a callback with the global animation without - // overwriting it - if (complete) { - animOptions.complete = complete; - } - animate(this, params, animOptions); - } else { - this.attr(params, null, complete); - if (animOptions.step) { - animOptions.step.call(this); - } - } - return this; - }, - - /** - * @typedef {Object} GradientOptions - * @property {Object} linearGradient Holds an object that defines the start - * position and the end position relative to the shape. - * @property {Number} linearGradient.x1 Start horizontal position of the - * gradient. Ranges 0-1. - * @property {Number} linearGradient.x2 End horizontal position of the - * gradient. Ranges 0-1. - * @property {Number} linearGradient.y1 Start vertical position of the - * gradient. Ranges 0-1. - * @property {Number} linearGradient.y2 End vertical position of the - * gradient. Ranges 0-1. - * @property {Object} radialGradient Holds an object that defines the center - * position and the radius. - * @property {Number} radialGradient.cx Center horizontal position relative - * to the shape. Ranges 0-1. - * @property {Number} radialGradient.cy Center vertical position relative - * to the shape. Ranges 0-1. - * @property {Number} radialGradient.r Radius relative to the shape. Ranges - * 0-1. - * @property {Array.} stops The first item in each tuple is the - * position in the gradient, where 0 is the start of the gradient and 1 - * is the end of the gradient. Multiple stops can be applied. The second - * item is the color for each stop. This color can also be given in the - * rgba format. - * - * @example - * // Linear gradient used as a color option - * color: { - * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 }, - * stops: [ - * [0, '#003399'], // start - * [0.5, '#ffffff'], // middle - * [1, '#3366AA'] // end - * ] - * } - * } - */ - /** - * Build and apply an SVG gradient out of a common JavaScript configuration - * object. This function is called from the attribute setters. An event - * hook is added for supporting other complex color types. - * - * @private - * @param {GradientOptions} color The gradient options structure. - * @param {string} prop The property to apply, can either be `fill` or - * `stroke`. - * @param {SVGDOMElement} elem SVG DOM element to apply the gradient on. - */ - complexColor: function (color, prop, elem) { - var renderer = this.renderer, - colorObject, - gradName, - gradAttr, - radAttr, - gradients, - gradientObject, - stops, - stopColor, - stopOpacity, - radialReference, - id, - key = [], - value; - - H.fireEvent(this.renderer, 'complexColor', { - args: arguments - }, function () { - // Apply linear or radial gradients - if (color.radialGradient) { - gradName = 'radialGradient'; - } else if (color.linearGradient) { - gradName = 'linearGradient'; - } - - if (gradName) { - gradAttr = color[gradName]; - gradients = renderer.gradients; - stops = color.stops; - radialReference = elem.radialReference; - - // Keep < 2.2 kompatibility - if (isArray(gradAttr)) { - color[gradName] = gradAttr = { - x1: gradAttr[0], - y1: gradAttr[1], - x2: gradAttr[2], - y2: gradAttr[3], - gradientUnits: 'userSpaceOnUse' - }; - } - - // Correct the radial gradient for the radial reference system - if ( - gradName === 'radialGradient' && - radialReference && - !defined(gradAttr.gradientUnits) - ) { - // Save the radial attributes for updating - radAttr = gradAttr; - gradAttr = merge( - gradAttr, - renderer.getRadialAttr(radialReference, radAttr), - { gradientUnits: 'userSpaceOnUse' } - ); - } - - // Build the unique key to detect whether we need to create a - // new element (#1282) - objectEach(gradAttr, function (val, n) { - if (n !== 'id') { - key.push(n, val); - } - }); - objectEach(stops, function (val) { - key.push(val); - }); - key = key.join(','); - - // Check if a gradient object with the same config object is - // created within this renderer - if (gradients[key]) { - id = gradients[key].attr('id'); - - } else { - - // Set the id and create the element - gradAttr.id = id = H.uniqueKey(); - gradients[key] = gradientObject = - renderer.createElement(gradName) - .attr(gradAttr) - .add(renderer.defs); - - gradientObject.radAttr = radAttr; - - // The gradient needs to keep a list of stops to be able to - // destroy them - gradientObject.stops = []; - each(stops, function (stop) { - var stopObject; - if (stop[1].indexOf('rgba') === 0) { - colorObject = H.color(stop[1]); - stopColor = colorObject.get('rgb'); - stopOpacity = colorObject.get('a'); - } else { - stopColor = stop[1]; - stopOpacity = 1; - } - stopObject = renderer.createElement('stop').attr({ - offset: stop[0], - 'stop-color': stopColor, - 'stop-opacity': stopOpacity - }).add(gradientObject); - - // Add the stop element to the gradient - gradientObject.stops.push(stopObject); - }); - } - - // Set the reference to the gradient object - value = 'url(' + renderer.url + '#' + id + ')'; - elem.setAttribute(prop, value); - elem.gradient = key; - - // Allow the color to be concatenated into tooltips formatters - // etc. (#2995) - color.toString = function () { - return value; - }; - } - }); - }, - - /** - * Apply a text outline through a custom CSS property, by copying the text - * element and apply stroke to the copy. Used internally. Contrast checks - * at http://jsfiddle.net/highcharts/43soe9m1/2/ . - * - * @private - * @param {String} textOutline A custom CSS `text-outline` setting, defined - * by `width color`. - * @example - * // Specific color - * text.css({ - * textOutline: '1px black' - * }); - * // Automatic contrast - * text.css({ - * color: '#000000', // black text - * textOutline: '1px contrast' // => white outline - * }); - */ - applyTextOutline: function (textOutline) { - var elem = this.element, - tspans, - tspan, - hasContrast = textOutline.indexOf('contrast') !== -1, - styles = {}, - color, - strokeWidth, - firstRealChild, - i; - - // When the text shadow is set to contrast, use dark stroke for light - // text and vice versa. - if (hasContrast) { - styles.textOutline = textOutline = textOutline.replace( - /contrast/g, - this.renderer.getContrast(elem.style.fill) - ); - } - - // Extract the stroke width and color - textOutline = textOutline.split(' '); - color = textOutline[textOutline.length - 1]; - strokeWidth = textOutline[0]; - - if (strokeWidth && strokeWidth !== 'none' && H.svg) { - - this.fakeTS = true; // Fake text shadow - - tspans = [].slice.call(elem.getElementsByTagName('tspan')); - - // In order to get the right y position of the clone, - // copy over the y setter - this.ySetter = this.xSetter; - - // Since the stroke is applied on center of the actual outline, we - // need to double it to get the correct stroke-width outside the - // glyphs. - strokeWidth = strokeWidth.replace( - /(^[\d\.]+)(.*?)$/g, - function (match, digit, unit) { - return (2 * digit) + unit; - } - ); - - // Remove shadows from previous runs. Iterate from the end to - // support removing items inside the cycle (#6472). - i = tspans.length; - while (i--) { - tspan = tspans[i]; - if (tspan.getAttribute('class') === 'highcharts-text-outline') { - // Remove then erase - erase(tspans, elem.removeChild(tspan)); - } - } - - // For each of the tspans, create a stroked copy behind it. - firstRealChild = elem.firstChild; - each(tspans, function (tspan, y) { - var clone; - - // Let the first line start at the correct X position - if (y === 0) { - tspan.setAttribute('x', elem.getAttribute('x')); - y = elem.getAttribute('y'); - tspan.setAttribute('y', y || 0); - if (y === null) { - elem.setAttribute('y', 0); - } - } - - // Create the clone and apply outline properties - clone = tspan.cloneNode(1); - attr(clone, { - 'class': 'highcharts-text-outline', - 'fill': color, - 'stroke': color, - 'stroke-width': strokeWidth, - 'stroke-linejoin': 'round' - }); - elem.insertBefore(clone, firstRealChild); - }); - } - }, - - /** - * - * @typedef {Object} SVGAttributes An object of key-value pairs for SVG - * attributes. Attributes in Highcharts elements for the most parts - * correspond to SVG, but some are specific to Highcharts, like `zIndex`, - * `rotation`, `rotationOriginX`, `rotationOriginY`, `translateX`, - * `translateY`, `scaleX` and `scaleY`. SVG attributes containing a hyphen - * are _not_ camel-cased, they should be quoted to preserve the hyphen. - * - * @example - * { - * 'stroke': '#ff0000', // basic - * 'stroke-width': 2, // hyphenated - * 'rotation': 45 // custom - * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format - * } - */ - /** - * Apply native and custom attributes to the SVG elements. - * - * In order to set the rotation center for rotation, set x and y to 0 and - * use `translateX` and `translateY` attributes to position the element - * instead. - * - * Attributes frequently used in Highcharts are `fill`, `stroke`, - * `stroke-width`. - * - * @param {SVGAttributes|String} hash - The native and custom SVG - * attributes. - * @param {string} [val] - If the type of the first argument is `string`, - * the second can be a value, which will serve as a single attribute - * setter. If the first argument is a string and the second is undefined, - * the function serves as a getter and the current value of the property - * is returned. - * @param {Function} [complete] - A callback function to execute after - * setting the attributes. This makes the function compliant and - * interchangeable with the {@link SVGElement#animate} function. - * @param {boolean} [continueAnimation=true] Used internally when `.attr` is - * called as part of an animation step. Otherwise, calling `.attr` for an - * attribute will stop animation for that attribute. - * - * @returns {SVGElement|string|number} If used as a setter, it returns the - * current {@link SVGElement} so the calls can be chained. If used as a - * getter, the current value of the attribute is returned. - * - * @sample highcharts/members/renderer-rect/ - * Setting some attributes - * - * @example - * // Set multiple attributes - * element.attr({ - * stroke: 'red', - * fill: 'blue', - * x: 10, - * y: 10 - * }); - * - * // Set a single attribute - * element.attr('stroke', 'red'); - * - * // Get an attribute - * element.attr('stroke'); // => 'red' - * - */ - attr: function (hash, val, complete, continueAnimation) { - var key, - element = this.element, - hasSetSymbolSize, - ret = this, - skipAttr, - setter; - - // single key-value pair - if (typeof hash === 'string' && val !== undefined) { - key = hash; - hash = {}; - hash[key] = val; - } - - // used as a getter: first argument is a string, second is undefined - if (typeof hash === 'string') { - ret = (this[hash + 'Getter'] || this._defaultGetter).call( - this, - hash, - element - ); - - // setter - } else { - - objectEach(hash, function eachAttribute(val, key) { - skipAttr = false; - - // Unless .attr is from the animator update, stop current - // running animation of this property - if (!continueAnimation) { - stop(this, key); - } - - // Special handling of symbol attributes - if ( - this.symbolName && - /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)$/ - .test(key) - ) { - if (!hasSetSymbolSize) { - this.symbolAttr(hash); - hasSetSymbolSize = true; - } - skipAttr = true; - } - - if (this.rotation && (key === 'x' || key === 'y')) { - this.doTransform = true; - } - - if (!skipAttr) { - setter = this[key + 'Setter'] || this._defaultSetter; - setter.call(this, val, key, element); - - /*= if (build.classic) { =*/ - // Let the shadow follow the main element - if ( - this.shadows && - /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/ - .test(key) - ) { - this.updateShadows(key, val, setter); - } - /*= } =*/ - } - }, this); - - this.afterSetters(); - } - - // In accordance with animate, run a complete callback - if (complete) { - complete.call(this); - } - - return ret; - }, - - /** - * This method is executed in the end of `attr()`, after setting all - * attributes in the hash. In can be used to efficiently consolidate - * multiple attributes in one SVG property -- e.g., translate, rotate and - * scale are merged in one "transform" attribute in the SVG node. - * - * @private - */ - afterSetters: function () { - // Update transform. Do this outside the loop to prevent redundant - // updating for batch setting of attributes. - if (this.doTransform) { - this.updateTransform(); - this.doTransform = false; - } - }, - - /*= if (build.classic) { =*/ - /** - * Update the shadow elements with new attributes. - * - * @private - * @param {String} key - The attribute name. - * @param {String|Number} value - The value of the attribute. - * @param {Function} setter - The setter function, inherited from the - * parent wrapper - * - */ - updateShadows: function (key, value, setter) { - var shadows = this.shadows, - i = shadows.length; - - while (i--) { - setter.call( - shadows[i], - key === 'height' ? - Math.max(value - (shadows[i].cutHeight || 0), 0) : - key === 'd' ? this.d : value, - key, - shadows[i] - ); - } - }, - /*= } =*/ - - /** - * Add a class name to an element. - * - * @param {string} className - The new class name to add. - * @param {boolean} [replace=false] - When true, the existing class name(s) - * will be overwritten with the new one. When false, the new one is - * added. - * @returns {SVGElement} Return the SVG element for chainability. - */ - addClass: function (className, replace) { - var currentClassName = this.attr('class') || ''; - if (currentClassName.indexOf(className) === -1) { - if (!replace) { - className = - (currentClassName + (currentClassName ? ' ' : '') + - className).replace(' ', ' '); - } - this.attr('class', className); - } - - return this; - }, - - /** - * Check if an element has the given class name. - * @param {string} className - * The class name to check for. - * @return {Boolean} - * Whether the class name is found. - */ - hasClass: function (className) { - return inArray( - className, - (this.attr('class') || '').split(' ') - ) !== -1; - }, - - /** - * Remove a class name from the element. - * @param {String|RegExp} className The class name to remove. - * @return {SVGElement} Returns the SVG element for chainability. - */ - removeClass: function (className) { - return this.attr( - 'class', - (this.attr('class') || '').replace(className, '') - ); - }, - - /** - * If one of the symbol size affecting parameters are changed, - * check all the others only once for each call to an element's - * .attr() method - * @param {Object} hash - The attributes to set. - * @private - */ - symbolAttr: function (hash) { - var wrapper = this; - - each([ - 'x', - 'y', - 'r', - 'start', - 'end', - 'width', - 'height', - 'innerR', - 'anchorX', - 'anchorY' - ], function (key) { - wrapper[key] = pick(hash[key], wrapper[key]); - }); - - wrapper.attr({ - d: wrapper.renderer.symbols[wrapper.symbolName]( - wrapper.x, - wrapper.y, - wrapper.width, - wrapper.height, - wrapper - ) - }); - }, - - /** - * Apply a clipping rectangle to this element. - * - * @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the - * current clip is removed. - * @returns {SVGElement} Returns the SVG element to allow chaining. - */ - clip: function (clipRect) { - return this.attr( - 'clip-path', - clipRect ? - 'url(' + this.renderer.url + '#' + clipRect.id + ')' : - 'none' - ); - }, - - /** - * Calculate the coordinates needed for drawing a rectangle crisply and - * return the calculated attributes. - * - * @param {Object} rect - A rectangle. - * @param {number} rect.x - The x position. - * @param {number} rect.y - The y position. - * @param {number} rect.width - The width. - * @param {number} rect.height - The height. - * @param {number} [strokeWidth] - The stroke width to consider when - * computing crisp positioning. It can also be set directly on the rect - * parameter. - * - * @returns {{x: Number, y: Number, width: Number, height: Number}} The - * modified rectangle arguments. - */ - crisp: function (rect, strokeWidth) { - - var wrapper = this, - normalizer; - - strokeWidth = strokeWidth || rect.strokeWidth || 0; - // Math.round because strokeWidth can sometimes have roundoff errors - normalizer = Math.round(strokeWidth) % 2 / 2; - - // normalize for crisp edges - rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer; - rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer; - rect.width = Math.floor( - (rect.width || wrapper.width || 0) - 2 * normalizer - ); - rect.height = Math.floor( - (rect.height || wrapper.height || 0) - 2 * normalizer - ); - if (defined(rect.strokeWidth)) { - rect.strokeWidth = strokeWidth; - } - return rect; - }, - - /** - * Set styles for the element. In addition to CSS styles supported by - * native SVG and HTML elements, there are also some custom made for - * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text - * elements. - * @param {CSSObject} styles The new CSS styles. - * @returns {SVGElement} Return the SVG element for chaining. - * - * @sample highcharts/members/renderer-text-on-chart/ - * Styled text - */ - css: function (styles) { - var oldStyles = this.styles, - newStyles = {}, - elem = this.element, - textWidth, - serializedCss = '', - hyphenate, - hasNew = !oldStyles, - // These CSS properties are interpreted internally by the SVG - // renderer, but are not supported by SVG and should not be added to - // the DOM. In styled mode, no CSS should find its way to the DOM - // whatsoever (#6173, #6474). - svgPseudoProps = ['textOutline', 'textOverflow', 'width']; - - // convert legacy - if (styles && styles.color) { - styles.fill = styles.color; - } - - // Filter out existing styles to increase performance (#2640) - if (oldStyles) { - objectEach(styles, function (style, n) { - if (style !== oldStyles[n]) { - newStyles[n] = style; - hasNew = true; - } - }); - } - if (hasNew) { - - // Merge the new styles with the old ones - if (oldStyles) { - styles = extend( - oldStyles, - newStyles - ); - } - - // Get the text width from style - textWidth = this.textWidth = ( - styles && - styles.width && - styles.width !== 'auto' && - elem.nodeName.toLowerCase() === 'text' && - pInt(styles.width) - ); - - // store object - this.styles = styles; - - if (textWidth && (!svg && this.renderer.forExport)) { - delete styles.width; - } - - // Serialize and set style attribute - if (elem.namespaceURI === this.SVG_NS) { // #7633 - hyphenate = function (a, b) { - return '-' + b.toLowerCase(); - }; - objectEach(styles, function (style, n) { - if (inArray(n, svgPseudoProps) === -1) { - serializedCss += - n.replace(/([A-Z])/g, hyphenate) + ':' + - style + ';'; - } - }); - if (serializedCss) { - attr(elem, 'style', serializedCss); // #1881 - } - } else { - css(elem, styles); - } - - - if (this.added) { - - // Rebuild text after added. Cache mechanisms in the buildText - // will prevent building if there are no significant changes. - if (this.element.nodeName === 'text') { - this.renderer.buildText(this); - } - - // Apply text outline after added - if (styles && styles.textOutline) { - this.applyTextOutline(styles.textOutline); - } - } - } - - return this; - }, - - /*= if (build.classic) { =*/ - /** - * Get the current stroke width. In classic mode, the setter registers it - * directly on the element. - * @returns {number} The stroke width in pixels. - * @ignore - */ - strokeWidth: function () { - return this['stroke-width'] || 0; - }, - - /*= } else { =*/ - /** - * Get the computed style. Only in styled mode. - * @param {string} prop - The property name to check for. - * @returns {string} The current computed value. - * @example - * chart.series[0].points[0].graphic.getStyle('stroke-width'); // => '1px' - */ - getStyle: function (prop) { - return win.getComputedStyle(this.element || this, '') - .getPropertyValue(prop); - }, - - /** - * Get the computed stroke width in pixel values. This is used extensively - * when drawing shapes to ensure the shapes are rendered crisp and - * positioned correctly relative to each other. Using - * `shape-rendering: crispEdges` leaves us less control over positioning, - * for example when we want to stack columns next to each other, or position - * things pixel-perfectly within the plot box. - * - * The common pattern when placing a shape is: - * * Create the SVGElement and add it to the DOM. In styled mode, it will - * now receive a stroke width from the style sheet. In classic mode we - * will add the `stroke-width` attribute. - * * Read the computed `elem.strokeWidth()`. - * * Place it based on the stroke width. - * - * @returns {Number} The stroke width in pixels. Even if the given stroke - * widtch (in CSS or by attributes) is based on `em` or other units, the - * pixel size is returned. - */ - strokeWidth: function () { - var val = this.getStyle('stroke-width'), - ret, - dummy; - - // Read pixel values directly - if (val.indexOf('px') === val.length - 2) { - ret = pInt(val); - - // Other values like em, pt etc need to be measured - } else { - dummy = doc.createElementNS(SVG_NS, 'rect'); - attr(dummy, { - 'width': val, - 'stroke-width': 0 - }); - this.element.parentNode.appendChild(dummy); - ret = dummy.getBBox().width; - dummy.parentNode.removeChild(dummy); - } - return ret; - }, - /*= } =*/ - /** - * Add an event listener. This is a simple setter that replaces all other - * events of the same type, opposed to the {@link Highcharts#addEvent} - * function. - * @param {string} eventType - The event type. If the type is `click`, - * Highcharts will internally translate it to a `touchstart` event on - * touch devices, to prevent the browser from waiting for a click event - * from firing. - * @param {Function} handler - The handler callback. - * @returns {SVGElement} The SVGElement for chaining. - * - * @sample highcharts/members/element-on/ - * A clickable rectangle - */ - on: function (eventType, handler) { - var svgElement = this, - element = svgElement.element; - - // touch - if (hasTouch && eventType === 'click') { - element.ontouchstart = function (e) { - svgElement.touchEventFired = Date.now(); // #2269 - e.preventDefault(); - handler.call(element, e); - }; - element.onclick = function (e) { - if (win.navigator.userAgent.indexOf('Android') === -1 || - Date.now() - (svgElement.touchEventFired || 0) > 1100) { - handler.call(element, e); - } - }; - } else { - // simplest possible event model for internal use - element['on' + eventType] = handler; - } - return this; - }, - - /** - * Set the coordinates needed to draw a consistent radial gradient across - * a shape regardless of positioning inside the chart. Used on pie slices - * to make all the slices have the same radial reference point. - * - * @param {Array} coordinates The center reference. The format is - * `[centerX, centerY, diameter]` in pixels. - * @returns {SVGElement} Returns the SVGElement for chaining. - */ - setRadialReference: function (coordinates) { - var existingGradient = this.renderer.gradients[this.element.gradient]; - - this.element.radialReference = coordinates; - - // On redrawing objects with an existing gradient, the gradient needs - // to be repositioned (#3801) - if (existingGradient && existingGradient.radAttr) { - existingGradient.animate( - this.renderer.getRadialAttr( - coordinates, - existingGradient.radAttr - ) - ); - } - - return this; - }, - - /** - * Move an object and its children by x and y values. - * - * @param {number} x - The x value. - * @param {number} y - The y value. - */ - translate: function (x, y) { - return this.attr({ - translateX: x, - translateY: y - }); - }, - - /** - * Invert a group, rotate and flip. This is used internally on inverted - * charts, where the points and graphs are drawn as if not inverted, then - * the series group elements are inverted. - * - * @param {boolean} inverted - * Whether to invert or not. An inverted shape can be un-inverted by - * setting it to false. - * @return {SVGElement} - * Return the SVGElement for chaining. - */ - invert: function (inverted) { - var wrapper = this; - wrapper.inverted = inverted; - wrapper.updateTransform(); - return wrapper; - }, - - /** - * Update the transform attribute based on internal properties. Deals with - * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY` - * attributes and updates the SVG `transform` attribute. - * @private - * - */ - updateTransform: function () { - var wrapper = this, - translateX = wrapper.translateX || 0, - translateY = wrapper.translateY || 0, - scaleX = wrapper.scaleX, - scaleY = wrapper.scaleY, - inverted = wrapper.inverted, - rotation = wrapper.rotation, - matrix = wrapper.matrix, - element = wrapper.element, - transform; - - // Flipping affects translate as adjustment for flipping around the - // group's axis - if (inverted) { - translateX += wrapper.width; - translateY += wrapper.height; - } - - // Apply translate. Nearly all transformed elements have translation, - // so instead of checking for translate = 0, do it always (#1767, - // #1846). - transform = ['translate(' + translateX + ',' + translateY + ')']; - - // apply matrix - if (defined(matrix)) { - transform.push( - 'matrix(' + matrix.join(',') + ')' - ); - } - - // apply rotation - if (inverted) { - transform.push('rotate(90) scale(-1,1)'); - } else if (rotation) { // text rotation - transform.push( - 'rotate(' + rotation + ' ' + - pick(this.rotationOriginX, element.getAttribute('x'), 0) + - ' ' + - pick(this.rotationOriginY, element.getAttribute('y') || 0) + ')' - ); - } - - // apply scale - if (defined(scaleX) || defined(scaleY)) { - transform.push( - 'scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')' - ); - } - - if (transform.length) { - element.setAttribute('transform', transform.join(' ')); - } - }, - - /** - * Bring the element to the front. Alternatively, a new zIndex can be set. - * - * @returns {SVGElement} Returns the SVGElement for chaining. - * - * @sample highcharts/members/element-tofront/ - * Click an element to bring it to front - */ - toFront: function () { - var element = this.element; - element.parentNode.appendChild(element); - return this; - }, - - - /** - * Align the element relative to the chart or another box. - * - * @param {Object} [alignOptions] The alignment options. The function can be - * called without this parameter in order to re-align an element after the - * box has been updated. - * @param {string} [alignOptions.align=left] Horizontal alignment. Can be - * one of `left`, `center` and `right`. - * @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can - * be one of `top`, `middle` and `bottom`. - * @param {number} [alignOptions.x=0] Horizontal pixel offset from - * alignment. - * @param {number} [alignOptions.y=0] Vertical pixel offset from alignment. - * @param {Boolean} [alignByTranslate=false] Use the `transform` attribute - * with translateX and translateY custom attributes to align this elements - * rather than `x` and `y` attributes. - * @param {String|Object} box The box to align to, needs a width and height. - * When the box is a string, it refers to an object in the Renderer. For - * example, when box is `spacingBox`, it refers to `Renderer.spacingBox` - * which holds `width`, `height`, `x` and `y` properties. - * @returns {SVGElement} Returns the SVGElement for chaining. - */ - align: function (alignOptions, alignByTranslate, box) { - var align, - vAlign, - x, - y, - attribs = {}, - alignTo, - renderer = this.renderer, - alignedObjects = renderer.alignedObjects, - alignFactor, - vAlignFactor; - - // First call on instanciate - if (alignOptions) { - this.alignOptions = alignOptions; - this.alignByTranslate = alignByTranslate; - if (!box || isString(box)) { - this.alignTo = alignTo = box || 'renderer'; - // prevent duplicates, like legendGroup after resize - erase(alignedObjects, this); - alignedObjects.push(this); - box = null; // reassign it below - } - - // When called on resize, no arguments are supplied - } else { - alignOptions = this.alignOptions; - alignByTranslate = this.alignByTranslate; - alignTo = this.alignTo; - } - - box = pick(box, renderer[alignTo], renderer); - - // Assign variables - align = alignOptions.align; - vAlign = alignOptions.verticalAlign; - x = (box.x || 0) + (alignOptions.x || 0); // default: left align - y = (box.y || 0) + (alignOptions.y || 0); // default: top align - - // Align - if (align === 'right') { - alignFactor = 1; - } else if (align === 'center') { - alignFactor = 2; - } - if (alignFactor) { - x += (box.width - (alignOptions.width || 0)) / alignFactor; - } - attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x); - - - // Vertical align - if (vAlign === 'bottom') { - vAlignFactor = 1; - } else if (vAlign === 'middle') { - vAlignFactor = 2; - } - if (vAlignFactor) { - y += (box.height - (alignOptions.height || 0)) / vAlignFactor; - } - attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y); - - // Animate only if already placed - this[this.placed ? 'animate' : 'attr'](attribs); - this.placed = true; - this.alignAttr = attribs; - - return this; - }, - - /** - * Get the bounding box (width, height, x and y) for the element. Generally - * used to get rendered text size. Since this is called a lot in charts, - * the results are cached based on text properties, in order to save DOM - * traffic. The returned bounding box includes the rotation, so for example - * a single text line of rotation 90 will report a greater height, and a - * width corresponding to the line-height. - * - * @param {boolean} [reload] Skip the cache and get the updated DOM bouding - * box. - * @param {number} [rot] Override the element's rotation. This is internally - * used on axis labels with a value of 0 to find out what the bounding box - * would be have been if it were not rotated. - * @returns {Object} The bounding box with `x`, `y`, `width` and `height` - * properties. - * - * @sample highcharts/members/renderer-on-chart/ - * Draw a rectangle based on a text's bounding box - */ - getBBox: function (reload, rot) { - var wrapper = this, - bBox, // = wrapper.bBox, - renderer = wrapper.renderer, - width, - height, - rotation, - rad, - element = wrapper.element, - styles = wrapper.styles, - fontSize, - textStr = wrapper.textStr, - toggleTextShadowShim, - cache = renderer.cache, - cacheKeys = renderer.cacheKeys, - cacheKey; - - rotation = pick(rot, wrapper.rotation); - rad = rotation * deg2rad; - - /*= if (build.classic) { =*/ - fontSize = styles && styles.fontSize; - /*= } else { =*/ - fontSize = element && - SVGElement.prototype.getStyle.call(element, 'font-size'); - /*= } =*/ - - // Avoid undefined and null (#7316) - if (defined(textStr)) { - - cacheKey = textStr.toString(); - - // Since numbers are monospaced, and numerical labels appear a lot - // in a chart, we assume that a label of n characters has the same - // bounding box as others of the same length. Unless there is inner - // HTML in the label. In that case, leave the numbers as is (#5899). - if (cacheKey.indexOf('<') === -1) { - cacheKey = cacheKey.replace(/[0-9]/g, '0'); - } - - // Properties that affect bounding box - cacheKey += [ - '', - rotation || 0, - fontSize, - wrapper.textWidth, // #7874, also useHTML - styles && styles.textOverflow // #5968 - ] - .join(','); - - } - - if (cacheKey && !reload) { - bBox = cache[cacheKey]; - } - - // No cache found - if (!bBox) { - - // SVG elements - if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) { - try { // Fails in Firefox if the container has display: none. - - // When the text shadow shim is used, we need to hide the - // fake shadows to get the correct bounding box (#3872) - toggleTextShadowShim = this.fakeTS && function (display) { - each( - element.querySelectorAll( - '.highcharts-text-outline' - ), - function (tspan) { - tspan.style.display = display; - } - ); - }; - - // Workaround for #3842, Firefox reporting wrong bounding - // box for shadows - if (toggleTextShadowShim) { - toggleTextShadowShim('none'); - } - - bBox = element.getBBox ? - // SVG: use extend because IE9 is not allowed to change - // width and height in case of rotation (below) - extend({}, element.getBBox()) : { - - // Legacy IE in export mode - width: element.offsetWidth, - height: element.offsetHeight - }; - - // #3842 - if (toggleTextShadowShim) { - toggleTextShadowShim(''); - } - } catch (e) {} - - // If the bBox is not set, the try-catch block above failed. The - // other condition is for Opera that returns a width of - // -Infinity on hidden elements. - if (!bBox || bBox.width < 0) { - bBox = { width: 0, height: 0 }; - } - - - // VML Renderer or useHTML within SVG - } else { - - bBox = wrapper.htmlGetBBox(); - - } - - // True SVG elements as well as HTML elements in modern browsers - // using the .useHTML option need to compensated for rotation - if (renderer.isSVG) { - width = bBox.width; - height = bBox.height; - - // Workaround for wrong bounding box in IE, Edge and Chrome on - // Windows. With Highcharts' default font, IE and Edge report - // a box height of 16.899 and Chrome rounds it to 17. If this - // stands uncorrected, it results in more padding added below - // the text than above when adding a label border or background. - // Also vertical positioning is affected. - // http://jsfiddle.net/highcharts/em37nvuj/ - // (#1101, #1505, #1669, #2568, #6213). - if ( - styles && - styles.fontSize === '11px' && - Math.round(height) === 17 - ) { - bBox.height = height = 14; - } - - // Adjust for rotated text - if (rotation) { - bBox.width = Math.abs(height * Math.sin(rad)) + - Math.abs(width * Math.cos(rad)); - bBox.height = Math.abs(height * Math.cos(rad)) + - Math.abs(width * Math.sin(rad)); - } - } - - // Cache it. When loading a chart in a hidden iframe in Firefox and - // IE/Edge, the bounding box height is 0, so don't cache it (#5620). - if (cacheKey && bBox.height > 0) { - - // Rotate (#4681) - while (cacheKeys.length > 250) { - delete cache[cacheKeys.shift()]; - } - - if (!cache[cacheKey]) { - cacheKeys.push(cacheKey); - } - cache[cacheKey] = bBox; - } - } - return bBox; - }, - - /** - * Show the element after it has been hidden. - * - * @param {boolean} [inherit=false] Set the visibility attribute to - * `inherit` rather than `visible`. The difference is that an element with - * `visibility="visible"` will be visible even if the parent is hidden. - * - * @returns {SVGElement} Returns the SVGElement for chaining. - */ - show: function (inherit) { - return this.attr({ visibility: inherit ? 'inherit' : 'visible' }); - }, - - /** - * Hide the element, equivalent to setting the `visibility` attribute to - * `hidden`. - * - * @returns {SVGElement} Returns the SVGElement for chaining. - */ - hide: function () { - return this.attr({ visibility: 'hidden' }); - }, - - /** - * Fade out an element by animating its opacity down to 0, and hide it on - * complete. Used internally for the tooltip. - * - * @param {number} [duration=150] The fade duration in milliseconds. - */ - fadeOut: function (duration) { - var elemWrapper = this; - elemWrapper.animate({ - opacity: 0 - }, { - duration: duration || 150, - complete: function () { - // #3088, assuming we're only using this for tooltips - elemWrapper.attr({ y: -9999 }); - } - }); - }, - - /** - * Add the element to the DOM. All elements must be added this way. - * - * @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to. - * If undefined, the element is added to the {@link - * Highcharts.SVGRenderer.box}. - * - * @returns {SVGElement} Returns the SVGElement for chaining. - * - * @sample highcharts/members/renderer-g - Elements added to a group - */ - add: function (parent) { - - var renderer = this.renderer, - element = this.element, - inserted; - - if (parent) { - this.parentGroup = parent; - } - - // mark as inverted - this.parentInverted = parent && parent.inverted; - - // build formatted text - if (this.textStr !== undefined) { - renderer.buildText(this); - } - - // Mark as added - this.added = true; - - // If we're adding to renderer root, or other elements in the group - // have a z index, we need to handle it - if (!parent || parent.handleZ || this.zIndex) { - inserted = this.zIndexSetter(); - } - - // If zIndex is not handled, append at the end - if (!inserted) { - (parent ? parent.element : renderer.box).appendChild(element); - } - - // fire an event for internal hooks - if (this.onAdd) { - this.onAdd(); - } - - return this; - }, - - /** - * Removes an element from the DOM. - * - * @private - * @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove. - */ - safeRemoveChild: function (element) { - var parentNode = element.parentNode; - if (parentNode) { - parentNode.removeChild(element); - } - }, - - /** - * Destroy the element and element wrapper and clear up the DOM and event - * hooks. - * - * - */ - destroy: function () { - var wrapper = this, - element = wrapper.element || {}, - parentToClean = - wrapper.renderer.isSVG && - element.nodeName === 'SPAN' && - wrapper.parentGroup, - grandParent, - ownerSVGElement = element.ownerSVGElement, - i, - clipPath = wrapper.clipPath; - - // remove events - element.onclick = element.onmouseout = element.onmouseover = - element.onmousemove = element.point = null; - stop(wrapper); // stop running animations - - if (clipPath && ownerSVGElement) { - // Look for existing references to this clipPath and remove them - // before destroying the element (#6196). - each( - // The upper case version is for Edge - ownerSVGElement.querySelectorAll('[clip-path],[CLIP-PATH]'), - function (el) { - var clipPathAttr = el.getAttribute('clip-path'), - clipPathId = clipPath.element.id; - // Include the closing paranthesis in the test to rule out - // id's from 10 and above (#6550). Edge puts quotes inside - // the url, others not. - if ( - clipPathAttr.indexOf('(#' + clipPathId + ')') > -1 || - clipPathAttr.indexOf('("#' + clipPathId + '")') > -1 - ) { - el.removeAttribute('clip-path'); - } - } - ); - wrapper.clipPath = clipPath.destroy(); - } - - // Destroy stops in case this is a gradient object - if (wrapper.stops) { - for (i = 0; i < wrapper.stops.length; i++) { - wrapper.stops[i] = wrapper.stops[i].destroy(); - } - wrapper.stops = null; - } - - // remove element - wrapper.safeRemoveChild(element); - - /*= if (build.classic) { =*/ - wrapper.destroyShadows(); - /*= } =*/ - - // In case of useHTML, clean up empty containers emulating SVG groups - // (#1960, #2393, #2697). - while ( - parentToClean && - parentToClean.div && - parentToClean.div.childNodes.length === 0 - ) { - grandParent = parentToClean.parentGroup; - wrapper.safeRemoveChild(parentToClean.div); - delete parentToClean.div; - parentToClean = grandParent; - } - - // remove from alignObjects - if (wrapper.alignTo) { - erase(wrapper.renderer.alignedObjects, wrapper); - } - - objectEach(wrapper, function (val, key) { - delete wrapper[key]; - }); - - return null; - }, - - /*= if (build.classic) { =*/ - /** - * @typedef {Object} ShadowOptions - * @property {string} [color=${palette.neutralColor100}] The shadow color. - * @property {number} [offsetX=1] The horizontal offset from the element. - * @property {number} [offsetY=1] The vertical offset from the element. - * @property {number} [opacity=0.15] The shadow opacity. - * @property {number} [width=3] The shadow width or distance from the - * element. - */ - /** - * Add a shadow to the element. Must be called after the element is added to - * the DOM. In styled mode, this method is not used, instead use `defs` and - * filters. - * - * @param {boolean|ShadowOptions} shadowOptions The shadow options. If - * `true`, the default options are applied. If `false`, the current - * shadow will be removed. - * @param {SVGElement} [group] The SVG group element where the shadows will - * be applied. The default is to add it to the same parent as the current - * element. Internally, this is ised for pie slices, where all the - * shadows are added to an element behind all the slices. - * @param {boolean} [cutOff] Used internally for column shadows. - * - * @returns {SVGElement} Returns the SVGElement for chaining. - * - * @example - * renderer.rect(10, 100, 100, 100) - * .attr({ fill: 'red' }) - * .shadow(true); - */ - shadow: function (shadowOptions, group, cutOff) { - var shadows = [], - i, - shadow, - element = this.element, - strokeWidth, - shadowWidth, - shadowElementOpacity, - - // compensate for inverted plot area - transform; - - if (!shadowOptions) { - this.destroyShadows(); - - } else if (!this.shadows) { - shadowWidth = pick(shadowOptions.width, 3); - shadowElementOpacity = (shadowOptions.opacity || 0.15) / - shadowWidth; - transform = this.parentInverted ? - '(-1,-1)' : - '(' + pick(shadowOptions.offsetX, 1) + ', ' + - pick(shadowOptions.offsetY, 1) + ')'; - for (i = 1; i <= shadowWidth; i++) { - shadow = element.cloneNode(0); - strokeWidth = (shadowWidth * 2) + 1 - (2 * i); - attr(shadow, { - 'isShadow': 'true', - 'stroke': - shadowOptions.color || '${palette.neutralColor100}', - 'stroke-opacity': shadowElementOpacity * i, - 'stroke-width': strokeWidth, - 'transform': 'translate' + transform, - 'fill': 'none' - }); - if (cutOff) { - attr( - shadow, - 'height', - Math.max(attr(shadow, 'height') - strokeWidth, 0) - ); - shadow.cutHeight = strokeWidth; - } - - if (group) { - group.element.appendChild(shadow); - } else if (element.parentNode) { - element.parentNode.insertBefore(shadow, element); - } - - shadows.push(shadow); - } - - this.shadows = shadows; - } - return this; - - }, - - /** - * Destroy shadows on the element. - * @private - */ - destroyShadows: function () { - each(this.shadows || [], function (shadow) { - this.safeRemoveChild(shadow); - }, this); - this.shadows = undefined; - }, - - /*= } =*/ - - xGetter: function (key) { - if (this.element.nodeName === 'circle') { - if (key === 'x') { - key = 'cx'; - } else if (key === 'y') { - key = 'cy'; - } - } - return this._defaultGetter(key); - }, - - /** - * Get the current value of an attribute or pseudo attribute, used mainly - * for animation. Called internally from the {@link - * Highcharts.SVGRenderer#attr} - * function. - * - * @private - */ - _defaultGetter: function (key) { - var ret = pick( - this[key + 'Value'], // align getter - this[key], - this.element ? this.element.getAttribute(key) : null, - 0 - ); - - if (/^[\-0-9\.]+$/.test(ret)) { // is numerical - ret = parseFloat(ret); - } - return ret; - }, - - - dSetter: function (value, key, element) { - if (value && value.join) { // join path - value = value.join(' '); - } - if (/(NaN| {2}|^$)/.test(value)) { - value = 'M 0 0'; - } - - // Check for cache before resetting. Resetting causes disturbance in the - // DOM, causing flickering in some cases in Edge/IE (#6747). Also - // possible performance gain. - if (this[key] !== value) { - element.setAttribute(key, value); - this[key] = value; - } - - }, - /*= if (build.classic) { =*/ - dashstyleSetter: function (value) { - var i, - strokeWidth = this['stroke-width']; - - // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new - // strokeWidth function, we should be able to use that instead. - if (strokeWidth === 'inherit') { - strokeWidth = 1; - } - value = value && value.toLowerCase(); - if (value) { - value = value - .replace('shortdashdotdot', '3,1,1,1,1,1,') - .replace('shortdashdot', '3,1,1,1') - .replace('shortdot', '1,1,') - .replace('shortdash', '3,1,') - .replace('longdash', '8,3,') - .replace(/dot/g, '1,3,') - .replace('dash', '4,3,') - .replace(/,$/, '') - .split(','); // ending comma - - i = value.length; - while (i--) { - value[i] = pInt(value[i]) * strokeWidth; - } - value = value.join(',') - .replace(/NaN/g, 'none'); // #3226 - this.element.setAttribute('stroke-dasharray', value); - } - }, - /*= } =*/ - alignSetter: function (value) { - var convert = { left: 'start', center: 'middle', right: 'end' }; - this.alignValue = value; - this.element.setAttribute('text-anchor', convert[value]); - }, - opacitySetter: function (value, key, element) { - this[key] = value; - element.setAttribute(key, value); - }, - titleSetter: function (value) { - var titleNode = this.element.getElementsByTagName('title')[0]; - if (!titleNode) { - titleNode = doc.createElementNS(this.SVG_NS, 'title'); - this.element.appendChild(titleNode); - } - - // Remove text content if it exists - if (titleNode.firstChild) { - titleNode.removeChild(titleNode.firstChild); - } - - titleNode.appendChild( - doc.createTextNode( - // #3276, #3895 - (String(pick(value), '')) - .replace(/<[^>]*>/g, '') - .replace(/</g, '<') - .replace(/>/g, '>') - ) - ); - }, - textSetter: function (value) { - if (value !== this.textStr) { - // Delete bBox memo when the text changes - delete this.bBox; - - this.textStr = value; - if (this.added) { - this.renderer.buildText(this); - } - } - }, - fillSetter: function (value, key, element) { - if (typeof value === 'string') { - element.setAttribute(key, value); - } else if (value) { - this.complexColor(value, key, element); - } - }, - visibilitySetter: function (value, key, element) { - // IE9-11 doesn't handle visibilty:inherit well, so we remove the - // attribute instead (#2881, #3909) - if (value === 'inherit') { - element.removeAttribute(key); - } else if (this[key] !== value) { // #6747 - element.setAttribute(key, value); - } - this[key] = value; - }, - zIndexSetter: function (value, key) { - var renderer = this.renderer, - parentGroup = this.parentGroup, - parentWrapper = parentGroup || renderer, - parentNode = parentWrapper.element || renderer.box, - childNodes, - otherElement, - otherZIndex, - element = this.element, - inserted, - undefinedOtherZIndex, - svgParent = parentNode === renderer.box, - run = this.added, - i; - - if (defined(value)) { - // So we can read it for other elements in the group - element.zIndex = value; - - value = +value; - if (this[key] === value) { // Only update when needed (#3865) - run = false; - } - this[key] = value; - } - - // Insert according to this and other elements' zIndex. Before .add() is - // called, nothing is done. Then on add, or by later calls to - // zIndexSetter, the node is placed on the right place in the DOM. - if (run) { - value = this.zIndex; - - if (value && parentGroup) { - parentGroup.handleZ = true; - } - - childNodes = parentNode.childNodes; - for (i = childNodes.length - 1; i >= 0 && !inserted; i--) { - otherElement = childNodes[i]; - otherZIndex = otherElement.zIndex; - undefinedOtherZIndex = !defined(otherZIndex); - - if (otherElement !== element) { - if ( - // Negative zIndex versus no zIndex: - // On all levels except the highest. If the parent is - // , then we don't want to put items before - // or - (value < 0 && undefinedOtherZIndex && !svgParent && !i) - ) { - parentNode.insertBefore(element, childNodes[i]); - inserted = true; - } else if ( - // Insert after the first element with a lower zIndex - pInt(otherZIndex) <= value || - // If negative zIndex, add this before first undefined - // zIndex element - ( - undefinedOtherZIndex && - (!defined(value) || value >= 0) - ) - ) { - parentNode.insertBefore( - element, - childNodes[i + 1] || null // null for oldIE export - ); - inserted = true; - } - } - } - - if (!inserted) { - parentNode.insertBefore( - element, - childNodes[svgParent ? 3 : 0] || null // null for oldIE - ); - inserted = true; - } - } - return inserted; - }, - _defaultSetter: function (value, key, element) { - element.setAttribute(key, value); - } + // Default base for animation + opacity: 1, + SVG_NS: SVG_NS, + + /** + * For labels, these CSS properties are applied to the `text` node directly. + * + * @private + * @type {Array.} + */ + textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', + 'fontStyle', 'color', 'lineHeight', 'width', 'textAlign', + 'textDecoration', 'textOverflow', 'textOutline'], + + /** + * Initialize the SVG element. This function only exists to make the + * initiation process overridable. It should not be called directly. + * + * @param {SVGRenderer} renderer + * The SVGRenderer instance to initialize to. + * @param {String} nodeName + * The SVG node name. + * + */ + init: function (renderer, nodeName) { + + /** + * The primary DOM node. Each `SVGElement` instance wraps a main DOM + * node, but may also represent more nodes. + * + * @name element + * @memberOf SVGElement + * @type {SVGDOMNode|HTMLDOMNode} + */ + this.element = nodeName === 'span' ? + createElement(nodeName) : + doc.createElementNS(this.SVG_NS, nodeName); + + /** + * The renderer that the SVGElement belongs to. + * + * @name renderer + * @memberOf SVGElement + * @type {SVGRenderer} + */ + this.renderer = renderer; + }, + + /** + * Animate to given attributes or CSS properties. + * + * @param {SVGAttributes} params SVG attributes or CSS to animate. + * @param {AnimationOptions} [options] Animation options. + * @param {Function} [complete] Function to perform at the end of animation. + * + * @sample highcharts/members/element-on/ + * Setting some attributes by animation + * + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + animate: function (params, options, complete) { + var animOptions = H.animObject( + pick(options, this.renderer.globalAnimation, true) + ); + if (animOptions.duration !== 0) { + // allows using a callback with the global animation without + // overwriting it + if (complete) { + animOptions.complete = complete; + } + animate(this, params, animOptions); + } else { + this.attr(params, null, complete); + if (animOptions.step) { + animOptions.step.call(this); + } + } + return this; + }, + + /** + * @typedef {Object} GradientOptions + * @property {Object} linearGradient Holds an object that defines the start + * position and the end position relative to the shape. + * @property {Number} linearGradient.x1 Start horizontal position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.x2 End horizontal position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.y1 Start vertical position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.y2 End vertical position of the + * gradient. Ranges 0-1. + * @property {Object} radialGradient Holds an object that defines the center + * position and the radius. + * @property {Number} radialGradient.cx Center horizontal position relative + * to the shape. Ranges 0-1. + * @property {Number} radialGradient.cy Center vertical position relative + * to the shape. Ranges 0-1. + * @property {Number} radialGradient.r Radius relative to the shape. Ranges + * 0-1. + * @property {Array.} stops The first item in each tuple is the + * position in the gradient, where 0 is the start of the gradient and 1 + * is the end of the gradient. Multiple stops can be applied. The second + * item is the color for each stop. This color can also be given in the + * rgba format. + * + * @example + * // Linear gradient used as a color option + * color: { + * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 }, + * stops: [ + * [0, '#003399'], // start + * [0.5, '#ffffff'], // middle + * [1, '#3366AA'] // end + * ] + * } + * } + */ + /** + * Build and apply an SVG gradient out of a common JavaScript configuration + * object. This function is called from the attribute setters. An event + * hook is added for supporting other complex color types. + * + * @private + * @param {GradientOptions} color The gradient options structure. + * @param {string} prop The property to apply, can either be `fill` or + * `stroke`. + * @param {SVGDOMElement} elem SVG DOM element to apply the gradient on. + */ + complexColor: function (color, prop, elem) { + var renderer = this.renderer, + colorObject, + gradName, + gradAttr, + radAttr, + gradients, + gradientObject, + stops, + stopColor, + stopOpacity, + radialReference, + id, + key = [], + value; + + H.fireEvent(this.renderer, 'complexColor', { + args: arguments + }, function () { + // Apply linear or radial gradients + if (color.radialGradient) { + gradName = 'radialGradient'; + } else if (color.linearGradient) { + gradName = 'linearGradient'; + } + + if (gradName) { + gradAttr = color[gradName]; + gradients = renderer.gradients; + stops = color.stops; + radialReference = elem.radialReference; + + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } + + // Correct the radial gradient for the radial reference system + if ( + gradName === 'radialGradient' && + radialReference && + !defined(gradAttr.gradientUnits) + ) { + // Save the radial attributes for updating + radAttr = gradAttr; + gradAttr = merge( + gradAttr, + renderer.getRadialAttr(radialReference, radAttr), + { gradientUnits: 'userSpaceOnUse' } + ); + } + + // Build the unique key to detect whether we need to create a + // new element (#1282) + objectEach(gradAttr, function (val, n) { + if (n !== 'id') { + key.push(n, val); + } + }); + objectEach(stops, function (val) { + key.push(val); + }); + key = key.join(','); + + // Check if a gradient object with the same config object is + // created within this renderer + if (gradients[key]) { + id = gradients[key].attr('id'); + + } else { + + // Set the id and create the element + gradAttr.id = id = H.uniqueKey(); + gradients[key] = gradientObject = + renderer.createElement(gradName) + .attr(gradAttr) + .add(renderer.defs); + + gradientObject.radAttr = radAttr; + + // The gradient needs to keep a list of stops to be able to + // destroy them + gradientObject.stops = []; + each(stops, function (stop) { + var stopObject; + if (stop[1].indexOf('rgba') === 0) { + colorObject = H.color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + stopObject = renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + + // Add the stop element to the gradient + gradientObject.stops.push(stopObject); + }); + } + + // Set the reference to the gradient object + value = 'url(' + renderer.url + '#' + id + ')'; + elem.setAttribute(prop, value); + elem.gradient = key; + + // Allow the color to be concatenated into tooltips formatters + // etc. (#2995) + color.toString = function () { + return value; + }; + } + }); + }, + + /** + * Apply a text outline through a custom CSS property, by copying the text + * element and apply stroke to the copy. Used internally. Contrast checks + * at http://jsfiddle.net/highcharts/43soe9m1/2/ . + * + * @private + * @param {String} textOutline A custom CSS `text-outline` setting, defined + * by `width color`. + * @example + * // Specific color + * text.css({ + * textOutline: '1px black' + * }); + * // Automatic contrast + * text.css({ + * color: '#000000', // black text + * textOutline: '1px contrast' // => white outline + * }); + */ + applyTextOutline: function (textOutline) { + var elem = this.element, + tspans, + tspan, + hasContrast = textOutline.indexOf('contrast') !== -1, + styles = {}, + color, + strokeWidth, + firstRealChild, + i; + + // When the text shadow is set to contrast, use dark stroke for light + // text and vice versa. + if (hasContrast) { + styles.textOutline = textOutline = textOutline.replace( + /contrast/g, + this.renderer.getContrast(elem.style.fill) + ); + } + + // Extract the stroke width and color + textOutline = textOutline.split(' '); + color = textOutline[textOutline.length - 1]; + strokeWidth = textOutline[0]; + + if (strokeWidth && strokeWidth !== 'none' && H.svg) { + + this.fakeTS = true; // Fake text shadow + + tspans = [].slice.call(elem.getElementsByTagName('tspan')); + + // In order to get the right y position of the clone, + // copy over the y setter + this.ySetter = this.xSetter; + + // Since the stroke is applied on center of the actual outline, we + // need to double it to get the correct stroke-width outside the + // glyphs. + strokeWidth = strokeWidth.replace( + /(^[\d\.]+)(.*?)$/g, + function (match, digit, unit) { + return (2 * digit) + unit; + } + ); + + // Remove shadows from previous runs. Iterate from the end to + // support removing items inside the cycle (#6472). + i = tspans.length; + while (i--) { + tspan = tspans[i]; + if (tspan.getAttribute('class') === 'highcharts-text-outline') { + // Remove then erase + erase(tspans, elem.removeChild(tspan)); + } + } + + // For each of the tspans, create a stroked copy behind it. + firstRealChild = elem.firstChild; + each(tspans, function (tspan, y) { + var clone; + + // Let the first line start at the correct X position + if (y === 0) { + tspan.setAttribute('x', elem.getAttribute('x')); + y = elem.getAttribute('y'); + tspan.setAttribute('y', y || 0); + if (y === null) { + elem.setAttribute('y', 0); + } + } + + // Create the clone and apply outline properties + clone = tspan.cloneNode(1); + attr(clone, { + 'class': 'highcharts-text-outline', + 'fill': color, + 'stroke': color, + 'stroke-width': strokeWidth, + 'stroke-linejoin': 'round' + }); + elem.insertBefore(clone, firstRealChild); + }); + } + }, + + /** + * + * @typedef {Object} SVGAttributes An object of key-value pairs for SVG + * attributes. Attributes in Highcharts elements for the most parts + * correspond to SVG, but some are specific to Highcharts, like `zIndex`, + * `rotation`, `rotationOriginX`, `rotationOriginY`, `translateX`, + * `translateY`, `scaleX` and `scaleY`. SVG attributes containing a hyphen + * are _not_ camel-cased, they should be quoted to preserve the hyphen. + * + * @example + * { + * 'stroke': '#ff0000', // basic + * 'stroke-width': 2, // hyphenated + * 'rotation': 45 // custom + * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format + * } + */ + /** + * Apply native and custom attributes to the SVG elements. + * + * In order to set the rotation center for rotation, set x and y to 0 and + * use `translateX` and `translateY` attributes to position the element + * instead. + * + * Attributes frequently used in Highcharts are `fill`, `stroke`, + * `stroke-width`. + * + * @param {SVGAttributes|String} hash - The native and custom SVG + * attributes. + * @param {string} [val] - If the type of the first argument is `string`, + * the second can be a value, which will serve as a single attribute + * setter. If the first argument is a string and the second is undefined, + * the function serves as a getter and the current value of the property + * is returned. + * @param {Function} [complete] - A callback function to execute after + * setting the attributes. This makes the function compliant and + * interchangeable with the {@link SVGElement#animate} function. + * @param {boolean} [continueAnimation=true] Used internally when `.attr` is + * called as part of an animation step. Otherwise, calling `.attr` for an + * attribute will stop animation for that attribute. + * + * @returns {SVGElement|string|number} If used as a setter, it returns the + * current {@link SVGElement} so the calls can be chained. If used as a + * getter, the current value of the attribute is returned. + * + * @sample highcharts/members/renderer-rect/ + * Setting some attributes + * + * @example + * // Set multiple attributes + * element.attr({ + * stroke: 'red', + * fill: 'blue', + * x: 10, + * y: 10 + * }); + * + * // Set a single attribute + * element.attr('stroke', 'red'); + * + * // Get an attribute + * element.attr('stroke'); // => 'red' + * + */ + attr: function (hash, val, complete, continueAnimation) { + var key, + element = this.element, + hasSetSymbolSize, + ret = this, + skipAttr, + setter; + + // single key-value pair + if (typeof hash === 'string' && val !== undefined) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter: first argument is a string, second is undefined + if (typeof hash === 'string') { + ret = (this[hash + 'Getter'] || this._defaultGetter).call( + this, + hash, + element + ); + + // setter + } else { + + objectEach(hash, function eachAttribute(val, key) { + skipAttr = false; + + // Unless .attr is from the animator update, stop current + // running animation of this property + if (!continueAnimation) { + stop(this, key); + } + + // Special handling of symbol attributes + if ( + this.symbolName && + /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)$/ + .test(key) + ) { + if (!hasSetSymbolSize) { + this.symbolAttr(hash); + hasSetSymbolSize = true; + } + skipAttr = true; + } + + if (this.rotation && (key === 'x' || key === 'y')) { + this.doTransform = true; + } + + if (!skipAttr) { + setter = this[key + 'Setter'] || this._defaultSetter; + setter.call(this, val, key, element); + + /*= if (build.classic) { =*/ + // Let the shadow follow the main element + if ( + this.shadows && + /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/ + .test(key) + ) { + this.updateShadows(key, val, setter); + } + /*= } =*/ + } + }, this); + + this.afterSetters(); + } + + // In accordance with animate, run a complete callback + if (complete) { + complete.call(this); + } + + return ret; + }, + + /** + * This method is executed in the end of `attr()`, after setting all + * attributes in the hash. In can be used to efficiently consolidate + * multiple attributes in one SVG property -- e.g., translate, rotate and + * scale are merged in one "transform" attribute in the SVG node. + * + * @private + */ + afterSetters: function () { + // Update transform. Do this outside the loop to prevent redundant + // updating for batch setting of attributes. + if (this.doTransform) { + this.updateTransform(); + this.doTransform = false; + } + }, + + /*= if (build.classic) { =*/ + /** + * Update the shadow elements with new attributes. + * + * @private + * @param {String} key - The attribute name. + * @param {String|Number} value - The value of the attribute. + * @param {Function} setter - The setter function, inherited from the + * parent wrapper + * + */ + updateShadows: function (key, value, setter) { + var shadows = this.shadows, + i = shadows.length; + + while (i--) { + setter.call( + shadows[i], + key === 'height' ? + Math.max(value - (shadows[i].cutHeight || 0), 0) : + key === 'd' ? this.d : value, + key, + shadows[i] + ); + } + }, + /*= } =*/ + + /** + * Add a class name to an element. + * + * @param {string} className - The new class name to add. + * @param {boolean} [replace=false] - When true, the existing class name(s) + * will be overwritten with the new one. When false, the new one is + * added. + * @returns {SVGElement} Return the SVG element for chainability. + */ + addClass: function (className, replace) { + var currentClassName = this.attr('class') || ''; + if (currentClassName.indexOf(className) === -1) { + if (!replace) { + className = + (currentClassName + (currentClassName ? ' ' : '') + + className).replace(' ', ' '); + } + this.attr('class', className); + } + + return this; + }, + + /** + * Check if an element has the given class name. + * @param {string} className + * The class name to check for. + * @return {Boolean} + * Whether the class name is found. + */ + hasClass: function (className) { + return inArray( + className, + (this.attr('class') || '').split(' ') + ) !== -1; + }, + + /** + * Remove a class name from the element. + * @param {String|RegExp} className The class name to remove. + * @return {SVGElement} Returns the SVG element for chainability. + */ + removeClass: function (className) { + return this.attr( + 'class', + (this.attr('class') || '').replace(className, '') + ); + }, + + /** + * If one of the symbol size affecting parameters are changed, + * check all the others only once for each call to an element's + * .attr() method + * @param {Object} hash - The attributes to set. + * @private + */ + symbolAttr: function (hash) { + var wrapper = this; + + each([ + 'x', + 'y', + 'r', + 'start', + 'end', + 'width', + 'height', + 'innerR', + 'anchorX', + 'anchorY' + ], function (key) { + wrapper[key] = pick(hash[key], wrapper[key]); + }); + + wrapper.attr({ + d: wrapper.renderer.symbols[wrapper.symbolName]( + wrapper.x, + wrapper.y, + wrapper.width, + wrapper.height, + wrapper + ) + }); + }, + + /** + * Apply a clipping rectangle to this element. + * + * @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the + * current clip is removed. + * @returns {SVGElement} Returns the SVG element to allow chaining. + */ + clip: function (clipRect) { + return this.attr( + 'clip-path', + clipRect ? + 'url(' + this.renderer.url + '#' + clipRect.id + ')' : + 'none' + ); + }, + + /** + * Calculate the coordinates needed for drawing a rectangle crisply and + * return the calculated attributes. + * + * @param {Object} rect - A rectangle. + * @param {number} rect.x - The x position. + * @param {number} rect.y - The y position. + * @param {number} rect.width - The width. + * @param {number} rect.height - The height. + * @param {number} [strokeWidth] - The stroke width to consider when + * computing crisp positioning. It can also be set directly on the rect + * parameter. + * + * @returns {{x: Number, y: Number, width: Number, height: Number}} The + * modified rectangle arguments. + */ + crisp: function (rect, strokeWidth) { + + var wrapper = this, + normalizer; + + strokeWidth = strokeWidth || rect.strokeWidth || 0; + // Math.round because strokeWidth can sometimes have roundoff errors + normalizer = Math.round(strokeWidth) % 2 / 2; + + // normalize for crisp edges + rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer; + rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer; + rect.width = Math.floor( + (rect.width || wrapper.width || 0) - 2 * normalizer + ); + rect.height = Math.floor( + (rect.height || wrapper.height || 0) - 2 * normalizer + ); + if (defined(rect.strokeWidth)) { + rect.strokeWidth = strokeWidth; + } + return rect; + }, + + /** + * Set styles for the element. In addition to CSS styles supported by + * native SVG and HTML elements, there are also some custom made for + * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text + * elements. + * @param {CSSObject} styles The new CSS styles. + * @returns {SVGElement} Return the SVG element for chaining. + * + * @sample highcharts/members/renderer-text-on-chart/ + * Styled text + */ + css: function (styles) { + var oldStyles = this.styles, + newStyles = {}, + elem = this.element, + textWidth, + serializedCss = '', + hyphenate, + hasNew = !oldStyles, + // These CSS properties are interpreted internally by the SVG + // renderer, but are not supported by SVG and should not be added to + // the DOM. In styled mode, no CSS should find its way to the DOM + // whatsoever (#6173, #6474). + svgPseudoProps = ['textOutline', 'textOverflow', 'width']; + + // convert legacy + if (styles && styles.color) { + styles.fill = styles.color; + } + + // Filter out existing styles to increase performance (#2640) + if (oldStyles) { + objectEach(styles, function (style, n) { + if (style !== oldStyles[n]) { + newStyles[n] = style; + hasNew = true; + } + }); + } + if (hasNew) { + + // Merge the new styles with the old ones + if (oldStyles) { + styles = extend( + oldStyles, + newStyles + ); + } + + // Get the text width from style + textWidth = this.textWidth = ( + styles && + styles.width && + styles.width !== 'auto' && + elem.nodeName.toLowerCase() === 'text' && + pInt(styles.width) + ); + + // store object + this.styles = styles; + + if (textWidth && (!svg && this.renderer.forExport)) { + delete styles.width; + } + + // Serialize and set style attribute + if (elem.namespaceURI === this.SVG_NS) { // #7633 + hyphenate = function (a, b) { + return '-' + b.toLowerCase(); + }; + objectEach(styles, function (style, n) { + if (inArray(n, svgPseudoProps) === -1) { + serializedCss += + n.replace(/([A-Z])/g, hyphenate) + ':' + + style + ';'; + } + }); + if (serializedCss) { + attr(elem, 'style', serializedCss); // #1881 + } + } else { + css(elem, styles); + } + + + if (this.added) { + + // Rebuild text after added. Cache mechanisms in the buildText + // will prevent building if there are no significant changes. + if (this.element.nodeName === 'text') { + this.renderer.buildText(this); + } + + // Apply text outline after added + if (styles && styles.textOutline) { + this.applyTextOutline(styles.textOutline); + } + } + } + + return this; + }, + + /*= if (build.classic) { =*/ + /** + * Get the current stroke width. In classic mode, the setter registers it + * directly on the element. + * @returns {number} The stroke width in pixels. + * @ignore + */ + strokeWidth: function () { + return this['stroke-width'] || 0; + }, + + /*= } else { =*/ + /** + * Get the computed style. Only in styled mode. + * @param {string} prop - The property name to check for. + * @returns {string} The current computed value. + * @example + * chart.series[0].points[0].graphic.getStyle('stroke-width'); // => '1px' + */ + getStyle: function (prop) { + return win.getComputedStyle(this.element || this, '') + .getPropertyValue(prop); + }, + + /** + * Get the computed stroke width in pixel values. This is used extensively + * when drawing shapes to ensure the shapes are rendered crisp and + * positioned correctly relative to each other. Using + * `shape-rendering: crispEdges` leaves us less control over positioning, + * for example when we want to stack columns next to each other, or position + * things pixel-perfectly within the plot box. + * + * The common pattern when placing a shape is: + * * Create the SVGElement and add it to the DOM. In styled mode, it will + * now receive a stroke width from the style sheet. In classic mode we + * will add the `stroke-width` attribute. + * * Read the computed `elem.strokeWidth()`. + * * Place it based on the stroke width. + * + * @returns {Number} The stroke width in pixels. Even if the given stroke + * widtch (in CSS or by attributes) is based on `em` or other units, the + * pixel size is returned. + */ + strokeWidth: function () { + var val = this.getStyle('stroke-width'), + ret, + dummy; + + // Read pixel values directly + if (val.indexOf('px') === val.length - 2) { + ret = pInt(val); + + // Other values like em, pt etc need to be measured + } else { + dummy = doc.createElementNS(SVG_NS, 'rect'); + attr(dummy, { + 'width': val, + 'stroke-width': 0 + }); + this.element.parentNode.appendChild(dummy); + ret = dummy.getBBox().width; + dummy.parentNode.removeChild(dummy); + } + return ret; + }, + /*= } =*/ + /** + * Add an event listener. This is a simple setter that replaces all other + * events of the same type, opposed to the {@link Highcharts#addEvent} + * function. + * @param {string} eventType - The event type. If the type is `click`, + * Highcharts will internally translate it to a `touchstart` event on + * touch devices, to prevent the browser from waiting for a click event + * from firing. + * @param {Function} handler - The handler callback. + * @returns {SVGElement} The SVGElement for chaining. + * + * @sample highcharts/members/element-on/ + * A clickable rectangle + */ + on: function (eventType, handler) { + var svgElement = this, + element = svgElement.element; + + // touch + if (hasTouch && eventType === 'click') { + element.ontouchstart = function (e) { + svgElement.touchEventFired = Date.now(); // #2269 + e.preventDefault(); + handler.call(element, e); + }; + element.onclick = function (e) { + if (win.navigator.userAgent.indexOf('Android') === -1 || + Date.now() - (svgElement.touchEventFired || 0) > 1100) { + handler.call(element, e); + } + }; + } else { + // simplest possible event model for internal use + element['on' + eventType] = handler; + } + return this; + }, + + /** + * Set the coordinates needed to draw a consistent radial gradient across + * a shape regardless of positioning inside the chart. Used on pie slices + * to make all the slices have the same radial reference point. + * + * @param {Array} coordinates The center reference. The format is + * `[centerX, centerY, diameter]` in pixels. + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + setRadialReference: function (coordinates) { + var existingGradient = this.renderer.gradients[this.element.gradient]; + + this.element.radialReference = coordinates; + + // On redrawing objects with an existing gradient, the gradient needs + // to be repositioned (#3801) + if (existingGradient && existingGradient.radAttr) { + existingGradient.animate( + this.renderer.getRadialAttr( + coordinates, + existingGradient.radAttr + ) + ); + } + + return this; + }, + + /** + * Move an object and its children by x and y values. + * + * @param {number} x - The x value. + * @param {number} y - The y value. + */ + translate: function (x, y) { + return this.attr({ + translateX: x, + translateY: y + }); + }, + + /** + * Invert a group, rotate and flip. This is used internally on inverted + * charts, where the points and graphs are drawn as if not inverted, then + * the series group elements are inverted. + * + * @param {boolean} inverted + * Whether to invert or not. An inverted shape can be un-inverted by + * setting it to false. + * @return {SVGElement} + * Return the SVGElement for chaining. + */ + invert: function (inverted) { + var wrapper = this; + wrapper.inverted = inverted; + wrapper.updateTransform(); + return wrapper; + }, + + /** + * Update the transform attribute based on internal properties. Deals with + * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY` + * attributes and updates the SVG `transform` attribute. + * @private + * + */ + updateTransform: function () { + var wrapper = this, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + scaleX = wrapper.scaleX, + scaleY = wrapper.scaleY, + inverted = wrapper.inverted, + rotation = wrapper.rotation, + matrix = wrapper.matrix, + element = wrapper.element, + transform; + + // Flipping affects translate as adjustment for flipping around the + // group's axis + if (inverted) { + translateX += wrapper.width; + translateY += wrapper.height; + } + + // Apply translate. Nearly all transformed elements have translation, + // so instead of checking for translate = 0, do it always (#1767, + // #1846). + transform = ['translate(' + translateX + ',' + translateY + ')']; + + // apply matrix + if (defined(matrix)) { + transform.push( + 'matrix(' + matrix.join(',') + ')' + ); + } + + // apply rotation + if (inverted) { + transform.push('rotate(90) scale(-1,1)'); + } else if (rotation) { // text rotation + transform.push( + 'rotate(' + rotation + ' ' + + pick(this.rotationOriginX, element.getAttribute('x'), 0) + + ' ' + + pick(this.rotationOriginY, element.getAttribute('y') || 0) + ')' + ); + } + + // apply scale + if (defined(scaleX) || defined(scaleY)) { + transform.push( + 'scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')' + ); + } + + if (transform.length) { + element.setAttribute('transform', transform.join(' ')); + } + }, + + /** + * Bring the element to the front. Alternatively, a new zIndex can be set. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @sample highcharts/members/element-tofront/ + * Click an element to bring it to front + */ + toFront: function () { + var element = this.element; + element.parentNode.appendChild(element); + return this; + }, + + + /** + * Align the element relative to the chart or another box. + * + * @param {Object} [alignOptions] The alignment options. The function can be + * called without this parameter in order to re-align an element after the + * box has been updated. + * @param {string} [alignOptions.align=left] Horizontal alignment. Can be + * one of `left`, `center` and `right`. + * @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can + * be one of `top`, `middle` and `bottom`. + * @param {number} [alignOptions.x=0] Horizontal pixel offset from + * alignment. + * @param {number} [alignOptions.y=0] Vertical pixel offset from alignment. + * @param {Boolean} [alignByTranslate=false] Use the `transform` attribute + * with translateX and translateY custom attributes to align this elements + * rather than `x` and `y` attributes. + * @param {String|Object} box The box to align to, needs a width and height. + * When the box is a string, it refers to an object in the Renderer. For + * example, when box is `spacingBox`, it refers to `Renderer.spacingBox` + * which holds `width`, `height`, `x` and `y` properties. + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + align: function (alignOptions, alignByTranslate, box) { + var align, + vAlign, + x, + y, + attribs = {}, + alignTo, + renderer = this.renderer, + alignedObjects = renderer.alignedObjects, + alignFactor, + vAlignFactor; + + // First call on instanciate + if (alignOptions) { + this.alignOptions = alignOptions; + this.alignByTranslate = alignByTranslate; + if (!box || isString(box)) { + this.alignTo = alignTo = box || 'renderer'; + // prevent duplicates, like legendGroup after resize + erase(alignedObjects, this); + alignedObjects.push(this); + box = null; // reassign it below + } + + // When called on resize, no arguments are supplied + } else { + alignOptions = this.alignOptions; + alignByTranslate = this.alignByTranslate; + alignTo = this.alignTo; + } + + box = pick(box, renderer[alignTo], renderer); + + // Assign variables + align = alignOptions.align; + vAlign = alignOptions.verticalAlign; + x = (box.x || 0) + (alignOptions.x || 0); // default: left align + y = (box.y || 0) + (alignOptions.y || 0); // default: top align + + // Align + if (align === 'right') { + alignFactor = 1; + } else if (align === 'center') { + alignFactor = 2; + } + if (alignFactor) { + x += (box.width - (alignOptions.width || 0)) / alignFactor; + } + attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x); + + + // Vertical align + if (vAlign === 'bottom') { + vAlignFactor = 1; + } else if (vAlign === 'middle') { + vAlignFactor = 2; + } + if (vAlignFactor) { + y += (box.height - (alignOptions.height || 0)) / vAlignFactor; + } + attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y); + + // Animate only if already placed + this[this.placed ? 'animate' : 'attr'](attribs); + this.placed = true; + this.alignAttr = attribs; + + return this; + }, + + /** + * Get the bounding box (width, height, x and y) for the element. Generally + * used to get rendered text size. Since this is called a lot in charts, + * the results are cached based on text properties, in order to save DOM + * traffic. The returned bounding box includes the rotation, so for example + * a single text line of rotation 90 will report a greater height, and a + * width corresponding to the line-height. + * + * @param {boolean} [reload] Skip the cache and get the updated DOM bouding + * box. + * @param {number} [rot] Override the element's rotation. This is internally + * used on axis labels with a value of 0 to find out what the bounding box + * would be have been if it were not rotated. + * @returns {Object} The bounding box with `x`, `y`, `width` and `height` + * properties. + * + * @sample highcharts/members/renderer-on-chart/ + * Draw a rectangle based on a text's bounding box + */ + getBBox: function (reload, rot) { + var wrapper = this, + bBox, // = wrapper.bBox, + renderer = wrapper.renderer, + width, + height, + rotation, + rad, + element = wrapper.element, + styles = wrapper.styles, + fontSize, + textStr = wrapper.textStr, + toggleTextShadowShim, + cache = renderer.cache, + cacheKeys = renderer.cacheKeys, + cacheKey; + + rotation = pick(rot, wrapper.rotation); + rad = rotation * deg2rad; + + /*= if (build.classic) { =*/ + fontSize = styles && styles.fontSize; + /*= } else { =*/ + fontSize = element && + SVGElement.prototype.getStyle.call(element, 'font-size'); + /*= } =*/ + + // Avoid undefined and null (#7316) + if (defined(textStr)) { + + cacheKey = textStr.toString(); + + // Since numbers are monospaced, and numerical labels appear a lot + // in a chart, we assume that a label of n characters has the same + // bounding box as others of the same length. Unless there is inner + // HTML in the label. In that case, leave the numbers as is (#5899). + if (cacheKey.indexOf('<') === -1) { + cacheKey = cacheKey.replace(/[0-9]/g, '0'); + } + + // Properties that affect bounding box + cacheKey += [ + '', + rotation || 0, + fontSize, + wrapper.textWidth, // #7874, also useHTML + styles && styles.textOverflow // #5968 + ] + .join(','); + + } + + if (cacheKey && !reload) { + bBox = cache[cacheKey]; + } + + // No cache found + if (!bBox) { + + // SVG elements + if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) { + try { // Fails in Firefox if the container has display: none. + + // When the text shadow shim is used, we need to hide the + // fake shadows to get the correct bounding box (#3872) + toggleTextShadowShim = this.fakeTS && function (display) { + each( + element.querySelectorAll( + '.highcharts-text-outline' + ), + function (tspan) { + tspan.style.display = display; + } + ); + }; + + // Workaround for #3842, Firefox reporting wrong bounding + // box for shadows + if (toggleTextShadowShim) { + toggleTextShadowShim('none'); + } + + bBox = element.getBBox ? + // SVG: use extend because IE9 is not allowed to change + // width and height in case of rotation (below) + extend({}, element.getBBox()) : { + + // Legacy IE in export mode + width: element.offsetWidth, + height: element.offsetHeight + }; + + // #3842 + if (toggleTextShadowShim) { + toggleTextShadowShim(''); + } + } catch (e) {} + + // If the bBox is not set, the try-catch block above failed. The + // other condition is for Opera that returns a width of + // -Infinity on hidden elements. + if (!bBox || bBox.width < 0) { + bBox = { width: 0, height: 0 }; + } + + + // VML Renderer or useHTML within SVG + } else { + + bBox = wrapper.htmlGetBBox(); + + } + + // True SVG elements as well as HTML elements in modern browsers + // using the .useHTML option need to compensated for rotation + if (renderer.isSVG) { + width = bBox.width; + height = bBox.height; + + // Workaround for wrong bounding box in IE, Edge and Chrome on + // Windows. With Highcharts' default font, IE and Edge report + // a box height of 16.899 and Chrome rounds it to 17. If this + // stands uncorrected, it results in more padding added below + // the text than above when adding a label border or background. + // Also vertical positioning is affected. + // http://jsfiddle.net/highcharts/em37nvuj/ + // (#1101, #1505, #1669, #2568, #6213). + if ( + styles && + styles.fontSize === '11px' && + Math.round(height) === 17 + ) { + bBox.height = height = 14; + } + + // Adjust for rotated text + if (rotation) { + bBox.width = Math.abs(height * Math.sin(rad)) + + Math.abs(width * Math.cos(rad)); + bBox.height = Math.abs(height * Math.cos(rad)) + + Math.abs(width * Math.sin(rad)); + } + } + + // Cache it. When loading a chart in a hidden iframe in Firefox and + // IE/Edge, the bounding box height is 0, so don't cache it (#5620). + if (cacheKey && bBox.height > 0) { + + // Rotate (#4681) + while (cacheKeys.length > 250) { + delete cache[cacheKeys.shift()]; + } + + if (!cache[cacheKey]) { + cacheKeys.push(cacheKey); + } + cache[cacheKey] = bBox; + } + } + return bBox; + }, + + /** + * Show the element after it has been hidden. + * + * @param {boolean} [inherit=false] Set the visibility attribute to + * `inherit` rather than `visible`. The difference is that an element with + * `visibility="visible"` will be visible even if the parent is hidden. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + show: function (inherit) { + return this.attr({ visibility: inherit ? 'inherit' : 'visible' }); + }, + + /** + * Hide the element, equivalent to setting the `visibility` attribute to + * `hidden`. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + hide: function () { + return this.attr({ visibility: 'hidden' }); + }, + + /** + * Fade out an element by animating its opacity down to 0, and hide it on + * complete. Used internally for the tooltip. + * + * @param {number} [duration=150] The fade duration in milliseconds. + */ + fadeOut: function (duration) { + var elemWrapper = this; + elemWrapper.animate({ + opacity: 0 + }, { + duration: duration || 150, + complete: function () { + // #3088, assuming we're only using this for tooltips + elemWrapper.attr({ y: -9999 }); + } + }); + }, + + /** + * Add the element to the DOM. All elements must be added this way. + * + * @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to. + * If undefined, the element is added to the {@link + * Highcharts.SVGRenderer.box}. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @sample highcharts/members/renderer-g - Elements added to a group + */ + add: function (parent) { + + var renderer = this.renderer, + element = this.element, + inserted; + + if (parent) { + this.parentGroup = parent; + } + + // mark as inverted + this.parentInverted = parent && parent.inverted; + + // build formatted text + if (this.textStr !== undefined) { + renderer.buildText(this); + } + + // Mark as added + this.added = true; + + // If we're adding to renderer root, or other elements in the group + // have a z index, we need to handle it + if (!parent || parent.handleZ || this.zIndex) { + inserted = this.zIndexSetter(); + } + + // If zIndex is not handled, append at the end + if (!inserted) { + (parent ? parent.element : renderer.box).appendChild(element); + } + + // fire an event for internal hooks + if (this.onAdd) { + this.onAdd(); + } + + return this; + }, + + /** + * Removes an element from the DOM. + * + * @private + * @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove. + */ + safeRemoveChild: function (element) { + var parentNode = element.parentNode; + if (parentNode) { + parentNode.removeChild(element); + } + }, + + /** + * Destroy the element and element wrapper and clear up the DOM and event + * hooks. + * + * + */ + destroy: function () { + var wrapper = this, + element = wrapper.element || {}, + parentToClean = + wrapper.renderer.isSVG && + element.nodeName === 'SPAN' && + wrapper.parentGroup, + grandParent, + ownerSVGElement = element.ownerSVGElement, + i, + clipPath = wrapper.clipPath; + + // remove events + element.onclick = element.onmouseout = element.onmouseover = + element.onmousemove = element.point = null; + stop(wrapper); // stop running animations + + if (clipPath && ownerSVGElement) { + // Look for existing references to this clipPath and remove them + // before destroying the element (#6196). + each( + // The upper case version is for Edge + ownerSVGElement.querySelectorAll('[clip-path],[CLIP-PATH]'), + function (el) { + var clipPathAttr = el.getAttribute('clip-path'), + clipPathId = clipPath.element.id; + // Include the closing paranthesis in the test to rule out + // id's from 10 and above (#6550). Edge puts quotes inside + // the url, others not. + if ( + clipPathAttr.indexOf('(#' + clipPathId + ')') > -1 || + clipPathAttr.indexOf('("#' + clipPathId + '")') > -1 + ) { + el.removeAttribute('clip-path'); + } + } + ); + wrapper.clipPath = clipPath.destroy(); + } + + // Destroy stops in case this is a gradient object + if (wrapper.stops) { + for (i = 0; i < wrapper.stops.length; i++) { + wrapper.stops[i] = wrapper.stops[i].destroy(); + } + wrapper.stops = null; + } + + // remove element + wrapper.safeRemoveChild(element); + + /*= if (build.classic) { =*/ + wrapper.destroyShadows(); + /*= } =*/ + + // In case of useHTML, clean up empty containers emulating SVG groups + // (#1960, #2393, #2697). + while ( + parentToClean && + parentToClean.div && + parentToClean.div.childNodes.length === 0 + ) { + grandParent = parentToClean.parentGroup; + wrapper.safeRemoveChild(parentToClean.div); + delete parentToClean.div; + parentToClean = grandParent; + } + + // remove from alignObjects + if (wrapper.alignTo) { + erase(wrapper.renderer.alignedObjects, wrapper); + } + + objectEach(wrapper, function (val, key) { + delete wrapper[key]; + }); + + return null; + }, + + /*= if (build.classic) { =*/ + /** + * @typedef {Object} ShadowOptions + * @property {string} [color=${palette.neutralColor100}] The shadow color. + * @property {number} [offsetX=1] The horizontal offset from the element. + * @property {number} [offsetY=1] The vertical offset from the element. + * @property {number} [opacity=0.15] The shadow opacity. + * @property {number} [width=3] The shadow width or distance from the + * element. + */ + /** + * Add a shadow to the element. Must be called after the element is added to + * the DOM. In styled mode, this method is not used, instead use `defs` and + * filters. + * + * @param {boolean|ShadowOptions} shadowOptions The shadow options. If + * `true`, the default options are applied. If `false`, the current + * shadow will be removed. + * @param {SVGElement} [group] The SVG group element where the shadows will + * be applied. The default is to add it to the same parent as the current + * element. Internally, this is ised for pie slices, where all the + * shadows are added to an element behind all the slices. + * @param {boolean} [cutOff] Used internally for column shadows. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @example + * renderer.rect(10, 100, 100, 100) + * .attr({ fill: 'red' }) + * .shadow(true); + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + shadow, + element = this.element, + strokeWidth, + shadowWidth, + shadowElementOpacity, + + // compensate for inverted plot area + transform; + + if (!shadowOptions) { + this.destroyShadows(); + + } else if (!this.shadows) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / + shadowWidth; + transform = this.parentInverted ? + '(-1,-1)' : + '(' + pick(shadowOptions.offsetX, 1) + ', ' + + pick(shadowOptions.offsetY, 1) + ')'; + for (i = 1; i <= shadowWidth; i++) { + shadow = element.cloneNode(0); + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + attr(shadow, { + 'isShadow': 'true', + 'stroke': + shadowOptions.color || '${palette.neutralColor100}', + 'stroke-opacity': shadowElementOpacity * i, + 'stroke-width': strokeWidth, + 'transform': 'translate' + transform, + 'fill': 'none' + }); + if (cutOff) { + attr( + shadow, + 'height', + Math.max(attr(shadow, 'height') - strokeWidth, 0) + ); + shadow.cutHeight = strokeWidth; + } + + if (group) { + group.element.appendChild(shadow); + } else if (element.parentNode) { + element.parentNode.insertBefore(shadow, element); + } + + shadows.push(shadow); + } + + this.shadows = shadows; + } + return this; + + }, + + /** + * Destroy shadows on the element. + * @private + */ + destroyShadows: function () { + each(this.shadows || [], function (shadow) { + this.safeRemoveChild(shadow); + }, this); + this.shadows = undefined; + }, + + /*= } =*/ + + xGetter: function (key) { + if (this.element.nodeName === 'circle') { + if (key === 'x') { + key = 'cx'; + } else if (key === 'y') { + key = 'cy'; + } + } + return this._defaultGetter(key); + }, + + /** + * Get the current value of an attribute or pseudo attribute, used mainly + * for animation. Called internally from the {@link + * Highcharts.SVGRenderer#attr} + * function. + * + * @private + */ + _defaultGetter: function (key) { + var ret = pick( + this[key + 'Value'], // align getter + this[key], + this.element ? this.element.getAttribute(key) : null, + 0 + ); + + if (/^[\-0-9\.]+$/.test(ret)) { // is numerical + ret = parseFloat(ret); + } + return ret; + }, + + + dSetter: function (value, key, element) { + if (value && value.join) { // join path + value = value.join(' '); + } + if (/(NaN| {2}|^$)/.test(value)) { + value = 'M 0 0'; + } + + // Check for cache before resetting. Resetting causes disturbance in the + // DOM, causing flickering in some cases in Edge/IE (#6747). Also + // possible performance gain. + if (this[key] !== value) { + element.setAttribute(key, value); + this[key] = value; + } + + }, + /*= if (build.classic) { =*/ + dashstyleSetter: function (value) { + var i, + strokeWidth = this['stroke-width']; + + // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new + // strokeWidth function, we should be able to use that instead. + if (strokeWidth === 'inherit') { + strokeWidth = 1; + } + value = value && value.toLowerCase(); + if (value) { + value = value + .replace('shortdashdotdot', '3,1,1,1,1,1,') + .replace('shortdashdot', '3,1,1,1') + .replace('shortdot', '1,1,') + .replace('shortdash', '3,1,') + .replace('longdash', '8,3,') + .replace(/dot/g, '1,3,') + .replace('dash', '4,3,') + .replace(/,$/, '') + .split(','); // ending comma + + i = value.length; + while (i--) { + value[i] = pInt(value[i]) * strokeWidth; + } + value = value.join(',') + .replace(/NaN/g, 'none'); // #3226 + this.element.setAttribute('stroke-dasharray', value); + } + }, + /*= } =*/ + alignSetter: function (value) { + var convert = { left: 'start', center: 'middle', right: 'end' }; + this.alignValue = value; + this.element.setAttribute('text-anchor', convert[value]); + }, + opacitySetter: function (value, key, element) { + this[key] = value; + element.setAttribute(key, value); + }, + titleSetter: function (value) { + var titleNode = this.element.getElementsByTagName('title')[0]; + if (!titleNode) { + titleNode = doc.createElementNS(this.SVG_NS, 'title'); + this.element.appendChild(titleNode); + } + + // Remove text content if it exists + if (titleNode.firstChild) { + titleNode.removeChild(titleNode.firstChild); + } + + titleNode.appendChild( + doc.createTextNode( + // #3276, #3895 + (String(pick(value), '')) + .replace(/<[^>]*>/g, '') + .replace(/</g, '<') + .replace(/>/g, '>') + ) + ); + }, + textSetter: function (value) { + if (value !== this.textStr) { + // Delete bBox memo when the text changes + delete this.bBox; + + this.textStr = value; + if (this.added) { + this.renderer.buildText(this); + } + } + }, + fillSetter: function (value, key, element) { + if (typeof value === 'string') { + element.setAttribute(key, value); + } else if (value) { + this.complexColor(value, key, element); + } + }, + visibilitySetter: function (value, key, element) { + // IE9-11 doesn't handle visibilty:inherit well, so we remove the + // attribute instead (#2881, #3909) + if (value === 'inherit') { + element.removeAttribute(key); + } else if (this[key] !== value) { // #6747 + element.setAttribute(key, value); + } + this[key] = value; + }, + zIndexSetter: function (value, key) { + var renderer = this.renderer, + parentGroup = this.parentGroup, + parentWrapper = parentGroup || renderer, + parentNode = parentWrapper.element || renderer.box, + childNodes, + otherElement, + otherZIndex, + element = this.element, + inserted, + undefinedOtherZIndex, + svgParent = parentNode === renderer.box, + run = this.added, + i; + + if (defined(value)) { + // So we can read it for other elements in the group + element.zIndex = value; + + value = +value; + if (this[key] === value) { // Only update when needed (#3865) + run = false; + } + this[key] = value; + } + + // Insert according to this and other elements' zIndex. Before .add() is + // called, nothing is done. Then on add, or by later calls to + // zIndexSetter, the node is placed on the right place in the DOM. + if (run) { + value = this.zIndex; + + if (value && parentGroup) { + parentGroup.handleZ = true; + } + + childNodes = parentNode.childNodes; + for (i = childNodes.length - 1; i >= 0 && !inserted; i--) { + otherElement = childNodes[i]; + otherZIndex = otherElement.zIndex; + undefinedOtherZIndex = !defined(otherZIndex); + + if (otherElement !== element) { + if ( + // Negative zIndex versus no zIndex: + // On all levels except the highest. If the parent is + // , then we don't want to put items before + // or + (value < 0 && undefinedOtherZIndex && !svgParent && !i) + ) { + parentNode.insertBefore(element, childNodes[i]); + inserted = true; + } else if ( + // Insert after the first element with a lower zIndex + pInt(otherZIndex) <= value || + // If negative zIndex, add this before first undefined + // zIndex element + ( + undefinedOtherZIndex && + (!defined(value) || value >= 0) + ) + ) { + parentNode.insertBefore( + element, + childNodes[i + 1] || null // null for oldIE export + ); + inserted = true; + } + } + } + + if (!inserted) { + parentNode.insertBefore( + element, + childNodes[svgParent ? 3 : 0] || null // null for oldIE + ); + inserted = true; + } + } + return inserted; + }, + _defaultSetter: function (value, key, element) { + element.setAttribute(key, value); + } }); // Some shared setters and getters @@ -1907,12 +1907,12 @@ SVGElement.prototype.translateYSetter = SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter = SVGElement.prototype.rotationOriginXSetter = -SVGElement.prototype.rotationOriginYSetter = +SVGElement.prototype.rotationOriginYSetter = SVGElement.prototype.scaleXSetter = -SVGElement.prototype.scaleYSetter = +SVGElement.prototype.scaleYSetter = SVGElement.prototype.matrixSetter = function (value, key) { - this[key] = value; - this.doTransform = true; + this[key] = value; + this.doTransform = true; }; /*= if (build.classic) { =*/ @@ -1920,24 +1920,24 @@ SVGElement.prototype.matrixSetter = function (value, key) { // we remove the stroke attribute altogether. #1270, #1369, #3065, #3072. SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function (value, key, element) { - this[key] = value; - // Only apply the stroke attribute if the stroke width is defined and larger - // than 0 - if (this.stroke && this['stroke-width']) { - // Use prototype as instance may be overridden - SVGElement.prototype.fillSetter.call( - this, - this.stroke, - 'stroke', - element - ); - - element.setAttribute('stroke-width', this['stroke-width']); - this.hasStroke = true; - } else if (key === 'stroke-width' && value === 0 && this.hasStroke) { - element.removeAttribute('stroke'); - this.hasStroke = false; - } + this[key] = value; + // Only apply the stroke attribute if the stroke width is defined and larger + // than 0 + if (this.stroke && this['stroke-width']) { + // Use prototype as instance may be overridden + SVGElement.prototype.fillSetter.call( + this, + this.stroke, + 'stroke', + element + ); + + element.setAttribute('stroke-width', this['stroke-width']); + this.hasStroke = true; + } else if (key === 'stroke-width' && value === 0 && this.hasStroke) { + element.removeAttribute('stroke'); + this.hasStroke = false; + } }; /*= } =*/ @@ -1971,2308 +1971,2308 @@ SVGElement.prototype.strokeSetter = function (value, key, element) { * @class Highcharts.SVGRenderer */ SVGRenderer = H.SVGRenderer = function () { - this.init.apply(this, arguments); + this.init.apply(this, arguments); }; extend(SVGRenderer.prototype, /** @lends Highcharts.SVGRenderer.prototype */ { - /** - * A pointer to the renderer's associated Element class. The VMLRenderer - * will have a pointer to VMLElement here. - * @type {SVGElement} - */ - Element: SVGElement, - SVG_NS: SVG_NS, - /** - * Initialize the SVGRenderer. Overridable initiator function that takes - * the same parameters as the constructor. - */ - init: function (container, width, height, style, forExport, allowHTML) { - var renderer = this, - boxWrapper, - element, - desc; - - boxWrapper = renderer.createElement('svg') - .attr({ - 'version': '1.1', - 'class': 'highcharts-root' - }) - /*= if (build.classic) { =*/ - .css(this.getStyle(style)) - /*= } =*/; - element = boxWrapper.element; - container.appendChild(element); - - // Always use ltr on the container, otherwise text-anchor will be - // flipped and text appear outside labels, buttons, tooltip etc (#3482) - attr(container, 'dir', 'ltr'); - - // For browsers other than IE, add the namespace attribute (#1978) - if (container.innerHTML.indexOf('xmlns') === -1) { - attr(element, 'xmlns', this.SVG_NS); - } - - // object properties - renderer.isSVG = true; - - /** - * The root `svg` node of the renderer. - * @name box - * @memberOf SVGRenderer - * @type {SVGDOMElement} - */ - this.box = element; - /** - * The wrapper for the root `svg` node of the renderer. - * - * @name boxWrapper - * @memberOf SVGRenderer - * @type {SVGElement} - */ - this.boxWrapper = boxWrapper; - renderer.alignedObjects = []; - - /** - * Page url used for internal references. - * @type {string} - */ - // #24, #672, #1070 - this.url = ( - (isFirefox || isWebKit) && - doc.getElementsByTagName('base').length - ) ? - win.location.href - .replace(/#.*?$/, '') // remove the hash - .replace(/<[^>]*>/g, '') // wing cut HTML - // escape parantheses and quotes - .replace(/([\('\)])/g, '\\$1') - // replace spaces (needed for Safari only) - .replace(/ /g, '%20') : - ''; - - // Add description - desc = this.createElement('desc').add(); - desc.element.appendChild( - doc.createTextNode('Created with @product.name@ @product.version@') - ); - - /** - * A pointer to the `defs` node of the root SVG. - * @type {SVGElement} - * @name defs - * @memberOf SVGRenderer - */ - renderer.defs = this.createElement('defs').add(); - renderer.allowHTML = allowHTML; - renderer.forExport = forExport; - renderer.gradients = {}; // Object where gradient SvgElements are stored - renderer.cache = {}; // Cache for numerical bounding boxes - renderer.cacheKeys = []; - renderer.imgCount = 0; - - renderer.setSize(width, height, false); - - - - // Issue 110 workaround: - // In Firefox, if a div is positioned by percentage, its pixel position - // may land between pixels. The container itself doesn't display this, - // but an SVG element inside this container will be drawn at subpixel - // precision. In order to draw sharp lines, this must be compensated - // for. This doesn't seem to work inside iframes though (like in - // jsFiddle). - var subPixelFix, rect; - if (isFirefox && container.getBoundingClientRect) { - subPixelFix = function () { - css(container, { left: 0, top: 0 }); - rect = container.getBoundingClientRect(); - css(container, { - left: (Math.ceil(rect.left) - rect.left) + 'px', - top: (Math.ceil(rect.top) - rect.top) + 'px' - }); - }; - - // run the fix now - subPixelFix(); - - // run it on resize - renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix); - } - }, - /*= if (!build.classic) { =*/ - /** - * General method for adding a definition to the SVG `defs` tag. Can be used - * for gradients, fills, filters etc. Styled mode only. A hook for adding - * general definitions to the SVG's defs tag. Definitions can be - * referenced from the CSS by its `id`. Read more in - * [gradients, shadows and patterns]{@link http://www.highcharts.com/docs/ - * chart-design-and-style/gradients-shadows-and-patterns}. - * Styled mode only. - * - * @param {Object} def - A serialized form of an SVG definition, including - * children - * - * @return {SVGElement} The inserted node. - */ - definition: function (def) { - var ren = this; - - function recurse(config, parent) { - var ret; - each(splat(config), function (item) { - var node = ren.createElement(item.tagName), - attr = {}; - - // Set attributes - objectEach(item, function (val, key) { - if ( - key !== 'tagName' && - key !== 'children' && - key !== 'textContent' - ) { - attr[key] = val; - } - }); - node.attr(attr); - - // Add to the tree - node.add(parent || ren.defs); - - // Add text content - if (item.textContent) { - node.element.appendChild( - doc.createTextNode(item.textContent) - ); - } - - // Recurse - recurse(item.children || [], node); - - ret = node; - }); - - // Return last node added (on top level it's the only one) - return ret; - } - return recurse(def); - }, - /*= } =*/ - - /*= if (build.classic) { =*/ - /** - * Get the global style setting for the renderer. - * @private - * @param {CSSObject} style - Style settings. - * @return {CSSObject} The style settings mixed with defaults. - */ - getStyle: function (style) { - this.style = extend({ - - fontFamily: '"Lucida Grande", "Lucida Sans Unicode", ' + - 'Arial, Helvetica, sans-serif', - fontSize: '12px' - - }, style); - return this.style; - }, - /** - * Apply the global style on the renderer, mixed with the default styles. - * - * @param {CSSObject} style - CSS to apply. - */ - setStyle: function (style) { - this.boxWrapper.css(this.getStyle(style)); - }, - /*= } =*/ - - /** - * Detect whether the renderer is hidden. This happens when one of the - * parent elements has `display: none`. Used internally to detect when we - * needto render preliminarily in another div to get the text bounding boxes - * right. - * - * @returns {boolean} True if it is hidden. - */ - isHidden: function () { // #608 - return !this.boxWrapper.getBBox().width; - }, - - /** - * Destroys the renderer and its allocated members. - */ - destroy: function () { - var renderer = this, - rendererDefs = renderer.defs; - renderer.box = null; - renderer.boxWrapper = renderer.boxWrapper.destroy(); - - // Call destroy on all gradient elements - destroyObjectProperties(renderer.gradients || {}); - renderer.gradients = null; - - // Defs are null in VMLRenderer - // Otherwise, destroy them here. - if (rendererDefs) { - renderer.defs = rendererDefs.destroy(); - } - - // Remove sub pixel fix handler (#982) - if (renderer.unSubPixelFix) { - renderer.unSubPixelFix(); - } - - renderer.alignedObjects = null; - - return null; - }, - - /** - * Create a wrapper for an SVG element. Serves as a factory for - * {@link SVGElement}, but this function is itself mostly called from - * primitive factories like {@link SVGRenderer#path}, {@link - * SVGRenderer#rect} or {@link SVGRenderer#text}. - * - * @param {string} nodeName - The node name, for example `rect`, `g` etc. - * @returns {SVGElement} The generated SVGElement. - */ - createElement: function (nodeName) { - var wrapper = new this.Element(); - wrapper.init(this, nodeName); - return wrapper; - }, - - /** - * Dummy function for plugins, called every time the renderer is updated. - * Prior to Highcharts 5, this was used for the canvg renderer. - * @function - */ - draw: noop, - - /** - * Get converted radial gradient attributes according to the radial - * reference. Used internally from the {@link SVGElement#colorGradient} - * function. - * - * @private - */ - getRadialAttr: function (radialReference, gradAttr) { - return { - cx: (radialReference[0] - radialReference[2] / 2) + - gradAttr.cx * radialReference[2], - cy: (radialReference[1] - radialReference[2] / 2) + - gradAttr.cy * radialReference[2], - r: gradAttr.r * radialReference[2] - }; - }, - - /** - * Extendable function to measure the tspan width. - * - * @private - */ - getSpanWidth: function (wrapper) { - return wrapper.getBBox(true).width; - }, - - applyEllipsis: function (wrapper, tspan, text, width) { - var renderer = this, - rotation = wrapper.rotation, - str = text, - currentIndex, - minIndex = 0, - maxIndex = text.length, - updateTSpan = function (s) { - tspan.removeChild(tspan.firstChild); - if (s) { - tspan.appendChild(doc.createTextNode(s)); - } - }, - actualWidth, - wasTooLong; - wrapper.rotation = 0; // discard rotation when computing box - actualWidth = renderer.getSpanWidth(wrapper, tspan); - wasTooLong = actualWidth > width; - if (wasTooLong) { - while (minIndex <= maxIndex) { - currentIndex = Math.ceil((minIndex + maxIndex) / 2); - str = text.substring(0, currentIndex) + '\u2026'; - updateTSpan(str); - actualWidth = renderer.getSpanWidth(wrapper, tspan); - if (minIndex === maxIndex) { - // Complete - minIndex = maxIndex + 1; - } else if (actualWidth > width) { - // Too large. Set max index to current. - maxIndex = currentIndex - 1; - } else { - // Within width. Set min index to current. - minIndex = currentIndex; - } - } - // If max index was 0 it means just ellipsis was also to large. - if (maxIndex === 0) { - // Remove ellipses. - updateTSpan(''); - } - } - wrapper.rotation = rotation; // Apply rotation again. - return wasTooLong; - }, - - /** - * A collection of characters mapped to HTML entities. When `useHTML` on an - * element is true, these entities will be rendered correctly by HTML. In - * the SVG pseudo-HTML, they need to be unescaped back to simple characters, - * so for example `<` will render as `<`. - * - * @example - * // Add support for unescaping quotes - * Highcharts.SVGRenderer.prototype.escapes['"'] = '"'; - * - * @type {Object} - */ - escapes: { - '&': '&', - '<': '<', - '>': '>', - "'": ''', // eslint-disable-line quotes - '"': '"' - }, - - /** - * Parse a simple HTML string into SVG tspans. Called internally when text - * is set on an SVGElement. The function supports a subset of HTML tags, - * CSS text features like `width`, `text-overflow`, `white-space`, and - * also attributes like `href` and `style`. - * @private - * @param {SVGElement} wrapper The parent SVGElement. - */ - buildText: function (wrapper) { - var textNode = wrapper.element, - renderer = this, - forExport = renderer.forExport, - textStr = pick(wrapper.textStr, '').toString(), - hasMarkup = textStr.indexOf('<') !== -1, - lines, - childNodes = textNode.childNodes, - wasTooLong, - parentX = attr(textNode, 'x'), - textStyles = wrapper.styles, - width = wrapper.textWidth, - textLineHeight = textStyles && textStyles.lineHeight, - textOutline = textStyles && textStyles.textOutline, - ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', - noWrap = textStyles && textStyles.whiteSpace === 'nowrap', - fontSize = textStyles && textStyles.fontSize, - textCache, - isSubsequentLine, - i = childNodes.length, - tempParent = width && !wrapper.added && this.box, - getLineHeight = function (tspan) { - var fontSizeStyle; - /*= if (build.classic) { =*/ - fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ? - tspan.style.fontSize : - (fontSize || renderer.style.fontSize || 12); - /*= } =*/ - - return textLineHeight ? - pInt(textLineHeight) : - renderer.fontMetrics( - fontSizeStyle, - // Get the computed size from parent if not explicit - tspan.getAttribute('style') ? tspan : textNode - ).h; - }, - unescapeEntities = function (inputStr, except) { - objectEach(renderer.escapes, function (value, key) { - if (!except || inArray(value, except) === -1) { - inputStr = inputStr.toString().replace( - new RegExp(value, 'g'), // eslint-disable-line security/detect-non-literal-regexp - key - ); - } - }); - return inputStr; - }, - parseAttribute = function (s, attr) { - var start, - delimiter; - - start = s.indexOf('<'); - s = s.substring(start, s.indexOf('>') - start); - - start = s.indexOf(attr + '='); - if (start !== -1) { - start = start + attr.length + 1; - delimiter = s.charAt(start); - if (delimiter === '"' || delimiter === "'") { // eslint-disable-line quotes - s = s.substring(start + 1); - return s.substring(0, s.indexOf(delimiter)); - } - } - }; - - // The buildText code is quite heavy, so if we're not changing something - // that affects the text, skip it (#6113). - textCache = [ - textStr, - ellipsis, - noWrap, - textLineHeight, - textOutline, - fontSize, - width - ].join(','); - if (textCache === wrapper.textCache) { - return; - } - wrapper.textCache = textCache; - - // Remove old text - while (i--) { - textNode.removeChild(childNodes[i]); - } - - // Skip tspans, add text directly to text node. The forceTSpan is a hook - // used in text outline hack. - if ( - !hasMarkup && - !textOutline && - !ellipsis && - !width && - textStr.indexOf(' ') === -1 - ) { - textNode.appendChild(doc.createTextNode(unescapeEntities(textStr))); - - // Complex strings, add more logic - } else { - - if (tempParent) { - // attach it to the DOM to read offset width - tempParent.appendChild(textNode); - } - - if (hasMarkup) { - lines = textStr - /*= if (build.classic) { =*/ - .replace(/<(b|strong)>/g, '') - .replace(/<(i|em)>/g, '') - /*= } else { =*/ - .replace( - /<(b|strong)>/g, - '' - ) - .replace( - /<(i|em)>/g, - '' - ) - /*= } =*/ - .replace(//g, '') - .split(//g); - - } else { - lines = [textStr]; - } - - - // Trim empty lines (#5261) - lines = grep(lines, function (line) { - return line !== ''; - }); - - - // build the lines - each(lines, function buildTextLines(line, lineNo) { - var spans, - spanNo = 0; - line = line - // Trim to prevent useless/costly process on the spaces - // (#5258) - .replace(/^\s+|\s+$/g, '') - .replace(//g, '|||'); - spans = line.split('|||'); - - each(spans, function buildTextSpans(span) { - if (span !== '' || spans.length === 1) { - var attributes = {}, - tspan = doc.createElementNS( - renderer.SVG_NS, - 'tspan' - ), - classAttribute, - styleAttribute, // #390 - hrefAttribute; - - classAttribute = parseAttribute(span, 'class'); - if (classAttribute) { - attr(tspan, 'class', classAttribute); - } - - styleAttribute = parseAttribute(span, 'style'); - if (styleAttribute) { - styleAttribute = styleAttribute.replace( - /(;| |^)color([ :])/, - '$1fill$2' - ); - attr(tspan, 'style', styleAttribute); - } - - // Not for export - #1529 - hrefAttribute = parseAttribute(span, 'href'); - if (hrefAttribute && !forExport) { - attr( - tspan, - 'onclick', - 'location.href=\"' + hrefAttribute + '\"' - ); - attr(tspan, 'class', 'highcharts-anchor'); - /*= if (build.classic) { =*/ - css(tspan, { cursor: 'pointer' }); - /*= } =*/ - } - - // Strip away unsupported HTML tags (#7126) - span = unescapeEntities( - span.replace(/<[a-zA-Z\/](.|\n)*?>/g, '') || ' ' - ); - - // Nested tags aren't supported, and cause crash in - // Safari (#1596) - if (span !== ' ') { - - // add the text node - tspan.appendChild(doc.createTextNode(span)); - - // First span in a line, align it to the left - if (!spanNo) { - if (lineNo && parentX !== null) { - attributes.x = parentX; - } - } else { - attributes.dx = 0; // #16 - } - - // add attributes - attr(tspan, attributes); - - // Append it - textNode.appendChild(tspan); - - // first span on subsequent line, add the line - // height - if (!spanNo && isSubsequentLine) { - - // allow getting the right offset height in - // exporting in IE - if (!svg && forExport) { - css(tspan, { display: 'block' }); - } - - // Set the line height based on the font size of - // either the text element or the tspan element - attr( - tspan, - 'dy', - getLineHeight(tspan) - ); - } - - /* - // Experimental text wrapping based on - // getSubstringLength - if (width) { - var spans = renderer.breakText(wrapper, width); - - each(spans, function (span) { - - var dy = getLineHeight(tspan); - tspan = doc.createElementNS( - SVG_NS, - 'tspan' - ); - tspan.appendChild( - doc.createTextNode(span) - ); - attr(tspan, { - dy: dy, - x: parentX - }); - if (spanStyle) { // #390 - attr(tspan, 'style', spanStyle); - } - textNode.appendChild(tspan); - }); - - } - // */ - - // Check width and apply soft breaks or ellipsis - if (width) { - var words = span.replace( - /([^\^])-/g, - '$1- ' - ).split(' '), // #1273 - hasWhiteSpace = ( - spans.length > 1 || - lineNo || - (words.length > 1 && !noWrap) - ), - tooLong, - rest = [], - actualWidth, - dy = getLineHeight(tspan), - rotation = wrapper.rotation; - - if (ellipsis) { - wasTooLong = renderer.applyEllipsis( - wrapper, - tspan, - span, - width - ); - } - - while ( - !ellipsis && - hasWhiteSpace && - (words.length || rest.length) - ) { - // discard rotation when computing box - wrapper.rotation = 0; - actualWidth = renderer.getSpanWidth( - wrapper, - tspan - ); - tooLong = actualWidth > width; - - // For ellipsis, do a binary search for the - // correct string length - if (wasTooLong === undefined) { - wasTooLong = tooLong; // First time - } - - // Looping down, this is the first word - // sequence that is not too long, so we can - // move on to build the next line. - if (!tooLong || words.length === 1) { - words = rest; - rest = []; - - if (words.length && !noWrap) { - tspan = doc.createElementNS( - SVG_NS, - 'tspan' - ); - attr(tspan, { - dy: dy, - x: parentX - }); - if (styleAttribute) { // #390 - attr( - tspan, - 'style', - styleAttribute - ); - } - textNode.appendChild(tspan); - } - - // a single word is pressing it out - if (actualWidth > width) { - width = actualWidth; - } - } else { // append to existing line tspan - tspan.removeChild(tspan.firstChild); - rest.unshift(words.pop()); - } - if (words.length) { - tspan.appendChild( - doc.createTextNode( - words.join(' ') - .replace(/- /g, '-') - ) - ); - } - } - wrapper.rotation = rotation; - } - - spanNo++; - } - } - }); - // To avoid beginning lines that doesn't add to the textNode - // (#6144) - isSubsequentLine = ( - isSubsequentLine || - textNode.childNodes.length - ); - }); - - if (wasTooLong) { - wrapper.attr( - 'title', - unescapeEntities(wrapper.textStr, ['<', '>']) // #7179 - ); - } - if (tempParent) { - tempParent.removeChild(textNode); - } - - // Apply the text outline - if (textOutline && wrapper.applyTextOutline) { - wrapper.applyTextOutline(textOutline); - } - } - }, - - - - /* - breakText: function (wrapper, width) { - var bBox = wrapper.getBBox(), - node = wrapper.element, - charnum = node.textContent.length, - stringWidth, - // try this position first, based on average character width - guessedLineCharLength = Math.round(width * charnum / bBox.width), - pos = guessedLineCharLength, - spans = [], - increment = 0, - startPos = 0, - endPos, - safe = 0; - - if (bBox.width > width) { - while (startPos < charnum && safe < 100) { - - while (endPos === undefined && safe < 100) { - stringWidth = node.getSubStringLength( - startPos, - pos - startPos - ); - - if (stringWidth <= width) { - if (increment === -1) { - endPos = pos; - } else { - increment = 1; - } - } else { - if (increment === 1) { - endPos = pos - 1; - } else { - increment = -1; - } - } - pos += increment; - safe++; - } - - spans.push( - node.textContent.substr(startPos, endPos - startPos) - ); - - startPos = endPos; - pos = startPos + guessedLineCharLength; - endPos = undefined; - } - } - - return spans; - }, - // */ - - /** - * Returns white for dark colors and black for bright colors. - * - * @param {ColorString} rgba - The color to get the contrast for. - * @returns {string} The contrast color, either `#000000` or `#FFFFFF`. - */ - getContrast: function (rgba) { - rgba = color(rgba).rgba; - - // The threshold may be discussed. Here's a proposal for adding - // different weight to the color channels (#6216) - /* + /** + * A pointer to the renderer's associated Element class. The VMLRenderer + * will have a pointer to VMLElement here. + * @type {SVGElement} + */ + Element: SVGElement, + SVG_NS: SVG_NS, + /** + * Initialize the SVGRenderer. Overridable initiator function that takes + * the same parameters as the constructor. + */ + init: function (container, width, height, style, forExport, allowHTML) { + var renderer = this, + boxWrapper, + element, + desc; + + boxWrapper = renderer.createElement('svg') + .attr({ + 'version': '1.1', + 'class': 'highcharts-root' + }) + /*= if (build.classic) { =*/ + .css(this.getStyle(style)) + /*= } =*/; + element = boxWrapper.element; + container.appendChild(element); + + // Always use ltr on the container, otherwise text-anchor will be + // flipped and text appear outside labels, buttons, tooltip etc (#3482) + attr(container, 'dir', 'ltr'); + + // For browsers other than IE, add the namespace attribute (#1978) + if (container.innerHTML.indexOf('xmlns') === -1) { + attr(element, 'xmlns', this.SVG_NS); + } + + // object properties + renderer.isSVG = true; + + /** + * The root `svg` node of the renderer. + * @name box + * @memberOf SVGRenderer + * @type {SVGDOMElement} + */ + this.box = element; + /** + * The wrapper for the root `svg` node of the renderer. + * + * @name boxWrapper + * @memberOf SVGRenderer + * @type {SVGElement} + */ + this.boxWrapper = boxWrapper; + renderer.alignedObjects = []; + + /** + * Page url used for internal references. + * @type {string} + */ + // #24, #672, #1070 + this.url = ( + (isFirefox || isWebKit) && + doc.getElementsByTagName('base').length + ) ? + win.location.href + .replace(/#.*?$/, '') // remove the hash + .replace(/<[^>]*>/g, '') // wing cut HTML + // escape parantheses and quotes + .replace(/([\('\)])/g, '\\$1') + // replace spaces (needed for Safari only) + .replace(/ /g, '%20') : + ''; + + // Add description + desc = this.createElement('desc').add(); + desc.element.appendChild( + doc.createTextNode('Created with @product.name@ @product.version@') + ); + + /** + * A pointer to the `defs` node of the root SVG. + * @type {SVGElement} + * @name defs + * @memberOf SVGRenderer + */ + renderer.defs = this.createElement('defs').add(); + renderer.allowHTML = allowHTML; + renderer.forExport = forExport; + renderer.gradients = {}; // Object where gradient SvgElements are stored + renderer.cache = {}; // Cache for numerical bounding boxes + renderer.cacheKeys = []; + renderer.imgCount = 0; + + renderer.setSize(width, height, false); + + + + // Issue 110 workaround: + // In Firefox, if a div is positioned by percentage, its pixel position + // may land between pixels. The container itself doesn't display this, + // but an SVG element inside this container will be drawn at subpixel + // precision. In order to draw sharp lines, this must be compensated + // for. This doesn't seem to work inside iframes though (like in + // jsFiddle). + var subPixelFix, rect; + if (isFirefox && container.getBoundingClientRect) { + subPixelFix = function () { + css(container, { left: 0, top: 0 }); + rect = container.getBoundingClientRect(); + css(container, { + left: (Math.ceil(rect.left) - rect.left) + 'px', + top: (Math.ceil(rect.top) - rect.top) + 'px' + }); + }; + + // run the fix now + subPixelFix(); + + // run it on resize + renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix); + } + }, + /*= if (!build.classic) { =*/ + /** + * General method for adding a definition to the SVG `defs` tag. Can be used + * for gradients, fills, filters etc. Styled mode only. A hook for adding + * general definitions to the SVG's defs tag. Definitions can be + * referenced from the CSS by its `id`. Read more in + * [gradients, shadows and patterns]{@link http://www.highcharts.com/docs/ + * chart-design-and-style/gradients-shadows-and-patterns}. + * Styled mode only. + * + * @param {Object} def - A serialized form of an SVG definition, including + * children + * + * @return {SVGElement} The inserted node. + */ + definition: function (def) { + var ren = this; + + function recurse(config, parent) { + var ret; + each(splat(config), function (item) { + var node = ren.createElement(item.tagName), + attr = {}; + + // Set attributes + objectEach(item, function (val, key) { + if ( + key !== 'tagName' && + key !== 'children' && + key !== 'textContent' + ) { + attr[key] = val; + } + }); + node.attr(attr); + + // Add to the tree + node.add(parent || ren.defs); + + // Add text content + if (item.textContent) { + node.element.appendChild( + doc.createTextNode(item.textContent) + ); + } + + // Recurse + recurse(item.children || [], node); + + ret = node; + }); + + // Return last node added (on top level it's the only one) + return ret; + } + return recurse(def); + }, + /*= } =*/ + + /*= if (build.classic) { =*/ + /** + * Get the global style setting for the renderer. + * @private + * @param {CSSObject} style - Style settings. + * @return {CSSObject} The style settings mixed with defaults. + */ + getStyle: function (style) { + this.style = extend({ + + fontFamily: '"Lucida Grande", "Lucida Sans Unicode", ' + + 'Arial, Helvetica, sans-serif', + fontSize: '12px' + + }, style); + return this.style; + }, + /** + * Apply the global style on the renderer, mixed with the default styles. + * + * @param {CSSObject} style - CSS to apply. + */ + setStyle: function (style) { + this.boxWrapper.css(this.getStyle(style)); + }, + /*= } =*/ + + /** + * Detect whether the renderer is hidden. This happens when one of the + * parent elements has `display: none`. Used internally to detect when we + * needto render preliminarily in another div to get the text bounding boxes + * right. + * + * @returns {boolean} True if it is hidden. + */ + isHidden: function () { // #608 + return !this.boxWrapper.getBBox().width; + }, + + /** + * Destroys the renderer and its allocated members. + */ + destroy: function () { + var renderer = this, + rendererDefs = renderer.defs; + renderer.box = null; + renderer.boxWrapper = renderer.boxWrapper.destroy(); + + // Call destroy on all gradient elements + destroyObjectProperties(renderer.gradients || {}); + renderer.gradients = null; + + // Defs are null in VMLRenderer + // Otherwise, destroy them here. + if (rendererDefs) { + renderer.defs = rendererDefs.destroy(); + } + + // Remove sub pixel fix handler (#982) + if (renderer.unSubPixelFix) { + renderer.unSubPixelFix(); + } + + renderer.alignedObjects = null; + + return null; + }, + + /** + * Create a wrapper for an SVG element. Serves as a factory for + * {@link SVGElement}, but this function is itself mostly called from + * primitive factories like {@link SVGRenderer#path}, {@link + * SVGRenderer#rect} or {@link SVGRenderer#text}. + * + * @param {string} nodeName - The node name, for example `rect`, `g` etc. + * @returns {SVGElement} The generated SVGElement. + */ + createElement: function (nodeName) { + var wrapper = new this.Element(); + wrapper.init(this, nodeName); + return wrapper; + }, + + /** + * Dummy function for plugins, called every time the renderer is updated. + * Prior to Highcharts 5, this was used for the canvg renderer. + * @function + */ + draw: noop, + + /** + * Get converted radial gradient attributes according to the radial + * reference. Used internally from the {@link SVGElement#colorGradient} + * function. + * + * @private + */ + getRadialAttr: function (radialReference, gradAttr) { + return { + cx: (radialReference[0] - radialReference[2] / 2) + + gradAttr.cx * radialReference[2], + cy: (radialReference[1] - radialReference[2] / 2) + + gradAttr.cy * radialReference[2], + r: gradAttr.r * radialReference[2] + }; + }, + + /** + * Extendable function to measure the tspan width. + * + * @private + */ + getSpanWidth: function (wrapper) { + return wrapper.getBBox(true).width; + }, + + applyEllipsis: function (wrapper, tspan, text, width) { + var renderer = this, + rotation = wrapper.rotation, + str = text, + currentIndex, + minIndex = 0, + maxIndex = text.length, + updateTSpan = function (s) { + tspan.removeChild(tspan.firstChild); + if (s) { + tspan.appendChild(doc.createTextNode(s)); + } + }, + actualWidth, + wasTooLong; + wrapper.rotation = 0; // discard rotation when computing box + actualWidth = renderer.getSpanWidth(wrapper, tspan); + wasTooLong = actualWidth > width; + if (wasTooLong) { + while (minIndex <= maxIndex) { + currentIndex = Math.ceil((minIndex + maxIndex) / 2); + str = text.substring(0, currentIndex) + '\u2026'; + updateTSpan(str); + actualWidth = renderer.getSpanWidth(wrapper, tspan); + if (minIndex === maxIndex) { + // Complete + minIndex = maxIndex + 1; + } else if (actualWidth > width) { + // Too large. Set max index to current. + maxIndex = currentIndex - 1; + } else { + // Within width. Set min index to current. + minIndex = currentIndex; + } + } + // If max index was 0 it means just ellipsis was also to large. + if (maxIndex === 0) { + // Remove ellipses. + updateTSpan(''); + } + } + wrapper.rotation = rotation; // Apply rotation again. + return wasTooLong; + }, + + /** + * A collection of characters mapped to HTML entities. When `useHTML` on an + * element is true, these entities will be rendered correctly by HTML. In + * the SVG pseudo-HTML, they need to be unescaped back to simple characters, + * so for example `<` will render as `<`. + * + * @example + * // Add support for unescaping quotes + * Highcharts.SVGRenderer.prototype.escapes['"'] = '"'; + * + * @type {Object} + */ + escapes: { + '&': '&', + '<': '<', + '>': '>', + "'": ''', // eslint-disable-line quotes + '"': '"' + }, + + /** + * Parse a simple HTML string into SVG tspans. Called internally when text + * is set on an SVGElement. The function supports a subset of HTML tags, + * CSS text features like `width`, `text-overflow`, `white-space`, and + * also attributes like `href` and `style`. + * @private + * @param {SVGElement} wrapper The parent SVGElement. + */ + buildText: function (wrapper) { + var textNode = wrapper.element, + renderer = this, + forExport = renderer.forExport, + textStr = pick(wrapper.textStr, '').toString(), + hasMarkup = textStr.indexOf('<') !== -1, + lines, + childNodes = textNode.childNodes, + wasTooLong, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + width = wrapper.textWidth, + textLineHeight = textStyles && textStyles.lineHeight, + textOutline = textStyles && textStyles.textOutline, + ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', + noWrap = textStyles && textStyles.whiteSpace === 'nowrap', + fontSize = textStyles && textStyles.fontSize, + textCache, + isSubsequentLine, + i = childNodes.length, + tempParent = width && !wrapper.added && this.box, + getLineHeight = function (tspan) { + var fontSizeStyle; + /*= if (build.classic) { =*/ + fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ? + tspan.style.fontSize : + (fontSize || renderer.style.fontSize || 12); + /*= } =*/ + + return textLineHeight ? + pInt(textLineHeight) : + renderer.fontMetrics( + fontSizeStyle, + // Get the computed size from parent if not explicit + tspan.getAttribute('style') ? tspan : textNode + ).h; + }, + unescapeEntities = function (inputStr, except) { + objectEach(renderer.escapes, function (value, key) { + if (!except || inArray(value, except) === -1) { + inputStr = inputStr.toString().replace( + new RegExp(value, 'g'), // eslint-disable-line security/detect-non-literal-regexp + key + ); + } + }); + return inputStr; + }, + parseAttribute = function (s, attr) { + var start, + delimiter; + + start = s.indexOf('<'); + s = s.substring(start, s.indexOf('>') - start); + + start = s.indexOf(attr + '='); + if (start !== -1) { + start = start + attr.length + 1; + delimiter = s.charAt(start); + if (delimiter === '"' || delimiter === "'") { // eslint-disable-line quotes + s = s.substring(start + 1); + return s.substring(0, s.indexOf(delimiter)); + } + } + }; + + // The buildText code is quite heavy, so if we're not changing something + // that affects the text, skip it (#6113). + textCache = [ + textStr, + ellipsis, + noWrap, + textLineHeight, + textOutline, + fontSize, + width + ].join(','); + if (textCache === wrapper.textCache) { + return; + } + wrapper.textCache = textCache; + + // Remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } + + // Skip tspans, add text directly to text node. The forceTSpan is a hook + // used in text outline hack. + if ( + !hasMarkup && + !textOutline && + !ellipsis && + !width && + textStr.indexOf(' ') === -1 + ) { + textNode.appendChild(doc.createTextNode(unescapeEntities(textStr))); + + // Complex strings, add more logic + } else { + + if (tempParent) { + // attach it to the DOM to read offset width + tempParent.appendChild(textNode); + } + + if (hasMarkup) { + lines = textStr + /*= if (build.classic) { =*/ + .replace(/<(b|strong)>/g, '') + .replace(/<(i|em)>/g, '') + /*= } else { =*/ + .replace( + /<(b|strong)>/g, + '' + ) + .replace( + /<(i|em)>/g, + '' + ) + /*= } =*/ + .replace(//g, '') + .split(//g); + + } else { + lines = [textStr]; + } + + + // Trim empty lines (#5261) + lines = grep(lines, function (line) { + return line !== ''; + }); + + + // build the lines + each(lines, function buildTextLines(line, lineNo) { + var spans, + spanNo = 0; + line = line + // Trim to prevent useless/costly process on the spaces + // (#5258) + .replace(/^\s+|\s+$/g, '') + .replace(//g, '|||'); + spans = line.split('|||'); + + each(spans, function buildTextSpans(span) { + if (span !== '' || spans.length === 1) { + var attributes = {}, + tspan = doc.createElementNS( + renderer.SVG_NS, + 'tspan' + ), + classAttribute, + styleAttribute, // #390 + hrefAttribute; + + classAttribute = parseAttribute(span, 'class'); + if (classAttribute) { + attr(tspan, 'class', classAttribute); + } + + styleAttribute = parseAttribute(span, 'style'); + if (styleAttribute) { + styleAttribute = styleAttribute.replace( + /(;| |^)color([ :])/, + '$1fill$2' + ); + attr(tspan, 'style', styleAttribute); + } + + // Not for export - #1529 + hrefAttribute = parseAttribute(span, 'href'); + if (hrefAttribute && !forExport) { + attr( + tspan, + 'onclick', + 'location.href=\"' + hrefAttribute + '\"' + ); + attr(tspan, 'class', 'highcharts-anchor'); + /*= if (build.classic) { =*/ + css(tspan, { cursor: 'pointer' }); + /*= } =*/ + } + + // Strip away unsupported HTML tags (#7126) + span = unescapeEntities( + span.replace(/<[a-zA-Z\/](.|\n)*?>/g, '') || ' ' + ); + + // Nested tags aren't supported, and cause crash in + // Safari (#1596) + if (span !== ' ') { + + // add the text node + tspan.appendChild(doc.createTextNode(span)); + + // First span in a line, align it to the left + if (!spanNo) { + if (lineNo && parentX !== null) { + attributes.x = parentX; + } + } else { + attributes.dx = 0; // #16 + } + + // add attributes + attr(tspan, attributes); + + // Append it + textNode.appendChild(tspan); + + // first span on subsequent line, add the line + // height + if (!spanNo && isSubsequentLine) { + + // allow getting the right offset height in + // exporting in IE + if (!svg && forExport) { + css(tspan, { display: 'block' }); + } + + // Set the line height based on the font size of + // either the text element or the tspan element + attr( + tspan, + 'dy', + getLineHeight(tspan) + ); + } + + /* + // Experimental text wrapping based on + // getSubstringLength + if (width) { + var spans = renderer.breakText(wrapper, width); + + each(spans, function (span) { + + var dy = getLineHeight(tspan); + tspan = doc.createElementNS( + SVG_NS, + 'tspan' + ); + tspan.appendChild( + doc.createTextNode(span) + ); + attr(tspan, { + dy: dy, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + textNode.appendChild(tspan); + }); + + } + // */ + + // Check width and apply soft breaks or ellipsis + if (width) { + var words = span.replace( + /([^\^])-/g, + '$1- ' + ).split(' '), // #1273 + hasWhiteSpace = ( + spans.length > 1 || + lineNo || + (words.length > 1 && !noWrap) + ), + tooLong, + rest = [], + actualWidth, + dy = getLineHeight(tspan), + rotation = wrapper.rotation; + + if (ellipsis) { + wasTooLong = renderer.applyEllipsis( + wrapper, + tspan, + span, + width + ); + } + + while ( + !ellipsis && + hasWhiteSpace && + (words.length || rest.length) + ) { + // discard rotation when computing box + wrapper.rotation = 0; + actualWidth = renderer.getSpanWidth( + wrapper, + tspan + ); + tooLong = actualWidth > width; + + // For ellipsis, do a binary search for the + // correct string length + if (wasTooLong === undefined) { + wasTooLong = tooLong; // First time + } + + // Looping down, this is the first word + // sequence that is not too long, so we can + // move on to build the next line. + if (!tooLong || words.length === 1) { + words = rest; + rest = []; + + if (words.length && !noWrap) { + tspan = doc.createElementNS( + SVG_NS, + 'tspan' + ); + attr(tspan, { + dy: dy, + x: parentX + }); + if (styleAttribute) { // #390 + attr( + tspan, + 'style', + styleAttribute + ); + } + textNode.appendChild(tspan); + } + + // a single word is pressing it out + if (actualWidth > width) { + width = actualWidth; + } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); + } + if (words.length) { + tspan.appendChild( + doc.createTextNode( + words.join(' ') + .replace(/- /g, '-') + ) + ); + } + } + wrapper.rotation = rotation; + } + + spanNo++; + } + } + }); + // To avoid beginning lines that doesn't add to the textNode + // (#6144) + isSubsequentLine = ( + isSubsequentLine || + textNode.childNodes.length + ); + }); + + if (wasTooLong) { + wrapper.attr( + 'title', + unescapeEntities(wrapper.textStr, ['<', '>']) // #7179 + ); + } + if (tempParent) { + tempParent.removeChild(textNode); + } + + // Apply the text outline + if (textOutline && wrapper.applyTextOutline) { + wrapper.applyTextOutline(textOutline); + } + } + }, + + + + /* + breakText: function (wrapper, width) { + var bBox = wrapper.getBBox(), + node = wrapper.element, + charnum = node.textContent.length, + stringWidth, + // try this position first, based on average character width + guessedLineCharLength = Math.round(width * charnum / bBox.width), + pos = guessedLineCharLength, + spans = [], + increment = 0, + startPos = 0, + endPos, + safe = 0; + + if (bBox.width > width) { + while (startPos < charnum && safe < 100) { + + while (endPos === undefined && safe < 100) { + stringWidth = node.getSubStringLength( + startPos, + pos - startPos + ); + + if (stringWidth <= width) { + if (increment === -1) { + endPos = pos; + } else { + increment = 1; + } + } else { + if (increment === 1) { + endPos = pos - 1; + } else { + increment = -1; + } + } + pos += increment; + safe++; + } + + spans.push( + node.textContent.substr(startPos, endPos - startPos) + ); + + startPos = endPos; + pos = startPos + guessedLineCharLength; + endPos = undefined; + } + } + + return spans; + }, + // */ + + /** + * Returns white for dark colors and black for bright colors. + * + * @param {ColorString} rgba - The color to get the contrast for. + * @returns {string} The contrast color, either `#000000` or `#FFFFFF`. + */ + getContrast: function (rgba) { + rgba = color(rgba).rgba; + + // The threshold may be discussed. Here's a proposal for adding + // different weight to the color channels (#6216) + /* rgba[0] *= 1; // red rgba[1] *= 1.2; // green rgba[2] *= 0.7; // blue */ - return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF'; - }, - - /** - * Create a button with preset states. - * @param {string} text - The text or HTML to draw. - * @param {number} x - The x position of the button's left side. - * @param {number} y - The y position of the button's top side. - * @param {Function} callback - The function to execute on button click or - * touch. - * @param {SVGAttributes} [normalState] - SVG attributes for the normal - * state. - * @param {SVGAttributes} [hoverState] - SVG attributes for the hover state. - * @param {SVGAttributes} [pressedState] - SVG attributes for the pressed - * state. - * @param {SVGAttributes} [disabledState] - SVG attributes for the disabled - * state. - * @param {Symbol} [shape=rect] - The shape type. - * @returns {SVGRenderer} The button element. - */ - button: function ( - text, - x, - y, - callback, - normalState, - hoverState, - pressedState, - disabledState, - shape - ) { - var label = this.label( - text, - x, - y, - shape, - null, - null, - null, - null, - 'button' - ), - curState = 0; - - // Default, non-stylable attributes - label.attr(merge({ - 'padding': 8, - 'r': 2 - }, normalState)); - - /*= if (build.classic) { =*/ - // Presentational - var normalStyle, - hoverStyle, - pressedStyle, - disabledStyle; - - // Normal state - prepare the attributes - normalState = merge({ - fill: '${palette.neutralColor3}', - stroke: '${palette.neutralColor20}', - 'stroke-width': 1, - style: { - color: '${palette.neutralColor80}', - cursor: 'pointer', - fontWeight: 'normal' - } - }, normalState); - normalStyle = normalState.style; - delete normalState.style; - - // Hover state - hoverState = merge(normalState, { - fill: '${palette.neutralColor10}' - }, hoverState); - hoverStyle = hoverState.style; - delete hoverState.style; - - // Pressed state - pressedState = merge(normalState, { - fill: '${palette.highlightColor10}', - style: { - color: '${palette.neutralColor100}', - fontWeight: 'bold' - } - }, pressedState); - pressedStyle = pressedState.style; - delete pressedState.style; - - // Disabled state - disabledState = merge(normalState, { - style: { - color: '${palette.neutralColor20}' - } - }, disabledState); - disabledStyle = disabledState.style; - delete disabledState.style; - /*= } =*/ - - // Add the events. IE9 and IE10 need mouseover and mouseout to funciton - // (#667). - addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () { - if (curState !== 3) { - label.setState(1); - } - }); - addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () { - if (curState !== 3) { - label.setState(curState); - } - }); - - label.setState = function (state) { - // Hover state is temporary, don't record it - if (state !== 1) { - label.state = curState = state; - } - // Update visuals - label.removeClass( - /highcharts-button-(normal|hover|pressed|disabled)/ - ) - .addClass( - 'highcharts-button-' + - ['normal', 'hover', 'pressed', 'disabled'][state || 0] - ); - - /*= if (build.classic) { =*/ - label.attr([ - normalState, - hoverState, - pressedState, - disabledState - ][state || 0]) - .css([ - normalStyle, - hoverStyle, - pressedStyle, - disabledStyle - ][state || 0]); - /*= } =*/ - }; - - - /*= if (build.classic) { =*/ - // Presentational attributes - label - .attr(normalState) - .css(extend({ cursor: 'default' }, normalStyle)); - /*= } =*/ - - return label - .on('click', function (e) { - if (curState !== 3) { - callback.call(label, e); - } - }); - }, - - /** - * Make a straight line crisper by not spilling out to neighbour pixels. - * - * @param {Array} points - The original points on the format - * `['M', 0, 0, 'L', 100, 0]`. - * @param {number} width - The width of the line. - * @returns {Array} The original points array, but modified to render - * crisply. - */ - crispLine: function (points, width) { - // normalize to a crisp line - if (points[1] === points[4]) { - // Substract due to #1129. Now bottom and left axis gridlines behave - // the same. - points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2); - } - if (points[2] === points[5]) { - points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2); - } - return points; - }, - - - /** - * Draw a path, wraps the SVG `path` element. - * - * @param {Array} [path] An SVG path definition in array form. - * - * @example - * var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z']) - * .attr({ stroke: '#ff00ff' }) - * .add(); - * @returns {SVGElement} The generated wrapper element. - * - * @sample highcharts/members/renderer-path-on-chart/ - * Draw a path in a chart - * @sample highcharts/members/renderer-path/ - * Draw a path independent from a chart - * - *//** - * Draw a path, wraps the SVG `path` element. - * - * @param {SVGAttributes} [attribs] The initial attributes. - * @returns {SVGElement} The generated wrapper element. - */ - path: function (path) { - var attribs = { - /*= if (build.classic) { =*/ - fill: 'none' - /*= } =*/ - }; - if (isArray(path)) { - attribs.d = path; - } else if (isObject(path)) { // attributes - extend(attribs, path); - } - return this.createElement('path').attr(attribs); - }, - - /** - * Draw a circle, wraps the SVG `circle` element. - * - * @param {number} [x] The center x position. - * @param {number} [y] The center y position. - * @param {number} [r] The radius. - * @returns {SVGElement} The generated wrapper element. - * - * @sample highcharts/members/renderer-circle/ Drawing a circle - *//** - * Draw a circle, wraps the SVG `circle` element. - * - * @param {SVGAttributes} [attribs] The initial attributes. - * @returns {SVGElement} The generated wrapper element. - */ - circle: function (x, y, r) { - var attribs = isObject(x) ? x : { x: x, y: y, r: r }, - wrapper = this.createElement('circle'); - - // Setting x or y translates to cx and cy - wrapper.xSetter = wrapper.ySetter = function (value, key, element) { - element.setAttribute('c' + key, value); - }; - - return wrapper.attr(attribs); - }, - - /** - * Draw and return an arc. - * @param {number} [x=0] Center X position. - * @param {number} [y=0] Center Y position. - * @param {number} [r=0] The outer radius of the arc. - * @param {number} [innerR=0] Inner radius like used in donut charts. - * @param {number} [start=0] The starting angle of the arc in radians, where - * 0 is to the right and `-Math.PI/2` is up. - * @param {number} [end=0] The ending angle of the arc in radians, where 0 - * is to the right and `-Math.PI/2` is up. - * @returns {SVGElement} The generated wrapper element. - * - * @sample highcharts/members/renderer-arc/ - * Drawing an arc - *//** - * Draw and return an arc. Overloaded function that takes arguments object. - * @param {SVGAttributes} attribs Initial SVG attributes. - * @returns {SVGElement} The generated wrapper element. - */ - arc: function (x, y, r, innerR, start, end) { - var arc, - options; - - if (isObject(x)) { - options = x; - y = options.y; - r = options.r; - innerR = options.innerR; - start = options.start; - end = options.end; - x = options.x; - } else { - options = { - innerR: innerR, - start: start, - end: end - }; - } - - // Arcs are defined as symbols for the ability to set - // attributes in attr and animate - arc = this.symbol('arc', x, y, r, r, options); - arc.r = r; // #959 - return arc; - }, - - /** - * Draw and return a rectangle. - * @param {number} [x] Left position. - * @param {number} [y] Top position. - * @param {number} [width] Width of the rectangle. - * @param {number} [height] Height of the rectangle. - * @param {number} [r] Border corner radius. - * @param {number} [strokeWidth] A stroke width can be supplied to allow - * crisp drawing. - * @returns {SVGElement} The generated wrapper element. - *//** - * Draw and return a rectangle. - * @param {SVGAttributes} [attributes] - * General SVG attributes for the rectangle. - * @return {SVGElement} - * The generated wrapper element. - * - * @sample highcharts/members/renderer-rect-on-chart/ - * Draw a rectangle in a chart - * @sample highcharts/members/renderer-rect/ - * Draw a rectangle independent from a chart - */ - rect: function (x, y, width, height, r, strokeWidth) { - - r = isObject(x) ? x.r : r; - - var wrapper = this.createElement('rect'), - attribs = isObject(x) ? x : x === undefined ? {} : { - x: x, - y: y, - width: Math.max(width, 0), - height: Math.max(height, 0) - }; - - /*= if (build.classic) { =*/ - if (strokeWidth !== undefined) { - attribs.strokeWidth = strokeWidth; - attribs = wrapper.crisp(attribs); - } - attribs.fill = 'none'; - /*= } =*/ - - if (r) { - attribs.r = r; - } - - wrapper.rSetter = function (value, key, element) { - attr(element, { - rx: value, - ry: value - }); - }; - - return wrapper.attr(attribs); - }, - - /** - * Resize the {@link SVGRenderer#box} and re-align all aligned child - * elements. - * @param {number} width - * The new pixel width. - * @param {number} height - * The new pixel height. - * @param {Boolean|AnimationOptions} [animate=true] - * Whether and how to animate. - */ - setSize: function (width, height, animate) { - var renderer = this, - alignedObjects = renderer.alignedObjects, - i = alignedObjects.length; - - renderer.width = width; - renderer.height = height; - - renderer.boxWrapper.animate({ - width: width, - height: height - }, { - step: function () { - this.attr({ - viewBox: '0 0 ' + this.attr('width') + ' ' + - this.attr('height') - }); - }, - duration: pick(animate, true) ? undefined : 0 - }); - - while (i--) { - alignedObjects[i].align(); - } - }, - - /** - * Create and return an svg group element. Child - * {@link Highcharts.SVGElement} objects are added to the group by using the - * group as the first parameter - * in {@link Highcharts.SVGElement#add|add()}. - * - * @param {string} [name] The group will be given a class name of - * `highcharts-{name}`. This can be used for styling and scripting. - * @returns {SVGElement} The generated wrapper element. - * - * @sample highcharts/members/renderer-g/ - * Show and hide grouped objects - */ - g: function (name) { - var elem = this.createElement('g'); - return name ? elem.attr({ 'class': 'highcharts-' + name }) : elem; - }, - - /** - * Display an image. - * @param {string} src The image source. - * @param {number} [x] The X position. - * @param {number} [y] The Y position. - * @param {number} [width] The image width. If omitted, it defaults to the - * image file width. - * @param {number} [height] The image height. If omitted it defaults to the - * image file height. - * @param {function} [onload] Event handler for image load. - * @returns {SVGElement} The generated wrapper element. - * - * @sample highcharts/members/renderer-image-on-chart/ - * Add an image in a chart - * @sample highcharts/members/renderer-image/ - * Add an image independent of a chart - */ - image: function (src, x, y, width, height, onload) { - var attribs = { - preserveAspectRatio: 'none' - }, - elemWrapper, - dummy, - setSVGImageSource = function (el, src) { - // Set the href in the xlink namespace - if (el.setAttributeNS) { - el.setAttributeNS( - 'http://www.w3.org/1999/xlink', 'href', src - ); - } else { - // could be exporting in IE - // using href throws "not supported" in ie7 and under, - // requries regex shim to fix later - el.setAttribute('hc-svg-href', src); - } - }; - - // optional properties - if (arguments.length > 1) { - extend(attribs, { - x: x, - y: y, - width: width, - height: height - }); - } - - elemWrapper = this.createElement('image').attr(attribs); - - // Add load event if supplied - if (onload) { - // We have to use a dummy HTML image since IE support for SVG image - // load events is very buggy. First set a transparent src, wait for - // dummy to load, and then add the real src to the SVG image. - setSVGImageSource( - elemWrapper.element, - 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* eslint-disable-line */ - ); - dummy = new win.Image(); - addEvent(dummy, 'load', function (e) { - setSVGImageSource(elemWrapper.element, src); - onload.call(elemWrapper, e); - }); - dummy.src = src; - } else { - setSVGImageSource(elemWrapper.element, src); - } - - return elemWrapper; - }, - - /** - * Draw a symbol out of pre-defined shape paths from - * {@link SVGRenderer#symbols}. - * It is used in Highcharts for point makers, which cake a `symbol` option, - * and label and button backgrounds like in the tooltip and stock flags. - * - * @param {Symbol} symbol - The symbol name. - * @param {number} x - The X coordinate for the top left position. - * @param {number} y - The Y coordinate for the top left position. - * @param {number} width - The pixel width. - * @param {number} height - The pixel height. - * @param {Object} [options] - Additional options, depending on the actual - * symbol drawn. - * @param {number} [options.anchorX] - The anchor X position for the - * `callout` symbol. This is where the chevron points to. - * @param {number} [options.anchorY] - The anchor Y position for the - * `callout` symbol. This is where the chevron points to. - * @param {number} [options.end] - The end angle of an `arc` symbol. - * @param {boolean} [options.open] - Whether to draw `arc` symbol open or - * closed. - * @param {number} [options.r] - The radius of an `arc` symbol, or the - * border radius for the `callout` symbol. - * @param {number} [options.start] - The start angle of an `arc` symbol. - */ - symbol: function (symbol, x, y, width, height, options) { - - var ren = this, - obj, - imageRegex = /^url\((.*?)\)$/, - isImage = imageRegex.test(symbol), - sym = !isImage && (this.symbols[symbol] ? symbol : 'circle'), - - - // get the symbol definition function - symbolFn = sym && this.symbols[sym], - - // check if there's a path defined for this symbol - path = defined(x) && symbolFn && symbolFn.call( - this.symbols, - Math.round(x), - Math.round(y), - width, - height, - options - ), - imageSrc, - centerImage; - - if (symbolFn) { - obj = this.path(path); - - /*= if (build.classic) { =*/ - obj.attr('fill', 'none'); - /*= } =*/ - - // expando properties for use in animate and attr - extend(obj, { - symbolName: sym, - x: x, - y: y, - width: width, - height: height - }); - if (options) { - extend(obj, options); - } - - - // Image symbols - } else if (isImage) { - - - imageSrc = symbol.match(imageRegex)[1]; - - // Create the image synchronously, add attribs async - obj = this.image(imageSrc); - - // The image width is not always the same as the symbol width. The - // image may be centered within the symbol, as is the case when - // image shapes are used as label backgrounds, for example in flags. - obj.imgwidth = pick( - symbolSizes[imageSrc] && symbolSizes[imageSrc].width, - options && options.width - ); - obj.imgheight = pick( - symbolSizes[imageSrc] && symbolSizes[imageSrc].height, - options && options.height - ); - /** - * Set the size and position - */ - centerImage = function () { - obj.attr({ - width: obj.width, - height: obj.height - }); - }; - - /** - * Width and height setters that take both the image's physical size - * and the label size into consideration, and translates the image - * to center within the label. - */ - each(['width', 'height'], function (key) { - obj[key + 'Setter'] = function (value, key) { - var attribs = {}, - imgSize = this['img' + key], - trans = key === 'width' ? 'translateX' : 'translateY'; - this[key] = value; - if (defined(imgSize)) { - if (this.element) { - this.element.setAttribute(key, imgSize); - } - if (!this.alignByTranslate) { - attribs[trans] = ((this[key] || 0) - imgSize) / 2; - this.attr(attribs); - } - } - }; - }); - - - if (defined(x)) { - obj.attr({ - x: x, - y: y - }); - } - obj.isImg = true; - - if (defined(obj.imgwidth) && defined(obj.imgheight)) { - centerImage(); - } else { - // Initialize image to be 0 size so export will still function - // if there's no cached sizes. - obj.attr({ width: 0, height: 0 }); - - // Create a dummy JavaScript image to get the width and height. - createElement('img', { - onload: function () { - - var chart = charts[ren.chartIndex]; - - // Special case for SVGs on IE11, the width is not - // accessible until the image is part of the DOM - // (#2854). - if (this.width === 0) { - css(this, { - position: 'absolute', - top: '-999em' - }); - doc.body.appendChild(this); - } - - // Center the image - symbolSizes[imageSrc] = { // Cache for next - width: this.width, - height: this.height - }; - obj.imgwidth = this.width; - obj.imgheight = this.height; - - if (obj.element) { - centerImage(); - } - - // Clean up after #2854 workaround. - if (this.parentNode) { - this.parentNode.removeChild(this); - } - - // Fire the load event when all external images are - // loaded - ren.imgCount--; - if (!ren.imgCount && chart && chart.onload) { - chart.onload(); - } - }, - src: imageSrc - }); - this.imgCount++; - } - } - - return obj; - }, - - /** - * @typedef {string} Symbol - * - * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`, - * `triangle`, `triangle-down`. Symbols are used internally for point - * markers, button and label borders and backgrounds, or custom shapes. - * Extendable by adding to {@link SVGRenderer#symbols}. - */ - /** - * An extendable collection of functions for defining symbol paths. - */ - symbols: { - 'circle': function (x, y, w, h) { - // Return a full arc - return this.arc(x + w / 2, y + h / 2, w / 2, h / 2, { - start: 0, - end: Math.PI * 2, - open: false - }); - }, - - 'square': function (x, y, w, h) { - return [ - 'M', x, y, - 'L', x + w, y, - x + w, y + h, - x, y + h, - 'Z' - ]; - }, - - 'triangle': function (x, y, w, h) { - return [ - 'M', x + w / 2, y, - 'L', x + w, y + h, - x, y + h, - 'Z' - ]; - }, - - 'triangle-down': function (x, y, w, h) { - return [ - 'M', x, y, - 'L', x + w, y, - x + w / 2, y + h, - 'Z' - ]; - }, - 'diamond': function (x, y, w, h) { - return [ - 'M', x + w / 2, y, - 'L', x + w, y + h / 2, - x + w / 2, y + h, - x, y + h / 2, - 'Z' - ]; - }, - 'arc': function (x, y, w, h, options) { - var start = options.start, - rx = options.r || w, - ry = options.r || h || w, - proximity = 0.001, - fullCircle = - Math.abs(options.end - options.start - 2 * Math.PI) < - proximity, - // Substract a small number to prevent cos and sin of start and - // end from becoming equal on 360 arcs (related: #1561) - end = options.end - proximity, - innerRadius = options.innerR, - open = pick(options.open, fullCircle), - cosStart = Math.cos(start), - sinStart = Math.sin(start), - cosEnd = Math.cos(end), - sinEnd = Math.sin(end), - // Proximity takes care of rounding errors around PI (#6971) - longArc = options.end - start - Math.PI < proximity ? 0 : 1, - arc; - - arc = [ - 'M', - x + rx * cosStart, - y + ry * sinStart, - 'A', // arcTo - rx, // x radius - ry, // y radius - 0, // slanting - longArc, // long or short arc - 1, // clockwise - x + rx * cosEnd, - y + ry * sinEnd - ]; - - if (defined(innerRadius)) { - arc.push( - open ? 'M' : 'L', - x + innerRadius * cosEnd, - y + innerRadius * sinEnd, - 'A', // arcTo - innerRadius, // x radius - innerRadius, // y radius - 0, // slanting - longArc, // long or short arc - 0, // clockwise - x + innerRadius * cosStart, - y + innerRadius * sinStart - ); - } - - arc.push(open ? '' : 'Z'); // close - return arc; - }, - - /** - * Callout shape used for default tooltips, also used for rounded - * rectangles in VML - */ - callout: function (x, y, w, h, options) { - var arrowLength = 6, - halfDistance = 6, - r = Math.min((options && options.r) || 0, w, h), - safeDistance = r + halfDistance, - anchorX = options && options.anchorX, - anchorY = options && options.anchorY, - path; - - path = [ - 'M', x + r, y, - 'L', x + w - r, y, // top side - 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner - 'L', x + w, y + h - r, // right side - 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-rgt - 'L', x + r, y + h, // bottom side - 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner - 'L', x, y + r, // left side - 'C', x, y, x, y, x + r, y // top-left corner - ]; - - // Anchor on right side - if (anchorX && anchorX > w) { - - // Chevron - if ( - anchorY > y + safeDistance && - anchorY < y + h - safeDistance - ) { - path.splice(13, 3, - 'L', x + w, anchorY - halfDistance, - x + w + arrowLength, anchorY, - x + w, anchorY + halfDistance, - x + w, y + h - r - ); - - // Simple connector - } else { - path.splice(13, 3, - 'L', x + w, h / 2, - anchorX, anchorY, - x + w, h / 2, - x + w, y + h - r - ); - } - - // Anchor on left side - } else if (anchorX && anchorX < 0) { - - // Chevron - if ( - anchorY > y + safeDistance && - anchorY < y + h - safeDistance - ) { - path.splice(33, 3, - 'L', x, anchorY + halfDistance, - x - arrowLength, anchorY, - x, anchorY - halfDistance, - x, y + r - ); - - // Simple connector - } else { - path.splice(33, 3, - 'L', x, h / 2, - anchorX, anchorY, - x, h / 2, - x, y + r - ); - } - - } else if ( // replace bottom - anchorY && - anchorY > h && - anchorX > x + safeDistance && - anchorX < x + w - safeDistance - ) { - path.splice(23, 3, - 'L', anchorX + halfDistance, y + h, - anchorX, y + h + arrowLength, - anchorX - halfDistance, y + h, - x + r, y + h - ); - - } else if ( // replace top - anchorY && - anchorY < 0 && - anchorX > x + safeDistance && - anchorX < x + w - safeDistance - ) { - path.splice(3, 3, - 'L', anchorX - halfDistance, y, - anchorX, y - arrowLength, - anchorX + halfDistance, y, - w - r, y - ); - } - - return path; - } - }, - - /** - * @typedef {SVGElement} ClipRect - A clipping rectangle that can be applied - * to one or more {@link SVGElement} instances. It is instanciated with the - * {@link SVGRenderer#clipRect} function and applied with the {@link - * SVGElement#clip} function. - * - * @example - * var circle = renderer.circle(100, 100, 100) - * .attr({ fill: 'red' }) - * .add(); - * var clipRect = renderer.clipRect(100, 100, 100, 100); - * - * // Leave only the lower right quarter visible - * circle.clip(clipRect); - */ - /** - * Define a clipping rectangle. The clipping rectangle is later applied - * to {@link SVGElement} objects through the {@link SVGElement#clip} - * function. - * - * @param {String} id - * @param {number} x - * @param {number} y - * @param {number} width - * @param {number} height - * @returns {ClipRect} A clipping rectangle. - * - * @example - * var circle = renderer.circle(100, 100, 100) - * .attr({ fill: 'red' }) - * .add(); - * var clipRect = renderer.clipRect(100, 100, 100, 100); - * - * // Leave only the lower right quarter visible - * circle.clip(clipRect); - */ - clipRect: function (x, y, width, height) { - var wrapper, - id = H.uniqueKey(), - - clipPath = this.createElement('clipPath').attr({ - id: id - }).add(this.defs); - - wrapper = this.rect(x, y, width, height, 0).add(clipPath); - wrapper.id = id; - wrapper.clipPath = clipPath; - wrapper.count = 0; - - return wrapper; - }, - - - - - - /** - * Draw text. The text can contain a subset of HTML, like spans and anchors - * and some basic text styling of these. For more advanced features like - * border and background, use {@link Highcharts.SVGRenderer#label} instead. - * To update the text after render, run `text.attr({ text: 'New text' })`. - * @param {String} str - * The text of (subset) HTML to draw. - * @param {number} x - * The x position of the text's lower left corner. - * @param {number} y - * The y position of the text's lower left corner. - * @param {Boolean} [useHTML=false] - * Use HTML to render the text. - * - * @return {SVGElement} The text object. - * - * @sample highcharts/members/renderer-text-on-chart/ - * Annotate the chart freely - * @sample highcharts/members/renderer-on-chart/ - * Annotate with a border and in response to the data - * @sample highcharts/members/renderer-text/ - * Formatted text - */ - text: function (str, x, y, useHTML) { - - // declare variables - var renderer = this, - wrapper, - attribs = {}; - - if (useHTML && (renderer.allowHTML || !renderer.forExport)) { - return renderer.html(str, x, y); - } - - attribs.x = Math.round(x || 0); // X always needed for line-wrap logic - if (y) { - attribs.y = Math.round(y); - } - if (str || str === 0) { - attribs.text = str; - } - - wrapper = renderer.createElement('text') - .attr(attribs); - - if (!useHTML) { - wrapper.xSetter = function (value, key, element) { - var tspans = element.getElementsByTagName('tspan'), - tspan, - parentVal = element.getAttribute(key), - i; - for (i = 0; i < tspans.length; i++) { - tspan = tspans[i]; - // If the x values are equal, the tspan represents a - // linebreak - if (tspan.getAttribute(key) === parentVal) { - tspan.setAttribute(key, value); - } - } - element.setAttribute(key, value); - }; - } - - return wrapper; - }, - - /** - * Utility to return the baseline offset and total line height from the font - * size. - * - * @param {?string} fontSize The current font size to inspect. If not given, - * the font size will be found from the DOM element. - * @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a - * current font size. - * @returns {Object} An object containing `h`: the line height, `b`: the - * baseline relative to the top of the box, and `f`: the font size. - */ - fontMetrics: function (fontSize, elem) { - var lineHeight, - baseline; - - /*= if (build.classic) { =*/ - fontSize = fontSize || - // When the elem is a DOM element (#5932) - (elem && elem.style && elem.style.fontSize) || - // Fall back on the renderer style default - (this.style && this.style.fontSize); - - /*= } else { =*/ - fontSize = elem && SVGElement.prototype.getStyle.call( - elem, - 'font-size' - ); - /*= } =*/ - - // Handle different units - if (/px/.test(fontSize)) { - fontSize = pInt(fontSize); - } else if (/em/.test(fontSize)) { - // The em unit depends on parent items - fontSize = parseFloat(fontSize) * - (elem ? this.fontMetrics(null, elem.parentNode).f : 16); - } else { - fontSize = 12; - } - - // Empirical values found by comparing font size and bounding box - // height. Applies to the default font family. - // http://jsfiddle.net/highcharts/7xvn7/ - lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2); - baseline = Math.round(lineHeight * 0.8); - - return { - h: lineHeight, - b: baseline, - f: fontSize - }; - }, - - /** - * Correct X and Y positioning of a label for rotation (#1764). - * - * @private - */ - rotCorr: function (baseline, rotation, alterY) { - var y = baseline; - if (rotation && alterY) { - y = Math.max(y * Math.cos(rotation * deg2rad), 4); - } - return { - x: (-baseline / 3) * Math.sin(rotation * deg2rad), - y: y - }; - }, - - /** - * Draw a label, which is an extended text element with support for border - * and background. Highcharts creates a `g` element with a text and a `path` - * or `rect` inside, to make it behave somewhat like a HTML div. Border and - * background are set through `stroke`, `stroke-width` and `fill` attributes - * using the {@link Highcharts.SVGElement#attr|attr} method. To update the - * text after render, run `label.attr({ text: 'New text' })`. - * - * @param {string} str - * The initial text string or (subset) HTML to render. - * @param {number} x - * The x position of the label's left side. - * @param {number} y - * The y position of the label's top side or baseline, depending on - * the `baseline` parameter. - * @param {String} shape - * The shape of the label's border/background, if any. Defaults to - * `rect`. Other possible values are `callout` or other shapes - * defined in {@link Highcharts.SVGRenderer#symbols}. - * @param {number} anchorX - * In case the `shape` has a pointer, like a flag, this is the - * coordinates it should be pinned to. - * @param {number} anchorY - * In case the `shape` has a pointer, like a flag, this is the - * coordinates it should be pinned to. - * @param {Boolean} baseline - * Whether to position the label relative to the text baseline, - * like {@link Highcharts.SVGRenderer#text|renderer.text}, or to the - * upper border of the rectangle. - * @param {String} className - * Class name for the group. - * - * @return {SVGElement} - * The generated label. - * - * @sample highcharts/members/renderer-label-on-chart/ - * A label on the chart - */ - label: function ( - str, - x, - y, - shape, - anchorX, - anchorY, - useHTML, - baseline, - className - ) { - - var renderer = this, - wrapper = renderer.g(className !== 'button' && 'label'), - text = wrapper.text = renderer.text('', 0, 0, useHTML) - .attr({ - zIndex: 1 - }), - box, - bBox, - alignFactor = 0, - padding = 3, - paddingLeft = 0, - width, - height, - wrapperX, - wrapperY, - textAlign, - deferredAttr = {}, - strokeWidth, - baselineOffset, - hasBGImage = /^url\((.*?)\)$/.test(shape), - needsBox = hasBGImage, - getCrispAdjust, - updateBoxSize, - updateTextPadding, - boxAttr; - - if (className) { - wrapper.addClass('highcharts-' + className); - } - - /*= if (!build.classic) { =*/ - needsBox = true; // for styling - getCrispAdjust = function () { - return box.strokeWidth() % 2 / 2; - }; - /*= } else { =*/ - needsBox = hasBGImage; - getCrispAdjust = function () { - return (strokeWidth || 0) % 2 / 2; - }; - - /*= } =*/ - - /** - * This function runs after the label is added to the DOM (when the - * bounding box is available), and after the text of the label is - * updated to detect the new bounding box and reflect it in the border - * box. - */ - updateBoxSize = function () { - var style = text.element.style, - crispAdjust, - attribs = {}; - - bBox = ( - (width === undefined || height === undefined || textAlign) && - defined(text.textStr) && - text.getBBox() - ); // #3295 && 3514 box failure when string equals 0 - wrapper.width = ( - (width || bBox.width || 0) + - 2 * padding + - paddingLeft - ); - wrapper.height = (height || bBox.height || 0) + 2 * padding; - - // Update the label-scoped y offset - baselineOffset = padding + - renderer.fontMetrics(style && style.fontSize, text).b; - - - if (needsBox) { - - // Create the border box if it is not already present - if (!box) { - // Symbol definition exists (#5324) - wrapper.box = box = renderer.symbols[shape] || hasBGImage ? - renderer.symbol(shape) : - renderer.rect(); - - box.addClass( // Don't use label className for buttons - (className === 'button' ? '' : 'highcharts-label-box') + - (className ? ' highcharts-' + className + '-box' : '') - ); - - box.add(wrapper); - - crispAdjust = getCrispAdjust(); - attribs.x = crispAdjust; - attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust; - } - - // Apply the box attributes - attribs.width = Math.round(wrapper.width); - attribs.height = Math.round(wrapper.height); - - box.attr(extend(attribs, deferredAttr)); - deferredAttr = {}; - } - }; - - /** - * This function runs after setting text or padding, but only if padding - * is changed - */ - updateTextPadding = function () { - var textX = paddingLeft + padding, - textY; - - // determin y based on the baseline - textY = baseline ? 0 : baselineOffset; - - // compensate for alignment - if ( - defined(width) && - bBox && - (textAlign === 'center' || textAlign === 'right') - ) { - textX += { center: 0.5, right: 1 }[textAlign] * - (width - bBox.width); - } - - // update if anything changed - if (textX !== text.x || textY !== text.y) { - text.attr('x', textX); - if (textY !== undefined) { - text.attr('y', textY); - } - } - - // record current values - text.x = textX; - text.y = textY; - }; - - /** - * Set a box attribute, or defer it if the box is not yet created - * @param {Object} key - * @param {Object} value - */ - boxAttr = function (key, value) { - if (box) { - box.attr(key, value); - } else { - deferredAttr[key] = value; - } - }; - - /** - * After the text element is added, get the desired size of the border - * box and add it before the text in the DOM. - */ - wrapper.onAdd = function () { - text.add(wrapper); - wrapper.attr({ - // Alignment is available now (#3295, 0 not rendered if given - // as a value) - text: (str || str === 0) ? str : '', - x: x, - y: y - }); - - if (box && defined(anchorX)) { - wrapper.attr({ - anchorX: anchorX, - anchorY: anchorY - }); - } - }; - - /* - * Add specific attribute setters. - */ - - // only change local variables - wrapper.widthSetter = function (value) { - width = H.isNumber(value) ? value : null; // width:auto => null - }; - wrapper.heightSetter = function (value) { - height = value; - }; - wrapper['text-alignSetter'] = function (value) { - textAlign = value; - }; - wrapper.paddingSetter = function (value) { - if (defined(value) && value !== padding) { - padding = wrapper.padding = value; - updateTextPadding(); - } - }; - wrapper.paddingLeftSetter = function (value) { - if (defined(value) && value !== paddingLeft) { - paddingLeft = value; - updateTextPadding(); - } - }; - - - // change local variable and prevent setting attribute on the group - wrapper.alignSetter = function (value) { - value = { left: 0, center: 0.5, right: 1 }[value]; - if (value !== alignFactor) { - alignFactor = value; - // Bounding box exists, means we're dynamically changing - if (bBox) { - wrapper.attr({ x: wrapperX }); // #5134 - } - } - }; - - // apply these to the box and the text alike - wrapper.textSetter = function (value) { - if (value !== undefined) { - text.textSetter(value); - } - updateBoxSize(); - updateTextPadding(); - }; - - // apply these to the box but not to the text - wrapper['stroke-widthSetter'] = function (value, key) { - if (value) { - needsBox = true; - } - strokeWidth = this['stroke-width'] = value; - boxAttr(key, value); - }; - /*= if (!build.classic) { =*/ - wrapper.rSetter = function (value, key) { - boxAttr(key, value); - }; - /*= } else { =*/ - wrapper.strokeSetter = - wrapper.fillSetter = - wrapper.rSetter = function (value, key) { - if (key !== 'r') { - if (key === 'fill' && value) { - needsBox = true; - } - // for animation getter (#6776) - wrapper[key] = value; - } - boxAttr(key, value); - }; - /*= } =*/ - wrapper.anchorXSetter = function (value, key) { - anchorX = wrapper.anchorX = value; - boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX); - }; - wrapper.anchorYSetter = function (value, key) { - anchorY = wrapper.anchorY = value; - boxAttr(key, value - wrapperY); - }; - - // rename attributes - wrapper.xSetter = function (value) { - wrapper.x = value; // for animation getter - if (alignFactor) { - value -= alignFactor * ((width || bBox.width) + 2 * padding); - - // Force animation even when setting to the same value (#7898) - wrapper['forceAnimate:x'] = true; - } - wrapperX = Math.round(value); - wrapper.attr('translateX', wrapperX); - }; - wrapper.ySetter = function (value) { - wrapperY = wrapper.y = Math.round(value); - wrapper.attr('translateY', wrapperY); - }; - - // Redirect certain methods to either the box or the text - var baseCss = wrapper.css; - return extend(wrapper, { - /** - * Pick up some properties and apply them to the text instead of the - * wrapper. - * @ignore - */ - css: function (styles) { - if (styles) { - var textStyles = {}; - // Create a copy to avoid altering the original object - // (#537) - styles = merge(styles); - each(wrapper.textProps, function (prop) { - if (styles[prop] !== undefined) { - textStyles[prop] = styles[prop]; - delete styles[prop]; - } - }); - text.css(textStyles); - - if ('width' in textStyles) { - updateBoxSize(); - } - } - return baseCss.call(wrapper, styles); - }, - /** - * Return the bounding box of the box, not the group. - * @ignore - */ - getBBox: function () { - return { - width: bBox.width + 2 * padding, - height: bBox.height + 2 * padding, - x: bBox.x - padding, - y: bBox.y - padding - }; - }, - /*= if (build.classic) { =*/ - /** - * Apply the shadow to the box. - * @ignore - */ - shadow: function (b) { - if (b) { - updateBoxSize(); - if (box) { - box.shadow(b); - } - } - return wrapper; - }, - /*= } =*/ - /** - * Destroy and release memory. - * @ignore - */ - destroy: function () { - - // Added by button implementation - removeEvent(wrapper.element, 'mouseenter'); - removeEvent(wrapper.element, 'mouseleave'); - - if (text) { - text = text.destroy(); - } - if (box) { - box = box.destroy(); - } - // Call base implementation to destroy the rest - SVGElement.prototype.destroy.call(wrapper); - - // Release local pointers (#1298) - wrapper = - renderer = - updateBoxSize = - updateTextPadding = - boxAttr = null; - } - }); - } + return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF'; + }, + + /** + * Create a button with preset states. + * @param {string} text - The text or HTML to draw. + * @param {number} x - The x position of the button's left side. + * @param {number} y - The y position of the button's top side. + * @param {Function} callback - The function to execute on button click or + * touch. + * @param {SVGAttributes} [normalState] - SVG attributes for the normal + * state. + * @param {SVGAttributes} [hoverState] - SVG attributes for the hover state. + * @param {SVGAttributes} [pressedState] - SVG attributes for the pressed + * state. + * @param {SVGAttributes} [disabledState] - SVG attributes for the disabled + * state. + * @param {Symbol} [shape=rect] - The shape type. + * @returns {SVGRenderer} The button element. + */ + button: function ( + text, + x, + y, + callback, + normalState, + hoverState, + pressedState, + disabledState, + shape + ) { + var label = this.label( + text, + x, + y, + shape, + null, + null, + null, + null, + 'button' + ), + curState = 0; + + // Default, non-stylable attributes + label.attr(merge({ + 'padding': 8, + 'r': 2 + }, normalState)); + + /*= if (build.classic) { =*/ + // Presentational + var normalStyle, + hoverStyle, + pressedStyle, + disabledStyle; + + // Normal state - prepare the attributes + normalState = merge({ + fill: '${palette.neutralColor3}', + stroke: '${palette.neutralColor20}', + 'stroke-width': 1, + style: { + color: '${palette.neutralColor80}', + cursor: 'pointer', + fontWeight: 'normal' + } + }, normalState); + normalStyle = normalState.style; + delete normalState.style; + + // Hover state + hoverState = merge(normalState, { + fill: '${palette.neutralColor10}' + }, hoverState); + hoverStyle = hoverState.style; + delete hoverState.style; + + // Pressed state + pressedState = merge(normalState, { + fill: '${palette.highlightColor10}', + style: { + color: '${palette.neutralColor100}', + fontWeight: 'bold' + } + }, pressedState); + pressedStyle = pressedState.style; + delete pressedState.style; + + // Disabled state + disabledState = merge(normalState, { + style: { + color: '${palette.neutralColor20}' + } + }, disabledState); + disabledStyle = disabledState.style; + delete disabledState.style; + /*= } =*/ + + // Add the events. IE9 and IE10 need mouseover and mouseout to funciton + // (#667). + addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () { + if (curState !== 3) { + label.setState(1); + } + }); + addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () { + if (curState !== 3) { + label.setState(curState); + } + }); + + label.setState = function (state) { + // Hover state is temporary, don't record it + if (state !== 1) { + label.state = curState = state; + } + // Update visuals + label.removeClass( + /highcharts-button-(normal|hover|pressed|disabled)/ + ) + .addClass( + 'highcharts-button-' + + ['normal', 'hover', 'pressed', 'disabled'][state || 0] + ); + + /*= if (build.classic) { =*/ + label.attr([ + normalState, + hoverState, + pressedState, + disabledState + ][state || 0]) + .css([ + normalStyle, + hoverStyle, + pressedStyle, + disabledStyle + ][state || 0]); + /*= } =*/ + }; + + + /*= if (build.classic) { =*/ + // Presentational attributes + label + .attr(normalState) + .css(extend({ cursor: 'default' }, normalStyle)); + /*= } =*/ + + return label + .on('click', function (e) { + if (curState !== 3) { + callback.call(label, e); + } + }); + }, + + /** + * Make a straight line crisper by not spilling out to neighbour pixels. + * + * @param {Array} points - The original points on the format + * `['M', 0, 0, 'L', 100, 0]`. + * @param {number} width - The width of the line. + * @returns {Array} The original points array, but modified to render + * crisply. + */ + crispLine: function (points, width) { + // normalize to a crisp line + if (points[1] === points[4]) { + // Substract due to #1129. Now bottom and left axis gridlines behave + // the same. + points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2); + } + if (points[2] === points[5]) { + points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2); + } + return points; + }, + + + /** + * Draw a path, wraps the SVG `path` element. + * + * @param {Array} [path] An SVG path definition in array form. + * + * @example + * var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z']) + * .attr({ stroke: '#ff00ff' }) + * .add(); + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-path-on-chart/ + * Draw a path in a chart + * @sample highcharts/members/renderer-path/ + * Draw a path independent from a chart + * + *//** + * Draw a path, wraps the SVG `path` element. + * + * @param {SVGAttributes} [attribs] The initial attributes. + * @returns {SVGElement} The generated wrapper element. + */ + path: function (path) { + var attribs = { + /*= if (build.classic) { =*/ + fill: 'none' + /*= } =*/ + }; + if (isArray(path)) { + attribs.d = path; + } else if (isObject(path)) { // attributes + extend(attribs, path); + } + return this.createElement('path').attr(attribs); + }, + + /** + * Draw a circle, wraps the SVG `circle` element. + * + * @param {number} [x] The center x position. + * @param {number} [y] The center y position. + * @param {number} [r] The radius. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-circle/ Drawing a circle + *//** + * Draw a circle, wraps the SVG `circle` element. + * + * @param {SVGAttributes} [attribs] The initial attributes. + * @returns {SVGElement} The generated wrapper element. + */ + circle: function (x, y, r) { + var attribs = isObject(x) ? x : { x: x, y: y, r: r }, + wrapper = this.createElement('circle'); + + // Setting x or y translates to cx and cy + wrapper.xSetter = wrapper.ySetter = function (value, key, element) { + element.setAttribute('c' + key, value); + }; + + return wrapper.attr(attribs); + }, + + /** + * Draw and return an arc. + * @param {number} [x=0] Center X position. + * @param {number} [y=0] Center Y position. + * @param {number} [r=0] The outer radius of the arc. + * @param {number} [innerR=0] Inner radius like used in donut charts. + * @param {number} [start=0] The starting angle of the arc in radians, where + * 0 is to the right and `-Math.PI/2` is up. + * @param {number} [end=0] The ending angle of the arc in radians, where 0 + * is to the right and `-Math.PI/2` is up. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-arc/ + * Drawing an arc + *//** + * Draw and return an arc. Overloaded function that takes arguments object. + * @param {SVGAttributes} attribs Initial SVG attributes. + * @returns {SVGElement} The generated wrapper element. + */ + arc: function (x, y, r, innerR, start, end) { + var arc, + options; + + if (isObject(x)) { + options = x; + y = options.y; + r = options.r; + innerR = options.innerR; + start = options.start; + end = options.end; + x = options.x; + } else { + options = { + innerR: innerR, + start: start, + end: end + }; + } + + // Arcs are defined as symbols for the ability to set + // attributes in attr and animate + arc = this.symbol('arc', x, y, r, r, options); + arc.r = r; // #959 + return arc; + }, + + /** + * Draw and return a rectangle. + * @param {number} [x] Left position. + * @param {number} [y] Top position. + * @param {number} [width] Width of the rectangle. + * @param {number} [height] Height of the rectangle. + * @param {number} [r] Border corner radius. + * @param {number} [strokeWidth] A stroke width can be supplied to allow + * crisp drawing. + * @returns {SVGElement} The generated wrapper element. + *//** + * Draw and return a rectangle. + * @param {SVGAttributes} [attributes] + * General SVG attributes for the rectangle. + * @return {SVGElement} + * The generated wrapper element. + * + * @sample highcharts/members/renderer-rect-on-chart/ + * Draw a rectangle in a chart + * @sample highcharts/members/renderer-rect/ + * Draw a rectangle independent from a chart + */ + rect: function (x, y, width, height, r, strokeWidth) { + + r = isObject(x) ? x.r : r; + + var wrapper = this.createElement('rect'), + attribs = isObject(x) ? x : x === undefined ? {} : { + x: x, + y: y, + width: Math.max(width, 0), + height: Math.max(height, 0) + }; + + /*= if (build.classic) { =*/ + if (strokeWidth !== undefined) { + attribs.strokeWidth = strokeWidth; + attribs = wrapper.crisp(attribs); + } + attribs.fill = 'none'; + /*= } =*/ + + if (r) { + attribs.r = r; + } + + wrapper.rSetter = function (value, key, element) { + attr(element, { + rx: value, + ry: value + }); + }; + + return wrapper.attr(attribs); + }, + + /** + * Resize the {@link SVGRenderer#box} and re-align all aligned child + * elements. + * @param {number} width + * The new pixel width. + * @param {number} height + * The new pixel height. + * @param {Boolean|AnimationOptions} [animate=true] + * Whether and how to animate. + */ + setSize: function (width, height, animate) { + var renderer = this, + alignedObjects = renderer.alignedObjects, + i = alignedObjects.length; + + renderer.width = width; + renderer.height = height; + + renderer.boxWrapper.animate({ + width: width, + height: height + }, { + step: function () { + this.attr({ + viewBox: '0 0 ' + this.attr('width') + ' ' + + this.attr('height') + }); + }, + duration: pick(animate, true) ? undefined : 0 + }); + + while (i--) { + alignedObjects[i].align(); + } + }, + + /** + * Create and return an svg group element. Child + * {@link Highcharts.SVGElement} objects are added to the group by using the + * group as the first parameter + * in {@link Highcharts.SVGElement#add|add()}. + * + * @param {string} [name] The group will be given a class name of + * `highcharts-{name}`. This can be used for styling and scripting. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-g/ + * Show and hide grouped objects + */ + g: function (name) { + var elem = this.createElement('g'); + return name ? elem.attr({ 'class': 'highcharts-' + name }) : elem; + }, + + /** + * Display an image. + * @param {string} src The image source. + * @param {number} [x] The X position. + * @param {number} [y] The Y position. + * @param {number} [width] The image width. If omitted, it defaults to the + * image file width. + * @param {number} [height] The image height. If omitted it defaults to the + * image file height. + * @param {function} [onload] Event handler for image load. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-image-on-chart/ + * Add an image in a chart + * @sample highcharts/members/renderer-image/ + * Add an image independent of a chart + */ + image: function (src, x, y, width, height, onload) { + var attribs = { + preserveAspectRatio: 'none' + }, + elemWrapper, + dummy, + setSVGImageSource = function (el, src) { + // Set the href in the xlink namespace + if (el.setAttributeNS) { + el.setAttributeNS( + 'http://www.w3.org/1999/xlink', 'href', src + ); + } else { + // could be exporting in IE + // using href throws "not supported" in ie7 and under, + // requries regex shim to fix later + el.setAttribute('hc-svg-href', src); + } + }; + + // optional properties + if (arguments.length > 1) { + extend(attribs, { + x: x, + y: y, + width: width, + height: height + }); + } + + elemWrapper = this.createElement('image').attr(attribs); + + // Add load event if supplied + if (onload) { + // We have to use a dummy HTML image since IE support for SVG image + // load events is very buggy. First set a transparent src, wait for + // dummy to load, and then add the real src to the SVG image. + setSVGImageSource( + elemWrapper.element, + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* eslint-disable-line */ + ); + dummy = new win.Image(); + addEvent(dummy, 'load', function (e) { + setSVGImageSource(elemWrapper.element, src); + onload.call(elemWrapper, e); + }); + dummy.src = src; + } else { + setSVGImageSource(elemWrapper.element, src); + } + + return elemWrapper; + }, + + /** + * Draw a symbol out of pre-defined shape paths from + * {@link SVGRenderer#symbols}. + * It is used in Highcharts for point makers, which cake a `symbol` option, + * and label and button backgrounds like in the tooltip and stock flags. + * + * @param {Symbol} symbol - The symbol name. + * @param {number} x - The X coordinate for the top left position. + * @param {number} y - The Y coordinate for the top left position. + * @param {number} width - The pixel width. + * @param {number} height - The pixel height. + * @param {Object} [options] - Additional options, depending on the actual + * symbol drawn. + * @param {number} [options.anchorX] - The anchor X position for the + * `callout` symbol. This is where the chevron points to. + * @param {number} [options.anchorY] - The anchor Y position for the + * `callout` symbol. This is where the chevron points to. + * @param {number} [options.end] - The end angle of an `arc` symbol. + * @param {boolean} [options.open] - Whether to draw `arc` symbol open or + * closed. + * @param {number} [options.r] - The radius of an `arc` symbol, or the + * border radius for the `callout` symbol. + * @param {number} [options.start] - The start angle of an `arc` symbol. + */ + symbol: function (symbol, x, y, width, height, options) { + + var ren = this, + obj, + imageRegex = /^url\((.*?)\)$/, + isImage = imageRegex.test(symbol), + sym = !isImage && (this.symbols[symbol] ? symbol : 'circle'), + + + // get the symbol definition function + symbolFn = sym && this.symbols[sym], + + // check if there's a path defined for this symbol + path = defined(x) && symbolFn && symbolFn.call( + this.symbols, + Math.round(x), + Math.round(y), + width, + height, + options + ), + imageSrc, + centerImage; + + if (symbolFn) { + obj = this.path(path); + + /*= if (build.classic) { =*/ + obj.attr('fill', 'none'); + /*= } =*/ + + // expando properties for use in animate and attr + extend(obj, { + symbolName: sym, + x: x, + y: y, + width: width, + height: height + }); + if (options) { + extend(obj, options); + } + + + // Image symbols + } else if (isImage) { + + + imageSrc = symbol.match(imageRegex)[1]; + + // Create the image synchronously, add attribs async + obj = this.image(imageSrc); + + // The image width is not always the same as the symbol width. The + // image may be centered within the symbol, as is the case when + // image shapes are used as label backgrounds, for example in flags. + obj.imgwidth = pick( + symbolSizes[imageSrc] && symbolSizes[imageSrc].width, + options && options.width + ); + obj.imgheight = pick( + symbolSizes[imageSrc] && symbolSizes[imageSrc].height, + options && options.height + ); + /** + * Set the size and position + */ + centerImage = function () { + obj.attr({ + width: obj.width, + height: obj.height + }); + }; + + /** + * Width and height setters that take both the image's physical size + * and the label size into consideration, and translates the image + * to center within the label. + */ + each(['width', 'height'], function (key) { + obj[key + 'Setter'] = function (value, key) { + var attribs = {}, + imgSize = this['img' + key], + trans = key === 'width' ? 'translateX' : 'translateY'; + this[key] = value; + if (defined(imgSize)) { + if (this.element) { + this.element.setAttribute(key, imgSize); + } + if (!this.alignByTranslate) { + attribs[trans] = ((this[key] || 0) - imgSize) / 2; + this.attr(attribs); + } + } + }; + }); + + + if (defined(x)) { + obj.attr({ + x: x, + y: y + }); + } + obj.isImg = true; + + if (defined(obj.imgwidth) && defined(obj.imgheight)) { + centerImage(); + } else { + // Initialize image to be 0 size so export will still function + // if there's no cached sizes. + obj.attr({ width: 0, height: 0 }); + + // Create a dummy JavaScript image to get the width and height. + createElement('img', { + onload: function () { + + var chart = charts[ren.chartIndex]; + + // Special case for SVGs on IE11, the width is not + // accessible until the image is part of the DOM + // (#2854). + if (this.width === 0) { + css(this, { + position: 'absolute', + top: '-999em' + }); + doc.body.appendChild(this); + } + + // Center the image + symbolSizes[imageSrc] = { // Cache for next + width: this.width, + height: this.height + }; + obj.imgwidth = this.width; + obj.imgheight = this.height; + + if (obj.element) { + centerImage(); + } + + // Clean up after #2854 workaround. + if (this.parentNode) { + this.parentNode.removeChild(this); + } + + // Fire the load event when all external images are + // loaded + ren.imgCount--; + if (!ren.imgCount && chart && chart.onload) { + chart.onload(); + } + }, + src: imageSrc + }); + this.imgCount++; + } + } + + return obj; + }, + + /** + * @typedef {string} Symbol + * + * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`, + * `triangle`, `triangle-down`. Symbols are used internally for point + * markers, button and label borders and backgrounds, or custom shapes. + * Extendable by adding to {@link SVGRenderer#symbols}. + */ + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'circle': function (x, y, w, h) { + // Return a full arc + return this.arc(x + w / 2, y + h / 2, w / 2, h / 2, { + start: 0, + end: Math.PI * 2, + open: false + }); + }, + + 'square': function (x, y, w, h) { + return [ + 'M', x, y, + 'L', x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle': function (x, y, w, h) { + return [ + 'M', x + w / 2, y, + 'L', x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, w, h) { + return [ + 'M', x, y, + 'L', x + w, y, + x + w / 2, y + h, + 'Z' + ]; + }, + 'diamond': function (x, y, w, h) { + return [ + 'M', x + w / 2, y, + 'L', x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2, + 'Z' + ]; + }, + 'arc': function (x, y, w, h, options) { + var start = options.start, + rx = options.r || w, + ry = options.r || h || w, + proximity = 0.001, + fullCircle = + Math.abs(options.end - options.start - 2 * Math.PI) < + proximity, + // Substract a small number to prevent cos and sin of start and + // end from becoming equal on 360 arcs (related: #1561) + end = options.end - proximity, + innerRadius = options.innerR, + open = pick(options.open, fullCircle), + cosStart = Math.cos(start), + sinStart = Math.sin(start), + cosEnd = Math.cos(end), + sinEnd = Math.sin(end), + // Proximity takes care of rounding errors around PI (#6971) + longArc = options.end - start - Math.PI < proximity ? 0 : 1, + arc; + + arc = [ + 'M', + x + rx * cosStart, + y + ry * sinStart, + 'A', // arcTo + rx, // x radius + ry, // y radius + 0, // slanting + longArc, // long or short arc + 1, // clockwise + x + rx * cosEnd, + y + ry * sinEnd + ]; + + if (defined(innerRadius)) { + arc.push( + open ? 'M' : 'L', + x + innerRadius * cosEnd, + y + innerRadius * sinEnd, + 'A', // arcTo + innerRadius, // x radius + innerRadius, // y radius + 0, // slanting + longArc, // long or short arc + 0, // clockwise + x + innerRadius * cosStart, + y + innerRadius * sinStart + ); + } + + arc.push(open ? '' : 'Z'); // close + return arc; + }, + + /** + * Callout shape used for default tooltips, also used for rounded + * rectangles in VML + */ + callout: function (x, y, w, h, options) { + var arrowLength = 6, + halfDistance = 6, + r = Math.min((options && options.r) || 0, w, h), + safeDistance = r + halfDistance, + anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path; + + path = [ + 'M', x + r, y, + 'L', x + w - r, y, // top side + 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner + 'L', x + w, y + h - r, // right side + 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-rgt + 'L', x + r, y + h, // bottom side + 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner + 'L', x, y + r, // left side + 'C', x, y, x, y, x + r, y // top-left corner + ]; + + // Anchor on right side + if (anchorX && anchorX > w) { + + // Chevron + if ( + anchorY > y + safeDistance && + anchorY < y + h - safeDistance + ) { + path.splice(13, 3, + 'L', x + w, anchorY - halfDistance, + x + w + arrowLength, anchorY, + x + w, anchorY + halfDistance, + x + w, y + h - r + ); + + // Simple connector + } else { + path.splice(13, 3, + 'L', x + w, h / 2, + anchorX, anchorY, + x + w, h / 2, + x + w, y + h - r + ); + } + + // Anchor on left side + } else if (anchorX && anchorX < 0) { + + // Chevron + if ( + anchorY > y + safeDistance && + anchorY < y + h - safeDistance + ) { + path.splice(33, 3, + 'L', x, anchorY + halfDistance, + x - arrowLength, anchorY, + x, anchorY - halfDistance, + x, y + r + ); + + // Simple connector + } else { + path.splice(33, 3, + 'L', x, h / 2, + anchorX, anchorY, + x, h / 2, + x, y + r + ); + } + + } else if ( // replace bottom + anchorY && + anchorY > h && + anchorX > x + safeDistance && + anchorX < x + w - safeDistance + ) { + path.splice(23, 3, + 'L', anchorX + halfDistance, y + h, + anchorX, y + h + arrowLength, + anchorX - halfDistance, y + h, + x + r, y + h + ); + + } else if ( // replace top + anchorY && + anchorY < 0 && + anchorX > x + safeDistance && + anchorX < x + w - safeDistance + ) { + path.splice(3, 3, + 'L', anchorX - halfDistance, y, + anchorX, y - arrowLength, + anchorX + halfDistance, y, + w - r, y + ); + } + + return path; + } + }, + + /** + * @typedef {SVGElement} ClipRect - A clipping rectangle that can be applied + * to one or more {@link SVGElement} instances. It is instanciated with the + * {@link SVGRenderer#clipRect} function and applied with the {@link + * SVGElement#clip} function. + * + * @example + * var circle = renderer.circle(100, 100, 100) + * .attr({ fill: 'red' }) + * .add(); + * var clipRect = renderer.clipRect(100, 100, 100, 100); + * + * // Leave only the lower right quarter visible + * circle.clip(clipRect); + */ + /** + * Define a clipping rectangle. The clipping rectangle is later applied + * to {@link SVGElement} objects through the {@link SVGElement#clip} + * function. + * + * @param {String} id + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @returns {ClipRect} A clipping rectangle. + * + * @example + * var circle = renderer.circle(100, 100, 100) + * .attr({ fill: 'red' }) + * .add(); + * var clipRect = renderer.clipRect(100, 100, 100, 100); + * + * // Leave only the lower right quarter visible + * circle.clip(clipRect); + */ + clipRect: function (x, y, width, height) { + var wrapper, + id = H.uniqueKey(), + + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); + + wrapper = this.rect(x, y, width, height, 0).add(clipPath); + wrapper.id = id; + wrapper.clipPath = clipPath; + wrapper.count = 0; + + return wrapper; + }, + + + + + + /** + * Draw text. The text can contain a subset of HTML, like spans and anchors + * and some basic text styling of these. For more advanced features like + * border and background, use {@link Highcharts.SVGRenderer#label} instead. + * To update the text after render, run `text.attr({ text: 'New text' })`. + * @param {String} str + * The text of (subset) HTML to draw. + * @param {number} x + * The x position of the text's lower left corner. + * @param {number} y + * The y position of the text's lower left corner. + * @param {Boolean} [useHTML=false] + * Use HTML to render the text. + * + * @return {SVGElement} The text object. + * + * @sample highcharts/members/renderer-text-on-chart/ + * Annotate the chart freely + * @sample highcharts/members/renderer-on-chart/ + * Annotate with a border and in response to the data + * @sample highcharts/members/renderer-text/ + * Formatted text + */ + text: function (str, x, y, useHTML) { + + // declare variables + var renderer = this, + wrapper, + attribs = {}; + + if (useHTML && (renderer.allowHTML || !renderer.forExport)) { + return renderer.html(str, x, y); + } + + attribs.x = Math.round(x || 0); // X always needed for line-wrap logic + if (y) { + attribs.y = Math.round(y); + } + if (str || str === 0) { + attribs.text = str; + } + + wrapper = renderer.createElement('text') + .attr(attribs); + + if (!useHTML) { + wrapper.xSetter = function (value, key, element) { + var tspans = element.getElementsByTagName('tspan'), + tspan, + parentVal = element.getAttribute(key), + i; + for (i = 0; i < tspans.length; i++) { + tspan = tspans[i]; + // If the x values are equal, the tspan represents a + // linebreak + if (tspan.getAttribute(key) === parentVal) { + tspan.setAttribute(key, value); + } + } + element.setAttribute(key, value); + }; + } + + return wrapper; + }, + + /** + * Utility to return the baseline offset and total line height from the font + * size. + * + * @param {?string} fontSize The current font size to inspect. If not given, + * the font size will be found from the DOM element. + * @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a + * current font size. + * @returns {Object} An object containing `h`: the line height, `b`: the + * baseline relative to the top of the box, and `f`: the font size. + */ + fontMetrics: function (fontSize, elem) { + var lineHeight, + baseline; + + /*= if (build.classic) { =*/ + fontSize = fontSize || + // When the elem is a DOM element (#5932) + (elem && elem.style && elem.style.fontSize) || + // Fall back on the renderer style default + (this.style && this.style.fontSize); + + /*= } else { =*/ + fontSize = elem && SVGElement.prototype.getStyle.call( + elem, + 'font-size' + ); + /*= } =*/ + + // Handle different units + if (/px/.test(fontSize)) { + fontSize = pInt(fontSize); + } else if (/em/.test(fontSize)) { + // The em unit depends on parent items + fontSize = parseFloat(fontSize) * + (elem ? this.fontMetrics(null, elem.parentNode).f : 16); + } else { + fontSize = 12; + } + + // Empirical values found by comparing font size and bounding box + // height. Applies to the default font family. + // http://jsfiddle.net/highcharts/7xvn7/ + lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2); + baseline = Math.round(lineHeight * 0.8); + + return { + h: lineHeight, + b: baseline, + f: fontSize + }; + }, + + /** + * Correct X and Y positioning of a label for rotation (#1764). + * + * @private + */ + rotCorr: function (baseline, rotation, alterY) { + var y = baseline; + if (rotation && alterY) { + y = Math.max(y * Math.cos(rotation * deg2rad), 4); + } + return { + x: (-baseline / 3) * Math.sin(rotation * deg2rad), + y: y + }; + }, + + /** + * Draw a label, which is an extended text element with support for border + * and background. Highcharts creates a `g` element with a text and a `path` + * or `rect` inside, to make it behave somewhat like a HTML div. Border and + * background are set through `stroke`, `stroke-width` and `fill` attributes + * using the {@link Highcharts.SVGElement#attr|attr} method. To update the + * text after render, run `label.attr({ text: 'New text' })`. + * + * @param {string} str + * The initial text string or (subset) HTML to render. + * @param {number} x + * The x position of the label's left side. + * @param {number} y + * The y position of the label's top side or baseline, depending on + * the `baseline` parameter. + * @param {String} shape + * The shape of the label's border/background, if any. Defaults to + * `rect`. Other possible values are `callout` or other shapes + * defined in {@link Highcharts.SVGRenderer#symbols}. + * @param {number} anchorX + * In case the `shape` has a pointer, like a flag, this is the + * coordinates it should be pinned to. + * @param {number} anchorY + * In case the `shape` has a pointer, like a flag, this is the + * coordinates it should be pinned to. + * @param {Boolean} baseline + * Whether to position the label relative to the text baseline, + * like {@link Highcharts.SVGRenderer#text|renderer.text}, or to the + * upper border of the rectangle. + * @param {String} className + * Class name for the group. + * + * @return {SVGElement} + * The generated label. + * + * @sample highcharts/members/renderer-label-on-chart/ + * A label on the chart + */ + label: function ( + str, + x, + y, + shape, + anchorX, + anchorY, + useHTML, + baseline, + className + ) { + + var renderer = this, + wrapper = renderer.g(className !== 'button' && 'label'), + text = wrapper.text = renderer.text('', 0, 0, useHTML) + .attr({ + zIndex: 1 + }), + box, + bBox, + alignFactor = 0, + padding = 3, + paddingLeft = 0, + width, + height, + wrapperX, + wrapperY, + textAlign, + deferredAttr = {}, + strokeWidth, + baselineOffset, + hasBGImage = /^url\((.*?)\)$/.test(shape), + needsBox = hasBGImage, + getCrispAdjust, + updateBoxSize, + updateTextPadding, + boxAttr; + + if (className) { + wrapper.addClass('highcharts-' + className); + } + + /*= if (!build.classic) { =*/ + needsBox = true; // for styling + getCrispAdjust = function () { + return box.strokeWidth() % 2 / 2; + }; + /*= } else { =*/ + needsBox = hasBGImage; + getCrispAdjust = function () { + return (strokeWidth || 0) % 2 / 2; + }; + + /*= } =*/ + + /** + * This function runs after the label is added to the DOM (when the + * bounding box is available), and after the text of the label is + * updated to detect the new bounding box and reflect it in the border + * box. + */ + updateBoxSize = function () { + var style = text.element.style, + crispAdjust, + attribs = {}; + + bBox = ( + (width === undefined || height === undefined || textAlign) && + defined(text.textStr) && + text.getBBox() + ); // #3295 && 3514 box failure when string equals 0 + wrapper.width = ( + (width || bBox.width || 0) + + 2 * padding + + paddingLeft + ); + wrapper.height = (height || bBox.height || 0) + 2 * padding; + + // Update the label-scoped y offset + baselineOffset = padding + + renderer.fontMetrics(style && style.fontSize, text).b; + + + if (needsBox) { + + // Create the border box if it is not already present + if (!box) { + // Symbol definition exists (#5324) + wrapper.box = box = renderer.symbols[shape] || hasBGImage ? + renderer.symbol(shape) : + renderer.rect(); + + box.addClass( // Don't use label className for buttons + (className === 'button' ? '' : 'highcharts-label-box') + + (className ? ' highcharts-' + className + '-box' : '') + ); + + box.add(wrapper); + + crispAdjust = getCrispAdjust(); + attribs.x = crispAdjust; + attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust; + } + + // Apply the box attributes + attribs.width = Math.round(wrapper.width); + attribs.height = Math.round(wrapper.height); + + box.attr(extend(attribs, deferredAttr)); + deferredAttr = {}; + } + }; + + /** + * This function runs after setting text or padding, but only if padding + * is changed + */ + updateTextPadding = function () { + var textX = paddingLeft + padding, + textY; + + // determin y based on the baseline + textY = baseline ? 0 : baselineOffset; + + // compensate for alignment + if ( + defined(width) && + bBox && + (textAlign === 'center' || textAlign === 'right') + ) { + textX += { center: 0.5, right: 1 }[textAlign] * + (width - bBox.width); + } + + // update if anything changed + if (textX !== text.x || textY !== text.y) { + text.attr('x', textX); + if (textY !== undefined) { + text.attr('y', textY); + } + } + + // record current values + text.x = textX; + text.y = textY; + }; + + /** + * Set a box attribute, or defer it if the box is not yet created + * @param {Object} key + * @param {Object} value + */ + boxAttr = function (key, value) { + if (box) { + box.attr(key, value); + } else { + deferredAttr[key] = value; + } + }; + + /** + * After the text element is added, get the desired size of the border + * box and add it before the text in the DOM. + */ + wrapper.onAdd = function () { + text.add(wrapper); + wrapper.attr({ + // Alignment is available now (#3295, 0 not rendered if given + // as a value) + text: (str || str === 0) ? str : '', + x: x, + y: y + }); + + if (box && defined(anchorX)) { + wrapper.attr({ + anchorX: anchorX, + anchorY: anchorY + }); + } + }; + + /* + * Add specific attribute setters. + */ + + // only change local variables + wrapper.widthSetter = function (value) { + width = H.isNumber(value) ? value : null; // width:auto => null + }; + wrapper.heightSetter = function (value) { + height = value; + }; + wrapper['text-alignSetter'] = function (value) { + textAlign = value; + }; + wrapper.paddingSetter = function (value) { + if (defined(value) && value !== padding) { + padding = wrapper.padding = value; + updateTextPadding(); + } + }; + wrapper.paddingLeftSetter = function (value) { + if (defined(value) && value !== paddingLeft) { + paddingLeft = value; + updateTextPadding(); + } + }; + + + // change local variable and prevent setting attribute on the group + wrapper.alignSetter = function (value) { + value = { left: 0, center: 0.5, right: 1 }[value]; + if (value !== alignFactor) { + alignFactor = value; + // Bounding box exists, means we're dynamically changing + if (bBox) { + wrapper.attr({ x: wrapperX }); // #5134 + } + } + }; + + // apply these to the box and the text alike + wrapper.textSetter = function (value) { + if (value !== undefined) { + text.textSetter(value); + } + updateBoxSize(); + updateTextPadding(); + }; + + // apply these to the box but not to the text + wrapper['stroke-widthSetter'] = function (value, key) { + if (value) { + needsBox = true; + } + strokeWidth = this['stroke-width'] = value; + boxAttr(key, value); + }; + /*= if (!build.classic) { =*/ + wrapper.rSetter = function (value, key) { + boxAttr(key, value); + }; + /*= } else { =*/ + wrapper.strokeSetter = + wrapper.fillSetter = + wrapper.rSetter = function (value, key) { + if (key !== 'r') { + if (key === 'fill' && value) { + needsBox = true; + } + // for animation getter (#6776) + wrapper[key] = value; + } + boxAttr(key, value); + }; + /*= } =*/ + wrapper.anchorXSetter = function (value, key) { + anchorX = wrapper.anchorX = value; + boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX); + }; + wrapper.anchorYSetter = function (value, key) { + anchorY = wrapper.anchorY = value; + boxAttr(key, value - wrapperY); + }; + + // rename attributes + wrapper.xSetter = function (value) { + wrapper.x = value; // for animation getter + if (alignFactor) { + value -= alignFactor * ((width || bBox.width) + 2 * padding); + + // Force animation even when setting to the same value (#7898) + wrapper['forceAnimate:x'] = true; + } + wrapperX = Math.round(value); + wrapper.attr('translateX', wrapperX); + }; + wrapper.ySetter = function (value) { + wrapperY = wrapper.y = Math.round(value); + wrapper.attr('translateY', wrapperY); + }; + + // Redirect certain methods to either the box or the text + var baseCss = wrapper.css; + return extend(wrapper, { + /** + * Pick up some properties and apply them to the text instead of the + * wrapper. + * @ignore + */ + css: function (styles) { + if (styles) { + var textStyles = {}; + // Create a copy to avoid altering the original object + // (#537) + styles = merge(styles); + each(wrapper.textProps, function (prop) { + if (styles[prop] !== undefined) { + textStyles[prop] = styles[prop]; + delete styles[prop]; + } + }); + text.css(textStyles); + + if ('width' in textStyles) { + updateBoxSize(); + } + } + return baseCss.call(wrapper, styles); + }, + /** + * Return the bounding box of the box, not the group. + * @ignore + */ + getBBox: function () { + return { + width: bBox.width + 2 * padding, + height: bBox.height + 2 * padding, + x: bBox.x - padding, + y: bBox.y - padding + }; + }, + /*= if (build.classic) { =*/ + /** + * Apply the shadow to the box. + * @ignore + */ + shadow: function (b) { + if (b) { + updateBoxSize(); + if (box) { + box.shadow(b); + } + } + return wrapper; + }, + /*= } =*/ + /** + * Destroy and release memory. + * @ignore + */ + destroy: function () { + + // Added by button implementation + removeEvent(wrapper.element, 'mouseenter'); + removeEvent(wrapper.element, 'mouseleave'); + + if (text) { + text = text.destroy(); + } + if (box) { + box = box.destroy(); + } + // Call base implementation to destroy the rest + SVGElement.prototype.destroy.call(wrapper); + + // Release local pointers (#1298) + wrapper = + renderer = + updateBoxSize = + updateTextPadding = + boxAttr = null; + } + }); + } }); // end SVGRenderer diff --git a/js/parts/Tick.js b/js/parts/Tick.js index 54aae08010a..360925a3674 100644 --- a/js/parts/Tick.js +++ b/js/parts/Tick.js @@ -7,602 +7,602 @@ import H from './Globals.js'; import './Utilities.js'; var correctFloat = H.correctFloat, - defined = H.defined, - destroyObjectProperties = H.destroyObjectProperties, - fireEvent = H.fireEvent, - isNumber = H.isNumber, - merge = H.merge, - pick = H.pick, - deg2rad = H.deg2rad; + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + fireEvent = H.fireEvent, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + deg2rad = H.deg2rad; /** * The Tick class */ H.Tick = function (axis, pos, type, noLabel) { - this.axis = axis; - this.pos = pos; - this.type = type || ''; - this.isNew = true; - this.isNewLabel = true; - - if (!type && !noLabel) { - this.addLabel(); - } + this.axis = axis; + this.pos = pos; + this.type = type || ''; + this.isNew = true; + this.isNewLabel = true; + + if (!type && !noLabel) { + this.addLabel(); + } }; H.Tick.prototype = { - /** - * Write the tick label - */ - addLabel: function () { - var tick = this, - axis = tick.axis, - options = axis.options, - chart = axis.chart, - categories = axis.categories, - names = axis.names, - pos = tick.pos, - labelOptions = options.labels, - str, - tickPositions = axis.tickPositions, - isFirst = pos === tickPositions[0], - isLast = pos === tickPositions[tickPositions.length - 1], - value = categories ? - pick(categories[pos], names[pos], pos) : - pos, - label = tick.label, - tickPositionInfo = tickPositions.info, - dateTimeLabelFormat; - - // Set the datetime label format. If a higher rank is set for this - // position, use that. If not, use the general format. - if (axis.isDatetimeAxis && tickPositionInfo) { - dateTimeLabelFormat = - options.dateTimeLabelFormats[ - tickPositionInfo.higherRanks[pos] || - tickPositionInfo.unitName - ]; - } - // set properties for access in render method - tick.isFirst = isFirst; - tick.isLast = isLast; - - // get the string - str = axis.labelFormatter.call({ - axis: axis, - chart: chart, - isFirst: isFirst, - isLast: isLast, - dateTimeLabelFormat: dateTimeLabelFormat, - value: axis.isLog ? correctFloat(axis.lin2log(value)) : value, - pos: pos - }); - - // first call - if (!defined(label)) { - - tick.label = label = - defined(str) && labelOptions.enabled ? - chart.renderer.text( - str, - 0, - 0, - labelOptions.useHTML - ) - /*= if (build.classic) { =*/ - // without position absolute, IE export sometimes is - // wrong. - .css(merge(labelOptions.style)) - /*= } =*/ - .add(axis.labelGroup) : - null; - - // Un-rotated length - if (label) { - label.textPxLength = label.getBBox().width; - } - - - // Base value to detect change for new calls to getBBox - tick.rotation = 0; - - // update - } else if (label) { - label.attr({ text: str }); - } - }, - - /** - * Get the offset height or width of the label - */ - getLabelSize: function () { - return this.label ? - this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : - 0; - }, - - /** - * Handle the label overflow by adjusting the labels to the left and right - * edge, or hide them if they collide into the neighbour label. - */ - handleOverflow: function (xy) { - var axis = this.axis, - labelOptions = axis.options.labels, - pxPos = xy.x, - chartWidth = axis.chart.chartWidth, - spacing = axis.chart.spacing, - leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])), - rightBound = pick( - axis.labelRight, - Math.max( - !axis.isRadial ? axis.pos + axis.len : 0, - chartWidth - spacing[1] - ) - ), - label = this.label, - rotation = this.rotation, - factor = { left: 0, center: 0.5, right: 1 }[ - axis.labelAlign || label.attr('align') - ], - labelWidth = label.getBBox().width, - slotWidth = axis.getSlotWidth(), - modifiedSlotWidth = slotWidth, - xCorrection = factor, - goRight = 1, - leftPos, - rightPos, - textWidth, - css = {}; - - // Check if the label overshoots the chart spacing box. If it does, move - // it. If it now overshoots the slotWidth, add ellipsis. - if (!rotation && labelOptions.overflow !== false) { - leftPos = pxPos - factor * labelWidth; - rightPos = pxPos + (1 - factor) * labelWidth; - - if (leftPos < leftBound) { - modifiedSlotWidth = - xy.x + modifiedSlotWidth * (1 - factor) - leftBound; - } else if (rightPos > rightBound) { - modifiedSlotWidth = - rightBound - xy.x + modifiedSlotWidth * factor; - goRight = -1; - } - - modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177 - if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') { - xy.x += ( - goRight * - ( - slotWidth - - modifiedSlotWidth - - xCorrection * ( - slotWidth - Math.min(labelWidth, modifiedSlotWidth) - ) - ) - ); - } - // If the label width exceeds the available space, set a text width - // to be picked up below. Also, if a width has been set before, we - // need to set a new one because the reported labelWidth will be - // limited by the box (#3938). - if ( - labelWidth > modifiedSlotWidth || - (axis.autoRotation && (label.styles || {}).width) - ) { - textWidth = modifiedSlotWidth; - } - - // Add ellipsis to prevent rotated labels to be clipped against the edge - // of the chart - } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { - textWidth = Math.round( - pxPos / Math.cos(rotation * deg2rad) - leftBound - ); - } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { - textWidth = Math.round( - (chartWidth - pxPos) / Math.cos(rotation * deg2rad) - ); - } - - if (textWidth) { - css.width = textWidth; - if (!(labelOptions.style || {}).textOverflow) { - css.textOverflow = 'ellipsis'; - } - label.css(css); - } - }, - - /** - * Get the x and y position for ticks and labels - */ - getPosition: function (horiz, tickPos, tickmarkOffset, old) { - var axis = this.axis, - chart = axis.chart, - cHeight = (old && chart.oldChartHeight) || chart.chartHeight, - pos; - - pos = { - x: horiz ? - H.correctFloat( - axis.translate(tickPos + tickmarkOffset, null, null, old) + - axis.transB - ) : - ( - axis.left + - axis.offset + - ( - axis.opposite ? - ( - ( - (old && chart.oldChartWidth) || - chart.chartWidth - ) - - axis.right - - axis.left - ) : - 0 - ) - ), - - y: horiz ? - ( - cHeight - - axis.bottom + - axis.offset - - (axis.opposite ? axis.height : 0) - ) : - H.correctFloat( - cHeight - - axis.translate(tickPos + tickmarkOffset, null, null, old) - - axis.transB - ) - }; - - fireEvent(this, 'afterGetPosition', { pos: pos }); - - return pos; - - }, - - /** - * Get the x, y position of the tick label - */ - getLabelPosition: function ( - x, - y, - label, - horiz, - labelOptions, - tickmarkOffset, - index, - step - ) { - var axis = this.axis, - transA = axis.transA, - reversed = axis.reversed, - staggerLines = axis.staggerLines, - rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, - yOffset = labelOptions.y, - - // Adjust for label alignment if we use reserveSpace: true (#5286) - labelOffsetCorrection = ( - !horiz && !axis.reserveSpaceDefault ? - -axis.labelOffset * ( - axis.labelAlign === 'center' ? 0.5 : 1 - ) : - 0 - ), - line, - pos = {}; - - if (!defined(yOffset)) { - if (axis.side === 0) { - yOffset = label.rotation ? -8 : -label.getBBox().height; - } else if (axis.side === 2) { - yOffset = rotCorr.y + 8; - } else { - // #3140, #3140 - yOffset = Math.cos(label.rotation * deg2rad) * - (rotCorr.y - label.getBBox(false, 0).height / 2); - } - } - - x = x + - labelOptions.x + - labelOffsetCorrection + - rotCorr.x - - ( - tickmarkOffset && horiz ? - tickmarkOffset * transA * (reversed ? -1 : 1) : - 0 - ); - y = y + yOffset - (tickmarkOffset && !horiz ? - tickmarkOffset * transA * (reversed ? 1 : -1) : 0); - - // Correct for staggered labels - if (staggerLines) { - line = (index / (step || 1) % staggerLines); - if (axis.opposite) { - line = staggerLines - line - 1; - } - y += line * (axis.labelOffset / staggerLines); - } - - pos.x = x; - pos.y = Math.round(y); - - fireEvent(this, 'afterGetLabelPosition', { pos: pos }); - - return pos; - }, - - /** - * Extendible method to return the path of the marker - */ - getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { - return renderer.crispLine([ - 'M', - x, - y, - 'L', - x + (horiz ? 0 : -tickLength), - y + (horiz ? tickLength : 0) - ], tickWidth); - }, - - /** - * Renders the gridLine. - * @param {Boolean} old Whether or not the tick is old - * @param {number} opacity The opacity of the grid line - * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 - * @return {undefined} - */ - renderGridLine: function (old, opacity, reverseCrisp) { - var tick = this, - axis = tick.axis, - options = axis.options, - gridLine = tick.gridLine, - gridLinePath, - attribs = {}, - pos = tick.pos, - type = tick.type, - tickmarkOffset = axis.tickmarkOffset, - renderer = axis.chart.renderer; - - /*= if (build.classic) { =*/ - var gridPrefix = type ? type + 'Grid' : 'grid', - gridLineWidth = options[gridPrefix + 'LineWidth'], - gridLineColor = options[gridPrefix + 'LineColor'], - dashStyle = options[gridPrefix + 'LineDashStyle']; - /*= } =*/ - - if (!gridLine) { - /*= if (build.classic) { =*/ - attribs.stroke = gridLineColor; - attribs['stroke-width'] = gridLineWidth; - if (dashStyle) { - attribs.dashstyle = dashStyle; - } - /*= } =*/ - if (!type) { - attribs.zIndex = 1; - } - if (old) { - attribs.opacity = 0; - } - tick.gridLine = gridLine = renderer.path() - .attr(attribs) - .addClass( - 'highcharts-' + (type ? type + '-' : '') + 'grid-line' - ) - .add(axis.gridGroup); - } - - // If the parameter 'old' is set, the current call will be followed - // by another call, therefore do not do any animations this time - if (!old && gridLine) { - gridLinePath = axis.getPlotLinePath( - pos + tickmarkOffset, - gridLine.strokeWidth() * reverseCrisp, - old, true - ); - if (gridLinePath) { - gridLine[tick.isNew ? 'attr' : 'animate']({ - d: gridLinePath, - opacity: opacity - }); - } - } - }, - - /** - * Renders the tick mark. - * @param {Object} xy The position vector of the mark - * @param {number} xy.x The x position of the mark - * @param {number} xy.y The y position of the mark - * @param {number} opacity The opacity of the mark - * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 - * @return {undefined} - */ - renderMark: function (xy, opacity, reverseCrisp) { - var tick = this, - axis = tick.axis, - options = axis.options, - renderer = axis.chart.renderer, - type = tick.type, - tickPrefix = type ? type + 'Tick' : 'tick', - tickSize = axis.tickSize(tickPrefix), - mark = tick.mark, - isNewMark = !mark, - x = xy.x, - y = xy.y; - - /*= if (build.classic) { =*/ - var tickWidth = pick( - options[tickPrefix + 'Width'], - !type && axis.isXAxis ? 1 : 0 - ), // X axis defaults to 1 - tickColor = options[tickPrefix + 'Color']; - /*= } =*/ - - if (tickSize) { - - // negate the length - if (axis.opposite) { - tickSize[0] = -tickSize[0]; - } - - // First time, create it - if (isNewMark) { - tick.mark = mark = renderer.path() - .addClass('highcharts-' + (type ? type + '-' : '') + 'tick') - .add(axis.axisGroup); - - /*= if (build.classic) { =*/ - mark.attr({ - stroke: tickColor, - 'stroke-width': tickWidth - }); - /*= } =*/ - } - mark[isNewMark ? 'attr' : 'animate']({ - d: tick.getMarkPath( - x, - y, - tickSize[0], - mark.strokeWidth() * reverseCrisp, - axis.horiz, - renderer), - opacity: opacity - }); - - } - }, - - /** - * Renders the tick label. - * Note: The label should already be created in init(), so it should only - * have to be moved into place. - * @param {Object} xy The position vector of the label - * @param {number} xy.x The x position of the label - * @param {number} xy.y The y position of the label - * @param {Boolean} old Whether or not the tick is old - * @param {number} opacity The opacity of the label - * @param {number} index The index of the tick - * @return {undefined} - */ - renderLabel: function (xy, old, opacity, index) { - var tick = this, - axis = tick.axis, - horiz = axis.horiz, - options = axis.options, - label = tick.label, - labelOptions = options.labels, - step = labelOptions.step, - tickmarkOffset = axis.tickmarkOffset, - show = true, - x = xy.x, - y = xy.y; - if (label && isNumber(x)) { - label.xy = xy = tick.getLabelPosition( - x, - y, - label, - horiz, - labelOptions, - tickmarkOffset, - index, - step - ); - - // Apply show first and show last. If the tick is both first and - // last, it is a single centered tick, in which case we show the - // label anyway (#2100). - if ( - ( - tick.isFirst && - !tick.isLast && - !pick(options.showFirstLabel, 1) - ) || - ( - tick.isLast && - !tick.isFirst && - !pick(options.showLastLabel, 1) - ) - ) { - show = false; - - // Handle label overflow and show or hide accordingly - } else if ( - horiz && - !labelOptions.step && - !labelOptions.rotation && - !old && - opacity !== 0 - ) { - tick.handleOverflow(xy); - } - - // apply step - if (step && index % step) { - // show those indices dividable by step - show = false; - } - - // Set the new position, and show or hide - if (show && isNumber(xy.y)) { - xy.opacity = opacity; - label[tick.isNewLabel ? 'attr' : 'animate'](xy); - tick.isNewLabel = false; - } else { - label.attr('y', -9999); // #1338 - tick.isNewLabel = true; - } - } - }, - - /** - * Put everything in place - * - * @param index {Number} - * @param old {Boolean} Use old coordinates to prepare an animation into new - * position - */ - render: function (index, old, opacity) { - var tick = this, - axis = tick.axis, - horiz = axis.horiz, - pos = tick.pos, - tickmarkOffset = axis.tickmarkOffset, - xy = tick.getPosition(horiz, pos, tickmarkOffset, old), - x = xy.x, - y = xy.y, - reverseCrisp = ((horiz && x === axis.pos + axis.len) || - (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 - - opacity = pick(opacity, 1); - this.isActive = true; - - // Create the grid line - this.renderGridLine(old, opacity, reverseCrisp); - - // create the tick mark - this.renderMark(xy, opacity, reverseCrisp); - - // the label is created on init - now move it into place - this.renderLabel(xy, old, opacity, index); - - tick.isNew = false; - - H.fireEvent(this, 'afterRender'); - }, - - /** - * Destructor for the tick prototype - */ - destroy: function () { - destroyObjectProperties(this, this.axis); - } + /** + * Write the tick label + */ + addLabel: function () { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + categories = axis.categories, + names = axis.names, + pos = tick.pos, + labelOptions = options.labels, + str, + tickPositions = axis.tickPositions, + isFirst = pos === tickPositions[0], + isLast = pos === tickPositions[tickPositions.length - 1], + value = categories ? + pick(categories[pos], names[pos], pos) : + pos, + label = tick.label, + tickPositionInfo = tickPositions.info, + dateTimeLabelFormat; + + // Set the datetime label format. If a higher rank is set for this + // position, use that. If not, use the general format. + if (axis.isDatetimeAxis && tickPositionInfo) { + dateTimeLabelFormat = + options.dateTimeLabelFormats[ + tickPositionInfo.higherRanks[pos] || + tickPositionInfo.unitName + ]; + } + // set properties for access in render method + tick.isFirst = isFirst; + tick.isLast = isLast; + + // get the string + str = axis.labelFormatter.call({ + axis: axis, + chart: chart, + isFirst: isFirst, + isLast: isLast, + dateTimeLabelFormat: dateTimeLabelFormat, + value: axis.isLog ? correctFloat(axis.lin2log(value)) : value, + pos: pos + }); + + // first call + if (!defined(label)) { + + tick.label = label = + defined(str) && labelOptions.enabled ? + chart.renderer.text( + str, + 0, + 0, + labelOptions.useHTML + ) + /*= if (build.classic) { =*/ + // without position absolute, IE export sometimes is + // wrong. + .css(merge(labelOptions.style)) + /*= } =*/ + .add(axis.labelGroup) : + null; + + // Un-rotated length + if (label) { + label.textPxLength = label.getBBox().width; + } + + + // Base value to detect change for new calls to getBBox + tick.rotation = 0; + + // update + } else if (label) { + label.attr({ text: str }); + } + }, + + /** + * Get the offset height or width of the label + */ + getLabelSize: function () { + return this.label ? + this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : + 0; + }, + + /** + * Handle the label overflow by adjusting the labels to the left and right + * edge, or hide them if they collide into the neighbour label. + */ + handleOverflow: function (xy) { + var axis = this.axis, + labelOptions = axis.options.labels, + pxPos = xy.x, + chartWidth = axis.chart.chartWidth, + spacing = axis.chart.spacing, + leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])), + rightBound = pick( + axis.labelRight, + Math.max( + !axis.isRadial ? axis.pos + axis.len : 0, + chartWidth - spacing[1] + ) + ), + label = this.label, + rotation = this.rotation, + factor = { left: 0, center: 0.5, right: 1 }[ + axis.labelAlign || label.attr('align') + ], + labelWidth = label.getBBox().width, + slotWidth = axis.getSlotWidth(), + modifiedSlotWidth = slotWidth, + xCorrection = factor, + goRight = 1, + leftPos, + rightPos, + textWidth, + css = {}; + + // Check if the label overshoots the chart spacing box. If it does, move + // it. If it now overshoots the slotWidth, add ellipsis. + if (!rotation && labelOptions.overflow !== false) { + leftPos = pxPos - factor * labelWidth; + rightPos = pxPos + (1 - factor) * labelWidth; + + if (leftPos < leftBound) { + modifiedSlotWidth = + xy.x + modifiedSlotWidth * (1 - factor) - leftBound; + } else if (rightPos > rightBound) { + modifiedSlotWidth = + rightBound - xy.x + modifiedSlotWidth * factor; + goRight = -1; + } + + modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177 + if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') { + xy.x += ( + goRight * + ( + slotWidth - + modifiedSlotWidth - + xCorrection * ( + slotWidth - Math.min(labelWidth, modifiedSlotWidth) + ) + ) + ); + } + // If the label width exceeds the available space, set a text width + // to be picked up below. Also, if a width has been set before, we + // need to set a new one because the reported labelWidth will be + // limited by the box (#3938). + if ( + labelWidth > modifiedSlotWidth || + (axis.autoRotation && (label.styles || {}).width) + ) { + textWidth = modifiedSlotWidth; + } + + // Add ellipsis to prevent rotated labels to be clipped against the edge + // of the chart + } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { + textWidth = Math.round( + pxPos / Math.cos(rotation * deg2rad) - leftBound + ); + } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { + textWidth = Math.round( + (chartWidth - pxPos) / Math.cos(rotation * deg2rad) + ); + } + + if (textWidth) { + css.width = textWidth; + if (!(labelOptions.style || {}).textOverflow) { + css.textOverflow = 'ellipsis'; + } + label.css(css); + } + }, + + /** + * Get the x and y position for ticks and labels + */ + getPosition: function (horiz, tickPos, tickmarkOffset, old) { + var axis = this.axis, + chart = axis.chart, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + pos; + + pos = { + x: horiz ? + H.correctFloat( + axis.translate(tickPos + tickmarkOffset, null, null, old) + + axis.transB + ) : + ( + axis.left + + axis.offset + + ( + axis.opposite ? + ( + ( + (old && chart.oldChartWidth) || + chart.chartWidth + ) - + axis.right - + axis.left + ) : + 0 + ) + ), + + y: horiz ? + ( + cHeight - + axis.bottom + + axis.offset - + (axis.opposite ? axis.height : 0) + ) : + H.correctFloat( + cHeight - + axis.translate(tickPos + tickmarkOffset, null, null, old) - + axis.transB + ) + }; + + fireEvent(this, 'afterGetPosition', { pos: pos }); + + return pos; + + }, + + /** + * Get the x, y position of the tick label + */ + getLabelPosition: function ( + x, + y, + label, + horiz, + labelOptions, + tickmarkOffset, + index, + step + ) { + var axis = this.axis, + transA = axis.transA, + reversed = axis.reversed, + staggerLines = axis.staggerLines, + rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, + yOffset = labelOptions.y, + + // Adjust for label alignment if we use reserveSpace: true (#5286) + labelOffsetCorrection = ( + !horiz && !axis.reserveSpaceDefault ? + -axis.labelOffset * ( + axis.labelAlign === 'center' ? 0.5 : 1 + ) : + 0 + ), + line, + pos = {}; + + if (!defined(yOffset)) { + if (axis.side === 0) { + yOffset = label.rotation ? -8 : -label.getBBox().height; + } else if (axis.side === 2) { + yOffset = rotCorr.y + 8; + } else { + // #3140, #3140 + yOffset = Math.cos(label.rotation * deg2rad) * + (rotCorr.y - label.getBBox(false, 0).height / 2); + } + } + + x = x + + labelOptions.x + + labelOffsetCorrection + + rotCorr.x - + ( + tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : + 0 + ); + y = y + yOffset - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); + + // Correct for staggered labels + if (staggerLines) { + line = (index / (step || 1) % staggerLines); + if (axis.opposite) { + line = staggerLines - line - 1; + } + y += line * (axis.labelOffset / staggerLines); + } + + pos.x = x; + pos.y = Math.round(y); + + fireEvent(this, 'afterGetLabelPosition', { pos: pos }); + + return pos; + }, + + /** + * Extendible method to return the path of the marker + */ + getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { + return renderer.crispLine([ + 'M', + x, + y, + 'L', + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + }, + + /** + * Renders the gridLine. + * @param {Boolean} old Whether or not the tick is old + * @param {number} opacity The opacity of the grid line + * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 + * @return {undefined} + */ + renderGridLine: function (old, opacity, reverseCrisp) { + var tick = this, + axis = tick.axis, + options = axis.options, + gridLine = tick.gridLine, + gridLinePath, + attribs = {}, + pos = tick.pos, + type = tick.type, + tickmarkOffset = axis.tickmarkOffset, + renderer = axis.chart.renderer; + + /*= if (build.classic) { =*/ + var gridPrefix = type ? type + 'Grid' : 'grid', + gridLineWidth = options[gridPrefix + 'LineWidth'], + gridLineColor = options[gridPrefix + 'LineColor'], + dashStyle = options[gridPrefix + 'LineDashStyle']; + /*= } =*/ + + if (!gridLine) { + /*= if (build.classic) { =*/ + attribs.stroke = gridLineColor; + attribs['stroke-width'] = gridLineWidth; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + /*= } =*/ + if (!type) { + attribs.zIndex = 1; + } + if (old) { + attribs.opacity = 0; + } + tick.gridLine = gridLine = renderer.path() + .attr(attribs) + .addClass( + 'highcharts-' + (type ? type + '-' : '') + 'grid-line' + ) + .add(axis.gridGroup); + } + + // If the parameter 'old' is set, the current call will be followed + // by another call, therefore do not do any animations this time + if (!old && gridLine) { + gridLinePath = axis.getPlotLinePath( + pos + tickmarkOffset, + gridLine.strokeWidth() * reverseCrisp, + old, true + ); + if (gridLinePath) { + gridLine[tick.isNew ? 'attr' : 'animate']({ + d: gridLinePath, + opacity: opacity + }); + } + } + }, + + /** + * Renders the tick mark. + * @param {Object} xy The position vector of the mark + * @param {number} xy.x The x position of the mark + * @param {number} xy.y The y position of the mark + * @param {number} opacity The opacity of the mark + * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 + * @return {undefined} + */ + renderMark: function (xy, opacity, reverseCrisp) { + var tick = this, + axis = tick.axis, + options = axis.options, + renderer = axis.chart.renderer, + type = tick.type, + tickPrefix = type ? type + 'Tick' : 'tick', + tickSize = axis.tickSize(tickPrefix), + mark = tick.mark, + isNewMark = !mark, + x = xy.x, + y = xy.y; + + /*= if (build.classic) { =*/ + var tickWidth = pick( + options[tickPrefix + 'Width'], + !type && axis.isXAxis ? 1 : 0 + ), // X axis defaults to 1 + tickColor = options[tickPrefix + 'Color']; + /*= } =*/ + + if (tickSize) { + + // negate the length + if (axis.opposite) { + tickSize[0] = -tickSize[0]; + } + + // First time, create it + if (isNewMark) { + tick.mark = mark = renderer.path() + .addClass('highcharts-' + (type ? type + '-' : '') + 'tick') + .add(axis.axisGroup); + + /*= if (build.classic) { =*/ + mark.attr({ + stroke: tickColor, + 'stroke-width': tickWidth + }); + /*= } =*/ + } + mark[isNewMark ? 'attr' : 'animate']({ + d: tick.getMarkPath( + x, + y, + tickSize[0], + mark.strokeWidth() * reverseCrisp, + axis.horiz, + renderer), + opacity: opacity + }); + + } + }, + + /** + * Renders the tick label. + * Note: The label should already be created in init(), so it should only + * have to be moved into place. + * @param {Object} xy The position vector of the label + * @param {number} xy.x The x position of the label + * @param {number} xy.y The y position of the label + * @param {Boolean} old Whether or not the tick is old + * @param {number} opacity The opacity of the label + * @param {number} index The index of the tick + * @return {undefined} + */ + renderLabel: function (xy, old, opacity, index) { + var tick = this, + axis = tick.axis, + horiz = axis.horiz, + options = axis.options, + label = tick.label, + labelOptions = options.labels, + step = labelOptions.step, + tickmarkOffset = axis.tickmarkOffset, + show = true, + x = xy.x, + y = xy.y; + if (label && isNumber(x)) { + label.xy = xy = tick.getLabelPosition( + x, + y, + label, + horiz, + labelOptions, + tickmarkOffset, + index, + step + ); + + // Apply show first and show last. If the tick is both first and + // last, it is a single centered tick, in which case we show the + // label anyway (#2100). + if ( + ( + tick.isFirst && + !tick.isLast && + !pick(options.showFirstLabel, 1) + ) || + ( + tick.isLast && + !tick.isFirst && + !pick(options.showLastLabel, 1) + ) + ) { + show = false; + + // Handle label overflow and show or hide accordingly + } else if ( + horiz && + !labelOptions.step && + !labelOptions.rotation && + !old && + opacity !== 0 + ) { + tick.handleOverflow(xy); + } + + // apply step + if (step && index % step) { + // show those indices dividable by step + show = false; + } + + // Set the new position, and show or hide + if (show && isNumber(xy.y)) { + xy.opacity = opacity; + label[tick.isNewLabel ? 'attr' : 'animate'](xy); + tick.isNewLabel = false; + } else { + label.attr('y', -9999); // #1338 + tick.isNewLabel = true; + } + } + }, + + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new + * position + */ + render: function (index, old, opacity) { + var tick = this, + axis = tick.axis, + horiz = axis.horiz, + pos = tick.pos, + tickmarkOffset = axis.tickmarkOffset, + xy = tick.getPosition(horiz, pos, tickmarkOffset, old), + x = xy.x, + y = xy.y, + reverseCrisp = ((horiz && x === axis.pos + axis.len) || + (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 + + opacity = pick(opacity, 1); + this.isActive = true; + + // Create the grid line + this.renderGridLine(old, opacity, reverseCrisp); + + // create the tick mark + this.renderMark(xy, opacity, reverseCrisp); + + // the label is created on init - now move it into place + this.renderLabel(xy, old, opacity, index); + + tick.isNew = false; + + H.fireEvent(this, 'afterRender'); + }, + + /** + * Destructor for the tick prototype + */ + destroy: function () { + destroyObjectProperties(this, this.axis); + } }; diff --git a/js/parts/Time.js b/js/parts/Time.js index 978a25d627a..57ce3b74244 100644 --- a/js/parts/Time.js +++ b/js/parts/Time.js @@ -3,19 +3,19 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import Highcharts from './Globals.js'; var H = Highcharts, - defined = H.defined, - each = H.each, - extend = H.extend, - merge = H.merge, - pick = H.pick, - timeUnits = H.timeUnits, - win = H.win; + defined = H.defined, + each = H.each, + extend = H.extend, + merge = H.merge, + pick = H.pick, + timeUnits = H.timeUnits, + win = H.win; /** * The Time class. Time settings are applied in general for each page using @@ -34,7 +34,7 @@ var H = Highcharts, * timezone: 'Europe/London' * } * }); - * + * * // Apply time settings by instance * var chart = Highcharts.chart('container', { * time: { @@ -47,8 +47,8 @@ var H = Highcharts, * * // Use the Time object * console.log( - * 'Current time in New York', - * chart.time.dateFormat('%Y-%m-%d %H:%M:%S', Date.now()) + * 'Current time in New York', + * chart.time.dateFormat('%Y-%m-%d %H:%M:%S', Date.now()) * ); * * @param options {Object} @@ -57,695 +57,695 @@ var H = Highcharts, * @class */ Highcharts.Time = function (options) { - this.update(options, false); + this.update(options, false); }; Highcharts.Time.prototype = { - /** - * Time options that can apply globally or to individual charts. These - * settings affect how `datetime` axes are laid out, how tooltips are - * formatted, how series - * [pointIntervalUnit](#plotOptions.series.pointIntervalUnit) works and how - * the Highstock range selector handles time. - * - * The common use case is that all charts in the same Highcharts object - * share the same time settings, in which case the global settings are set - * using `setOptions`. - * - * ```js - * // Apply time settings globally - * Highcharts.setOptions({ - * time: { - * timezone: 'Europe/London' - * } - * }); - * // Apply time settings by instance - * var chart = Highcharts.chart('container', { - * time: { - * timezone: 'America/New_York' - * }, - * series: [{ - * data: [1, 4, 3, 5] - * }] - * }); - * - * // Use the Time object - * console.log( - * 'Current time in New York', - * chart.time.dateFormat('%Y-%m-%d %H:%M:%S', Date.now()) - * ); - * ``` - * - * Since v6.0.5, the time options were moved from the `global` obect to the - * `time` object, and time options can be set on each individual chart. - * - * @sample {highcharts|highstock} - * highcharts/time/timezone/ - * Set the timezone globally - * @sample {highcharts} - * highcharts/time/individual/ - * Set the timezone per chart instance - * @sample {highstock} - * stock/time/individual/ - * Set the timezone per chart instance - * @since 6.0.5 - * @apioption time - */ - - /** - * Whether to use UTC time for axis scaling, tickmark placement and - * time display in `Highcharts.dateFormat`. Advantages of using UTC - * is that the time displays equally regardless of the user agent's - * time zone settings. Local time can be used when the data is loaded - * in real time or when correct Daylight Saving Time transitions are - * required. - * - * @type {Boolean} - * @sample {highcharts} highcharts/time/useutc-true/ True by default - * @sample {highcharts} highcharts/time/useutc-false/ False - * @apioption time.useUTC - * @default true - */ - - /** - * A custom `Date` class for advanced date handling. For example, - * [JDate](https://github.com/tahajahangir/jdate) can be hooked in to - * handle Jalali dates. - * - * @type {Object} - * @since 4.0.4 - * @product highcharts highstock - * @apioption time.Date - */ - - /** - * A callback to return the time zone offset for a given datetime. It - * takes the timestamp in terms of milliseconds since January 1 1970, - * and returns the timezone offset in minutes. This provides a hook - * for drawing time based charts in specific time zones using their - * local DST crossover dates, with the help of external libraries. - * - * @type {Function} - * @see [global.timezoneOffset](#global.timezoneOffset) - * @sample {highcharts|highstock} - * highcharts/time/gettimezoneoffset/ - * Use moment.js to draw Oslo time regardless of browser locale - * @since 4.1.0 - * @product highcharts highstock - * @apioption time.getTimezoneOffset - */ - - /** - * Requires [moment.js](http://momentjs.com/). If the timezone option - * is specified, it creates a default - * [getTimezoneOffset](#time.getTimezoneOffset) function that looks - * up the specified timezone in moment.js. If moment.js is not included, - * this throws a Highcharts error in the console, but does not crash the - * chart. - * - * @type {String} - * @see [getTimezoneOffset](#time.getTimezoneOffset) - * @sample {highcharts|highstock} - * highcharts/time/timezone/ - * Europe/Oslo - * @default undefined - * @since 5.0.7 - * @product highcharts highstock - * @apioption time.timezone - */ - - /** - * The timezone offset in minutes. Positive values are west, negative - * values are east of UTC, as in the ECMAScript - * [getTimezoneOffset](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset) - * method. Use this to display UTC based data in a predefined time zone. - * - * @type {Number} - * @see [time.getTimezoneOffset](#time.getTimezoneOffset) - * @sample {highcharts|highstock} - * highcharts/time/timezoneoffset/ - * Timezone offset - * @default 0 - * @since 3.0.8 - * @product highcharts highstock - * @apioption time.timezoneOffset - */ - defaultOptions: {}, - - /** - * Update the Time object with current options. It is called internally on - * initiating Highcharts, after running `Highcharts.setOptions` and on - * `Chart.update`. - * - * @private - */ - update: function (options) { - var useUTC = pick(options && options.useUTC, true), - time = this; - - this.options = options = merge(true, this.options || {}, options); - - // Allow using a different Date class - this.Date = options.Date || win.Date; - - this.useUTC = useUTC; - this.timezoneOffset = useUTC && options.timezoneOffset; - - /** - * Get the time zone offset based on the current timezone information as - * set in the global options. - * - * @function #getTimezoneOffset - * @memberOf Highcharts.Time - * @param {Number} timestamp - * The JavaScript timestamp to inspect. - * @return {Number} - * The timezone offset in minutes compared to UTC. - */ - this.getTimezoneOffset = this.timezoneOffsetFunction(); - - /* - * The time object has options allowing for variable time zones, meaning - * the axis ticks or series data needs to consider this. - */ - this.variableTimezone = !!( - !useUTC || - options.getTimezoneOffset || - options.timezone - ); - - // UTC time with timezone handling - if (this.variableTimezone || this.timezoneOffset) { - this.get = function (unit, date) { - var realMs = date.getTime(), - ms = realMs - time.getTimezoneOffset(date), - ret; - - date.setTime(ms); // Temporary adjust to timezone - ret = date['getUTC' + unit](); - date.setTime(realMs); // Reset - - return ret; - }; - this.set = function (unit, date, value) { - var ms, offset, newOffset; - - // For lower order time units, just set it directly using local - // time - if ( - H.inArray(unit, ['Milliseconds', 'Seconds', 'Minutes']) !== - -1 - ) { - date['set' + unit](value); - - // Higher order time units need to take the time zone into - // account - } else { - - // Adjust by timezone - offset = time.getTimezoneOffset(date); - ms = date.getTime() - offset; - date.setTime(ms); - - date['setUTC' + unit](value); - newOffset = time.getTimezoneOffset(date); - - ms = date.getTime() + newOffset; - date.setTime(ms); - } - - }; - - // UTC time with no timezone handling - } else if (useUTC) { - this.get = function (unit, date) { - return date['getUTC' + unit](); - }; - this.set = function (unit, date, value) { - return date['setUTC' + unit](value); - }; - - // Local time - } else { - this.get = function (unit, date) { - return date['get' + unit](); - }; - this.set = function (unit, date, value) { - return date['set' + unit](value); - }; - } - - }, - - /** - * Make a time and returns milliseconds. Interprets the inputs as UTC time, - * local time or a specific timezone time depending on the current time - * settings. - * - * @param {Number} year - * The year - * @param {Number} month - * The month. Zero-based, so January is 0. - * @param {Number} date - * The day of the month - * @param {Number} hours - * The hour of the day, 0-23. - * @param {Number} minutes - * The minutes - * @param {Number} seconds - * The seconds - * - * @return {Number} - * The time in milliseconds since January 1st 1970. - */ - makeTime: function (year, month, date, hours, minutes, seconds) { - var d, offset, newOffset; - if (this.useUTC) { - d = this.Date.UTC.apply(0, arguments); - offset = this.getTimezoneOffset(d); - d += offset; - newOffset = this.getTimezoneOffset(d); - - if (offset !== newOffset) { - d += newOffset - offset; - - // A special case for transitioning from summer time to winter time. - // When the clock is set back, the same time is repeated twice, i.e. - // 02:30 am is repeated since the clock is set back from 3 am to - // 2 am. We need to make the same time as local Date does. - } else if ( - offset - 36e5 === this.getTimezoneOffset(d - 36e5) && - !H.isSafari - ) { - d -= 36e5; - } - - } else { - d = new this.Date( - year, - month, - pick(date, 1), - pick(hours, 0), - pick(minutes, 0), - pick(seconds, 0) - ).getTime(); - } - return d; - }, - - /** - * Sets the getTimezoneOffset function. If the `timezone` option is set, a - * default getTimezoneOffset function with that timezone is returned. If - * a `getTimezoneOffset` option is defined, it is returned. If neither are - * specified, the function using the `timezoneOffset` option or 0 offset is - * returned. - * - * @private - * @return {Function} A getTimezoneOffset function - */ - timezoneOffsetFunction: function () { - var time = this, - options = this.options, - moment = win.moment; - - if (!this.useUTC) { - return function (timestamp) { - return new Date(timestamp).getTimezoneOffset() * 60000; - }; - } - - if (options.timezone) { - if (!moment) { - // getTimezoneOffset-function stays undefined because it depends - // on Moment.js - H.error(25); - - } else { - return function (timestamp) { - return -moment.tz( - timestamp, - options.timezone - ).utcOffset() * 60000; - }; - } - } - - // If not timezone is set, look for the getTimezoneOffset callback - if (this.useUTC && options.getTimezoneOffset) { - return function (timestamp) { - return options.getTimezoneOffset(timestamp) * 60000; - }; - } - - // Last, use the `timezoneOffset` option if set - return function () { - return (time.timezoneOffset || 0) * 60000; - }; - }, - - /** - * Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970) - * into a human readable date string. The format is a subset of the formats - * for PHP's [strftime](http://www.php.net/manual/en/function.strftime.php) - * function. Additional formats can be given in the - * {@link Highcharts.dateFormats} hook. - * - * @param {String} format - * The desired format where various time - * representations are prefixed with %. - * @param {Number} timestamp - * The JavaScript timestamp. - * @param {Boolean} [capitalize=false] - * Upper case first letter in the return. - * @returns {String} The formatted date. - */ - dateFormat: function (format, timestamp, capitalize) { - if (!H.defined(timestamp) || isNaN(timestamp)) { - return H.defaultOptions.lang.invalidDate || ''; - } - format = H.pick(format, '%Y-%m-%d %H:%M:%S'); - - var time = this, - date = new this.Date(timestamp), - // get the basic time values - hours = this.get('Hours', date), - day = this.get('Day', date), - dayOfMonth = this.get('Date', date), - month = this.get('Month', date), - fullYear = this.get('FullYear', date), - lang = H.defaultOptions.lang, - langWeekdays = lang.weekdays, - shortWeekdays = lang.shortWeekdays, - pad = H.pad, - - // List all format keys. Custom formats can be added from the - // outside. - replacements = H.extend( - { - - // Day - // Short weekday, like 'Mon' - 'a': shortWeekdays ? - shortWeekdays[day] : - langWeekdays[day].substr(0, 3), - // Long weekday, like 'Monday' - 'A': langWeekdays[day], - // Two digit day of the month, 01 to 31 - 'd': pad(dayOfMonth), - // Day of the month, 1 through 31 - 'e': pad(dayOfMonth, 2, ' '), - 'w': day, - - // Week (none implemented) - // 'W': weekNumber(), - - // Month - // Short month, like 'Jan' - 'b': lang.shortMonths[month], - // Long month, like 'January' - 'B': lang.months[month], - // Two digit month number, 01 through 12 - 'm': pad(month + 1), - - // Year - // Two digits year, like 09 for 2009 - 'y': fullYear.toString().substr(2, 2), - // Four digits year, like 2009 - 'Y': fullYear, - - // Time - // Two digits hours in 24h format, 00 through 23 - 'H': pad(hours), - // Hours in 24h format, 0 through 23 - 'k': hours, - // Two digits hours in 12h format, 00 through 11 - 'I': pad((hours % 12) || 12), - // Hours in 12h format, 1 through 12 - 'l': (hours % 12) || 12, - // Two digits minutes, 00 through 59 - 'M': pad(time.get('Minutes', date)), - // Upper case AM or PM - 'p': hours < 12 ? 'AM' : 'PM', - // Lower case AM or PM - 'P': hours < 12 ? 'am' : 'pm', - // Two digits seconds, 00 through 59 - 'S': pad(date.getSeconds()), - // Milliseconds (naming from Ruby) - 'L': pad(Math.round(timestamp % 1000), 3) - }, - - /** - * A hook for defining additional date format specifiers. New - * specifiers are defined as key-value pairs by using the - * specifier as key, and a function which takes the timestamp as - * value. This function returns the formatted portion of the - * date. - * - * @type {Object} - * @name dateFormats - * @memberOf Highcharts - * @sample highcharts/global/dateformats/ - * Adding support for week - * number - */ - H.dateFormats - ); - - - // Do the replaces - H.objectEach(replacements, function (val, key) { - // Regex would do it in one line, but this is faster - while (format.indexOf('%' + key) !== -1) { - format = format.replace( - '%' + key, - typeof val === 'function' ? val.call(time, timestamp) : val - ); - } - - }); - - // Optionally capitalize the string and return - return capitalize ? - format.substr(0, 1).toUpperCase() + format.substr(1) : - format; - }, - - /** - * Return an array with time positions distributed on round time values - * right and right after min and max. Used in datetime axes as well as for - * grouping data on a datetime axis. - * - * @param {Object} normalizedInterval - * The interval in axis values (ms) and thecount - * @param {Number} min The minimum in axis values - * @param {Number} max The maximum in axis values - * @param {Number} startOfWeek - */ - getTimeTicks: function ( - normalizedInterval, - min, - max, - startOfWeek - ) { - var time = this, - Date = time.Date, - tickPositions = [], - i, - higherRanks = {}, - minYear, // used in months and years as a basis for Date.UTC() - // When crossing DST, use the max. Resolves #6278. - minDate = new Date(min), - interval = normalizedInterval.unitRange, - count = normalizedInterval.count || 1, - variableDayLength; - - if (defined(min)) { // #1300 - time.set( - 'Milliseconds', - minDate, - interval >= timeUnits.second ? - 0 : // #3935 - count * Math.floor( - time.get('Milliseconds', minDate) / count - ) - ); // #3652, #3654 - - if (interval >= timeUnits.second) { // second - time.set('Seconds', - minDate, - interval >= timeUnits.minute ? - 0 : // #3935 - count * Math.floor(time.get('Seconds', minDate) / count) - ); - } - - if (interval >= timeUnits.minute) { // minute - time.set('Minutes', minDate, - interval >= timeUnits.hour ? - 0 : - count * Math.floor(time.get('Minutes', minDate) / count) - ); - } - - if (interval >= timeUnits.hour) { // hour - time.set( - 'Hours', - minDate, - interval >= timeUnits.day ? - 0 : - count * Math.floor( - time.get('Hours', minDate) / count - ) - ); - } - - if (interval >= timeUnits.day) { // day - time.set( - 'Date', - minDate, - interval >= timeUnits.month ? - 1 : - count * Math.floor(time.get('Date', minDate) / count) - ); - } - - if (interval >= timeUnits.month) { // month - time.set( - 'Month', - minDate, - interval >= timeUnits.year ? 0 : - count * Math.floor(time.get('Month', minDate) / count) - ); - minYear = time.get('FullYear', minDate); - } - - if (interval >= timeUnits.year) { // year - minYear -= minYear % count; - time.set('FullYear', minDate, minYear); - } - - // week is a special case that runs outside the hierarchy - if (interval === timeUnits.week) { - // get start of current week, independent of count - time.set( - 'Date', - minDate, - ( - time.get('Date', minDate) - - time.get('Day', minDate) + - pick(startOfWeek, 1) - ) - ); - } - - - // Get basics for variable time spans - minYear = time.get('FullYear', minDate); - var minMonth = time.get('Month', minDate), - minDateDate = time.get('Date', minDate), - minHours = time.get('Hours', minDate); - - // Redefine min to the floored/rounded minimum time (#7432) - min = minDate.getTime(); - - // Handle local timezone offset - if (time.variableTimezone) { - - // Detect whether we need to take the DST crossover into - // consideration. If we're crossing over DST, the day length may - // be 23h or 25h and we need to compute the exact clock time for - // each tick instead of just adding hours. This comes at a cost, - // so first we find out if it is needed (#4951). - variableDayLength = ( - // Long range, assume we're crossing over. - max - min > 4 * timeUnits.month || - // Short range, check if min and max are in different time - // zones. - time.getTimezoneOffset(min) !== time.getTimezoneOffset(max) - ); - } - - // Iterate and add tick positions at appropriate values - var t = minDate.getTime(); - i = 1; - while (t < max) { - tickPositions.push(t); - - // if the interval is years, use Date.UTC to increase years - if (interval === timeUnits.year) { - t = time.makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval === timeUnits.month) { - t = time.makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it - // jumps one hour at the DST crossover - } else if ( - variableDayLength && - (interval === timeUnits.day || interval === timeUnits.week) - ) { - t = time.makeTime( - minYear, - minMonth, - minDateDate + - i * count * (interval === timeUnits.day ? 1 : 7) - ); - - } else if ( - variableDayLength && - interval === timeUnits.hour && - count > 1 - ) { - // make sure higher ranks are preserved across DST (#6797, - // #7621) - t = time.makeTime( - minYear, - minMonth, - minDateDate, - minHours + i * count - ); - - // else, the interval is fixed and we use simple addition - } else { - t += interval * count; - } - - i++; - } - - // push the last time - tickPositions.push(t); - - - // Handle higher ranks. Mark new days if the time is on midnight - // (#950, #1649, #1760, #3349). Use a reasonable dropout threshold - // to prevent looping over dense data grouping (#6156). - if (interval <= timeUnits.hour && tickPositions.length < 10000) { - each(tickPositions, function (t) { - if ( - // Speed optimization, no need to run dateFormat unless - // we're on a full or half hour - t % 1800000 === 0 && - // Check for local or global midnight - time.dateFormat('%H%M%S%L', t) === '000000000' - ) { - higherRanks[t] = 'day'; - } - }); - } - } - - - // record information on the chosen unit - for dynamic label formatter - tickPositions.info = extend(normalizedInterval, { - higherRanks: higherRanks, - totalRange: interval * count - }); - - return tickPositions; - } + /** + * Time options that can apply globally or to individual charts. These + * settings affect how `datetime` axes are laid out, how tooltips are + * formatted, how series + * [pointIntervalUnit](#plotOptions.series.pointIntervalUnit) works and how + * the Highstock range selector handles time. + * + * The common use case is that all charts in the same Highcharts object + * share the same time settings, in which case the global settings are set + * using `setOptions`. + * + * ```js + * // Apply time settings globally + * Highcharts.setOptions({ + * time: { + * timezone: 'Europe/London' + * } + * }); + * // Apply time settings by instance + * var chart = Highcharts.chart('container', { + * time: { + * timezone: 'America/New_York' + * }, + * series: [{ + * data: [1, 4, 3, 5] + * }] + * }); + * + * // Use the Time object + * console.log( + * 'Current time in New York', + * chart.time.dateFormat('%Y-%m-%d %H:%M:%S', Date.now()) + * ); + * ``` + * + * Since v6.0.5, the time options were moved from the `global` obect to the + * `time` object, and time options can be set on each individual chart. + * + * @sample {highcharts|highstock} + * highcharts/time/timezone/ + * Set the timezone globally + * @sample {highcharts} + * highcharts/time/individual/ + * Set the timezone per chart instance + * @sample {highstock} + * stock/time/individual/ + * Set the timezone per chart instance + * @since 6.0.5 + * @apioption time + */ + + /** + * Whether to use UTC time for axis scaling, tickmark placement and + * time display in `Highcharts.dateFormat`. Advantages of using UTC + * is that the time displays equally regardless of the user agent's + * time zone settings. Local time can be used when the data is loaded + * in real time or when correct Daylight Saving Time transitions are + * required. + * + * @type {Boolean} + * @sample {highcharts} highcharts/time/useutc-true/ True by default + * @sample {highcharts} highcharts/time/useutc-false/ False + * @apioption time.useUTC + * @default true + */ + + /** + * A custom `Date` class for advanced date handling. For example, + * [JDate](https://github.com/tahajahangir/jdate) can be hooked in to + * handle Jalali dates. + * + * @type {Object} + * @since 4.0.4 + * @product highcharts highstock + * @apioption time.Date + */ + + /** + * A callback to return the time zone offset for a given datetime. It + * takes the timestamp in terms of milliseconds since January 1 1970, + * and returns the timezone offset in minutes. This provides a hook + * for drawing time based charts in specific time zones using their + * local DST crossover dates, with the help of external libraries. + * + * @type {Function} + * @see [global.timezoneOffset](#global.timezoneOffset) + * @sample {highcharts|highstock} + * highcharts/time/gettimezoneoffset/ + * Use moment.js to draw Oslo time regardless of browser locale + * @since 4.1.0 + * @product highcharts highstock + * @apioption time.getTimezoneOffset + */ + + /** + * Requires [moment.js](http://momentjs.com/). If the timezone option + * is specified, it creates a default + * [getTimezoneOffset](#time.getTimezoneOffset) function that looks + * up the specified timezone in moment.js. If moment.js is not included, + * this throws a Highcharts error in the console, but does not crash the + * chart. + * + * @type {String} + * @see [getTimezoneOffset](#time.getTimezoneOffset) + * @sample {highcharts|highstock} + * highcharts/time/timezone/ + * Europe/Oslo + * @default undefined + * @since 5.0.7 + * @product highcharts highstock + * @apioption time.timezone + */ + + /** + * The timezone offset in minutes. Positive values are west, negative + * values are east of UTC, as in the ECMAScript + * [getTimezoneOffset](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset) + * method. Use this to display UTC based data in a predefined time zone. + * + * @type {Number} + * @see [time.getTimezoneOffset](#time.getTimezoneOffset) + * @sample {highcharts|highstock} + * highcharts/time/timezoneoffset/ + * Timezone offset + * @default 0 + * @since 3.0.8 + * @product highcharts highstock + * @apioption time.timezoneOffset + */ + defaultOptions: {}, + + /** + * Update the Time object with current options. It is called internally on + * initiating Highcharts, after running `Highcharts.setOptions` and on + * `Chart.update`. + * + * @private + */ + update: function (options) { + var useUTC = pick(options && options.useUTC, true), + time = this; + + this.options = options = merge(true, this.options || {}, options); + + // Allow using a different Date class + this.Date = options.Date || win.Date; + + this.useUTC = useUTC; + this.timezoneOffset = useUTC && options.timezoneOffset; + + /** + * Get the time zone offset based on the current timezone information as + * set in the global options. + * + * @function #getTimezoneOffset + * @memberOf Highcharts.Time + * @param {Number} timestamp + * The JavaScript timestamp to inspect. + * @return {Number} + * The timezone offset in minutes compared to UTC. + */ + this.getTimezoneOffset = this.timezoneOffsetFunction(); + + /* + * The time object has options allowing for variable time zones, meaning + * the axis ticks or series data needs to consider this. + */ + this.variableTimezone = !!( + !useUTC || + options.getTimezoneOffset || + options.timezone + ); + + // UTC time with timezone handling + if (this.variableTimezone || this.timezoneOffset) { + this.get = function (unit, date) { + var realMs = date.getTime(), + ms = realMs - time.getTimezoneOffset(date), + ret; + + date.setTime(ms); // Temporary adjust to timezone + ret = date['getUTC' + unit](); + date.setTime(realMs); // Reset + + return ret; + }; + this.set = function (unit, date, value) { + var ms, offset, newOffset; + + // For lower order time units, just set it directly using local + // time + if ( + H.inArray(unit, ['Milliseconds', 'Seconds', 'Minutes']) !== + -1 + ) { + date['set' + unit](value); + + // Higher order time units need to take the time zone into + // account + } else { + + // Adjust by timezone + offset = time.getTimezoneOffset(date); + ms = date.getTime() - offset; + date.setTime(ms); + + date['setUTC' + unit](value); + newOffset = time.getTimezoneOffset(date); + + ms = date.getTime() + newOffset; + date.setTime(ms); + } + + }; + + // UTC time with no timezone handling + } else if (useUTC) { + this.get = function (unit, date) { + return date['getUTC' + unit](); + }; + this.set = function (unit, date, value) { + return date['setUTC' + unit](value); + }; + + // Local time + } else { + this.get = function (unit, date) { + return date['get' + unit](); + }; + this.set = function (unit, date, value) { + return date['set' + unit](value); + }; + } + + }, + + /** + * Make a time and returns milliseconds. Interprets the inputs as UTC time, + * local time or a specific timezone time depending on the current time + * settings. + * + * @param {Number} year + * The year + * @param {Number} month + * The month. Zero-based, so January is 0. + * @param {Number} date + * The day of the month + * @param {Number} hours + * The hour of the day, 0-23. + * @param {Number} minutes + * The minutes + * @param {Number} seconds + * The seconds + * + * @return {Number} + * The time in milliseconds since January 1st 1970. + */ + makeTime: function (year, month, date, hours, minutes, seconds) { + var d, offset, newOffset; + if (this.useUTC) { + d = this.Date.UTC.apply(0, arguments); + offset = this.getTimezoneOffset(d); + d += offset; + newOffset = this.getTimezoneOffset(d); + + if (offset !== newOffset) { + d += newOffset - offset; + + // A special case for transitioning from summer time to winter time. + // When the clock is set back, the same time is repeated twice, i.e. + // 02:30 am is repeated since the clock is set back from 3 am to + // 2 am. We need to make the same time as local Date does. + } else if ( + offset - 36e5 === this.getTimezoneOffset(d - 36e5) && + !H.isSafari + ) { + d -= 36e5; + } + + } else { + d = new this.Date( + year, + month, + pick(date, 1), + pick(hours, 0), + pick(minutes, 0), + pick(seconds, 0) + ).getTime(); + } + return d; + }, + + /** + * Sets the getTimezoneOffset function. If the `timezone` option is set, a + * default getTimezoneOffset function with that timezone is returned. If + * a `getTimezoneOffset` option is defined, it is returned. If neither are + * specified, the function using the `timezoneOffset` option or 0 offset is + * returned. + * + * @private + * @return {Function} A getTimezoneOffset function + */ + timezoneOffsetFunction: function () { + var time = this, + options = this.options, + moment = win.moment; + + if (!this.useUTC) { + return function (timestamp) { + return new Date(timestamp).getTimezoneOffset() * 60000; + }; + } + + if (options.timezone) { + if (!moment) { + // getTimezoneOffset-function stays undefined because it depends + // on Moment.js + H.error(25); + + } else { + return function (timestamp) { + return -moment.tz( + timestamp, + options.timezone + ).utcOffset() * 60000; + }; + } + } + + // If not timezone is set, look for the getTimezoneOffset callback + if (this.useUTC && options.getTimezoneOffset) { + return function (timestamp) { + return options.getTimezoneOffset(timestamp) * 60000; + }; + } + + // Last, use the `timezoneOffset` option if set + return function () { + return (time.timezoneOffset || 0) * 60000; + }; + }, + + /** + * Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970) + * into a human readable date string. The format is a subset of the formats + * for PHP's [strftime](http://www.php.net/manual/en/function.strftime.php) + * function. Additional formats can be given in the + * {@link Highcharts.dateFormats} hook. + * + * @param {String} format + * The desired format where various time + * representations are prefixed with %. + * @param {Number} timestamp + * The JavaScript timestamp. + * @param {Boolean} [capitalize=false] + * Upper case first letter in the return. + * @returns {String} The formatted date. + */ + dateFormat: function (format, timestamp, capitalize) { + if (!H.defined(timestamp) || isNaN(timestamp)) { + return H.defaultOptions.lang.invalidDate || ''; + } + format = H.pick(format, '%Y-%m-%d %H:%M:%S'); + + var time = this, + date = new this.Date(timestamp), + // get the basic time values + hours = this.get('Hours', date), + day = this.get('Day', date), + dayOfMonth = this.get('Date', date), + month = this.get('Month', date), + fullYear = this.get('FullYear', date), + lang = H.defaultOptions.lang, + langWeekdays = lang.weekdays, + shortWeekdays = lang.shortWeekdays, + pad = H.pad, + + // List all format keys. Custom formats can be added from the + // outside. + replacements = H.extend( + { + + // Day + // Short weekday, like 'Mon' + 'a': shortWeekdays ? + shortWeekdays[day] : + langWeekdays[day].substr(0, 3), + // Long weekday, like 'Monday' + 'A': langWeekdays[day], + // Two digit day of the month, 01 to 31 + 'd': pad(dayOfMonth), + // Day of the month, 1 through 31 + 'e': pad(dayOfMonth, 2, ' '), + 'w': day, + + // Week (none implemented) + // 'W': weekNumber(), + + // Month + // Short month, like 'Jan' + 'b': lang.shortMonths[month], + // Long month, like 'January' + 'B': lang.months[month], + // Two digit month number, 01 through 12 + 'm': pad(month + 1), + + // Year + // Two digits year, like 09 for 2009 + 'y': fullYear.toString().substr(2, 2), + // Four digits year, like 2009 + 'Y': fullYear, + + // Time + // Two digits hours in 24h format, 00 through 23 + 'H': pad(hours), + // Hours in 24h format, 0 through 23 + 'k': hours, + // Two digits hours in 12h format, 00 through 11 + 'I': pad((hours % 12) || 12), + // Hours in 12h format, 1 through 12 + 'l': (hours % 12) || 12, + // Two digits minutes, 00 through 59 + 'M': pad(time.get('Minutes', date)), + // Upper case AM or PM + 'p': hours < 12 ? 'AM' : 'PM', + // Lower case AM or PM + 'P': hours < 12 ? 'am' : 'pm', + // Two digits seconds, 00 through 59 + 'S': pad(date.getSeconds()), + // Milliseconds (naming from Ruby) + 'L': pad(Math.round(timestamp % 1000), 3) + }, + + /** + * A hook for defining additional date format specifiers. New + * specifiers are defined as key-value pairs by using the + * specifier as key, and a function which takes the timestamp as + * value. This function returns the formatted portion of the + * date. + * + * @type {Object} + * @name dateFormats + * @memberOf Highcharts + * @sample highcharts/global/dateformats/ + * Adding support for week + * number + */ + H.dateFormats + ); + + + // Do the replaces + H.objectEach(replacements, function (val, key) { + // Regex would do it in one line, but this is faster + while (format.indexOf('%' + key) !== -1) { + format = format.replace( + '%' + key, + typeof val === 'function' ? val.call(time, timestamp) : val + ); + } + + }); + + // Optionally capitalize the string and return + return capitalize ? + format.substr(0, 1).toUpperCase() + format.substr(1) : + format; + }, + + /** + * Return an array with time positions distributed on round time values + * right and right after min and max. Used in datetime axes as well as for + * grouping data on a datetime axis. + * + * @param {Object} normalizedInterval + * The interval in axis values (ms) and thecount + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ + getTimeTicks: function ( + normalizedInterval, + min, + max, + startOfWeek + ) { + var time = this, + Date = time.Date, + tickPositions = [], + i, + higherRanks = {}, + minYear, // used in months and years as a basis for Date.UTC() + // When crossing DST, use the max. Resolves #6278. + minDate = new Date(min), + interval = normalizedInterval.unitRange, + count = normalizedInterval.count || 1, + variableDayLength; + + if (defined(min)) { // #1300 + time.set( + 'Milliseconds', + minDate, + interval >= timeUnits.second ? + 0 : // #3935 + count * Math.floor( + time.get('Milliseconds', minDate) / count + ) + ); // #3652, #3654 + + if (interval >= timeUnits.second) { // second + time.set('Seconds', + minDate, + interval >= timeUnits.minute ? + 0 : // #3935 + count * Math.floor(time.get('Seconds', minDate) / count) + ); + } + + if (interval >= timeUnits.minute) { // minute + time.set('Minutes', minDate, + interval >= timeUnits.hour ? + 0 : + count * Math.floor(time.get('Minutes', minDate) / count) + ); + } + + if (interval >= timeUnits.hour) { // hour + time.set( + 'Hours', + minDate, + interval >= timeUnits.day ? + 0 : + count * Math.floor( + time.get('Hours', minDate) / count + ) + ); + } + + if (interval >= timeUnits.day) { // day + time.set( + 'Date', + minDate, + interval >= timeUnits.month ? + 1 : + count * Math.floor(time.get('Date', minDate) / count) + ); + } + + if (interval >= timeUnits.month) { // month + time.set( + 'Month', + minDate, + interval >= timeUnits.year ? 0 : + count * Math.floor(time.get('Month', minDate) / count) + ); + minYear = time.get('FullYear', minDate); + } + + if (interval >= timeUnits.year) { // year + minYear -= minYear % count; + time.set('FullYear', minDate, minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval === timeUnits.week) { + // get start of current week, independent of count + time.set( + 'Date', + minDate, + ( + time.get('Date', minDate) - + time.get('Day', minDate) + + pick(startOfWeek, 1) + ) + ); + } + + + // Get basics for variable time spans + minYear = time.get('FullYear', minDate); + var minMonth = time.get('Month', minDate), + minDateDate = time.get('Date', minDate), + minHours = time.get('Hours', minDate); + + // Redefine min to the floored/rounded minimum time (#7432) + min = minDate.getTime(); + + // Handle local timezone offset + if (time.variableTimezone) { + + // Detect whether we need to take the DST crossover into + // consideration. If we're crossing over DST, the day length may + // be 23h or 25h and we need to compute the exact clock time for + // each tick instead of just adding hours. This comes at a cost, + // so first we find out if it is needed (#4951). + variableDayLength = ( + // Long range, assume we're crossing over. + max - min > 4 * timeUnits.month || + // Short range, check if min and max are in different time + // zones. + time.getTimezoneOffset(min) !== time.getTimezoneOffset(max) + ); + } + + // Iterate and add tick positions at appropriate values + var t = minDate.getTime(); + i = 1; + while (t < max) { + tickPositions.push(t); + + // if the interval is years, use Date.UTC to increase years + if (interval === timeUnits.year) { + t = time.makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval === timeUnits.month) { + t = time.makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it + // jumps one hour at the DST crossover + } else if ( + variableDayLength && + (interval === timeUnits.day || interval === timeUnits.week) + ) { + t = time.makeTime( + minYear, + minMonth, + minDateDate + + i * count * (interval === timeUnits.day ? 1 : 7) + ); + + } else if ( + variableDayLength && + interval === timeUnits.hour && + count > 1 + ) { + // make sure higher ranks are preserved across DST (#6797, + // #7621) + t = time.makeTime( + minYear, + minMonth, + minDateDate, + minHours + i * count + ); + + // else, the interval is fixed and we use simple addition + } else { + t += interval * count; + } + + i++; + } + + // push the last time + tickPositions.push(t); + + + // Handle higher ranks. Mark new days if the time is on midnight + // (#950, #1649, #1760, #3349). Use a reasonable dropout threshold + // to prevent looping over dense data grouping (#6156). + if (interval <= timeUnits.hour && tickPositions.length < 10000) { + each(tickPositions, function (t) { + if ( + // Speed optimization, no need to run dateFormat unless + // we're on a full or half hour + t % 1800000 === 0 && + // Check for local or global midnight + time.dateFormat('%H%M%S%L', t) === '000000000' + ) { + higherRanks[t] = 'day'; + } + }); + } + } + + + // record information on the chosen unit - for dynamic label formatter + tickPositions.info = extend(normalizedInterval, { + higherRanks: higherRanks, + totalRange: interval * count + }); + + return tickPositions; + } }; // end of Time diff --git a/js/parts/Tooltip.js b/js/parts/Tooltip.js index a12f19a26ed..b4f52c865af 100644 --- a/js/parts/Tooltip.js +++ b/js/parts/Tooltip.js @@ -7,911 +7,911 @@ import H from './Globals.js'; import './Utilities.js'; var each = H.each, - extend = H.extend, - format = H.format, - isNumber = H.isNumber, - map = H.map, - merge = H.merge, - pick = H.pick, - splat = H.splat, - syncTimeout = H.syncTimeout, - timeUnits = H.timeUnits; + extend = H.extend, + format = H.format, + isNumber = H.isNumber, + map = H.map, + merge = H.merge, + pick = H.pick, + splat = H.splat, + syncTimeout = H.syncTimeout, + timeUnits = H.timeUnits; /** * The tooltip object * @param {Object} chart The chart instance * @param {Object} options Tooltip options */ H.Tooltip = function () { - this.init.apply(this, arguments); + this.init.apply(this, arguments); }; H.Tooltip.prototype = { - init: function (chart, options) { - - // Save the chart and options - this.chart = chart; - this.options = options; - - // List of crosshairs - this.crosshairs = []; - - // Current values of x and y when animating - this.now = { x: 0, y: 0 }; - - // The tooltip is initially hidden - this.isHidden = true; - - - - // Public property for getting the shared state. - this.split = options.split && !chart.inverted; - this.shared = options.shared || this.split; - - }, - - /** - * Destroy the single tooltips in a split tooltip. - * If the tooltip is active then it is not destroyed, unless forced to. - * @param {boolean} force Force destroy all tooltips. - * @return {undefined} - */ - cleanSplit: function (force) { - each(this.chart.series, function (series) { - var tt = series && series.tt; - if (tt) { - if (!tt.isActive || force) { - series.tt = tt.destroy(); - } else { - tt.isActive = false; - } - } - }); - }, - - /*= if (!build.classic) { =*/ - /** - * In styled mode, apply the default filter for the tooltip drop-shadow. It - * needs to have an id specific to the chart, otherwise there will be issues - * when one tooltip adopts the filter of a different chart, specifically one - * where the container is hidden. - */ - applyFilter: function () { - - var chart = this.chart; - chart.renderer.definition({ - tagName: 'filter', - id: 'drop-shadow-' + chart.index, - opacity: 0.5, - children: [{ - tagName: 'feGaussianBlur', - in: 'SourceAlpha', - stdDeviation: 1 - }, { - tagName: 'feOffset', - dx: 1, - dy: 1 - }, { - tagName: 'feComponentTransfer', - children: [{ - tagName: 'feFuncA', - type: 'linear', - slope: 0.3 - }] - }, { - tagName: 'feMerge', - children: [{ - tagName: 'feMergeNode' - }, { - tagName: 'feMergeNode', - in: 'SourceGraphic' - }] - }] - }); - chart.renderer.definition({ - tagName: 'style', - textContent: '.highcharts-tooltip-' + chart.index + '{' + - 'filter:url(#drop-shadow-' + chart.index + ')' + - '}' - }); - }, - /*= } =*/ - - - /** - * Create the Tooltip label element if it doesn't exist, then return the - * label. - */ - getLabel: function () { - - var renderer = this.chart.renderer, - options = this.options; - - if (!this.label) { - // Create the label - if (this.split) { - this.label = renderer.g('tooltip'); - } else { - this.label = renderer.label( - '', - 0, - 0, - options.shape || 'callout', - null, - null, - options.useHTML, - null, - 'tooltip' - ) - .attr({ - padding: options.padding, - r: options.borderRadius - }); - - /*= if (build.classic) { =*/ - this.label - .attr({ - 'fill': options.backgroundColor, - 'stroke-width': options.borderWidth - }) - // #2301, #2657 - .css(options.style) - .shadow(options.shadow); - /*= } =*/ - } - - /*= if (!build.classic) { =*/ - // Apply the drop-shadow filter - this.applyFilter(); - this.label.addClass('highcharts-tooltip-' + this.chart.index); - /*= } =*/ - - this.label - .attr({ - zIndex: 8 - }) - .add(); - } - return this.label; - }, - - update: function (options) { - this.destroy(); - // Update user options (#6218) - merge(true, this.chart.options.tooltip.userOptions, options); - this.init(this.chart, merge(true, this.options, options)); - }, - - /** - * Destroy the tooltip and its elements. - */ - destroy: function () { - // Destroy and clear local variables - if (this.label) { - this.label = this.label.destroy(); - } - if (this.split && this.tt) { - this.cleanSplit(this.chart, true); - this.tt = this.tt.destroy(); - } - H.clearTimeout(this.hideTimer); - H.clearTimeout(this.tooltipTimeout); - }, - - /** - * Provide a soft movement for the tooltip - * - * @param {Number} x - * @param {Number} y - * @private - */ - move: function (x, y, anchorX, anchorY) { - var tooltip = this, - now = tooltip.now, - animate = tooltip.options.animation !== false && - !tooltip.isHidden && - // When we get close to the target position, abort animation and - // land on the right place (#3056) - (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1), - skipAnchor = tooltip.followPointer || tooltip.len > 1; - - // Get intermediate values for animation - extend(now, { - x: animate ? (2 * now.x + x) / 3 : x, - y: animate ? (now.y + y) / 2 : y, - anchorX: skipAnchor ? - undefined : - animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, - anchorY: skipAnchor ? - undefined : - animate ? (now.anchorY + anchorY) / 2 : anchorY - }); - - // Move to the intermediate value - tooltip.getLabel().attr(now); - - - // Run on next tick of the mouse tracker - if (animate) { - - // Never allow two timeouts - H.clearTimeout(this.tooltipTimeout); - - // Set the fixed interval ticking for the smooth tooltip - this.tooltipTimeout = setTimeout(function () { - // The interval function may still be running during destroy, - // so check that the chart is really there before calling. - if (tooltip) { - tooltip.move(x, y, anchorX, anchorY); - } - }, 32); - - } - }, - - /** - * Hide the tooltip - */ - hide: function (delay) { - var tooltip = this; - // disallow duplicate timers (#1728, #1766) - H.clearTimeout(this.hideTimer); - delay = pick(delay, this.options.hideDelay, 500); - if (!this.isHidden) { - this.hideTimer = syncTimeout(function () { - tooltip.getLabel()[delay ? 'fadeOut' : 'hide'](); - tooltip.isHidden = true; - }, delay); - } - }, - - /** - * Extendable method to get the anchor position of the tooltip - * from a point or set of points - */ - getAnchor: function (points, mouseEvent) { - var ret, - chart = this.chart, - inverted = chart.inverted, - plotTop = chart.plotTop, - plotLeft = chart.plotLeft, - plotX = 0, - plotY = 0, - yAxis, - xAxis; - - points = splat(points); - - // Pie uses a special tooltipPos - ret = points[0].tooltipPos; - - // When tooltip follows mouse, relate the position to the mouse - if (this.followPointer && mouseEvent) { - if (mouseEvent.chartX === undefined) { - mouseEvent = chart.pointer.normalize(mouseEvent); - } - ret = [ - mouseEvent.chartX - chart.plotLeft, - mouseEvent.chartY - plotTop - ]; - } - // When shared, use the average position - if (!ret) { - each(points, function (point) { - yAxis = point.series.yAxis; - xAxis = point.series.xAxis; - plotX += point.plotX + - (!inverted && xAxis ? xAxis.left - plotLeft : 0); - plotY += - ( - point.plotLow ? - (point.plotLow + point.plotHigh) / 2 : - point.plotY - ) + - (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 - }); - - plotX /= points.length; - plotY /= points.length; - - ret = [ - inverted ? chart.plotWidth - plotY : plotX, - this.shared && !inverted && points.length > 1 && mouseEvent ? - // place shared tooltip next to the mouse (#424) - mouseEvent.chartY - plotTop : - inverted ? chart.plotHeight - plotX : plotY - ]; - } - - return map(ret, Math.round); - }, - - /** - * Place the tooltip in a chart without spilling over - * and not covering the point it self. - */ - getPosition: function (boxWidth, boxHeight, point) { - - var chart = this.chart, - distance = this.distance, - ret = {}, - // Don't use h if chart isn't inverted (#7242) - h = (chart.inverted && point.h) || 0, // #4117 - swapped, - first = ['y', chart.chartHeight, boxHeight, - point.plotY + chart.plotTop, chart.plotTop, - chart.plotTop + chart.plotHeight], - second = ['x', chart.chartWidth, boxWidth, - point.plotX + chart.plotLeft, chart.plotLeft, - chart.plotLeft + chart.plotWidth], - // The far side is right or bottom - preferFarSide = !this.followPointer && pick( - point.ttBelow, - !chart.inverted === !!point.negative - ), // #4984 - - /** - * Handle the preferred dimension. When the preferred dimension is - * tooltip on top or bottom of the point, it will look for space - * there. - */ - firstDimension = function ( - dim, - outerSize, - innerSize, - point, - min, - max - ) { - var roomLeft = innerSize < point - distance, - roomRight = point + distance + innerSize < outerSize, - alignedLeft = point - distance - innerSize, - alignedRight = point + distance; - - if (preferFarSide && roomRight) { - ret[dim] = alignedRight; - } else if (!preferFarSide && roomLeft) { - ret[dim] = alignedLeft; - } else if (roomLeft) { - ret[dim] = Math.min( - max - innerSize, - alignedLeft - h < 0 ? alignedLeft : alignedLeft - h - ); - } else if (roomRight) { - ret[dim] = Math.max( - min, - alignedRight + h + innerSize > outerSize ? - alignedRight : - alignedRight + h - ); - } else { - return false; - } - }, - /** - * Handle the secondary dimension. If the preferred dimension is - * tooltip on top or bottom of the point, the second dimension is to - * align the tooltip above the point, trying to align center but - * allowing left or right align within the chart box. - */ - secondDimension = function (dim, outerSize, innerSize, point) { - var retVal; - - // Too close to the edge, return false and swap dimensions - if (point < distance || point > outerSize - distance) { - retVal = false; - // Align left/top - } else if (point < innerSize / 2) { - ret[dim] = 1; - // Align right/bottom - } else if (point > outerSize - innerSize / 2) { - ret[dim] = outerSize - innerSize - 2; - // Align center - } else { - ret[dim] = point - innerSize / 2; - } - return retVal; - }, - /** - * Swap the dimensions - */ - swap = function (count) { - var temp = first; - first = second; - second = temp; - swapped = count; - }, - run = function () { - if (firstDimension.apply(0, first) !== false) { - if ( - secondDimension.apply(0, second) === false && - !swapped - ) { - swap(true); - run(); - } - } else if (!swapped) { - swap(true); - run(); - } else { - ret.x = ret.y = 0; - } - }; - - // Under these conditions, prefer the tooltip on the side of the point - if (chart.inverted || this.len > 1) { - swap(); - } - run(); - - return ret; - - }, - - /** - * In case no user defined formatter is given, this will be used. Note that - * the context here is an object holding point, series, x, y etc. - * - * @returns {String|Array} - */ - defaultFormatter: function (tooltip) { - var items = this.points || splat(this), - s; - - // Build the header - s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; - - // build the values - s = s.concat(tooltip.bodyFormatter(items)); - - // footer - s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); - - return s; - }, - - /** - * Refresh the tooltip's text and position. - * @param {Object|Array} pointOrPoints Rither a point or an array of points - */ - refresh: function (pointOrPoints, mouseEvent) { - var tooltip = this, - label, - options = tooltip.options, - x, - y, - point = pointOrPoints, - anchor, - textConfig = {}, - text, - pointConfig = [], - formatter = options.formatter || tooltip.defaultFormatter, - shared = tooltip.shared, - currentSeries; - - if (!options.enabled) { - return; - } - - H.clearTimeout(this.hideTimer); - - // get the reference point coordinates (pie charts use tooltipPos) - tooltip.followPointer = splat(point)[0].series.tooltipOptions - .followPointer; - anchor = tooltip.getAnchor(point, mouseEvent); - x = anchor[0]; - y = anchor[1]; - - // shared tooltip, array is sent over - if (shared && !(point.series && point.series.noSharedTooltip)) { - each(point, function (item) { - item.setState('hover'); - - pointConfig.push(item.getLabelConfig()); - }); - - textConfig = { - x: point[0].category, - y: point[0].y - }; - textConfig.points = pointConfig; - point = point[0]; - - // single point tooltip - } else { - textConfig = point.getLabelConfig(); - } - this.len = pointConfig.length; // #6128 - text = formatter.call(textConfig, tooltip); - - // register the current series - currentSeries = point.series; - this.distance = pick(currentSeries.tooltipOptions.distance, 16); - - // update the inner HTML - if (text === false) { - this.hide(); - } else { - - label = tooltip.getLabel(); - - // show it - if (tooltip.isHidden) { - label.attr({ - opacity: 1 - }).show(); - } - - // update text - if (tooltip.split) { - this.renderSplit(text, splat(pointOrPoints)); - } else { - - // Prevent the tooltip from flowing over the chart box (#6659) - /*= if (build.classic) { =*/ - if (!options.style.width) { - /*= } =*/ - label.css({ - width: this.chart.spacingBox.width - }); - /*= if (build.classic) { =*/ - } - /*= } =*/ - - label.attr({ - text: text && text.join ? text.join('') : text - }); - - // Set the stroke color of the box to reflect the point - label.removeClass(/highcharts-color-[\d]+/g) - .addClass( - 'highcharts-color-' + - pick(point.colorIndex, currentSeries.colorIndex) - ); - - /*= if (build.classic) { =*/ - label.attr({ - stroke: ( - options.borderColor || - point.color || - currentSeries.color || - '${palette.neutralColor60}' - ) - }); - /*= } =*/ - - tooltip.updatePosition({ - plotX: x, - plotY: y, - negative: point.negative, - ttBelow: point.ttBelow, - h: anchor[2] || 0 - }); - } - - this.isHidden = false; - } - }, - - /** - * Render the split tooltip. Loops over each point's text and adds - * a label next to the point, then uses the distribute function to - * find best non-overlapping positions. - */ - renderSplit: function (labels, points) { - var tooltip = this, - boxes = [], - chart = this.chart, - ren = chart.renderer, - rightAligned = true, - options = this.options, - headerHeight = 0, - tooltipLabel = this.getLabel(); - - // Graceful degradation for legacy formatters - if (H.isString(labels)) { - labels = [false, labels]; - } - // Create the individual labels for header and points, ignore footer - each(labels.slice(0, points.length + 1), function (str, i) { - if (str !== false) { - var point = points[i - 1] || - // Item 0 is the header. Instead of this, we could also - // use the crosshair label - { isHeader: true, plotX: points[0].plotX }, - owner = point.series || tooltip, - tt = owner.tt, - series = point.series || {}, - colorClass = 'highcharts-color-' + pick( - point.colorIndex, - series.colorIndex, - 'none' - ), - target, - x, - bBox, - boxWidth; - - // Store the tooltip referance on the series - if (!tt) { - owner.tt = tt = ren.label( - null, - null, - null, - 'callout', - null, - null, - options.useHTML - ) - .addClass('highcharts-tooltip-box ' + colorClass) - .attr({ - 'padding': options.padding, - 'r': options.borderRadius, - /*= if (build.classic) { =*/ - 'fill': options.backgroundColor, - 'stroke': ( - options.borderColor || - point.color || - series.color || - '${palette.neutralColor80}' - ), - 'stroke-width': options.borderWidth - /*= } =*/ - }) - .add(tooltipLabel); - } - - tt.isActive = true; - tt.attr({ - text: str - }); - /*= if (build.classic) { =*/ - tt.css(options.style) - .shadow(options.shadow); - /*= } =*/ - - // Get X position now, so we can move all to the other side in - // case of overflow - bBox = tt.getBBox(); - boxWidth = bBox.width + tt.strokeWidth(); - if (point.isHeader) { - headerHeight = bBox.height; - x = Math.max( - 0, // No left overflow - Math.min( - point.plotX + chart.plotLeft - boxWidth / 2, - // No right overflow (#5794) - chart.chartWidth - boxWidth - ) - ); - } else { - x = point.plotX + chart.plotLeft - - pick(options.distance, 16) - boxWidth; - } - - - // If overflow left, we don't use this x in the next loop - if (x < 0) { - rightAligned = false; - } - - // Prepare for distribution - target = (point.series && point.series.yAxis && - point.series.yAxis.pos) + (point.plotY || 0); - target -= chart.plotTop; - boxes.push({ - target: point.isHeader ? - chart.plotHeight + headerHeight : - target, - rank: point.isHeader ? 1 : 0, - size: owner.tt.getBBox().height + 1, - point: point, - x: x, - tt: tt - }); - } - }); - - // Clean previous run (for missing points) - this.cleanSplit(); - - // Distribute and put in place - H.distribute(boxes, chart.plotHeight + headerHeight); - each(boxes, function (box) { - var point = box.point, - series = point.series; - - // Put the label in place - box.tt.attr({ - visibility: box.pos === undefined ? 'hidden' : 'inherit', - x: (rightAligned || point.isHeader ? - box.x : - point.plotX + chart.plotLeft + pick(options.distance, 16)), - y: box.pos + chart.plotTop, - anchorX: point.isHeader ? - point.plotX + chart.plotLeft : - point.plotX + series.xAxis.pos, - anchorY: point.isHeader ? - box.pos + chart.plotTop - 15 : - point.plotY + series.yAxis.pos - }); - }); - }, - - /** - * Find the new position and perform the move - */ - updatePosition: function (point) { - var chart = this.chart, - label = this.getLabel(), - pos = (this.options.positioner || this.getPosition).call( - this, - label.width, - label.height, - point - ); - - // do the move - this.move( - Math.round(pos.x), - Math.round(pos.y || 0), // can be undefined (#3977) - point.plotX + chart.plotLeft, - point.plotY + chart.plotTop - ); - }, - - /** - * Get the optimal date format for a point, based on a range. - * @param {number} range - The time range - * @param {number|Date} date - The date of the point in question - * @param {number} startOfWeek - An integer representing the first day of - * the week, where 0 is Sunday - * @param {Object} dateTimeLabelFormats - A map of time units to formats - * @return {string} - the optimal date format for a point - */ - getDateFormat: function (range, date, startOfWeek, dateTimeLabelFormats) { - var time = this.chart.time, - dateStr = time.dateFormat('%m-%d %H:%M:%S.%L', date), - format, - n, - blank = '01-01 00:00:00.000', - strpos = { - millisecond: 15, - second: 12, - minute: 9, - hour: 6, - day: 3 - }, - lastN = 'millisecond'; // for sub-millisecond data, #4223 - for (n in timeUnits) { - - // If the range is exactly one week and we're looking at a - // Sunday/Monday, go for the week format - if ( - range === timeUnits.week && - +time.dateFormat('%w', date) === startOfWeek && - dateStr.substr(6) === blank.substr(6) - ) { - n = 'week'; - break; - } - - // The first format that is too great for the range - if (timeUnits[n] > range) { - n = lastN; - break; - } - - // If the point is placed every day at 23:59, we need to show - // the minutes as well. #2637. - if ( - strpos[n] && - dateStr.substr(strpos[n]) !== blank.substr(strpos[n]) - ) { - break; - } - - // Weeks are outside the hierarchy, only apply them on - // Mondays/Sundays like in the first condition - if (n !== 'week') { - lastN = n; - } - } - - if (n) { - format = dateTimeLabelFormats[n]; - } - - return format; - }, - - /** - * Get the best X date format based on the closest point range on the axis. - */ - getXDateFormat: function (point, options, xAxis) { - var xDateFormat, - dateTimeLabelFormats = options.dateTimeLabelFormats, - closestPointRange = xAxis && xAxis.closestPointRange; - - if (closestPointRange) { - xDateFormat = this.getDateFormat( - closestPointRange, - point.x, - xAxis.options.startOfWeek, - dateTimeLabelFormats - ); - } else { - xDateFormat = dateTimeLabelFormats.day; - } - - return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 - }, - - /** - * Format the footer/header of the tooltip - * #3397: abstraction to enable formatting of footer and header - */ - tooltipFooterHeaderFormatter: function (labelConfig, isFooter) { - var footOrHead = isFooter ? 'footer' : 'header', - series = labelConfig.series, - tooltipOptions = series.tooltipOptions, - xDateFormat = tooltipOptions.xDateFormat, - xAxis = series.xAxis, - isDateTime = ( - xAxis && - xAxis.options.type === 'datetime' && - isNumber(labelConfig.key) - ), - formatString = tooltipOptions[footOrHead + 'Format']; - - // Guess the best date format based on the closest point distance (#568, - // #3418) - if (isDateTime && !xDateFormat) { - xDateFormat = this.getXDateFormat( - labelConfig, - tooltipOptions, - xAxis - ); - } - - // Insert the footer date format if any - if (isDateTime && xDateFormat) { - each( - (labelConfig.point && labelConfig.point.tooltipDateKeys) || - ['key'], - function (key) { - formatString = formatString.replace( - '{point.' + key + '}', - '{point.' + key + ':' + xDateFormat + '}' - ); - } - ); - } - - return format(formatString, { - point: labelConfig, - series: series - }, this.chart.time); - }, - - /** - * Build the body (lines) of the tooltip by iterating over the items and - * returning one entry for each item, abstracting this functionality allows - * to easily overwrite and extend it. - */ - bodyFormatter: function (items) { - return map(items, function (item) { - var tooltipOptions = item.series.tooltipOptions; - return ( - tooltipOptions[ - (item.point.formatPrefix || 'point') + 'Formatter' - ] || - item.point.tooltipFormatter - ).call( - item.point, - tooltipOptions[(item.point.formatPrefix || 'point') + 'Format'] - ); - }); - } + init: function (chart, options) { + + // Save the chart and options + this.chart = chart; + this.options = options; + + // List of crosshairs + this.crosshairs = []; + + // Current values of x and y when animating + this.now = { x: 0, y: 0 }; + + // The tooltip is initially hidden + this.isHidden = true; + + + + // Public property for getting the shared state. + this.split = options.split && !chart.inverted; + this.shared = options.shared || this.split; + + }, + + /** + * Destroy the single tooltips in a split tooltip. + * If the tooltip is active then it is not destroyed, unless forced to. + * @param {boolean} force Force destroy all tooltips. + * @return {undefined} + */ + cleanSplit: function (force) { + each(this.chart.series, function (series) { + var tt = series && series.tt; + if (tt) { + if (!tt.isActive || force) { + series.tt = tt.destroy(); + } else { + tt.isActive = false; + } + } + }); + }, + + /*= if (!build.classic) { =*/ + /** + * In styled mode, apply the default filter for the tooltip drop-shadow. It + * needs to have an id specific to the chart, otherwise there will be issues + * when one tooltip adopts the filter of a different chart, specifically one + * where the container is hidden. + */ + applyFilter: function () { + + var chart = this.chart; + chart.renderer.definition({ + tagName: 'filter', + id: 'drop-shadow-' + chart.index, + opacity: 0.5, + children: [{ + tagName: 'feGaussianBlur', + in: 'SourceAlpha', + stdDeviation: 1 + }, { + tagName: 'feOffset', + dx: 1, + dy: 1 + }, { + tagName: 'feComponentTransfer', + children: [{ + tagName: 'feFuncA', + type: 'linear', + slope: 0.3 + }] + }, { + tagName: 'feMerge', + children: [{ + tagName: 'feMergeNode' + }, { + tagName: 'feMergeNode', + in: 'SourceGraphic' + }] + }] + }); + chart.renderer.definition({ + tagName: 'style', + textContent: '.highcharts-tooltip-' + chart.index + '{' + + 'filter:url(#drop-shadow-' + chart.index + ')' + + '}' + }); + }, + /*= } =*/ + + + /** + * Create the Tooltip label element if it doesn't exist, then return the + * label. + */ + getLabel: function () { + + var renderer = this.chart.renderer, + options = this.options; + + if (!this.label) { + // Create the label + if (this.split) { + this.label = renderer.g('tooltip'); + } else { + this.label = renderer.label( + '', + 0, + 0, + options.shape || 'callout', + null, + null, + options.useHTML, + null, + 'tooltip' + ) + .attr({ + padding: options.padding, + r: options.borderRadius + }); + + /*= if (build.classic) { =*/ + this.label + .attr({ + 'fill': options.backgroundColor, + 'stroke-width': options.borderWidth + }) + // #2301, #2657 + .css(options.style) + .shadow(options.shadow); + /*= } =*/ + } + + /*= if (!build.classic) { =*/ + // Apply the drop-shadow filter + this.applyFilter(); + this.label.addClass('highcharts-tooltip-' + this.chart.index); + /*= } =*/ + + this.label + .attr({ + zIndex: 8 + }) + .add(); + } + return this.label; + }, + + update: function (options) { + this.destroy(); + // Update user options (#6218) + merge(true, this.chart.options.tooltip.userOptions, options); + this.init(this.chart, merge(true, this.options, options)); + }, + + /** + * Destroy the tooltip and its elements. + */ + destroy: function () { + // Destroy and clear local variables + if (this.label) { + this.label = this.label.destroy(); + } + if (this.split && this.tt) { + this.cleanSplit(this.chart, true); + this.tt = this.tt.destroy(); + } + H.clearTimeout(this.hideTimer); + H.clearTimeout(this.tooltipTimeout); + }, + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} x + * @param {Number} y + * @private + */ + move: function (x, y, anchorX, anchorY) { + var tooltip = this, + now = tooltip.now, + animate = tooltip.options.animation !== false && + !tooltip.isHidden && + // When we get close to the target position, abort animation and + // land on the right place (#3056) + (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1), + skipAnchor = tooltip.followPointer || tooltip.len > 1; + + // Get intermediate values for animation + extend(now, { + x: animate ? (2 * now.x + x) / 3 : x, + y: animate ? (now.y + y) / 2 : y, + anchorX: skipAnchor ? + undefined : + animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, + anchorY: skipAnchor ? + undefined : + animate ? (now.anchorY + anchorY) / 2 : anchorY + }); + + // Move to the intermediate value + tooltip.getLabel().attr(now); + + + // Run on next tick of the mouse tracker + if (animate) { + + // Never allow two timeouts + H.clearTimeout(this.tooltipTimeout); + + // Set the fixed interval ticking for the smooth tooltip + this.tooltipTimeout = setTimeout(function () { + // The interval function may still be running during destroy, + // so check that the chart is really there before calling. + if (tooltip) { + tooltip.move(x, y, anchorX, anchorY); + } + }, 32); + + } + }, + + /** + * Hide the tooltip + */ + hide: function (delay) { + var tooltip = this; + // disallow duplicate timers (#1728, #1766) + H.clearTimeout(this.hideTimer); + delay = pick(delay, this.options.hideDelay, 500); + if (!this.isHidden) { + this.hideTimer = syncTimeout(function () { + tooltip.getLabel()[delay ? 'fadeOut' : 'hide'](); + tooltip.isHidden = true; + }, delay); + } + }, + + /** + * Extendable method to get the anchor position of the tooltip + * from a point or set of points + */ + getAnchor: function (points, mouseEvent) { + var ret, + chart = this.chart, + inverted = chart.inverted, + plotTop = chart.plotTop, + plotLeft = chart.plotLeft, + plotX = 0, + plotY = 0, + yAxis, + xAxis; + + points = splat(points); + + // Pie uses a special tooltipPos + ret = points[0].tooltipPos; + + // When tooltip follows mouse, relate the position to the mouse + if (this.followPointer && mouseEvent) { + if (mouseEvent.chartX === undefined) { + mouseEvent = chart.pointer.normalize(mouseEvent); + } + ret = [ + mouseEvent.chartX - chart.plotLeft, + mouseEvent.chartY - plotTop + ]; + } + // When shared, use the average position + if (!ret) { + each(points, function (point) { + yAxis = point.series.yAxis; + xAxis = point.series.xAxis; + plotX += point.plotX + + (!inverted && xAxis ? xAxis.left - plotLeft : 0); + plotY += + ( + point.plotLow ? + (point.plotLow + point.plotHigh) / 2 : + point.plotY + ) + + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 + }); + + plotX /= points.length; + plotY /= points.length; + + ret = [ + inverted ? chart.plotWidth - plotY : plotX, + this.shared && !inverted && points.length > 1 && mouseEvent ? + // place shared tooltip next to the mouse (#424) + mouseEvent.chartY - plotTop : + inverted ? chart.plotHeight - plotX : plotY + ]; + } + + return map(ret, Math.round); + }, + + /** + * Place the tooltip in a chart without spilling over + * and not covering the point it self. + */ + getPosition: function (boxWidth, boxHeight, point) { + + var chart = this.chart, + distance = this.distance, + ret = {}, + // Don't use h if chart isn't inverted (#7242) + h = (chart.inverted && point.h) || 0, // #4117 + swapped, + first = ['y', chart.chartHeight, boxHeight, + point.plotY + chart.plotTop, chart.plotTop, + chart.plotTop + chart.plotHeight], + second = ['x', chart.chartWidth, boxWidth, + point.plotX + chart.plotLeft, chart.plotLeft, + chart.plotLeft + chart.plotWidth], + // The far side is right or bottom + preferFarSide = !this.followPointer && pick( + point.ttBelow, + !chart.inverted === !!point.negative + ), // #4984 + + /** + * Handle the preferred dimension. When the preferred dimension is + * tooltip on top or bottom of the point, it will look for space + * there. + */ + firstDimension = function ( + dim, + outerSize, + innerSize, + point, + min, + max + ) { + var roomLeft = innerSize < point - distance, + roomRight = point + distance + innerSize < outerSize, + alignedLeft = point - distance - innerSize, + alignedRight = point + distance; + + if (preferFarSide && roomRight) { + ret[dim] = alignedRight; + } else if (!preferFarSide && roomLeft) { + ret[dim] = alignedLeft; + } else if (roomLeft) { + ret[dim] = Math.min( + max - innerSize, + alignedLeft - h < 0 ? alignedLeft : alignedLeft - h + ); + } else if (roomRight) { + ret[dim] = Math.max( + min, + alignedRight + h + innerSize > outerSize ? + alignedRight : + alignedRight + h + ); + } else { + return false; + } + }, + /** + * Handle the secondary dimension. If the preferred dimension is + * tooltip on top or bottom of the point, the second dimension is to + * align the tooltip above the point, trying to align center but + * allowing left or right align within the chart box. + */ + secondDimension = function (dim, outerSize, innerSize, point) { + var retVal; + + // Too close to the edge, return false and swap dimensions + if (point < distance || point > outerSize - distance) { + retVal = false; + // Align left/top + } else if (point < innerSize / 2) { + ret[dim] = 1; + // Align right/bottom + } else if (point > outerSize - innerSize / 2) { + ret[dim] = outerSize - innerSize - 2; + // Align center + } else { + ret[dim] = point - innerSize / 2; + } + return retVal; + }, + /** + * Swap the dimensions + */ + swap = function (count) { + var temp = first; + first = second; + second = temp; + swapped = count; + }, + run = function () { + if (firstDimension.apply(0, first) !== false) { + if ( + secondDimension.apply(0, second) === false && + !swapped + ) { + swap(true); + run(); + } + } else if (!swapped) { + swap(true); + run(); + } else { + ret.x = ret.y = 0; + } + }; + + // Under these conditions, prefer the tooltip on the side of the point + if (chart.inverted || this.len > 1) { + swap(); + } + run(); + + return ret; + + }, + + /** + * In case no user defined formatter is given, this will be used. Note that + * the context here is an object holding point, series, x, y etc. + * + * @returns {String|Array} + */ + defaultFormatter: function (tooltip) { + var items = this.points || splat(this), + s; + + // Build the header + s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; + + // build the values + s = s.concat(tooltip.bodyFormatter(items)); + + // footer + s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); + + return s; + }, + + /** + * Refresh the tooltip's text and position. + * @param {Object|Array} pointOrPoints Rither a point or an array of points + */ + refresh: function (pointOrPoints, mouseEvent) { + var tooltip = this, + label, + options = tooltip.options, + x, + y, + point = pointOrPoints, + anchor, + textConfig = {}, + text, + pointConfig = [], + formatter = options.formatter || tooltip.defaultFormatter, + shared = tooltip.shared, + currentSeries; + + if (!options.enabled) { + return; + } + + H.clearTimeout(this.hideTimer); + + // get the reference point coordinates (pie charts use tooltipPos) + tooltip.followPointer = splat(point)[0].series.tooltipOptions + .followPointer; + anchor = tooltip.getAnchor(point, mouseEvent); + x = anchor[0]; + y = anchor[1]; + + // shared tooltip, array is sent over + if (shared && !(point.series && point.series.noSharedTooltip)) { + each(point, function (item) { + item.setState('hover'); + + pointConfig.push(item.getLabelConfig()); + }); + + textConfig = { + x: point[0].category, + y: point[0].y + }; + textConfig.points = pointConfig; + point = point[0]; + + // single point tooltip + } else { + textConfig = point.getLabelConfig(); + } + this.len = pointConfig.length; // #6128 + text = formatter.call(textConfig, tooltip); + + // register the current series + currentSeries = point.series; + this.distance = pick(currentSeries.tooltipOptions.distance, 16); + + // update the inner HTML + if (text === false) { + this.hide(); + } else { + + label = tooltip.getLabel(); + + // show it + if (tooltip.isHidden) { + label.attr({ + opacity: 1 + }).show(); + } + + // update text + if (tooltip.split) { + this.renderSplit(text, splat(pointOrPoints)); + } else { + + // Prevent the tooltip from flowing over the chart box (#6659) + /*= if (build.classic) { =*/ + if (!options.style.width) { + /*= } =*/ + label.css({ + width: this.chart.spacingBox.width + }); + /*= if (build.classic) { =*/ + } + /*= } =*/ + + label.attr({ + text: text && text.join ? text.join('') : text + }); + + // Set the stroke color of the box to reflect the point + label.removeClass(/highcharts-color-[\d]+/g) + .addClass( + 'highcharts-color-' + + pick(point.colorIndex, currentSeries.colorIndex) + ); + + /*= if (build.classic) { =*/ + label.attr({ + stroke: ( + options.borderColor || + point.color || + currentSeries.color || + '${palette.neutralColor60}' + ) + }); + /*= } =*/ + + tooltip.updatePosition({ + plotX: x, + plotY: y, + negative: point.negative, + ttBelow: point.ttBelow, + h: anchor[2] || 0 + }); + } + + this.isHidden = false; + } + }, + + /** + * Render the split tooltip. Loops over each point's text and adds + * a label next to the point, then uses the distribute function to + * find best non-overlapping positions. + */ + renderSplit: function (labels, points) { + var tooltip = this, + boxes = [], + chart = this.chart, + ren = chart.renderer, + rightAligned = true, + options = this.options, + headerHeight = 0, + tooltipLabel = this.getLabel(); + + // Graceful degradation for legacy formatters + if (H.isString(labels)) { + labels = [false, labels]; + } + // Create the individual labels for header and points, ignore footer + each(labels.slice(0, points.length + 1), function (str, i) { + if (str !== false) { + var point = points[i - 1] || + // Item 0 is the header. Instead of this, we could also + // use the crosshair label + { isHeader: true, plotX: points[0].plotX }, + owner = point.series || tooltip, + tt = owner.tt, + series = point.series || {}, + colorClass = 'highcharts-color-' + pick( + point.colorIndex, + series.colorIndex, + 'none' + ), + target, + x, + bBox, + boxWidth; + + // Store the tooltip referance on the series + if (!tt) { + owner.tt = tt = ren.label( + null, + null, + null, + 'callout', + null, + null, + options.useHTML + ) + .addClass('highcharts-tooltip-box ' + colorClass) + .attr({ + 'padding': options.padding, + 'r': options.borderRadius, + /*= if (build.classic) { =*/ + 'fill': options.backgroundColor, + 'stroke': ( + options.borderColor || + point.color || + series.color || + '${palette.neutralColor80}' + ), + 'stroke-width': options.borderWidth + /*= } =*/ + }) + .add(tooltipLabel); + } + + tt.isActive = true; + tt.attr({ + text: str + }); + /*= if (build.classic) { =*/ + tt.css(options.style) + .shadow(options.shadow); + /*= } =*/ + + // Get X position now, so we can move all to the other side in + // case of overflow + bBox = tt.getBBox(); + boxWidth = bBox.width + tt.strokeWidth(); + if (point.isHeader) { + headerHeight = bBox.height; + x = Math.max( + 0, // No left overflow + Math.min( + point.plotX + chart.plotLeft - boxWidth / 2, + // No right overflow (#5794) + chart.chartWidth - boxWidth + ) + ); + } else { + x = point.plotX + chart.plotLeft - + pick(options.distance, 16) - boxWidth; + } + + + // If overflow left, we don't use this x in the next loop + if (x < 0) { + rightAligned = false; + } + + // Prepare for distribution + target = (point.series && point.series.yAxis && + point.series.yAxis.pos) + (point.plotY || 0); + target -= chart.plotTop; + boxes.push({ + target: point.isHeader ? + chart.plotHeight + headerHeight : + target, + rank: point.isHeader ? 1 : 0, + size: owner.tt.getBBox().height + 1, + point: point, + x: x, + tt: tt + }); + } + }); + + // Clean previous run (for missing points) + this.cleanSplit(); + + // Distribute and put in place + H.distribute(boxes, chart.plotHeight + headerHeight); + each(boxes, function (box) { + var point = box.point, + series = point.series; + + // Put the label in place + box.tt.attr({ + visibility: box.pos === undefined ? 'hidden' : 'inherit', + x: (rightAligned || point.isHeader ? + box.x : + point.plotX + chart.plotLeft + pick(options.distance, 16)), + y: box.pos + chart.plotTop, + anchorX: point.isHeader ? + point.plotX + chart.plotLeft : + point.plotX + series.xAxis.pos, + anchorY: point.isHeader ? + box.pos + chart.plotTop - 15 : + point.plotY + series.yAxis.pos + }); + }); + }, + + /** + * Find the new position and perform the move + */ + updatePosition: function (point) { + var chart = this.chart, + label = this.getLabel(), + pos = (this.options.positioner || this.getPosition).call( + this, + label.width, + label.height, + point + ); + + // do the move + this.move( + Math.round(pos.x), + Math.round(pos.y || 0), // can be undefined (#3977) + point.plotX + chart.plotLeft, + point.plotY + chart.plotTop + ); + }, + + /** + * Get the optimal date format for a point, based on a range. + * @param {number} range - The time range + * @param {number|Date} date - The date of the point in question + * @param {number} startOfWeek - An integer representing the first day of + * the week, where 0 is Sunday + * @param {Object} dateTimeLabelFormats - A map of time units to formats + * @return {string} - the optimal date format for a point + */ + getDateFormat: function (range, date, startOfWeek, dateTimeLabelFormats) { + var time = this.chart.time, + dateStr = time.dateFormat('%m-%d %H:%M:%S.%L', date), + format, + n, + blank = '01-01 00:00:00.000', + strpos = { + millisecond: 15, + second: 12, + minute: 9, + hour: 6, + day: 3 + }, + lastN = 'millisecond'; // for sub-millisecond data, #4223 + for (n in timeUnits) { + + // If the range is exactly one week and we're looking at a + // Sunday/Monday, go for the week format + if ( + range === timeUnits.week && + +time.dateFormat('%w', date) === startOfWeek && + dateStr.substr(6) === blank.substr(6) + ) { + n = 'week'; + break; + } + + // The first format that is too great for the range + if (timeUnits[n] > range) { + n = lastN; + break; + } + + // If the point is placed every day at 23:59, we need to show + // the minutes as well. #2637. + if ( + strpos[n] && + dateStr.substr(strpos[n]) !== blank.substr(strpos[n]) + ) { + break; + } + + // Weeks are outside the hierarchy, only apply them on + // Mondays/Sundays like in the first condition + if (n !== 'week') { + lastN = n; + } + } + + if (n) { + format = dateTimeLabelFormats[n]; + } + + return format; + }, + + /** + * Get the best X date format based on the closest point range on the axis. + */ + getXDateFormat: function (point, options, xAxis) { + var xDateFormat, + dateTimeLabelFormats = options.dateTimeLabelFormats, + closestPointRange = xAxis && xAxis.closestPointRange; + + if (closestPointRange) { + xDateFormat = this.getDateFormat( + closestPointRange, + point.x, + xAxis.options.startOfWeek, + dateTimeLabelFormats + ); + } else { + xDateFormat = dateTimeLabelFormats.day; + } + + return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 + }, + + /** + * Format the footer/header of the tooltip + * #3397: abstraction to enable formatting of footer and header + */ + tooltipFooterHeaderFormatter: function (labelConfig, isFooter) { + var footOrHead = isFooter ? 'footer' : 'header', + series = labelConfig.series, + tooltipOptions = series.tooltipOptions, + xDateFormat = tooltipOptions.xDateFormat, + xAxis = series.xAxis, + isDateTime = ( + xAxis && + xAxis.options.type === 'datetime' && + isNumber(labelConfig.key) + ), + formatString = tooltipOptions[footOrHead + 'Format']; + + // Guess the best date format based on the closest point distance (#568, + // #3418) + if (isDateTime && !xDateFormat) { + xDateFormat = this.getXDateFormat( + labelConfig, + tooltipOptions, + xAxis + ); + } + + // Insert the footer date format if any + if (isDateTime && xDateFormat) { + each( + (labelConfig.point && labelConfig.point.tooltipDateKeys) || + ['key'], + function (key) { + formatString = formatString.replace( + '{point.' + key + '}', + '{point.' + key + ':' + xDateFormat + '}' + ); + } + ); + } + + return format(formatString, { + point: labelConfig, + series: series + }, this.chart.time); + }, + + /** + * Build the body (lines) of the tooltip by iterating over the items and + * returning one entry for each item, abstracting this functionality allows + * to easily overwrite and extend it. + */ + bodyFormatter: function (items) { + return map(items, function (item) { + var tooltipOptions = item.series.tooltipOptions; + return ( + tooltipOptions[ + (item.point.formatPrefix || 'point') + 'Formatter' + ] || + item.point.tooltipFormatter + ).call( + item.point, + tooltipOptions[(item.point.formatPrefix || 'point') + 'Format'] + ); + }); + } }; diff --git a/js/parts/TouchPointer.js b/js/parts/TouchPointer.js index 1bf4d034e9b..aa61bba5715 100644 --- a/js/parts/TouchPointer.js +++ b/js/parts/TouchPointer.js @@ -7,321 +7,321 @@ import H from './Globals.js'; import './Utilities.js'; var charts = H.charts, - each = H.each, - extend = H.extend, - map = H.map, - noop = H.noop, - pick = H.pick, - Pointer = H.Pointer; + each = H.each, + extend = H.extend, + map = H.map, + noop = H.noop, + pick = H.pick, + Pointer = H.Pointer; /* Support for touch devices */ extend(Pointer.prototype, /** @lends Pointer.prototype */ { - /** - * Run translation operations - */ - pinchTranslate: function ( - pinchDown, - touches, - transform, - selectionMarker, - clip, - lastValidTouch - ) { - if (this.zoomHor) { - this.pinchTranslateDirection( - true, - pinchDown, - touches, - transform, - selectionMarker, - clip, - lastValidTouch - ); - } - if (this.zoomVert) { - this.pinchTranslateDirection( - false, - pinchDown, - touches, - transform, - selectionMarker, - clip, - lastValidTouch - ); - } - }, - - /** - * Run translation operations for each direction (horizontal and vertical) - * independently - */ - pinchTranslateDirection: function (horiz, pinchDown, touches, transform, - selectionMarker, clip, lastValidTouch, forcedScale) { - var chart = this.chart, - xy = horiz ? 'x' : 'y', - XY = horiz ? 'X' : 'Y', - sChartXY = 'chart' + XY, - wh = horiz ? 'width' : 'height', - plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], - selectionWH, - selectionXY, - clipXY, - scale = forcedScale || 1, - inverted = chart.inverted, - bounds = chart.bounds[horiz ? 'h' : 'v'], - singleTouch = pinchDown.length === 1, - touch0Start = pinchDown[0][sChartXY], - touch0Now = touches[0][sChartXY], - touch1Start = !singleTouch && pinchDown[1][sChartXY], - touch1Now = !singleTouch && touches[1][sChartXY], - outOfBounds, - transformScale, - scaleKey, - setScale = function () { - // Don't zoom if fingers are too close on this axis - if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { - scale = forcedScale || - Math.abs(touch0Now - touch1Now) / - Math.abs(touch0Start - touch1Start); - } - - clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; - selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / - scale; - }; - - // Set the scale, first pass - setScale(); - - // The clip position (x or y) is altered if out of bounds, the selection - // position is not - selectionXY = clipXY; - - // Out of bounds - if (selectionXY < bounds.min) { - selectionXY = bounds.min; - outOfBounds = true; - } else if (selectionXY + selectionWH > bounds.max) { - selectionXY = bounds.max - selectionWH; - outOfBounds = true; - } - - // Is the chart dragged off its bounds, determined by dataMin and - // dataMax? - if (outOfBounds) { - - // Modify the touchNow position in order to create an elastic drag - // movement. This indicates to the user that the chart is responsive - // but can't be dragged further. - touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); - if (!singleTouch) { - touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); - } - - // Set the scale, second pass to adapt to the modified touchNow - // positions - setScale(); - - } else { - lastValidTouch[xy] = [touch0Now, touch1Now]; - } - - // Set geometry for clipping, selection and transformation - if (!inverted) { - clip[xy] = clipXY - plotLeftTop; - clip[wh] = selectionWH; - } - scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; - transformScale = inverted ? 1 / scale : scale; - - selectionMarker[wh] = selectionWH; - selectionMarker[xy] = selectionXY; - transform[scaleKey] = scale; - transform['translate' + XY] = (transformScale * plotLeftTop) + - (touch0Now - (transformScale * touch0Start)); - }, - - /** - * Handle touch events with two touches - */ - pinch: function (e) { - - var self = this, - chart = self.chart, - pinchDown = self.pinchDown, - touches = e.touches, - touchesLength = touches.length, - lastValidTouch = self.lastValidTouch, - hasZoom = self.hasZoom, - selectionMarker = self.selectionMarker, - transform = {}, - fireClickEvent = touchesLength === 1 && - ((self.inClass(e.target, 'highcharts-tracker') && - chart.runTrackerClick) || self.runChartClick), - clip = {}; - - // Don't initiate panning until the user has pinched. This prevents us - // from blocking page scrolling as users scroll down a long page - // (#4210). - if (touchesLength > 1) { - self.initiated = true; - } - - // On touch devices, only proceed to trigger click if a handler is - // defined - if (hasZoom && self.initiated && !fireClickEvent) { - e.preventDefault(); - } - - // Normalize each touch - map(touches, function (e) { - return self.normalize(e); - }); - - // Register the touch start position - if (e.type === 'touchstart') { - each(touches, function (e, i) { - pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; - }); - lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && - pinchDown[1].chartX]; - lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && - pinchDown[1].chartY]; - - // Identify the data bounds in pixels - each(chart.axes, function (axis) { - if (axis.zoomEnabled) { - var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], - minPixelPadding = axis.minPixelPadding, - min = axis.toPixels( - pick(axis.options.min, axis.dataMin) - ), - max = axis.toPixels( - pick(axis.options.max, axis.dataMax) - ), - absMin = Math.min(min, max), - absMax = Math.max(min, max); - - // Store the bounds for use in the touchmove handler - bounds.min = Math.min(axis.pos, absMin - minPixelPadding); - bounds.max = Math.max( - axis.pos + axis.len, - absMax + minPixelPadding - ); - } - }); - self.res = true; // reset on next move - - // Optionally move the tooltip on touchmove - } else if (self.followTouchMove && touchesLength === 1) { - this.runPointActions(self.normalize(e)); - - // Event type is touchmove, handle panning and pinching - } else if (pinchDown.length) { // can be 0 when releasing, if touchend - // fires first - - - // Set the marker - if (!selectionMarker) { - self.selectionMarker = selectionMarker = extend({ - destroy: noop, - touch: true - }, chart.plotBox); - } - - self.pinchTranslate( - pinchDown, - touches, - transform, - selectionMarker, - clip, - lastValidTouch - ); - - self.hasPinched = hasZoom; - - // Scale and translate the groups to provide visual feedback during - // pinching - self.scaleGroups(transform, clip); - - if (self.res) { - self.res = false; - this.reset(false, 0); - } - } - }, - - /** - * General touch handler shared by touchstart and touchmove. - */ - touch: function (e, start) { - var chart = this.chart, - hasMoved, - pinchDown, - isInside; - - if (chart.index !== H.hoverChartIndex) { - this.onContainerMouseLeave({ relatedTarget: true }); - } - H.hoverChartIndex = chart.index; - - if (e.touches.length === 1) { - - e = this.normalize(e); - - isInside = chart.isInsidePlot( - e.chartX - chart.plotLeft, - e.chartY - chart.plotTop - ); - if (isInside && !chart.openMenu) { - - // Run mouse events and display tooltip etc - if (start) { - this.runPointActions(e); - } - - // Android fires touchmove events after the touchstart even if - // the finger hasn't moved, or moved only a pixel or two. In iOS - // however, the touchmove doesn't fire unless the finger moves - // more than ~4px. So we emulate this behaviour in Android by - // checking how much it moved, and cancelling on small - // distances. #3450. - if (e.type === 'touchmove') { - pinchDown = this.pinchDown; - hasMoved = pinchDown[0] ? Math.sqrt( // #5266 - Math.pow(pinchDown[0].chartX - e.chartX, 2) + - Math.pow(pinchDown[0].chartY - e.chartY, 2) - ) >= 4 : false; - } - - if (pick(hasMoved, true)) { - this.pinch(e); - } - - } else if (start) { - // Hide the tooltip on touching outside the plot area (#1203) - this.reset(); - } - - } else if (e.touches.length === 2) { - this.pinch(e); - } - }, - - onContainerTouchStart: function (e) { - this.zoomOption(e); - this.touch(e, true); - }, - - onContainerTouchMove: function (e) { - this.touch(e); - }, - - onDocumentTouchEnd: function (e) { - if (charts[H.hoverChartIndex]) { - charts[H.hoverChartIndex].pointer.drop(e); - } - } + /** + * Run translation operations + */ + pinchTranslate: function ( + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ) { + if (this.zoomHor) { + this.pinchTranslateDirection( + true, + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ); + } + if (this.zoomVert) { + this.pinchTranslateDirection( + false, + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ); + } + }, + + /** + * Run translation operations for each direction (horizontal and vertical) + * independently + */ + pinchTranslateDirection: function (horiz, pinchDown, touches, transform, + selectionMarker, clip, lastValidTouch, forcedScale) { + var chart = this.chart, + xy = horiz ? 'x' : 'y', + XY = horiz ? 'X' : 'Y', + sChartXY = 'chart' + XY, + wh = horiz ? 'width' : 'height', + plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], + selectionWH, + selectionXY, + clipXY, + scale = forcedScale || 1, + inverted = chart.inverted, + bounds = chart.bounds[horiz ? 'h' : 'v'], + singleTouch = pinchDown.length === 1, + touch0Start = pinchDown[0][sChartXY], + touch0Now = touches[0][sChartXY], + touch1Start = !singleTouch && pinchDown[1][sChartXY], + touch1Now = !singleTouch && touches[1][sChartXY], + outOfBounds, + transformScale, + scaleKey, + setScale = function () { + // Don't zoom if fingers are too close on this axis + if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { + scale = forcedScale || + Math.abs(touch0Now - touch1Now) / + Math.abs(touch0Start - touch1Start); + } + + clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; + selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / + scale; + }; + + // Set the scale, first pass + setScale(); + + // The clip position (x or y) is altered if out of bounds, the selection + // position is not + selectionXY = clipXY; + + // Out of bounds + if (selectionXY < bounds.min) { + selectionXY = bounds.min; + outOfBounds = true; + } else if (selectionXY + selectionWH > bounds.max) { + selectionXY = bounds.max - selectionWH; + outOfBounds = true; + } + + // Is the chart dragged off its bounds, determined by dataMin and + // dataMax? + if (outOfBounds) { + + // Modify the touchNow position in order to create an elastic drag + // movement. This indicates to the user that the chart is responsive + // but can't be dragged further. + touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); + if (!singleTouch) { + touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); + } + + // Set the scale, second pass to adapt to the modified touchNow + // positions + setScale(); + + } else { + lastValidTouch[xy] = [touch0Now, touch1Now]; + } + + // Set geometry for clipping, selection and transformation + if (!inverted) { + clip[xy] = clipXY - plotLeftTop; + clip[wh] = selectionWH; + } + scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; + transformScale = inverted ? 1 / scale : scale; + + selectionMarker[wh] = selectionWH; + selectionMarker[xy] = selectionXY; + transform[scaleKey] = scale; + transform['translate' + XY] = (transformScale * plotLeftTop) + + (touch0Now - (transformScale * touch0Start)); + }, + + /** + * Handle touch events with two touches + */ + pinch: function (e) { + + var self = this, + chart = self.chart, + pinchDown = self.pinchDown, + touches = e.touches, + touchesLength = touches.length, + lastValidTouch = self.lastValidTouch, + hasZoom = self.hasZoom, + selectionMarker = self.selectionMarker, + transform = {}, + fireClickEvent = touchesLength === 1 && + ((self.inClass(e.target, 'highcharts-tracker') && + chart.runTrackerClick) || self.runChartClick), + clip = {}; + + // Don't initiate panning until the user has pinched. This prevents us + // from blocking page scrolling as users scroll down a long page + // (#4210). + if (touchesLength > 1) { + self.initiated = true; + } + + // On touch devices, only proceed to trigger click if a handler is + // defined + if (hasZoom && self.initiated && !fireClickEvent) { + e.preventDefault(); + } + + // Normalize each touch + map(touches, function (e) { + return self.normalize(e); + }); + + // Register the touch start position + if (e.type === 'touchstart') { + each(touches, function (e, i) { + pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; + }); + lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && + pinchDown[1].chartX]; + lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && + pinchDown[1].chartY]; + + // Identify the data bounds in pixels + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], + minPixelPadding = axis.minPixelPadding, + min = axis.toPixels( + pick(axis.options.min, axis.dataMin) + ), + max = axis.toPixels( + pick(axis.options.max, axis.dataMax) + ), + absMin = Math.min(min, max), + absMax = Math.max(min, max); + + // Store the bounds for use in the touchmove handler + bounds.min = Math.min(axis.pos, absMin - minPixelPadding); + bounds.max = Math.max( + axis.pos + axis.len, + absMax + minPixelPadding + ); + } + }); + self.res = true; // reset on next move + + // Optionally move the tooltip on touchmove + } else if (self.followTouchMove && touchesLength === 1) { + this.runPointActions(self.normalize(e)); + + // Event type is touchmove, handle panning and pinching + } else if (pinchDown.length) { // can be 0 when releasing, if touchend + // fires first + + + // Set the marker + if (!selectionMarker) { + self.selectionMarker = selectionMarker = extend({ + destroy: noop, + touch: true + }, chart.plotBox); + } + + self.pinchTranslate( + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ); + + self.hasPinched = hasZoom; + + // Scale and translate the groups to provide visual feedback during + // pinching + self.scaleGroups(transform, clip); + + if (self.res) { + self.res = false; + this.reset(false, 0); + } + } + }, + + /** + * General touch handler shared by touchstart and touchmove. + */ + touch: function (e, start) { + var chart = this.chart, + hasMoved, + pinchDown, + isInside; + + if (chart.index !== H.hoverChartIndex) { + this.onContainerMouseLeave({ relatedTarget: true }); + } + H.hoverChartIndex = chart.index; + + if (e.touches.length === 1) { + + e = this.normalize(e); + + isInside = chart.isInsidePlot( + e.chartX - chart.plotLeft, + e.chartY - chart.plotTop + ); + if (isInside && !chart.openMenu) { + + // Run mouse events and display tooltip etc + if (start) { + this.runPointActions(e); + } + + // Android fires touchmove events after the touchstart even if + // the finger hasn't moved, or moved only a pixel or two. In iOS + // however, the touchmove doesn't fire unless the finger moves + // more than ~4px. So we emulate this behaviour in Android by + // checking how much it moved, and cancelling on small + // distances. #3450. + if (e.type === 'touchmove') { + pinchDown = this.pinchDown; + hasMoved = pinchDown[0] ? Math.sqrt( // #5266 + Math.pow(pinchDown[0].chartX - e.chartX, 2) + + Math.pow(pinchDown[0].chartY - e.chartY, 2) + ) >= 4 : false; + } + + if (pick(hasMoved, true)) { + this.pinch(e); + } + + } else if (start) { + // Hide the tooltip on touching outside the plot area (#1203) + this.reset(); + } + + } else if (e.touches.length === 2) { + this.pinch(e); + } + }, + + onContainerTouchStart: function (e) { + this.zoomOption(e); + this.touch(e, true); + }, + + onContainerTouchMove: function (e) { + this.touch(e); + }, + + onDocumentTouchEnd: function (e) { + if (charts[H.hoverChartIndex]) { + charts[H.hoverChartIndex].pointer.drop(e); + } + } }); diff --git a/js/parts/Utilities.js b/js/parts/Utilities.js index fde581fa561..10d24bc00b4 100644 --- a/js/parts/Utilities.js +++ b/js/parts/Utilities.js @@ -3,7 +3,7 @@ * * License: www.highcharts.com/license */ - + 'use strict'; import H from './Globals.js'; @@ -14,15 +14,15 @@ import H from './Globals.js'; * * @example * var chart = Highcharts.chart('container', { ... }); - * + * * @namespace Highcharts */ H.timers = []; var charts = H.charts, - doc = H.doc, - win = H.win; + doc = H.doc, + win = H.win; /** * Provide error messages for debugging, with links to online explanation. This @@ -30,26 +30,26 @@ var charts = H.charts, * * @function #error * @memberOf Highcharts - * @param {Number|String} code - The error code. See [errors.xml]{@link + * @param {Number|String} code - The error code. See [errors.xml]{@link * https://github.com/highcharts/highcharts/blob/master/errors/errors.xml} * for available codes. If it is a string, the error message is printed * directly in the console. - * @param {Boolean} [stop=false] - Whether to throw an error or just log a + * @param {Boolean} [stop=false] - Whether to throw an error or just log a * warning in the console. * * @sample highcharts/chart/highcharts-error/ Custom error handler */ H.error = function (code, stop) { - var msg = H.isNumber(code) ? - 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code : - code; - if (stop) { - throw new Error(msg); - } - // else ... - if (win.console) { - console.log(msg); // eslint-disable-line no-console - } + var msg = H.isNumber(code) ? + 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code : + code; + if (stop) { + throw new Error(msg); + } + // else ... + if (win.console) { + console.log(msg); // eslint-disable-line no-console + } }; /** @@ -69,426 +69,426 @@ H.error = function (code, stop) { * rect.animate({ width: 100 }); */ H.Fx = function (elem, options, prop) { - this.options = options; - this.elem = elem; - this.prop = prop; + this.options = options; + this.elem = elem; + this.prop = prop; }; H.Fx.prototype = { - - /** - * Set the current step of a path definition on SVGElement. - * - * @function #dSetter - * @memberOf Highcharts.Fx - */ - dSetter: function () { - var start = this.paths[0], - end = this.paths[1], - ret = [], - now = this.now, - i = start.length, - startVal; - - // Land on the final path without adjustment points appended in the ends - if (now === 1) { - ret = this.toD; - - } else if (i === end.length && now < 1) { - while (i--) { - startVal = parseFloat(start[i]); - ret[i] = - isNaN(startVal) ? // a letter instruction like M or L - end[i] : - now * (parseFloat(end[i] - startVal)) + startVal; - - } - // If animation is finished or length not matching, land on right value - } else { - ret = end; - } - this.elem.attr('d', ret, null, true); - }, - - /** - * Update the element with the current animation step. - * - * @function #update - * @memberOf Highcharts.Fx - */ - update: function () { - var elem = this.elem, - prop = this.prop, // if destroyed, it is null - now = this.now, - step = this.options.step; - - // Animation setter defined from outside - if (this[prop + 'Setter']) { - this[prop + 'Setter'](); - - // Other animations on SVGElement - } else if (elem.attr) { - if (elem.element) { - elem.attr(prop, now, null, true); - } - - // HTML styles, raw HTML content like container size - } else { - elem.style[prop] = now + this.unit; - } - - if (step) { - step.call(elem, now, this); - } - - }, - - /** - * Run an animation. - * - * @function #run - * @memberOf Highcharts.Fx - * @param {Number} from - The current value, value to start from. - * @param {Number} to - The end value, value to land on. - * @param {String} [unit] - The property unit, for example `px`. - * - */ - run: function (from, to, unit) { - var self = this, - options = self.options, - timer = function (gotoEnd) { - return timer.stopped ? false : self.step(gotoEnd); - }, - requestAnimationFrame = - win.requestAnimationFrame || - function (step) { - setTimeout(step, 13); - }, - step = function () { - for (var i = 0; i < H.timers.length; i++) { - if (!H.timers[i]()) { - H.timers.splice(i--, 1); - } - } - - if (H.timers.length) { - requestAnimationFrame(step); - } - }; - - if (from === to && !this.elem['forceAnimate:' + this.prop]) { - delete options.curAnim[this.prop]; - if (options.complete && H.keys(options.curAnim).length === 0) { - options.complete.call(this.elem); - } - } else { // #7166 - this.startTime = +new Date(); - this.start = from; - this.end = to; - this.unit = unit; - this.now = this.start; - this.pos = 0; - - timer.elem = this.elem; - timer.prop = this.prop; - - if (timer() && H.timers.push(timer) === 1) { - requestAnimationFrame(step); - } - } - }, - - /** - * Run a single step in the animation. - * - * @function #step - * @memberOf Highcharts.Fx - * @param {Boolean} [gotoEnd] - Whether to go to the endpoint of the - * animation after abort. - * @returns {Boolean} Returns `true` if animation continues. - */ - step: function (gotoEnd) { - var t = +new Date(), - ret, - done, - options = this.options, - elem = this.elem, - complete = options.complete, - duration = options.duration, - curAnim = options.curAnim; - - if (elem.attr && !elem.element) { // #2616, element is destroyed - ret = false; - - } else if (gotoEnd || t >= duration + this.startTime) { - this.now = this.end; - this.pos = 1; - this.update(); - - curAnim[this.prop] = true; - - done = true; - - H.objectEach(curAnim, function (val) { - if (val !== true) { - done = false; - } - }); - - if (done && complete) { - complete.call(elem); - } - ret = false; - - } else { - this.pos = options.easing((t - this.startTime) / duration); - this.now = this.start + ((this.end - this.start) * this.pos); - this.update(); - ret = true; - } - return ret; - }, - - /** - * Prepare start and end values so that the path can be animated one to one. - * - * @function #initPath - * @memberOf Highcharts.Fx - * @param {SVGElement} elem - The SVGElement item. - * @param {String} fromD - Starting path definition. - * @param {Array} toD - Ending path definition. - * @returns {Array} An array containing start and end paths in array form - * so that they can be animated in parallel. - */ - initPath: function (elem, fromD, toD) { - fromD = fromD || ''; - var shift, - startX = elem.startX, - endX = elem.endX, - bezier = fromD.indexOf('C') > -1, - numParams = bezier ? 7 : 3, - fullLength, - slice, - i, - start = fromD.split(' '), - end = toD.slice(), // copy - isArea = elem.isArea, - positionFactor = isArea ? 2 : 1, - reverse; - - /** - * In splines make moveTo and lineTo points have six parameters like - * bezier curves, to allow animation one-to-one. - */ - function sixify(arr) { - var isOperator, - nextIsOperator; - i = arr.length; - while (i--) { - - // Fill in dummy coordinates only if the next operator comes - // three places behind (#5788) - isOperator = arr[i] === 'M' || arr[i] === 'L'; - nextIsOperator = /[a-zA-Z]/.test(arr[i + 3]); - if (isOperator && nextIsOperator) { - arr.splice( - i + 1, 0, - arr[i + 1], arr[i + 2], - arr[i + 1], arr[i + 2] - ); - } - } - } - - /** - * Insert an array at the given position of another array - */ - function insertSlice(arr, subArr, index) { - [].splice.apply( - arr, - [index, 0].concat(subArr) - ); - } - - /** - * If shifting points, prepend a dummy point to the end path. - */ - function prepend(arr, other) { - while (arr.length < fullLength) { - - // Move to, line to or curve to? - arr[0] = other[fullLength - arr.length]; - - // Prepend a copy of the first point - insertSlice(arr, arr.slice(0, numParams), 0); - - // For areas, the bottom path goes back again to the left, so we - // need to append a copy of the last point. - if (isArea) { - insertSlice( - arr, - arr.slice(arr.length - numParams), arr.length - ); - i--; - } - } - arr[0] = 'M'; - } - - /** - * Copy and append last point until the length matches the end length - */ - function append(arr, other) { - var i = (fullLength - arr.length) / numParams; - while (i > 0 && i--) { - - // Pull out the slice that is going to be appended or inserted. - // In a line graph, the positionFactor is 1, and the last point - // is sliced out. In an area graph, the positionFactor is 2, - // causing the middle two points to be sliced out, since an area - // path starts at left, follows the upper path then turns and - // follows the bottom back. - slice = arr.slice().splice( - (arr.length / positionFactor) - numParams, - numParams * positionFactor - ); - - // Move to, line to or curve to? - slice[0] = other[fullLength - numParams - (i * numParams)]; - - // Disable first control point - if (bezier) { - slice[numParams - 6] = slice[numParams - 2]; - slice[numParams - 5] = slice[numParams - 1]; - } - - // Now insert the slice, either in the middle (for areas) or at - // the end (for lines) - insertSlice(arr, slice, arr.length / positionFactor); - - if (isArea) { - i--; - } - } - } - - if (bezier) { - sixify(start); - sixify(end); - } - - // For sideways animation, find out how much we need to shift to get the - // start path Xs to match the end path Xs. - if (startX && endX) { - for (i = 0; i < startX.length; i++) { - // Moving left, new points coming in on right - if (startX[i] === endX[0]) { - shift = i; - break; - // Moving right - } else if (startX[0] === - endX[endX.length - startX.length + i]) { - shift = i; - reverse = true; - break; - } - } - if (shift === undefined) { - start = []; - } - } - - if (start.length && H.isNumber(shift)) { - - // The common target length for the start and end array, where both - // arrays are padded in opposite ends - fullLength = end.length + shift * positionFactor * numParams; - - if (!reverse) { - prepend(end, start); - append(start, end); - } else { - prepend(start, end); - append(end, start); - } - } - - return [start, end]; - } + + /** + * Set the current step of a path definition on SVGElement. + * + * @function #dSetter + * @memberOf Highcharts.Fx + */ + dSetter: function () { + var start = this.paths[0], + end = this.paths[1], + ret = [], + now = this.now, + i = start.length, + startVal; + + // Land on the final path without adjustment points appended in the ends + if (now === 1) { + ret = this.toD; + + } else if (i === end.length && now < 1) { + while (i--) { + startVal = parseFloat(start[i]); + ret[i] = + isNaN(startVal) ? // a letter instruction like M or L + end[i] : + now * (parseFloat(end[i] - startVal)) + startVal; + + } + // If animation is finished or length not matching, land on right value + } else { + ret = end; + } + this.elem.attr('d', ret, null, true); + }, + + /** + * Update the element with the current animation step. + * + * @function #update + * @memberOf Highcharts.Fx + */ + update: function () { + var elem = this.elem, + prop = this.prop, // if destroyed, it is null + now = this.now, + step = this.options.step; + + // Animation setter defined from outside + if (this[prop + 'Setter']) { + this[prop + 'Setter'](); + + // Other animations on SVGElement + } else if (elem.attr) { + if (elem.element) { + elem.attr(prop, now, null, true); + } + + // HTML styles, raw HTML content like container size + } else { + elem.style[prop] = now + this.unit; + } + + if (step) { + step.call(elem, now, this); + } + + }, + + /** + * Run an animation. + * + * @function #run + * @memberOf Highcharts.Fx + * @param {Number} from - The current value, value to start from. + * @param {Number} to - The end value, value to land on. + * @param {String} [unit] - The property unit, for example `px`. + * + */ + run: function (from, to, unit) { + var self = this, + options = self.options, + timer = function (gotoEnd) { + return timer.stopped ? false : self.step(gotoEnd); + }, + requestAnimationFrame = + win.requestAnimationFrame || + function (step) { + setTimeout(step, 13); + }, + step = function () { + for (var i = 0; i < H.timers.length; i++) { + if (!H.timers[i]()) { + H.timers.splice(i--, 1); + } + } + + if (H.timers.length) { + requestAnimationFrame(step); + } + }; + + if (from === to && !this.elem['forceAnimate:' + this.prop]) { + delete options.curAnim[this.prop]; + if (options.complete && H.keys(options.curAnim).length === 0) { + options.complete.call(this.elem); + } + } else { // #7166 + this.startTime = +new Date(); + this.start = from; + this.end = to; + this.unit = unit; + this.now = this.start; + this.pos = 0; + + timer.elem = this.elem; + timer.prop = this.prop; + + if (timer() && H.timers.push(timer) === 1) { + requestAnimationFrame(step); + } + } + }, + + /** + * Run a single step in the animation. + * + * @function #step + * @memberOf Highcharts.Fx + * @param {Boolean} [gotoEnd] - Whether to go to the endpoint of the + * animation after abort. + * @returns {Boolean} Returns `true` if animation continues. + */ + step: function (gotoEnd) { + var t = +new Date(), + ret, + done, + options = this.options, + elem = this.elem, + complete = options.complete, + duration = options.duration, + curAnim = options.curAnim; + + if (elem.attr && !elem.element) { // #2616, element is destroyed + ret = false; + + } else if (gotoEnd || t >= duration + this.startTime) { + this.now = this.end; + this.pos = 1; + this.update(); + + curAnim[this.prop] = true; + + done = true; + + H.objectEach(curAnim, function (val) { + if (val !== true) { + done = false; + } + }); + + if (done && complete) { + complete.call(elem); + } + ret = false; + + } else { + this.pos = options.easing((t - this.startTime) / duration); + this.now = this.start + ((this.end - this.start) * this.pos); + this.update(); + ret = true; + } + return ret; + }, + + /** + * Prepare start and end values so that the path can be animated one to one. + * + * @function #initPath + * @memberOf Highcharts.Fx + * @param {SVGElement} elem - The SVGElement item. + * @param {String} fromD - Starting path definition. + * @param {Array} toD - Ending path definition. + * @returns {Array} An array containing start and end paths in array form + * so that they can be animated in parallel. + */ + initPath: function (elem, fromD, toD) { + fromD = fromD || ''; + var shift, + startX = elem.startX, + endX = elem.endX, + bezier = fromD.indexOf('C') > -1, + numParams = bezier ? 7 : 3, + fullLength, + slice, + i, + start = fromD.split(' '), + end = toD.slice(), // copy + isArea = elem.isArea, + positionFactor = isArea ? 2 : 1, + reverse; + + /** + * In splines make moveTo and lineTo points have six parameters like + * bezier curves, to allow animation one-to-one. + */ + function sixify(arr) { + var isOperator, + nextIsOperator; + i = arr.length; + while (i--) { + + // Fill in dummy coordinates only if the next operator comes + // three places behind (#5788) + isOperator = arr[i] === 'M' || arr[i] === 'L'; + nextIsOperator = /[a-zA-Z]/.test(arr[i + 3]); + if (isOperator && nextIsOperator) { + arr.splice( + i + 1, 0, + arr[i + 1], arr[i + 2], + arr[i + 1], arr[i + 2] + ); + } + } + } + + /** + * Insert an array at the given position of another array + */ + function insertSlice(arr, subArr, index) { + [].splice.apply( + arr, + [index, 0].concat(subArr) + ); + } + + /** + * If shifting points, prepend a dummy point to the end path. + */ + function prepend(arr, other) { + while (arr.length < fullLength) { + + // Move to, line to or curve to? + arr[0] = other[fullLength - arr.length]; + + // Prepend a copy of the first point + insertSlice(arr, arr.slice(0, numParams), 0); + + // For areas, the bottom path goes back again to the left, so we + // need to append a copy of the last point. + if (isArea) { + insertSlice( + arr, + arr.slice(arr.length - numParams), arr.length + ); + i--; + } + } + arr[0] = 'M'; + } + + /** + * Copy and append last point until the length matches the end length + */ + function append(arr, other) { + var i = (fullLength - arr.length) / numParams; + while (i > 0 && i--) { + + // Pull out the slice that is going to be appended or inserted. + // In a line graph, the positionFactor is 1, and the last point + // is sliced out. In an area graph, the positionFactor is 2, + // causing the middle two points to be sliced out, since an area + // path starts at left, follows the upper path then turns and + // follows the bottom back. + slice = arr.slice().splice( + (arr.length / positionFactor) - numParams, + numParams * positionFactor + ); + + // Move to, line to or curve to? + slice[0] = other[fullLength - numParams - (i * numParams)]; + + // Disable first control point + if (bezier) { + slice[numParams - 6] = slice[numParams - 2]; + slice[numParams - 5] = slice[numParams - 1]; + } + + // Now insert the slice, either in the middle (for areas) or at + // the end (for lines) + insertSlice(arr, slice, arr.length / positionFactor); + + if (isArea) { + i--; + } + } + } + + if (bezier) { + sixify(start); + sixify(end); + } + + // For sideways animation, find out how much we need to shift to get the + // start path Xs to match the end path Xs. + if (startX && endX) { + for (i = 0; i < startX.length; i++) { + // Moving left, new points coming in on right + if (startX[i] === endX[0]) { + shift = i; + break; + // Moving right + } else if (startX[0] === + endX[endX.length - startX.length + i]) { + shift = i; + reverse = true; + break; + } + } + if (shift === undefined) { + start = []; + } + } + + if (start.length && H.isNumber(shift)) { + + // The common target length for the start and end array, where both + // arrays are padded in opposite ends + fullLength = end.length + shift * positionFactor * numParams; + + if (!reverse) { + prepend(end, start); + append(start, end); + } else { + prepend(start, end); + append(end, start); + } + } + + return [start, end]; + } }; // End of Fx prototype /** * Handle animation of the color attributes directly. */ -H.Fx.prototype.fillSetter = +H.Fx.prototype.fillSetter = H.Fx.prototype.strokeSetter = function () { - this.elem.attr( - this.prop, - H.color(this.start).tweenTo(H.color(this.end), this.pos), - null, - true - ); + this.elem.attr( + this.prop, + H.color(this.start).tweenTo(H.color(this.end), this.pos), + null, + true + ); }; /** * Utility function to deep merge two or more objects and return a third object. * If the first argument is true, the contents of the second object is copied - * into the first object. The merge function can also be used with a single + * into the first object. The merge function can also be used with a single * object argument to create a deep copy of an object. * * @function #merge * @memberOf Highcharts * @param {Boolean} [extend] - Whether to extend the left-side object (a) or - return a whole new object. + return a whole new object. * @param {Object} a - The first object to extend. When only this is given, the - function returns a deep copy. + function returns a deep copy. * @param {...Object} [n] - An object to merge into the previous one. - * @returns {Object} - The merged object. If the first argument is true, the + * @returns {Object} - The merged object. If the first argument is true, the * return is the same as the second argument. */ H.merge = function () { - var i, - args = arguments, - len, - ret = {}, - doCopy = function (copy, original) { - // An object is replacing a primitive - if (typeof copy !== 'object') { - copy = {}; - } - - H.objectEach(original, function (value, key) { - - // Copy the contents of objects, but not arrays or DOM nodes - if ( - H.isObject(value, true) && - !H.isClass(value) && - !H.isDOMElement(value) - ) { - copy[key] = doCopy(copy[key] || {}, value); - - // Primitives and arrays are copied over directly - } else { - copy[key] = original[key]; - } - }); - return copy; - }; - - // If first argument is true, copy into the existing object. Used in - // setOptions. - if (args[0] === true) { - ret = args[1]; - args = Array.prototype.slice.call(args, 2); - } - - // For each argument, extend the return - len = args.length; - for (i = 0; i < len; i++) { - ret = doCopy(ret, args[i]); - } - - return ret; + var i, + args = arguments, + len, + ret = {}, + doCopy = function (copy, original) { + // An object is replacing a primitive + if (typeof copy !== 'object') { + copy = {}; + } + + H.objectEach(original, function (value, key) { + + // Copy the contents of objects, but not arrays or DOM nodes + if ( + H.isObject(value, true) && + !H.isClass(value) && + !H.isDOMElement(value) + ) { + copy[key] = doCopy(copy[key] || {}, value); + + // Primitives and arrays are copied over directly + } else { + copy[key] = original[key]; + } + }); + return copy; + }; + + // If first argument is true, copy into the existing object. Used in + // setOptions. + if (args[0] === true) { + ret = args[1]; + args = Array.prototype.slice.call(args, 2); + } + + // For each argument, extend the return + len = args.length; + for (i = 0; i < len; i++) { + ret = doCopy(ret, args[i]); + } + + return ret; }; /** @@ -498,7 +498,7 @@ H.merge = function () { * @param {Number} mag Magnitude */ H.pInt = function (s, mag) { - return parseInt(s, mag || 10); + return parseInt(s, mag || 10); }; /** @@ -510,7 +510,7 @@ H.pInt = function (s, mag) { * @returns {Boolean} - True if the argument is a string. */ H.isString = function (s) { - return typeof s === 'string'; + return typeof s === 'string'; }; /** @@ -522,8 +522,8 @@ H.isString = function (s) { * @returns {Boolean} - True if the argument is an array. */ H.isArray = function (obj) { - var str = Object.prototype.toString.call(obj); - return str === '[object Array]' || str === '[object Array Iterator]'; + var str = Object.prototype.toString.call(obj); + return str === '[object Array]' || str === '[object Array Iterator]'; }; /** @@ -537,7 +537,7 @@ H.isArray = function (obj) { * @returns {Boolean} - True if the argument is an object. */ H.isObject = function (obj, strict) { - return !!obj && typeof obj === 'object' && (!strict || !H.isArray(obj)); + return !!obj && typeof obj === 'object' && (!strict || !H.isArray(obj)); }; /** @@ -549,7 +549,7 @@ H.isObject = function (obj, strict) { * @returns {Boolean} - True if the argument is a HTML Element. */ H.isDOMElement = function (obj) { - return H.isObject(obj) && typeof obj.nodeType === 'number'; + return H.isObject(obj) && typeof obj.nodeType === 'number'; }; /** @@ -561,12 +561,12 @@ H.isDOMElement = function (obj) { * @returns {Boolean} - True if the argument is an class. */ H.isClass = function (obj) { - var c = obj && obj.constructor; - return !!( - H.isObject(obj, true) && - !H.isDOMElement(obj) && - (c && c.name && c.name !== 'Object') - ); + var c = obj && obj.constructor; + return !!( + H.isObject(obj, true) && + !H.isDOMElement(obj) && + (c && c.name && c.name !== 'Object') + ); }; /** @@ -581,7 +581,7 @@ H.isClass = function (obj) { * True if the item is a finite number */ H.isNumber = function (n) { - return typeof n === 'number' && !isNaN(n) && n < Infinity && n > -Infinity; + return typeof n === 'number' && !isNaN(n) && n < Infinity && n > -Infinity; }; /** @@ -593,13 +593,13 @@ H.isNumber = function (n) { * @param {*} item - The item to remove. */ H.erase = function (arr, item) { - var i = arr.length; - while (i--) { - if (arr[i] === item) { - arr.splice(i, 1); - break; - } - } + var i = arr.length; + while (i--) { + if (arr[i] === item) { + arr.splice(i, 1); + break; + } + } }; /** @@ -612,7 +612,7 @@ H.erase = function (arr, item) { * true. */ H.defined = function (obj) { - return obj !== undefined && obj !== null; + return obj !== undefined && obj !== null; }; /** @@ -628,31 +628,31 @@ H.defined = function (obj) { * @returns {*} When used as a getter, return the value. */ H.attr = function (elem, prop, value) { - var ret; - - // if the prop is a string - if (H.isString(prop)) { - // set the value - if (H.defined(value)) { - elem.setAttribute(prop, value); - - // get the value - } else if (elem && elem.getAttribute) { - ret = elem.getAttribute(prop); - - // IE7 and below cannot get class through getAttribute (#7850) - if (!ret && prop === 'class') { - ret = elem.getAttribute(prop + 'Name'); - } - } - - // else if prop is defined, it is a hash of key/value pairs - } else if (H.defined(prop) && H.isObject(prop)) { - H.objectEach(prop, function (val, key) { - elem.setAttribute(key, val); - }); - } - return ret; + var ret; + + // if the prop is a string + if (H.isString(prop)) { + // set the value + if (H.defined(value)) { + elem.setAttribute(prop, value); + + // get the value + } else if (elem && elem.getAttribute) { + ret = elem.getAttribute(prop); + + // IE7 and below cannot get class through getAttribute (#7850) + if (!ret && prop === 'class') { + ret = elem.getAttribute(prop + 'Name'); + } + } + + // else if prop is defined, it is a hash of key/value pairs + } else if (H.defined(prop) && H.isObject(prop)) { + H.objectEach(prop, function (val, key) { + elem.setAttribute(key, val); + }); + } + return ret; }; /** @@ -664,7 +664,7 @@ H.attr = function (elem, prop, value) { * @returns {Array} The produced or original array. */ H.splat = function (obj) { - return H.isArray(obj) ? obj : [obj]; + return H.isArray(obj) ? obj : [obj]; }; /** @@ -680,10 +680,10 @@ H.splat = function (obj) { * with H.clearTimeout. */ H.syncTimeout = function (fn, delay, context) { - if (delay) { - return setTimeout(fn, delay, context); - } - fn.call(0, context); + if (delay) { + return setTimeout(fn, delay, context); + } + fn.call(0, context); }; /** @@ -696,9 +696,9 @@ H.syncTimeout = function (fn, delay, context) { * @param {Number} id - id of a timeout. */ H.clearTimeout = function (id) { - if (H.defined(id)) { - clearTimeout(id); - } + if (H.defined(id)) { + clearTimeout(id); + } }; /** @@ -711,14 +711,14 @@ H.clearTimeout = function (id) { * @returns {Object} Object a, the original object. */ H.extend = function (a, b) { - var n; - if (!a) { - a = {}; - } - for (n in b) { - a[n] = b[n]; - } - return a; + var n; + if (!a) { + a = {}; + } + for (n in b) { + a[n] = b[n]; + } + return a; }; @@ -731,16 +731,16 @@ H.extend = function (a, b) { * @returns {*} The value of the first argument that is not null or undefined. */ H.pick = function () { - var args = arguments, - i, - arg, - length = args.length; - for (i = 0; i < length; i++) { - arg = args[i]; - if (arg !== undefined && arg !== null) { - return arg; - } - } + var args = arguments, + i, + arg, + length = args.length; + for (i = 0; i < length; i++) { + arg = args[i]; + if (arg !== undefined && arg !== null) { + return arg; + } + } }; /** @@ -760,15 +760,15 @@ H.pick = function () { * @memberOf Highcharts * @param {HTMLDOMElement} el - A HTML DOM element. * @param {CSSObject} styles - Style object with camel case property names. - * + * */ H.css = function (el, styles) { - if (H.isMS && !H.svg) { // #2686 - if (styles && styles.opacity !== undefined) { - styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; - } - } - H.extend(el.style, styles); + if (H.isMS && !H.svg) { // #2686 + if (styles && styles.opacity !== undefined) { + styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; + } + } + H.extend(el.style, styles); }; /** @@ -790,21 +790,21 @@ H.css = function (el, styles) { * @returns {HTMLDOMElement} The created DOM element. */ H.createElement = function (tag, attribs, styles, parent, nopad) { - var el = doc.createElement(tag), - css = H.css; - if (attribs) { - H.extend(el, attribs); - } - if (nopad) { - css(el, { padding: 0, border: 'none', margin: 0 }); - } - if (styles) { - css(el, styles); - } - if (parent) { - parent.appendChild(el); - } - return el; + var el = doc.createElement(tag), + css = H.css; + if (attribs) { + H.extend(el, attribs); + } + if (nopad) { + css(el, { padding: 0, border: 'none', margin: 0 }); + } + if (styles) { + css(el, styles); + } + if (parent) { + parent.appendChild(el); + } + return el; }; /** @@ -818,10 +818,10 @@ H.createElement = function (tag, attribs, styles, parent, nopad) { * @returns {Object} A new prototype. */ H.extendClass = function (parent, members) { - var object = function () {}; - object.prototype = new parent(); // eslint-disable-line new-cap - H.extend(object.prototype, members); - return object; + var object = function () {}; + object.prototype = new parent(); // eslint-disable-line new-cap + H.extend(object.prototype, members); + return object; }; /** @@ -835,13 +835,13 @@ H.extendClass = function (parent, members) { * @returns {String} The padded string. */ H.pad = function (number, length, padder) { - return new Array( - (length || 2) + - 1 - - String(number) - .replace('-', '') - .length - ).join(padder || 0) + number; + return new Array( + (length || 2) + + 1 - + String(number) + .replace('-', '') + .length + ).join(padder || 0) + number; }; /** @@ -860,15 +860,15 @@ H.pad = function (number, length, padder) { * @param {number} base * The full length that represents 100%. * @param {number} [offset=0] - * A pixel offset to apply for percentage values. Used internally in + * A pixel offset to apply for percentage values. Used internally in * axis positioning. * @return {number} * The computed length. */ H.relativeLength = function (value, base, offset) { - return (/%$/).test(value) ? - (base * parseFloat(value) / 100) + (offset || 0) : - parseFloat(value); + return (/%$/).test(value) ? + (base * parseFloat(value) / 100) + (offset || 0) : + parseFloat(value); }; /** @@ -882,23 +882,23 @@ H.relativeLength = function (value, base, offset) { * @param {Function} func - A wrapper function callback. This function is called * with the same arguments as the original function, except that the * original function is unshifted and passed as the first argument. - * + * */ H.wrap = function (obj, method, func) { - var proceed = obj[method]; - obj[method] = function () { - var args = Array.prototype.slice.call(arguments), - outerArgs = arguments, - ctx = this, - ret; - ctx.proceed = function () { - proceed.apply(ctx, arguments.length ? arguments : outerArgs); - }; - args.unshift(proceed); - ret = func.apply(this, args); - ctx.proceed = null; - return ret; - }; + var proceed = obj[method]; + obj[method] = function () { + var args = Array.prototype.slice.call(arguments), + outerArgs = arguments, + ctx = this, + ret; + ctx.proceed = function () { + proceed.apply(ctx, arguments.length ? arguments : outerArgs); + }; + args.unshift(proceed); + ret = func.apply(this, args); + ctx.proceed = null; + return ret; + }; }; @@ -916,30 +916,30 @@ H.wrap = function (obj, method, func) { * @param {Time} [time] * A `Time` instance that determines the date formatting, for example for * applying time zone corrections to the formatted date. - + * @returns {String} The formatted representation of the value. */ H.formatSingle = function (format, val, time) { - var floatRegex = /f$/, - decRegex = /\.([0-9])/, - lang = H.defaultOptions.lang, - decimals; - - if (floatRegex.test(format)) { // float - decimals = format.match(decRegex); - decimals = decimals ? decimals[1] : -1; - if (val !== null) { - val = H.numberFormat( - val, - decimals, - lang.decimalPoint, - format.indexOf(',') > -1 ? lang.thousandsSep : '' - ); - } - } else { - val = (time || H.time).dateFormat(format, val); - } - return val; + var floatRegex = /f$/, + decRegex = /\.([0-9])/, + lang = H.defaultOptions.lang, + decimals; + + if (floatRegex.test(format)) { // float + decimals = format.match(decRegex); + decimals = decimals ? decimals[1] : -1; + if (val !== null) { + val = H.numberFormat( + val, + decimals, + lang.decimalPoint, + format.indexOf(',') > -1 ? lang.thousandsSep : '' + ); + } + } else { + val = (time || H.time).dateFormat(format, val); + } + return val; }; /** @@ -966,56 +966,56 @@ H.formatSingle = function (format, val, time) { * // => The red fox was 3.14 feet long */ H.format = function (str, ctx, time) { - var splitter = '{', - isInside = false, - segment, - valueAndFormat, - path, - i, - len, - ret = [], - val, - index; - - while (str) { - index = str.indexOf(splitter); - if (index === -1) { - break; - } - - segment = str.slice(0, index); - if (isInside) { // we're on the closing bracket looking back - - valueAndFormat = segment.split(':'); - path = valueAndFormat.shift().split('.'); // get first and leave - len = path.length; - val = ctx; - - // Assign deeper paths - for (i = 0; i < len; i++) { - if (val) { - val = val[path[i]]; - } - } - - // Format the replacement - if (valueAndFormat.length) { - val = H.formatSingle(valueAndFormat.join(':'), val, time); - } - - // Push the result and advance the cursor - ret.push(val); - - } else { - ret.push(segment); - - } - str = str.slice(index + 1); // the rest - isInside = !isInside; // toggle - splitter = isInside ? '}' : '{'; // now look for next matching bracket - } - ret.push(str); - return ret.join(''); + var splitter = '{', + isInside = false, + segment, + valueAndFormat, + path, + i, + len, + ret = [], + val, + index; + + while (str) { + index = str.indexOf(splitter); + if (index === -1) { + break; + } + + segment = str.slice(0, index); + if (isInside) { // we're on the closing bracket looking back + + valueAndFormat = segment.split(':'); + path = valueAndFormat.shift().split('.'); // get first and leave + len = path.length; + val = ctx; + + // Assign deeper paths + for (i = 0; i < len; i++) { + if (val) { + val = val[path[i]]; + } + } + + // Format the replacement + if (valueAndFormat.length) { + val = H.formatSingle(valueAndFormat.join(':'), val, time); + } + + // Push the result and advance the cursor + ret.push(val); + + } else { + ret.push(segment); + + } + str = str.slice(index + 1); // the rest + isInside = !isInside; // toggle + splitter = isInside ? '}' : '{'; // now look for next matching bracket + } + ret.push(str); + return ret.join(''); }; /** @@ -1028,7 +1028,7 @@ H.format = function (str, ctx, time) { * etc. */ H.getMagnitude = function (num) { - return Math.pow(10, Math.floor(Math.log(num) / Math.LN10)); + return Math.pow(10, Math.floor(Math.log(num) / Math.LN10)); }; /** @@ -1047,70 +1047,70 @@ H.getMagnitude = function (num) { * @returns {Number} The normalized interval. */ H.normalizeTickInterval = function (interval, multiples, magnitude, - allowDecimals, hasTickAmount) { - var normalized, - i, - retInterval = interval; - - // round to a tenfold of 1, 2, 2.5 or 5 - magnitude = H.pick(magnitude, 1); - normalized = interval / magnitude; - - // multiples for a linear scale - if (!multiples) { - multiples = hasTickAmount ? - // Finer grained ticks when the tick amount is hard set, including - // when alignTicks is true on multiple axes (#4580). - [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] : - - // Else, let ticks fall on rounder numbers - [1, 2, 2.5, 5, 10]; - - - // the allowDecimals option - if (allowDecimals === false) { - if (magnitude === 1) { - multiples = H.grep(multiples, function (num) { - return num % 1 === 0; - }); - } else if (magnitude <= 0.1) { - multiples = [1 / magnitude]; - } - } - } - - // normalize the interval to the nearest multiple - for (i = 0; i < multiples.length; i++) { - retInterval = multiples[i]; - // only allow tick amounts smaller than natural - if ( - ( - hasTickAmount && - retInterval * magnitude >= interval - ) || - ( - !hasTickAmount && - ( - normalized <= - ( - multiples[i] + - (multiples[i + 1] || multiples[i]) - ) / 2 - ) - ) - ) { - break; - } - } - - // Multiply back to the correct magnitude. Correct floats to appropriate - // precision (#6085). - retInterval = H.correctFloat( - retInterval * magnitude, - -Math.round(Math.log(0.001) / Math.LN10) - ); - - return retInterval; + allowDecimals, hasTickAmount) { + var normalized, + i, + retInterval = interval; + + // round to a tenfold of 1, 2, 2.5 or 5 + magnitude = H.pick(magnitude, 1); + normalized = interval / magnitude; + + // multiples for a linear scale + if (!multiples) { + multiples = hasTickAmount ? + // Finer grained ticks when the tick amount is hard set, including + // when alignTicks is true on multiple axes (#4580). + [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] : + + // Else, let ticks fall on rounder numbers + [1, 2, 2.5, 5, 10]; + + + // the allowDecimals option + if (allowDecimals === false) { + if (magnitude === 1) { + multiples = H.grep(multiples, function (num) { + return num % 1 === 0; + }); + } else if (magnitude <= 0.1) { + multiples = [1 / magnitude]; + } + } + } + + // normalize the interval to the nearest multiple + for (i = 0; i < multiples.length; i++) { + retInterval = multiples[i]; + // only allow tick amounts smaller than natural + if ( + ( + hasTickAmount && + retInterval * magnitude >= interval + ) || + ( + !hasTickAmount && + ( + normalized <= + ( + multiples[i] + + (multiples[i + 1] || multiples[i]) + ) / 2 + ) + ) + ) { + break; + } + } + + // Multiply back to the correct magnitude. Correct floats to appropriate + // precision (#6085). + retInterval = H.correctFloat( + retInterval * magnitude, + -Math.round(Math.log(0.001) / Math.LN10) + ); + + return retInterval; }; @@ -1121,29 +1121,29 @@ H.normalizeTickInterval = function (interval, multiples, magnitude, * @function #stableSort * @memberOf Highcharts * @param {Array} arr - The array to sort. - * @param {Function} sortFunction - The function to sort it with, like with + * @param {Function} sortFunction - The function to sort it with, like with * regular Array.prototype.sort. - * + * */ H.stableSort = function (arr, sortFunction) { - var length = arr.length, - sortValue, - i; - - // Add index to each item - for (i = 0; i < length; i++) { - arr[i].safeI = i; // stable sort index - } - - arr.sort(function (a, b) { - sortValue = sortFunction(a, b); - return sortValue === 0 ? a.safeI - b.safeI : sortValue; - }); - - // Remove index from items - for (i = 0; i < length; i++) { - delete arr[i].safeI; // stable sort index - } + var length = arr.length, + sortValue, + i; + + // Add index to each item + for (i = 0; i < length; i++) { + arr[i].safeI = i; // stable sort index + } + + arr.sort(function (a, b) { + sortValue = sortFunction(a, b); + return sortValue === 0 ? a.safeI - b.safeI : sortValue; + }); + + // Remove index from items + for (i = 0; i < length; i++) { + delete arr[i].safeI; // stable sort index + } }; /** @@ -1157,15 +1157,15 @@ H.stableSort = function (arr, sortFunction) { * @returns {Number} The lowest number. */ H.arrayMin = function (data) { - var i = data.length, - min = data[0]; - - while (i--) { - if (data[i] < min) { - min = data[i]; - } - } - return min; + var i = data.length, + min = data[0]; + + while (i--) { + if (data[i] < min) { + min = data[i]; + } + } + return min; }; /** @@ -1179,15 +1179,15 @@ H.arrayMin = function (data) { * @returns {Number} The highest number. */ H.arrayMax = function (data) { - var i = data.length, - max = data[0]; - - while (i--) { - if (data[i] > max) { - max = data[i]; - } - } - return max; + var i = data.length, + max = data[0]; + + while (i--) { + if (data[i] > max) { + max = data[i]; + } + } + return max; }; /** @@ -1200,19 +1200,19 @@ H.arrayMax = function (data) { * @param {Object} obj - The object to destroy properties on. * @param {Object} [except] - Exception, do not destroy this property, only * delete it. - * + * */ H.destroyObjectProperties = function (obj, except) { - H.objectEach(obj, function (val, n) { - // If the object is non-null and destroy is defined - if (val && val !== except && val.destroy) { - // Invoke the destroy - val.destroy(); - } - - // Delete the property from the object. - delete obj[n]; - }); + H.objectEach(obj, function (val, n) { + // If the object is non-null and destroy is defined + if (val && val !== except && val.destroy) { + // Invoke the destroy + val.destroy(); + } + + // Delete the property from the object. + delete obj[n]; + }); }; @@ -1222,20 +1222,20 @@ H.destroyObjectProperties = function (obj, except) { * @function #discardElement * @memberOf Highcharts * @param {HTMLDOMElement} element - The HTML node to discard. - * + * */ H.discardElement = function (element) { - var garbageBin = H.garbageBin; - // create a garbage bin element, not part of the DOM - if (!garbageBin) { - garbageBin = H.createElement('div'); - } - - // move the node and empty bin - if (element) { - garbageBin.appendChild(element); - } - garbageBin.innerHTML = ''; + var garbageBin = H.garbageBin; + // create a garbage bin element, not part of the DOM + if (!garbageBin) { + garbageBin = H.createElement('div'); + } + + // move the node and empty bin + if (element) { + garbageBin.appendChild(element); + } + garbageBin.innerHTML = ''; }; /** @@ -1248,9 +1248,9 @@ H.discardElement = function (element) { * @returns {Number} The corrected float number. */ H.correctFloat = function (num, prec) { - return parseFloat( - num.toPrecision(prec || 14) - ); + return parseFloat( + num.toPrecision(prec || 14) + ); }; /** @@ -1261,16 +1261,16 @@ H.correctFloat = function (num, prec) { * @memberOf Highcharts * @param {Boolean|Animation} animation - The animation object. * @param {Object} chart - The chart instance. - * + * * @todo This function always relates to a chart, and sets a property on the * renderer, so it should be moved to the SVGRenderer. */ H.setAnimation = function (animation, chart) { - chart.renderer.globalAnimation = H.pick( - animation, - chart.options.chart.animation, - true - ); + chart.renderer.globalAnimation = H.pick( + animation, + chart.options.chart.animation, + true + ); }; /** @@ -1285,23 +1285,23 @@ H.setAnimation = function (animation, chart) { * @returns {AnimationOptions} An object with at least a duration property. */ H.animObject = function (animation) { - return H.isObject(animation) ? - H.merge(animation) : - { duration: animation ? 500 : 0 }; + return H.isObject(animation) ? + H.merge(animation) : + { duration: animation ? 500 : 0 }; }; /** * The time unit lookup */ H.timeUnits = { - millisecond: 1, - second: 1000, - minute: 60000, - hour: 3600000, - day: 24 * 3600000, - week: 7 * 24 * 3600000, - month: 28 * 24 * 3600000, - year: 364 * 24 * 3600000 + millisecond: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 24 * 3600000, + week: 7 * 24 * 3600000, + month: 28 * 24 * 3600000, + year: 364 * 24 * 3600000 }; /** @@ -1321,87 +1321,87 @@ H.timeUnits = { * @sample highcharts/members/highcharts-numberformat/ Custom number format */ H.numberFormat = function (number, decimals, decimalPoint, thousandsSep) { - number = +number || 0; - decimals = +decimals; - - var lang = H.defaultOptions.lang, - origDec = (number.toString().split('.')[1] || '').split('e')[0].length, - strinteger, - thousands, - ret, - roundedNumber, - exponent = number.toString().split('e'), - fractionDigits; - - if (decimals === -1) { - // Preserve decimals. Not huge numbers (#3793). - decimals = Math.min(origDec, 20); - } else if (!H.isNumber(decimals)) { - decimals = 2; - } else if (decimals && exponent[1] && exponent[1] < 0) { - // Expose decimals from exponential notation (#7042) - fractionDigits = decimals + +exponent[1]; - if (fractionDigits >= 0) { - // remove too small part of the number while keeping the notation - exponent[0] = (+exponent[0]).toExponential(fractionDigits) - .split('e')[0]; - decimals = fractionDigits; - } else { - // fractionDigits < 0 - exponent[0] = exponent[0].split('.')[0] || 0; - - if (decimals < 20) { - // use number instead of exponential notation (#7405) - number = (exponent[0] * Math.pow(10, exponent[1])) - .toFixed(decimals); - } else { - // or zero - number = 0; - } - exponent[1] = 0; - } - } - - // Add another decimal to avoid rounding errors of float numbers. (#4573) - // Then use toFixed to handle rounding. - roundedNumber = ( - Math.abs(exponent[1] ? exponent[0] : number) + - Math.pow(10, -Math.max(decimals, origDec) - 1) - ).toFixed(decimals); - - // A string containing the positive integer component of the number - strinteger = String(H.pInt(roundedNumber)); - - // Leftover after grouping into thousands. Can be 0, 1 or 3. - thousands = strinteger.length > 3 ? strinteger.length % 3 : 0; - - // Language - decimalPoint = H.pick(decimalPoint, lang.decimalPoint); - thousandsSep = H.pick(thousandsSep, lang.thousandsSep); - - // Start building the return - ret = number < 0 ? '-' : ''; - - // Add the leftover after grouping into thousands. For example, in the - // number 42 000 000, this line adds 42. - ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : ''; - - // Add the remaining thousands groups, joined by the thousands separator - ret += strinteger - .substr(thousands) - .replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); - - // Add the decimal point and the decimal component - if (decimals) { - // Get the decimal component - ret += decimalPoint + roundedNumber.slice(-decimals); - } - - if (exponent[1] && +ret !== 0) { - ret += 'e' + exponent[1]; - } - - return ret; + number = +number || 0; + decimals = +decimals; + + var lang = H.defaultOptions.lang, + origDec = (number.toString().split('.')[1] || '').split('e')[0].length, + strinteger, + thousands, + ret, + roundedNumber, + exponent = number.toString().split('e'), + fractionDigits; + + if (decimals === -1) { + // Preserve decimals. Not huge numbers (#3793). + decimals = Math.min(origDec, 20); + } else if (!H.isNumber(decimals)) { + decimals = 2; + } else if (decimals && exponent[1] && exponent[1] < 0) { + // Expose decimals from exponential notation (#7042) + fractionDigits = decimals + +exponent[1]; + if (fractionDigits >= 0) { + // remove too small part of the number while keeping the notation + exponent[0] = (+exponent[0]).toExponential(fractionDigits) + .split('e')[0]; + decimals = fractionDigits; + } else { + // fractionDigits < 0 + exponent[0] = exponent[0].split('.')[0] || 0; + + if (decimals < 20) { + // use number instead of exponential notation (#7405) + number = (exponent[0] * Math.pow(10, exponent[1])) + .toFixed(decimals); + } else { + // or zero + number = 0; + } + exponent[1] = 0; + } + } + + // Add another decimal to avoid rounding errors of float numbers. (#4573) + // Then use toFixed to handle rounding. + roundedNumber = ( + Math.abs(exponent[1] ? exponent[0] : number) + + Math.pow(10, -Math.max(decimals, origDec) - 1) + ).toFixed(decimals); + + // A string containing the positive integer component of the number + strinteger = String(H.pInt(roundedNumber)); + + // Leftover after grouping into thousands. Can be 0, 1 or 3. + thousands = strinteger.length > 3 ? strinteger.length % 3 : 0; + + // Language + decimalPoint = H.pick(decimalPoint, lang.decimalPoint); + thousandsSep = H.pick(thousandsSep, lang.thousandsSep); + + // Start building the return + ret = number < 0 ? '-' : ''; + + // Add the leftover after grouping into thousands. For example, in the + // number 42 000 000, this line adds 42. + ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : ''; + + // Add the remaining thousands groups, joined by the thousands separator + ret += strinteger + .substr(thousands) + .replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); + + // Add the decimal point and the decimal component + if (decimals) { + // Get the decimal component + ret += decimalPoint + roundedNumber.slice(-decimals); + } + + if (exponent[1] && +ret !== 0) { + ret += 'e' + exponent[1]; + } + + return ret; }; /** @@ -1410,7 +1410,7 @@ H.numberFormat = function (number, decimals, decimalPoint, thousandsSep) { * @param {Number} pos Current position, ranging from 0 to 1. */ Math.easeInOutSine = function (pos) { - return -0.5 * (Math.cos(Math.PI * pos) - 1); + return -0.5 * (Math.cos(Math.PI * pos) - 1); }; /** @@ -1427,33 +1427,33 @@ Math.easeInOutSine = function (pos) { */ H.getStyle = function (el, prop, toInt) { - var style; - - // For width and height, return the actual inner pixel size (#4913) - if (prop === 'width') { - return Math.min(el.offsetWidth, el.scrollWidth) - - H.getStyle(el, 'padding-left') - - H.getStyle(el, 'padding-right'); - } else if (prop === 'height') { - return Math.min(el.offsetHeight, el.scrollHeight) - - H.getStyle(el, 'padding-top') - - H.getStyle(el, 'padding-bottom'); - } - - if (!win.getComputedStyle) { - // SVG not supported, forgot to load oldie.js? - H.error(27, true); - } - - // Otherwise, get the computed style - style = win.getComputedStyle(el, undefined); - if (style) { - style = style.getPropertyValue(prop); - if (H.pick(toInt, prop !== 'opacity')) { - style = H.pInt(style); - } - } - return style; + var style; + + // For width and height, return the actual inner pixel size (#4913) + if (prop === 'width') { + return Math.min(el.offsetWidth, el.scrollWidth) - + H.getStyle(el, 'padding-left') - + H.getStyle(el, 'padding-right'); + } else if (prop === 'height') { + return Math.min(el.offsetHeight, el.scrollHeight) - + H.getStyle(el, 'padding-top') - + H.getStyle(el, 'padding-bottom'); + } + + if (!win.getComputedStyle) { + // SVG not supported, forgot to load oldie.js? + H.error(27, true); + } + + // Otherwise, get the computed style + style = win.getComputedStyle(el, undefined); + if (style) { + style = style.getPropertyValue(prop); + if (H.pick(toInt, prop !== 'opacity')) { + style = H.pInt(style); + } + } + return style; }; /** @@ -1467,10 +1467,10 @@ H.getStyle = function (el, prop, toInt) { * @returns {Number} - The index within the array, or -1 if not found. */ H.inArray = function (item, arr, fromIndex) { - return ( - H.indexOfPolyfill || - Array.prototype.indexOf.call(arr, item, fromIndex) - ); + return ( + H.indexOfPolyfill || + Array.prototype.indexOf.call(arr, item, fromIndex) + ); }; /** @@ -1485,11 +1485,11 @@ H.inArray = function (item, arr, fromIndex) { * @returns {Array} - A new, filtered array. */ H.grep = function (arr, callback) { - return (H.filterPolyfill || Array.prototype.filter).call(arr, callback); + return (H.filterPolyfill || Array.prototype.filter).call(arr, callback); }; /** - * Return the value of the first element in the array that satisfies the + * Return the value of the first element in the array that satisfies the * provided testing function. * * @function #find @@ -1501,20 +1501,20 @@ H.grep = function (arr, callback) { * @returns {Mixed} - The value of the element. */ H.find = Array.prototype.find ? - function (arr, callback) { - return arr.find(callback); - } : - // Legacy implementation. PhantomJS, IE <= 11 etc. #7223. - function (arr, fn) { - var i, - length = arr.length; - - for (i = 0; i < length; i++) { - if (fn(arr[i], i)) { - return arr[i]; - } - } - }; + function (arr, callback) { + return arr.find(callback); + } : + // Legacy implementation. PhantomJS, IE <= 11 etc. #7223. + function (arr, fn) { + var i, + length = arr.length; + + for (i = 0; i < length; i++) { + if (fn(arr[i], i)) { + return arr[i]; + } + } + }; /** * Test whether at least one element in the array passes the test implemented by @@ -1529,7 +1529,7 @@ H.find = Array.prototype.find ? * @param {Object} ctx The context. */ H.some = function (arr, fn, ctx) { - return (H.somePolyfill || Array.prototype.some).call(arr, fn, ctx); + return (H.somePolyfill || Array.prototype.some).call(arr, fn, ctx); }; /** @@ -1538,20 +1538,20 @@ H.some = function (arr, fn, ctx) { * @function #map * @memberOf Highcharts * @param {Array} arr - The array to map. - * @param {Function} fn - The callback function. Return the new value for the + * @param {Function} fn - The callback function. Return the new value for the * new array. * @returns {Array} - A new array item with modified items. */ H.map = function (arr, fn) { - var results = [], - i = 0, - len = arr.length; + var results = [], + i = 0, + len = arr.length; - for (; i < len; i++) { - results[i] = fn.call(arr[i], arr[i], i, arr); - } + for (; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } - return results; + return results; }; /** @@ -1563,7 +1563,7 @@ H.map = function (arr, fn) { * @returns {Array} - An array of strings that represents all the properties. */ H.keys = function (obj) { - return (H.keysPolyfill || Object.keys).call(undefined, obj); + return (H.keysPolyfill || Object.keys).call(undefined, obj); }; /** @@ -1572,18 +1572,18 @@ H.keys = function (obj) { * @function #reduce * @memberOf Highcharts * @param {Array} arr - The array to reduce. - * @param {Function} fn - The callback function. Return the reduced value. - * Receives 4 arguments: Accumulated/reduced value, current value, current + * @param {Function} fn - The callback function. Return the reduced value. + * Receives 4 arguments: Accumulated/reduced value, current value, current * array index, and the array. * @param {Mixed} initialValue - The initial value of the accumulator. * @returns {Mixed} - The reduced value. */ H.reduce = function (arr, func, initialValue) { - return (H.reducePolyfill || Array.prototype.reduce).call( - arr, - func, - initialValue - ); + return (H.reducePolyfill || Array.prototype.reduce).call( + arr, + func, + initialValue + ); }; /** @@ -1596,17 +1596,17 @@ H.reduce = function (arr, func, initialValue) { * position in the page. */ H.offset = function (el) { - var docElem = doc.documentElement, - box = el.parentElement ? // IE11 throws Unspecified error in test suite - el.getBoundingClientRect() : - { top: 0, left: 0 }; - - return { - top: box.top + (win.pageYOffset || docElem.scrollTop) - - (docElem.clientTop || 0), - left: box.left + (win.pageXOffset || docElem.scrollLeft) - - (docElem.clientLeft || 0) - }; + var docElem = doc.documentElement, + box = el.parentElement ? // IE11 throws Unspecified error in test suite + el.getBoundingClientRect() : + { top: 0, left: 0 }; + + return { + top: box.top + (win.pageYOffset || docElem.scrollTop) - + (docElem.clientTop || 0), + left: box.left + (win.pageXOffset || docElem.scrollLeft) - + (docElem.clientLeft || 0) + }; }; /** @@ -1623,18 +1623,18 @@ H.offset = function (el) { * @param {SVGElement} el - The SVGElement to stop animation on. * @param {string} [prop] - The property to stop animating. If given, the stop * method will stop a single property from animating, while others continue. - * + * */ H.stop = function (el, prop) { - var i = H.timers.length; + var i = H.timers.length; - // Remove timers related to this element (#4519) - while (i--) { - if (H.timers[i].elem === el && (!prop || prop === H.timers[i].prop)) { - H.timers[i].stopped = true; // #4667 - } - } + // Remove timers related to this element (#4519) + while (i--) { + if (H.timers[i].elem === el && (!prop || prop === H.timers[i].prop)) { + H.timers[i].stopped = true; // #4667 + } + } }; /** @@ -1650,7 +1650,7 @@ H.stop = function (el, prop) { * @param {Object} [ctx] The context. */ H.each = function (arr, fn, ctx) { // modern browsers - return (H.forEachPolyfill || Array.prototype.forEach).call(arr, fn, ctx); + return (H.forEachPolyfill || Array.prototype.forEach).call(arr, fn, ctx); }; /** @@ -1666,11 +1666,11 @@ H.each = function (arr, fn, ctx) { // modern browsers * @param {Object} ctx The context */ H.objectEach = function (obj, fn, ctx) { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - fn.call(ctx || obj[key], obj[key], key, obj); - } - } + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + fn.call(ctx || obj[key], obj[key], key, obj); + } + } }; /** @@ -1681,39 +1681,39 @@ H.objectEach = function (obj, fn, ctx) { * @param {Object} el - The element or object to add a listener to. It can be a * {@link HTMLDOMElement}, an {@link SVGElement} or any other object. * @param {String} type - The event type. - * @param {Function} fn - The function callback to execute when the event is + * @param {Function} fn - The function callback to execute when the event is * fired. * @returns {Function} A callback function to remove the added event. */ H.addEvent = function (el, type, fn) { - var events, - addEventListener = el.addEventListener || H.addEventListenerPolyfill; - - // If we're setting events directly on the constructor, use a separate - // collection, `protoEvents` to distinguish it from the item events in - // `hcEvents`. - if (typeof el === 'function' && el.prototype) { - events = el.prototype.protoEvents = el.prototype.protoEvents || {}; - } else { - events = el.hcEvents = el.hcEvents || {}; - } - - // Handle DOM events - if (addEventListener) { - addEventListener.call(el, type, fn, false); - } - - if (!events[type]) { - events[type] = []; - } - - events[type].push(fn); - - // Return a function that can be called to remove this event. - return function () { - H.removeEvent(el, type, fn); - }; + var events, + addEventListener = el.addEventListener || H.addEventListenerPolyfill; + + // If we're setting events directly on the constructor, use a separate + // collection, `protoEvents` to distinguish it from the item events in + // `hcEvents`. + if (typeof el === 'function' && el.prototype) { + events = el.prototype.protoEvents = el.prototype.protoEvents || {}; + } else { + events = el.hcEvents = el.hcEvents || {}; + } + + // Handle DOM events + if (addEventListener) { + addEventListener.call(el, type, fn, false); + } + + if (!events[type]) { + events[type] = []; + } + + events[type].push(fn); + + // Return a function that can be called to remove this event. + return function () { + H.removeEvent(el, type, fn); + }; }; /** @@ -1726,70 +1726,70 @@ H.addEvent = function (el, type, fn) { * events are removed from the element. * @param {Function} [fn] - The specific callback to remove. If undefined, all * events that match the element and optionally the type are removed. - * + * */ H.removeEvent = function (el, type, fn) { - - var events, - index; - - function removeOneEvent(type, fn) { - var removeEventListener = - el.removeEventListener || H.removeEventListenerPolyfill; - - if (removeEventListener) { - removeEventListener.call(el, type, fn, false); - } - } - - function removeAllEvents(eventCollection) { - var types, - len; - - if (!el.nodeName) { - return; // break on non-DOM events - } - - if (type) { - types = {}; - types[type] = true; - } else { - types = eventCollection; - } - - H.objectEach(types, function (val, n) { - if (eventCollection[n]) { - len = eventCollection[n].length; - while (len--) { - removeOneEvent(n, eventCollection[n][len]); - } - } - }); - } - - H.each(['protoEvents', 'hcEvents'], function (coll) { - var eventCollection = el[coll]; - if (eventCollection) { - if (type) { - events = eventCollection[type] || []; - if (fn) { - index = H.inArray(fn, events); - if (index > -1) { - events.splice(index, 1); - eventCollection[type] = events; - } - removeOneEvent(type, fn); - - } else { - removeAllEvents(eventCollection); - eventCollection[type] = []; - } - } else { - removeAllEvents(eventCollection); - el[coll] = {}; - } - } - }); + + var events, + index; + + function removeOneEvent(type, fn) { + var removeEventListener = + el.removeEventListener || H.removeEventListenerPolyfill; + + if (removeEventListener) { + removeEventListener.call(el, type, fn, false); + } + } + + function removeAllEvents(eventCollection) { + var types, + len; + + if (!el.nodeName) { + return; // break on non-DOM events + } + + if (type) { + types = {}; + types[type] = true; + } else { + types = eventCollection; + } + + H.objectEach(types, function (val, n) { + if (eventCollection[n]) { + len = eventCollection[n].length; + while (len--) { + removeOneEvent(n, eventCollection[n][len]); + } + } + }); + } + + H.each(['protoEvents', 'hcEvents'], function (coll) { + var eventCollection = el[coll]; + if (eventCollection) { + if (type) { + events = eventCollection[type] || []; + if (fn) { + index = H.inArray(fn, events); + if (index > -1) { + events.splice(index, 1); + eventCollection[type] = events; + } + removeOneEvent(type, fn); + + } else { + removeAllEvents(eventCollection); + eventCollection[type] = []; + } + } else { + removeAllEvents(eventCollection); + el[coll] = {}; + } + } + }); }; /** @@ -1802,76 +1802,76 @@ H.removeEvent = function (el, type, fn) { * @param {String} type - The type of event. * @param {Object} [eventArguments] - Custom event arguments that are passed on * as an argument to the event handler. - * @param {Function} [defaultFunction] - The default function to execute if the + * @param {Function} [defaultFunction] - The default function to execute if the * other listeners haven't returned false. - * + * */ H.fireEvent = function (el, type, eventArguments, defaultFunction) { - var e, - events, - len, - i, - fn; - - eventArguments = eventArguments || {}; - - if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { - e = doc.createEvent('Events'); - e.initEvent(type, true, true); - - H.extend(e, eventArguments); - - if (el.dispatchEvent) { - el.dispatchEvent(e); - } else { - el.fireEvent(type, e); - } - - } else { - - H.each(['protoEvents', 'hcEvents'], function (coll) { - - if (el[coll]) { - events = el[coll][type] || []; - len = events.length; - - if (!eventArguments.target) { // We're running a custom event - - H.extend(eventArguments, { - // Attach a simple preventDefault function to skip - // default handler if called. The built-in - // defaultPrevented property is not overwritable (#5112) - preventDefault: function () { - eventArguments.defaultPrevented = true; - }, - // Setting target to native events fails with clicking - // the zoom-out button in Chrome. - target: el, - // If the type is not set, we're running a custom event - // (#2297). If it is set, we're running a browser event, - // and setting it will cause en error in IE8 (#2465). - type: type - }); - } - - - for (i = 0; i < len; i++) { - fn = events[i]; - - // If the event handler return false, prevent the default - // handler from executing - if (fn && fn.call(el, eventArguments) === false) { - eventArguments.preventDefault(); - } - } - } - }); - } - - // Run the default if not prevented - if (defaultFunction && !eventArguments.defaultPrevented) { - defaultFunction.call(el, eventArguments); - } + var e, + events, + len, + i, + fn; + + eventArguments = eventArguments || {}; + + if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { + e = doc.createEvent('Events'); + e.initEvent(type, true, true); + + H.extend(e, eventArguments); + + if (el.dispatchEvent) { + el.dispatchEvent(e); + } else { + el.fireEvent(type, e); + } + + } else { + + H.each(['protoEvents', 'hcEvents'], function (coll) { + + if (el[coll]) { + events = el[coll][type] || []; + len = events.length; + + if (!eventArguments.target) { // We're running a custom event + + H.extend(eventArguments, { + // Attach a simple preventDefault function to skip + // default handler if called. The built-in + // defaultPrevented property is not overwritable (#5112) + preventDefault: function () { + eventArguments.defaultPrevented = true; + }, + // Setting target to native events fails with clicking + // the zoom-out button in Chrome. + target: el, + // If the type is not set, we're running a custom event + // (#2297). If it is set, we're running a browser event, + // and setting it will cause en error in IE8 (#2465). + type: type + }); + } + + + for (i = 0; i < len; i++) { + fn = events[i]; + + // If the event handler return false, prevent the default + // handler from executing + if (fn && fn.call(el, eventArguments) === false) { + eventArguments.preventDefault(); + } + } + } + }); + } + + // Run the default if not prevented + if (defaultFunction && !eventArguments.defaultPrevented) { + defaultFunction.call(el, eventArguments); + } }; /** @@ -1902,61 +1902,61 @@ H.fireEvent = function (el, type, eventArguments, defaultFunction) { * @param {AnimationOptions} [opt] - Animation options. */ H.animate = function (el, params, opt) { - var start, - unit = '', - end, - fx, - args; - - if (!H.isObject(opt)) { // Number or undefined/null - args = arguments; - opt = { - duration: args[2], - easing: args[3], - complete: args[4] - }; - } - if (!H.isNumber(opt.duration)) { - opt.duration = 400; - } - opt.easing = typeof opt.easing === 'function' ? - opt.easing : - (Math[opt.easing] || Math.easeInOutSine); - opt.curAnim = H.merge(params); - - H.objectEach(params, function (val, prop) { - // Stop current running animation of this property - H.stop(el, prop); - - fx = new H.Fx(el, opt, prop); - end = null; - - if (prop === 'd') { - fx.paths = fx.initPath( - el, - el.d, - params.d - ); - fx.toD = params.d; - start = 0; - end = 1; - } else if (el.attr) { - start = el.attr(prop); - } else { - start = parseFloat(H.getStyle(el, prop)) || 0; - if (prop !== 'opacity') { - unit = 'px'; - } - } - - if (!end) { - end = val; - } - if (end && end.match && end.match('px')) { - end = end.replace(/px/g, ''); // #4351 - } - fx.run(start, end, unit); - }); + var start, + unit = '', + end, + fx, + args; + + if (!H.isObject(opt)) { // Number or undefined/null + args = arguments; + opt = { + duration: args[2], + easing: args[3], + complete: args[4] + }; + } + if (!H.isNumber(opt.duration)) { + opt.duration = 400; + } + opt.easing = typeof opt.easing === 'function' ? + opt.easing : + (Math[opt.easing] || Math.easeInOutSine); + opt.curAnim = H.merge(params); + + H.objectEach(params, function (val, prop) { + // Stop current running animation of this property + H.stop(el, prop); + + fx = new H.Fx(el, opt, prop); + end = null; + + if (prop === 'd') { + fx.paths = fx.initPath( + el, + el.d, + params.d + ); + fx.toD = params.d; + start = 0; + end = 1; + } else if (el.attr) { + start = el.attr(prop); + } else { + start = parseFloat(H.getStyle(el, prop)) || 0; + if (prop !== 'opacity') { + unit = 'px'; + } + } + + if (!end) { + end = val; + } + if (end && end.match && end.match('px')) { + end = end.replace(/px/g, ''); // #4351 + } + fx.run(start, end, unit); + }); }; /** @@ -1979,32 +1979,32 @@ H.animate = function (el, params, opt) { */ // docs: add to API + extending Highcharts H.seriesType = function (type, parent, options, props, pointProps) { - var defaultOptions = H.getOptions(), - seriesTypes = H.seriesTypes; - - // Merge the options - defaultOptions.plotOptions[type] = H.merge( - defaultOptions.plotOptions[parent], - options - ); - - // Create the class - seriesTypes[type] = H.extendClass(seriesTypes[parent] || - function () {}, props); - seriesTypes[type].prototype.type = type; - - // Create the point class if needed - if (pointProps) { - seriesTypes[type].prototype.pointClass = - H.extendClass(H.Point, pointProps); - } - - return seriesTypes[type]; + var defaultOptions = H.getOptions(), + seriesTypes = H.seriesTypes; + + // Merge the options + defaultOptions.plotOptions[type] = H.merge( + defaultOptions.plotOptions[parent], + options + ); + + // Create the class + seriesTypes[type] = H.extendClass(seriesTypes[parent] || + function () {}, props); + seriesTypes[type].prototype.type = type; + + // Create the point class if needed + if (pointProps) { + seriesTypes[type].prototype.pointClass = + H.extendClass(H.Point, pointProps); + } + + return seriesTypes[type]; }; /** * Get a unique key for using in internal element id's and pointers. The key - * is composed of a random hash specific to this Highcharts instance, and a + * is composed of a random hash specific to this Highcharts instance, and a * counter. * @function #uniqueKey * @memberOf Highcharts @@ -2013,36 +2013,36 @@ H.seriesType = function (type, parent, options, props, pointProps) { * var id = H.uniqueKey(); // => 'highcharts-x45f6hp-0' */ H.uniqueKey = (function () { - - var uniqueKeyHash = Math.random().toString(36).substring(2, 9), - idCounter = 0; - return function () { - return 'highcharts-' + uniqueKeyHash + '-' + idCounter++; - }; + var uniqueKeyHash = Math.random().toString(36).substring(2, 9), + idCounter = 0; + + return function () { + return 'highcharts-' + uniqueKeyHash + '-' + idCounter++; + }; }()); /** * Register Highcharts as a plugin in jQuery */ if (win.jQuery) { - win.jQuery.fn.highcharts = function () { - var args = [].slice.call(arguments); - - if (this[0]) { // this[0] is the renderTo div - - // Create the chart - if (args[0]) { - new H[ // eslint-disable-line no-new - // Constructor defaults to Chart - H.isString(args[0]) ? args.shift() : 'Chart' - ](this[0], args[0], args[1]); - return this; - } - - // When called without parameters or with the return argument, - // return an existing chart - return charts[H.attr(this[0], 'data-highcharts-chart')]; - } - }; + win.jQuery.fn.highcharts = function () { + var args = [].slice.call(arguments); + + if (this[0]) { // this[0] is the renderTo div + + // Create the chart + if (args[0]) { + new H[ // eslint-disable-line no-new + // Constructor defaults to Chart + H.isString(args[0]) ? args.shift() : 'Chart' + ](this[0], args[0], args[1]); + return this; + } + + // When called without parameters or with the return argument, + // return an existing chart + return charts[H.attr(this[0], 'data-highcharts-chart')]; + } + }; } diff --git a/js/themes/avocado.js b/js/themes/avocado.js index c2396527396..36e6eb60d3c 100644 --- a/js/themes/avocado.js +++ b/js/themes/avocado.js @@ -2,8 +2,8 @@ * (c) 2010-2017 Highsoft AS * * License: www.highcharts.com/license - * - * Accessible high-contrast theme for Highcharts. Considers colorblindness and + * + * Accessible high-contrast theme for Highcharts. Considers colorblindness and * monochrome rendering. * @author Øystein Moseng */ @@ -11,26 +11,26 @@ 'use strict'; import Highcharts from '../parts/Globals.js'; Highcharts.theme = { - colors: ['#F3E796', '#95C471', '#35729E', '#251735'], + colors: ['#F3E796', '#95C471', '#35729E', '#251735'], - colorAxis: { - maxColor: '#05426E', - minColor: '#F3E796' - }, + colorAxis: { + maxColor: '#05426E', + minColor: '#F3E796' + }, - plotOptions: { - map: { - nullColor: '#fcfefe' - } - }, + plotOptions: { + map: { + nullColor: '#fcfefe' + } + }, - navigator: { - maskFill: 'rgba(170, 205, 170, 0.5)', - series: { - color: '#95C471', - lineColor: '#35729E' - } - } + navigator: { + maskFill: 'rgba(170, 205, 170, 0.5)', + series: { + color: '#95C471', + lineColor: '#35729E' + } + } }; // Apply the theme diff --git a/js/themes/dark-blue.js b/js/themes/dark-blue.js index 14638baf027..dae291e2ece 100644 --- a/js/themes/dark-blue.js +++ b/js/themes/dark-blue.js @@ -9,250 +9,250 @@ 'use strict'; import Highcharts from '../parts/Globals.js'; Highcharts.theme = { - colors: ['#DDDF0D', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee', - '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], - chart: { - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 }, - stops: [ - [0, 'rgb(48, 48, 96)'], - [1, 'rgb(0, 0, 0)'] - ] - }, - borderColor: '#000000', - borderWidth: 2, - className: 'dark-container', - plotBackgroundColor: 'rgba(255, 255, 255, .1)', - plotBorderColor: '#CCCCCC', - plotBorderWidth: 1 - }, - title: { - style: { - color: '#C0C0C0', - font: 'bold 16px "Trebuchet MS", Verdana, sans-serif' - } - }, - subtitle: { - style: { - color: '#666666', - font: 'bold 12px "Trebuchet MS", Verdana, sans-serif' - } - }, - xAxis: { - gridLineColor: '#333333', - gridLineWidth: 1, - labels: { - style: { - color: '#A0A0A0' - } - }, - lineColor: '#A0A0A0', - tickColor: '#A0A0A0', - title: { - style: { - color: '#CCC', - fontWeight: 'bold', - fontSize: '12px', - fontFamily: 'Trebuchet MS, Verdana, sans-serif' + colors: ['#DDDF0D', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee', + '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], + chart: { + backgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 }, + stops: [ + [0, 'rgb(48, 48, 96)'], + [1, 'rgb(0, 0, 0)'] + ] + }, + borderColor: '#000000', + borderWidth: 2, + className: 'dark-container', + plotBackgroundColor: 'rgba(255, 255, 255, .1)', + plotBorderColor: '#CCCCCC', + plotBorderWidth: 1 + }, + title: { + style: { + color: '#C0C0C0', + font: 'bold 16px "Trebuchet MS", Verdana, sans-serif' + } + }, + subtitle: { + style: { + color: '#666666', + font: 'bold 12px "Trebuchet MS", Verdana, sans-serif' + } + }, + xAxis: { + gridLineColor: '#333333', + gridLineWidth: 1, + labels: { + style: { + color: '#A0A0A0' + } + }, + lineColor: '#A0A0A0', + tickColor: '#A0A0A0', + title: { + style: { + color: '#CCC', + fontWeight: 'bold', + fontSize: '12px', + fontFamily: 'Trebuchet MS, Verdana, sans-serif' - } - } - }, - yAxis: { - gridLineColor: '#333333', - labels: { - style: { - color: '#A0A0A0' - } - }, - lineColor: '#A0A0A0', - minorTickInterval: null, - tickColor: '#A0A0A0', - tickWidth: 1, - title: { - style: { - color: '#CCC', - fontWeight: 'bold', - fontSize: '12px', - fontFamily: 'Trebuchet MS, Verdana, sans-serif' - } - } - }, - tooltip: { - backgroundColor: 'rgba(0, 0, 0, 0.75)', - style: { - color: '#F0F0F0' - } - }, - toolbar: { - itemStyle: { - color: 'silver' - } - }, - plotOptions: { - line: { - dataLabels: { - color: '#CCC' - }, - marker: { - lineColor: '#333' - } - }, - spline: { - marker: { - lineColor: '#333' - } - }, - scatter: { - marker: { - lineColor: '#333' - } - }, - candlestick: { - lineColor: 'white' - } - }, - legend: { - itemStyle: { - font: '9pt Trebuchet MS, Verdana, sans-serif', - color: '#A0A0A0' - }, - itemHoverStyle: { - color: '#FFF' - }, - itemHiddenStyle: { - color: '#444' - } - }, - credits: { - style: { - color: '#666' - } - }, - labels: { - style: { - color: '#CCC' - } - }, + } + } + }, + yAxis: { + gridLineColor: '#333333', + labels: { + style: { + color: '#A0A0A0' + } + }, + lineColor: '#A0A0A0', + minorTickInterval: null, + tickColor: '#A0A0A0', + tickWidth: 1, + title: { + style: { + color: '#CCC', + fontWeight: 'bold', + fontSize: '12px', + fontFamily: 'Trebuchet MS, Verdana, sans-serif' + } + } + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + style: { + color: '#F0F0F0' + } + }, + toolbar: { + itemStyle: { + color: 'silver' + } + }, + plotOptions: { + line: { + dataLabels: { + color: '#CCC' + }, + marker: { + lineColor: '#333' + } + }, + spline: { + marker: { + lineColor: '#333' + } + }, + scatter: { + marker: { + lineColor: '#333' + } + }, + candlestick: { + lineColor: 'white' + } + }, + legend: { + itemStyle: { + font: '9pt Trebuchet MS, Verdana, sans-serif', + color: '#A0A0A0' + }, + itemHoverStyle: { + color: '#FFF' + }, + itemHiddenStyle: { + color: '#444' + } + }, + credits: { + style: { + color: '#666' + } + }, + labels: { + style: { + color: '#CCC' + } + }, - navigation: { - buttonOptions: { - symbolStroke: '#DDDDDD', - hoverSymbolStroke: '#FFFFFF', - theme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#606060'], - [0.6, '#333333'] - ] - }, - stroke: '#000000' - } - } - }, + navigation: { + buttonOptions: { + symbolStroke: '#DDDDDD', + hoverSymbolStroke: '#FFFFFF', + theme: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#606060'], + [0.6, '#333333'] + ] + }, + stroke: '#000000' + } + } + }, - // scroll charts - rangeSelector: { - buttonTheme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - stroke: '#000000', - style: { - color: '#CCC', - fontWeight: 'bold' - }, - states: { - hover: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#BBB'], - [0.6, '#888'] - ] - }, - stroke: '#000000', - style: { - color: 'white' - } - }, - select: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.1, '#000'], - [0.3, '#333'] - ] - }, - stroke: '#000000', - style: { - color: 'yellow' - } - } - } - }, - inputStyle: { - backgroundColor: '#333', - color: 'silver' - }, - labelStyle: { - color: 'silver' - } - }, + // scroll charts + rangeSelector: { + buttonTheme: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + stroke: '#000000', + style: { + color: '#CCC', + fontWeight: 'bold' + }, + states: { + hover: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#BBB'], + [0.6, '#888'] + ] + }, + stroke: '#000000', + style: { + color: 'white' + } + }, + select: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.1, '#000'], + [0.3, '#333'] + ] + }, + stroke: '#000000', + style: { + color: 'yellow' + } + } + } + }, + inputStyle: { + backgroundColor: '#333', + color: 'silver' + }, + labelStyle: { + color: 'silver' + } + }, - navigator: { - handles: { - backgroundColor: '#666', - borderColor: '#AAA' - }, - outlineColor: '#CCC', - maskFill: 'rgba(16, 16, 16, 0.5)', - series: { - color: '#7798BF', - lineColor: '#A6C7ED' - } - }, + navigator: { + handles: { + backgroundColor: '#666', + borderColor: '#AAA' + }, + outlineColor: '#CCC', + maskFill: 'rgba(16, 16, 16, 0.5)', + series: { + color: '#7798BF', + lineColor: '#A6C7ED' + } + }, - scrollbar: { - barBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - barBorderColor: '#CCC', - buttonArrowColor: '#CCC', - buttonBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - buttonBorderColor: '#CCC', - rifleColor: '#FFF', - trackBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, '#000'], - [1, '#333'] - ] - }, - trackBorderColor: '#666' - }, + scrollbar: { + barBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + barBorderColor: '#CCC', + buttonArrowColor: '#CCC', + buttonBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + buttonBorderColor: '#CCC', + rifleColor: '#FFF', + trackBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0, '#000'], + [1, '#333'] + ] + }, + trackBorderColor: '#666' + }, - // special colors for some of the - legendBackgroundColor: 'rgba(0, 0, 0, 0.5)', - background2: 'rgb(35, 35, 70)', - dataLabelsColor: '#444', - textColor: '#C0C0C0', - maskColor: 'rgba(255,255,255,0.3)' + // special colors for some of the + legendBackgroundColor: 'rgba(0, 0, 0, 0.5)', + background2: 'rgb(35, 35, 70)', + dataLabelsColor: '#444', + textColor: '#C0C0C0', + maskColor: 'rgba(255,255,255,0.3)' }; // Apply the theme diff --git a/js/themes/dark-green.js b/js/themes/dark-green.js index ec90d25e5e1..93adfe81ac4 100644 --- a/js/themes/dark-green.js +++ b/js/themes/dark-green.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license - * + * * Dark blue theme for Highcharts JS * @author Torstein Honsi */ @@ -10,251 +10,251 @@ 'use strict'; import Highcharts from '../parts/Globals.js'; Highcharts.theme = { - colors: ['#DDDF0D', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee', - '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], - chart: { - backgroundColor: { - linearGradient: [0, 0, 250, 500], - stops: [ - [0, 'rgb(48, 96, 48)'], - [1, 'rgb(0, 0, 0)'] - ] - }, - borderColor: '#000000', - borderWidth: 2, - className: 'dark-container', - plotBackgroundColor: 'rgba(255, 255, 255, .1)', - plotBorderColor: '#CCCCCC', - plotBorderWidth: 1 - }, - title: { - style: { - color: '#C0C0C0', - font: 'bold 16px "Trebuchet MS", Verdana, sans-serif' - } - }, - subtitle: { - style: { - color: '#666666', - font: 'bold 12px "Trebuchet MS", Verdana, sans-serif' - } - }, - xAxis: { - gridLineColor: '#333333', - gridLineWidth: 1, - labels: { - style: { - color: '#A0A0A0' - } - }, - lineColor: '#A0A0A0', - tickColor: '#A0A0A0', - title: { - style: { - color: '#CCC', - fontWeight: 'bold', - fontSize: '12px', - fontFamily: 'Trebuchet MS, Verdana, sans-serif' + colors: ['#DDDF0D', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee', + '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], + chart: { + backgroundColor: { + linearGradient: [0, 0, 250, 500], + stops: [ + [0, 'rgb(48, 96, 48)'], + [1, 'rgb(0, 0, 0)'] + ] + }, + borderColor: '#000000', + borderWidth: 2, + className: 'dark-container', + plotBackgroundColor: 'rgba(255, 255, 255, .1)', + plotBorderColor: '#CCCCCC', + plotBorderWidth: 1 + }, + title: { + style: { + color: '#C0C0C0', + font: 'bold 16px "Trebuchet MS", Verdana, sans-serif' + } + }, + subtitle: { + style: { + color: '#666666', + font: 'bold 12px "Trebuchet MS", Verdana, sans-serif' + } + }, + xAxis: { + gridLineColor: '#333333', + gridLineWidth: 1, + labels: { + style: { + color: '#A0A0A0' + } + }, + lineColor: '#A0A0A0', + tickColor: '#A0A0A0', + title: { + style: { + color: '#CCC', + fontWeight: 'bold', + fontSize: '12px', + fontFamily: 'Trebuchet MS, Verdana, sans-serif' - } - } - }, - yAxis: { - gridLineColor: '#333333', - labels: { - style: { - color: '#A0A0A0' - } - }, - lineColor: '#A0A0A0', - minorTickInterval: null, - tickColor: '#A0A0A0', - tickWidth: 1, - title: { - style: { - color: '#CCC', - fontWeight: 'bold', - fontSize: '12px', - fontFamily: 'Trebuchet MS, Verdana, sans-serif' - } - } - }, - tooltip: { - backgroundColor: 'rgba(0, 0, 0, 0.75)', - style: { - color: '#F0F0F0' - } - }, - toolbar: { - itemStyle: { - color: 'silver' - } - }, - plotOptions: { - line: { - dataLabels: { - color: '#CCC' - }, - marker: { - lineColor: '#333' - } - }, - spline: { - marker: { - lineColor: '#333' - } - }, - scatter: { - marker: { - lineColor: '#333' - } - }, - candlestick: { - lineColor: 'white' - } - }, - legend: { - itemStyle: { - font: '9pt Trebuchet MS, Verdana, sans-serif', - color: '#A0A0A0' - }, - itemHoverStyle: { - color: '#FFF' - }, - itemHiddenStyle: { - color: '#444' - } - }, - credits: { - style: { - color: '#666' - } - }, - labels: { - style: { - color: '#CCC' - } - }, + } + } + }, + yAxis: { + gridLineColor: '#333333', + labels: { + style: { + color: '#A0A0A0' + } + }, + lineColor: '#A0A0A0', + minorTickInterval: null, + tickColor: '#A0A0A0', + tickWidth: 1, + title: { + style: { + color: '#CCC', + fontWeight: 'bold', + fontSize: '12px', + fontFamily: 'Trebuchet MS, Verdana, sans-serif' + } + } + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + style: { + color: '#F0F0F0' + } + }, + toolbar: { + itemStyle: { + color: 'silver' + } + }, + plotOptions: { + line: { + dataLabels: { + color: '#CCC' + }, + marker: { + lineColor: '#333' + } + }, + spline: { + marker: { + lineColor: '#333' + } + }, + scatter: { + marker: { + lineColor: '#333' + } + }, + candlestick: { + lineColor: 'white' + } + }, + legend: { + itemStyle: { + font: '9pt Trebuchet MS, Verdana, sans-serif', + color: '#A0A0A0' + }, + itemHoverStyle: { + color: '#FFF' + }, + itemHiddenStyle: { + color: '#444' + } + }, + credits: { + style: { + color: '#666' + } + }, + labels: { + style: { + color: '#CCC' + } + }, - navigation: { - buttonOptions: { - symbolStroke: '#DDDDDD', - hoverSymbolStroke: '#FFFFFF', - theme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#606060'], - [0.6, '#333333'] - ] - }, - stroke: '#000000' - } - } - }, + navigation: { + buttonOptions: { + symbolStroke: '#DDDDDD', + hoverSymbolStroke: '#FFFFFF', + theme: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#606060'], + [0.6, '#333333'] + ] + }, + stroke: '#000000' + } + } + }, - // scroll charts - rangeSelector: { - buttonTheme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - stroke: '#000000', - style: { - color: '#CCC', - fontWeight: 'bold' - }, - states: { - hover: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#BBB'], - [0.6, '#888'] - ] - }, - stroke: '#000000', - style: { - color: 'white' - } - }, - select: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.1, '#000'], - [0.3, '#333'] - ] - }, - stroke: '#000000', - style: { - color: 'yellow' - } - } - } - }, - inputStyle: { - backgroundColor: '#333', - color: 'silver' - }, - labelStyle: { - color: 'silver' - } - }, + // scroll charts + rangeSelector: { + buttonTheme: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + stroke: '#000000', + style: { + color: '#CCC', + fontWeight: 'bold' + }, + states: { + hover: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#BBB'], + [0.6, '#888'] + ] + }, + stroke: '#000000', + style: { + color: 'white' + } + }, + select: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.1, '#000'], + [0.3, '#333'] + ] + }, + stroke: '#000000', + style: { + color: 'yellow' + } + } + } + }, + inputStyle: { + backgroundColor: '#333', + color: 'silver' + }, + labelStyle: { + color: 'silver' + } + }, - navigator: { - handles: { - backgroundColor: '#666', - borderColor: '#AAA' - }, - outlineColor: '#CCC', - maskFill: 'rgba(16, 16, 16, 0.5)', - series: { - color: '#7798BF', - lineColor: '#A6C7ED' - } - }, + navigator: { + handles: { + backgroundColor: '#666', + borderColor: '#AAA' + }, + outlineColor: '#CCC', + maskFill: 'rgba(16, 16, 16, 0.5)', + series: { + color: '#7798BF', + lineColor: '#A6C7ED' + } + }, - scrollbar: { - barBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - barBorderColor: '#CCC', - buttonArrowColor: '#CCC', - buttonBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - buttonBorderColor: '#CCC', - rifleColor: '#FFF', - trackBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, '#000'], - [1, '#333'] - ] - }, - trackBorderColor: '#666' - }, + scrollbar: { + barBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + barBorderColor: '#CCC', + buttonArrowColor: '#CCC', + buttonBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + buttonBorderColor: '#CCC', + rifleColor: '#FFF', + trackBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0, '#000'], + [1, '#333'] + ] + }, + trackBorderColor: '#666' + }, - // special colors for some of the - legendBackgroundColor: 'rgba(0, 0, 0, 0.5)', - background2: 'rgb(35, 35, 70)', - dataLabelsColor: '#444', - textColor: '#C0C0C0', - maskColor: 'rgba(255,255,255,0.3)' + // special colors for some of the + legendBackgroundColor: 'rgba(0, 0, 0, 0.5)', + background2: 'rgb(35, 35, 70)', + dataLabelsColor: '#444', + textColor: '#C0C0C0', + maskColor: 'rgba(255,255,255,0.3)' }; // Apply the theme diff --git a/js/themes/dark-unica.js b/js/themes/dark-unica.js index 27fb8bd5ea3..9d2914be542 100644 --- a/js/themes/dark-unica.js +++ b/js/themes/dark-unica.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license - * + * * Dark theme for Highcharts JS * @author Torstein Honsi */ @@ -12,208 +12,208 @@ // Load the fonts import Highcharts from '../parts/Globals.js'; Highcharts.createElement('link', { - href: 'https://fonts.googleapis.com/css?family=Unica+One', - rel: 'stylesheet', - type: 'text/css' + href: 'https://fonts.googleapis.com/css?family=Unica+One', + rel: 'stylesheet', + type: 'text/css' }, null, document.getElementsByTagName('head')[0]); Highcharts.theme = { - colors: ['#2b908f', '#90ee7e', '#f45b5b', '#7798BF', '#aaeeee', '#ff0066', - '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], - chart: { - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 }, - stops: [ - [0, '#2a2a2b'], - [1, '#3e3e40'] - ] - }, - style: { - fontFamily: '\'Unica One\', sans-serif' - }, - plotBorderColor: '#606063' - }, - title: { - style: { - color: '#E0E0E3', - textTransform: 'uppercase', - fontSize: '20px' - } - }, - subtitle: { - style: { - color: '#E0E0E3', - textTransform: 'uppercase' - } - }, - xAxis: { - gridLineColor: '#707073', - labels: { - style: { - color: '#E0E0E3' - } - }, - lineColor: '#707073', - minorGridLineColor: '#505053', - tickColor: '#707073', - title: { - style: { - color: '#A0A0A3' + colors: ['#2b908f', '#90ee7e', '#f45b5b', '#7798BF', '#aaeeee', '#ff0066', + '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], + chart: { + backgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 }, + stops: [ + [0, '#2a2a2b'], + [1, '#3e3e40'] + ] + }, + style: { + fontFamily: '\'Unica One\', sans-serif' + }, + plotBorderColor: '#606063' + }, + title: { + style: { + color: '#E0E0E3', + textTransform: 'uppercase', + fontSize: '20px' + } + }, + subtitle: { + style: { + color: '#E0E0E3', + textTransform: 'uppercase' + } + }, + xAxis: { + gridLineColor: '#707073', + labels: { + style: { + color: '#E0E0E3' + } + }, + lineColor: '#707073', + minorGridLineColor: '#505053', + tickColor: '#707073', + title: { + style: { + color: '#A0A0A3' - } - } - }, - yAxis: { - gridLineColor: '#707073', - labels: { - style: { - color: '#E0E0E3' - } - }, - lineColor: '#707073', - minorGridLineColor: '#505053', - tickColor: '#707073', - tickWidth: 1, - title: { - style: { - color: '#A0A0A3' - } - } - }, - tooltip: { - backgroundColor: 'rgba(0, 0, 0, 0.85)', - style: { - color: '#F0F0F0' - } - }, - plotOptions: { - series: { - dataLabels: { - color: '#B0B0B3' - }, - marker: { - lineColor: '#333' - } - }, - boxplot: { - fillColor: '#505053' - }, - candlestick: { - lineColor: 'white' - }, - errorbar: { - color: 'white' - } - }, - legend: { - itemStyle: { - color: '#E0E0E3' - }, - itemHoverStyle: { - color: '#FFF' - }, - itemHiddenStyle: { - color: '#606063' - } - }, - credits: { - style: { - color: '#666' - } - }, - labels: { - style: { - color: '#707073' - } - }, + } + } + }, + yAxis: { + gridLineColor: '#707073', + labels: { + style: { + color: '#E0E0E3' + } + }, + lineColor: '#707073', + minorGridLineColor: '#505053', + tickColor: '#707073', + tickWidth: 1, + title: { + style: { + color: '#A0A0A3' + } + } + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.85)', + style: { + color: '#F0F0F0' + } + }, + plotOptions: { + series: { + dataLabels: { + color: '#B0B0B3' + }, + marker: { + lineColor: '#333' + } + }, + boxplot: { + fillColor: '#505053' + }, + candlestick: { + lineColor: 'white' + }, + errorbar: { + color: 'white' + } + }, + legend: { + itemStyle: { + color: '#E0E0E3' + }, + itemHoverStyle: { + color: '#FFF' + }, + itemHiddenStyle: { + color: '#606063' + } + }, + credits: { + style: { + color: '#666' + } + }, + labels: { + style: { + color: '#707073' + } + }, - drilldown: { - activeAxisLabelStyle: { - color: '#F0F0F3' - }, - activeDataLabelStyle: { - color: '#F0F0F3' - } - }, + drilldown: { + activeAxisLabelStyle: { + color: '#F0F0F3' + }, + activeDataLabelStyle: { + color: '#F0F0F3' + } + }, - navigation: { - buttonOptions: { - symbolStroke: '#DDDDDD', - theme: { - fill: '#505053' - } - } - }, + navigation: { + buttonOptions: { + symbolStroke: '#DDDDDD', + theme: { + fill: '#505053' + } + } + }, - // scroll charts - rangeSelector: { - buttonTheme: { - fill: '#505053', - stroke: '#000000', - style: { - color: '#CCC' - }, - states: { - hover: { - fill: '#707073', - stroke: '#000000', - style: { - color: 'white' - } - }, - select: { - fill: '#000003', - stroke: '#000000', - style: { - color: 'white' - } - } - } - }, - inputBoxBorderColor: '#505053', - inputStyle: { - backgroundColor: '#333', - color: 'silver' - }, - labelStyle: { - color: 'silver' - } - }, + // scroll charts + rangeSelector: { + buttonTheme: { + fill: '#505053', + stroke: '#000000', + style: { + color: '#CCC' + }, + states: { + hover: { + fill: '#707073', + stroke: '#000000', + style: { + color: 'white' + } + }, + select: { + fill: '#000003', + stroke: '#000000', + style: { + color: 'white' + } + } + } + }, + inputBoxBorderColor: '#505053', + inputStyle: { + backgroundColor: '#333', + color: 'silver' + }, + labelStyle: { + color: 'silver' + } + }, - navigator: { - handles: { - backgroundColor: '#666', - borderColor: '#AAA' - }, - outlineColor: '#CCC', - maskFill: 'rgba(255,255,255,0.1)', - series: { - color: '#7798BF', - lineColor: '#A6C7ED' - }, - xAxis: { - gridLineColor: '#505053' - } - }, + navigator: { + handles: { + backgroundColor: '#666', + borderColor: '#AAA' + }, + outlineColor: '#CCC', + maskFill: 'rgba(255,255,255,0.1)', + series: { + color: '#7798BF', + lineColor: '#A6C7ED' + }, + xAxis: { + gridLineColor: '#505053' + } + }, - scrollbar: { - barBackgroundColor: '#808083', - barBorderColor: '#808083', - buttonArrowColor: '#CCC', - buttonBackgroundColor: '#606063', - buttonBorderColor: '#606063', - rifleColor: '#FFF', - trackBackgroundColor: '#404043', - trackBorderColor: '#404043' - }, + scrollbar: { + barBackgroundColor: '#808083', + barBorderColor: '#808083', + buttonArrowColor: '#CCC', + buttonBackgroundColor: '#606063', + buttonBorderColor: '#606063', + rifleColor: '#FFF', + trackBackgroundColor: '#404043', + trackBorderColor: '#404043' + }, - // special colors for some of the - legendBackgroundColor: 'rgba(0, 0, 0, 0.5)', - background2: '#505053', - dataLabelsColor: '#B0B0B3', - textColor: '#C0C0C0', - contrastTextColor: '#F0F0F3', - maskColor: 'rgba(255,255,255,0.3)' + // special colors for some of the + legendBackgroundColor: 'rgba(0, 0, 0, 0.5)', + background2: '#505053', + dataLabelsColor: '#B0B0B3', + textColor: '#C0C0C0', + contrastTextColor: '#F0F0F3', + maskColor: 'rgba(255,255,255,0.3)' }; // Apply the theme diff --git a/js/themes/gray.js b/js/themes/gray.js index 79fea464a02..172ee19b10d 100644 --- a/js/themes/gray.js +++ b/js/themes/gray.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license - * + * * Gray theme for Highcharts JS * @author Torstein Honsi */ @@ -10,257 +10,257 @@ 'use strict'; import Highcharts from '../parts/Globals.js'; Highcharts.theme = { - colors: ['#DDDF0D', '#7798BF', '#55BF3B', '#DF5353', '#aaeeee', - '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], - chart: { - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, 'rgb(96, 96, 96)'], - [1, 'rgb(16, 16, 16)'] - ] - }, - borderWidth: 0, - borderRadius: 0, - plotBackgroundColor: null, - plotShadow: false, - plotBorderWidth: 0 - }, - title: { - style: { - color: '#FFF', - font: '16px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - }, - subtitle: { - style: { - color: '#DDD', - font: '12px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - }, - xAxis: { - gridLineWidth: 0, - lineColor: '#999', - tickColor: '#999', - labels: { - style: { - color: '#999', - fontWeight: 'bold' - } - }, - title: { - style: { - color: '#AAA', - font: 'bold 12px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - } - }, - yAxis: { - alternateGridColor: null, - minorTickInterval: null, - gridLineColor: 'rgba(255, 255, 255, .1)', - minorGridLineColor: 'rgba(255,255,255,0.07)', - lineWidth: 0, - tickWidth: 0, - labels: { - style: { - color: '#999', - fontWeight: 'bold' - } - }, - title: { - style: { - color: '#AAA', - font: 'bold 12px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - } - }, - legend: { - itemStyle: { - color: '#CCC' - }, - itemHoverStyle: { - color: '#FFF' - }, - itemHiddenStyle: { - color: '#333' - } - }, - labels: { - style: { - color: '#CCC' - } - }, - tooltip: { - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, 'rgba(96, 96, 96, .8)'], - [1, 'rgba(16, 16, 16, .8)'] - ] - }, - borderWidth: 0, - style: { - color: '#FFF' - } - }, + colors: ['#DDDF0D', '#7798BF', '#55BF3B', '#DF5353', '#aaeeee', + '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], + chart: { + backgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0, 'rgb(96, 96, 96)'], + [1, 'rgb(16, 16, 16)'] + ] + }, + borderWidth: 0, + borderRadius: 0, + plotBackgroundColor: null, + plotShadow: false, + plotBorderWidth: 0 + }, + title: { + style: { + color: '#FFF', + font: '16px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + }, + subtitle: { + style: { + color: '#DDD', + font: '12px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + }, + xAxis: { + gridLineWidth: 0, + lineColor: '#999', + tickColor: '#999', + labels: { + style: { + color: '#999', + fontWeight: 'bold' + } + }, + title: { + style: { + color: '#AAA', + font: 'bold 12px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + } + }, + yAxis: { + alternateGridColor: null, + minorTickInterval: null, + gridLineColor: 'rgba(255, 255, 255, .1)', + minorGridLineColor: 'rgba(255,255,255,0.07)', + lineWidth: 0, + tickWidth: 0, + labels: { + style: { + color: '#999', + fontWeight: 'bold' + } + }, + title: { + style: { + color: '#AAA', + font: 'bold 12px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + } + }, + legend: { + itemStyle: { + color: '#CCC' + }, + itemHoverStyle: { + color: '#FFF' + }, + itemHiddenStyle: { + color: '#333' + } + }, + labels: { + style: { + color: '#CCC' + } + }, + tooltip: { + backgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0, 'rgba(96, 96, 96, .8)'], + [1, 'rgba(16, 16, 16, .8)'] + ] + }, + borderWidth: 0, + style: { + color: '#FFF' + } + }, - plotOptions: { - series: { - nullColor: '#444444' - }, - line: { - dataLabels: { - color: '#CCC' - }, - marker: { - lineColor: '#333' - } - }, - spline: { - marker: { - lineColor: '#333' - } - }, - scatter: { - marker: { - lineColor: '#333' - } - }, - candlestick: { - lineColor: 'white' - } - }, + plotOptions: { + series: { + nullColor: '#444444' + }, + line: { + dataLabels: { + color: '#CCC' + }, + marker: { + lineColor: '#333' + } + }, + spline: { + marker: { + lineColor: '#333' + } + }, + scatter: { + marker: { + lineColor: '#333' + } + }, + candlestick: { + lineColor: 'white' + } + }, - toolbar: { - itemStyle: { - color: '#CCC' - } - }, + toolbar: { + itemStyle: { + color: '#CCC' + } + }, - navigation: { - buttonOptions: { - symbolStroke: '#DDDDDD', - hoverSymbolStroke: '#FFFFFF', - theme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#606060'], - [0.6, '#333333'] - ] - }, - stroke: '#000000' - } - } - }, + navigation: { + buttonOptions: { + symbolStroke: '#DDDDDD', + hoverSymbolStroke: '#FFFFFF', + theme: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#606060'], + [0.6, '#333333'] + ] + }, + stroke: '#000000' + } + } + }, - // scroll charts - rangeSelector: { - buttonTheme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - stroke: '#000000', - style: { - color: '#CCC', - fontWeight: 'bold' - }, - states: { - hover: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#BBB'], - [0.6, '#888'] - ] - }, - stroke: '#000000', - style: { - color: 'white' - } - }, - select: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.1, '#000'], - [0.3, '#333'] - ] - }, - stroke: '#000000', - style: { - color: 'yellow' - } - } - } - }, - inputStyle: { - backgroundColor: '#333', - color: 'silver' - }, - labelStyle: { - color: 'silver' - } - }, + // scroll charts + rangeSelector: { + buttonTheme: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + stroke: '#000000', + style: { + color: '#CCC', + fontWeight: 'bold' + }, + states: { + hover: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#BBB'], + [0.6, '#888'] + ] + }, + stroke: '#000000', + style: { + color: 'white' + } + }, + select: { + fill: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.1, '#000'], + [0.3, '#333'] + ] + }, + stroke: '#000000', + style: { + color: 'yellow' + } + } + } + }, + inputStyle: { + backgroundColor: '#333', + color: 'silver' + }, + labelStyle: { + color: 'silver' + } + }, - navigator: { - handles: { - backgroundColor: '#666', - borderColor: '#AAA' - }, - outlineColor: '#CCC', - maskFill: 'rgba(16, 16, 16, 0.5)', - series: { - color: '#7798BF', - lineColor: '#A6C7ED' - } - }, + navigator: { + handles: { + backgroundColor: '#666', + borderColor: '#AAA' + }, + outlineColor: '#CCC', + maskFill: 'rgba(16, 16, 16, 0.5)', + series: { + color: '#7798BF', + lineColor: '#A6C7ED' + } + }, - scrollbar: { - barBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - barBorderColor: '#CCC', - buttonArrowColor: '#CCC', - buttonBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - buttonBorderColor: '#CCC', - rifleColor: '#FFF', - trackBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, '#000'], - [1, '#333'] - ] - }, - trackBorderColor: '#666' - }, + scrollbar: { + barBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + barBorderColor: '#CCC', + buttonArrowColor: '#CCC', + buttonBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0.4, '#888'], + [0.6, '#555'] + ] + }, + buttonBorderColor: '#CCC', + rifleColor: '#FFF', + trackBackgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0, '#000'], + [1, '#333'] + ] + }, + trackBorderColor: '#666' + }, - // special colors for some of the demo examples - legendBackgroundColor: 'rgba(48, 48, 48, 0.8)', - background2: 'rgb(70, 70, 70)', - dataLabelsColor: '#444', - textColor: '#E0E0E0', - maskColor: 'rgba(255,255,255,0.3)' + // special colors for some of the demo examples + legendBackgroundColor: 'rgba(48, 48, 48, 0.8)', + background2: 'rgb(70, 70, 70)', + dataLabelsColor: '#444', + textColor: '#E0E0E0', + maskColor: 'rgba(255,255,255,0.3)' }; // Apply the theme diff --git a/js/themes/grid-light.js b/js/themes/grid-light.js index 268e45158bd..9b88ff78f07 100644 --- a/js/themes/grid-light.js +++ b/js/themes/grid-light.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license - * + * * Grid-light theme for Highcharts JS * @author Torstein Honsi */ @@ -12,68 +12,68 @@ import Highcharts from '../parts/Globals.js'; /* global document */ // Load the fonts Highcharts.createElement('link', { - href: 'https://fonts.googleapis.com/css?family=Dosis:400,600', - rel: 'stylesheet', - type: 'text/css' + href: 'https://fonts.googleapis.com/css?family=Dosis:400,600', + rel: 'stylesheet', + type: 'text/css' }, null, document.getElementsByTagName('head')[0]); Highcharts.theme = { - colors: ['#7cb5ec', '#f7a35c', '#90ee7e', '#7798BF', '#aaeeee', '#ff0066', - '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], - chart: { - backgroundColor: null, - style: { - fontFamily: 'Dosis, sans-serif' - } - }, - title: { - style: { - fontSize: '16px', - fontWeight: 'bold', - textTransform: 'uppercase' - } - }, - tooltip: { - borderWidth: 0, - backgroundColor: 'rgba(219,219,216,0.8)', - shadow: false - }, - legend: { - itemStyle: { - fontWeight: 'bold', - fontSize: '13px' - } - }, - xAxis: { - gridLineWidth: 1, - labels: { - style: { - fontSize: '12px' - } - } - }, - yAxis: { - minorTickInterval: 'auto', - title: { - style: { - textTransform: 'uppercase' - } - }, - labels: { - style: { - fontSize: '12px' - } - } - }, - plotOptions: { - candlestick: { - lineColor: '#404048' - } - }, + colors: ['#7cb5ec', '#f7a35c', '#90ee7e', '#7798BF', '#aaeeee', '#ff0066', + '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], + chart: { + backgroundColor: null, + style: { + fontFamily: 'Dosis, sans-serif' + } + }, + title: { + style: { + fontSize: '16px', + fontWeight: 'bold', + textTransform: 'uppercase' + } + }, + tooltip: { + borderWidth: 0, + backgroundColor: 'rgba(219,219,216,0.8)', + shadow: false + }, + legend: { + itemStyle: { + fontWeight: 'bold', + fontSize: '13px' + } + }, + xAxis: { + gridLineWidth: 1, + labels: { + style: { + fontSize: '12px' + } + } + }, + yAxis: { + minorTickInterval: 'auto', + title: { + style: { + textTransform: 'uppercase' + } + }, + labels: { + style: { + fontSize: '12px' + } + } + }, + plotOptions: { + candlestick: { + lineColor: '#404048' + } + }, - // General - background2: '#F0F0EA' + // General + background2: '#F0F0EA' }; diff --git a/js/themes/grid.js b/js/themes/grid.js index e65bf42ee0a..637ba892c9c 100644 --- a/js/themes/grid.js +++ b/js/themes/grid.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license - * + * * Grid theme for Highcharts JS * @author Torstein Honsi */ @@ -10,100 +10,100 @@ 'use strict'; import Highcharts from '../parts/Globals.js'; Highcharts.theme = { - colors: ['#058DC7', '#50B432', '#ED561B', '#DDDF00', '#24CBE5', '#64E572', - '#FF9655', '#FFF263', '#6AF9C4'], - chart: { - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 }, - stops: [ - [0, 'rgb(255, 255, 255)'], - [1, 'rgb(240, 240, 255)'] - ] - }, - borderWidth: 2, - plotBackgroundColor: 'rgba(255, 255, 255, .9)', - plotShadow: true, - plotBorderWidth: 1 - }, - title: { - style: { - color: '#000', - font: 'bold 16px "Trebuchet MS", Verdana, sans-serif' - } - }, - subtitle: { - style: { - color: '#666666', - font: 'bold 12px "Trebuchet MS", Verdana, sans-serif' - } - }, - xAxis: { - gridLineWidth: 1, - lineColor: '#000', - tickColor: '#000', - labels: { - style: { - color: '#000', - font: '11px Trebuchet MS, Verdana, sans-serif' - } - }, - title: { - style: { - color: '#333', - fontWeight: 'bold', - fontSize: '12px', - fontFamily: 'Trebuchet MS, Verdana, sans-serif' + colors: ['#058DC7', '#50B432', '#ED561B', '#DDDF00', '#24CBE5', '#64E572', + '#FF9655', '#FFF263', '#6AF9C4'], + chart: { + backgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 }, + stops: [ + [0, 'rgb(255, 255, 255)'], + [1, 'rgb(240, 240, 255)'] + ] + }, + borderWidth: 2, + plotBackgroundColor: 'rgba(255, 255, 255, .9)', + plotShadow: true, + plotBorderWidth: 1 + }, + title: { + style: { + color: '#000', + font: 'bold 16px "Trebuchet MS", Verdana, sans-serif' + } + }, + subtitle: { + style: { + color: '#666666', + font: 'bold 12px "Trebuchet MS", Verdana, sans-serif' + } + }, + xAxis: { + gridLineWidth: 1, + lineColor: '#000', + tickColor: '#000', + labels: { + style: { + color: '#000', + font: '11px Trebuchet MS, Verdana, sans-serif' + } + }, + title: { + style: { + color: '#333', + fontWeight: 'bold', + fontSize: '12px', + fontFamily: 'Trebuchet MS, Verdana, sans-serif' - } - } - }, - yAxis: { - minorTickInterval: 'auto', - lineColor: '#000', - lineWidth: 1, - tickWidth: 1, - tickColor: '#000', - labels: { - style: { - color: '#000', - font: '11px Trebuchet MS, Verdana, sans-serif' - } - }, - title: { - style: { - color: '#333', - fontWeight: 'bold', - fontSize: '12px', - fontFamily: 'Trebuchet MS, Verdana, sans-serif' - } - } - }, - legend: { - itemStyle: { - font: '9pt Trebuchet MS, Verdana, sans-serif', - color: 'black' + } + } + }, + yAxis: { + minorTickInterval: 'auto', + lineColor: '#000', + lineWidth: 1, + tickWidth: 1, + tickColor: '#000', + labels: { + style: { + color: '#000', + font: '11px Trebuchet MS, Verdana, sans-serif' + } + }, + title: { + style: { + color: '#333', + fontWeight: 'bold', + fontSize: '12px', + fontFamily: 'Trebuchet MS, Verdana, sans-serif' + } + } + }, + legend: { + itemStyle: { + font: '9pt Trebuchet MS, Verdana, sans-serif', + color: 'black' - }, - itemHoverStyle: { - color: '#039' - }, - itemHiddenStyle: { - color: 'gray' - } - }, - labels: { - style: { - color: '#99b' - } - }, + }, + itemHoverStyle: { + color: '#039' + }, + itemHiddenStyle: { + color: 'gray' + } + }, + labels: { + style: { + color: '#99b' + } + }, - navigation: { - buttonOptions: { - theme: { - stroke: '#CCCCCC' - } - } - } + navigation: { + buttonOptions: { + theme: { + stroke: '#CCCCCC' + } + } + } }; // Apply the theme diff --git a/js/themes/sand-signika.js b/js/themes/sand-signika.js index 2be76b70b01..b219583ed5e 100644 --- a/js/themes/sand-signika.js +++ b/js/themes/sand-signika.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license - * + * * Sand-Signika theme for Highcharts JS * @author Torstein Honsi */ @@ -12,99 +12,99 @@ import Highcharts from '../parts/Globals.js'; /* global document */ // Load the fonts Highcharts.createElement('link', { - href: 'https://fonts.googleapis.com/css?family=Signika:400,700', - rel: 'stylesheet', - type: 'text/css' + href: 'https://fonts.googleapis.com/css?family=Signika:400,700', + rel: 'stylesheet', + type: 'text/css' }, null, document.getElementsByTagName('head')[0]); // Add the background image to the container Highcharts.wrap(Highcharts.Chart.prototype, 'getContainer', function (proceed) { - proceed.call(this); - this.container.style.background = - 'url(http://www.highcharts.com/samples/graphics/sand.png)'; + proceed.call(this); + this.container.style.background = + 'url(http://www.highcharts.com/samples/graphics/sand.png)'; }); Highcharts.theme = { - colors: ['#f45b5b', '#8085e9', '#8d4654', '#7798BF', '#aaeeee', - '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], - chart: { - backgroundColor: null, - style: { - fontFamily: 'Signika, serif' - } - }, - title: { - style: { - color: 'black', - fontSize: '16px', - fontWeight: 'bold' - } - }, - subtitle: { - style: { - color: 'black' - } - }, - tooltip: { - borderWidth: 0 - }, - legend: { - itemStyle: { - fontWeight: 'bold', - fontSize: '13px' - } - }, - xAxis: { - labels: { - style: { - color: '#6e6e70' - } - } - }, - yAxis: { - labels: { - style: { - color: '#6e6e70' - } - } - }, - plotOptions: { - series: { - shadow: true - }, - candlestick: { - lineColor: '#404048' - }, - map: { - shadow: false - } - }, + colors: ['#f45b5b', '#8085e9', '#8d4654', '#7798BF', '#aaeeee', + '#ff0066', '#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'], + chart: { + backgroundColor: null, + style: { + fontFamily: 'Signika, serif' + } + }, + title: { + style: { + color: 'black', + fontSize: '16px', + fontWeight: 'bold' + } + }, + subtitle: { + style: { + color: 'black' + } + }, + tooltip: { + borderWidth: 0 + }, + legend: { + itemStyle: { + fontWeight: 'bold', + fontSize: '13px' + } + }, + xAxis: { + labels: { + style: { + color: '#6e6e70' + } + } + }, + yAxis: { + labels: { + style: { + color: '#6e6e70' + } + } + }, + plotOptions: { + series: { + shadow: true + }, + candlestick: { + lineColor: '#404048' + }, + map: { + shadow: false + } + }, - // Highstock specific - navigator: { - xAxis: { - gridLineColor: '#D0D0D8' - } - }, - rangeSelector: { - buttonTheme: { - fill: 'white', - stroke: '#C0C0C8', - 'stroke-width': 1, - states: { - select: { - fill: '#D0D0D8' - } - } - } - }, - scrollbar: { - trackBorderColor: '#C0C0C8' - }, + // Highstock specific + navigator: { + xAxis: { + gridLineColor: '#D0D0D8' + } + }, + rangeSelector: { + buttonTheme: { + fill: 'white', + stroke: '#C0C0C8', + 'stroke-width': 1, + states: { + select: { + fill: '#D0D0D8' + } + } + } + }, + scrollbar: { + trackBorderColor: '#C0C0C8' + }, - // General - background2: '#E0E0E8' + // General + background2: '#E0E0E8' }; diff --git a/js/themes/skies.js b/js/themes/skies.js index 808dcd33375..5297e9cadda 100644 --- a/js/themes/skies.js +++ b/js/themes/skies.js @@ -2,7 +2,7 @@ * (c) 2010-2017 Torstein Honsi * * License: www.highcharts.com/license - * + * * Skies theme for Highcharts JS * @author Torstein Honsi */ @@ -10,90 +10,90 @@ 'use strict'; import Highcharts from '../parts/Globals.js'; Highcharts.theme = { - colors: ['#514F78', '#42A07B', '#9B5E4A', '#72727F', '#1F949A', - '#82914E', '#86777F', '#42A07B'], - chart: { - className: 'skies', - borderWidth: 0, - plotShadow: true, - plotBackgroundImage: 'http://www.highcharts.com/demo/gfx/skies.jpg', - plotBackgroundColor: { - linearGradient: [0, 0, 250, 500], - stops: [ - [0, 'rgba(255, 255, 255, 1)'], - [1, 'rgba(255, 255, 255, 0)'] - ] - }, - plotBorderWidth: 1 - }, - title: { - style: { - color: '#3E576F', - font: '16px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - }, - subtitle: { - style: { - color: '#6D869F', - font: '12px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - }, - xAxis: { - gridLineWidth: 0, - lineColor: '#C0D0E0', - tickColor: '#C0D0E0', - labels: { - style: { - color: '#666', - fontWeight: 'bold' - } - }, - title: { - style: { - color: '#666', - font: '12px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - } - }, - yAxis: { - alternateGridColor: 'rgba(255, 255, 255, .5)', - lineColor: '#C0D0E0', - tickColor: '#C0D0E0', - tickWidth: 1, - labels: { - style: { - color: '#666', - fontWeight: 'bold' - } - }, - title: { - style: { - color: '#666', - font: '12px Lucida Grande, Lucida Sans Unicode,' + - ' Verdana, Arial, Helvetica, sans-serif' - } - } - }, - legend: { - itemStyle: { - font: '9pt Trebuchet MS, Verdana, sans-serif', - color: '#3E576F' - }, - itemHoverStyle: { - color: 'black' - }, - itemHiddenStyle: { - color: 'silver' - } - }, - labels: { - style: { - color: '#3E576F' - } - } + colors: ['#514F78', '#42A07B', '#9B5E4A', '#72727F', '#1F949A', + '#82914E', '#86777F', '#42A07B'], + chart: { + className: 'skies', + borderWidth: 0, + plotShadow: true, + plotBackgroundImage: 'http://www.highcharts.com/demo/gfx/skies.jpg', + plotBackgroundColor: { + linearGradient: [0, 0, 250, 500], + stops: [ + [0, 'rgba(255, 255, 255, 1)'], + [1, 'rgba(255, 255, 255, 0)'] + ] + }, + plotBorderWidth: 1 + }, + title: { + style: { + color: '#3E576F', + font: '16px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + }, + subtitle: { + style: { + color: '#6D869F', + font: '12px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + }, + xAxis: { + gridLineWidth: 0, + lineColor: '#C0D0E0', + tickColor: '#C0D0E0', + labels: { + style: { + color: '#666', + fontWeight: 'bold' + } + }, + title: { + style: { + color: '#666', + font: '12px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + } + }, + yAxis: { + alternateGridColor: 'rgba(255, 255, 255, .5)', + lineColor: '#C0D0E0', + tickColor: '#C0D0E0', + tickWidth: 1, + labels: { + style: { + color: '#666', + fontWeight: 'bold' + } + }, + title: { + style: { + color: '#666', + font: '12px Lucida Grande, Lucida Sans Unicode,' + + ' Verdana, Arial, Helvetica, sans-serif' + } + } + }, + legend: { + itemStyle: { + font: '9pt Trebuchet MS, Verdana, sans-serif', + color: '#3E576F' + }, + itemHoverStyle: { + color: 'black' + }, + itemHiddenStyle: { + color: 'silver' + } + }, + labels: { + style: { + color: '#3E576F' + } + } }; // Apply the theme diff --git a/js/themes/sunset.js b/js/themes/sunset.js index 78ff964170e..07af6b91d72 100644 --- a/js/themes/sunset.js +++ b/js/themes/sunset.js @@ -2,8 +2,8 @@ * (c) 2010-2017 Highsoft AS * * License: www.highcharts.com/license - * - * Accessible high-contrast theme for Highcharts. Considers colorblindness and + * + * Accessible high-contrast theme for Highcharts. Considers colorblindness and * monochrome rendering. * @author Øystein Moseng */ @@ -11,25 +11,25 @@ 'use strict'; import Highcharts from '../parts/Globals.js'; Highcharts.theme = { - colors: ['#FDD089', '#FF7F79', '#A0446E', '#251535'], + colors: ['#FDD089', '#FF7F79', '#A0446E', '#251535'], - colorAxis: { - maxColor: '#60042E', - minColor: '#FDD089' - }, + colorAxis: { + maxColor: '#60042E', + minColor: '#FDD089' + }, - plotOptions: { - map: { - nullColor: '#fefefc' - } - }, + plotOptions: { + map: { + nullColor: '#fefefc' + } + }, - navigator: { - series: { - color: '#FF7F79', - lineColor: '#A0446E' - } - } + navigator: { + series: { + color: '#FF7F79', + lineColor: '#A0446E' + } + } }; // Apply the theme