initial commit
This commit is contained in:
@@ -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
|
||||
*/
|
||||
+2307
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
Reference in New Issue
Block a user