highcharts
Version:
JavaScript charting framework
335 lines (334 loc) • 14.3 kB
JavaScript
/* *
*
* (c) 2010-2026 Highsoft AS
* Author: Torstein Hønsi
*
* A commercial license may be required depending on use.
* See www.highcharts.com/license
*
*
* */
'use strict';
import AreaSeriesDefaults from './AreaSeriesDefaults.js';
import SeriesRegistry from '../../Core/Series/SeriesRegistry.js';
const { seriesTypes: { line: LineSeries } } = SeriesRegistry;
import { defined, extend, merge, objectEach, pick } from '../../Shared/Utilities.js';
/* *
*
* Class
*
* */
/**
* Area series type.
*
* @internal
* @class
* @name AreaSeries
*
* @augments LineSeries
*/
class AreaSeries extends LineSeries {
/* *
*
* Functions
*
* */
/**
* 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.
* @internal
*/
drawGraph() {
// Define or reset areaPath
this.areaPath = [];
// Call the base method
super.drawGraph.apply(this);
// Define local variables
const { areaPath, options } = this;
[this, ...this.zones].forEach((owner, i) => {
const attribs = {}, fillColor = owner.fillColor || options.fillColor;
let area = owner.area;
const verb = area ? 'animate' : 'attr';
// Create or update the area
if (area) { // Update
area.endX = this.preventGraphAnimation ?
null :
areaPath.xMap;
area.animate({ d: areaPath });
}
else { // Create
attribs.zIndex = 0; // #1069
/**
* SVG element of area-based charts. Can be used for styling
* purposes. If zones are configured, this element will be
* hidden and replaced by multiple zone areas, accessible
* via `series.zones[i].area`.
*
* @name Highcharts.Series#area
* @type {Highcharts.SVGElement|undefined}
*/
area = owner.area = this.chart.renderer
.path(areaPath)
.addClass('highcharts-area' +
(i ? ` highcharts-zone-area-${i - 1} ` : ' ') +
((i && owner.className) || ''))
.add(this.group);
area.isArea = true;
}
if (!this.chart.styledMode) {
// If there is fillColor defined for the area, set it.
// Otherwise, we set it to the zone/series color and add
// fill-opacity (#18939).
attribs.fill = fillColor || owner.color || this.color;
attribs['fill-opacity'] = fillColor ?
1 : (options.fillOpacity ?? 0.75);
// Allow clicking through the area if sticky tracking is true
// (#18744)
area.css({
pointerEvents: this.stickyTracking ? 'none' : 'auto'
});
}
area[verb](attribs);
area.startX = areaPath.xMap;
area.shiftUnit = options.step ? 2 : 1;
});
}
/**
* @internal
*/
getGraphPath(points) {
const getGraphPath = LineSeries.prototype.getGraphPath, options = this.options, stacking = options.stacking, yAxis = this.yAxis, bottomPoints = [], graphPoints = [], seriesIndex = this.index, stacks = yAxis.stacking.stacks[this.stackKey], threshold = options.threshold, translatedThreshold = Math.round(// #10909
yAxis.getThreshold(options.threshold)), connectNulls = pick(// #10574
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) {
const point = points[i], otherPoint = points[otherI], stackedValues = stacking && (stacks[point.x].points[seriesIndex]), nullVal = point[side + 'Null'] || 0, cliffVal = point[side + 'Cliff'] || 0;
let top, bottom, isNull = true;
if (stackedValues && (cliffVal || nullVal)) {
top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal;
bottom = stackedValues[0] + cliffVal;
isNull = !!nullVal;
}
else if (!stacking &&
otherPoint &&
(otherPoint.isNull ||
!defined(otherPoint.plotY))) {
top = bottom = threshold;
}
// Add to the top and bottom line of the area
if (typeof 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
});
}
};
let plotX, isNull, yBottom;
// Find what points to use
points = points || this.points;
// Fill in missing points
if (stacking) {
points = this.getStackPoints(points);
}
for (let i = 0, iEnd = points.length; i < iEnd; ++i) {
// Reset after series.update of stacking property (#12033)
if (!stacking) {
points[i].leftCliff = points[i].rightCliff =
points[i].leftNull = points[i].rightNull = void 0;
}
// Treat points with undefined plotY as null (e.g. non-positive
// values on logarithmic axis, #18422)
isNull = points[i].isNull || !defined(points[i].plotY);
plotX = pick(points[i].rectPlotX, points[i].plotX);
yBottom = stacking ?
pick(points[i].yBottom, translatedThreshold) :
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');
}
}
}
const topPath = getGraphPath.call(this, graphPoints, true, true);
bottomPoints.reversed = true;
const bottomPath = getGraphPath.call(this, bottomPoints, true, true);
const firstBottomPoint = bottomPath[0];
if (firstBottomPoint && firstBottomPoint[0] === 'M') {
bottomPath[0] = ['L', firstBottomPoint[1], firstBottomPoint[2]];
}
const areaPath = topPath.concat(bottomPath);
if (areaPath.length) {
areaPath.push(['Z']);
}
// TODO: don't set leftCliff and rightCliff when connectNulls?
const graphPath = getGraphPath
.call(this, graphPoints, false, connectNulls);
if (this.chart.series.length > 1 &&
stacking &&
graphPoints.some((point) => point.isCliff)) {
areaPath.hasStackedCliffs = graphPath.hasStackedCliffs = true;
}
areaPath.xMap = topPath.xMap;
this.areaPath = areaPath;
return graphPath;
}
/**
* 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.
* @internal
*/
getStackPoints(points) {
const series = this, segment = [], keys = [], xAxis = this.xAxis, yAxis = this.yAxis, stack = yAxis.stacking.stacks[this.stackKey], pointMap = {}, yAxisSeries = yAxis.series, seriesLength = yAxisSeries.length, upOrDown = yAxis.options.reversedStacks ? 1 : -1, seriesIndex = yAxisSeries.indexOf(series), translatedThreshold = yAxis.getThreshold(series.options.threshold || 0);
points = points || this.points;
if (this.options.stacking) {
for (let i = 0; i < points.length; i++) {
// Reset after point update (#7326)
points[i].leftNull = points[i].rightNull = void 0;
// 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)
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;
});
const visibleSeries = yAxisSeries.map((s) => s.visible);
keys.forEach(function (x, idx) {
let 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) {
const nullName = direction === 1 ?
'rightNull' :
'leftNull', cliffName = direction === 1 ?
'rightCliff' :
'leftCliff', otherStack = stack[keys[idx + direction]];
let cliff = 0;
// If there is a stack next to this one,
// to the left or to the right...
if (otherStack) {
let i = seriesIndex;
// Can go either up or down,
// depending on reversedStacks
while (i >= 0 && i < seriesLength) {
const si = yAxisSeries[i].index;
stackPoint = otherStack.points[si];
if (!stackPoint) {
// If the next point in this series is
// missing, mark the point with
// point.leftNull or point.rightNull = true.
if (si === series.index) {
pointMap[x][nullName] = true;
// If there are missing points in the next
// stack in any of the series below this
// one, we need to subtract the missing
// values and add a hiatus to the left or
// right.
}
else if (visibleSeries[i]) {
stackedValues = stack[x].points[si];
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)
let i = seriesIndex;
while (i >= 0 && i < seriesLength) {
const si = yAxisSeries[i].index;
stackPoint = stack[x].points[si];
if (stackPoint) {
y = stackPoint[1];
break;
}
// When reversedStacks is true, loop up, else loop
// down
i += upOrDown;
}
y || (y = 0);
const plotY = yAxis.positiveValuesOnly && y <= 0 ?
translatedThreshold :
yAxis.translate(// #6272
y, false, true, false, true);
segment.push({
isNull: true,
plotX: xAxis.translate(// #6272
x, false, false, false, true),
x: x,
plotY,
yBottom: plotY
});
}
});
}
return segment;
}
}
/* *
*
* Static Properties
*
* */
AreaSeries.defaultOptions = merge(LineSeries.defaultOptions, AreaSeriesDefaults);
extend(AreaSeries.prototype, {
singleStacks: false
});
SeriesRegistry.registerSeriesType('area', AreaSeries);
/* *
*
* Default Export
*
* */
/** @internal */
export default AreaSeries;