initial commit

This commit is contained in:
2021-07-10 16:25:16 +02:00
parent bff57cbf45
commit 20fb7267a8
2426 changed files with 792090 additions and 0 deletions
@@ -0,0 +1,578 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Color.js';
import './Legend.js';
import './Series.js';
import './Options.js';
var color = H.color,
LegendSymbolMixin = H.LegendSymbolMixin,
pick = H.pick,
Series = H.Series,
seriesType = H.seriesType;
/**
* Area series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.area
*
* @augments Highcharts.Series
*/
seriesType('area', 'line',
/**
* The area series type.
*
* @sample {highcharts} highcharts/demo/area-basic/
* Area chart
* @sample {highstock} stock/demo/area/
* Area chart
*
* @extends plotOptions.line
* @excluding useOhlcData
* @product highcharts highstock
* @optionparent plotOptions.area
*/
{
/**
* 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.
*
* @sample {highcharts} highcharts/plotoptions/area-fillcolor-default/
* Null by default
* @sample {highcharts} highcharts/plotoptions/area-fillcolor-gradient/
* Gradient
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @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.
*
* @sample {highcharts} highcharts/plotoptions/area-fillopacity/
* Automatic fill color and fill opacity of 0.1
*
* @type {number}
* @default {highcharts} 0.75
* @default {highstock} 0.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.
*
* @sample {highcharts} highcharts/plotoptions/area-linecolor/
* Dark gray line
*
* @type {Highcharts.ColorString}
* @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.
*
* @see [negativeColor](#plotOptions.area.negativeColor)
*
* @sample {highcharts} highcharts/css/series-negative-color/
* Negative color in styled mode
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @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.
*
* @sample {highcharts|highstock} highcharts/plotoptions/area-trackbyarea/
* Display the tooltip when the area is hovered
*
* @type {boolean}
* @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. The area between the graph
* and the threshold is filled.
*
* * If a number is given, the Y axis will scale to the threshold.
* * If `null`, the scaling behaves like a line series with fill between the
* graph and the Y axis minimum.
* * If `Infinity` or `-Infinity`, the area between the graph and the
* corresponing Y axis extreme is filled (since v6.1.0).
*
* @sample {highcharts} highcharts/plotoptions/area-threshold/
* A threshold of 100
* @sample {highcharts} highcharts/plotoptions/area-threshold-infinity/
* A threshold of Infinity
*
* @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 = yAxisSeries.map(function (s) {
return s.visible;
});
keys.forEach(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.
[-1, 1].forEach(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',
this.color,
options.fillColor
]]; // area name, main color, fill color
zones.forEach(function (zone, i) {
props.push([
'zone-area-' + i,
'highcharts-area highcharts-zone-area-' + i + ' ' +
zone.className,
zone.color || series.color,
zone.fillColor || options.fillColor
]);
});
props.forEach(function (prop) {
var areaKey = prop[0],
area = series[areaKey],
attribs;
// Create or update the area
if (area) { // update
area.endX = series.preventGraphAnimation ?
null :
areaPath.xMap;
area.animate({ d: areaPath });
} else { // create
attribs = {
zIndex: 0 // #1069
};
if (!series.chart.styledMode) {
attribs.fill = pick(
prop[3],
color(prop[2])
.setOpacity(pick(options.fillOpacity, 0.75))
.get()
);
}
area = series[areaKey] = series.chart.renderer
.path(areaPath)
.addClass(prop[1])
.attr(attribs)
.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).
*
* @extends series,plotOptions.area
* @excluding dataParser, dataURL, useOhlcData
* @product highcharts highstock
* @apioption series.area
*/
/**
* 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],
* [1, 7],
* [2, 6]
* ]
* ```
*
* 3. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.area.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* y: 9,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 6,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @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
*
* @type {Array<number|Array<(number|string),number>|*>}
* @extends series.line.data
* @product highcharts highstock
* @apioption series.area.data
*/
@@ -0,0 +1,122 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Legend.js';
import './AreaSeries.js';
import './SplineSeries.js';
var areaProto = H.seriesTypes.area.prototype,
defaultPlotOptions = H.defaultPlotOptions,
LegendSymbolMixin = H.LegendSymbolMixin,
seriesType = H.seriesType;
/**
* AreaSpline series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.areaspline
*
* @augments Highcharts.Series
*/
seriesType('areaspline', 'spline',
/**
* The area spline series is an area series where the graph between the
* points is smoothed into a spline.
*
* @sample {highcharts} highcharts/demo/areaspline/
* Area spline chart
* @sample {highstock} stock/demo/areaspline/
* Area spline chart
*
* @extends plotOptions.area
* @excluding step
* @product highcharts highstock
* @apioption plotOptions.areaspline
*/
defaultPlotOptions.area
, {
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).
*
*
* @extends series,plotOptions.areaspline
* @excluding dataParser, dataURL
* @product highcharts highstock
* @apioption series.areaspline
*/
/**
* 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],
* [1, 9],
* [2, 3]
* ]
* ```
*
* 3. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.areaspline.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* y: 4,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 4,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @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
*
* @type {Array<number|Array<(number|string),number>|*>}
* @extends series.line.data
* @product highcharts highstock
* @apioption series.areaspline.data
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './ColumnSeries.js';
var seriesType = H.seriesType;
/**
* Bar series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.bar
*
* @augments Highcharts.Series
*/
seriesType('bar', 'column',
/**
* A bar series is a special type of column series where the columns are
* horizontal.
*
* @sample highcharts/demo/bar-basic/
* Bar chart
*
* @extends plotOptions.column
* @product highcharts
* @apioption plotOptions.bar
*/
/**
* Alignment of the data label relative to the data point.
*
* @sample {highcharts} highcharts/plotoptions/bar-datalabels-align-inside-bar/
* Data labels inside the bar
*
* @type {string}
* @default left
* @product highcharts
* @apioption plotOptions.bar.dataLabels.align
*/
/**
* The x position of the data label relative to the data point.
*
* @sample {highcharts} highcharts/plotoptions/bar-datalabels-align-inside-bar/
* Data labels inside the bar
*
* @type {number}
* @default 5
* @product highcharts
* @apioption plotOptions.bar.dataLabels.x
*/
/**
* @ignore
*/
null
, {
inverted: true
});
/**
* A `bar` series. If the [type](#series.bar.type) option is not specified,
* it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.bar
* @excluding connectNulls, dashStyle, dataParser, dataURL, gapSize, gapUnit,
* linecap, lineWidth, marker, connectEnds, step
* @product highcharts
* @apioption series.bar
*/
/**
* 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],
* [1, 10],
* [2, 3]
* ]
* ```
*
* 3. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.bar.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* y: 1,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 10,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @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
*
* @type {Array<number|Array<(number|string),number>|*>}
* @extends series.column.data
* @product highcharts
* @apioption series.bar.data
*/
/**
* @excluding halo,lineWidth,lineWidthPlus,marker
* @product highcharts highstock
* @apioption series.bar.states.hover
*/
/**
* @excluding halo,lineWidth,lineWidthPlus,marker
* @product highcharts highstock
* @apioption series.bar.states.select
*/
@@ -0,0 +1,358 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
var defaultPlotOptions = H.defaultPlotOptions,
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.
*
* @sample {highstock} stock/plotoptions/candlestick-linecolor/
* Candlestick line colors
*
* @type {Highcharts.ColorString}
* @since 1.3.6
* @product highstock
* @apioption plotOptions.candlestick.upLineColor
*/
/**
* @type {string|Function}
* @default ohlc
* @product highstock
* @apioption plotOptions.candlestick.dataGrouping.approximation
*/
states: {
/**
* @extends plotOptions.column.states.hover
* @product highstock
*/
hover: {
/**
* The pixel width of the line/border around the candlestick.
*
* @product highstock
*/
lineWidth: 2
}
},
/**
* @extends plotOptions.ohlc.tooltip
*/
tooltip: defaultPlotOptions.ohlc.tooltip,
/**
* @type {number|null}
* @product highstock
*/
threshold: null,
/**
* 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.
*
* @see [upLineColor](#plotOptions.candlestick.upLineColor)
*
* @sample {highstock} stock/plotoptions/candlestick-linecolor/
* Candlestick line colors
*
* @type {Highcharts.ColorString}
* @default #000000
* @product highstock
*/
lineColor: '#000000',
/**
* 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.
*
* @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.
*
* @sample {highstock} stock/plotoptions/candlestick-color/
* Custom colors
* @sample {highstock} highcharts/css/candlestick/
* Colors in styled mode
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @default #ffffff
* @product highstock
*/
upColor: '#ffffff',
/**
* @product highstock
*/
stickyTracking: true
};
/**
* The candlestick series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.candlestick
*
* @augments Highcharts.seriesTypes.ohlc
*/
seriesType('candlestick', 'ohlc', merge(
defaultPlotOptions.column,
candlestickOptions
), /** @lends seriesTypes.candlestick */ {
/**
* Postprocess mapping between options and SVG attributes
*
* @private
* @function Highcharts.seriesTypes.candlestick#pointAttribs
*
* @param {Highcharts.Point} point
*
* @param {string} [state]
*
* @return {Highcharts.SVGAttributes}
*/
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.
*
* @private
* @function Highcharts.seriesTypes.candlestick#drawPoints
*/
drawPoints: function () {
var series = this,
points = series.points,
chart = series.chart,
reversedYAxis = series.yAxis.reversed;
points.forEach(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 (!series.chart.styledMode) {
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 = reversedYAxis ?
bottomBox !== point.yBottom :
Math.round(topBox) !== Math.round(point.plotHigh);
hasBottomWhisker = reversedYAxis ?
Math.round(topBox) !== Math.round(point.plotHigh) :
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, instanciating 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', // Ensure a nice rectangle #2602
'M',
crispX, topBox,
'L',
// #460, #2094
crispX, hasTopWhisker ?
Math.round(
reversedYAxis ? point.yBottom : point.plotHigh
) :
topBox,
'M',
crispX, bottomBox,
'L',
// #460, #2094
crispX, hasBottomWhisker ?
Math.round(
reversedYAxis ? point.plotHigh : point.yBottom
) :
bottomBox
);
graphic[isNew ? 'attr' : 'animate']({ d: path })
.addClass(point.getClassName(), true);
}
});
}
});
/**
* A `candlestick` series. If the [type](#series.candlestick.type)
* option is not specified, it is inherited from [chart.type](
* #chart.type).
*
* @type {*}
* @extends series,plotOptions.candlestick
* @excluding dataParser, dataURL
* @product highstock
* @apioption series.candlestick
*/
/**
* An array of data points for the series. For the `candlestick` series
* type, points can be given in the following ways:
*
* 1. An array of arrays with 5 or 4 values. In this case, the values correspond
* to `x,open,high,low,close`. If the first value is a string, it is applied
* as the name of the point, and the `x` value is inferred. The `x` value can
* also be omitted, in which case the inner arrays should be of length 4.
* Then the `x` value is automatically calculated, either starting at 0 and
* incremented by 1, or from `pointStart` and `pointInterval` given in the
* series options.
* ```js
* data: [
* [0, 7, 2, 0, 4],
* [1, 1, 4, 2, 8],
* [2, 3, 3, 9, 3]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.candlestick.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* open: 9,
* high: 2,
* low: 4,
* close: 6,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* open: 1,
* high: 4,
* low: 7,
* close: 7,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @type {Array<Array<(number|string),number,number,number>|Array<(number|string),number,number,number,number>|*>}
* @extends series.ohlc.data
* @excluding y
* @product highstock
* @apioption series.candlestick.data
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,301 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* 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
* and apply concepts like opacity and brightening.
*
* @typedef {string} Highcharts.ColorString
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
var isNumber = H.isNumber,
merge = H.merge,
pInt = H.pInt;
/**
* Handle color operations. The object methods are chainable.
*
* @private
* @class
* @name Highcharts.Color
*
* @param {Highcharts.ColorString} 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);
}
// Initialize
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: {
white: '#ffffff',
black: '#000000'
},
/**
* Parse the input color to rgba array
*
* @private
* @function Highcharts.Color#init
*
* @param {Highcharts.ColorString} input
* The input color in either rbga or hex format
*/
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 = input.stops.map(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 in the specified format
*
* @function Highcharts.Color#get
*
* @param {string} format
* Possible values are 'a', 'rgb', undefined
*
* @return {Highcharts.ColorString}
* This color as a string.
*/
get: function (format) {
var input = this.input,
rgba = this.rgba,
ret;
if (this.stops) {
ret = merge(input);
ret.stops = [].concat(ret.stops);
this.stops.forEach(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 instance.
*
* @function Highcharts.Color#brighten
*
* @param {number} alpha
* The alpha value.
*
* @return {Highcharts.ColorString}
* This color with modifications.
*/
brighten: function (alpha) {
var i,
rgba = this.rgba;
if (this.stops) {
this.stops.forEach(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.
*
* @function Highcharts.Color#setOpacity
*
* @param {number} alpha
* Opacity between 0 and 1.
*
* @return {Highcharts.ColorString}
* Color with modifications.
*/
setOpacity: function (alpha) {
this.rgba[3] = alpha;
return this;
},
/**
* Return an intermediate color between two colors.
*
* @function Highcharts.Color#tweenTo
*
* @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 {Highcharts.ColorString}
* 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;
}
};
/**
* Creates a color instance out of a color string.
*
* @private
* @function Highcharts.color
*
* @param {Highcharts.ColorString} input
* The input color in either rbga or hex format
*/
H.color = function (input) {
return new H.Color(input);
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,134 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* 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;
/**
* 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
* with the time positions. Used in datetime axes as well as for grouping
* data on a datetime axis.
*
* @private
* @function Highcharts.Axis#getTimeTicks
*
* @param {*} 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
*
* @return {number}
*/
Axis.prototype.getTimeTicks = function () {
return this.chart.time.getTimeTicks.apply(this.chart.time, arguments);
};
/**
* Get a normalized tick interval for dates. Returns a configuration object with
* unit range (interval), count and name. Used to prepare data for getTimeTicks.
* Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
* of segments in stock charts, the normalizing logic was extracted in order to
* prevent it for running over again for each segment having the same interval.
* #662, #697.
*
* @private
* @function Highcharts.Axis#normalizeTimeTickInterval
*
* @param {number} tickInterval
*
* @param {Array<Array<number|string>>} [unitsOption]
*
* @return {*}
*/
Axis.prototype.normalizeTimeTickInterval = function (
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;
// 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;
// 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];
}
// 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]
};
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,739 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Series.js';
import './SvgRenderer.js';
import onSeriesMixin from '../mixins/on-series.js';
var addEvent = H.addEvent,
merge = H.merge,
noop = H.noop,
defined = H.defined,
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.
*
* @private
* @class
* @name Highcharts.seriesTypes.flags
*
* @augments Highcharts.Series
*/
seriesType(
'flags',
'column'
/**
* Flags are used to mark events in stock charts. They can be added on the
* timeline, or attached to a specific series.
*
* @sample stock/demo/flags-general/
* Flags on a line series
*
* @extends plotOptions.column
* @excluding animation, borderColor, borderRadius, borderWidth,
* colorByPoint, dataGrouping, pointPadding, pointWidth,
* turboThreshold
* @product highstock
* @optionparent plotOptions.flags
*/
, {
/**
* 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.
*
* @sample {highstock} stock/plotoptions/flags-onkey/
* Range series, flag on high
*
* @type {string}
* @default y
* @since 4.2.2
* @product highstock
* @validvalue ["y", "open", "high", "low", "close"]
* @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.
*
* @sample {highstock} stock/plotoptions/flags/
* Flags on series and on x axis
*
* @type {string}
* @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 of the format `url(/path-to-image.jpg)`.
* Individual shapes can also be set for each point.
*
* @sample {highstock} stock/plotoptions/flags/
* Different shapes
*
* @product highstock
* @validvalue ["flag", "circlepin", "squarepin"]
*/
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.
*
* @since 5.0.0
* @product highstock
* @validvalue ["left", "center", "right"]
*/
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.
*
* @extends plotOptions.series.tooltip
* @excluding changeDecimals, valueDecimals, valuePrefix, valueSuffix
* @product highstock
*/
tooltip: {
pointFormat: '{point.text}<br/>'
},
threshold: null,
/**
* The text to display on each flag. This can be defined on series
* level, or individually for each point. Defaults to `"A"`.
*
* @type {string}
* @default A
* @product highstock
* @apioption plotOptions.flags.title
*/
/**
* 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
*/
/**
* Fixed width of the flag's shape. By default, width is autocalculated
* according to the flag's title.
*
* @sample {highstock} stock/demo/flags-shapes/
* Flags with fixed width
*
* @type {number}
* @product highstock
* @apioption plotOptions.flags.width
*/
/**
* Fixed height of the flag's shape. By default, height is
* autocalculated according to the flag's title.
*
* @type {number}
* @product highstock
* @apioption plotOptions.flags.height
*/
/**
* The fill color for the flags.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highstock
*/
fillColor: '#ffffff',
/**
* 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 {Highcharts.ColorString}
* @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 {Highcharts.ColorString}
* @product highstock
*/
lineColor: '#000000',
/**
* The fill or background color of the flag.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highstock
*/
fillColor: '#ccd6eb'
}
},
/**
* The text styles of the flag.
*
* In styled mode, the styles are set in the
* `.highcharts-flag-series .highcharts-point` rule.
*
* @type {Highcharts.CSSObject}
* @default {"fontSize": "11px", "fontWeight": "bold"}
* @product highstock
*/
style: {
/** @ignore-option */
fontSize: '11px',
/** @ignore-option */
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.
*
* @private
* @borrows Highcharts.Series#init as Highcharts.seriesTypes.flags#init
*/
init: Series.prototype.init,
/**
* Get presentational attributes
*
* @private
* @function Highcharts.seriesTypes.flags#pointAttribs
*
* @param {Highcharts.Point} point
*
* @param {string} [state]
*
* @return {Highcharts.SVGAttributes}
*/
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.
*
* @private
* @function Highcharts.seriesTypes.flags#drawPoints
*/
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 (!chart.styledMode) {
graphic
.attr(series.pointAttribs(point))
.css(merge(options.style, point.style));
}
graphic.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 (!chart.styledMode) {
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);
points.forEach(function (point) {
var box = point.graphic && boxesMap[point.plotX];
if (box) {
point.graphic[
point.graphic.isNew ? 'attr' : 'animate'
]({
x: box.pos,
anchorX: point.anchorX
});
// Hide flag when its box position is not specified
// (#8573, #9299)
if (!defined(box.pos)) {
point.graphic.attr({
x: -9999,
anchorX: -9999
});
point.graphic.isNew = true;
} else {
point.graphic.isNew = false;
}
}
});
}
// Can 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.
*
* @private
* @function Highcharts.seriesTypes.flags#drawTracker
*/
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.
*/
points.forEach(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
points.forEach(function (otherPoint) {
if (
otherPoint !== point &&
otherPoint.raised &&
otherPoint.graphic
) {
otherPoint.graphic.attr({
y: otherPoint._y
});
otherPoint.raised = false;
}
});
});
}
});
},
/**
* Disable animation, but keep clipping (#8546).
*
* @private
* @function Highcharts.seriesTypes.flags#animate
*
* @param {boolean} [init]
*/
animate: function (init) {
if (init) {
this.setClip();
} else {
this.animate = null;
}
},
/**
* @private
* @function Highcharts.seriesTypes.flags#setClip
*/
setClip: function () {
Series.prototype.setClip.apply(this, arguments);
if (this.options.clip !== false && this.sharedClipKey) {
this.markerGroup.clip(this.chart[this.sharedClipKey]);
}
},
/**
* @private
* @function Highcharts.seriesTypes.flags#buildKDTree
*/
buildKDTree: noop,
/**
* Don't invert the flag marker group (#4960).
*
* @private
* @function Highcharts.seriesTypes.flags#invertGroups
*/
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'
]
);
};
// 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;
};
}
createPinSymbol('circle');
createPinSymbol('square');
/**
* 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) {
['flag', 'circlepin', 'squarepin'].forEach(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).
*
* @extends series,plotOptions.flags
* @excluding dataParser, dataURL
* @product highstock
* @apioption series.flags
*/
/**
* An array of data points for the series. For the `flags` series type,
* points can be given in the following ways:
*
* 1. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.flags.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* title: "A",
* text: "First event"
* }, {
* x: 1,
* title: "B",
* text: "Second event"
* }]
* ```
*
* @type {Array<*>}
* @extends series.line.data
* @excluding dataLabels, marker, name, y
* @product highstock
* @apioption series.flags.data
*/
/**
* The fill color of an individual flag. By default it inherits from
* the series color.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highstock
* @apioption series.flags.data.fillColor
*/
/**
* The longer text to be shown in the flag's tooltip.
*
* @type {string}
* @product highstock
* @apioption series.flags.data.text
*/
/**
* The short text to be shown on the flag.
*
* @type {string}
* @product highstock
* @apioption series.flags.data.title
*/
@@ -0,0 +1,75 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* Reference to the global SVGElement class as a workaround for a name conflict
* in the Highcharts namespace.
*
* @global
* @typedef {global.SVGElement} GlobalSVGElement
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/SVGElement
*/
'use strict';
/* global win, window */
// glob is a temporary fix to allow our es-modules to work.
var glob = typeof win === 'undefined' ?
(typeof window !== '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
);
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`.
*
* @name Highcharts.charts
* @type {Array<Highcharts.Chart>}
*/
charts: []
};
export default Highcharts;
@@ -0,0 +1,539 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* 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,
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;
// 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
*
* @private
* @function Highcharts.SVGElement#htmlCss
*
* @param {Highcharts.CSSObject} styles
*
* @return {Highcharts.SVGElement}
*/
htmlCss: function (styles) {
var wrapper = this,
element = wrapper.element,
// When setting or unsetting the width style, we need to update
// transform (#8809)
isSettingWidth = (
element.tagName === 'SPAN' &&
styles &&
'width' in styles
),
textWidth = pick(
isSettingWidth && styles.width,
undefined
),
doTransform;
if (isSettingWidth) {
delete styles.width;
wrapper.textWidth = textWidth;
doTransform = true;
}
if (styles && styles.textOverflow === 'ellipsis') {
styles.whiteSpace = 'nowrap';
styles.overflow = 'hidden';
}
wrapper.styles = extend(wrapper.styles, styles);
css(wrapper.element, styles);
// Now that all styles are applied, to the transform
if (doTransform) {
wrapper.htmlUpdateTransform();
}
return wrapper;
},
/**
* VML and useHTML method for calculating the bounding box based on offsets.
*
* @private
* @function Highcharts.SVGElement#htmlGetBBox
*
* @param {boolean} refresh
* Whether to force a fresh value from the DOM or to use the cached
* value.
*
* @return {Highcharts.BBoxObject}
* 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.
*
* @private
* @function Highcharts.SVGElement#htmlUpdateTransform
*/
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 (!renderer.styledMode && wrapper.shadows) { // used in labels/tooltip
wrapper.shadows.forEach(function (shadow) {
css(shadow, {
marginLeft: translateX + 1,
marginTop: translateY + 1
});
});
}
// apply inversion
if (wrapper.inverted) { // wrapper is a group
[].forEach.call(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
) && (
// Only set the width if the text is able to word-wrap, or
// text-overflow is ellipsis (#9537)
/[ \-]/.test(elem.textContent || elem.innerText) ||
elem.style.textOverflow === 'ellipsis'
)
) { // #983, #1254
css(elem, {
width: textWidth + 'px',
display: 'block',
whiteSpace: whiteSpace || 'normal' // #3331
});
wrapper.oldTextWidth = textWidth;
wrapper.hasBoxWidthChanged = true; // #8159
} else {
wrapper.hasBoxWidthChanged = false; // #8159
}
// Do the calculations and DOM access only if properties changed
if (currentTextTransform !== wrapper.cTT) {
baseline = renderer.fontMetrics(elem.style.fontSize, elem).b;
// Renderer specific handling of span rotation, but only if we
// have something to update.
if (
defined(rotation) &&
(
(rotation !== (wrapper.oldRotation || 0)) ||
(align !== wrapper.oldAlign)
)
) {
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;
wrapper.oldAlign = align;
}
},
/**
* Set the rotation of an individual HTML span.
*
* @private
* @function Highcharts.SVGElement#setSpanRotation
*
* @param {number} rotation
*
* @param {number} alignCorrection
*
* @param {number} baseline
*/
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.
*
* @private
* @function Highcharts.SVGElement#getSpanCorrection
*
* @param {number} width
*
* @param {number} baseline
*
* @param {number} alignCorrection
*/
getSpanCorrection: function (width, baseline, alignCorrection) {
this.xCorr = -width * alignCorrection;
this.yCorr = -baseline;
}
});
// Extend SvgRenderer for useHTML option.
extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
/**
* @private
* @function Highcharts.SVGRenderer#getTransformKey
*
* @return {string}
*/
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.
*
* @private
* @function Highcharts.SVGRenderer#html
*
* @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.
*
* @return {Highcharts.HTMLDOMElement}
*/
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)
['opacity', 'visibility'].forEach(function (prop) {
element[prop + 'Setter'] = function (
value,
key,
elem
) {
SVGElement.prototype[prop + 'Setter']
.call(this, value, key, elem);
style[key] = value;
};
});
element.addedSetters = true;
},
chart = H.charts[renderer.chartIndex],
styledMode = chart && chart.styledMode;
// 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({
position: 'absolute'
});
if (!styledMode) {
wrapper.css({
fontFamily: this.style.fontFamily,
fontSize: this.style.fontSize
});
}
// 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
parents.reverse().forEach(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;
}
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,168 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* 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,
pick = H.pick;
/*
* Methods defined on the Axis prototype
*/
/**
* Set the tick positions of a logarithmic axis.
*
* @private
* @function Highcharts.Axis#getLogTickPositions
*
* @param {number} interval
*
* @param {number} min
*
* @param {number} max
*
* @param {number} minor
*
* @return {Array<number>}
*/
Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
var axis = this,
options = axis.options,
axisLength = axis.len,
// 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;
}
// 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;
if (interval > 0.3) {
intermediate = [1, 2, 4];
// 0.2 equals five minor ticks per 1, 10, 100 etc
} else if (interval > 0.15) {
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 = axis.log2lin(axis.lin2log(i) * intermediate[j]);
// #1670, lastPos is #3113
if (
pos > min &&
(!minor || lastPos <= max) &&
lastPos !== undefined
) {
positions.push(lastPos);
}
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 = axis.lin2log(min),
realMax = axis.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 = normalizeTickInterval(
interval,
null,
getMagnitude(interval)
);
positions = axis.getLinearTickPositions(
interval,
realMin,
realMax
).map(axis.log2lin);
if (!minor) {
axis._minorAutoInterval = interval / 5;
}
}
// Set the axis-level tickInterval variable
if (!minor) {
axis.tickInterval = interval;
}
return positions;
};
/**
* @private
* @function Highcharts.Axis#log2lin
*
* @param {number} num
*
* @return {number}
*/
Axis.prototype.log2lin = function (num) {
return Math.log(num) / Math.LN10;
};
/**
* @private
* @function Highcharts.Axis#lin2log
*
* @param {number} num
*
* @return {number}
*/
Axis.prototype.lin2log = function (num) {
return Math.pow(10, num);
};
@@ -0,0 +1,176 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
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;
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 */ {
/**
* @private
* @function Highcharts.Pointer#onContainerPointerDown
*
* @param {Highcharts.PointerEventObject} e
*/
onContainerPointerDown: function (e) {
translateMSPointer(
e,
'onContainerTouchStart',
'touchstart',
function (e) {
touches[e.pointerId] = {
pageX: e.pageX,
pageY: e.pageY,
target: e.currentTarget
};
}
);
},
/**
* @private
* @function Highcharts.Pointer#onContainerPointerMove
*
* @param {Highcharts.PointerEventObject} e
*/
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;
}
}
);
},
/**
* @private
* @function Highcharts.Pointer#onDocumentPointerUp
*
* @param {Highcharts.PointerEventObject} e
*/
onDocumentPointerUp: function (e) {
translateMSPointer(
e,
'onDocumentTouchEnd',
'touchend',
function (e) {
delete touches[e.pointerId];
}
);
},
/**
* Add or remove the MS Pointer specific events
*
* @private
* @function Highcharts.Pointer#batchMSEvents
*
* @param {Function} fn
*/
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
);
}
});
// 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);
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,412 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Point.js';
var Point = H.Point,
seriesType = H.seriesType,
seriesTypes = H.seriesTypes;
/**
* The ohlc series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.ohlc
*
* @augments Highcharts.Series
*/
seriesType(
'ohlc',
'column'
/**
* An OHLC chart is a style of financial chart used to describe price
* movements over time. It displays open, high, low and close values per
* data point.
*
* @sample stock/demo/ohlc/
* OHLC chart
*
* @extends plotOptions.column
* @excluding borderColor, borderRadius, borderWidth, crisp, stacking,
* stack
* @product highstock
* @optionparent plotOptions.ohlc
*/
, {
/**
* 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`.
*
* @sample {highstock} stock/plotoptions/ohlc-linewidth/
* A greater line width
*
* @type {number}
* @default 1
* @product highstock
*/
lineWidth: 1,
tooltip: {
pointFormat: '<span style="color:{point.color}">\u25CF</span> ' +
'<b> {series.name}</b><br/>' +
'Open: {point.open}<br/>' +
'High: {point.high}<br/>' +
'Low: {point.low}<br/>' +
'Close: {point.close}<br/>'
},
threshold: null,
states: {
/**
* @extends plotOptions.column.states.hover
* @product highstock
*/
hover: {
/**
* The pixel width of the line representing the OHLC point.
*
* @type {number}
* @default 3
* @product highstock
*/
lineWidth: 3
}
},
/**
* Determines which one of `open`, `high`, `low`, `close` values should
* be represented as `point.y`, which is later used to set dataLabel
* position and [compare](#plotOptions.series.compare).
*
* @sample {highstock} stock/plotoptions/ohlc-pointvalkey/
* Possible values
*
* @type {string}
* @default close
* @validvalue ["open", "high", "low", "close"]
* @product highstock
* @apioption plotOptions.ohlc.pointValKey
*/
/**
* Line color for up points.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @product highstock
* @apioption plotOptions.ohlc.upColor
*/
stickyTracking: true
}, /** @lends Highcharts.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',
pointAttrToOptions: {
'stroke': 'color',
'stroke-width': 'lineWidth'
},
/**
* @private
* @function Highcarts.seriesTypes.ohlc#init
*/
init: function () {
seriesTypes.column.prototype.init.apply(this, arguments);
this.options.stacking = false; // #8817
},
/**
* Postprocess mapping between options and SVG attributes
*
* @private
* @function Highcharts.seriesTypes.ohlc#pointAttribs
*
* @param {Highcharts.Point} point
*
* @param {string} state
*
* @return {Highcharts.Dictionary<*>}
*/
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
*
* @private
* @function Highcharts.seriesTypes.ohlc#translate
*/
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
series.points.forEach(function (point) {
[point.open, point.high, point.low, point.close, point.low]
.forEach(
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
*
* @private
* @function Highcharts.seriesTypes.ohlc#drawPoints
*/
drawPoints: function () {
var series = this,
points = series.points,
chart = series.chart;
points.forEach(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 (!chart.styledMode) {
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 Highcharts.seriesTypes.ohlc.prototype.pointClass.prototype */
{
/**
* Extend the parent method by adding up or down to the class name.
*
* @private
* @function Highcharts.seriesTypes.ohlc#getClassName
*/
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).
*
* @extends series,plotOptions.ohlc
* @excluding dataParser, dataURL
* @product highstock
* @apioption series.ohlc
*/
/**
* An array of data points for the series. For the `ohlc` series type,
* points can be given in the following ways:
*
* 1. An array of arrays with 5 or 4 values. In this case, the values correspond
* to `x,open,high,low,close`. If the first value is a string, it is applied
* as the name of the point, and the `x` value is inferred. The `x` value can
* also be omitted, in which case the inner arrays should be of length 4\.
* Then the `x` value is automatically calculated, either starting at 0 and
* incremented by 1, or from `pointStart` and `pointInterval` given in the
* series options.
* ```js
* data: [
* [0, 6, 5, 6, 7],
* [1, 9, 4, 8, 2],
* [2, 6, 3, 4, 10]
* ]
* ```
*
* 2. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.ohlc.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* open: 3,
* high: 4,
* low: 5,
* close: 2,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* open: 4,
* high: 3,
* low: 6,
* close: 7,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @type {Array<Array<(number|string),number,number,number>|Array<(number|string),number,number,number,number>|*>}
* @extends series.arearange.data
* @excluding y, marker
* @product highstock
* @apioption series.ohlc.data
*/
/**
* The closing value of each data point.
*
* @type {number}
* @product highstock
* @apioption series.ohlc.data.close
*/
/**
* The opening value of each data point.
*
* @type {number}
* @product highstock
* @apioption series.ohlc.data.open
*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,962 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Axis.js';
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,
extend = H.extend,
noop = H.noop,
pick = H.pick,
Series = H.Series,
timeUnits = H.timeUnits;
/* ****************************************************************************
* Start ordinal axis logic *
*****************************************************************************/
addEvent(Series, 'updatedData', function () {
var xAxis = this.xAxis;
// Destroy the extended ordinal index on updated data
if (xAxis && xAxis.options.ordinal) {
delete xAxis.ordinalIndex;
}
});
/* *
* In an ordinal axis, there might be areas with dense consentrations of points,
* then large gaps between some. Creating equally distributed ticks over this
* entire range may lead to a huge number of ticks that will later be removed.
* So instead, break the positions up in segments, find the tick positions for
* each segment then concatenize them. This method is used from both data
* grouping logic and X axis tick position logic.
*/
Axis.prototype.getTimeTicks = function (
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,
// Record all the start positions of a segment, to use when deciding
// what's a gap in the data.
segmentStarts = [];
// 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 time.getTimeTicks.apply(time, arguments);
}
// 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 = time.getTimeTicks(
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];
}
segmentStarts.push(groupPositions.length);
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
info.segmentStarts = segmentStarts;
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)) {
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.
*
* @private
* @function Highcharts.Axis#beforeSetTickPositions
*/
beforeSetTickPositions: function () {
var axis = this,
len,
ordinalPositions = [],
uniqueOrdinalPositions,
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,
i,
hasBoostedSeries;
// Apply the ordinal logic
if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
axis.series.forEach(function (series, i) {
uniqueOrdinalPositions = [];
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) {
// without a custom function it is sorted as strings
return a - b;
});
overscrollPointsRange = Math.min(
overscrollPointsRange,
pick(
// Check for a single-point series:
series.closestPointRange,
overscrollPointsRange
)
);
if (len) {
i = 0;
while (i < len - 1) {
if (
ordinalPositions[i] !== ordinalPositions[i + 1]
) {
uniqueOrdinalPositions.push(
ordinalPositions[i + 1]
);
}
i++;
}
// Check first item:
if (
uniqueOrdinalPositions[0] !== ordinalPositions[0]
) {
uniqueOrdinalPositions.unshift(
ordinalPositions[0]
);
}
ordinalPositions = uniqueOrdinalPositions;
}
}
if (series.isSeriesBoosting) {
hasBoostedSeries = true;
}
});
if (hasBoostedSeries) {
ordinalPositions.length = 0;
}
// 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.
*
* @private
* @function Highcharts.Axis#val2lin
*
* @param {number} val
* The axis value.
*
* @param {boolean} toIndex
* Whether to return the index in the ordinalPositions or the new
* value.
*
* @return {number}
*/
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
// something between 0 and 1
distance = (val - ordinalPositions[i]) /
(ordinalPositions[i + 1] - ordinalPositions[i]);
ordinalIndex = i + distance;
break;
}
}
ret = toIndex ?
ordinalIndex :
axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
}
return ret;
},
/**
* Translate from linear (internal) to axis value.
*
* @private
* @function Highcharts.Axis#lin2val
*
* @param {number} val
* The linear abstracted value.
*
* @param {boolean} fromIndex
* Translate from an index in the ordinal positions rather than a
* value.
*
* @return {number}
*/
lin2val: function (val, fromIndex) {
var axis = this,
ordinalPositions = axis.ordinalPositions,
ret;
// the visible range contains only equally spaced values
if (!ordinalPositions) {
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;
// something between 0 and 1
distance = (val - linearEquivalentLeft) /
(linearEquivalentRight - linearEquivalentLeft);
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.
*
* @private
* @function Highcharts.Axis#getExtendedPositions
*
* @return {Array<number>}
*/
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
axis.series.forEach(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,
// doesn't matter which, use the fastest
approximation: 'open',
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 Axis.dataMax, Axis.dataMax + Axis.overscroll
* evenly spaced. Used in panning and navigator scrolling.
*
* @private
* @function Highcharts.Axis#getOverscrollPositions
*
* @returns {Array<number>}
* Generated ticks
*/
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.
*
* @private
* @function Highcharts.Axis#getGroupIntervalFactor
*
* @param {number} xMin
*
* @param {number} xMax
*
* @param {Highcharts.Series} series
*
* @return {number}
*/
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.
*
* @private
* @function Highcharts.Axis#postProcessTickInterval
*
* @param {number} tickInterval
*
* @return {number}
*/
postProcessTickInterval: function (tickInterval) {
// Problem: https://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)
Axis.prototype.ordinal2lin = Axis.prototype.val2lin;
// Extending the Chart.pan method for ordinal axes
addEvent(Chart, 'pan', function (e) {
var chart = this,
xAxis = chart.xAxis[0],
overscroll = xAxis.options.overscroll,
chartX = e.originalEvent.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)
),
// how many ordinal units did we move?
movedUnits = (mouseDownX - chartX) / pointPixelWidth,
// get index of all the chart's points
extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() },
ordinalPositions,
searchAxisLeft,
lin2val = xAxis.lin2val,
val2lin = xAxis.val2lin,
searchAxisRight;
// we have an ordinal axis, but the data is equally spaced
if (!extendedAxis.ordinalPositions) {
runBase = true;
} else if (Math.abs(movedUnits) > 1) {
// Remove active points for shared tooltip
if (hoverPoints) {
hoverPoints.forEach(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,
true // translate from index
]),
lin2val.apply(searchAxisRight, [
val2lin.apply(searchAxisRight, [max, true]) + movedUnits,
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;
}
} else {
e.preventDefault();
}
});
addEvent(Axis, 'foundExtremes', function () {
var axis = this;
if (
axis.isXAxis &&
defined(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 ||
axis.isInternal
) && (
// 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 (!axis.isInternal && defined(axis.userMin)) {
axis.min += axis.options.overscroll;
}
}
});
/* ****************************************************************************
* End ordinal axis logic *
*****************************************************************************/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/* ************************************************************************** *
* Start PlotBand series code *
* ************************************************************************** */
/* This is an experiment of implementing plotBands and plotLines as a series.
It could solve problems with export, updating etc., add tooltip and mouse
events, and provide a more compact and consistent implementation.
Demo: https://jsfiddle.net/highcharts/5Rbf6/ */
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Series.js';
import './Options.js';
var seriesType = H.seriesType,
Series = H.Series;
seriesType('plotband', 'column', {
lineWidth: 0,
threshold: null
}, {
// 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;
Series.prototype.translate.apply(series);
series.points.forEach(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
};
});
}
});
/* ************************************************************************** *
* End PlotBand series code *
* ************************************************************************** */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,681 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* Configuration hash for the data label and tooltip formatters.
*
* @interface Highcharts.PointLabelObject
*//**
* The point's current color.
* @name Highcharts.PointLabelObject#color
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*//**
* 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 Highcharts.PointLabelObject#colorIndex
* @type {number}
*//**
* The name of the related point.
* @name Highcharts.PointLabelObject#key
* @type {number|string}
*//**
* The percentage for related points in a stacked series or pies.
* @name Highcharts.PointLabelObject#percentage
* @type {number}
*//**
* The related point.
* @name Highcharts.PointLabelObject#point
* @type {Highcharts.Point}
*//**
* The related series.
* @name Highcharts.PointLabelObject#series
* @type {Highcharts.Series}
*//**
* The total of values in either a stack for stacked series, or a pie in a pie
* series.
* @name Highcharts.PointLabelObject#total
* @type {number}
*//**
* For categorized axes this property holds the category name for the point. For
* other axes it holds the X value.
* @name Highcharts.PointLabelObject#x
* @type {number|string}
*//**
* The y value of the point.
* @name Highcharts.PointLabelObject#y
* @type {number|undefined}
*/
'use strict';
import Highcharts from './Globals.js';
import './Utilities.js';
var Point,
H = Highcharts,
extend = H.extend,
erase = H.erase,
fireEvent = H.fireEvent,
format = H.format,
isArray = H.isArray,
isNumber = H.isNumber,
pick = H.pick,
uniqueKey = H.uniqueKey,
defined = H.defined,
removeEvent = H.removeEvent;
/**
* 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}.
*
* @class
* @name Highcharts.Point
*/
Highcharts.Point = Point = function () {};
Highcharts.Point.prototype = {
/**
* Initialize the point. Called internally based on the `series.data`
* option.
*
* @function Highcharts.Point#init
*
* @param {Highcharts.Series} series
* The series object containing this point.
*
* @param {number|object|Array<number|string>|null} options
* The data in either number, array or object format.
*
* @param {number} [x]
* Optionally, the X value of the point.
*
* @return {Highcharts.Point}
* The Point instance.
*
* @fires Highcharts.Point#event:afterInit
*/
init: function (series, options, x) {
var point = this,
colors,
optionsChart = series.chart.options.chart,
colorCount = optionsChart.colorCount,
styledMode = series.chart.styledMode,
colorIndex;
/**
* The series object associated with the point.
*
* @name Highcharts.Point#series
* @type {Highcharts.Series}
*/
point.series = series;
/**
* The point's current color.
*
* @name Highcharts.Point#color
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
*/
if (!styledMode) {
point.color = series.color; // #3445
}
point.applyOptions(options, x);
// Add a unique ID to the point if none is assigned
point.id = defined(point.id) ? point.id : uniqueKey();
if (series.options.colorByPoint) {
if (!styledMode) {
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 Highcharts.Point#colorIndex
* @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
* @function Highcharts.Point#applyOptions
*
* @param {number|object|Array<number|string>|null} options
* The point options as defined in series.data.
*
* @param {number} [x]
* Optionally, the x value.
*
* @return {Highcharts.Point}
* 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);
/**
* The point's options as applied in the initial configuration, or
* extended through `Point.update`.
* @name Highcharts.Point#options
* @type {object}
*/
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;
}
if (options.dataLabels) {
delete point.dataLabels;
}
/**
* The y value of the point.
* @name Highcharts.Point#y
* @type {number|undefined}
*/
// 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';
}
/**
* The x value of the point.
* @name Highcharts.Point#x
* @type {number}
*/
// 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.
*
* @function Highcharts.Point#setNestedProperty
*
* @param {object} object
* The object to set the value on.
*
* @param {*} 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('.');
nestedKeys.reduce(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 }`.
*
* @function Highcharts.Point#optionsToObject
*
* @param {number|object|Array<number|string>|null} options
* The input option.
*
* @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.
*
* @function Highcharts.Point#getClassName
*
* @return {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.
*
* @function Highcharts.Point#getZone
*
* @return {Highcharts.PlotSeriesZonesOptions}
* 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];
}
// For resetting or reusing the point (#8100)
if (!this.nonZonedColor) {
this.nonZonedColor = this.color;
}
if (zone && zone.color && !this.options.color) {
this.color = zone.color;
} else {
this.color = this.nonZonedColor;
}
return zone;
},
/**
* Destroy a point to clear memory. Its reference still stays in
* `series.data`.
*
* @private
* @function Highcharts.Point#destroy
*/
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 and elements
if (point.graphic || point.dataLabel || point.dataLabels) {
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
* @function Highcharts.Point#destroyElements
*/
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();
}
}
// Handle point.dataLabels and point.connectors
if (point.dataLabels) {
point.dataLabels.forEach(function (label) {
if (label.element) {
label.destroy();
}
});
delete point.dataLabels;
}
if (point.connectors) {
point.connectors.forEach(function (connector) {
if (connector.element) {
connector.destroy();
}
});
delete point.connectors;
}
},
/**
* Return the configuration hash needed for the data label and tooltip
* formatters.
*
* @function Highcharts.Point#getLabelConfig
*
* @return {Highcharts.PointLabelObject}
* 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.
*
* @function Highcharts.Point#tooltipFormatter
*
* @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 || '';
// Replace default point style with class name
if (series.chart.styledMode) {
pointFormat = series.chart.tooltip.styledModeFormat(pointFormat);
}
// Loop over the point array map and replace unformatted values with
// sprintf formatting markup
(series.pointArrayMap || ['y']).forEach(function (key) {
key = '{point.' + key; // without the closing bracket
if (valuePrefix || valueSuffix) {
pointFormat = pointFormat.replace(
RegExp(key + '}', 'g'),
valuePrefix + key + '}' + valueSuffix
);
}
pointFormat = pointFormat.replace(
RegExp(key + '}', 'g'),
key + ':,.' + valueDecimals + 'f}'
);
});
return format(pointFormat, {
point: this,
series: this.series
}, series.chart.time);
},
/**
* Fire an event on the Point object.
*
* @private
* @function Highcharts.Point#firePointEvent
*
* @param {string} eventType
* Type of the event.
*
* @param {object} eventArgs
* Additional event arguments.
*
* @param {Function} defaultFunction
* Default event handler.
*
* @fires Highcharts.Point#event:*
*/
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 categorized axes this property holds the category name for the
* point. For other axes it holds the X value.
*
* @name Highcharts.Point#category
* @type {number|string}
*/
/**
* 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
* // Array config
* data: [
* ['John', 1],
* ['Jane', 2]
* ]
*
* // Object config
* data: [{
* name: 'John',
* y: 1
* }, {
* name: 'Jane',
* y: 2
* }]
*
* @name Highcharts.Point#name
* @type {string}
*/
/**
* The percentage for points in a stacked series or pies.
*
* @name Highcharts.Point#percentage
* @type {number}
*/
/**
* The total of values in either a stack for stacked series, or a pie in a
* pie series.
*
* @name Highcharts.Point#total
* @type {number}
*/
/**
* For certain series types, like pie charts, where individual points can
* be shown or hidden.
*
* @name Highcharts.Point#visible
* @type {boolean}
*/
visible: true
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,295 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* A callback function to gain complete control on when the responsive rule
* applies.
*
* @callback Highcharts.ResponsiveCallbackFunction
*
* @return {boolean}
* Return `true` if it applies.
*/
'use strict';
import H from './Globals.js';
import './Chart.js';
import './Utilities.js';
var Chart = H.Chart,
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
* @sample highcharts/responsive/classname/
* Class name
*
* @since 5.0.0
* @apioption responsive
*/
/**
* A set of rules for responsive settings. The rules are executed from
* the top down.
*
* @sample {highcharts} highcharts/responsive/axis/
* Axis changes
* @sample {highstock} highcharts/responsive/axis/
* Axis changes
* @sample {highmaps} highcharts/responsive/axis/
* Axis changes
*
* @type {Array<*>}
* @since 5.0.0
* @apioption responsive.rules
*/
/**
* 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
* an existing object. If an existing object of the same id is not found,
* 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.
*
* @sample {highstock} stock/demo/responsive/
* Stock chart
* @sample highcharts/responsive/axis/
* Axis
* @sample highcharts/responsive/legend/
* Legend
* @sample highcharts/responsive/classname/
* Class name
*
* @type {Highcharts.Options}
* @since 5.0.0
* @apioption responsive.rules.chartOptions
*/
/**
* Under which conditions the rule applies.
*
* @since 5.0.0
* @apioption responsive.rules.condition
*/
/**
* A callback function to gain complete control on when the responsive
* 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 {Highcharts.ResponsiveCallbackFunction}
* @since 5.0.0
* @context Highcharts.Chart
* @apioption responsive.rules.condition.callback
*/
/**
* The responsive rule applies if the chart height is less than this.
*
* @type {number}
* @since 5.0.0
* @apioption responsive.rules.condition.maxHeight
*/
/**
* The responsive rule applies if the chart width is less than this.
*
* @sample highcharts/responsive/axis/
* Max width is 500
*
* @type {number}
* @since 5.0.0
* @apioption responsive.rules.condition.maxWidth
*/
/**
* The responsive rule applies if the chart height is greater than this.
*
* @type {number}
* @default 0
* @since 5.0.0
* @apioption responsive.rules.condition.minHeight
*/
/**
* The responsive rule applies if the chart width is greater than this.
*
* @type {number}
* @default 0
* @since 5.0.0
* @apioption responsive.rules.condition.minWidth
*/
/**
* Update the chart based on the current chart/document size and options for
* responsiveness.
*
* @private
* @function Highcharts.Chart#setResponsive
*
* @param {boolean} [redraw=true]
* @param {Array} [reset=false]
* Reset by un-applying all rules. Chart.update resets all rules before
* applying updated options.
*/
Chart.prototype.setResponsive = function (redraw, reset) {
var options = this.options.responsive,
ruleIds = [],
currentResponsive = this.currentResponsive,
currentRuleIds,
undoOptions;
if (!reset && options && options.rules) {
options.rules.forEach(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, ruleIds.map(function (ruleId) {
return H.find(options.rules, function (rule) {
return rule._id === ruleId;
}).chartOptions;
}));
mergedOptions.isResponsiveOptions = true;
// 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
undoOptions = this.currentOptions(mergedOptions);
undoOptions.isResponsiveOptions = true;
this.currentResponsive = {
ruleIds: ruleIds,
mergedOptions: mergedOptions,
undoOptions: undoOptions
};
this.update(mergedOptions, redraw);
} else {
this.currentResponsive = undefined;
}
}
};
/**
* Handle a single responsiveness rule.
*
* @private
* @function Highcharts.Chart#matchResponsiveRule
*
* @param {Highcharts.ResponsiveRulesConditionOptions} rule
*
* @param {Array<number>} matches
*/
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);
}
};
/**
* Get the current values for a given set of options. Used before we update
* the chart with a new responsiveness rule.
* TODO: Restore axis options (by id?)
*
* @private
* @function Highcharts.Chart#currentOptions
*
* @param {Highcharts.Options} options
*
* @return {Highcharts.Options}
*/
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 && ['series', 'xAxis', 'yAxis'].indexOf(key) > -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;
};
@@ -0,0 +1,278 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Options.js';
import './Series.js';
var Series = H.Series,
seriesType = H.seriesType;
/**
* Scatter series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.scatter
*
* @augments Highcharts.Series
*/
seriesType(
'scatter',
'line',
/**
* A scatter plot uses cartesian coordinates to display values for two
* variables for a set of data.
*
* @sample {highcharts} highcharts/demo/scatter/
* Scatter plot
*
* @extends plotOptions.line
* @excluding pointPlacement, shadow, useOhlcData
* @product highcharts highstock
* @optionparent plotOptions.scatter
*/
{
/**
* 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',
/**
* Apply a jitter effect for the rendered markers. When plotting
* discrete values, a little random noise may help telling the points
* apart. The jitter setting applies a random displacement of up to `n`
* axis units in either direction. So for example on a horizontal X
* axis, setting the `jitter.x` to 0.24 will render the point in a
* random position between 0.24 units to the left and 0.24 units to the
* right of the true axis position. On a category axis, setting it to
* 0.5 will fill up the bin and make the data appear continuous.
*
* When rendered on top of a box plot or a column series, a jitter value
* of 0.24 will correspond to the underlying series' default
* [groupPadding](
* https://api.highcharts.com/highcharts/plotOptions.column.groupPadding)
* and [pointPadding](
* https://api.highcharts.com/highcharts/plotOptions.column.pointPadding)
* settings.
*
* @sample {highcharts} highcharts/series-scatter/jitter
* Jitter on a scatter plot
*
* @sample {highcharts} highcharts/series-scatter/jitter-boxplot
* Jittered scatter plot on top of a box plot
*
* @product highcharts highstock
* @since 7.0.2
*/
jitter: {
/**
* The maximal X offset for the random jitter effect.
*/
x: 0,
/**
* The maximal Y offset for the random jitter effect.
*/
y: 0
},
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
*/
/**
* A configuration object for the tooltip rendering of each single
* series. Properties are inherited from [tooltip](#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: {
headerFormat:
'<span style="color:{point.color}">\u25CF</span> ' +
'<span style="font-size: 10px"> {series.name}</span><br/>',
pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
}
// Prototype members
}, {
sorted: false,
requireSorting: false,
noSharedTooltip: true,
trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
takeOrdinalPosition: false, // #2342
/**
* @private
* @function Highcharts.seriesTypes.scatter#drawGraph
*/
drawGraph: function () {
if (this.options.lineWidth) {
Series.prototype.drawGraph.call(this);
}
},
// Optionally add the jitter effect
applyJitter: function () {
var series = this,
jitter = this.options.jitter,
len = this.points.length;
// Return a repeatable, pseudo-random number based on an integer
// seed
function unrandom(seed) {
var rand = Math.sin(seed) * 10000;
return rand - Math.floor(rand);
}
if (jitter) {
this.points.forEach(function (point, i) {
['x', 'y'].forEach(function (dim, j) {
var axis,
plotProp = 'plot' + dim.toUpperCase(),
min,
max,
translatedJitter;
if (jitter[dim] && !point.isNull) {
axis = series[dim + 'Axis'];
translatedJitter = jitter[dim] * axis.transA;
if (axis && !axis.isLog) {
// Identify the outer bounds of the jitter range
min = Math.max(
0,
point[plotProp] - translatedJitter
);
max = Math.min(
axis.len,
point[plotProp] + translatedJitter
);
// Find a random position within this range
point[plotProp] = min +
(max - min) * unrandom(i + j * len);
// Update clientX for the tooltip k-d-tree
if (dim === 'x') {
point.clientX = point.plotX;
}
}
}
});
});
}
}
}
);
H.addEvent(Series, 'afterTranslate', function () {
if (this.applyJitter) {
this.applyJitter();
}
});
/**
* A `scatter` series. If the [type](#series.scatter.type) option is
* not specified, it is inherited from [chart.type](#chart.type).
*
* @extends series,plotOptions.scatter
* @excluding dataParser, dataURL, useOhlcData
* @product highcharts highstock
* @apioption series.scatter
*/
/**
* 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],
* [1, 8],
* [2, 9]
* ]
* ```
*
* 3. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.scatter.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* y: 2,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 4,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @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
*
* @type {Array<number|Array<(number|string),number>|*>}
* @extends series.line.data
* @product highcharts highstock
* @apioption series.scatter.data
*/
@@ -0,0 +1,266 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*
* Highcharts feature to make the Y axis stay fixed when scrolling the chart
* horizontally on mobile devices. Supports left and right side axes.
*/
'use strict';
import H from './Globals.js';
var addEvent = H.addEvent,
Chart = H.Chart;
/**
* Options for a scrollable plot area. This feature provides a minimum width for
* the plot area of the chart. If the width gets smaller than this, typically
* on mobile devices, a native browser scrollbar is presented below the chart.
* This scrollbar provides smooth scrolling for the contents of the plot area,
* whereas the title, legend and axes are fixed.
*
* @sample {highcharts} highcharts/chart/scrollable-plotarea
* Scrollable plot area
*
* @since 6.1.0
* @product highcharts gantt
* @apioption chart.scrollablePlotArea
*/
/**
* The minimum width for the plot area. If it gets smaller than this, the plot
* area will become scrollable.
*
* @type {number}
* @apioption chart.scrollablePlotArea.minWidth
*/
/**
* The initial scrolling position of the scrollable plot area. Ranges from 0 to
* 1, where 0 aligns the plot area to the left and 1 aligns it to the right.
* Typically we would use 1 if the chart has right aligned Y axes.
*
* @type {number}
* @apioption chart.scrollablePlotArea.scrollPositionX
*/
addEvent(Chart, 'afterSetChartSize', function (e) {
var scrollablePlotArea = this.options.chart.scrollablePlotArea,
scrollableMinWidth =
scrollablePlotArea && scrollablePlotArea.minWidth,
scrollablePixels;
if (scrollableMinWidth && !this.renderer.forExport) {
// The amount of pixels to scroll, the difference between chart
// width and scrollable width
this.scrollablePixels = scrollablePixels = Math.max(
0,
scrollableMinWidth - this.chartWidth
);
if (scrollablePixels) {
this.plotWidth += scrollablePixels;
this.clipBox.width += scrollablePixels;
if (!e.skipAxes) {
this.axes.forEach(function (axis) {
if (axis.side === 1) {
// Get the plot lines right in getPlotLinePath,
// temporarily set it to the adjusted plot width.
axis.getPlotLinePath = function () {
var right = this.right,
path;
this.right = right - axis.chart.scrollablePixels;
path = H.Axis.prototype.getPlotLinePath.apply(
this,
arguments
);
this.right = right;
return path;
};
} else {
// Apply the corrected plotWidth
axis.setAxisSize();
axis.setAxisTranslation();
}
});
}
}
}
});
addEvent(Chart, 'render', function () {
if (this.scrollablePixels) {
if (this.setUpScrolling) {
this.setUpScrolling();
}
this.applyFixed();
} else if (this.fixedDiv) { // Has been in scrollable mode
this.applyFixed();
}
});
/**
* @private
* @function Highcharts.Chart#setUpScrolling
*/
Chart.prototype.setUpScrolling = function () {
// Add the necessary divs to provide scrolling
this.scrollingContainer = H.createElement('div', {
'className': 'highcharts-scrolling'
}, {
overflowX: 'auto',
WebkitOverflowScrolling: 'touch'
}, this.renderTo);
this.innerContainer = H.createElement('div', {
'className': 'highcharts-inner-container'
}, null, this.scrollingContainer);
// Now move the container inside
this.innerContainer.appendChild(this.container);
// Don't run again
this.setUpScrolling = null;
};
/**
* @private
* @function Highcharts.Chart#applyFixed
*/
Chart.prototype.applyFixed = function () {
var container = this.container,
fixedRenderer,
scrollableWidth,
firstTime = !this.fixedDiv;
// First render
if (firstTime) {
this.fixedDiv = H.createElement(
'div',
{
className: 'highcharts-fixed'
},
{
position: 'absolute',
overflow: 'hidden',
pointerEvents: 'none',
zIndex: 2
},
null,
true
);
this.renderTo.insertBefore(
this.fixedDiv,
this.renderTo.firstChild
);
this.renderTo.style.overflow = 'visible';
this.fixedRenderer = fixedRenderer = new H.Renderer(
this.fixedDiv,
0,
0
);
// Mask
this.scrollableMask = fixedRenderer.path()
.attr({
fill: H.color(
this.options.chart.backgroundColor || '#fff'
).setOpacity(0.85).get(),
zIndex: -1
})
.addClass('highcharts-scrollable-mask')
.add();
// These elements are moved over to the fixed renderer and stay fixed
// when the user scrolls the chart.
([
this.inverted ?
'.highcharts-xaxis' :
'.highcharts-yaxis',
this.inverted ?
'.highcharts-xaxis-labels' :
'.highcharts-yaxis-labels',
'.highcharts-contextbutton',
'.highcharts-credits',
'.highcharts-legend',
'.highcharts-subtitle',
'.highcharts-title',
'.highcharts-legend-checkbox'
]).forEach(function (className) {
[].forEach.call(
container.querySelectorAll(className),
function (elem) {
(
elem.namespaceURI === fixedRenderer.SVG_NS ?
fixedRenderer.box :
fixedRenderer.box.parentNode
).appendChild(elem);
elem.style.pointerEvents = 'auto';
}
);
});
}
// Set the size of the fixed renderer to the visible width
this.fixedRenderer.setSize(
this.chartWidth,
this.chartHeight
);
// Increase the size of the scrollable renderer and background
scrollableWidth = this.chartWidth + this.scrollablePixels;
H.stop(this.container);
this.container.style.width = scrollableWidth + 'px';
this.renderer.boxWrapper.attr({
width: scrollableWidth,
height: this.chartHeight,
viewBox: [0, 0, scrollableWidth, this.chartHeight].join(' ')
});
this.chartBackground.attr({ width: scrollableWidth });
// Set scroll position
if (firstTime) {
var options = this.options.chart.scrollablePlotArea;
if (options.scrollPositionX) {
this.scrollingContainer.scrollLeft =
this.scrollablePixels * options.scrollPositionX;
}
}
// Mask behind the left and right side
var axisOffset = this.axisOffset,
maskTop = this.plotTop - axisOffset[0] - 1,
maskBottom = this.plotTop + this.plotHeight + axisOffset[2],
maskPlotRight = this.plotLeft + this.plotWidth -
this.scrollablePixels;
this.scrollableMask.attr({
d: this.scrollablePixels ? [
// Left side
'M', 0, maskTop,
'L', this.plotLeft - 1, maskTop,
'L', this.plotLeft - 1, maskBottom,
'L', 0, maskBottom,
'Z',
// Right side
'M', maskPlotRight, maskTop,
'L', this.chartWidth, maskTop,
'L', this.chartWidth, maskBottom,
'L', maskPlotRight, maskBottom,
'Z'
] : ['M', 0, 0]
});
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,267 @@
/* *
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Options.js';
import './Series.js';
var pick = H.pick,
seriesType = H.seriesType;
/**
* Spline series type.
*
* @private
* @class
* @name Highcharts.seriesTypes.spline
*
* @augments Highcarts.Series
*/
seriesType(
'spline',
'line',
/**
* A spline series is a special type of line series, where the segments
* between the data points are smoothed.
*
* @sample {highcharts} highcharts/demo/spline-irregular-time/
* Spline chart
* @sample {highstock} stock/demo/spline/
* Spline chart
*
* @extends plotOptions.series
* @excluding step
* @product highcharts highstock
* @optionparent plotOptions.spline
*/
{
},
/** @lends seriesTypes.spline.prototype */ {
/**
* Get the spline segment from a given point's previous neighbour to the
* given point.
*
* @private
* @function Highcharts.seriesTypes.spline#getPointSpline
*
* @param {Array<Highcharts.Point>}
*
* @param {Highcharts.Point} point
*
* @param {number} i
*
* @return {Highcharts.SVGPathArray}
*/
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
}
// 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;
// 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;
// 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;
}
// 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).
*
* @extends series,plotOptions.spline
* @excluding dataParser, dataURL, step
* @product highcharts highstock
* @apioption series.spline
*/
/**
* 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],
* [1, 2],
* [2, 8]
* ]
* ```
*
* 3. An array of objects with named values. The following snippet shows only a
* few settings, see the complete options set below. If the total number of
* data points exceeds the series'
* [turboThreshold](#series.spline.turboThreshold), this option is not
* available.
* ```js
* data: [{
* x: 1,
* y: 9,
* name: "Point2",
* color: "#00FF00"
* }, {
* x: 1,
* y: 0,
* name: "Point1",
* color: "#FF00FF"
* }]
* ```
*
* @sample {highcharts} highcharts/chart/reflow-true/
* Numerical values
* @sample {highcharts} highcharts/series/data-array-of-arrays/
* Arrays of numeric x and y
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
* Arrays of datetime x and y
* @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
*
* @type {Array<number|Array<(number|string),number>|*>}
* @extends series.line.data
* @product highcharts highstock
* @apioption series.spline.data
*/
@@ -0,0 +1,604 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
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,
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
* or positive, has its own stack item.
*
* @private
* @class
* @name Highcharts.StackItem
*
* @param {Highcharts.Axis} axis
*
* @param {Highcharts.Options} options
*
* @param {boolean} isNegative
*
* @param {number} x
*
* @param {string|*} stackOption
*/
H.StackItem = function (axis, options, isNegative, x, stackOption) {
var inverted = axis.chart.inverted;
this.axis = axis;
// Tells if the stack is negative
this.isNegative = isNegative;
// 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;
// Initialize total value
this.total = null;
// 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;
// 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');
};
H.StackItem.prototype = {
/**
* @private
* @function Highcharts.StackItem#destroy
*/
destroy: function () {
destroyObjectProperties(this, this.axis);
},
/**
* Renders the stack total label and adds it to the stack label group.
*
* @private
* @function Highcharts.StackItem#render
*
* @param {Highcharts.SVGElement} 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
}
// Rank it higher than data labels (#8742)
this.label.labelrank = chart.plotHeight;
},
/**
* Sets the offset that the stack has from the x value and repositions the
* label.
*
* @private
* @function Highcarts.StackItem#setOffset
*
* @param {number} xOffset
*
* @param {number} xWidth
*/
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 = defined(y) && Math.abs(y - yZero), // stack height
x = chart.xAxis[0].translate(stackItem.x) + xOffset, // x position
stackBox = defined(y) && stackItem.getStackBox(
chart,
stackItem,
x,
y,
xWidth,
h,
axis
),
label = stackItem.label,
alignAttr;
if (label && stackBox) {
// 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);
}
},
/**
* @private
* @function Highcharts.StackItem#getStackBox
*
* @param {Highcharts.Chart} chart
*
* @param {Highcharts.StackItem} stackItem
*
* @param {number} x
*
* @param {number} y
*
* @param {number} xWidth
*
* @param {number} h
*
* @param {Highcharts.Axis} axis
*
* @return {*}
*/
getStackBox: function (chart, stackItem, x, y, xWidth, h, axis) {
var reversed = stackItem.axis.reversed,
inverted = chart.inverted,
axisPos = axis.height + axis.pos - (inverted ? chart.plotLeft :
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
*
* @private
* @function Highcharts.Chart#getStacks
*/
Chart.prototype.getStacks = function () {
var chart = this;
// reset stacks for each yAxis
chart.yAxis.forEach(function (axis) {
if (axis.stacks && axis.hasVisibleSeries) {
axis.oldStacks = axis.stacks;
}
});
chart.series.forEach(function (series) {
if (series.options.stacking && (series.visible === true ||
chart.options.chart.ignoreHiddenSeries === false)) {
series.stackKey = series.type + pick(series.options.stack, '');
}
});
};
// Stacking methods defined on the Axis prototype
/**
* Build the stacks from top down
*
* @private
* @function Highcharts.Axis#buildStacks
*/
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();
}
}
};
/**
* @private
* @function Highcharts.Axis#renderStackTotals
*/
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);
});
});
};
/**
* Set all the stacks to initial states and destroy unused ones.
*
* @private
* @function Highcharts.Axis#resetStacks
*/
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;
}
});
});
}
};
/**
* @private
* @function Highcharts.Axis#cleanStacks
*/
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;
});
});
}
};
// Stacking methods defnied for Series prototype
/**
* Adds series' points value to corresponding stack
*
* @private
* @function Highcharts.Series#setStackedPoints
*/
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 = {};
};
/**
* Iterate over all stacks and compute the absolute values to percent
*
* @private
* @function Highcharts.Series#modifyStacks
*/
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
[stackKey, '-' + stackKey].forEach(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%.
*
* @private
* @function Highcharts.Series#percentStacker
*
* @param {Array<number>} pointExtremes
*
* @param {Highcharts.StackItem} stack
*
* @param {number} i
*/
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];
};
/**
* Get stack indicator, according to it's x-value, to determine points with the
* same x-value
*
* @private
* @function Highcharts.Series#getStackIndicator
*
* @param {*} stackIndicator
*
* @param {number} x
*
* @param {number} index
*
* @param {string} key
*
* @return {*}
*/
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;
};
@@ -0,0 +1,951 @@
/* *
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from './Globals.js';
import './Utilities.js';
import './Chart.js';
import './Axis.js';
import './Point.js';
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,
extend = H.extend,
format = H.format,
isNumber = H.isNumber,
isString = H.isString,
merge = H.merge,
pick = H.pick,
Point = H.Point,
Renderer = H.Renderer,
Series = H.Series,
splat = H.splat,
SVGRenderer = H.SVGRenderer,
VMLRenderer = H.VMLRenderer,
seriesProto = Series.prototype,
seriesInit = seriesProto.init,
seriesProcessData = seriesProto.processData,
pointTooltipFormatter = Point.prototype.tooltipFormatter;
/**
* Compare the values of the series against the first non-null, non-
* zero value in the visible range. The y axis will show percentage
* or absolute change depending on whether `compare` is set to `"percent"`
* or `"value"`. When this is applied to multiple series, it allows
* comparing the development of the series against each other. Adds
* a `change` field to every point object.
*
* @see [compareBase](#plotOptions.series.compareBase)
* @see [Axis.setCompare()](/class-reference/Highcharts.Axis#setCompare)
*
* @sample {highstock} stock/plotoptions/series-compare-percent/
* Percent
* @sample {highstock} stock/plotoptions/series-compare-value/
* Value
*
* @type {string}
* @since 1.0.1
* @product highstock
* @apioption plotOptions.series.compare
*/
/**
* Defines if comparison should start from the first point within the visible
* range or should start from the first point <b>before</b> the range.
* In other words, this flag determines if first point within the visible range
* will have 0% (`compareStart=true`) or should have been already calculated
* according to the previous point (`compareStart=false`).
*
* @sample {highstock} stock/plotoptions/series-comparestart/
* Calculate compare within visible range
*
* @type {boolean}
* @default false
* @since 6.0.0
* @product highstock
* @apioption plotOptions.series.compareStart
*/
/**
* When [compare](#plotOptions.series.compare) is `percent`, this option
* dictates whether to use 0 or 100 as the base of comparison.
*
* @sample {highstock} stock/plotoptions/series-comparebase/
* Compare base is 100
*
* @type {number}
* @default 0
* @since 5.0.6
* @product highstock
* @validvalue [0, 100]
* @apioption plotOptions.series.compareBase
*/
/**
* Factory function for creating new stock charts. Creates a new
* {@link Highcharts.Chart|Chart} object with different default options than the
* basic Chart.
*
* @example
* var chart = Highcharts.stockChart('container', {
* series: [{
* data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
* pointInterval: 24 * 60 * 60 * 1000
* }]
* });
*
* @function Highcharts.stockChart
*
* @param {string|Highcharts.HTMLDOMElement} [renderTo]
* The DOM element to render to, or its id.
*
* @param {Highcharts.Options} options
* The chart options structure as described in the
* [options reference](https://api.highcharts.com/highstock).
*
* @param {Highcharts.ChartCallbackFunction} [callback]
* A function to execute when the chart object is finished loading and
* rendering. In most cases the chart is built in one thread, but in
* Internet Explorer version 8 or less the chart is sometimes
* initialized before the document is ready, and in these cases the
* chart object will not be finished synchronously. As a consequence,
* code that relies on the newly built Chart object should always run in
* the callback. Defining a
* [chart.events.load](https://api.highcharts.com/highstock/chart.events.load)
* handler is equivalent.
*
* @return {Highcharts.Chart}
* The chart object.
*/
H.StockChart = H.stockChart = function (a, b, c) {
var hasRenderToArg = isString(a) || a.nodeName,
options = arguments[hasRenderToArg ? 1 : 0],
userOptions = options,
// to increase performance, don't merge the data
seriesOptions = options.series,
defaultOptions = H.getOptions(),
opposite,
// Always disable startOnTick:true on the main axis when the navigator
// is enabled (#1090)
navigatorEnabled = pick(
options.navigator && options.navigator.enabled,
defaultOptions.navigator.enabled,
true
),
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 = splat(options.xAxis || {}).map(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 = splat(options.yAxis || {}).map(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 = userOptions.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
addEvent(Axis, 'autoLabelAlign', function (e) {
var chart = this.chart,
options = this.options,
panes = chart._labelPanes = chart._labelPanes || {},
key,
labelOptions = this.options.labels;
if (this.chart.options.isStock && this.coll === 'yAxis') {
key = options.top + ',' + options.height;
// do it only for the first Y axis of each pane
if (!panes[key] && labelOptions.enabled) {
if (labelOptions.x === 15) { // default
labelOptions.x = 0;
}
if (labelOptions.align === undefined) {
labelOptions.align = 'right';
}
panes[key] = this;
e.align = 'right';
e.preventDefault();
}
}
});
// Clear axis from label panes (#6071)
addEvent(Axis, 'destroy', function () {
var chart = this.chart,
key = this.options && (this.options.top + ',' + this.options.height);
if (key && chart._labelPanes && chart._labelPanes[key] === this) {
delete chart._labelPanes[key];
}
});
// Override getPlotLinePath to allow for multipane charts
addEvent(Axis, 'getPlotLinePath', function (e) {
var axis = this,
series = (
this.isLinked && !this.series ?
this.linkedParent.series :
this.series
),
chart = axis.chart,
renderer = chart.renderer,
axisLeft = axis.left,
axisTop = axis.top,
x1,
y1,
x2,
y2,
result = [],
axes = [], // #3416 need a default array
axes2,
uniqueAxes,
translatedValue = e.translatedValue,
value = e.value,
force = e.force,
transVal;
// Return the other axis based on either the axis option or on related
// series.
function getAxis(coll) {
var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis',
opt = axis.options[otherColl];
// Other axis indexed by number
if (isNumber(opt)) {
return [chart[otherColl][opt]];
}
// Other axis indexed by id (like navigator)
if (isString(opt)) {
return [chart.get(opt)];
}
// Auto detect based on existing series
return series.map(function (s) {
return s[otherColl];
});
}
// Ignore in case of colorAxis or zAxis. #3360, #3524, #6720
if (axis.coll === 'xAxis' || axis.coll === 'yAxis') {
e.preventDefault();
// Get the related axes based on series
axes = getAxis(axis.coll);
// Get the related axes based options.*Axis setting #2810
axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis);
axes2.forEach(function (A) {
if (
defined(A.options.id) ?
A.options.id.indexOf('navigator') === -1 :
true
) {
var a = (A.isXAxis ? 'yAxis' : 'xAxis'),
rax = (
defined(A.options[a]) ?
chart[a][A.options[a]] :
chart[a][0]
);
if (axis === rax) {
axes.push(A);
}
}
});
// Remove duplicates in the axes array. If there are no axes in the axes
// array, we are adding an axis without data, so we need to populate
// this with grid lines (#2796).
uniqueAxes = axes.length ?
[] :
[axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742
axes.forEach(function (axis2) {
if (
uniqueAxes.indexOf(axis2) === -1 &&
// Do not draw on axis which overlap completely. #5424
!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, e.old)
);
if (isNumber(transVal)) {
if (axis.horiz) {
uniqueAxes.forEach(function (axis2) {
var skip;
y1 = axis2.pos;
y2 = y1 + axis2.len;
x1 = x2 = Math.round(transVal + axis.transB);
// outside plot area
if (
force !== 'pass' &&
(x1 < axisLeft || x1 > axisLeft + axis.width)
) {
if (force) {
x1 = x2 = Math.min(
Math.max(axisLeft, x1),
axisLeft + axis.width
);
} else {
skip = true;
}
}
if (!skip) {
result.push('M', x1, y1, 'L', x2, y2);
}
});
} else {
uniqueAxes.forEach(function (axis2) {
var skip;
x1 = axis2.pos;
x2 = x1 + axis2.len;
y1 = y2 = Math.round(axisTop + axis.height - transVal);
// outside plot area
if (
force !== 'pass' &&
(y1 < axisTop || y1 > axisTop + axis.height)
) {
if (force) {
y1 = y2 = Math.min(
Math.max(axisTop, y1),
axis.top + axis.height
);
} else {
skip = true;
}
}
if (!skip) {
result.push('M', x1, y1, 'L', x2, y2);
}
});
}
}
e.path = result.length > 0 ?
renderer.crispPolyLine(result, e.lineWidth || 1) :
// #3557 getPlotLinePath in regular Highcharts also returns null
null;
}
});
/**
* Function to crisp a line with multiple segments
*
* @private
* @function Highcharts.SVGRenderer#crispPolyLine
*
* @param {Array<number>} points
*
* @param {number} width
*
* @return {Array<number>}
*/
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;
};
if (Renderer === VMLRenderer) {
VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine;
}
// Wrapper to hide the label
addEvent(Axis, 'afterHideCrosshair', function () {
if (this.crossLabel) {
this.crossLabel = this.crossLabel.hide();
}
});
// Extend crosshairs to also draw the label
addEvent(Axis, 'afterDrawCrosshair', function (event) {
// Check if the label has to be drawn
if (
!defined(this.crosshair.label) ||
!this.crosshair.label.enabled ||
!this.cross
) {
return;
}
var chart = this.chart,
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,
lin2log = this.lin2log,
min,
max;
if (this.isLog) {
min = lin2log(this.min);
max = lin2log(this.max);
} else {
min = this.min;
max = this.max;
}
align = (horiz ? 'center' : opposite ?
(this.labelAlign === 'right' ? 'right' : 'left') :
(this.labelAlign === 'left' ? 'left' : 'center'));
// If the label does not exist yet, create it.
if (!crossLabel) {
crossLabel = this.crossLabel = chart.renderer
.label(
null,
null,
null,
options.shape || 'callout'
)
.addClass(
'highcharts-crosshair-label' + (
this.series[0] &&
' highcharts-color-' + this.series[0].colorIndex
)
)
.attr({
align: options.align || align,
padding: pick(options.padding, 8),
r: pick(options.borderRadius, 3),
zIndex: 2
})
.add(this.labelGroup);
// Presentational
if (!chart.styledMode) {
crossLabel
.attr({
fill: options.backgroundColor ||
(this.series[0] && this.series[0].color) ||
'#666666',
stroke: options.borderColor || '',
'stroke-width': options.borderWidth || 0
})
.css(extend({
color: '#ffffff',
fontWeight: 'normal',
fontSize: '11px',
textAlign: 'center'
}, options.style));
}
}
if (horiz) {
posx = snap ? point.plotX + left : e.chartX;
posy = top + (opposite ? 0 : this.height);
} else {
posx = opposite ? this.width + left : 0;
posy = snap ? point.plotY + top : e.chartY;
}
if (!formatOption && !options.formatter) {
if (this.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 < min || value > max ? 'hidden' : 'visible'
});
crossBox = crossLabel.getBBox();
// now it is placed we can correct its position
if (horiz) {
if ((tickInside && !opposite) || (!tickInside && opposite)) {
posy = crossLabel.y - crossBox.height;
}
} else {
posy = crossLabel.y - (crossBox.height / 2);
}
// check the edges
if (horiz) {
limit = {
left: left - crossBox.x,
right: left + this.width - crossBox.x
};
} else {
limit = {
left: this.labelAlign === 'left' ? left : 0,
right: this.labelAlign === 'right' ?
left + this.width :
chart.chartWidth
};
}
// left edge
if (crossLabel.translateX < limit.left) {
offset = limit.left - crossLabel.translateX;
}
// right edge
if (crossLabel.translateX + crossBox.width >= limit.right) {
offset = -(crossLabel.translateX + crossBox.width - limit.right);
}
// show the crosslabel
crossLabel.attr({
x: posx + offset,
y: posy,
// First set x and y, then anchorX and anchorY, when box is actually
// calculated, #5702
anchorX: horiz ?
posx :
(this.opposite ? 0 : chart.chartWidth),
anchorY: horiz ?
(this.opposite ? chart.chartHeight : 0) :
posy + crossBox.height / 2
});
});
/* ************************************************************************** *
* Start value compare logic *
* ************************************************************************** */
/**
* Extend series.init by adding a method to modify the y value used for plotting
* on the y axis. This method is called both from the axis when finding dataMin
* and dataMax, and from the series.translate method.
*
* @ignore
* @function Highcharts.Series#init
*/
seriesProto.init = function () {
// Call base method
seriesInit.apply(this, arguments);
// Set comparison mode
this.setCompare(this.options.compare);
};
/**
* Highstock only. Set the
* [compare](https://api.highcharts.com/highstock/plotOptions.series.compare)
* mode of the series after render time. In most cases it is more useful running
* {@link Axis#setCompare} on the X axis to update all its series.
*
* @function Highcharts.Series#setCompare
*
* @param {string} compare
* Can be one of `null`, `"percent"` or `"value"`.
*/
seriesProto.setCompare = function (compare) {
// Set or unset the modifyValue method
this.modifyValue = (compare === 'value' || compare === 'percent') ?
function (value, point) {
var compareValue = this.compareValue;
if (
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;
}
};
/**
* Extend series.processData by finding the first y value in the plot area,
* used for comparing the following values
*
* @ignore
* @function Highcharts.Series#processData
*/
seriesProto.processData = function () {
var series = this,
i,
keyIndex = -1,
processedXData,
processedYData,
compareStart = series.options.compareStart === true ? 0 : 1,
length,
compareValue;
// call base method
seriesProcessData.apply(this, arguments);
if (series.xAxis && series.processedYData) { // not pies
// local variables
processedXData = series.processedXData;
processedYData = series.processedYData;
length = processedYData.length;
// For series with more than one value (range, OHLC etc), compare
// against close or the pointValKey (#4922, #3112, #9854)
if (series.pointArrayMap) {
keyIndex = series.pointArrayMap.indexOf(
series.options.pointValKey || series.pointValKey || 'y'
);
}
// find the first value for comparison
for (i = 0; i < length - compareStart; i++) {
compareValue = processedYData[i] && keyIndex > -1 ?
processedYData[i][keyIndex] :
processedYData[i];
if (
isNumber(compareValue) &&
processedXData[i + compareStart] >= series.xAxis.min &&
compareValue !== 0
) {
series.compareValue = compareValue;
break;
}
}
}
};
// Modify series extremes
addEvent(Series, 'afterGetExtremes', function () {
if (this.modifyValue) {
var extremes = [
this.modifyValue(this.dataMin),
this.modifyValue(this.dataMax)
];
this.dataMin = arrayMin(extremes);
this.dataMax = arrayMax(extremes);
}
});
/**
* Highstock only. Set the compare mode on all series belonging to an Y axis
* after render time.
*
* @see [series.plotOptions.compare](https://api.highcharts.com/highstock/series.plotOptions.compare)
*
* @sample stock/members/axis-setcompare/
* Set compoare
*
* @function Highcharts.Axis#setCompare
*
* @param {string} compare
* The compare mode. Can be one of `null`, `"value"` or `"percent"`.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart or to wait for a later call to
* {@link Chart#redraw}.
*/
Axis.prototype.setCompare = function (compare, redraw) {
if (!this.isXAxis) {
this.series.forEach(function (series) {
series.setCompare(compare);
});
if (pick(redraw, true)) {
this.chart.redraw();
}
}
};
/**
* Extend the tooltip formatter by adding support for the point.change variable
* as well as the changeDecimals option.
*
* @ignore
* @function Highcharts.Point#tooltipFormatter
*
* @param {string} pointFormat
*/
Point.prototype.tooltipFormatter = function (pointFormat) {
var point = this;
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 *
* ************************************************************************** */
// Extend the Series prototype to create a separate series clip box. This is
// related to using multiple panes, and a future pane logic should incorporate
// this feature (#2754).
addEvent(Series, 'render', function () {
var clipHeight;
// Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only
// if the series type handles clipping in the animate method (#2975).
if (
!(this.chart.is3d && this.chart.is3d()) &&
!this.chart.polar &&
this.xAxis &&
!this.xAxis.isRadial // Gauge, #6192
) {
// Include xAxis line width, #8031
clipHeight = this.yAxis.len - (this.xAxis.axisLine ?
Math.floor(this.xAxis.axisLine.strokeWidth() / 2) :
0);
// 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 = clipHeight;
// On redrawing, resizing etc, update the clip rectangle
} else if (this.chart[this.sharedClipKey]) {
// animate in case resize is done during initial animation
this.chart[this.sharedClipKey].animate({
width: this.xAxis.len,
height: clipHeight
});
// also change markers clip animation for consistency
// (marker clip rects should exist only on chart init)
if (
this.chart[this.sharedClipKey + 'm']
) {
this.chart[this.sharedClipKey + 'm'].animate({
width: this.xAxis.len
});
}
}
}
});
addEvent(Chart, 'update', function (e) {
var options = e.options;
// Use case: enabling scrollbar from a disabled state.
// Scrollbar needs to be initialized from a controller, Navigator in this
// case (#6615)
if ('scrollbar' in options && this.navigator) {
merge(true, this.options.scrollbar, options.scrollbar);
this.navigator.update({}, false);
delete options.scrollbar;
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,746 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
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;
/**
* The Tick class.
*
* @private
* @class
* @name Highcharts.Tick
*
* @param {Highcharts.Axis} axis
*
* @param {number} pos The position of the tick on the axis.
*
* @param {string} [type] The type of tick.
*
* @param {boolean} [noLabel=false] Wether to disable the label or not. Defaults to
* false.
*
* @param {object} [parameters] Optional parameters for the tick.
*
* @param {object} [parameters.tickmarkOffset] Set tickmarkOffset for the tick.
*
* @param {object} [parameters.category] Set category for the tick.
*/
H.Tick = function (axis, pos, type, noLabel, parameters) {
this.axis = axis;
this.pos = pos;
this.type = type || '';
this.isNew = true;
this.isNewLabel = true;
this.parameters = parameters || {};
// Usually undefined, numeric for grid axes
this.tickmarkOffset = this.parameters.tickmarkOffset;
this.options = this.parameters.options;
if (!type && !noLabel) {
this.addLabel();
}
};
/** @lends Highcharts.Tick.prototype */
H.Tick.prototype = {
/**
* Write the tick label.
*
* @private
* @function Highcharts.Tick#addLabel
*/
addLabel: function () {
var tick = this,
axis = tick.axis,
options = axis.options,
chart = axis.chart,
categories = axis.categories,
names = axis.names,
pos = tick.pos,
labelOptions = pick(
tick.options && tick.options.labels,
options.labels
),
str,
tickPositions = axis.tickPositions,
isFirst = pos === tickPositions[0],
isLast = pos === tickPositions[tickPositions.length - 1],
value = this.parameters.category || (
categories ?
pick(categories[pos], names[pos], pos) :
pos
),
label = tick.label,
tickPositionInfo = tickPositions.info,
dateTimeLabelFormat,
dateTimeLabelFormats,
i,
list;
// 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) {
dateTimeLabelFormats = chart.time.resolveDTLFormat(
options.dateTimeLabelFormats[
(
!options.grid &&
tickPositionInfo.higherRanks[pos]
) ||
tickPositionInfo.unitName
]
);
dateTimeLabelFormat = dateTimeLabelFormats.main;
}
// set properties for access in render method
tick.isFirst = isFirst;
tick.isLast = isLast;
// Get the string
tick.formatCtx = {
axis: axis,
chart: chart,
isFirst: isFirst,
isLast: isLast,
dateTimeLabelFormat: dateTimeLabelFormat,
tickPositionInfo: tickPositionInfo,
value: axis.isLog ? correctFloat(axis.lin2log(value)) : value,
pos: pos
};
str = axis.labelFormatter.call(tick.formatCtx, this.formatCtx);
// Set up conditional formatting based on the format list if existing.
list = dateTimeLabelFormats && dateTimeLabelFormats.list;
if (list) {
tick.shortenLabel = function () {
for (i = 0; i < list.length; i++) {
label.attr({
text: axis.labelFormatter.call(H.extend(
tick.formatCtx,
{ dateTimeLabelFormat: list[i] }
))
});
if (
label.getBBox().width <
axis.getSlotWidth(tick) - 2 *
pick(labelOptions.padding, 5)
) {
return;
}
}
label.attr({
text: ''
});
};
}
// first call
if (!defined(label)) {
tick.label = label =
defined(str) && labelOptions.enabled ?
chart.renderer
.text(
str,
0,
0,
labelOptions.useHTML
)
.add(axis.labelGroup) :
null;
// Un-rotated length
if (label) {
// Without position absolute, IE export sometimes is wrong
if (!chart.styledMode) {
label.css(merge(labelOptions.style));
}
label.textPxLength = label.getBBox().width;
}
// Base value to detect change for new calls to getBBox
tick.rotation = 0;
// update
} else if (label && label.textStr !== str) {
// When resetting text, also reset the width if dynamically set
// (#8809)
if (
label.textWidth &&
!(labelOptions.style && labelOptions.style.width) &&
!label.styles.width
) {
label.css({ width: null });
}
label.attr({ text: str });
}
},
/**
* Get the offset height or width of the label
*
* @private
* @function Highcharts.Tick#getLabelSize
*
* @return {number}
*/
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.
*
* @private
* @function Highcharts.Tick#handleOverflow
*
* @param {Highcharts.PositionObject} xy
*/
handleOverflow: function (xy) {
var tick = this,
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(tick),
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 && pick(labelOptions.overflow, 'justify') === 'justify') {
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) {
if (tick.shortenLabel) {
tick.shortenLabel();
} else {
css.width = Math.floor(textWidth);
if (!(labelOptions.style || {}).textOverflow) {
css.textOverflow = 'ellipsis';
}
label.css(css);
}
}
},
/**
* Get the x and y position for ticks and labels
*
* @private
* @function Highcharts.Tick#getPosition
*
* @param {boolean} horiz
*
* @param {number} tickPos
*
* @param {number} tickmarkOffset
*
* @param {boolean} [old]
*
* @return {number}
*
* @fires Highcharts.Tick#event:afterGetPosition
*/
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
*
* @private
*
*/
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, tickmarkOffset: tickmarkOffset, index: index }
);
return pos;
},
/**
* Extendible method to return the path of the marker
*
* @private
*
*/
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.
*
* @private
*
* @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 = pick(tick.tickmarkOffset, axis.tickmarkOffset),
renderer = axis.chart.renderer,
gridPrefix = type ? type + 'Grid' : 'grid',
gridLineWidth = options[gridPrefix + 'LineWidth'],
gridLineColor = options[gridPrefix + 'LineColor'],
dashStyle = options[gridPrefix + 'LineDashStyle'];
if (!gridLine) {
if (!axis.chart.styledMode) {
attribs.stroke = gridLineColor;
attribs['stroke-width'] = gridLineWidth;
if (dashStyle) {
attribs.dashstyle = dashStyle;
}
}
if (!type) {
attribs.zIndex = 1;
}
if (old) {
opacity = 0;
}
tick.gridLine = gridLine = renderer.path()
.attr(attribs)
.addClass(
'highcharts-' + (type ? type + '-' : '') + 'grid-line'
)
.add(axis.gridGroup);
}
if (gridLine) {
gridLinePath = axis.getPlotLinePath(
pos + tickmarkOffset,
gridLine.strokeWidth() * reverseCrisp,
old,
'pass'
);
// If the parameter 'old' is set, the current call will be followed
// by another call, therefore do not do any animations this time
if (gridLinePath) {
gridLine[old || tick.isNew ? 'attr' : 'animate']({
d: gridLinePath,
opacity: opacity
});
}
}
},
/**
* Renders the tick mark.
*
* @private
*
* @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,
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 (!axis.chart.styledMode) {
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.
*
* @private
*
* @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 = pick(tick.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
*
* @private
*
* @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 = pick(tick.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
*
* @private
* @function Highcharts.Tick#destroy
*/
destroy: function () {
destroyObjectProperties(this, this.axis);
}
};
@@ -0,0 +1,881 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* Normalized interval.
*
* @interface Highcharts.NormalizedIntervalObject
*//**
* The interval in axis values (ms).
*
* @name Highcharts.NormalizedIntervalObject#unitRange
* @type {number}
*//**
* The count.
*
* @name Highcharts.NormalizedIntervalObject#count
* @type {number}
*/
/**
* Function of an additional date format specifier.
*
* @callback Highcharts.TimeFormatCallbackFunction
*
* @param {number} timestamp
* The time to format.
*
* @return {string}
* The formatted portion of the date.
*/
/**
* Additonal time tick information.
*
* @interface Highcharts.TimeTicksInfoObject
* @augments Highcharts.NormalizedIntervalObject
*//**
* @name Highcharts.TimeTicksInfoObject#higherRanks
* @type {Array<string>}
*//**
* @name Highcharts.TimeTicksInfoObject#totalRange
* @type {number}
*/
/**
* Time ticks.
*
* @interface Highcharts.TimeTicksObject
* @augments Array<number>
*//**
* @name Highcharts.TimeTicksObject#info
* @type {Highcharts.TimeTicksInfoObject}
*/
'use strict';
import Highcharts from './Globals.js';
var H = Highcharts,
defined = H.defined,
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
* `Highcharts.setOptions`, or individually for each Chart item through the
* [time](https://api.highcharts.com/highcharts/time) options set.
*
* The Time object is available from {@link Highcharts.Chart#time},
* which refers to `Highcharts.time` if no individual time settings are
* applied.
*
* @example
* // 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())
* );
*
* @class
* @name Highcharts.Time
*
* @param {Highcharts.TimeOptions} options
* Time options as defined in [chart.options.time](/highcharts/time).
*
* @since 6.0.5
*/
Highcharts.Time = function (options) {
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.
*
* @sample {highcharts} highcharts/time/useutc-true/
* True by default
* @sample {highcharts} highcharts/time/useutc-false/
* False
*
* @type {boolean}
* @default true
* @apioption time.useUTC
*/
/**
* 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 {*}
* @since 4.0.4
* @product highcharts highstock gantt
* @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.
*
* @see [global.timezoneOffset](#global.timezoneOffset)
*
* @sample {highcharts|highstock} highcharts/time/gettimezoneoffset/
* Use moment.js to draw Oslo time regardless of browser locale
*
* @type {Function}
* @since 4.1.0
* @product highcharts highstock gantt
* @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.
*
* @see [getTimezoneOffset](#time.getTimezoneOffset)
*
* @sample {highcharts|highstock} highcharts/time/timezone/
* Europe/Oslo
*
* @type {string}
* @since 5.0.7
* @product highcharts highstock gantt
* @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.
*
* @see [time.getTimezoneOffset](#time.getTimezoneOffset)
*
* @sample {highcharts|highstock} highcharts/time/timezoneoffset/
* Timezone offset
*
* @type {number}
* @default 0
* @since 3.0.8
* @product highcharts highstock gantt
* @apioption time.timezoneOffset
*/
defaultOptions: {},
/**
* Update the Time object with current options. It is called internally on
* initializing Highcharts, after running `Highcharts.setOptions` and on
* `Chart.update`.
*
* @private
* @function Highcharts.Time#update
*
* @param {Highcharts.TimeOptions} options
*/
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 || 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 Highcharts.Time#getTimezoneOffset
*
* @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 (
unit === 'Milliseconds' ||
unit === 'Seconds' ||
// If we're dealting with minutes, we only need to
// consider timezone if we're in Indian time zones with
// half-hour offsets (#8768).
(
unit === 'Minutes' &&
date.getTimezoneOffset() % 60 === 0
)
) {
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.
*
* @function Highcharts.Time#makeTime
*
* @param {number} year
* The year
*
* @param {number} month
* The month. Zero-based, so January is 0.
*
* @param {number} [date=1]
* The day of the month
*
* @param {number} [hours=0]
* The hour of the day, 0-23.
*
* @param {number} [minutes=0]
* The minutes
*
* @param {number} [seconds=0]
* 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
* @function Highcharts.Time#timezoneOffsetFunction
*
* @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.
*
* @function Highcharts.Time#dateFormat
*
* @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.
*
* @return {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),
// Month number, 1 through 12 (#8150)
'o': 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.floor(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.
*
* @sample highcharts/global/dateformats/
* Adding support for week number
*
* @name Highcharts.dateFormats
* @type {Highcharts.Dictionary<Highcharts.TimeFormatCallbackFunction>}
*/
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;
},
/**
* Resolve legacy formats of dateTimeLabelFormats (strings and arrays) into
* an object.
* @param {String|Array|Object} f General format description
* @return {Object} The object definition
*/
resolveDTLFormat: function (f) {
if (!H.isObject(f, true)) {
f = H.splat(f);
return {
main: f[0],
from: f[1],
to: f[2]
};
}
return f;
},
/**
* 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.
*
* @function Highcharts.Time#getTimeTicks
*
* @param {Highcharts.NormalizedIntervalObject} normalizedInterval
* The interval in axis values (ms) and the count
*
* @param {number} [min]
* The minimum in axis values
*
* @param {number} [max]
* The maximum in axis values
*
* @param {number} [startOfWeek=1]
*
* @return {Highcharts.TimeTicksObject}
*/
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,
minDay;
startOfWeek = pick(startOfWeek, 1);
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 :
Math.max(
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
minDay = time.get('Day', minDate);
time.set(
'Date',
minDate,
(
time.get('Date', minDate) -
minDay + startOfWeek +
// We don't want to skip days that are before
// startOfWeek (#7051)
(minDay < startOfWeek ? -7 : 0)
)
);
}
// 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) {
tickPositions.forEach(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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,404 @@
/**
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
import H from './Globals.js';
import './Utilities.js';
var charts = H.charts,
extend = H.extend,
noop = H.noop,
pick = H.pick,
Pointer = H.Pointer;
// Support for touch devices
extend(Pointer.prototype, /** @lends Pointer.prototype */ {
/**
* Run translation operations
*
* @private
* @function Highcharts.Pointer#pinchTranslate
*
* @param {Array<*>} pinchDown
*
* @param {Array<*>} touches
*
* @param {*} transform
*
* @param {*} selectionMarker
*
* @param {*} clip
*
* @param {*} lastValidTouch
*/
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.
*
* @private
* @function Highcharts.Pointer#pinchTranslateDirection
*
* @param {boolean} horiz
*
* @param {Array<*>} pinchDown
*
* @param {Array<*>} touches
*
* @param {*} transform
*
* @param {*} selectionMarker
*
* @param {*} clip
*
* @param {*} lastValidTouch
*
* @param {number|undefined} [forcedScale=1]
*/
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
*
* @private
* @function Highcharts.Pointer#pinch
*
* @param {Highcharts.PointerEvent} e
*/
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.call(touches, function (e) {
return self.normalize(e);
});
// Register the touch start position
if (e.type === 'touchstart') {
[].forEach.call(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
chart.axes.forEach(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.
*
* @private
* @function Highcharts.Pointer#touch
*
* @param {Highcharts.PointerEvent} e
*
* @param {boolean} start
*/
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);
}
},
/**
* @private
* @function Highcharts.Pointer#onContainerTouchStart
*
* @param {Highcharts.PointerEvent} e
*/
onContainerTouchStart: function (e) {
this.zoomOption(e);
this.touch(e, true);
},
/**
* @private
* @function Highcharts.Pointer#onContainerTouchMove
*
* @param {Highcharts.PointerEvent} e
*/
onContainerTouchMove: function (e) {
this.touch(e);
},
/**
* @private
* @function Highcharts.Pointer#onDocumentTouchEnd
*
* @param {Highcharts.PointerEvent} e
*/
onDocumentTouchEnd: function (e) {
if (charts[H.hoverChartIndex]) {
charts[H.hoverChartIndex].pointer.drop(e);
}
}
});
File diff suppressed because it is too large Load Diff