initial commit

This commit is contained in:
2026-06-06 10:05:39 +07:00
commit b22608adbc
1577 changed files with 641460 additions and 0 deletions
@@ -0,0 +1,492 @@
import BarDataLabels from './common/bar/DataLabels'
import BarHelpers from './common/bar/Helpers'
import CoreUtils from '../modules/CoreUtils'
import Utils from '../utils/Utils'
import Filters from '../modules/Filters'
import Graphics from '../modules/Graphics'
/**
* ApexCharts Bar Class responsible for drawing both Columns and Bars.
*
* @module Bar
**/
class Bar {
constructor(ctx, xyRatios) {
this.ctx = ctx
this.w = ctx.w
const w = this.w
this.barOptions = w.config.plotOptions.bar
this.isHorizontal = this.barOptions.horizontal
this.strokeWidth = w.config.stroke.width
this.isNullValue = false
this.isRangeBar = w.globals.seriesRangeBar.length && this.isHorizontal
this.xyRatios = xyRatios
if (this.xyRatios !== null) {
this.xRatio = xyRatios.xRatio
this.initialXRatio = xyRatios.initialXRatio
this.yRatio = xyRatios.yRatio
this.invertedXRatio = xyRatios.invertedXRatio
this.invertedYRatio = xyRatios.invertedYRatio
this.baseLineY = xyRatios.baseLineY
this.baseLineInvertedY = xyRatios.baseLineInvertedY
}
this.yaxisIndex = 0
this.seriesLen = 0
this.barHelpers = new BarHelpers(this)
}
/** primary draw method which is called on bar object
* @memberof Bar
* @param {array} series - user supplied series values
* @param {int} seriesIndex - the index by which series will be drawn on the svg
* @return {node} element which is supplied to parent chart draw method for appending
**/
draw(series, seriesIndex) {
let w = this.w
let graphics = new Graphics(this.ctx)
const coreUtils = new CoreUtils(this.ctx, w)
series = coreUtils.getLogSeries(series)
this.series = series
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
this.barHelpers.initVariables(series)
let ret = graphics.group({
class: 'apexcharts-bar-series apexcharts-plot-series'
})
if (w.config.dataLabels.enabled) {
if (this.totalItems > this.barOptions.dataLabels.maxItems) {
console.warn(
'WARNING: DataLabels are enabled but there are too many to display. This may cause performance issue when rendering.'
)
}
}
for (let i = 0, bc = 0; i < series.length; i++, bc++) {
let x,
y,
xDivision, // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
yDivision, // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroH, // zeroH is the baseline where 0 meets y axis
zeroW // zeroW is the baseline where 0 meets x axis
let yArrj = [] // hold y values of current iterating series
let xArrj = [] // hold x values of current iterating series
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series`,
rel: i + 1,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
'data:realIndex': realIndex
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1
}
let barHeight = 0
let barWidth = 0
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
this.isReversed =
w.config.yaxis[this.yaxisIndex] &&
w.config.yaxis[this.yaxisIndex].reversed
let initPositions = this.barHelpers.initialPositions()
y = initPositions.y
barHeight = initPositions.barHeight
yDivision = initPositions.yDivision
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
if (!this.horizontal) {
xArrj.push(x + barWidth / 2)
}
// eldatalabels
let elDataLabelsWrap = graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex
})
let elGoalsMarkers = graphics.group({
class: 'apexcharts-bar-goals-markers',
style: `pointer-events: none`
})
for (let j = 0; j < w.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
let paths = null
const pathsParams = {
indexes: {
i,
j,
realIndex,
bc
},
x,
y,
strokeWidth,
elSeries
}
if (this.isHorizontal) {
paths = this.drawBarPaths({
...pathsParams,
barHeight,
zeroW,
yDivision
})
barWidth = this.series[i][j] / this.invertedYRatio
} else {
paths = this.drawColumnPaths({
...pathsParams,
xDivision,
barWidth,
zeroH
})
barHeight = this.series[i][j] / this.yRatio[this.yaxisIndex]
}
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition: paths.barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth
})
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine)
}
y = paths.y
x = paths.x
// push current X
if (j > 0) {
xArrj.push(x + barWidth / 2)
}
yArrj.push(y)
let pathFill = this.barHelpers.getPathFillColor(series, i, j, realIndex)
this.renderSeries({
realIndex,
pathFill,
j,
i,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight,
barWidth,
elDataLabelsWrap,
elGoalsMarkers,
visibleSeries: this.visibleI,
type: 'bar'
})
}
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = xArrj
w.globals.seriesYvalues[realIndex] = yArrj
ret.add(elSeries)
}
return ret
}
renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
pathFrom,
pathTo,
strokeWidth,
elSeries,
x,
y,
y1,
y2,
series,
barHeight,
barWidth,
barYPosition,
elDataLabelsWrap,
elGoalsMarkers,
visibleSeries,
type
}) {
const w = this.w
const graphics = new Graphics(this.ctx)
if (!lineFill) {
/* fix apexcharts#341 */
lineFill = this.barOptions.distributed
? w.globals.stroke.colors[j]
: w.globals.stroke.colors[realIndex]
}
if (w.config.series[i].data[j] && w.config.series[i].data[j].strokeColor) {
lineFill = w.config.series[i].data[j].strokeColor
}
if (this.isNullValue) {
pathFill = 'none'
}
let delay =
((j / w.config.chart.animations.animateGradually.delay) *
(w.config.chart.animations.speed / w.globals.dataPoints)) /
2.4
let renderedPath = graphics.renderPaths({
i,
j,
realIndex,
pathFrom,
pathTo,
stroke: lineFill,
strokeWidth,
strokeLineCap: w.config.stroke.lineCap,
fill: pathFill,
animationDelay: delay,
initialSpeed: w.config.chart.animations.speed,
dataChangeSpeed: w.config.chart.animations.dynamicAnimation.speed,
className: `apexcharts-${type}-area`
})
renderedPath.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
const forecast = w.config.forecastDataPoints
if (forecast.count > 0) {
if (j >= w.globals.dataPoints - forecast.count) {
renderedPath.node.setAttribute('stroke-dasharray', forecast.dashArray)
renderedPath.node.setAttribute('stroke-width', forecast.strokeWidth)
renderedPath.node.setAttribute('fill-opacity', forecast.fillOpacity)
}
}
if (typeof y1 !== 'undefined' && typeof y2 !== 'undefined') {
renderedPath.attr('data-range-y1', y1)
renderedPath.attr('data-range-y2', y2)
}
const filters = new Filters(this.ctx)
filters.setSelectionFilter(renderedPath, realIndex, j)
elSeries.add(renderedPath)
let barDataLabels = new BarDataLabels(this)
let dataLabels = barDataLabels.handleBarDataLabels({
x,
y,
y1,
y2,
i,
j,
series,
realIndex,
barHeight,
barWidth,
barYPosition,
renderedPath,
visibleSeries
})
if (dataLabels !== null) {
elDataLabelsWrap.add(dataLabels)
}
elSeries.add(elDataLabelsWrap)
if (elGoalsMarkers) {
elSeries.add(elGoalsMarkers)
}
return elSeries
}
drawBarPaths({
indexes,
barHeight,
strokeWidth,
zeroW,
x,
y,
yDivision,
elSeries
}) {
let w = this.w
let i = indexes.i
let j = indexes.j
if (w.globals.isXNumeric) {
y =
(w.globals.seriesX[i][j] - w.globals.minX) / this.invertedXRatio -
barHeight
}
let barYPosition = y + barHeight * this.visibleI
x = this.barHelpers.getXForValue(this.series[i][j], zeroW)
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1: zeroW,
x2: x,
strokeWidth,
series: this.series,
realIndex: indexes.realIndex,
i,
j,
w
})
if (!w.globals.isXNumeric) {
y = y + yDivision
}
this.barHelpers.barBackground({
j,
i,
y1: barYPosition - barHeight * this.visibleI,
y2: barHeight * this.seriesLen,
elSeries
})
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x,
y,
goalX: this.barHelpers.getGoalValues('x', zeroW, null, i, j),
barYPosition
}
}
drawColumnPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
strokeWidth,
elSeries
}) {
let w = this.w
let realIndex = indexes.realIndex
let i = indexes.i
let j = indexes.j
let bc = indexes.bc
if (w.globals.isXNumeric) {
let sxI = realIndex
if (!w.globals.seriesX[realIndex].length) {
sxI = w.globals.maxValsInArrayIndex
}
x =
(w.globals.seriesX[sxI][j] - w.globals.minX) / this.xRatio -
(barWidth * this.seriesLen) / 2
}
let barXPosition = x + barWidth * this.visibleI
y = this.barHelpers.getYForValue(this.series[i][j], zeroH)
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1: zeroH,
y2: y,
strokeWidth,
series: this.series,
realIndex: indexes.realIndex,
i,
j,
w
})
if (!w.globals.isXNumeric) {
x = x + xDivision
}
this.barHelpers.barBackground({
bc,
j,
i,
x1: barXPosition - strokeWidth / 2 - barWidth * this.visibleI,
x2: barWidth * this.seriesLen + strokeWidth / 2,
elSeries
})
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x,
y,
goalY: this.barHelpers.getGoalValues('y', null, zeroH, i, j),
barXPosition
}
}
/** getPreviousPath is a common function for bars/columns which is used to get previous paths when data changes.
* @memberof Bar
* @param {int} realIndex - current iterating i
* @param {int} j - current iterating series's j index
* @return {string} pathFrom is the string which will be appended in animations
**/
getPreviousPath(realIndex, j) {
let w = this.w
let pathFrom
for (let pp = 0; pp < w.globals.previousPaths.length; pp++) {
let gpp = w.globals.previousPaths[pp]
if (
gpp.paths &&
gpp.paths.length > 0 &&
parseInt(gpp.realIndex, 10) === parseInt(realIndex, 10)
) {
if (typeof w.globals.previousPaths[pp].paths[j] !== 'undefined') {
pathFrom = w.globals.previousPaths[pp].paths[j].d
}
}
}
return pathFrom
}
}
export default Bar
@@ -0,0 +1,481 @@
import CoreUtils from '../modules/CoreUtils'
import Bar from './Bar'
import Graphics from '../modules/Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts BarStacked Class responsible for drawing both Stacked Columns and Bars.
*
* @module BarStacked
* The whole calculation for stacked bar/column is different from normal bar/column,
* hence it makes sense to derive a new class for it extending most of the props of Parent Bar
**/
class BarStacked extends Bar {
draw(series, seriesIndex) {
let w = this.w
this.graphics = new Graphics(this.ctx)
this.bar = new Bar(this.ctx, this.xyRatios)
const coreUtils = new CoreUtils(this.ctx, w)
series = coreUtils.getLogSeries(series)
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
this.barHelpers.initVariables(series)
if (w.config.chart.stackType === '100%') {
series = w.globals.seriesPercent.slice()
}
this.series = series
this.totalItems = 0
this.prevY = [] // y position on chart
this.prevX = [] // x position on chart
this.prevYF = [] // y position including shapes on chart
this.prevXF = [] // x position including shapes on chart
this.prevYVal = [] // y values (series[i][j]) in columns
this.prevXVal = [] // x values (series[i][j]) in bars
this.xArrj = [] // xj indicates x position on graph in bars
this.xArrjF = [] // xjF indicates bar's x position + roundedShape's positions in bars
this.xArrjVal = [] // x val means the actual series's y values in horizontal/bars
this.yArrj = [] // yj indicates y position on graph in columns
this.yArrjF = [] // yjF indicates bar's y position + roundedShape's positions in columns
this.yArrjVal = [] // y val means the actual series's y values in columns
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length > 0) {
this.totalItems += series[sl].length
}
}
let ret = this.graphics.group({
class: 'apexcharts-bar-series apexcharts-plot-series'
})
let x = 0
let y = 0
for (let i = 0, bc = 0; i < series.length; i++, bc++) {
let xDivision // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
let yDivision // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
let zeroH // zeroH is the baseline where 0 meets y axis
let zeroW // zeroW is the baseline where 0 meets x axis
let xArrValues = []
let yArrValues = []
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
this.isReversed =
w.config.yaxis[this.yaxisIndex] &&
w.config.yaxis[this.yaxisIndex].reversed
// el to which series will be drawn
let elSeries = this.graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
rel: i + 1,
'data:realIndex': realIndex
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
// eldatalabels
let elDataLabelsWrap = this.graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex
})
let barHeight = 0
let barWidth = 0
let initPositions = this.initialPositions(
x,
y,
xDivision,
yDivision,
zeroH,
zeroW
)
y = initPositions.y
barHeight = initPositions.barHeight
yDivision = initPositions.yDivision
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
this.yArrj = []
this.yArrjF = []
this.yArrjVal = []
this.xArrj = []
this.xArrjF = []
this.xArrjVal = []
// if (!this.horizontal) {
// this.xArrj.push(x + barWidth / 2)
// }
// fix issue #1215;
// where all stack bar disappear after collapsing the first series
// sol: if only 1 arr in this.prevY(this.prevY.length === 1) and all are NaN
if (this.prevY.length === 1 && this.prevY[0].every((val) => isNaN(val))) {
// make this.prevY[0] all zeroH
this.prevY[0] = this.prevY[0].map((val) => zeroH)
// make this.prevYF[0] all 0
this.prevYF[0] = this.prevYF[0].map((val) => 0)
}
for (let j = 0; j < w.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
const commonPathOpts = {
indexes: { i, j, realIndex, bc },
strokeWidth,
x,
y,
elSeries
}
let paths = null
if (this.isHorizontal) {
paths = this.drawStackedBarPaths({
...commonPathOpts,
zeroW,
barHeight,
yDivision
})
barWidth = this.series[i][j] / this.invertedYRatio
} else {
paths = this.drawStackedColumnPaths({
...commonPathOpts,
xDivision,
barWidth,
zeroH
})
barHeight = this.series[i][j] / this.yRatio[this.yaxisIndex]
}
y = paths.y
x = paths.x
xArrValues.push(x)
yArrValues.push(y)
let pathFill = this.barHelpers.getPathFillColor(series, i, j, realIndex)
elSeries = this.renderSeries({
realIndex,
pathFill,
j,
i,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight,
barWidth,
elDataLabelsWrap,
type: 'bar',
visibleSeries: 0
})
}
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = xArrValues
w.globals.seriesYvalues[realIndex] = yArrValues
// push all current y values array to main PrevY Array
this.prevY.push(this.yArrj)
this.prevYF.push(this.yArrjF)
this.prevYVal.push(this.yArrjVal)
this.prevX.push(this.xArrj)
this.prevXF.push(this.xArrjF)
this.prevXVal.push(this.xArrjVal)
ret.add(elSeries)
}
return ret
}
initialPositions(x, y, xDivision, yDivision, zeroH, zeroW) {
let w = this.w
let barHeight, barWidth
if (this.isHorizontal) {
// height divided into equal parts
yDivision = w.globals.gridHeight / w.globals.dataPoints
barHeight = yDivision
barHeight =
(barHeight * parseInt(w.config.plotOptions.bar.barHeight, 10)) / 100
zeroW =
this.baseLineInvertedY +
w.globals.padHorizontal +
(this.isReversed ? w.globals.gridWidth : 0) -
(this.isReversed ? this.baseLineInvertedY * 2 : 0)
// initial y position is half of barHeight * half of number of Bars
y = (yDivision - barHeight) / 2
} else {
// width divided into equal parts
xDivision = w.globals.gridWidth / w.globals.dataPoints
barWidth = xDivision
if (w.globals.isXNumeric && w.globals.dataPoints > 1) {
// the check (w.globals.dataPoints > 1) fixes apexcharts.js #1617
xDivision = w.globals.minXDiff / this.xRatio
barWidth = (xDivision * parseInt(this.barOptions.columnWidth, 10)) / 100
} else {
barWidth =
(barWidth * parseInt(w.config.plotOptions.bar.columnWidth, 10)) / 100
}
zeroH =
this.baseLineY[this.yaxisIndex] +
(this.isReversed ? w.globals.gridHeight : 0) -
(this.isReversed ? this.baseLineY[this.yaxisIndex] * 2 : 0)
// initial x position is one third of barWidth
x = w.globals.padHorizontal + (xDivision - barWidth) / 2
}
return {
x,
y,
yDivision,
xDivision,
barHeight,
barWidth,
zeroH,
zeroW
}
}
drawStackedBarPaths({
indexes,
barHeight,
strokeWidth,
zeroW,
x,
y,
yDivision,
elSeries
}) {
let w = this.w
let barYPosition = y
let barXPosition
let i = indexes.i
let j = indexes.j
let prevBarW = 0
for (let k = 0; k < this.prevXF.length; k++) {
prevBarW = prevBarW + this.prevXF[k][j]
}
if (i > 0) {
let bXP = zeroW
if (this.prevXVal[i - 1][j] < 0) {
bXP =
this.series[i][j] >= 0
? this.prevX[i - 1][j] +
prevBarW -
(this.isReversed ? prevBarW : 0) * 2
: this.prevX[i - 1][j]
} else if (this.prevXVal[i - 1][j] >= 0) {
bXP =
this.series[i][j] >= 0
? this.prevX[i - 1][j]
: this.prevX[i - 1][j] -
prevBarW +
(this.isReversed ? prevBarW : 0) * 2
}
barXPosition = bXP
} else {
// the first series will not have prevX values
barXPosition = zeroW
}
if (this.series[i][j] === null) {
x = barXPosition
} else {
x =
barXPosition +
this.series[i][j] / this.invertedYRatio -
(this.isReversed ? this.series[i][j] / this.invertedYRatio : 0) * 2
}
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1: barXPosition,
x2: x,
strokeWidth,
series: this.series,
realIndex: indexes.realIndex,
i,
j,
w
})
this.barHelpers.barBackground({
j,
i,
y1: barYPosition,
y2: barHeight,
elSeries
})
y = y + yDivision
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x,
y
}
}
drawStackedColumnPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
strokeWidth,
elSeries
}) {
let w = this.w
let i = indexes.i
let j = indexes.j
let bc = indexes.bc
if (w.globals.isXNumeric) {
let seriesVal = w.globals.seriesX[i][j]
if (!seriesVal) seriesVal = 0
x = (seriesVal - w.globals.minX) / this.xRatio - barWidth / 2
}
let barXPosition = x
let barYPosition
let prevBarH = 0
for (let k = 0; k < this.prevYF.length; k++) {
// fix issue #1215
// in case where this.prevYF[k][j] is NaN, use 0 instead
prevBarH = prevBarH + (!isNaN(this.prevYF[k][j]) ? this.prevYF[k][j] : 0)
}
if (
(i > 0 && !w.globals.isXNumeric) ||
(i > 0 &&
w.globals.isXNumeric &&
w.globals.seriesX[i - 1][j] === w.globals.seriesX[i][j])
) {
let bYP
let prevYValue
const p = Math.min(this.yRatio.length + 1, i + 1)
if (this.prevY[i - 1] !== undefined) {
for (let ii = 1; ii < p; ii++) {
if (!isNaN(this.prevY[i - ii][j])) {
// find the previous available value to give prevYValue
prevYValue = this.prevY[i - ii][j]
// if found it, break the loop
break
}
}
}
for (let ii = 1; ii < p; ii++) {
// find the previous available value(non-NaN) to give bYP
if (this.prevYVal[i - ii][j] < 0) {
bYP =
this.series[i][j] >= 0
? prevYValue - prevBarH + (this.isReversed ? prevBarH : 0) * 2
: prevYValue
// found it? break the loop
break
} else if (this.prevYVal[i - ii][j] >= 0) {
bYP =
this.series[i][j] >= 0
? prevYValue
: prevYValue + prevBarH - (this.isReversed ? prevBarH : 0) * 2
// found it? break the loop
break
}
}
if (typeof bYP === 'undefined') bYP = w.globals.gridHeight
// if this.prevYF[0] is all 0 resulted from line #486
// AND every arr starting from the second only contains NaN
if (
this.prevYF[0].every((val) => val === 0) &&
this.prevYF.slice(1, i).every((arr) => arr.every((val) => isNaN(val)))
) {
// Use the same calc way as line #485
barYPosition = w.globals.gridHeight - zeroH
} else {
// Nothing special
barYPosition = bYP
}
} else {
// the first series will not have prevY values, also if the prev index's series X doesn't matches the current index's series X, then start from zero
barYPosition = w.globals.gridHeight - zeroH
}
y =
barYPosition -
this.series[i][j] / this.yRatio[this.yaxisIndex] +
(this.isReversed ? this.series[i][j] / this.yRatio[this.yaxisIndex] : 0) *
2
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1: barYPosition,
y2: y,
yRatio: this.yRatio[this.yaxisIndex],
strokeWidth: this.strokeWidth,
series: this.series,
realIndex: indexes.realIndex,
i,
j,
w
})
this.barHelpers.barBackground({
bc,
j,
i,
x1: barXPosition,
x2: barWidth,
elSeries
})
x = x + xDivision
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
x: w.globals.isXNumeric ? x - xDivision : x,
y
}
}
}
export default BarStacked
@@ -0,0 +1,428 @@
import CoreUtils from '../modules/CoreUtils'
import Bar from './Bar'
import Fill from '../modules/Fill'
import Graphics from '../modules/Graphics'
import Utils from '../utils/Utils'
/**
* ApexCharts BoxCandleStick Class responsible for drawing both Stacked Columns and Bars.
*
* @module BoxCandleStick
**/
class BoxCandleStick extends Bar {
draw(series, seriesIndex) {
let w = this.w
let graphics = new Graphics(this.ctx)
let fill = new Fill(this.ctx)
this.candlestickOptions = this.w.config.plotOptions.candlestick
this.boxOptions = this.w.config.plotOptions.boxPlot
this.isHorizontal = w.config.plotOptions.bar.horizontal
const coreUtils = new CoreUtils(this.ctx, w)
series = coreUtils.getLogSeries(series)
this.series = series
this.yRatio = coreUtils.getLogYRatios(this.yRatio)
this.barHelpers.initVariables(series)
let ret = graphics.group({
class: `apexcharts-${w.config.chart.type}-series apexcharts-plot-series`
})
for (let i = 0; i < series.length; i++) {
this.isBoxPlot =
w.config.chart.type === 'boxPlot' ||
w.config.series[i].type === 'boxPlot'
let x,
y,
xDivision, // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
yDivision, // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroH, // zeroH is the baseline where 0 meets y axis
zeroW // zeroW is the baseline where 0 meets x axis
let yArrj = [] // hold y values of current iterating series
let xArrj = [] // hold x values of current iterating series
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
rel: i + 1,
'data:realIndex': realIndex
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1
}
let barHeight = 0
let barWidth = 0
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
let initPositions = this.barHelpers.initialPositions()
y = initPositions.y
barHeight = initPositions.barHeight
yDivision = initPositions.yDivision
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
xArrj.push(x + barWidth / 2)
// eldatalabels
let elDataLabelsWrap = graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex
})
for (let j = 0; j < w.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
let paths = null
const pathsParams = {
indexes: {
i,
j,
realIndex
},
x,
y,
strokeWidth,
elSeries
}
if (this.isHorizontal) {
paths = this.drawHorizontalBoxPaths({
...pathsParams,
yDivision,
barHeight,
zeroW
})
} else {
paths = this.drawVerticalBoxPaths({
...pathsParams,
xDivision,
barWidth,
zeroH
})
}
y = paths.y
x = paths.x
// push current X
if (j > 0) {
xArrj.push(x + barWidth / 2)
}
yArrj.push(y)
paths.pathTo.forEach((pathTo, pi) => {
let lineFill =
!this.isBoxPlot && this.candlestickOptions.wick.useFillColor
? paths.color[pi]
: w.globals.stroke.colors[i]
let pathFill = fill.fillPath({
seriesNumber: realIndex,
dataPointIndex: j,
color: paths.color[pi],
value: series[i][j]
})
this.renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
pathFrom: paths.pathFrom,
pathTo,
strokeWidth,
elSeries,
x,
y,
series,
barHeight,
barWidth,
elDataLabelsWrap,
visibleSeries: this.visibleI,
type: w.config.chart.type
})
})
}
// push all x val arrays into main xArr
w.globals.seriesXvalues[realIndex] = xArrj
w.globals.seriesYvalues[realIndex] = yArrj
ret.add(elSeries)
}
return ret
}
drawVerticalBoxPaths({
indexes,
x,
y,
xDivision,
barWidth,
zeroH,
strokeWidth
}) {
let w = this.w
let graphics = new Graphics(this.ctx)
let i = indexes.i
let j = indexes.j
let isPositive = true
let colorPos = w.config.plotOptions.candlestick.colors.upward
let colorNeg = w.config.plotOptions.candlestick.colors.downward
let color = ''
if (this.isBoxPlot) {
color = [this.boxOptions.colors.lower, this.boxOptions.colors.upper]
}
const yRatio = this.yRatio[this.yaxisIndex]
let realIndex = indexes.realIndex
const ohlc = this.getOHLCValue(realIndex, j)
let l1 = zeroH
let l2 = zeroH
if (ohlc.o > ohlc.c) {
isPositive = false
}
let y1 = Math.min(ohlc.o, ohlc.c)
let y2 = Math.max(ohlc.o, ohlc.c)
let m = ohlc.m
if (w.globals.isXNumeric) {
x =
(w.globals.seriesX[realIndex][j] - w.globals.minX) / this.xRatio -
barWidth / 2
}
let barXPosition = x + barWidth * this.visibleI
if (
typeof this.series[i][j] === 'undefined' ||
this.series[i][j] === null
) {
y1 = zeroH
y2 = zeroH
} else {
y1 = zeroH - y1 / yRatio
y2 = zeroH - y2 / yRatio
l1 = zeroH - ohlc.h / yRatio
l2 = zeroH - ohlc.l / yRatio
m = zeroH - ohlc.m / yRatio
}
let pathTo = graphics.move(barXPosition, zeroH)
let pathFrom = graphics.move(barXPosition + barWidth / 2, y1)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, true)
}
if (this.isBoxPlot) {
pathTo = [
graphics.move(barXPosition, y1) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition + barWidth / 2, l1) +
graphics.line(barXPosition + barWidth / 4, l1) +
graphics.line(barXPosition + barWidth - barWidth / 4, l1) +
graphics.line(barXPosition + barWidth / 2, l1) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition + barWidth, y1) +
graphics.line(barXPosition + barWidth, m) +
graphics.line(barXPosition, m) +
graphics.line(barXPosition, y1 + strokeWidth / 2),
graphics.move(barXPosition, m) +
graphics.line(barXPosition + barWidth, m) +
graphics.line(barXPosition + barWidth, y2) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition + barWidth / 2, l2) +
graphics.line(barXPosition + barWidth - barWidth / 4, l2) +
graphics.line(barXPosition + barWidth / 4, l2) +
graphics.line(barXPosition + barWidth / 2, l2) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition, y2) +
graphics.line(barXPosition, m) +
'z'
]
} else {
// candlestick
pathTo = [
graphics.move(barXPosition, y2) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition + barWidth / 2, l1) +
graphics.line(barXPosition + barWidth / 2, y2) +
graphics.line(barXPosition + barWidth, y2) +
graphics.line(barXPosition + barWidth, y1) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition + barWidth / 2, l2) +
graphics.line(barXPosition + barWidth / 2, y1) +
graphics.line(barXPosition, y1) +
graphics.line(barXPosition, y2 - strokeWidth / 2)
]
}
pathFrom = pathFrom + graphics.move(barXPosition, y1)
if (!w.globals.isXNumeric) {
x = x + xDivision
}
return {
pathTo,
pathFrom,
x,
y: y2,
barXPosition,
color: this.isBoxPlot ? color : isPositive ? [colorPos] : [colorNeg]
}
}
drawHorizontalBoxPaths({
indexes,
x,
y,
yDivision,
barHeight,
zeroW,
strokeWidth
}) {
let w = this.w
let graphics = new Graphics(this.ctx)
let i = indexes.i
let j = indexes.j
let color = this.boxOptions.colors.lower
if (this.isBoxPlot) {
color = [this.boxOptions.colors.lower, this.boxOptions.colors.upper]
}
const yRatio = this.invertedYRatio
let realIndex = indexes.realIndex
const ohlc = this.getOHLCValue(realIndex, j)
let l1 = zeroW
let l2 = zeroW
let x1 = Math.min(ohlc.o, ohlc.c)
let x2 = Math.max(ohlc.o, ohlc.c)
let m = ohlc.m
if (w.globals.isXNumeric) {
y =
(w.globals.seriesX[realIndex][j] - w.globals.minX) /
this.invertedXRatio -
barHeight / 2
}
let barYPosition = y + barHeight * this.visibleI
if (
typeof this.series[i][j] === 'undefined' ||
this.series[i][j] === null
) {
x1 = zeroW
x2 = zeroW
} else {
x1 = zeroW + x1 / yRatio
x2 = zeroW + x2 / yRatio
l1 = zeroW + ohlc.h / yRatio
l2 = zeroW + ohlc.l / yRatio
m = zeroW + ohlc.m / yRatio
}
let pathTo = graphics.move(zeroW, barYPosition)
let pathFrom = graphics.move(x1, barYPosition + barHeight / 2)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(realIndex, j, true)
}
pathTo = [
graphics.move(x1, barYPosition) +
graphics.line(x1, barYPosition + barHeight / 2) +
graphics.line(l1, barYPosition + barHeight / 2) +
graphics.line(l1, barYPosition + barHeight / 2 - barHeight / 4) +
graphics.line(l1, barYPosition + barHeight / 2 + barHeight / 4) +
graphics.line(l1, barYPosition + barHeight / 2) +
graphics.line(x1, barYPosition + barHeight / 2) +
graphics.line(x1, barYPosition + barHeight) +
graphics.line(m, barYPosition + barHeight) +
graphics.line(m, barYPosition) +
graphics.line(x1 + strokeWidth / 2, barYPosition),
graphics.move(m, barYPosition) +
graphics.line(m, barYPosition + barHeight) +
graphics.line(x2, barYPosition + barHeight) +
graphics.line(x2, barYPosition + barHeight / 2) +
graphics.line(l2, barYPosition + barHeight / 2) +
graphics.line(l2, barYPosition + barHeight - barHeight / 4) +
graphics.line(l2, barYPosition + barHeight / 4) +
graphics.line(l2, barYPosition + barHeight / 2) +
graphics.line(x2, barYPosition + barHeight / 2) +
graphics.line(x2, barYPosition) +
graphics.line(m, barYPosition) +
'z'
]
pathFrom = pathFrom + graphics.move(x1, barYPosition)
if (!w.globals.isXNumeric) {
y = y + yDivision
}
return {
pathTo,
pathFrom,
x: x2,
y,
barYPosition,
color
}
}
getOHLCValue(i, j) {
const w = this.w
return {
o: this.isBoxPlot
? w.globals.seriesCandleH[i][j]
: w.globals.seriesCandleO[i][j],
h: this.isBoxPlot
? w.globals.seriesCandleO[i][j]
: w.globals.seriesCandleH[i][j],
m: w.globals.seriesCandleM[i][j],
l: this.isBoxPlot
? w.globals.seriesCandleC[i][j]
: w.globals.seriesCandleL[i][j],
c: this.isBoxPlot
? w.globals.seriesCandleL[i][j]
: w.globals.seriesCandleC[i][j]
}
}
}
export default BoxCandleStick
@@ -0,0 +1,241 @@
import Animations from '../modules/Animations'
import Graphics from '../modules/Graphics'
import Fill from '../modules/Fill'
import Utils from '../utils/Utils'
import Helpers from './common/treemap/Helpers'
import Filters from '../modules/Filters'
/**
* ApexCharts HeatMap Class.
* @module HeatMap
**/
export default class HeatMap {
constructor(ctx, xyRatios) {
this.ctx = ctx
this.w = ctx.w
this.xRatio = xyRatios.xRatio
this.yRatio = xyRatios.yRatio
this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation
this.helpers = new Helpers(ctx)
this.rectRadius = this.w.config.plotOptions.heatmap.radius
this.strokeWidth = this.w.config.stroke.show
? this.w.config.stroke.width
: 0
}
draw(series) {
let w = this.w
const graphics = new Graphics(this.ctx)
let ret = graphics.group({
class: 'apexcharts-heatmap'
})
ret.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
// width divided into equal parts
let xDivision = w.globals.gridWidth / w.globals.dataPoints
let yDivision = w.globals.gridHeight / w.globals.series.length
let y1 = 0
let rev = false
this.negRange = this.helpers.checkColorRange()
let heatSeries = series.slice()
if (w.config.yaxis[0].reversed) {
rev = true
heatSeries.reverse()
}
for (
let i = rev ? 0 : heatSeries.length - 1;
rev ? i < heatSeries.length : i >= 0;
rev ? i++ : i--
) {
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series apexcharts-heatmap-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[i]),
rel: i + 1,
'data:realIndex': i
})
this.ctx.series.addCollapsedClassToSeries(elSeries, i)
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow
const filters = new Filters(this.ctx)
filters.dropShadow(elSeries, shadow, i)
}
let x1 = 0
let shadeIntensity = w.config.plotOptions.heatmap.shadeIntensity
for (let j = 0; j < heatSeries[i].length; j++) {
let heatColor = this.helpers.getShadeColor(
w.config.chart.type,
i,
j,
this.negRange
)
let color = heatColor.color
let heatColorProps = heatColor.colorProps
if (w.config.fill.type === 'image') {
const fill = new Fill(this.ctx)
color = fill.fillPath({
seriesNumber: i,
dataPointIndex: j,
opacity: w.globals.hasNegs
? heatColorProps.percent < 0
? 1 - (1 + heatColorProps.percent / 100)
: shadeIntensity + heatColorProps.percent / 100
: heatColorProps.percent / 100,
patternID: Utils.randomId(),
width: w.config.fill.image.width
? w.config.fill.image.width
: xDivision,
height: w.config.fill.image.height
? w.config.fill.image.height
: yDivision
})
}
let radius = this.rectRadius
let rect = graphics.drawRect(x1, y1, xDivision, yDivision, radius)
rect.attr({
cx: x1,
cy: y1
})
rect.node.classList.add('apexcharts-heatmap-rect')
elSeries.add(rect)
rect.attr({
fill: color,
i,
index: i,
j,
val: heatSeries[i][j],
'stroke-width': this.strokeWidth,
stroke: w.config.plotOptions.heatmap.useFillColorAsStroke
? color
: w.globals.stroke.colors[0],
color
})
this.helpers.addListeners(rect)
if (w.config.chart.animations.enabled && !w.globals.dataChanged) {
let speed = 1
if (!w.globals.resized) {
speed = w.config.chart.animations.speed
}
this.animateHeatMap(rect, x1, y1, xDivision, yDivision, speed)
}
if (w.globals.dataChanged) {
let speed = 1
if (this.dynamicAnim.enabled && w.globals.shouldAnimate) {
speed = this.dynamicAnim.speed
let colorFrom =
w.globals.previousPaths[i] &&
w.globals.previousPaths[i][j] &&
w.globals.previousPaths[i][j].color
if (!colorFrom) colorFrom = 'rgba(255, 255, 255, 0)'
this.animateHeatColor(
rect,
Utils.isColorHex(colorFrom)
? colorFrom
: Utils.rgb2hex(colorFrom),
Utils.isColorHex(color) ? color : Utils.rgb2hex(color),
speed
)
}
}
let formatter = w.config.dataLabels.formatter
let formattedText = formatter(w.globals.series[i][j], {
value: w.globals.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w
})
let dataLabels = this.helpers.calculateDataLabels({
text: formattedText,
x: x1 + xDivision / 2,
y: y1 + yDivision / 2,
i,
j,
colorProps: heatColorProps,
series: heatSeries
})
if (dataLabels !== null) {
elSeries.add(dataLabels)
}
x1 = x1 + xDivision
}
y1 = y1 + yDivision
ret.add(elSeries)
}
// adjust yaxis labels for heatmap
let yAxisScale = w.globals.yAxisScale[0].result.slice()
if (w.config.yaxis[0].reversed) {
yAxisScale.unshift('')
} else {
yAxisScale.push('')
}
w.globals.yAxisScale[0].result = yAxisScale
let divisor = w.globals.gridHeight / w.globals.series.length
w.config.yaxis[0].labels.offsetY = -(divisor / 2)
return ret
}
animateHeatMap(el, x, y, width, height, speed) {
const animations = new Animations(this.ctx)
animations.animateRect(
el,
{
x: x + width / 2,
y: y + height / 2,
width: 0,
height: 0
},
{
x,
y,
width,
height
},
speed,
() => {
animations.animationCompleted(el)
}
)
}
animateHeatColor(el, colorFrom, colorTo, speed) {
el.attr({
fill: colorFrom
})
.animate(speed)
.attr({
fill: colorTo
})
}
}
@@ -0,0 +1,728 @@
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'
/**
* 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.lineHelpers = new Helpers(this)
this.markers = new Markers(this.ctx)
this.prevSeriesY = []
this.categoryAxisCorrection = 0
this.yaxisIndex = 0
}
draw(series, ptype, seriesIndex) {
let w = this.w
let graphics = new Graphics(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)
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)
// 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
this._initSerieVariables(series, i, realIndex)
let yArrj = [] // hold y values of current iterating series
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 prevX = pX
let prevY = this.zeroY
let lineYPosition = 0
// the first value in the current series is not null or undefined
let firstPrevY = this.lineHelpers.determineFirstPrevY({
i,
series,
prevY,
lineYPosition
})
prevY = firstPrevY.prevY
yArrj.push(prevY)
pY = prevY
let pathsFrom = this._calculatePathsFrom({
series,
i,
realIndex,
prevX,
prevY
})
let paths = this._iterateOverDataPoints({
series,
realIndex,
i,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
seriesIndex,
lineYPosition,
xArrj,
yArrj
})
this._handlePaths({ type, realIndex, i, paths })
this.elSeries.add(this.elPointsMain)
this.elSeries.add(this.elDataLabelsWrap)
allSeries.push(this.elSeries)
}
if (w.config.chart.stacked) {
for (let s = allSeries.length; s > 0; s--) {
ret.add(allSeries[s - 1])
}
} 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
if (this.yRatio.length > 1) {
this.yaxisIndex = 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[this.yaxisIndex] -
(this.isReversed ? w.globals.gridHeight : 0) +
(this.isReversed ? this.baseLineY[this.yaxisIndex] * 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`,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex])
})
// points
this.elPointsMain = graphics.group({
class: 'apexcharts-series-markers-wrap',
'data:realIndex': realIndex
})
// 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({ series, i, realIndex, prevX, prevY }) {
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[this.yaxisIndex]
linePath = graphics.move(prevX, prevY)
areaPath = graphics.move(prevX, this.areaBottomY)
break
}
}
} else {
linePath = graphics.move(prevX, prevY)
areaPath =
graphics.move(prevX, this.areaBottomY) + graphics.line(prevX, prevY)
}
pathFromLine = graphics.move(-1, this.zeroY) + graphics.line(-1, this.zeroY)
pathFromArea = graphics.move(-1, this.zeroY) + graphics.line(-1, this.zeroY)
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) {
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
}
}
for (let p = 0; p < paths.linePaths.length; p++) {
const linePathCommonOpts = {
...defaultRenderedPathOptions,
pathFrom: paths.pathFromLine,
pathTo: paths.linePaths[p],
stroke: lineFill,
strokeWidth: this.strokeWidth,
strokeLineCap: w.config.stroke.lineCap,
fill: 'none'
}
let renderedPath = graphics.renderPaths(linePathCommonOpts)
this.elSeries.add(renderedPath)
if (forecast.count > 0) {
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({
series,
realIndex,
i,
x,
y,
pX,
pY,
pathsFrom,
linePaths,
areaPaths,
seriesIndex,
lineYPosition,
xArrj,
yArrj
}) {
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
const iterations =
w.globals.dataPoints > 1 ? w.globals.dataPoints - 1 : w.globals.dataPoints
for (let j = 0; j < iterations; j++) {
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 (w.config.chart.stacked) {
if (
i > 0 &&
w.globals.collapsedSeries.length < w.config.series.length - 1
) {
// a collapsed series in a stacked bar 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) => {
let pii = pi
for (let cpi = 0; cpi < w.globals.series.length; cpi++) {
if (w.globals.collapsedSeriesIndices.indexOf(pi) > -1) {
pii--
break
}
}
return pii >= 0 ? pii : 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 =
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
}
// push current X
xArrj.push(x)
// push current Y that will be used as next series's bottom position
yArrj.push(y)
let pointsPos = this.lineHelpers.calculatePoints({
series,
x,
y,
realIndex,
i,
j,
prevY
})
let calculatedPaths = this._createPaths({
series,
i,
realIndex,
j,
x,
y,
pX,
pY,
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, this.zeroY)
pathFromArea = pathFromArea + graphics.line(x, this.zeroY)
}
this.handleNullDataPoints(series, pointsPos, i, j, realIndex)
this._handleMarkersAndLabels({
pointsPos,
series,
x,
y,
prevY,
i,
j,
realIndex
})
}
return {
yArrj,
xArrj,
pathFromArea,
areaPaths,
pathFromLine,
linePaths
}
}
_handleMarkersAndLabels({ pointsPos, series, x, y, prevY, 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,
realIndex,
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(
pointsPos,
realIndex,
j + 1,
null
)
if (drawnLabels !== null) {
this.elDataLabelsWrap.add(drawnLabels)
}
}
_createPaths({
series,
i,
realIndex,
j,
x,
y,
pX,
pY,
linePath,
areaPath,
linePaths,
areaPaths,
seriesIndex
}) {
let w = this.w
let graphics = new Graphics(this.ctx)
let curve = w.config.stroke.curve
const areaBottomY = this.areaBottomY
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)
const numericOrCatX = w.globals.isXNumeric
? (w.globals.seriesX[realIndex][j] - w.globals.minX) / this.xRatio
: x - this.xDivision
areaPath =
areaPath +
graphics.line(numericOrCatX, areaBottomY) +
graphics.move(x, y) +
'z'
}
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
}
}
handleNullDataPoints(series, pointsPos, i, j, realIndex) {
const w = this.w
if (
(series[i][j] === null && w.config.markers.showNullDataPoints) ||
series[i].length === 1
) {
// fixes apexcharts.js#1282, #1252
let elPointsWrap = this.markers.plotChartMarkers(
pointsPos,
realIndex,
j + 1,
this.strokeWidth - w.config.markers.strokeWidth / 2,
true
)
if (elPointsWrap !== null) {
this.elPointsMain.add(elPointsWrap)
}
}
}
}
export default Line
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,524 @@
import Fill from '../modules/Fill'
import Graphics from '../modules/Graphics'
import Markers from '../modules/Markers'
import DataLabels from '../modules/DataLabels'
import Filters from '../modules/Filters'
import Utils from '../utils/Utils'
import Helpers from './common/circle/Helpers'
import CoreUtils from '../modules/CoreUtils'
/**
* ApexCharts Radar Class for Spider/Radar Charts.
* @module Radar
**/
class Radar {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.chartType = this.w.config.chart.type
this.initialAnim = this.w.config.chart.animations.enabled
this.dynamicAnim =
this.initialAnim &&
this.w.config.chart.animations.dynamicAnimation.enabled
this.animDur = 0
const w = this.w
this.graphics = new Graphics(this.ctx)
this.lineColorArr =
w.globals.stroke.colors !== undefined
? w.globals.stroke.colors
: w.globals.colors
this.defaultSize =
w.globals.svgHeight < w.globals.svgWidth
? w.globals.gridHeight + w.globals.goldenPadding * 1.5
: w.globals.gridWidth
this.isLog = w.config.yaxis[0].logarithmic
this.coreUtils = new CoreUtils(this.ctx)
this.maxValue = this.isLog
? this.coreUtils.getLogVal(w.globals.maxY, 0)
: w.globals.maxY
this.minValue = this.isLog
? this.coreUtils.getLogVal(this.w.globals.minY, 0)
: w.globals.minY
this.polygons = w.config.plotOptions.radar.polygons
this.strokeWidth = w.config.stroke.show ? w.config.stroke.width : 0
this.size =
this.defaultSize / 2.1 - this.strokeWidth - w.config.chart.dropShadow.blur
if (w.config.xaxis.labels.show) {
this.size = this.size - w.globals.xAxisLabelsWidth / 1.75
}
if (w.config.plotOptions.radar.size !== undefined) {
this.size = w.config.plotOptions.radar.size
}
this.dataRadiusOfPercent = []
this.dataRadius = []
this.angleArr = []
this.yaxisLabelsTextsPos = []
}
draw(series) {
let w = this.w
const fill = new Fill(this.ctx)
const allSeries = []
const dataLabels = new DataLabels(this.ctx)
if (series.length) {
this.dataPointsLen = series[w.globals.maxValsInArrayIndex].length
}
this.disAngle = (Math.PI * 2) / this.dataPointsLen
let halfW = w.globals.gridWidth / 2
let halfH = w.globals.gridHeight / 2
let translateX = halfW + w.config.plotOptions.radar.offsetX
let translateY = halfH + w.config.plotOptions.radar.offsetY
let ret = this.graphics.group({
class: 'apexcharts-radar-series apexcharts-plot-series',
transform: `translate(${translateX || 0}, ${translateY || 0})`
})
let dataPointsPos = []
let elPointsMain = null
let elDataPointsMain = null
this.yaxisLabels = this.graphics.group({
class: 'apexcharts-yaxis'
})
series.forEach((s, i) => {
let longestSeries = s.length === w.globals.dataPoints
// el to which series will be drawn
let elSeries = this.graphics.group().attr({
class: `apexcharts-series`,
'data:longestSeries': longestSeries,
seriesName: Utils.escapeString(w.globals.seriesNames[i]),
rel: i + 1,
'data:realIndex': i
})
this.dataRadiusOfPercent[i] = []
this.dataRadius[i] = []
this.angleArr[i] = []
s.forEach((dv, j) => {
const range = Math.abs(this.maxValue - this.minValue)
dv = dv + Math.abs(this.minValue)
if (this.isLog) {
dv = this.coreUtils.getLogVal(dv, 0)
}
this.dataRadiusOfPercent[i][j] = dv / range
this.dataRadius[i][j] = this.dataRadiusOfPercent[i][j] * this.size
this.angleArr[i][j] = j * this.disAngle
})
dataPointsPos = this.getDataPointsPos(
this.dataRadius[i],
this.angleArr[i]
)
const paths = this.createPaths(dataPointsPos, {
x: 0,
y: 0
})
// points
elPointsMain = this.graphics.group({
class: 'apexcharts-series-markers-wrap apexcharts-element-hidden'
})
// datapoints
elDataPointsMain = this.graphics.group({
class: `apexcharts-datalabels`,
'data:realIndex': i
})
w.globals.delayedElements.push({
el: elPointsMain.node,
index: i
})
const defaultRenderedPathOptions = {
i,
realIndex: i,
animationDelay: i,
initialSpeed: w.config.chart.animations.speed,
dataChangeSpeed: w.config.chart.animations.dynamicAnimation.speed,
className: `apexcharts-radar`,
shouldClipToGrid: false,
bindEventsOnPaths: false,
stroke: w.globals.stroke.colors[i],
strokeLineCap: w.config.stroke.lineCap
}
let pathFrom = null
if (w.globals.previousPaths.length > 0) {
pathFrom = this.getPreviousPath(i)
}
for (let p = 0; p < paths.linePathsTo.length; p++) {
let renderedLinePath = this.graphics.renderPaths({
...defaultRenderedPathOptions,
pathFrom: pathFrom === null ? paths.linePathsFrom[p] : pathFrom,
pathTo: paths.linePathsTo[p],
strokeWidth: Array.isArray(this.strokeWidth)
? this.strokeWidth[i]
: this.strokeWidth,
fill: 'none',
drawShadow: false
})
elSeries.add(renderedLinePath)
let pathFill = fill.fillPath({
seriesNumber: i
})
let renderedAreaPath = this.graphics.renderPaths({
...defaultRenderedPathOptions,
pathFrom: pathFrom === null ? paths.areaPathsFrom[p] : pathFrom,
pathTo: paths.areaPathsTo[p],
strokeWidth: 0,
fill: pathFill,
drawShadow: false
})
if (w.config.chart.dropShadow.enabled) {
const filters = new Filters(this.ctx)
const shadow = w.config.chart.dropShadow
filters.dropShadow(
renderedAreaPath,
Object.assign({}, shadow, { noUserSpaceOnUse: true }),
i
)
}
elSeries.add(renderedAreaPath)
}
s.forEach((sj, j) => {
let markers = new Markers(this.ctx)
let opts = markers.getMarkerConfig({
cssClass: 'apexcharts-marker',
seriesIndex: i,
dataPointIndex: j
})
let point = this.graphics.drawMarker(
dataPointsPos[j].x,
dataPointsPos[j].y,
opts
)
point.attr('rel', j)
point.attr('j', j)
point.attr('index', i)
point.node.setAttribute('default-marker-size', opts.pSize)
let elPointsWrap = this.graphics.group({
class: 'apexcharts-series-markers'
})
if (elPointsWrap) {
elPointsWrap.add(point)
}
elPointsMain.add(elPointsWrap)
elSeries.add(elPointsMain)
const dataLabelsConfig = w.config.dataLabels
if (dataLabelsConfig.enabled) {
let text = dataLabelsConfig.formatter(w.globals.series[i][j], {
seriesIndex: i,
dataPointIndex: j,
w
})
dataLabels.plotDataLabelsText({
x: dataPointsPos[j].x,
y: dataPointsPos[j].y,
text,
textAnchor: 'middle',
i,
j: i,
parent: elDataPointsMain,
offsetCorrection: false,
dataLabelsConfig: {
...dataLabelsConfig
}
})
}
elSeries.add(elDataPointsMain)
})
allSeries.push(elSeries)
})
this.drawPolygons({
parent: ret
})
if (w.config.xaxis.labels.show) {
const xaxisTexts = this.drawXAxisTexts()
ret.add(xaxisTexts)
}
allSeries.forEach((elS) => {
ret.add(elS)
})
ret.add(this.yaxisLabels)
return ret
}
drawPolygons(opts) {
const w = this.w
const { parent } = opts
const helpers = new Helpers(this.ctx)
const yaxisTexts = w.globals.yAxisScale[0].result.reverse()
const layers = yaxisTexts.length
let radiusSizes = []
let layerDis = this.size / (layers - 1)
for (let i = 0; i < layers; i++) {
radiusSizes[i] = layerDis * i
}
radiusSizes.reverse()
let polygonStrings = []
let lines = []
radiusSizes.forEach((radiusSize, r) => {
const polygon = Utils.getPolygonPos(radiusSize, this.dataPointsLen)
let string = ''
polygon.forEach((p, i) => {
if (r === 0) {
const line = this.graphics.drawLine(
p.x,
p.y,
0,
0,
Array.isArray(this.polygons.connectorColors)
? this.polygons.connectorColors[i]
: this.polygons.connectorColors
)
lines.push(line)
}
if (i === 0) {
this.yaxisLabelsTextsPos.push({
x: p.x,
y: p.y
})
}
string += p.x + ',' + p.y + ' '
})
polygonStrings.push(string)
})
polygonStrings.forEach((p, i) => {
const strokeColors = this.polygons.strokeColors
const strokeWidth = this.polygons.strokeWidth
const polygon = this.graphics.drawPolygon(
p,
Array.isArray(strokeColors) ? strokeColors[i] : strokeColors,
Array.isArray(strokeWidth) ? strokeWidth[i] : strokeWidth,
w.globals.radarPolygons.fill.colors[i]
)
parent.add(polygon)
})
lines.forEach((l) => {
parent.add(l)
})
if (w.config.yaxis[0].show) {
this.yaxisLabelsTextsPos.forEach((p, i) => {
const yText = helpers.drawYAxisTexts(p.x, p.y, i, yaxisTexts[i])
this.yaxisLabels.add(yText)
})
}
}
drawXAxisTexts() {
const w = this.w
const xaxisLabelsConfig = w.config.xaxis.labels
let elXAxisWrap = this.graphics.group({
class: 'apexcharts-xaxis'
})
let polygonPos = Utils.getPolygonPos(this.size, this.dataPointsLen)
w.globals.labels.forEach((label, i) => {
let formatter = w.config.xaxis.labels.formatter
let dataLabels = new DataLabels(this.ctx)
if (polygonPos[i]) {
let textPos = this.getTextPos(polygonPos[i], this.size)
let text = formatter(label, {
seriesIndex: -1,
dataPointIndex: i,
w
})
dataLabels.plotDataLabelsText({
x: textPos.newX,
y: textPos.newY,
text,
textAnchor: textPos.textAnchor,
i,
j: i,
parent: elXAxisWrap,
color:
Array.isArray(xaxisLabelsConfig.style.colors) &&
xaxisLabelsConfig.style.colors[i]
? xaxisLabelsConfig.style.colors[i]
: '#a8a8a8',
dataLabelsConfig: {
textAnchor: textPos.textAnchor,
dropShadow: { enabled: false },
...xaxisLabelsConfig
},
offsetCorrection: false
})
}
})
return elXAxisWrap
}
createPaths(pos, origin) {
let linePathsTo = []
let linePathsFrom = []
let areaPathsTo = []
let areaPathsFrom = []
if (pos.length) {
linePathsFrom = [this.graphics.move(origin.x, origin.y)]
areaPathsFrom = [this.graphics.move(origin.x, origin.y)]
let linePathTo = this.graphics.move(pos[0].x, pos[0].y)
let areaPathTo = this.graphics.move(pos[0].x, pos[0].y)
pos.forEach((p, i) => {
linePathTo += this.graphics.line(p.x, p.y)
areaPathTo += this.graphics.line(p.x, p.y)
if (i === pos.length - 1) {
linePathTo += 'Z'
areaPathTo += 'Z'
}
})
linePathsTo.push(linePathTo)
areaPathsTo.push(areaPathTo)
}
return {
linePathsFrom,
linePathsTo,
areaPathsFrom,
areaPathsTo
}
}
getTextPos(pos, polygonSize) {
let limit = 10
let textAnchor = 'middle'
let newX = pos.x
let newY = pos.y
if (Math.abs(pos.x) >= limit) {
if (pos.x > 0) {
textAnchor = 'start'
newX += 10
} else if (pos.x < 0) {
textAnchor = 'end'
newX -= 10
}
} else {
textAnchor = 'middle'
}
if (Math.abs(pos.y) >= polygonSize - limit) {
if (pos.y < 0) {
newY -= 10
} else if (pos.y > 0) {
newY += 10
}
}
return {
textAnchor,
newX,
newY
}
}
getPreviousPath(realIndex) {
let w = this.w
let pathFrom = null
for (let pp = 0; pp < w.globals.previousPaths.length; pp++) {
let gpp = w.globals.previousPaths[pp]
if (
gpp.paths.length > 0 &&
parseInt(gpp.realIndex, 10) === parseInt(realIndex, 10)
) {
if (typeof w.globals.previousPaths[pp].paths[0] !== 'undefined') {
pathFrom = w.globals.previousPaths[pp].paths[0].d
}
}
}
return pathFrom
}
getDataPointsPos(
dataRadiusArr,
angleArr,
dataPointsLen = this.dataPointsLen
) {
dataRadiusArr = dataRadiusArr || []
angleArr = angleArr || []
let dataPointsPosArray = []
for (let j = 0; j < dataPointsLen; j++) {
let curPointPos = {}
curPointPos.x = dataRadiusArr[j] * Math.sin(angleArr[j])
curPointPos.y = -dataRadiusArr[j] * Math.cos(angleArr[j])
dataPointsPosArray.push(curPointPos)
}
return dataPointsPosArray
}
}
export default Radar
@@ -0,0 +1,469 @@
import Pie from './Pie'
import Utils from '../utils/Utils'
import Fill from '../modules/Fill'
import Graphics from '../modules/Graphics'
import Filters from '../modules/Filters'
/**
* ApexCharts Radial Class for drawing Circle / Semi Circle Charts.
* @module Radial
**/
class Radial extends Pie {
constructor(ctx) {
super(ctx)
this.ctx = ctx
this.w = ctx.w
this.animBeginArr = [0]
this.animDur = 0
const w = this.w
this.startAngle = w.config.plotOptions.radialBar.startAngle
this.endAngle = w.config.plotOptions.radialBar.endAngle
this.totalAngle = Math.abs(
w.config.plotOptions.radialBar.endAngle -
w.config.plotOptions.radialBar.startAngle
)
this.trackStartAngle = w.config.plotOptions.radialBar.track.startAngle
this.trackEndAngle = w.config.plotOptions.radialBar.track.endAngle
this.donutDataLabels = this.w.config.plotOptions.radialBar.dataLabels
this.radialDataLabels = this.donutDataLabels // make a copy for easy reference
if (!this.trackStartAngle) this.trackStartAngle = this.startAngle
if (!this.trackEndAngle) this.trackEndAngle = this.endAngle
if (this.endAngle === 360) this.endAngle = 359.99
this.margin = parseInt(w.config.plotOptions.radialBar.track.margin, 10)
}
draw(series) {
let w = this.w
const graphics = new Graphics(this.ctx)
let ret = graphics.group({
class: 'apexcharts-radialbar'
})
if (w.globals.noData) return ret
let elSeries = graphics.group()
let centerY = this.defaultSize / 2
let centerX = w.globals.gridWidth / 2
let size = this.defaultSize / 2.05
if (!w.config.chart.sparkline.enabled) {
size = size - w.config.stroke.width - w.config.chart.dropShadow.blur
}
let colorArr = w.globals.fill.colors
if (w.config.plotOptions.radialBar.track.show) {
let elTracks = this.drawTracks({
size,
centerX,
centerY,
colorArr,
series
})
elSeries.add(elTracks)
}
let elG = this.drawArcs({
size,
centerX,
centerY,
colorArr,
series
})
let totalAngle = 360
if (w.config.plotOptions.radialBar.startAngle < 0) {
totalAngle = this.totalAngle
}
let angleRatio = (360 - totalAngle) / 360
w.globals.radialSize = size - size * angleRatio
if (this.radialDataLabels.value.show) {
let offset = Math.max(
this.radialDataLabels.value.offsetY,
this.radialDataLabels.name.offsetY
)
w.globals.radialSize += offset * angleRatio
}
elSeries.add(elG.g)
if (w.config.plotOptions.radialBar.hollow.position === 'front') {
elG.g.add(elG.elHollow)
if (elG.dataLabels) {
elG.g.add(elG.dataLabels)
}
}
ret.add(elSeries)
return ret
}
drawTracks(opts) {
let w = this.w
const graphics = new Graphics(this.ctx)
let g = graphics.group({
class: 'apexcharts-tracks'
})
let filters = new Filters(this.ctx)
let fill = new Fill(this.ctx)
let strokeWidth = this.getStrokeWidth(opts)
opts.size = opts.size - strokeWidth / 2
for (let i = 0; i < opts.series.length; i++) {
let elRadialBarTrack = graphics.group({
class: 'apexcharts-radialbar-track apexcharts-track'
})
g.add(elRadialBarTrack)
elRadialBarTrack.attr({
rel: i + 1
})
opts.size = opts.size - strokeWidth - this.margin
const trackConfig = w.config.plotOptions.radialBar.track
let pathFill = fill.fillPath({
seriesNumber: 0,
size: opts.size,
fillColors: Array.isArray(trackConfig.background)
? trackConfig.background[i]
: trackConfig.background,
solid: true
})
let startAngle = this.trackStartAngle
let endAngle = this.trackEndAngle
if (Math.abs(endAngle) + Math.abs(startAngle) >= 360)
endAngle = 360 - Math.abs(this.startAngle) - 0.1
let elPath = graphics.drawPath({
d: '',
stroke: pathFill,
strokeWidth:
(strokeWidth * parseInt(trackConfig.strokeWidth, 10)) / 100,
fill: 'none',
strokeOpacity: trackConfig.opacity,
classes: 'apexcharts-radialbar-area'
})
if (trackConfig.dropShadow.enabled) {
const shadow = trackConfig.dropShadow
filters.dropShadow(elPath, shadow)
}
elRadialBarTrack.add(elPath)
elPath.attr('id', 'apexcharts-radialbarTrack-' + i)
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: 0,
dur: 0,
isTrack: true,
easing: w.globals.easing
})
}
return g
}
drawArcs(opts) {
let w = this.w
// size, donutSize, centerX, centerY, colorArr, lineColorArr, sectorAngleArr, series
let graphics = new Graphics(this.ctx)
let fill = new Fill(this.ctx)
let filters = new Filters(this.ctx)
let g = graphics.group()
let strokeWidth = this.getStrokeWidth(opts)
opts.size = opts.size - strokeWidth / 2
let hollowFillID = w.config.plotOptions.radialBar.hollow.background
let hollowSize =
opts.size -
strokeWidth * opts.series.length -
this.margin * opts.series.length -
(strokeWidth *
parseInt(w.config.plotOptions.radialBar.track.strokeWidth, 10)) /
100 /
2
let hollowRadius = hollowSize - w.config.plotOptions.radialBar.hollow.margin
if (w.config.plotOptions.radialBar.hollow.image !== undefined) {
hollowFillID = this.drawHollowImage(opts, g, hollowSize, hollowFillID)
}
let elHollow = this.drawHollow({
size: hollowRadius,
centerX: opts.centerX,
centerY: opts.centerY,
fill: hollowFillID ? hollowFillID : 'transparent'
})
if (w.config.plotOptions.radialBar.hollow.dropShadow.enabled) {
const shadow = w.config.plotOptions.radialBar.hollow.dropShadow
filters.dropShadow(elHollow, shadow)
}
let shown = 1
if (!this.radialDataLabels.total.show && w.globals.series.length > 1) {
shown = 0
}
let dataLabels = null
if (this.radialDataLabels.show) {
dataLabels = this.renderInnerDataLabels(this.radialDataLabels, {
hollowSize,
centerX: opts.centerX,
centerY: opts.centerY,
opacity: shown
})
}
if (w.config.plotOptions.radialBar.hollow.position === 'back') {
g.add(elHollow)
if (dataLabels) {
g.add(dataLabels)
}
}
let reverseLoop = false
if (w.config.plotOptions.radialBar.inverseOrder) {
reverseLoop = true
}
for (
let i = reverseLoop ? opts.series.length - 1 : 0;
reverseLoop ? i >= 0 : i < opts.series.length;
reverseLoop ? i-- : i++
) {
let elRadialBarArc = graphics.group({
class: `apexcharts-series apexcharts-radial-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[i])
})
g.add(elRadialBarArc)
elRadialBarArc.attr({
rel: i + 1,
'data:realIndex': i
})
this.ctx.series.addCollapsedClassToSeries(elRadialBarArc, i)
opts.size = opts.size - strokeWidth - this.margin
let pathFill = fill.fillPath({
seriesNumber: i,
size: opts.size,
value: opts.series[i]
})
let startAngle = this.startAngle
let prevStartAngle
// if data exceeds 100, make it 100
const dataValue =
Utils.negToZero(opts.series[i] > 100 ? 100 : opts.series[i]) / 100
let endAngle = Math.round(this.totalAngle * dataValue) + this.startAngle
let prevEndAngle
if (w.globals.dataChanged) {
prevStartAngle = this.startAngle
prevEndAngle =
Math.round(
(this.totalAngle * Utils.negToZero(w.globals.previousPaths[i])) /
100
) + prevStartAngle
}
const currFullAngle = Math.abs(endAngle) + Math.abs(startAngle)
if (currFullAngle >= 360) {
endAngle = endAngle - 0.01
}
const prevFullAngle = Math.abs(prevEndAngle) + Math.abs(prevStartAngle)
if (prevFullAngle >= 360) {
prevEndAngle = prevEndAngle - 0.01
}
let angle = endAngle - startAngle
const dashArray = Array.isArray(w.config.stroke.dashArray)
? w.config.stroke.dashArray[i]
: w.config.stroke.dashArray
let elPath = graphics.drawPath({
d: '',
stroke: pathFill,
strokeWidth,
fill: 'none',
fillOpacity: w.config.fill.opacity,
classes: 'apexcharts-radialbar-area apexcharts-radialbar-slice-' + i,
strokeDashArray: dashArray
})
Graphics.setAttrs(elPath.node, {
'data:angle': angle,
'data:value': opts.series[i]
})
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow
filters.dropShadow(elPath, shadow, i)
}
filters.setSelectionFilter(elPath, 0, i)
this.addListeners(elPath, this.radialDataLabels)
elRadialBarArc.add(elPath)
elPath.attr({
index: 0,
j: i
})
let dur = 0
if (this.initialAnim && !w.globals.resized && !w.globals.dataChanged) {
dur = w.config.chart.animations.speed
}
if (w.globals.dataChanged) {
dur = w.config.chart.animations.dynamicAnimation.speed
}
this.animDur = dur / (opts.series.length * 1.2) + this.animDur
this.animBeginArr.push(this.animDur)
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
prevEndAngle,
prevStartAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: this.animBeginArr,
dur,
shouldSetPrevPaths: true,
easing: w.globals.easing
})
}
return {
g,
elHollow,
dataLabels
}
}
drawHollow(opts) {
const graphics = new Graphics(this.ctx)
let circle = graphics.drawCircle(opts.size * 2)
circle.attr({
class: 'apexcharts-radialbar-hollow',
cx: opts.centerX,
cy: opts.centerY,
r: opts.size,
fill: opts.fill
})
return circle
}
drawHollowImage(opts, g, hollowSize, hollowFillID) {
const w = this.w
let fill = new Fill(this.ctx)
let randID = Utils.randomId()
let hollowFillImg = w.config.plotOptions.radialBar.hollow.image
if (w.config.plotOptions.radialBar.hollow.imageClipped) {
fill.clippedImgArea({
width: hollowSize,
height: hollowSize,
image: hollowFillImg,
patternID: `pattern${w.globals.cuid}${randID}`
})
hollowFillID = `url(#pattern${w.globals.cuid}${randID})`
} else {
const imgWidth = w.config.plotOptions.radialBar.hollow.imageWidth
const imgHeight = w.config.plotOptions.radialBar.hollow.imageHeight
if (imgWidth === undefined && imgHeight === undefined) {
let image = w.globals.dom.Paper.image(hollowFillImg).loaded(function(
loader
) {
this.move(
opts.centerX -
loader.width / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY -
loader.height / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetY
)
})
g.add(image)
} else {
let image = w.globals.dom.Paper.image(hollowFillImg).loaded(function(
loader
) {
this.move(
opts.centerX -
imgWidth / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY -
imgHeight / 2 +
w.config.plotOptions.radialBar.hollow.imageOffsetY
)
this.size(imgWidth, imgHeight)
})
g.add(image)
}
}
return hollowFillID
}
getStrokeWidth(opts) {
const w = this.w
return (
(opts.size *
(100 - parseInt(w.config.plotOptions.radialBar.hollow.size, 10))) /
100 /
(opts.series.length + 1) -
this.margin
)
}
}
export default Radial
@@ -0,0 +1,454 @@
import Bar from './Bar'
import Graphics from '../modules/Graphics'
import Utils from '../utils/Utils'
import DateTime from '../utils/DateTime'
/**
* ApexCharts RangeBar Class responsible for drawing Range/Timeline Bars.
*
* @module RangeBar
**/
class RangeBar extends Bar {
draw(series, seriesIndex) {
let w = this.w
let graphics = new Graphics(this.ctx)
this.rangeBarOptions = this.w.config.plotOptions.rangeBar
this.series = series
this.seriesRangeStart = w.globals.seriesRangeStart
this.seriesRangeEnd = w.globals.seriesRangeEnd
this.barHelpers.initVariables(series)
let ret = graphics.group({
class: 'apexcharts-rangebar-series apexcharts-plot-series'
})
for (let i = 0; i < series.length; i++) {
let x,
y,
xDivision, // xDivision is the GRIDWIDTH divided by number of datapoints (columns)
yDivision, // yDivision is the GRIDHEIGHT divided by number of datapoints (bars)
zeroH, // zeroH is the baseline where 0 meets y axis
zeroW // zeroW is the baseline where 0 meets x axis
let realIndex = w.globals.comboCharts ? seriesIndex[i] : i
// el to which series will be drawn
let elSeries = graphics.group({
class: `apexcharts-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[realIndex]),
rel: i + 1,
'data:realIndex': realIndex
})
this.ctx.series.addCollapsedClassToSeries(elSeries, realIndex)
if (series[i].length > 0) {
this.visibleI = this.visibleI + 1
}
let barHeight = 0
let barWidth = 0
if (this.yRatio.length > 1) {
this.yaxisIndex = realIndex
}
let initPositions = this.barHelpers.initialPositions()
y = initPositions.y
zeroW = initPositions.zeroW
x = initPositions.x
barWidth = initPositions.barWidth
xDivision = initPositions.xDivision
zeroH = initPositions.zeroH
// eldatalabels
let elDataLabelsWrap = graphics.group({
class: 'apexcharts-datalabels',
'data:realIndex': realIndex
})
let elGoalsMarkers = graphics.group({
class: 'apexcharts-rangebar-goals-markers',
style: `pointer-events: none`
})
for (let j = 0; j < w.globals.dataPoints; j++) {
const strokeWidth = this.barHelpers.getStrokeWidth(i, j, realIndex)
const y1 = this.seriesRangeStart[i][j]
const y2 = this.seriesRangeEnd[i][j]
let paths = null
let barYPosition = null
const params = { x, y, strokeWidth, elSeries }
yDivision = initPositions.yDivision
barHeight = initPositions.barHeight
if (this.isHorizontal) {
barYPosition = y + barHeight * this.visibleI
let seriesLen = this.seriesLen
if (w.config.plotOptions.bar.rangeBarGroupRows) {
seriesLen = 1
}
let srty = (yDivision - barHeight * seriesLen) / 2
if (typeof w.config.series[i].data[j] === 'undefined') {
// no data exists for further indexes, hence we need to get out the innr loop.
// As we are iterating over total datapoints, there is a possiblity the series might not have data for j index
break
}
if (w.config.series[i].data[j].x) {
let positions = this.detectOverlappingBars({
i,
j,
barYPosition,
srty,
barHeight,
yDivision,
initPositions
})
barHeight = positions.barHeight
barYPosition = positions.barYPosition
}
paths = this.drawRangeBarPaths({
indexes: { i, j, realIndex },
barHeight,
barYPosition,
zeroW,
yDivision,
y1,
y2,
...params
})
barWidth = paths.barWidth
} else {
paths = this.drawRangeColumnPaths({
indexes: { i, j, realIndex },
zeroH,
barWidth,
xDivision,
...params
})
barHeight = paths.barHeight
}
const barGoalLine = this.barHelpers.drawGoalLine({
barXPosition: paths.barXPosition,
barYPosition,
goalX: paths.goalX,
goalY: paths.goalY,
barHeight,
barWidth
})
if (barGoalLine) {
elGoalsMarkers.add(barGoalLine)
}
y = paths.y
x = paths.x
let pathFill = this.barHelpers.getPathFillColor(series, i, j, realIndex)
let lineFill = w.globals.stroke.colors[realIndex]
this.renderSeries({
realIndex,
pathFill,
lineFill,
j,
i,
x,
y,
y1,
y2,
pathFrom: paths.pathFrom,
pathTo: paths.pathTo,
strokeWidth,
elSeries,
series,
barHeight,
barYPosition,
barWidth,
elDataLabelsWrap,
elGoalsMarkers,
visibleSeries: this.visibleI,
type: 'rangebar'
})
}
ret.add(elSeries)
}
return ret
}
detectOverlappingBars({
i,
j,
barYPosition,
srty,
barHeight,
yDivision,
initPositions
}) {
const w = this.w
let overlaps = []
let rangeName = w.config.series[i].data[j].rangeName
const labelX = w.config.series[i].data[j].x
const rowIndex = w.globals.labels.indexOf(labelX)
const overlappedIndex = w.globals.seriesRangeBar[i].findIndex(
(tx) => tx.x === labelX && tx.overlaps.length > 0
)
if (w.config.plotOptions.bar.rangeBarGroupRows) {
barYPosition = srty + yDivision * rowIndex
} else {
barYPosition = srty + barHeight * this.visibleI + yDivision * rowIndex
}
if (overlappedIndex > -1 && !w.config.plotOptions.bar.rangeBarOverlap) {
overlaps = w.globals.seriesRangeBar[i][overlappedIndex].overlaps
if (overlaps.indexOf(rangeName) > -1) {
barHeight = initPositions.barHeight / overlaps.length
barYPosition =
barHeight * this.visibleI +
(yDivision * (100 - parseInt(this.barOptions.barHeight, 10))) /
100 /
2 +
barHeight * (this.visibleI + overlaps.indexOf(rangeName)) +
yDivision * rowIndex
}
}
return {
barYPosition,
barHeight
}
}
drawRangeColumnPaths({
indexes,
x,
strokeWidth,
xDivision,
barWidth,
zeroH
}) {
let w = this.w
let i = indexes.i
let j = indexes.j
const yRatio = this.yRatio[this.yaxisIndex]
let realIndex = indexes.realIndex
const range = this.getRangeValue(realIndex, j)
let y1 = Math.min(range.start, range.end)
let y2 = Math.max(range.start, range.end)
if (w.globals.isXNumeric) {
x =
(w.globals.seriesX[i][j] - w.globals.minX) / this.xRatio - barWidth / 2
}
let barXPosition = x + barWidth * this.visibleI
if (
typeof this.series[i][j] === 'undefined' ||
this.series[i][j] === null
) {
y1 = zeroH
} else {
y1 = zeroH - y1 / yRatio
y2 = zeroH - y2 / yRatio
}
const barHeight = Math.abs(y2 - y1)
const paths = this.barHelpers.getColumnPaths({
barXPosition,
barWidth,
y1,
y2,
strokeWidth: this.strokeWidth,
series: this.seriesRangeEnd,
realIndex: indexes.realIndex,
i: realIndex,
j,
w
})
if (!w.globals.isXNumeric) {
x = x + xDivision
}
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
barHeight,
x,
y: y2,
goalY: this.barHelpers.getGoalValues('y', null, zeroH, i, j),
barXPosition
}
}
drawRangeBarPaths({
indexes,
y,
y1,
y2,
yDivision,
barHeight,
barYPosition,
zeroW
}) {
let w = this.w
const x1 = zeroW + y1 / this.invertedYRatio
const x2 = zeroW + y2 / this.invertedYRatio
const barWidth = Math.abs(x2 - x1)
const paths = this.barHelpers.getBarpaths({
barYPosition,
barHeight,
x1,
x2,
strokeWidth: this.strokeWidth,
series: this.seriesRangeEnd,
i: indexes.realIndex,
realIndex: indexes.realIndex,
j: indexes.j,
w
})
if (!w.globals.isXNumeric) {
y = y + yDivision
}
return {
pathTo: paths.pathTo,
pathFrom: paths.pathFrom,
barWidth,
x: x2,
goalX: this.barHelpers.getGoalValues(
'x',
zeroW,
null,
indexes.realIndex,
indexes.j
),
y
}
}
getRangeValue(i, j) {
const w = this.w
return {
start: w.globals.seriesRangeStart[i][j],
end: w.globals.seriesRangeEnd[i][j]
}
}
getTooltipValues({ ctx, seriesIndex, dataPointIndex, y1, y2, w }) {
let start = w.globals.seriesRangeStart[seriesIndex][dataPointIndex]
let end = w.globals.seriesRangeEnd[seriesIndex][dataPointIndex]
let ylabel = w.globals.labels[dataPointIndex]
let seriesName = w.config.series[seriesIndex].name
? w.config.series[seriesIndex].name
: ''
const yLbFormatter = w.config.tooltip.y.formatter
const yLbTitleFormatter = w.config.tooltip.y.title.formatter
const opts = {
w,
seriesIndex,
dataPointIndex,
start,
end
}
if (typeof yLbTitleFormatter === 'function') {
seriesName = yLbTitleFormatter(seriesName, opts)
}
if (Number.isFinite(y1) && Number.isFinite(y2)) {
start = y1
end = y2
if (w.config.series[seriesIndex].data[dataPointIndex].x) {
ylabel = w.config.series[seriesIndex].data[dataPointIndex].x + ':'
}
if (typeof yLbFormatter === 'function') {
ylabel = yLbFormatter(ylabel, opts)
}
}
let startVal = ''
let endVal = ''
const color = w.globals.colors[seriesIndex]
if (w.config.tooltip.x.formatter === undefined) {
if (w.config.xaxis.type === 'datetime') {
let datetimeObj = new DateTime(ctx)
startVal = datetimeObj.formatDate(
datetimeObj.getDate(start),
w.config.tooltip.x.format
)
endVal = datetimeObj.formatDate(
datetimeObj.getDate(end),
w.config.tooltip.x.format
)
} else {
startVal = start
endVal = end
}
} else {
startVal = w.config.tooltip.x.formatter(start)
endVal = w.config.tooltip.x.formatter(end)
}
return { start, end, startVal, endVal, ylabel, color, seriesName }
}
buildCustomTooltipHTML({ color, seriesName, ylabel, start, end }) {
return (
'<div class="apexcharts-tooltip-rangebar">' +
'<div> <span class="series-name" style="color: ' +
color +
'">' +
(seriesName ? seriesName : '') +
'</span></div>' +
'<div> <span class="category">' +
ylabel +
' </span> <span class="value start-value">' +
start +
'</span> <span class="separator">-</span> <span class="value end-value">' +
end +
'</span></div>' +
'</div>'
)
}
}
export default RangeBar
@@ -0,0 +1,267 @@
import Animations from '../modules/Animations'
import Fill from '../modules/Fill'
import Filters from '../modules/Filters'
import Graphics from '../modules/Graphics'
import Markers from '../modules/Markers'
/**
* ApexCharts Scatter Class.
* This Class also handles bubbles chart as currently there is no major difference in drawing them,
* @module Scatter
**/
export default class Scatter {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
this.initialAnim = this.w.config.chart.animations.enabled
this.dynamicAnim =
this.initialAnim &&
this.w.config.chart.animations.dynamicAnimation.enabled
}
draw(elSeries, j, opts) {
let w = this.w
let graphics = new Graphics(this.ctx)
let realIndex = opts.realIndex
let pointsPos = opts.pointsPos
let zRatio = opts.zRatio
let elPointsMain = opts.elParent
let elPointsWrap = graphics.group({
class: `apexcharts-series-markers apexcharts-series-${w.config.chart.type}`
})
elPointsWrap.attr('clip-path', `url(#gridRectMarkerMask${w.globals.cuid})`)
if (Array.isArray(pointsPos.x)) {
for (let q = 0; q < pointsPos.x.length; q++) {
let dataPointIndex = j + 1
let shouldDraw = true
// a small hack as we have 2 points for the first val to connect it
if (j === 0 && q === 0) dataPointIndex = 0
if (j === 0 && q === 1) dataPointIndex = 1
let radius = 0
let finishRadius = w.globals.markers.size[realIndex]
if (zRatio !== Infinity) {
// means we have a bubble
finishRadius = w.globals.seriesZ[realIndex][dataPointIndex] / zRatio
const bubble = w.config.plotOptions.bubble
if (bubble.minBubbleRadius && finishRadius < bubble.minBubbleRadius) {
finishRadius = bubble.minBubbleRadius
}
if (bubble.maxBubbleRadius && finishRadius > bubble.maxBubbleRadius) {
finishRadius = bubble.maxBubbleRadius
}
}
if (!w.config.chart.animations.enabled) {
radius = finishRadius
}
let x = pointsPos.x[q]
let y = pointsPos.y[q]
radius = radius || 0
if (
y === null ||
typeof w.globals.series[realIndex][dataPointIndex] === 'undefined'
) {
shouldDraw = false
}
if (shouldDraw) {
const point = this.drawPoint(
x,
y,
radius,
finishRadius,
realIndex,
dataPointIndex,
j
)
elPointsWrap.add(point)
}
elPointsMain.add(elPointsWrap)
}
}
}
drawPoint(x, y, radius, finishRadius, realIndex, dataPointIndex, j) {
const w = this.w
let i = realIndex
let anim = new Animations(this.ctx)
let filters = new Filters(this.ctx)
let fill = new Fill(this.ctx)
let markers = new Markers(this.ctx)
const graphics = new Graphics(this.ctx)
const markerConfig = markers.getMarkerConfig({
cssClass: 'apexcharts-marker',
seriesIndex: i,
dataPointIndex,
finishRadius:
w.config.chart.type === 'bubble' ||
(w.globals.comboCharts &&
w.config.series[realIndex] &&
w.config.series[realIndex].type === 'bubble')
? finishRadius
: null
})
finishRadius = markerConfig.pSize
let pathFillCircle = fill.fillPath({
seriesNumber: realIndex,
dataPointIndex,
color: markerConfig.pointFillColor,
patternUnits: 'objectBoundingBox',
value: w.globals.series[realIndex][j]
})
let el
if (markerConfig.shape === 'circle') {
el = graphics.drawCircle(radius)
} else if (
markerConfig.shape === 'square' ||
markerConfig.shape === 'rect'
) {
el = graphics.drawRect(
0,
0,
markerConfig.width - markerConfig.pointStrokeWidth / 2,
markerConfig.height - markerConfig.pointStrokeWidth / 2,
markerConfig.pRadius
)
}
if (w.config.series[i].data[dataPointIndex]) {
if (w.config.series[i].data[dataPointIndex].fillColor) {
pathFillCircle = w.config.series[i].data[dataPointIndex].fillColor
}
}
el.attr({
x: x - markerConfig.width / 2 - markerConfig.pointStrokeWidth / 2,
y: y - markerConfig.height / 2 - markerConfig.pointStrokeWidth / 2,
cx: x,
cy: y,
fill: pathFillCircle,
'fill-opacity': markerConfig.pointFillOpacity,
stroke: markerConfig.pointStrokeColor,
r: finishRadius,
'stroke-width': markerConfig.pointStrokeWidth,
'stroke-dasharray': markerConfig.pointStrokeDashArray,
'stroke-opacity': markerConfig.pointStrokeOpacity
})
if (w.config.chart.dropShadow.enabled) {
const dropShadow = w.config.chart.dropShadow
filters.dropShadow(el, dropShadow, realIndex)
}
if (this.initialAnim && !w.globals.dataChanged && !w.globals.resized) {
let speed = w.config.chart.animations.speed
anim.animateMarker(
el,
0,
markerConfig.shape === 'circle'
? finishRadius
: { width: markerConfig.width, height: markerConfig.height },
speed,
w.globals.easing,
() => {
window.setTimeout(() => {
anim.animationCompleted(el)
}, 100)
}
)
} else {
w.globals.animationEnded = true
}
if (w.globals.dataChanged && markerConfig.shape === 'circle') {
if (this.dynamicAnim) {
let speed = w.config.chart.animations.dynamicAnimation.speed
let prevX, prevY, prevR
let prevPathJ = null
prevPathJ =
w.globals.previousPaths[realIndex] &&
w.globals.previousPaths[realIndex][j]
if (typeof prevPathJ !== 'undefined' && prevPathJ !== null) {
// series containing less elements will ignore these values and revert to 0
prevX = prevPathJ.x
prevY = prevPathJ.y
prevR =
typeof prevPathJ.r !== 'undefined' ? prevPathJ.r : finishRadius
}
for (let cs = 0; cs < w.globals.collapsedSeries.length; cs++) {
if (w.globals.collapsedSeries[cs].index === realIndex) {
speed = 1
finishRadius = 0
}
}
if (x === 0 && y === 0) finishRadius = 0
anim.animateCircle(
el,
{
cx: prevX,
cy: prevY,
r: prevR
},
{
cx: x,
cy: y,
r: finishRadius
},
speed,
w.globals.easing
)
} else {
el.attr({
r: finishRadius
})
}
}
el.attr({
rel: dataPointIndex,
j: dataPointIndex,
index: realIndex,
'default-marker-size': finishRadius
})
filters.setSelectionFilter(el, realIndex, dataPointIndex)
markers.addEvents(el)
el.node.classList.add('apexcharts-marker')
return el
}
centerTextInBubble(y) {
let w = this.w
y = y + parseInt(w.config.dataLabels.style.fontSize, 10) / 4
return {
y
}
}
}
@@ -0,0 +1,302 @@
import '../libs/Treemap-squared'
import Graphics from '../modules/Graphics'
import Animations from '../modules/Animations'
import Fill from '../modules/Fill'
import Helpers from './common/treemap/Helpers'
import Filters from '../modules/Filters'
import Utils from '../utils/Utils'
/**
* ApexCharts TreemapChart Class.
* @module TreemapChart
**/
export default class TreemapChart {
constructor(ctx, xyRatios) {
this.ctx = ctx
this.w = ctx.w
this.strokeWidth = this.w.config.stroke.width
this.helpers = new Helpers(ctx)
this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation
this.labels = []
}
draw(series) {
let w = this.w
const graphics = new Graphics(this.ctx)
const fill = new Fill(this.ctx)
let ret = graphics.group({
class: 'apexcharts-treemap'
})
if (w.globals.noData) return ret
let ser = []
series.forEach((s) => {
let d = s.map((v) => {
return Math.abs(v)
})
ser.push(d)
})
this.negRange = this.helpers.checkColorRange()
w.config.series.forEach((s, i) => {
s.data.forEach((l) => {
if (!Array.isArray(this.labels[i])) this.labels[i] = []
this.labels[i].push(l.x)
})
})
const nodes = window.TreemapSquared.generate(
ser,
w.globals.gridWidth,
w.globals.gridHeight
)
nodes.forEach((node, i) => {
let elSeries = graphics.group({
class: `apexcharts-series apexcharts-treemap-series`,
seriesName: Utils.escapeString(w.globals.seriesNames[i]),
rel: i + 1,
'data:realIndex': i
})
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow
const filters = new Filters(this.ctx)
filters.dropShadow(ret, shadow, i)
}
let elDataLabelWrap = graphics.group({
class: 'apexcharts-data-labels'
})
node.forEach((r, j) => {
const x1 = r[0]
const y1 = r[1]
const x2 = r[2]
const y2 = r[3]
let elRect = graphics.drawRect(
x1,
y1,
x2 - x1,
y2 - y1,
0,
'#fff',
1,
this.strokeWidth,
w.config.plotOptions.treemap.useFillColorAsStroke
? color
: w.globals.stroke.colors[i]
)
elRect.attr({
cx: x1,
cy: y1,
index: i,
i,
j,
width: x2 - x1,
height: y2 - y1
})
let colorProps = this.helpers.getShadeColor(
w.config.chart.type,
i,
j,
this.negRange
)
let color = colorProps.color
if (
typeof w.config.series[i].data[j] !== 'undefined' &&
w.config.series[i].data[j].fillColor
) {
color = w.config.series[i].data[j].fillColor
}
let pathFill = fill.fillPath({
color,
seriesNumber: i,
dataPointIndex: j
})
elRect.node.classList.add('apexcharts-treemap-rect')
elRect.attr({
fill: pathFill
})
this.helpers.addListeners(elRect)
let fromRect = {
x: x1 + (x2 - x1) / 2,
y: y1 + (y2 - y1) / 2,
width: 0,
height: 0
}
let toRect = {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1
}
if (w.config.chart.animations.enabled && !w.globals.dataChanged) {
let speed = 1
if (!w.globals.resized) {
speed = w.config.chart.animations.speed
}
this.animateTreemap(elRect, fromRect, toRect, speed)
}
if (w.globals.dataChanged) {
let speed = 1
if (this.dynamicAnim.enabled && w.globals.shouldAnimate) {
speed = this.dynamicAnim.speed
if (
w.globals.previousPaths[i] &&
w.globals.previousPaths[i][j] &&
w.globals.previousPaths[i][j].rect
) {
fromRect = w.globals.previousPaths[i][j].rect
}
this.animateTreemap(elRect, fromRect, toRect, speed)
}
}
const fontSize = this.getFontSize(r)
let formattedText = w.config.dataLabels.formatter(this.labels[i][j], {
value: w.globals.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w
})
let dataLabels = this.helpers.calculateDataLabels({
text: formattedText,
x: (x1 + x2) / 2,
y: (y1 + y2) / 2 + this.strokeWidth / 2 + fontSize / 3,
i,
j,
colorProps,
fontSize,
series
})
if (w.config.dataLabels.enabled && dataLabels) {
this.rotateToFitLabel(dataLabels, formattedText, x1, y1, x2, y2)
}
elSeries.add(elRect)
if (dataLabels !== null) {
elSeries.add(dataLabels)
}
})
elSeries.add(elDataLabelWrap)
ret.add(elSeries)
})
return ret
}
// This calculates a font-size based upon
// average label length and the size of the box the label is
// going into. The maximum font size is set in chart config.
getFontSize(coordinates) {
const w = this.w
// total length of labels (i.e [["Italy"],["Spain", "Greece"]] -> 16)
function totalLabelLength(arr) {
let i,
total = 0
if (Array.isArray(arr[0])) {
for (i = 0; i < arr.length; i++) {
total += totalLabelLength(arr[i])
}
} else {
for (i = 0; i < arr.length; i++) {
total += arr[i].length
}
}
return total
}
// count of labels (i.e [["Italy"],["Spain", "Greece"]] -> 3)
function countLabels(arr) {
let i,
total = 0
if (Array.isArray(arr[0])) {
for (i = 0; i < arr.length; i++) {
total += countLabels(arr[i])
}
} else {
for (i = 0; i < arr.length; i++) {
total += 1
}
}
return total
}
let averagelabelsize =
totalLabelLength(this.labels) / countLabels(this.labels)
function fontSize(width, height) {
// the font size should be proportional to the size of the box (and the value)
// otherwise you can end up creating a visual distortion where two boxes of identical
// size have different sized labels, and thus make it look as if the two boxes
// represent different sizes
let area = width * height
let arearoot = Math.pow(area, 0.5)
return Math.min(
arearoot / averagelabelsize,
parseInt(w.config.dataLabels.style.fontSize, 10)
)
}
return fontSize(
coordinates[2] - coordinates[0],
coordinates[3] - coordinates[1]
)
}
rotateToFitLabel(elText, text, x1, y1, x2, y2) {
const graphics = new Graphics(this.ctx)
const textRect = graphics.getTextRects(text)
//if the label fits better sideways then rotate it
if (textRect.width + 5 > x2 - x1 && textRect.width <= y2 - y1) {
let labelRotatingCenter = graphics.rotateAroundCenter(elText.node)
elText.node.setAttribute(
'transform',
`rotate(-90 ${labelRotatingCenter.x} ${labelRotatingCenter.y})`
)
}
}
animateTreemap(el, fromRect, toRect, speed) {
const animations = new Animations(this.ctx)
animations.animateRect(
el,
{
x: fromRect.x,
y: fromRect.y,
width: fromRect.width,
height: fromRect.height
},
{
x: toRect.x,
y: toRect.y,
width: toRect.width,
height: toRect.height
},
speed,
() => {
animations.animationCompleted(el)
}
)
}
}
@@ -0,0 +1,442 @@
import Graphics from '../../../modules/Graphics'
import DataLabels from '../../../modules/DataLabels'
export default class BarDataLabels {
constructor(barCtx) {
this.w = barCtx.w
this.barCtx = barCtx
}
/** handleBarDataLabels is used to calculate the positions for the data-labels
* It also sets the element's data attr for bars and calls drawCalculatedBarDataLabels()
* After calculating, it also calls the function to draw data labels
* @memberof Bar
* @param {object} {barProps} most of the bar properties used throughout the bar
* drawing function
* @return {object} dataLabels node-element which you can append later
**/
handleBarDataLabels(opts) {
let {
x,
y,
y1,
y2,
i,
j,
realIndex,
series,
barHeight,
barWidth,
barYPosition,
visibleSeries,
renderedPath
} = opts
let w = this.w
let graphics = new Graphics(this.barCtx.ctx)
let strokeWidth = Array.isArray(this.barCtx.strokeWidth)
? this.barCtx.strokeWidth[realIndex]
: this.barCtx.strokeWidth
let bcx = x + parseFloat(barWidth * visibleSeries)
let bcy = y + parseFloat(barHeight * visibleSeries)
if (w.globals.isXNumeric && !w.globals.isBarHorizontal) {
bcx = x + parseFloat(barWidth * (visibleSeries + 1))
bcy = y + parseFloat(barHeight * (visibleSeries + 1)) - strokeWidth
}
let dataLabels = null
let dataLabelsX = x
let dataLabelsY = y
let dataLabelsPos = {}
let dataLabelsConfig = w.config.dataLabels
let barDataLabelsConfig = this.barCtx.barOptions.dataLabels
if (typeof barYPosition !== 'undefined' && this.barCtx.isRangeBar) {
bcy = barYPosition
dataLabelsY = barYPosition
}
const offX = dataLabelsConfig.offsetX
const offY = dataLabelsConfig.offsetY
let textRects = {
width: 0,
height: 0
}
if (w.config.dataLabels.enabled) {
const yLabel = this.barCtx.series[i][j]
textRects = graphics.getTextRects(
w.globals.yLabelFormatters[0](yLabel),
parseFloat(dataLabelsConfig.style.fontSize)
)
}
const params = {
x,
y,
i,
j,
renderedPath,
bcx,
bcy,
barHeight,
barWidth,
textRects,
strokeWidth,
dataLabelsX,
dataLabelsY,
barDataLabelsConfig,
offX,
offY
}
if (this.barCtx.isHorizontal) {
dataLabelsPos = this.calculateBarsDataLabelsPosition(params)
} else {
dataLabelsPos = this.calculateColumnsDataLabelsPosition(params)
}
renderedPath.attr({
cy: dataLabelsPos.bcy,
cx: dataLabelsPos.bcx,
j,
val: series[i][j],
barHeight,
barWidth
})
dataLabels = this.drawCalculatedDataLabels({
x: dataLabelsPos.dataLabelsX,
y: dataLabelsPos.dataLabelsY,
val: this.barCtx.isRangeBar ? [y1, y2] : series[i][j],
i: realIndex,
j,
barWidth,
barHeight,
textRects,
dataLabelsConfig
})
return dataLabels
}
calculateColumnsDataLabelsPosition(opts) {
const w = this.w
let {
i,
j,
y,
bcx,
barWidth,
barHeight,
textRects,
dataLabelsY,
barDataLabelsConfig,
strokeWidth,
offX,
offY
} = opts
let dataLabelsX
barHeight = Math.abs(barHeight)
let vertical =
w.config.plotOptions.bar.dataLabels.orientation === 'vertical'
bcx = bcx - strokeWidth / 2
let dataPointsDividedWidth = w.globals.gridWidth / w.globals.dataPoints
if (w.globals.isXNumeric) {
dataLabelsX = bcx - barWidth / 2 + offX
} else {
dataLabelsX = bcx - dataPointsDividedWidth + barWidth / 2 + offX
}
if (vertical) {
const offsetDLX = 2
dataLabelsX =
dataLabelsX + textRects.height / 2 - strokeWidth / 2 - offsetDLX
}
let valIsNegative = this.barCtx.series[i][j] < 0
let newY = y
if (this.barCtx.isReversed) {
newY = y - barHeight + (valIsNegative ? barHeight * 2 : 0)
y = y - barHeight
}
switch (barDataLabelsConfig.position) {
case 'center':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY + barHeight / 2 + offY
} else {
dataLabelsY = newY + barHeight / 2 - offY
}
} else {
if (valIsNegative) {
dataLabelsY = newY - barHeight / 2 + textRects.height / 2 + offY
} else {
dataLabelsY = newY + barHeight / 2 + textRects.height / 2 - offY
}
}
break
case 'bottom':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY + barHeight + offY
} else {
dataLabelsY = newY + barHeight - offY
}
} else {
if (valIsNegative) {
dataLabelsY =
newY - barHeight + textRects.height + strokeWidth + offY
} else {
dataLabelsY =
newY + barHeight - textRects.height / 2 + strokeWidth - offY
}
}
break
case 'top':
if (vertical) {
if (valIsNegative) {
dataLabelsY = newY + offY
} else {
dataLabelsY = newY - offY
}
} else {
if (valIsNegative) {
dataLabelsY = newY - textRects.height / 2 - offY
} else {
dataLabelsY = newY + textRects.height + offY
}
}
break
}
if (!w.config.chart.stacked) {
if (dataLabelsY < 0) {
dataLabelsY = 0 + strokeWidth
} else if (dataLabelsY + textRects.height / 3 > w.globals.gridHeight) {
dataLabelsY = w.globals.gridHeight - strokeWidth
}
}
return {
bcx,
bcy: y,
dataLabelsX,
dataLabelsY
}
}
calculateBarsDataLabelsPosition(opts) {
const w = this.w
let {
x,
i,
j,
bcy,
barHeight,
barWidth,
textRects,
dataLabelsX,
strokeWidth,
barDataLabelsConfig,
offX,
offY
} = opts
let dataPointsDividedHeight = w.globals.gridHeight / w.globals.dataPoints
barWidth = Math.abs(barWidth)
let dataLabelsY =
bcy -
(this.barCtx.isRangeBar ? 0 : dataPointsDividedHeight) +
barHeight / 2 +
textRects.height / 2 +
offY -
3
let valIsNegative = this.barCtx.series[i][j] < 0
let newX = x
if (this.barCtx.isReversed) {
newX = x + barWidth - (valIsNegative ? barWidth * 2 : 0)
x = w.globals.gridWidth - barWidth
}
switch (barDataLabelsConfig.position) {
case 'center':
if (valIsNegative) {
dataLabelsX = newX + barWidth / 2 - offX
} else {
dataLabelsX =
Math.max(textRects.width / 2, newX - barWidth / 2) + offX
}
break
case 'bottom':
if (valIsNegative) {
dataLabelsX =
newX +
barWidth -
strokeWidth -
Math.round(textRects.width / 2) -
offX
} else {
dataLabelsX =
newX -
barWidth +
strokeWidth +
Math.round(textRects.width / 2) +
offX
}
break
case 'top':
if (valIsNegative) {
dataLabelsX =
newX - strokeWidth + Math.round(textRects.width / 2) - offX
} else {
dataLabelsX =
newX - strokeWidth - Math.round(textRects.width / 2) + offX
}
break
}
if (!w.config.chart.stacked) {
if (dataLabelsX < 0) {
dataLabelsX = dataLabelsX + textRects.width + strokeWidth
} else if (dataLabelsX + textRects.width / 2 > w.globals.gridWidth) {
dataLabelsX = w.globals.gridWidth - textRects.width - strokeWidth
}
}
return {
bcx: x,
bcy,
dataLabelsX,
dataLabelsY
}
}
drawCalculatedDataLabels({
x,
y,
val,
i,
j,
textRects,
barHeight,
barWidth,
dataLabelsConfig
}) {
const w = this.w
let rotate = 'rotate(0)'
if (w.config.plotOptions.bar.dataLabels.orientation === 'vertical')
rotate = `rotate(-90, ${x}, ${y})`
const dataLabels = new DataLabels(this.barCtx.ctx)
const graphics = new Graphics(this.barCtx.ctx)
const formatter = dataLabelsConfig.formatter
let elDataLabelsWrap = null
const isSeriesNotCollapsed =
w.globals.collapsedSeriesIndices.indexOf(i) > -1
if (dataLabelsConfig.enabled && !isSeriesNotCollapsed) {
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels',
transform: rotate
})
let text = ''
if (typeof val !== 'undefined') {
text = formatter(val, {
seriesIndex: i,
dataPointIndex: j,
w
})
}
let valIsNegative = w.globals.series[i][j] < 0
let position = w.config.plotOptions.bar.dataLabels.position
if (w.config.plotOptions.bar.dataLabels.orientation === 'vertical') {
if (position === 'top') {
if (valIsNegative) dataLabelsConfig.textAnchor = 'end'
else dataLabelsConfig.textAnchor = 'start'
}
if (position === 'center') {
dataLabelsConfig.textAnchor = 'middle'
}
if (position === 'bottom') {
if (valIsNegative) dataLabelsConfig.textAnchor = 'end'
else dataLabelsConfig.textAnchor = 'start'
}
}
if (
this.barCtx.isRangeBar &&
this.barCtx.barOptions.dataLabels.hideOverflowingLabels
) {
// hide the datalabel if it cannot fit into the rect
const txRect = graphics.getTextRects(
text,
parseFloat(dataLabelsConfig.style.fontSize)
)
if (barWidth < txRect.width) {
text = ''
}
}
if (
w.config.chart.stacked &&
this.barCtx.barOptions.dataLabels.hideOverflowingLabels
) {
// if there is not enough space to draw the label in the bar/column rect, check hideOverflowingLabels property to prevent overflowing on wrong rect
// Note: This issue is only seen in stacked charts
if (this.barCtx.isHorizontal) {
if (textRects.width / 1.6 > Math.abs(barWidth)) {
text = ''
}
} else {
if (textRects.height / 1.6 > Math.abs(barHeight)) {
text = ''
}
}
}
let modifiedDataLabelsConfig = {
...dataLabelsConfig
}
if (this.barCtx.isHorizontal) {
if (val < 0) {
if (dataLabelsConfig.textAnchor === 'start') {
modifiedDataLabelsConfig.textAnchor = 'end'
} else if (dataLabelsConfig.textAnchor === 'end') {
modifiedDataLabelsConfig.textAnchor = 'start'
}
}
}
dataLabels.plotDataLabelsText({
x,
y,
text,
i,
j,
parent: elDataLabelsWrap,
dataLabelsConfig: modifiedDataLabelsConfig,
alwaysDrawDataLabel: true,
offsetCorrection: true
})
}
return elDataLabelsWrap
}
}
@@ -0,0 +1,638 @@
import Fill from '../../../modules/Fill'
import Graphics from '../../../modules/Graphics'
import Series from '../../../modules/Series'
export default class Helpers {
constructor(barCtx) {
this.w = barCtx.w
this.barCtx = barCtx
}
initVariables(series) {
const w = this.w
this.barCtx.series = series
this.barCtx.totalItems = 0
this.barCtx.seriesLen = 0
this.barCtx.visibleI = -1 // visible Series
this.barCtx.visibleItems = 1 // number of visible bars after user zoomed in/out
for (let sl = 0; sl < series.length; sl++) {
if (series[sl].length > 0) {
this.barCtx.seriesLen = this.barCtx.seriesLen + 1
this.barCtx.totalItems += series[sl].length
}
if (w.globals.isXNumeric) {
// get max visible items
for (let j = 0; j < series[sl].length; j++) {
if (
w.globals.seriesX[sl][j] > w.globals.minX &&
w.globals.seriesX[sl][j] < w.globals.maxX
) {
this.barCtx.visibleItems++
}
}
} else {
this.barCtx.visibleItems = w.globals.dataPoints
}
}
if (this.barCtx.seriesLen === 0) {
// A small adjustment when combo charts are used
this.barCtx.seriesLen = 1
}
this.barCtx.zeroSerieses = []
this.barCtx.radiusOnSeriesNumber = series.length - 1 // which series to draw ending shape on
if (!w.globals.comboCharts) {
this.checkZeroSeries({ series })
}
}
initialPositions() {
let w = this.w
let x, y, yDivision, xDivision, barHeight, barWidth, zeroH, zeroW
let dataPoints = w.globals.dataPoints
if (this.barCtx.isRangeBar) {
// timeline rangebar chart
dataPoints = w.globals.labels.length
}
let seriesLen = this.barCtx.seriesLen
if (w.config.plotOptions.bar.rangeBarGroupRows) {
seriesLen = 1
}
if (this.barCtx.isHorizontal) {
// height divided into equal parts
yDivision = w.globals.gridHeight / dataPoints
barHeight = yDivision / seriesLen
if (w.globals.isXNumeric) {
yDivision = w.globals.gridHeight / this.barCtx.totalItems
barHeight = yDivision / this.barCtx.seriesLen
}
barHeight =
(barHeight * parseInt(this.barCtx.barOptions.barHeight, 10)) / 100
zeroW =
this.barCtx.baseLineInvertedY +
w.globals.padHorizontal +
(this.barCtx.isReversed ? w.globals.gridWidth : 0) -
(this.barCtx.isReversed ? this.barCtx.baseLineInvertedY * 2 : 0)
y = (yDivision - barHeight * this.barCtx.seriesLen) / 2
} else {
// width divided into equal parts
xDivision = w.globals.gridWidth / this.barCtx.visibleItems
if (w.config.xaxis.convertedCatToNumeric) {
xDivision = w.globals.gridWidth / w.globals.dataPoints
}
barWidth =
((xDivision / this.barCtx.seriesLen) *
parseInt(this.barCtx.barOptions.columnWidth, 10)) /
100
if (w.globals.isXNumeric) {
// max barwidth should be equal to minXDiff to avoid overlap
let xRatio = this.barCtx.xRatio
if (w.config.xaxis.convertedCatToNumeric) {
xRatio = this.barCtx.initialXRatio
}
if (
w.globals.minXDiff &&
w.globals.minXDiff !== 0.5 &&
w.globals.minXDiff / xRatio > 0
) {
xDivision = w.globals.minXDiff / xRatio
}
barWidth =
((xDivision / this.barCtx.seriesLen) *
parseInt(this.barCtx.barOptions.columnWidth, 10)) /
100
if (barWidth < 1) {
barWidth = 1
}
}
zeroH =
w.globals.gridHeight -
this.barCtx.baseLineY[this.barCtx.yaxisIndex] -
(this.barCtx.isReversed ? w.globals.gridHeight : 0) +
(this.barCtx.isReversed
? this.barCtx.baseLineY[this.barCtx.yaxisIndex] * 2
: 0)
x =
w.globals.padHorizontal +
(xDivision - barWidth * this.barCtx.seriesLen) / 2
}
return {
x,
y,
yDivision,
xDivision,
barHeight,
barWidth,
zeroH,
zeroW
}
}
getPathFillColor(series, i, j, realIndex) {
const w = this.w
let fill = new Fill(this.barCtx.ctx)
let fillColor = null
let seriesNumber = this.barCtx.barOptions.distributed ? j : i
if (this.barCtx.barOptions.colors.ranges.length > 0) {
const colorRange = this.barCtx.barOptions.colors.ranges
colorRange.map((range) => {
if (series[i][j] >= range.from && series[i][j] <= range.to) {
fillColor = range.color
}
})
}
if (w.config.series[i].data[j] && w.config.series[i].data[j].fillColor) {
fillColor = w.config.series[i].data[j].fillColor
}
let pathFill = fill.fillPath({
seriesNumber: this.barCtx.barOptions.distributed
? seriesNumber
: realIndex,
dataPointIndex: j,
color: fillColor,
value: series[i][j]
})
return pathFill
}
getStrokeWidth(i, j, realIndex) {
let strokeWidth = 0
const w = this.w
if (
typeof this.barCtx.series[i][j] === 'undefined' ||
this.barCtx.series[i][j] === null
) {
this.barCtx.isNullValue = true
} else {
this.barCtx.isNullValue = false
}
if (w.config.stroke.show) {
if (!this.barCtx.isNullValue) {
strokeWidth = Array.isArray(this.barCtx.strokeWidth)
? this.barCtx.strokeWidth[realIndex]
: this.barCtx.strokeWidth
}
}
return strokeWidth
}
barBackground({ j, i, x1, x2, y1, y2, elSeries }) {
const w = this.w
const graphics = new Graphics(this.barCtx.ctx)
const sr = new Series(this.barCtx.ctx)
let activeSeriesIndex = sr.getActiveConfigSeriesIndex()
if (
this.barCtx.barOptions.colors.backgroundBarColors.length > 0 &&
activeSeriesIndex === i
) {
if (j >= this.barCtx.barOptions.colors.backgroundBarColors.length) {
j %= this.barCtx.barOptions.colors.backgroundBarColors.length
}
let bcolor = this.barCtx.barOptions.colors.backgroundBarColors[j]
let rect = graphics.drawRect(
typeof x1 !== 'undefined' ? x1 : 0,
typeof y1 !== 'undefined' ? y1 : 0,
typeof x2 !== 'undefined' ? x2 : w.globals.gridWidth,
typeof y2 !== 'undefined' ? y2 : w.globals.gridHeight,
this.barCtx.barOptions.colors.backgroundBarRadius,
bcolor,
this.barCtx.barOptions.colors.backgroundBarOpacity
)
elSeries.add(rect)
rect.node.classList.add('apexcharts-backgroundBar')
}
}
getColumnPaths({
barWidth,
barXPosition,
yRatio,
y1,
y2,
strokeWidth,
series,
realIndex,
i,
j,
w
}) {
const graphics = new Graphics(this.barCtx.ctx)
strokeWidth = Array.isArray(strokeWidth)
? strokeWidth[realIndex]
: strokeWidth
if (!strokeWidth) strokeWidth = 0
let shapeOpts = {
barWidth,
strokeWidth,
yRatio,
barXPosition,
y1,
y2
}
let newPath = this.getRoundedBars(w, shapeOpts, series, i, j)
const x1 = barXPosition
const x2 = barXPosition + barWidth
let pathTo = graphics.move(x1, y1)
let pathFrom = graphics.move(x1, y1)
const sl = graphics.line(x2 - strokeWidth, y1)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, false)
}
pathTo =
pathTo +
graphics.line(x1, newPath.y2) +
newPath.pathWithRadius +
graphics.line(x2 - strokeWidth, newPath.y2) +
sl +
sl +
'z'
// the lines in pathFrom are repeated to equal it to the points of pathTo
// this is to avoid weird animation (bug in svg.js)
pathFrom =
pathFrom +
graphics.line(x1, y1) +
sl +
sl +
sl +
sl +
sl +
graphics.line(x1, y1)
if (w.config.chart.stacked) {
this.barCtx.yArrj.push(newPath.y2)
this.barCtx.yArrjF.push(Math.abs(y1 - newPath.y2))
this.barCtx.yArrjVal.push(this.barCtx.series[i][j])
}
return {
pathTo,
pathFrom
}
}
getBarpaths({
barYPosition,
barHeight,
x1,
x2,
strokeWidth,
series,
realIndex,
i,
j,
w
}) {
const graphics = new Graphics(this.barCtx.ctx)
strokeWidth = Array.isArray(strokeWidth)
? strokeWidth[realIndex]
: strokeWidth
if (!strokeWidth) strokeWidth = 0
let shapeOpts = {
barHeight,
strokeWidth,
barYPosition,
x2,
x1
}
let newPath = this.getRoundedBars(w, shapeOpts, series, i, j)
let pathTo = graphics.move(x1, barYPosition)
let pathFrom = graphics.move(x1, barYPosition)
if (w.globals.previousPaths.length > 0) {
pathFrom = this.barCtx.getPreviousPath(realIndex, j, false)
}
const y1 = barYPosition
const y2 = barYPosition + barHeight
const sl = graphics.line(x1, y2 - strokeWidth)
pathTo =
pathTo +
graphics.line(newPath.x2, y1) +
newPath.pathWithRadius +
graphics.line(newPath.x2, y2 - strokeWidth) +
sl +
sl +
'z'
pathFrom =
pathFrom +
graphics.line(x1, y1) +
sl +
sl +
sl +
sl +
sl +
graphics.line(x1, y1)
if (w.config.chart.stacked) {
this.barCtx.xArrj.push(newPath.x2)
this.barCtx.xArrjF.push(Math.abs(x1 - newPath.x2))
this.barCtx.xArrjVal.push(this.barCtx.series[i][j])
}
return {
pathTo,
pathFrom
}
}
/** getRoundedBars draws border radius for bars/columns
* @memberof Bar
* @param {object} w - chart context
* @param {object} opts - consists several properties like barHeight/barWidth
* @param {array} series - global primary series
* @param {int} i - current iterating series's index
* @param {int} j - series's j of i
* @return {object} pathWithRadius - ending shape path string
* newY/newX - which is calculated from existing x/y based on rounded border
**/
getRoundedBars(w, opts, series, i, j) {
let graphics = new Graphics(this.barCtx.ctx)
let radius = 0
const borderRadius = w.config.plotOptions.bar.borderRadius
const borderRadiusIsArray = Array.isArray(borderRadius)
if (borderRadiusIsArray) {
const radiusIndex =
i > borderRadius.length - 1 ? borderRadius.length - 1 : i
radius = borderRadius[radiusIndex]
} else {
radius = borderRadius
}
if (
w.config.chart.stacked &&
series.length > 1 &&
i !== this.barCtx.radiusOnSeriesNumber &&
!borderRadiusIsArray
) {
radius = 0
}
if (this.barCtx.isHorizontal) {
let pathWithRadius = ''
let x2 = opts.x2
if (Math.abs(opts.x1 - opts.x2) < radius) {
radius = Math.abs(opts.x1 - opts.x2)
}
if (typeof series[i][j] !== 'undefined' || series[i][j] !== null) {
let inverse = this.barCtx.isReversed
? series[i][j] > 0
: series[i][j] < 0
if (inverse) radius = radius * -1
x2 = x2 - radius
pathWithRadius =
graphics.quadraticCurve(
x2 + radius,
opts.barYPosition,
x2 + radius,
opts.barYPosition + (!inverse ? radius : radius * -1)
) +
graphics.line(
x2 + radius,
opts.barYPosition +
opts.barHeight -
opts.strokeWidth -
(!inverse ? radius : radius * -1)
) +
graphics.quadraticCurve(
x2 + radius,
opts.barYPosition + opts.barHeight - opts.strokeWidth,
x2,
opts.barYPosition + opts.barHeight - opts.strokeWidth
)
}
return {
pathWithRadius,
x2
}
} else {
let pathWithRadius = ''
let y2 = opts.y2
if (Math.abs(opts.y1 - opts.y2) < radius) {
radius = Math.abs(opts.y1 - opts.y2)
}
if (typeof series[i][j] !== 'undefined' || series[i][j] !== null) {
let inverse = series[i][j] < 0
if (inverse) radius = radius * -1
y2 = y2 + radius
pathWithRadius =
graphics.quadraticCurve(
opts.barXPosition,
y2 - radius,
opts.barXPosition + (!inverse ? radius : radius * -1),
y2 - radius
) +
graphics.line(
opts.barXPosition +
opts.barWidth -
opts.strokeWidth -
(!inverse ? radius : radius * -1),
y2 - radius
) +
graphics.quadraticCurve(
opts.barXPosition + opts.barWidth - opts.strokeWidth,
y2 - radius,
opts.barXPosition + opts.barWidth - opts.strokeWidth,
y2
)
}
return {
pathWithRadius,
y2
}
}
}
checkZeroSeries({ series }) {
let w = this.w
for (let zs = 0; zs < series.length; zs++) {
let total = 0
for (
let zsj = 0;
zsj < series[w.globals.maxValsInArrayIndex].length;
zsj++
) {
total += series[zs][zsj]
}
if (total === 0) {
this.barCtx.zeroSerieses.push(zs)
}
}
// After getting all zeroserieses, we need to ensure whether radiusOnSeriesNumber is not in that zeroseries array
for (let s = series.length - 1; s >= 0; s--) {
if (
this.barCtx.zeroSerieses.indexOf(s) > -1 &&
s === this.radiusOnSeriesNumber
) {
this.barCtx.radiusOnSeriesNumber -= 1
}
}
for (let s = series.length - 1; s >= 0; s--) {
if (
w.globals.collapsedSeriesIndices.indexOf(
this.barCtx.radiusOnSeriesNumber
) > -1
) {
this.barCtx.radiusOnSeriesNumber -= 1
}
}
}
getXForValue(value, zeroW, zeroPositionForNull = true) {
let xForVal = zeroPositionForNull ? zeroW : null
if (typeof value !== 'undefined' && value !== null) {
xForVal =
zeroW +
value / this.barCtx.invertedYRatio -
(this.barCtx.isReversed ? value / this.barCtx.invertedYRatio : 0) * 2
}
return xForVal
}
getYForValue(value, zeroH, zeroPositionForNull = true) {
let yForVal = zeroPositionForNull ? zeroH : null
if (typeof value !== 'undefined' && value !== null) {
yForVal =
zeroH -
value / this.barCtx.yRatio[this.barCtx.yaxisIndex] +
(this.barCtx.isReversed
? value / this.barCtx.yRatio[this.barCtx.yaxisIndex]
: 0) *
2
}
return yForVal
}
getGoalValues(type, zeroW, zeroH, i, j) {
const w = this.w
let goals = []
if (
w.globals.seriesGoals[i] &&
w.globals.seriesGoals[i][j] &&
Array.isArray(w.globals.seriesGoals[i][j])
) {
w.globals.seriesGoals[i][j].forEach((goal) => {
goals.push({
[type]:
type === 'x'
? this.getXForValue(goal.value, zeroW, false)
: this.getYForValue(goal.value, zeroH, false),
attrs: goal
})
})
}
return goals
}
drawGoalLine({
barXPosition,
barYPosition,
goalX,
goalY,
barWidth,
barHeight
}) {
let graphics = new Graphics(this.barCtx.ctx)
const lineGroup = graphics.group({
className: 'apexcharts-bar-goals-groups'
})
let line = null
if (this.barCtx.isHorizontal) {
if (Array.isArray(goalX)) {
goalX.forEach((goal) => {
let sHeight =
typeof goal.attrs.strokeHeight !== 'undefined'
? goal.attrs.strokeHeight
: barHeight / 2
let y = barYPosition + sHeight + barHeight / 2
line = graphics.drawLine(
goal.x,
y - sHeight * 2,
goal.x,
y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : undefined,
goal.attrs.strokeDashArray,
goal.attrs.strokeWidth ? goal.attrs.strokeWidth : 2,
goal.attrs.strokeLineCap
)
lineGroup.add(line)
})
}
} else {
if (Array.isArray(goalY)) {
goalY.forEach((goal) => {
let sWidth =
typeof goal.attrs.strokeWidth !== 'undefined'
? goal.attrs.strokeWidth
: barWidth / 2
let x = barXPosition + sWidth + barWidth / 2
line = graphics.drawLine(
x - sWidth * 2,
goal.y,
x,
goal.y,
goal.attrs.strokeColor ? goal.attrs.strokeColor : undefined,
goal.attrs.strokeDashArray,
goal.attrs.strokeHeight ? goal.attrs.strokeHeight : 2,
goal.attrs.strokeLineCap
)
lineGroup.add(line)
})
}
}
return lineGroup
}
}
@@ -0,0 +1,30 @@
import Graphics from '../../../modules/Graphics'
export default class CircularChartsHelpers {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
drawYAxisTexts(x, y, i, text) {
const w = this.w
const yaxisConfig = w.config.yaxis[0]
const formatter = w.globals.yLabelFormatters[0]
const graphics = new Graphics(this.ctx)
const yaxisLabel = graphics.drawText({
x: x + yaxisConfig.labels.offsetX,
y: y + yaxisConfig.labels.offsetY,
text: formatter(text, i),
textAnchor: 'middle',
fontSize: yaxisConfig.labels.style.fontSize,
fontFamily: yaxisConfig.labels.style.fontFamily,
foreColor: Array.isArray(yaxisConfig.labels.style.colors)
? yaxisConfig.labels.style.colors[i]
: yaxisConfig.labels.style.colors
})
return yaxisLabel
}
}
@@ -0,0 +1,149 @@
import CoreUtils from '../../../modules/CoreUtils'
import Utils from '../../../utils/Utils'
export default class Helpers {
constructor(lineCtx) {
this.w = lineCtx.w
this.lineCtx = lineCtx
}
sameValueSeriesFix(i, series) {
const w = this.w
if (
w.config.chart.type === 'line' &&
(w.config.fill.type === 'gradient' ||
w.config.fill.type[i] === 'gradient')
) {
const coreUtils = new CoreUtils(this.lineCtx.ctx, w)
// 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
}
}
return series
}
calculatePoints({ series, realIndex, x, y, i, j, prevY }) {
let w = this.w
let ptX = []
let ptY = []
if (j === 0) {
let xPT1st =
this.lineCtx.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) /
this.lineCtx.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, 10) === parseInt(realIndex, 10)
) {
if (gpp.type === 'line') {
this.lineCtx.appendPathFrom = false
pathFromLine = w.globals.previousPaths[pp].paths[0].d
} else if (gpp.type === 'area') {
this.lineCtx.appendPathFrom = false
pathFromArea = w.globals.previousPaths[pp].paths[0].d
if (w.config.stroke.show && w.globals.previousPaths[pp].paths[1]) {
pathFromLine = w.globals.previousPaths[pp].paths[1].d
}
}
}
}
return {
pathFromLine,
pathFromArea
}
}
determineFirstPrevY({ i, series, prevY, 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 = this.lineCtx.prevSeriesY[i - 1][0]
} else {
// the first series will not have prevY values
lineYPosition = this.lineCtx.zeroY
}
} else {
lineYPosition = this.lineCtx.zeroY
}
prevY =
lineYPosition -
series[i][0] / this.lineCtx.yRatio[this.lineCtx.yaxisIndex] +
(this.lineCtx.isReversed
? series[i][0] / this.lineCtx.yRatio[this.lineCtx.yaxisIndex]
: 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 = this.lineCtx.prevSeriesY[s][0]
prevY = lineYPosition
break
}
}
}
}
return {
prevY,
lineYPosition
}
}
}
@@ -0,0 +1,192 @@
import Utils from '../../../utils/Utils'
import Graphics from '../../../modules/Graphics'
import DataLabels from '../../../modules/DataLabels'
export default class TreemapHelpers {
constructor(ctx) {
this.ctx = ctx
this.w = ctx.w
}
checkColorRange() {
const w = this.w
let negRange = false
let chartOpts = w.config.plotOptions[w.config.chart.type]
if (chartOpts.colorScale.ranges.length > 0) {
chartOpts.colorScale.ranges.map((range, index) => {
if (range.from <= 0) {
negRange = true
}
})
}
return negRange
}
getShadeColor(chartType, i, j, negRange) {
const w = this.w
let colorShadePercent = 1
let shadeIntensity = w.config.plotOptions[chartType].shadeIntensity
const colorProps = this.determineColor(chartType, i, j)
if (w.globals.hasNegs || negRange) {
if (w.config.plotOptions[chartType].reverseNegativeShade) {
if (colorProps.percent < 0) {
colorShadePercent =
(colorProps.percent / 100) * (shadeIntensity * 1.25)
} else {
colorShadePercent =
(1 - colorProps.percent / 100) * (shadeIntensity * 1.25)
}
} else {
if (colorProps.percent <= 0) {
colorShadePercent =
1 - (1 + colorProps.percent / 100) * shadeIntensity
} else {
colorShadePercent = (1 - colorProps.percent / 100) * shadeIntensity
}
}
} else {
colorShadePercent = 1 - colorProps.percent / 100
if (chartType === 'treemap') {
colorShadePercent =
(1 - colorProps.percent / 100) * (shadeIntensity * 1.25)
}
}
let color = colorProps.color
let utils = new Utils()
if (w.config.plotOptions[chartType].enableShades) {
if (this.w.config.theme.mode === 'dark') {
color = Utils.hexToRgba(
utils.shadeColor(colorShadePercent * -1, colorProps.color),
w.config.fill.opacity
)
} else {
color = Utils.hexToRgba(
utils.shadeColor(colorShadePercent, colorProps.color),
w.config.fill.opacity
)
}
}
return { color, colorProps }
}
determineColor(chartType, i, j) {
const w = this.w
let val = w.globals.series[i][j]
let chartOpts = w.config.plotOptions[chartType]
let seriesNumber = chartOpts.colorScale.inverse ? j : i
if (chartOpts.distributed && w.config.chart.type === 'treemap') {
seriesNumber = j
}
let color = w.globals.colors[seriesNumber]
let foreColor = null
let min = Math.min(...w.globals.series[i])
let max = Math.max(...w.globals.series[i])
if (!chartOpts.distributed && chartType === 'heatmap') {
min = w.globals.minY
max = w.globals.maxY
}
if (typeof chartOpts.colorScale.min !== 'undefined') {
min =
chartOpts.colorScale.min < w.globals.minY
? chartOpts.colorScale.min
: w.globals.minY
max =
chartOpts.colorScale.max > w.globals.maxY
? chartOpts.colorScale.max
: w.globals.maxY
}
let total = Math.abs(max) + Math.abs(min)
let percent = (100 * val) / (total === 0 ? total - 0.000001 : total)
if (chartOpts.colorScale.ranges.length > 0) {
const colorRange = chartOpts.colorScale.ranges
colorRange.map((range, index) => {
if (val >= range.from && val <= range.to) {
color = range.color
foreColor = range.foreColor ? range.foreColor : null
min = range.from
max = range.to
let rTotal = Math.abs(max) + Math.abs(min)
percent = (100 * val) / (rTotal === 0 ? rTotal - 0.000001 : rTotal)
}
})
}
return {
color,
foreColor,
percent
}
}
calculateDataLabels({ text, x, y, i, j, colorProps, fontSize }) {
let w = this.w
let dataLabelsConfig = w.config.dataLabels
const graphics = new Graphics(this.ctx)
let dataLabels = new DataLabels(this.ctx)
let elDataLabelsWrap = null
if (dataLabelsConfig.enabled) {
elDataLabelsWrap = graphics.group({
class: 'apexcharts-data-labels'
})
const offX = dataLabelsConfig.offsetX
const offY = dataLabelsConfig.offsetY
let dataLabelsX = x + offX
let dataLabelsY =
y + parseFloat(dataLabelsConfig.style.fontSize) / 3 + offY
dataLabels.plotDataLabelsText({
x: dataLabelsX,
y: dataLabelsY,
text,
i,
j,
color: colorProps.foreColor,
parent: elDataLabelsWrap,
fontSize,
dataLabelsConfig
})
}
return elDataLabelsWrap
}
addListeners(elRect) {
const graphics = new Graphics(this.ctx)
elRect.node.addEventListener(
'mouseenter',
graphics.pathMouseEnter.bind(this, elRect)
)
elRect.node.addEventListener(
'mouseleave',
graphics.pathMouseLeave.bind(this, elRect)
)
elRect.node.addEventListener(
'mousedown',
graphics.pathMouseDown.bind(this, elRect)
)
}
}