apexcharts
Version:
A JavaScript Chart Library
572 lines (480 loc) • 16.7 kB
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'
/**
* 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
if (this.pointsChart) {
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
let xDivision = w.globals.gridWidth / w.globals.dataPoints
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
if (yRatio.length > 1) {
this.yaxisIndex = realIndex
}
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]
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) {
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 ${w.globals.seriesNames[realIndex].toString().replace(/ /g, '-')}`
})
// 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) {
x = (w.globals.seriesX[realIndex][j + 1] - 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]
} else {
y = (lineYPosition - series[i][j + 1] / yRatio[this.yaxisIndex])
}
} else {
if (typeof series[i][j + 1] === 'undefined' || series[i][j + 1] === null) {
y = zeroY - minY / yRatio[this.yaxisIndex]
} else {
y = (zeroY - series[i][j + 1] / yRatio[this.yaxisIndex])
}
}
// 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
})
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 dataLabels = new DataLabels(this.ctx)
let drawnLabels = dataLabels.drawDataLabel(pointsPos, realIndex, j + 1)
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 (w.config.stroke.show && !this.pointsChart) {
let lineFill = null
if (type === 'line') {
// fillable lines only for lineChart
lineFill = fill.fillPath(elSeries, {
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)
}
}
// we have drawn the lines, now if it is area chart, we need to fill paths
if (type === 'area') {
let pathFill = fill.fillPath(elSeries, {
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)
}
}
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
}) {
let w = this.w
let graphics = new Graphics(this.ctx)
const curve = Array.isArray(w.config.stroke.curve) ? w.config.stroke.curve[i] : w.config.stroke.curve
// 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
pathFromLine = w.globals.previousPaths[pp].paths[0].d
pathFromArea = 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)
} else {
prevY = (zeroY - series[i][0] / yRatio)
}
} 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