1171 lines
33 KiB
JavaScript
1171 lines
33 KiB
JavaScript
import CoreUtils from '../modules/CoreUtils'
|
|
import Graphics from '../modules/Graphics'
|
|
import Fill from '../modules/Fill'
|
|
import DataLabels from '../modules/DataLabels'
|
|
import Markers from '../modules/Markers'
|
|
import Scatter from './Scatter'
|
|
import Utils from '../utils/Utils'
|
|
import Helpers from './common/line/Helpers'
|
|
import { svgPath, spline } from '../libs/monotone-cubic'
|
|
/**
|
|
* ApexCharts Line Class responsible for drawing Line / Area / RangeArea Charts.
|
|
* This class is also responsible for generating values for Bubble/Scatter charts, so need to rename it to Axis Charts to avoid confusions
|
|
* @module Line
|
|
**/
|
|
|
|
class Line {
|
|
constructor(ctx, xyRatios, isPointsChart) {
|
|
this.ctx = ctx
|
|
this.w = ctx.w
|
|
|
|
this.xyRatios = xyRatios
|
|
|
|
this.pointsChart =
|
|
!(
|
|
this.w.config.chart.type !== 'bubble' &&
|
|
this.w.config.chart.type !== 'scatter'
|
|
) || isPointsChart
|
|
|
|
this.scatter = new Scatter(this.ctx)
|
|
|
|
this.noNegatives = this.w.globals.minX === Number.MAX_VALUE
|
|
|
|
this.lineHelpers = new Helpers(this)
|
|
this.markers = new Markers(this.ctx)
|
|
|
|
this.prevSeriesY = []
|
|
this.categoryAxisCorrection = 0
|
|
this.yaxisIndex = 0
|
|
}
|
|
|
|
draw(series, ctype, seriesIndex, seriesRangeEnd) {
|
|
let w = this.w
|
|
let graphics = new Graphics(this.ctx)
|
|
let type = w.globals.comboCharts ? ctype : w.config.chart.type
|
|
let ret = graphics.group({
|
|
class: `apexcharts-${type}-series apexcharts-plot-series`,
|
|
})
|
|
|
|
const coreUtils = new CoreUtils(this.ctx, w)
|
|
this.yRatio = this.xyRatios.yRatio
|
|
this.zRatio = this.xyRatios.zRatio
|
|
this.xRatio = this.xyRatios.xRatio
|
|
this.baseLineY = this.xyRatios.baseLineY
|
|
|
|
series = coreUtils.getLogSeries(series)
|
|
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
|
|
// We call draw() for each series group
|
|
this.prevSeriesY = []
|
|
|
|
// push all series in an array, so we can draw in reverse order
|
|
// (for stacked charts)
|
|
let allSeries = []
|
|
|
|
for (let i = 0; i < series.length; i++) {
|
|
series = this.lineHelpers.sameValueSeriesFix(i, series)
|
|
|
|
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
|
|
let translationsIndex = this.yRatio.length > 1 ? realIndex : 0
|
|
|
|
this._initSerieVariables(series, i, realIndex)
|
|
|
|
let yArrj = [] // hold y values of current iterating series
|
|
let y2Arrj = [] // holds y2 values in range-area charts
|
|
let xArrj = [] // hold x values of current iterating series
|
|
|
|
let x = w.globals.padHorizontal + this.categoryAxisCorrection
|
|
let y = 1
|
|
|
|
let linePaths = []
|
|
let areaPaths = []
|
|
|
|
this.ctx.series.addCollapsedClassToSeries(this.elSeries, realIndex)
|
|
|
|
if (w.globals.isXNumeric && w.globals.seriesX.length > 0) {
|
|
x = (w.globals.seriesX[realIndex][0] - w.globals.minX) / this.xRatio
|
|
}
|
|
|
|
xArrj.push(x)
|
|
|
|
let pX = x
|
|
let pY
|
|
let pY2
|
|
let prevX = pX
|
|
let prevY = this.zeroY
|
|
let prevY2 = this.zeroY
|
|
let lineYPosition = 0
|
|
|
|
// the first value in the current series is not null or undefined
|
|
let firstPrevY = this.lineHelpers.determineFirstPrevY({
|
|
i,
|
|
realIndex,
|
|
series,
|
|
prevY,
|
|
lineYPosition,
|
|
translationsIndex,
|
|
})
|
|
prevY = firstPrevY.prevY
|
|
if (w.config.stroke.curve === 'monotoneCubic' && series[i][0] === null) {
|
|
// we have to discard the y position if 1st dataPoint is null as it
|
|
// causes issues with monotoneCubic path creation
|
|
yArrj.push(null)
|
|
} else {
|
|
yArrj.push(prevY)
|
|
}
|
|
pY = prevY
|
|
|
|
// y2 are needed for range-area charts
|
|
let firstPrevY2
|
|
|
|
if (type === 'rangeArea') {
|
|
firstPrevY2 = this.lineHelpers.determineFirstPrevY({
|
|
i,
|
|
realIndex,
|
|
series: seriesRangeEnd,
|
|
prevY: prevY2,
|
|
lineYPosition,
|
|
translationsIndex,
|
|
})
|
|
prevY2 = firstPrevY2.prevY
|
|
pY2 = prevY2
|
|
y2Arrj.push(yArrj[0] !== null ? prevY2 : null)
|
|
}
|
|
|
|
let pathsFrom = this._calculatePathsFrom({
|
|
type,
|
|
series,
|
|
i,
|
|
realIndex,
|
|
translationsIndex,
|
|
prevX,
|
|
prevY,
|
|
prevY2,
|
|
})
|
|
|
|
// RangeArea will resume with these for the upper path creation
|
|
let rYArrj = [yArrj[0]]
|
|
let rY2Arrj = [y2Arrj[0]]
|
|
|
|
const iteratingOpts = {
|
|
type,
|
|
series,
|
|
realIndex,
|
|
translationsIndex,
|
|
i,
|
|
x,
|
|
y,
|
|
pX,
|
|
pY,
|
|
pathsFrom,
|
|
linePaths,
|
|
areaPaths,
|
|
seriesIndex,
|
|
lineYPosition,
|
|
xArrj,
|
|
yArrj,
|
|
y2Arrj,
|
|
seriesRangeEnd,
|
|
}
|
|
|
|
let paths = this._iterateOverDataPoints({
|
|
...iteratingOpts,
|
|
iterations: type === 'rangeArea' ? series[i].length - 1 : undefined,
|
|
isRangeStart: true,
|
|
})
|
|
|
|
if (type === 'rangeArea') {
|
|
let pathsFrom2 = this._calculatePathsFrom({
|
|
series: seriesRangeEnd,
|
|
i,
|
|
realIndex,
|
|
prevX,
|
|
prevY: prevY2,
|
|
})
|
|
let rangePaths = this._iterateOverDataPoints({
|
|
...iteratingOpts,
|
|
series: seriesRangeEnd,
|
|
xArrj: [x],
|
|
yArrj: rYArrj,
|
|
y2Arrj: rY2Arrj,
|
|
pY: pY2,
|
|
areaPaths: paths.areaPaths,
|
|
pathsFrom: pathsFrom2,
|
|
iterations: seriesRangeEnd[i].length - 1,
|
|
isRangeStart: false,
|
|
})
|
|
|
|
// Path may be segmented by nulls in data.
|
|
// paths.linePaths should hold (segments * 2) paths (upper and lower)
|
|
// the first n segments belong to the lower and the last n segments
|
|
// belong to the upper.
|
|
// paths.linePaths and rangePaths.linepaths are actually equivalent
|
|
// but we retain the distinction below for consistency with the
|
|
// unsegmented paths conditional branch.
|
|
let segments = paths.linePaths.length / 2
|
|
for (let s = 0; s < segments; s++) {
|
|
paths.linePaths[s] =
|
|
rangePaths.linePaths[s + segments] + paths.linePaths[s]
|
|
}
|
|
paths.linePaths.splice(segments)
|
|
paths.pathFromLine = rangePaths.pathFromLine + paths.pathFromLine
|
|
} else {
|
|
paths.pathFromArea += 'z'
|
|
}
|
|
|
|
this._handlePaths({ type, realIndex, i, paths })
|
|
|
|
this.elSeries.add(this.elPointsMain)
|
|
this.elSeries.add(this.elDataLabelsWrap)
|
|
|
|
allSeries.push(this.elSeries)
|
|
}
|
|
|
|
if (typeof w.config.series[0]?.zIndex !== 'undefined') {
|
|
allSeries.sort(
|
|
(a, b) =>
|
|
Number(a.node.getAttribute('zIndex')) -
|
|
Number(b.node.getAttribute('zIndex'))
|
|
)
|
|
}
|
|
|
|
if (w.config.chart.stacked) {
|
|
for (let s = allSeries.length - 1; s >= 0; s--) {
|
|
ret.add(allSeries[s])
|
|
}
|
|
} else {
|
|
for (let s = 0; s < allSeries.length; s++) {
|
|
ret.add(allSeries[s])
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
_initSerieVariables(series, i, realIndex) {
|
|
const w = this.w
|
|
const graphics = new Graphics(this.ctx)
|
|
|
|
// width divided into equal parts
|
|
this.xDivision =
|
|
w.globals.gridWidth /
|
|
(w.globals.dataPoints - (w.config.xaxis.tickPlacement === 'on' ? 1 : 0))
|
|
|
|
this.strokeWidth = Array.isArray(w.config.stroke.width)
|
|
? w.config.stroke.width[realIndex]
|
|
: w.config.stroke.width
|
|
|
|
let translationsIndex = 0
|
|
if (this.yRatio.length > 1) {
|
|
this.yaxisIndex = w.globals.seriesYAxisReverseMap[realIndex]
|
|
translationsIndex = realIndex
|
|
}
|
|
|
|
this.isReversed =
|
|
w.config.yaxis[this.yaxisIndex] &&
|
|
w.config.yaxis[this.yaxisIndex].reversed
|
|
|
|
// zeroY is the 0 value in y series which can be used in negative charts
|
|
this.zeroY =
|
|
w.globals.gridHeight -
|
|
this.baseLineY[translationsIndex] -
|
|
(this.isReversed ? w.globals.gridHeight : 0) +
|
|
(this.isReversed ? this.baseLineY[translationsIndex] * 2 : 0)
|
|
|
|
this.areaBottomY = this.zeroY
|
|
if (
|
|
this.zeroY > w.globals.gridHeight ||
|
|
w.config.plotOptions.area.fillTo === 'end'
|
|
) {
|
|
this.areaBottomY = w.globals.gridHeight
|
|
}
|
|
|
|
this.categoryAxisCorrection = this.xDivision / 2
|
|
|
|
// el to which series will be drawn
|
|
this.elSeries = graphics.group({
|
|
class: `apexcharts-series`,
|
|
zIndex:
|
|
typeof w.config.series[realIndex].zIndex !== 'undefined'
|
|
? w.config.series[realIndex].zIndex
|
|
: realIndex,
|
|
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
|
|
})
|
|
|
|
// points
|
|
this.elPointsMain = graphics.group({
|
|
class: 'apexcharts-series-markers-wrap',
|
|
'data:realIndex': realIndex,
|
|
})
|
|
|
|
if (w.globals.hasNullValues) {
|
|
// fixes https://github.com/apexcharts/apexcharts.js/issues/3641
|
|
const firstPoint = this.markers.plotChartMarkers({
|
|
pointsPos: {
|
|
x: [0],
|
|
y: [w.globals.gridHeight + w.globals.markers.largestSize],
|
|
},
|
|
seriesIndex: i,
|
|
j: 0,
|
|
pSize: 0.1,
|
|
alwaysDrawMarker: true,
|
|
isVirtualPoint: true,
|
|
})
|
|
|
|
if (firstPoint !== null) {
|
|
// firstPoint is rendered for cases where there are null values and when dynamic markers are required
|
|
this.elPointsMain.add(firstPoint)
|
|
}
|
|
}
|
|
|
|
// eldatalabels
|
|
this.elDataLabelsWrap = graphics.group({
|
|
class: 'apexcharts-datalabels',
|
|
'data:realIndex': realIndex,
|
|
})
|
|
|
|
let longestSeries = series[i].length === w.globals.dataPoints
|
|
this.elSeries.attr({
|
|
'data:longestSeries': longestSeries,
|
|
rel: i + 1,
|
|
'data:realIndex': realIndex,
|
|
})
|
|
|
|
this.appendPathFrom = true
|
|
}
|
|
|
|
_calculatePathsFrom({
|
|
type,
|
|
series,
|
|
i,
|
|
realIndex,
|
|
translationsIndex,
|
|
prevX,
|
|
prevY,
|
|
prevY2,
|
|
}) {
|
|
const w = this.w
|
|
const graphics = new Graphics(this.ctx)
|
|
let linePath, areaPath, pathFromLine, pathFromArea
|
|
|
|
if (series[i][0] === null) {
|
|
// when the first value itself is null, we need to move the pointer to a location where a null value is not found
|
|
for (let s = 0; s < series[i].length; s++) {
|
|
if (series[i][s] !== null) {
|
|
prevX = this.xDivision * s
|
|
prevY = this.zeroY - series[i][s] / this.yRatio[translationsIndex]
|
|
linePath = graphics.move(prevX, prevY)
|
|
areaPath = graphics.move(prevX, this.areaBottomY)
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
linePath = graphics.move(prevX, prevY)
|
|
|
|
if (type === 'rangeArea') {
|
|
linePath = graphics.move(prevX, prevY2) + graphics.line(prevX, prevY)
|
|
}
|
|
areaPath =
|
|
graphics.move(prevX, this.areaBottomY) + graphics.line(prevX, prevY)
|
|
}
|
|
|
|
pathFromLine =
|
|
graphics.move(0, this.areaBottomY) + graphics.line(0, this.areaBottomY)
|
|
pathFromArea =
|
|
graphics.move(0, this.areaBottomY) + graphics.line(0, this.areaBottomY)
|
|
|
|
if (w.globals.previousPaths.length > 0) {
|
|
const pathFrom = this.lineHelpers.checkPreviousPaths({
|
|
pathFromLine,
|
|
pathFromArea,
|
|
realIndex,
|
|
})
|
|
pathFromLine = pathFrom.pathFromLine
|
|
pathFromArea = pathFrom.pathFromArea
|
|
}
|
|
|
|
return {
|
|
prevX,
|
|
prevY,
|
|
linePath,
|
|
areaPath,
|
|
pathFromLine,
|
|
pathFromArea,
|
|
}
|
|
}
|
|
|
|
_handlePaths({ type, realIndex, i, paths }) {
|
|
const w = this.w
|
|
const graphics = new Graphics(this.ctx)
|
|
const fill = new Fill(this.ctx)
|
|
|
|
// push all current y values array to main PrevY Array
|
|
this.prevSeriesY.push(paths.yArrj)
|
|
|
|
// push all x val arrays into main xArr
|
|
w.globals.seriesXvalues[realIndex] = paths.xArrj
|
|
w.globals.seriesYvalues[realIndex] = paths.yArrj
|
|
|
|
const forecast = w.config.forecastDataPoints
|
|
if (forecast.count > 0 && type !== 'rangeArea') {
|
|
const forecastCutoff =
|
|
w.globals.seriesXvalues[realIndex][
|
|
w.globals.seriesXvalues[realIndex].length - forecast.count - 1
|
|
]
|
|
const elForecastMask = graphics.drawRect(
|
|
forecastCutoff,
|
|
0,
|
|
w.globals.gridWidth,
|
|
w.globals.gridHeight,
|
|
0
|
|
)
|
|
w.globals.dom.elForecastMask.appendChild(elForecastMask.node)
|
|
|
|
const elNonForecastMask = graphics.drawRect(
|
|
0,
|
|
0,
|
|
forecastCutoff,
|
|
w.globals.gridHeight,
|
|
0
|
|
)
|
|
w.globals.dom.elNonForecastMask.appendChild(elNonForecastMask.node)
|
|
}
|
|
|
|
// these elements will be shown after area path animation completes
|
|
if (!this.pointsChart) {
|
|
w.globals.delayedElements.push({
|
|
el: this.elPointsMain.node,
|
|
index: realIndex,
|
|
})
|
|
}
|
|
|
|
const defaultRenderedPathOptions = {
|
|
i,
|
|
realIndex,
|
|
animationDelay: i,
|
|
initialSpeed: w.config.chart.animations.speed,
|
|
dataChangeSpeed: w.config.chart.animations.dynamicAnimation.speed,
|
|
className: `apexcharts-${type}`,
|
|
}
|
|
|
|
if (type === 'area') {
|
|
let pathFill = fill.fillPath({
|
|
seriesNumber: realIndex,
|
|
})
|
|
|
|
for (let p = 0; p < paths.areaPaths.length; p++) {
|
|
let renderedPath = graphics.renderPaths({
|
|
...defaultRenderedPathOptions,
|
|
pathFrom: paths.pathFromArea,
|
|
pathTo: paths.areaPaths[p],
|
|
stroke: 'none',
|
|
strokeWidth: 0,
|
|
strokeLineCap: null,
|
|
fill: pathFill,
|
|
})
|
|
this.elSeries.add(renderedPath)
|
|
}
|
|
}
|
|
|
|
if (w.config.stroke.show && !this.pointsChart) {
|
|
let lineFill = null
|
|
if (type === 'line') {
|
|
lineFill = fill.fillPath({
|
|
seriesNumber: realIndex,
|
|
i,
|
|
})
|
|
} else {
|
|
if (w.config.stroke.fill.type === 'solid') {
|
|
lineFill = w.globals.stroke.colors[realIndex]
|
|
} else {
|
|
const prevFill = w.config.fill
|
|
w.config.fill = w.config.stroke.fill
|
|
|
|
lineFill = fill.fillPath({
|
|
seriesNumber: realIndex,
|
|
i,
|
|
})
|
|
w.config.fill = prevFill
|
|
}
|
|
}
|
|
|
|
// range-area paths are drawn using linePaths
|
|
for (let p = 0; p < paths.linePaths.length; p++) {
|
|
let pathFill = lineFill
|
|
if (type === 'rangeArea') {
|
|
pathFill = fill.fillPath({
|
|
seriesNumber: realIndex,
|
|
})
|
|
}
|
|
const linePathCommonOpts = {
|
|
...defaultRenderedPathOptions,
|
|
pathFrom: paths.pathFromLine,
|
|
pathTo: paths.linePaths[p],
|
|
stroke: lineFill,
|
|
strokeWidth: this.strokeWidth,
|
|
strokeLineCap: w.config.stroke.lineCap,
|
|
fill: type === 'rangeArea' ? pathFill : 'none',
|
|
}
|
|
let renderedPath = graphics.renderPaths(linePathCommonOpts)
|
|
this.elSeries.add(renderedPath)
|
|
renderedPath.attr('fill-rule', `evenodd`)
|
|
|
|
if (forecast.count > 0 && type !== 'rangeArea') {
|
|
let renderedForecastPath = graphics.renderPaths(linePathCommonOpts)
|
|
|
|
renderedForecastPath.node.setAttribute(
|
|
'stroke-dasharray',
|
|
forecast.dashArray
|
|
)
|
|
|
|
if (forecast.strokeWidth) {
|
|
renderedForecastPath.node.setAttribute(
|
|
'stroke-width',
|
|
forecast.strokeWidth
|
|
)
|
|
}
|
|
|
|
this.elSeries.add(renderedForecastPath)
|
|
renderedForecastPath.attr(
|
|
'clip-path',
|
|
`url(#forecastMask${w.globals.cuid})`
|
|
)
|
|
renderedPath.attr(
|
|
'clip-path',
|
|
`url(#nonForecastMask${w.globals.cuid})`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_iterateOverDataPoints({
|
|
type,
|
|
series,
|
|
iterations,
|
|
realIndex,
|
|
translationsIndex,
|
|
i,
|
|
x,
|
|
y,
|
|
pX,
|
|
pY,
|
|
pathsFrom,
|
|
linePaths,
|
|
areaPaths,
|
|
seriesIndex,
|
|
lineYPosition,
|
|
xArrj,
|
|
yArrj,
|
|
y2Arrj,
|
|
isRangeStart,
|
|
seriesRangeEnd,
|
|
}) {
|
|
const w = this.w
|
|
let graphics = new Graphics(this.ctx)
|
|
let yRatio = this.yRatio
|
|
let { prevY, linePath, areaPath, pathFromLine, pathFromArea } = pathsFrom
|
|
|
|
const minY = Utils.isNumber(w.globals.minYArr[realIndex])
|
|
? w.globals.minYArr[realIndex]
|
|
: w.globals.minY
|
|
|
|
if (!iterations) {
|
|
iterations =
|
|
w.globals.dataPoints > 1
|
|
? w.globals.dataPoints - 1
|
|
: w.globals.dataPoints
|
|
}
|
|
|
|
const getY = (_y, lineYPos) => {
|
|
return (
|
|
lineYPos -
|
|
_y / yRatio[translationsIndex] +
|
|
(this.isReversed ? _y / yRatio[translationsIndex] : 0) * 2
|
|
)
|
|
}
|
|
|
|
let y2 = y
|
|
|
|
let stackSeries =
|
|
(w.config.chart.stacked && !w.globals.comboCharts) ||
|
|
(w.config.chart.stacked &&
|
|
w.globals.comboCharts &&
|
|
(!this.w.config.chart.stackOnlyBar ||
|
|
this.w.config.series[realIndex]?.type === 'bar' ||
|
|
this.w.config.series[realIndex]?.type === 'column'))
|
|
|
|
let curve = w.config.stroke.curve
|
|
if (Array.isArray(curve)) {
|
|
if (Array.isArray(seriesIndex)) {
|
|
curve = curve[seriesIndex[i]]
|
|
} else {
|
|
curve = curve[i]
|
|
}
|
|
}
|
|
|
|
let pathState = 0
|
|
let segmentStartX
|
|
|
|
for (let j = 0; j < iterations; j++) {
|
|
if (series[i].length === 0) break
|
|
|
|
const isNull =
|
|
typeof series[i][j + 1] === 'undefined' || series[i][j + 1] === null
|
|
|
|
if (w.globals.isXNumeric) {
|
|
let sX = w.globals.seriesX[realIndex][j + 1]
|
|
if (typeof w.globals.seriesX[realIndex][j + 1] === 'undefined') {
|
|
/* fix #374 */
|
|
sX = w.globals.seriesX[realIndex][iterations - 1]
|
|
}
|
|
x = (sX - w.globals.minX) / this.xRatio
|
|
} else {
|
|
x = x + this.xDivision
|
|
}
|
|
|
|
if (stackSeries) {
|
|
if (
|
|
i > 0 &&
|
|
w.globals.collapsedSeries.length < w.config.series.length - 1
|
|
) {
|
|
// a collapsed series in a stacked chart may provide wrong result
|
|
// for the next series, hence find the prevIndex of prev series
|
|
// which is not collapsed - fixes apexcharts.js#1372
|
|
const prevIndex = (pi) => {
|
|
for (let pii = pi; pii > 0; pii--) {
|
|
if (
|
|
w.globals.collapsedSeriesIndices.indexOf(
|
|
seriesIndex?.[pii] || pii
|
|
) > -1
|
|
) {
|
|
pii--
|
|
} else {
|
|
return pii
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
lineYPosition = this.prevSeriesY[prevIndex(i - 1)][j + 1]
|
|
} else {
|
|
// the first series will not have prevY values
|
|
lineYPosition = this.zeroY
|
|
}
|
|
} else {
|
|
lineYPosition = this.zeroY
|
|
}
|
|
|
|
if (isNull) {
|
|
y = getY(minY, lineYPosition)
|
|
} else {
|
|
y = getY(series[i][j + 1], lineYPosition)
|
|
|
|
if (type === 'rangeArea') {
|
|
y2 = getY(seriesRangeEnd[i][j + 1], lineYPosition)
|
|
}
|
|
}
|
|
|
|
// push current X
|
|
xArrj.push(series[i][j + 1] === null ? null : x)
|
|
|
|
// push current Y that will be used as next series's bottom position
|
|
if (
|
|
isNull &&
|
|
(w.config.stroke.curve === 'smooth' ||
|
|
w.config.stroke.curve === 'monotoneCubic')
|
|
) {
|
|
yArrj.push(null)
|
|
y2Arrj.push(null)
|
|
} else {
|
|
yArrj.push(y)
|
|
y2Arrj.push(y2)
|
|
}
|
|
|
|
let pointsPos = this.lineHelpers.calculatePoints({
|
|
series,
|
|
x,
|
|
y,
|
|
realIndex,
|
|
i,
|
|
j,
|
|
prevY,
|
|
})
|
|
|
|
let calculatedPaths = this._createPaths({
|
|
type,
|
|
series,
|
|
i,
|
|
realIndex,
|
|
j,
|
|
x,
|
|
y,
|
|
y2,
|
|
xArrj,
|
|
yArrj,
|
|
y2Arrj,
|
|
pX,
|
|
pY,
|
|
pathState,
|
|
segmentStartX,
|
|
linePath,
|
|
areaPath,
|
|
linePaths,
|
|
areaPaths,
|
|
curve,
|
|
isRangeStart,
|
|
})
|
|
|
|
areaPaths = calculatedPaths.areaPaths
|
|
linePaths = calculatedPaths.linePaths
|
|
pX = calculatedPaths.pX
|
|
pY = calculatedPaths.pY
|
|
pathState = calculatedPaths.pathState
|
|
segmentStartX = calculatedPaths.segmentStartX
|
|
areaPath = calculatedPaths.areaPath
|
|
linePath = calculatedPaths.linePath
|
|
|
|
if (
|
|
this.appendPathFrom &&
|
|
!w.globals.hasNullValues &&
|
|
!(curve === 'monotoneCubic' && type === 'rangeArea')
|
|
) {
|
|
pathFromLine += graphics.line(x, this.areaBottomY)
|
|
pathFromArea += graphics.line(x, this.areaBottomY)
|
|
}
|
|
|
|
this.handleNullDataPoints(series, pointsPos, i, j, realIndex)
|
|
|
|
this._handleMarkersAndLabels({
|
|
type,
|
|
pointsPos,
|
|
i,
|
|
j,
|
|
realIndex,
|
|
isRangeStart,
|
|
})
|
|
}
|
|
|
|
return {
|
|
yArrj,
|
|
xArrj,
|
|
pathFromArea,
|
|
areaPaths,
|
|
pathFromLine,
|
|
linePaths,
|
|
linePath,
|
|
areaPath,
|
|
}
|
|
}
|
|
|
|
_handleMarkersAndLabels({ type, pointsPos, isRangeStart, i, j, realIndex }) {
|
|
const w = this.w
|
|
let dataLabels = new DataLabels(this.ctx)
|
|
|
|
if (!this.pointsChart) {
|
|
if (w.globals.series[i].length > 1) {
|
|
this.elPointsMain.node.classList.add('apexcharts-element-hidden')
|
|
}
|
|
|
|
let elPointsWrap = this.markers.plotChartMarkers({
|
|
pointsPos,
|
|
seriesIndex: realIndex,
|
|
j: j + 1,
|
|
})
|
|
if (elPointsWrap !== null) {
|
|
this.elPointsMain.add(elPointsWrap)
|
|
}
|
|
} else {
|
|
// scatter / bubble chart points creation
|
|
this.scatter.draw(this.elSeries, j, {
|
|
realIndex,
|
|
pointsPos,
|
|
zRatio: this.zRatio,
|
|
elParent: this.elPointsMain,
|
|
})
|
|
}
|
|
|
|
let drawnLabels = dataLabels.drawDataLabel({
|
|
type,
|
|
isRangeStart,
|
|
pos: pointsPos,
|
|
i: realIndex,
|
|
j: j + 1,
|
|
})
|
|
if (drawnLabels !== null) {
|
|
this.elDataLabelsWrap.add(drawnLabels)
|
|
}
|
|
}
|
|
|
|
_createPaths({
|
|
type,
|
|
series,
|
|
i,
|
|
realIndex,
|
|
j,
|
|
x,
|
|
y,
|
|
xArrj,
|
|
yArrj,
|
|
y2,
|
|
y2Arrj,
|
|
pX,
|
|
pY,
|
|
pathState,
|
|
segmentStartX,
|
|
linePath,
|
|
areaPath,
|
|
linePaths,
|
|
areaPaths,
|
|
curve,
|
|
isRangeStart,
|
|
}) {
|
|
let graphics = new Graphics(this.ctx)
|
|
const areaBottomY = this.areaBottomY
|
|
let rangeArea = type === 'rangeArea'
|
|
let isLowerRangeAreaPath = type === 'rangeArea' && isRangeStart
|
|
|
|
switch (curve) {
|
|
case 'monotoneCubic':
|
|
let yAj = isRangeStart ? yArrj : y2Arrj
|
|
let getSmoothInputs = (xArr, yArr) => {
|
|
return xArr
|
|
.map((_, i) => {
|
|
return [_, yArr[i]]
|
|
})
|
|
.filter((_) => _[1] !== null)
|
|
}
|
|
let getSegmentLengths = (yArr) => {
|
|
// Get the segment lengths so the segments can be extracted from
|
|
// the null-filtered smoothInputs array
|
|
let segLens = []
|
|
let count = 0
|
|
yArr.forEach((_) => {
|
|
if (_ !== null) {
|
|
count++
|
|
} else if (count > 0) {
|
|
segLens.push(count)
|
|
count = 0
|
|
}
|
|
})
|
|
if (count > 0) {
|
|
segLens.push(count)
|
|
}
|
|
return segLens
|
|
}
|
|
let getSegments = (yArr, points) => {
|
|
let segLens = getSegmentLengths(yArr)
|
|
let segments = []
|
|
for (let i = 0, len = 0; i < segLens.length; len += segLens[i++]) {
|
|
segments[i] = spline.slice(points, len, len + segLens[i])
|
|
}
|
|
return segments
|
|
}
|
|
|
|
switch (pathState) {
|
|
case 0:
|
|
// Find start of segment
|
|
if (yAj[j + 1] === null) {
|
|
break
|
|
}
|
|
pathState = 1
|
|
// continue through to pathState 1
|
|
case 1:
|
|
if (
|
|
!(rangeArea
|
|
? xArrj.length === series[i].length
|
|
: j === series[i].length - 2)
|
|
) {
|
|
break
|
|
}
|
|
// continue through to pathState 2
|
|
case 2:
|
|
// Interpolate the full series with nulls excluded then extract the
|
|
// null delimited segments with interpolated points included.
|
|
const _xAj = isRangeStart ? xArrj : xArrj.slice().reverse()
|
|
const _yAj = isRangeStart ? yAj : yAj.slice().reverse()
|
|
|
|
const smoothInputs = getSmoothInputs(_xAj, _yAj)
|
|
const points =
|
|
smoothInputs.length > 1
|
|
? spline.points(smoothInputs)
|
|
: smoothInputs
|
|
|
|
let smoothInputsLower = []
|
|
if (rangeArea) {
|
|
if (isLowerRangeAreaPath) {
|
|
// As we won't be needing it, borrow areaPaths to retain our
|
|
// rangeArea lower points.
|
|
areaPaths = smoothInputs
|
|
} else {
|
|
// Retrieve the corresponding lower raw interpolated points so we
|
|
// can join onto its end points. Note: the upper Y2 segments will
|
|
// be in the reverse order relative to the lower segments.
|
|
smoothInputsLower = areaPaths.reverse()
|
|
}
|
|
}
|
|
|
|
let segmentCount = 0
|
|
let smoothInputsIndex = 0
|
|
getSegments(_yAj, points).forEach((_) => {
|
|
segmentCount++
|
|
let svgPoints = svgPath(_)
|
|
let _start = smoothInputsIndex
|
|
smoothInputsIndex += _.length
|
|
let _end = smoothInputsIndex - 1
|
|
if (isLowerRangeAreaPath) {
|
|
linePath =
|
|
graphics.move(
|
|
smoothInputs[_start][0],
|
|
smoothInputs[_start][1]
|
|
) + svgPoints
|
|
} else if (rangeArea) {
|
|
linePath =
|
|
graphics.move(
|
|
smoothInputsLower[_start][0],
|
|
smoothInputsLower[_start][1]
|
|
) +
|
|
graphics.line(
|
|
smoothInputs[_start][0],
|
|
smoothInputs[_start][1]
|
|
) +
|
|
svgPoints +
|
|
graphics.line(
|
|
smoothInputsLower[_end][0],
|
|
smoothInputsLower[_end][1]
|
|
)
|
|
} else {
|
|
linePath =
|
|
graphics.move(
|
|
smoothInputs[_start][0],
|
|
smoothInputs[_start][1]
|
|
) + svgPoints
|
|
areaPath =
|
|
linePath +
|
|
graphics.line(smoothInputs[_end][0], areaBottomY) +
|
|
graphics.line(smoothInputs[_start][0], areaBottomY) +
|
|
'z'
|
|
areaPaths.push(areaPath)
|
|
}
|
|
linePaths.push(linePath)
|
|
})
|
|
|
|
if (rangeArea && segmentCount > 1 && !isLowerRangeAreaPath) {
|
|
// Reverse the order of the upper path segments
|
|
let upperLinePaths = linePaths.slice(segmentCount).reverse()
|
|
linePaths.splice(segmentCount)
|
|
upperLinePaths.forEach((u) => linePaths.push(u))
|
|
}
|
|
pathState = 0
|
|
break
|
|
}
|
|
break
|
|
case 'smooth':
|
|
let length = (x - pX) * 0.35
|
|
if (series[i][j] === null) {
|
|
pathState = 0
|
|
} else {
|
|
switch (pathState) {
|
|
case 0:
|
|
// Beginning of segment
|
|
segmentStartX = pX
|
|
if (isLowerRangeAreaPath) {
|
|
// Need to add path portion that will join to the upper path
|
|
linePath = graphics.move(pX, y2Arrj[j]) + graphics.line(pX, pY)
|
|
} else {
|
|
linePath = graphics.move(pX, pY)
|
|
}
|
|
areaPath = graphics.move(pX, pY)
|
|
|
|
// Check for single isolated point
|
|
if (
|
|
series[i][j + 1] === null ||
|
|
typeof series[i][j + 1] === 'undefined'
|
|
) {
|
|
linePaths.push(linePath)
|
|
areaPaths.push(areaPath)
|
|
// Stay in pathState = 0;
|
|
break
|
|
}
|
|
pathState = 1
|
|
if (j < series[i].length - 2) {
|
|
let p = graphics.curve(pX + length, pY, x - length, y, x, y)
|
|
linePath += p
|
|
areaPath += p
|
|
break
|
|
}
|
|
// Continue on with pathState 1 to finish the path and exit
|
|
case 1:
|
|
// Continuing with segment
|
|
if (series[i][j + 1] === null) {
|
|
// Segment ends here
|
|
if (isLowerRangeAreaPath) {
|
|
linePath += graphics.line(pX, y2)
|
|
} else {
|
|
linePath += graphics.move(pX, pY)
|
|
}
|
|
areaPath +=
|
|
graphics.line(pX, areaBottomY) +
|
|
graphics.line(segmentStartX, areaBottomY) +
|
|
'z'
|
|
linePaths.push(linePath)
|
|
areaPaths.push(areaPath)
|
|
pathState = -1
|
|
} else {
|
|
let p = graphics.curve(pX + length, pY, x - length, y, x, y)
|
|
linePath += p
|
|
areaPath += p
|
|
if (j >= series[i].length - 2) {
|
|
if (isLowerRangeAreaPath) {
|
|
// Need to add path portion that will join to the upper path
|
|
linePath +=
|
|
graphics.curve(x, y, x, y, x, y2) + graphics.move(x, y2)
|
|
}
|
|
areaPath +=
|
|
graphics.curve(x, y, x, y, x, areaBottomY) +
|
|
graphics.line(segmentStartX, areaBottomY) +
|
|
'z'
|
|
linePaths.push(linePath)
|
|
areaPaths.push(areaPath)
|
|
pathState = -1
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
pX = x
|
|
pY = y
|
|
|
|
break
|
|
default:
|
|
let pathToPoint = (curve, x, y) => {
|
|
let path = []
|
|
switch (curve) {
|
|
case 'stepline':
|
|
path = graphics.line(x, null, 'H') + graphics.line(null, y, 'V')
|
|
break
|
|
case 'linestep':
|
|
path = graphics.line(null, y, 'V') + graphics.line(x, null, 'H')
|
|
break
|
|
case 'straight':
|
|
path = graphics.line(x, y)
|
|
break
|
|
}
|
|
return path
|
|
}
|
|
if (series[i][j] === null) {
|
|
pathState = 0
|
|
} else {
|
|
switch (pathState) {
|
|
case 0:
|
|
// Beginning of segment
|
|
segmentStartX = pX
|
|
if (isLowerRangeAreaPath) {
|
|
// Need to add path portion that will join to the upper path
|
|
linePath = graphics.move(pX, y2Arrj[j]) + graphics.line(pX, pY)
|
|
} else {
|
|
linePath = graphics.move(pX, pY)
|
|
}
|
|
areaPath = graphics.move(pX, pY)
|
|
|
|
// Check for single isolated point
|
|
if (
|
|
series[i][j + 1] === null ||
|
|
typeof series[i][j + 1] === 'undefined'
|
|
) {
|
|
linePaths.push(linePath)
|
|
areaPaths.push(areaPath)
|
|
// Stay in pathState = 0
|
|
break
|
|
}
|
|
pathState = 1
|
|
if (j < series[i].length - 2) {
|
|
let p = pathToPoint(curve, x, y)
|
|
linePath += p
|
|
areaPath += p
|
|
break
|
|
}
|
|
// Continue on with pathState 1 to finish the path and exit
|
|
case 1:
|
|
// Continuing with segment
|
|
if (series[i][j + 1] === null) {
|
|
// Segment ends here
|
|
if (isLowerRangeAreaPath) {
|
|
linePath += graphics.line(pX, y2)
|
|
} else {
|
|
linePath += graphics.move(pX, pY)
|
|
}
|
|
areaPath +=
|
|
graphics.line(pX, areaBottomY) +
|
|
graphics.line(segmentStartX, areaBottomY) +
|
|
'z'
|
|
linePaths.push(linePath)
|
|
areaPaths.push(areaPath)
|
|
pathState = -1
|
|
} else {
|
|
let p = pathToPoint(curve, x, y)
|
|
linePath += p
|
|
areaPath += p
|
|
if (j >= series[i].length - 2) {
|
|
if (isLowerRangeAreaPath) {
|
|
// Need to add path portion that will join to the upper path
|
|
linePath += graphics.line(x, y2)
|
|
}
|
|
areaPath +=
|
|
graphics.line(x, areaBottomY) +
|
|
graphics.line(segmentStartX, areaBottomY) +
|
|
'z'
|
|
linePaths.push(linePath)
|
|
areaPaths.push(areaPath)
|
|
pathState = -1
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
pX = x
|
|
pY = y
|
|
|
|
break
|
|
}
|
|
|
|
return {
|
|
linePaths,
|
|
areaPaths,
|
|
pX,
|
|
pY,
|
|
pathState,
|
|
segmentStartX,
|
|
linePath,
|
|
areaPath,
|
|
}
|
|
}
|
|
|
|
handleNullDataPoints(series, pointsPos, i, j, realIndex) {
|
|
const w = this.w
|
|
if (
|
|
(series[i][j] === null && w.config.markers.showNullDataPoints) ||
|
|
series[i].length === 1
|
|
) {
|
|
let pSize = this.strokeWidth - w.config.markers.strokeWidth / 2
|
|
if (!(pSize > 0)) {
|
|
pSize = 0
|
|
}
|
|
|
|
// fixes apexcharts.js#1282, #1252
|
|
let elPointsWrap = this.markers.plotChartMarkers({
|
|
pointsPos,
|
|
seriesIndex: realIndex,
|
|
j: j + 1,
|
|
pSize,
|
|
alwaysDrawMarker: true,
|
|
})
|
|
if (elPointsWrap !== null) {
|
|
this.elPointsMain.add(elPointsWrap)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Line
|