UNPKG

ikizmet-apexcharts

Version:

iKizmet fork of Apex Charts library

703 lines (605 loc) 19.2 kB
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' /** * ApexCharts Line Class responsible for drawing Line / Area 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.yaxisIndex = 0 } draw(series, ptype, seriesIndex) { let w = this.w let graphics = new Graphics(this.ctx) let fill = new Fill(this.ctx) let type = w.globals.comboCharts ? ptype : w.config.chart.type let ret = graphics.group({ class: `apexcharts-${type}-series apexcharts-plot-series` }) const coreUtils = new CoreUtils(this.ctx, w) series = coreUtils.getLogSeries(series) let yRatio = this.xyRatios.yRatio yRatio = coreUtils.getLogYRatios(yRatio) let zRatio = this.xyRatios.zRatio let xRatio = this.xyRatios.xRatio let baseLineY = this.xyRatios.baseLineY // push all series in an array, so we can draw in reverse order (for stacked charts) let allSeries = [] let prevSeriesY = [] let categoryAxisCorrection = 0 for (let i = 0; i < series.length; i++) { // width divided into equal parts if ( type === 'line' && (w.config.fill.type === 'gradient' || w.config.fill.type[i] === 'gradient') ) { // a small adjustment to allow gradient line to draw correctly for all same values /* #fix https://github.com/apexcharts/apexcharts.js/issues/358 */ if (coreUtils.seriesHaveSameValues(i)) { let gSeries = series[i].slice() gSeries[gSeries.length - 1] = gSeries[gSeries.length - 1] + 0.000001 series[i] = gSeries } } let xDivision = w.globals.gridWidth / w.globals.dataPoints let realIndex = w.globals.comboCharts ? seriesIndex[i] : i if (yRatio.length > 1) { this.yaxisIndex = realIndex } this.isReversed = w.config.yaxis[this.yaxisIndex] && w.config.yaxis[this.yaxisIndex].reversed let yArrj = [] // hold y values of current iterating series let xArrj = [] // hold x values of current iterating series // zeroY is the 0 value in y series which can be used in negative charts let zeroY = w.globals.gridHeight - baseLineY[this.yaxisIndex] - (this.isReversed ? w.globals.gridHeight : 0) + (this.isReversed ? baseLineY[this.yaxisIndex] * 2 : 0) let areaBottomY = zeroY if (zeroY > w.globals.gridHeight) { areaBottomY = w.globals.gridHeight } categoryAxisCorrection = xDivision / 2 let x = w.globals.padHorizontal + categoryAxisCorrection let y = 1 if (w.globals.isXNumeric && w.globals.seriesX.length > 0) { x = (w.globals.seriesX[realIndex][0] - w.globals.minX) / xRatio } xArrj.push(x) let linePath, areaPath, pathFromLine, pathFromArea let linePaths = [] let areaPaths = [] // el to which series will be drawn let elSeries = graphics.group({ class: `apexcharts-series ${Utils.escapeString( w.globals.seriesNames[realIndex] )}` }) // points let elPointsMain = graphics.group({ class: 'apexcharts-series-markers-wrap' }) // eldatalabels let elDataLabelsWrap = graphics.group({ class: 'apexcharts-datalabels' }) this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex) let longestSeries = series[i].length === w.globals.dataPoints elSeries.attr({ 'data:longestSeries': longestSeries, rel: i + 1, 'data:realIndex': realIndex }) this.appendPathFrom = true let pX = x let pY let prevX = pX let prevY = zeroY // w.globals.svgHeight; let lineYPosition = 0 // the first value in the current series is not null or undefined let firstPrevY = this.determineFirstPrevY({ i, series, yRatio: yRatio[this.yaxisIndex], zeroY, prevY, prevSeriesY, lineYPosition }) prevY = firstPrevY.prevY yArrj.push(prevY) pY = prevY 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 = xDivision * s prevY = zeroY - series[i][s] / yRatio[this.yaxisIndex] linePath = graphics.move(prevX, prevY) areaPath = graphics.move(prevX, areaBottomY) break } } } else { linePath = graphics.move(prevX, prevY) areaPath = graphics.move(prevX, areaBottomY) + graphics.line(prevX, prevY) } pathFromLine = graphics.move(-1, zeroY) + graphics.line(-1, zeroY) pathFromArea = graphics.move(-1, zeroY) + graphics.line(-1, zeroY) if (w.globals.previousPaths.length > 0) { const pathFrom = this.checkPreviousPaths({ pathFromLine, pathFromArea, realIndex }) pathFromLine = pathFrom.pathFromLine pathFromArea = pathFrom.pathFromArea } const iterations = w.globals.dataPoints > 1 ? w.globals.dataPoints - 1 : w.globals.dataPoints for (let j = 0; j < iterations; j++) { 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) / xRatio } else { x = x + xDivision } const minY = Utils.isNumber(w.globals.minYArr[realIndex]) ? w.globals.minYArr[realIndex] : w.globals.minY if (w.config.chart.stacked) { if ( i > 0 && w.globals.collapsedSeries.length < w.config.series.length - 1 ) { lineYPosition = prevSeriesY[i - 1][j + 1] } else { // the first series will not have prevY values lineYPosition = zeroY } if ( typeof series[i][j + 1] === 'undefined' || series[i][j + 1] === null ) { y = lineYPosition - minY / yRatio[this.yaxisIndex] + (this.isReversed ? minY / yRatio[this.yaxisIndex] : 0) * 2 } else { y = lineYPosition - series[i][j + 1] / yRatio[this.yaxisIndex] + (this.isReversed ? series[i][j + 1] / yRatio[this.yaxisIndex] : 0) * 2 } } else { if ( typeof series[i][j + 1] === 'undefined' || series[i][j + 1] === null ) { y = zeroY - minY / yRatio[this.yaxisIndex] + (this.isReversed ? minY / yRatio[this.yaxisIndex] : 0) * 2 } else { y = zeroY - series[i][j + 1] / yRatio[this.yaxisIndex] + (this.isReversed ? series[i][j + 1] / yRatio[this.yaxisIndex] : 0) * 2 } } // push current X xArrj.push(x) // push current Y that will be used as next series's bottom position yArrj.push(y) let calculatedPaths = this.createPaths({ series, i, j, x, y, xDivision, pX, pY, areaBottomY, linePath, areaPath, linePaths, areaPaths, seriesIndex }) areaPaths = calculatedPaths.areaPaths linePaths = calculatedPaths.linePaths pX = calculatedPaths.pX pY = calculatedPaths.pY areaPath = calculatedPaths.areaPath linePath = calculatedPaths.linePath if (this.appendPathFrom) { pathFromLine = pathFromLine + graphics.line(x, zeroY) pathFromArea = pathFromArea + graphics.line(x, zeroY) } let pointsPos = this.calculatePoints({ series, x, y, realIndex, i, j, prevY, categoryAxisCorrection, xRatio }) if (!this.pointsChart) { let markers = new Markers(this.ctx) if (w.globals.dataPoints > 1) { elPointsMain.node.classList.add('hidden') } let elPointsWrap = markers.plotChartMarkers( pointsPos, realIndex, j + 1 ) if (elPointsWrap !== null) { elPointsMain.add(elPointsWrap) } } else { // scatter / bubble chart points creation this.scatter.draw(elSeries, j, { realIndex, pointsPos, zRatio, elParent: elPointsMain }) } let dataLabelAlign = !series[i][j + 1] || series[i][j + 1] > series[i][j] ? 'top' : 'bottom' let dataLabels = new DataLabels(this.ctx) let drawnLabels = dataLabels.drawDataLabel( pointsPos, realIndex, j + 1, null, dataLabelAlign ) if (drawnLabels !== null) { elDataLabelsWrap.add(drawnLabels) } } // push all current y values array to main PrevY Array prevSeriesY.push(yArrj) // push all x val arrays into main xArr w.globals.seriesXvalues[realIndex] = xArrj w.globals.seriesYvalues[realIndex] = yArrj // these elements will be shown after area path animation completes if (!this.pointsChart) { w.globals.delayedElements.push({ el: 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}`, id: `apexcharts-${type}` } if (type === 'area') { let pathFill = fill.fillPath({ seriesNumber: realIndex }) for (let p = 0; p < areaPaths.length; p++) { let renderedPath = graphics.renderPaths({ ...defaultRenderedPathOptions, pathFrom: pathFromArea, pathTo: areaPaths[p], stroke: 'none', strokeWidth: 0, strokeLineCap: null, fill: pathFill }) elSeries.add(renderedPath) } } if (w.config.stroke.show && !this.pointsChart) { let lineFill = null if (type === 'line') { // fillable lines only for lineChart lineFill = fill.fillPath({ seriesNumber: realIndex, i: i }) } else { lineFill = w.globals.stroke.colors[realIndex] } for (let p = 0; p < linePaths.length; p++) { let renderedPath = graphics.renderPaths({ ...defaultRenderedPathOptions, pathFrom: pathFromLine, pathTo: linePaths[p], stroke: lineFill, strokeWidth: Array.isArray(w.config.stroke.width) ? w.config.stroke.width[realIndex] : w.config.stroke.width, strokeLineCap: w.config.stroke.lineCap, fill: 'none' }) elSeries.add(renderedPath) } } elSeries.add(elPointsMain) elSeries.add(elDataLabelsWrap) allSeries.push(elSeries) } for (let s = allSeries.length; s > 0; s--) { ret.add(allSeries[s - 1]) } return ret } createPaths({ series, i, j, x, y, pX, pY, xDivision, areaBottomY, linePath, areaPath, linePaths, areaPaths, seriesIndex }) { let w = this.w let graphics = new Graphics(this.ctx) var curve = w.config.stroke.curve if (Array.isArray(w.config.stroke.curve)) { if (Array.isArray(seriesIndex)) { curve = w.config.stroke.curve[seriesIndex[i]] } else { curve = w.config.stroke.curve[i] } } // logic of smooth curve derived from chartist // CREDITS: https://gionkunz.github.io/chartist-js/ if (curve === 'smooth') { let length = (x - pX) * 0.35 if (w.globals.hasNullValues) { if (series[i][j] !== null) { if (series[i][j + 1] !== null) { linePath = graphics.move(pX, pY) + graphics.curve(pX + length, pY, x - length, y, x + 1, y) areaPath = graphics.move(pX + 1, pY) + graphics.curve(pX + length, pY, x - length, y, x + 1, y) + graphics.line(x, areaBottomY) + graphics.line(pX, areaBottomY) + 'z' } else { linePath = graphics.move(pX, pY) areaPath = graphics.move(pX, pY) + 'z' } } linePaths.push(linePath) areaPaths.push(areaPath) } else { linePath = linePath + graphics.curve(pX + length, pY, x - length, y, x, y) areaPath = areaPath + graphics.curve(pX + length, pY, x - length, y, x, y) } pX = x pY = y if (j === series[i].length - 2) { // last loop, close path areaPath = areaPath + graphics.curve(pX, pY, x, y, x, areaBottomY) + graphics.move(x, y) + 'z' if (!w.globals.hasNullValues) { linePaths.push(linePath) areaPaths.push(areaPath) } } } else { if (series[i][j + 1] === null) { linePath = linePath + graphics.move(x, y) areaPath = areaPath + graphics.line(x - xDivision, areaBottomY) + graphics.move(x, y) } if (series[i][j] === null) { linePath = linePath + graphics.move(x, y) areaPath = areaPath + graphics.move(x, areaBottomY) } if (curve === 'stepline') { linePath = linePath + graphics.line(x, null, 'H') + graphics.line(null, y, 'V') areaPath = areaPath + graphics.line(x, null, 'H') + graphics.line(null, y, 'V') } else if (curve === 'straight') { linePath = linePath + graphics.line(x, y) areaPath = areaPath + graphics.line(x, y) } if (j === series[i].length - 2) { // last loop, close path areaPath = areaPath + graphics.line(x, areaBottomY) + graphics.move(x, y) + 'z' linePaths.push(linePath) areaPaths.push(areaPath) } } return { linePaths, areaPaths, pX, pY, linePath, areaPath } } calculatePoints({ series, realIndex, x, y, i, j, prevY, categoryAxisCorrection, xRatio }) { let w = this.w let ptX = [] let ptY = [] if (j === 0) { let xPT1st = categoryAxisCorrection + w.config.markers.offsetX // the first point for line series // we need to check whether it's not a time series, because a time series may // start from the middle of the x axis if (w.globals.isXNumeric) { xPT1st = (w.globals.seriesX[realIndex][0] - w.globals.minX) / xRatio + w.config.markers.offsetX } // push 2 points for the first data values ptX.push(xPT1st) ptY.push( Utils.isNumber(series[i][0]) ? prevY + w.config.markers.offsetY : null ) ptX.push(x + w.config.markers.offsetX) ptY.push( Utils.isNumber(series[i][j + 1]) ? y + w.config.markers.offsetY : null ) } else { ptX.push(x + w.config.markers.offsetX) ptY.push( Utils.isNumber(series[i][j + 1]) ? y + w.config.markers.offsetY : null ) } let pointsPos = { x: ptX, y: ptY } return pointsPos } checkPreviousPaths({ pathFromLine, pathFromArea, realIndex }) { let w = this.w for (let pp = 0; pp < w.globals.previousPaths.length; pp++) { let gpp = w.globals.previousPaths[pp] if ( (gpp.type === 'line' || gpp.type === 'area') && gpp.paths.length > 0 && parseInt(gpp.realIndex) === parseInt(realIndex) ) { if (gpp.type === 'line') { this.appendPathFrom = false pathFromLine = w.globals.previousPaths[pp].paths[0].d } else if (gpp.type === 'area') { this.appendPathFrom = false pathFromArea = w.globals.previousPaths[pp].paths[0].d if (w.config.stroke.show) { pathFromLine = w.globals.previousPaths[pp].paths[1].d } } } } return { pathFromLine, pathFromArea } } determineFirstPrevY({ i, series, yRatio, zeroY, prevY, prevSeriesY, lineYPosition }) { let w = this.w if (typeof series[i][0] !== 'undefined') { if (w.config.chart.stacked) { if (i > 0) { // 1st y value of previous series lineYPosition = prevSeriesY[i - 1][0] } else { // the first series will not have prevY values lineYPosition = zeroY } prevY = lineYPosition - series[i][0] / yRatio + (this.isReversed ? series[i][0] / yRatio : 0) * 2 } else { prevY = zeroY - series[i][0] / yRatio + (this.isReversed ? series[i][0] / yRatio : 0) * 2 } } else { // the first value in the current series is null if ( w.config.chart.stacked && i > 0 && typeof series[i][0] === 'undefined' ) { // check for undefined value (undefined value will occur when we clear the series while user clicks on legend to hide serieses) for (let s = i - 1; s >= 0; s--) { // for loop to get to 1st previous value until we get it if (series[s][0] !== null && typeof series[s][0] !== 'undefined') { lineYPosition = prevSeriesY[s][0] prevY = lineYPosition break } } } } return { prevY, lineYPosition } } } export default Line