UNPKG

apexcharts

Version:

A JavaScript Chart Library

1,273 lines (1,154 loc) 36.8 kB
// @ts-check 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 Series from '../modules/Series' 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 { /** * @param {import('../types/internal').ChartStateW} w * @param {import('../types/internal').ChartContext} ctx * @param {import('../types/internal').XYRatios} xyRatios * @param {boolean} isPointsChart */ constructor(w, ctx, xyRatios, isPointsChart) { this.ctx = ctx this.w = w this.xyRatios = xyRatios /** @type {number} */ this.xRatio = 0 /** @type {number[]} */ this.yRatio = [] /** @type {number} */ this.zRatio = 0 /** @type {number[]} */ this.baseLineY = [] this.pointsChart = !( this.w.config.chart.type !== 'bubble' && this.w.config.chart.type !== 'scatter' ) || isPointsChart this.scatter = new Scatter(this.w, this.ctx) this.noNegatives = this.w.globals.minX === Number.MAX_VALUE this.lineHelpers = new Helpers(this) this.markers = new Markers(this.w, this.ctx) /** @type {any} */ this.prevSeriesY = [] this.categoryAxisCorrection = 0 this.yaxisIndex = 0 /** @type {number} */ this.xDivision = 0 /** @type {number} */ this.zeroY = 0 /** @type {number} */ this.areaBottomY = 0 /** @type {number} */ this.strokeWidth = 0 /** @type {boolean} */ this.isReversed = false /** @type {boolean} */ this.appendPathFrom = false /** @type {any} */ this.elSeries = null /** @type {any} */ this.elPointsMain = null /** @type {any} */ this.elDataLabelsWrap = null } /** * @param {any[]} series * @param {string} ctype * @param {number} seriesIndex * @param {any} seriesRangeEnd */ draw(series, ctype, seriesIndex, seriesRangeEnd) { const w = this.w const graphics = new Graphics(this.w) const type = w.globals.comboCharts ? ctype : w.config.chart.type const ret = graphics.group({ class: `apexcharts-${type}-series apexcharts-plot-series`, }) const coreUtils = new CoreUtils(this.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) const allSeries = [] for (let i = 0; i < series.length; i++) { series = this.lineHelpers.sameValueSeriesFix(i, series) const realIndex = w.globals.comboCharts ? /** @type {any} */ (seriesIndex)[i] : i const translationsIndex = this.yRatio.length > 1 ? realIndex : 0 this._initSerieVariables(series, i, realIndex) const yArrj = [] // hold y values of current iterating series const y2Arrj = [] // holds y2 values in range-area charts const xArrj = [] // hold x values of current iterating series let x = w.globals.padHorizontal + this.categoryAxisCorrection const y = 1 /** @type {any[]} */ const linePaths = [] /** @type {any[]} */ const areaPaths = [] Series.addCollapsedClassToSeries(this.w, this.elSeries, realIndex) if (w.axisFlags.isXNumeric && w.seriesData.seriesX.length > 0) { x = (w.seriesData.seriesX[realIndex][0] - w.globals.minX) / this.xRatio } xArrj.push(x) const pX = x let pY2 const prevX = pX let prevY = this.zeroY let prevY2 = this.zeroY const lineYPosition = 0 // the first value in the current series is not null or undefined const 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) } const 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) } const pathsFrom = this._calculatePathsFrom({ type, series, i, realIndex, translationsIndex, prevX, prevY, prevY2, }) // RangeArea will resume with these for the upper path creation const rYArrj = [yArrj[0]] const rY2Arrj = [y2Arrj[0]] const iteratingOpts = { type, series, realIndex, translationsIndex, i, x, y, pX, pY, pathsFrom, linePaths, areaPaths, seriesIndex, lineYPosition, xArrj, yArrj, y2Arrj, seriesRangeEnd, } const paths = this._iterateOverDataPoints({ ...iteratingOpts, iterations: type === 'rangeArea' ? series[i].length - 1 : undefined, isRangeStart: true, }) if (type === 'rangeArea') { const pathsFrom2 = this._calculatePathsFrom({ series: seriesRangeEnd, i, realIndex, prevX, prevY: prevY2, }) const 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. const 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 ( /** @type {Record<string,any>} */ (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 } /** * @param {any[]} series * @param {number} i * @param {number} realIndex */ _initSerieVariables(series, i, realIndex) { const w = this.w const graphics = new Graphics(this.w) // width divided into equal parts this.xDivision = w.layout.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.layout.gridHeight - this.baseLineY[translationsIndex] - (this.isReversed ? w.layout.gridHeight : 0) + (this.isReversed ? this.baseLineY[translationsIndex] * 2 : 0) this.areaBottomY = this.zeroY if ( this.zeroY > w.layout.gridHeight || w.config.plotOptions.area.fillTo === 'end' ) { this.areaBottomY = w.layout.gridHeight } this.categoryAxisCorrection = this.xDivision / 2 // el to which series will be drawn const seriesItem = /** @type {Record<string,any>} */ ( w.config.series[realIndex] ) this.elSeries = graphics.group({ class: `apexcharts-series`, zIndex: typeof seriesItem.zIndex !== 'undefined' ? seriesItem.zIndex : realIndex, seriesName: Utils.escapeString(w.seriesData.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.layout.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, }) const longestSeries = series[i].length === w.globals.dataPoints this.elSeries.attr({ 'data:longestSeries': longestSeries, rel: i + 1, 'data:realIndex': realIndex, }) this.appendPathFrom = true } /** @param {{ type?: any, series?: any, i?: any, realIndex?: any, translationsIndex?: any, prevX?: any, prevY?: any, prevY2?: any }} opts */ _calculatePathsFrom({ type, series, i, realIndex, translationsIndex, prevX, prevY, prevY2, }) { const w = this.w const graphics = new Graphics(this.w) 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, } } /** @param {{type: any, realIndex: any, i: any, paths: any}} opts */ _handlePaths({ type, realIndex, i, paths }) { const w = this.w const graphics = new Graphics(this.w) const fill = new Fill(this.w) // 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.layout.gridWidth, w.layout.gridHeight, 0, ) w.dom.elForecastMask.appendChild(elForecastMask.node) const elNonForecastMask = graphics.drawRect( 0, 0, forecastCutoff, w.layout.gridHeight, 0, ) w.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') { const pathFill = fill.fillPath({ seriesNumber: realIndex, }) for (let p = 0; p < paths.areaPaths.length; p++) { const 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', } const renderedPath = graphics.renderPaths(linePathCommonOpts) this.elSeries.add(renderedPath) renderedPath.attr('fill-rule', `evenodd`) if (forecast.count > 0 && type !== 'rangeArea') { const 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 {any} */ { type, series, iterations, realIndex, translationsIndex, i, x, y, pX, pY, pathsFrom, linePaths, areaPaths, seriesIndex, lineYPosition, xArrj, yArrj, y2Arrj, isRangeStart, seriesRangeEnd, }, ) { const w = this.w const graphics = new Graphics(this.w) const 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 } /** * @param {number} _y * @param {number} lineYPos */ const getY = (_y, lineYPos) => { return ( lineYPos - _y / yRatio[translationsIndex] + (this.isReversed ? _y / yRatio[translationsIndex] : 0) * 2 ) } let y2 = y const stackSeries = (w.config.chart.stacked && !w.globals.comboCharts) || (w.config.chart.stacked && w.globals.comboCharts && (!this.w.config.chart.stackOnlyBar || /** @type {Record<string,any>} */ (this.w.config.series[realIndex]) ?.type === 'bar' || /** @type {Record<string,any>} */ (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.axisFlags.isXNumeric) { let sX = w.seriesData.seriesX[realIndex][j + 1] if (typeof w.seriesData.seriesX[realIndex][j + 1] === 'undefined') { /* fix #374 */ sX = w.seriesData.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 /** * @param {number} pi */ 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) } const pointsPos = this.lineHelpers.calculatePoints({ series, x, y, realIndex, i, j, prevY, }) const calculatedPaths = this._createPaths({ type, series, i, 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, } } /** @param {{type: any, pointsPos: any, isRangeStart: any, i: any, j: any, realIndex: any}} opts */ _handleMarkersAndLabels({ type, pointsPos, isRangeStart, i, j, realIndex }) { const w = this.w const dataLabels = new DataLabels(this.w, this.ctx) if (!this.pointsChart) { if (w.seriesData.series[i].length > 1) { this.elPointsMain.node.classList.add('apexcharts-element-hidden') } const 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, }) } const drawnLabels = dataLabels.drawDataLabel({ type, isRangeStart, pos: pointsPos, i: realIndex, j: j + 1, }) if (drawnLabels !== null) { this.elDataLabelsWrap.add(drawnLabels) } } /** @param {{type: any, series: any, i: any, j: any, x: any, y: any, xArrj: any, yArrj: any, y2: any, y2Arrj: any, pX: any, pY: any, pathState: any, segmentStartX: any, linePath: any, areaPath: any, linePaths: any, areaPaths: any, curve: any, isRangeStart: any}} opts */ _createPaths({ type, series, i, j, x, y, xArrj, yArrj, y2, y2Arrj, pX, pY, pathState, segmentStartX, linePath, areaPath, linePaths, areaPaths, curve, isRangeStart, }) { const graphics = new Graphics(this.w) const areaBottomY = this.areaBottomY const rangeArea = type === 'rangeArea' const isLowerRangeAreaPath = type === 'rangeArea' && isRangeStart switch (curve) { case 'monotoneCubic': { const yAj = isRangeStart ? yArrj : y2Arrj /** * @param {any[]} xArr * @param {any[]} yArr */ const getSmoothInputs = (xArr, yArr) => { return ( xArr /** * @param {any} _ * @param {number} i */ .map((_, i) => { return [_, yArr[i]] }) /** * @param {any} _ */ .filter((_) => _[1] !== null) ) } /** * @param {any[]} yArr */ const getSegmentLengths = (yArr) => { // Get the segment lengths so the segments can be extracted from // the null-filtered smoothInputs array const segLens = [] let count = 0 /** * @param {any} _ */ yArr.forEach((_) => { if (_ !== null) { count++ } else if (count > 0) { segLens.push(count) count = 0 } }) if (count > 0) { segLens.push(count) } return segLens } /** * @param {any[]} yArr * @param {any} points */ const getSegments = (yArr, points) => { const segLens = getSegmentLengths(yArr) const 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 // falls through case 1: if ( !(rangeArea ? xArrj.length === series[i].length : j === series[i].length - 2) ) { break } // falls through 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 /** @type {any[]} */ 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++ const svgPoints = svgPath(_) const _start = smoothInputsIndex smoothInputsIndex += _.length const _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 const upperLinePaths = linePaths.slice(segmentCount).reverse() linePaths.splice(segmentCount) /** * @param {string} u */ upperLinePaths.forEach((/** @type {any} */ u) => linePaths.push(u), ) } pathState = 0 break } } break } case 'smooth': { const 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) { const p = graphics.curve(pX + length, pY, x - length, y, x, y) linePath += p areaPath += p break } // falls through 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 { const 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: { /** * @param {string} curve * @param {number} x * @param {number} y */ const pathToPoint = (curve, x, y) => { /** @type {string} */ 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) { const p = pathToPoint(curve, x, y) linePath += p areaPath += p break } // falls through 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 { const 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, } } /** * @param {any[]} series * @param {any} pointsPos * @param {number} i * @param {number} j * @param {number} realIndex */ 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 const elPointsWrap = this.markers.plotChartMarkers({ pointsPos, seriesIndex: realIndex, j: j + 1, pSize, alwaysDrawMarker: true, }) if (elPointsWrap !== null) { this.elPointsMain.add(elPointsWrap) } } } } export default Line