ikizmet-apexcharts
Version:
iKizmet fork of Apex Charts library
703 lines (605 loc) • 19.2 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
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