UNPKG

ikizmet-apexcharts

Version:

iKizmet fork of Apex Charts library

1,049 lines (909 loc) 26.8 kB
import CoreUtils from '../modules/CoreUtils' import Utils from '../utils/Utils' import Fill from '../modules/Fill' import Filters from '../modules/Filters' import Graphics from '../modules/Graphics' import DataLabels from '../modules/DataLabels' /** * 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.xyRatios = xyRatios if (this.xyRatios !== null) { this.xRatio = xyRatios.xRatio 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 } /** 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.initVariables(series) let ret = graphics.group({ class: 'apexcharts-bar-series apexcharts-plot-series' }) if (w.config.dataLabels.enabled) { if (this.totalItems > w.config.plotOptions.bar.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 pathTo, pathFrom 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 ${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 strokeWidth = 0 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.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' }) for ( let j = 0, tj = w.globals.dataPoints; j < w.globals.dataPoints; j++, tj-- ) { if (typeof this.series[i][j] === 'undefined' || series[i][j] === null) { this.isNullValue = true } else { this.isNullValue = false } if (w.config.stroke.show) { if (this.isNullValue) { strokeWidth = 0 } else { strokeWidth = Array.isArray(this.strokeWidth) ? this.strokeWidth[realIndex] : this.strokeWidth } } let paths = null if (this.isHorizontal) { paths = this.drawBarPaths({ indexes: { i, j, realIndex, bc }, barHeight, strokeWidth, pathTo, pathFrom, zeroW, x, y, yDivision, elSeries }) } else { paths = this.drawColumnPaths({ indexes: { i, j, realIndex, bc }, x, y, xDivision, pathTo, pathFrom, barWidth, zeroH, strokeWidth, elSeries }) } pathTo = paths.pathTo pathFrom = paths.pathFrom y = paths.y x = paths.x // push current X if (j > 0) { xArrj.push(x + barWidth / 2) } yArrj.push(y) let pathFill = this.getPathFillColor(series, i, j, realIndex) elSeries = this.renderSeries({ realIndex, pathFill, j, i, pathFrom, pathTo, strokeWidth, elSeries, x, y, series, barHeight, barWidth, elDataLabelsWrap, 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 } getPathFillColor(series, i, j, realIndex) { const w = this.w let fill = new Fill(this.ctx) let fillColor = null let seriesNumber = this.barOptions.distributed ? j : i if (this.barOptions.colors.ranges.length > 0) { const colorRange = this.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.barOptions.distributed ? seriesNumber : realIndex, color: fillColor }) return pathFill } renderSeries({ realIndex, pathFill, lineFill, j, i, pathFrom, pathTo, strokeWidth, elSeries, x, y, series, barHeight, barWidth, elDataLabelsWrap, 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: pathFrom, pathTo: 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`, id: `apexcharts-${type}-area` }) renderedPath.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`) const filters = new Filters(this.ctx) filters.setSelectionFilter(renderedPath, realIndex, j) elSeries.add(renderedPath) let dataLabels = this.calculateDataLabelsPos({ x, y, i, j, series, realIndex, barHeight, barWidth, renderedPath, visibleSeries }) if (dataLabels !== null) { elDataLabelsWrap.add(dataLabels) } elSeries.add(elDataLabelsWrap) return elSeries } initVariables(series) { const w = this.w this.series = series this.totalItems = 0 this.seriesLen = 0 this.visibleI = -1 this.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.seriesLen = this.seriesLen + 1 this.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.visibleItems++ } } } else { this.visibleItems = w.globals.dataPoints } } if (this.seriesLen === 0) { // A small adjustment when combo charts are used this.seriesLen = 1 } } initialPositions() { let w = this.w let x, y, yDivision, xDivision, barHeight, barWidth, zeroH, zeroW if (this.isHorizontal) { // height divided into equal parts yDivision = w.globals.gridHeight / w.globals.dataPoints barHeight = yDivision / this.seriesLen if (w.globals.isXNumeric) { yDivision = w.globals.gridHeight / this.totalItems barHeight = yDivision / this.seriesLen } barHeight = (barHeight * parseInt(this.barOptions.barHeight)) / 100 zeroW = this.baseLineInvertedY + w.globals.padHorizontal + (this.isReversed ? w.globals.gridWidth : 0) - (this.isReversed ? this.baseLineInvertedY * 2 : 0) y = (yDivision - barHeight * this.seriesLen) / 2 } else { // width divided into equal parts xDivision = w.globals.gridWidth / this.visibleItems barWidth = ((xDivision / this.seriesLen) * parseInt(this.barOptions.columnWidth)) / 100 if (w.globals.isXNumeric) { // max barwidth should be equal to minXDiff to avoid overlap xDivision = w.globals.minXDiff / this.xRatio barWidth = ((xDivision / this.seriesLen) * parseInt(this.barOptions.columnWidth)) / 100 } zeroH = w.globals.gridHeight - this.baseLineY[this.yaxisIndex] - (this.isReversed ? w.globals.gridHeight : 0) + (this.isReversed ? this.baseLineY[this.yaxisIndex] * 2 : 0) x = w.globals.padHorizontal + (xDivision - barWidth * this.seriesLen) / 2 } return { x, y, yDivision, xDivision, barHeight, barWidth, zeroH, zeroW } } drawBarPaths({ indexes, barHeight, strokeWidth, pathTo, pathFrom, zeroW, x, y, yDivision, elSeries }) { let w = this.w let graphics = new Graphics(this.ctx) let i = indexes.i let j = indexes.j let realIndex = indexes.realIndex let bc = indexes.bc if (w.globals.isXNumeric) { y = (w.globals.seriesX[i][j] - w.globals.minX) / this.invertedXRatio - barHeight } let barYPosition = y + barHeight * this.visibleI pathTo = graphics.move(zeroW, barYPosition) pathFrom = graphics.move(zeroW, barYPosition) if (w.globals.previousPaths.length > 0) { pathFrom = this.getPathFrom(realIndex, j) } if ( typeof this.series[i][j] === 'undefined' || this.series[i][j] === null ) { x = zeroW } else { x = zeroW + this.series[i][j] / this.invertedYRatio - (this.isReversed ? this.series[i][j] / this.invertedYRatio : 0) * 2 } let endingShapeOpts = { barHeight, strokeWidth, barYPosition, x, zeroW } let endingShape = this.barEndingShape(w, endingShapeOpts, this.series, i, j) pathTo = pathTo + graphics.line(endingShape.newX, barYPosition) + endingShape.path + graphics.line(zeroW, barYPosition + barHeight - strokeWidth) + graphics.line(zeroW, barYPosition) pathFrom = pathFrom + graphics.line(zeroW, barYPosition) + endingShape.ending_p_from + graphics.line(zeroW, barYPosition + barHeight - strokeWidth) + graphics.line(zeroW, barYPosition + barHeight - strokeWidth) + graphics.line(zeroW, barYPosition) if (!w.globals.isXNumeric) { y = y + yDivision } if (this.barOptions.colors.backgroundBarColors.length > 0 && i === 0) { if (bc >= this.barOptions.colors.backgroundBarColors.length) { bc = 0 } let bcolor = this.barOptions.colors.backgroundBarColors[bc] let rect = graphics.drawRect( 0, barYPosition - barHeight * this.visibleI, w.globals.gridWidth, barHeight * this.seriesLen, 0, bcolor, this.barOptions.colors.backgroundBarOpacity ) elSeries.add(rect) rect.node.classList.add('apexcharts-backgroundBar') } return { pathTo, pathFrom, x, y, barYPosition } } drawColumnPaths({ indexes, x, y, xDivision, pathTo, pathFrom, barWidth, zeroH, strokeWidth, elSeries }) { let w = this.w let graphics = new Graphics(this.ctx) let i = indexes.i let j = indexes.j let realIndex = indexes.realIndex let bc = indexes.bc if (w.globals.isXNumeric) { x = (w.globals.seriesX[i][j] - w.globals.minX) / this.xRatio - barWidth / 2 } let barXPosition = x + barWidth * this.visibleI pathTo = graphics.move(barXPosition, zeroH) pathFrom = graphics.move(barXPosition, zeroH) if (w.globals.previousPaths.length > 0) { pathFrom = this.getPathFrom(realIndex, j) } if ( typeof this.series[i][j] === 'undefined' || this.series[i][j] === null ) { y = zeroH } else { y = zeroH - this.series[i][j] / this.yRatio[this.yaxisIndex] + (this.isReversed ? this.series[i][j] / this.yRatio[this.yaxisIndex] : 0) * 2 } let endingShapeOpts = { barWidth, strokeWidth, barXPosition, y, zeroH } let endingShape = this.barEndingShape(w, endingShapeOpts, this.series, i, j) pathTo = pathTo + graphics.line(barXPosition, endingShape.newY) + endingShape.path + graphics.line(barXPosition + barWidth - strokeWidth, zeroH) + graphics.line(barXPosition - strokeWidth / 2, zeroH) pathFrom = pathFrom + graphics.line(barXPosition, zeroH) + endingShape.ending_p_from + graphics.line(barXPosition + barWidth - strokeWidth, zeroH) + graphics.line(barXPosition + barWidth - strokeWidth, zeroH) + graphics.line(barXPosition - strokeWidth / 2, zeroH) if (!w.globals.isXNumeric) { x = x + xDivision } if (this.barOptions.colors.backgroundBarColors.length > 0 && i === 0) { if (bc >= this.barOptions.colors.backgroundBarColors.length) { bc = 0 } let bcolor = this.barOptions.colors.backgroundBarColors[bc] let rect = graphics.drawRect( barXPosition - barWidth * this.visibleI, 0, barWidth * this.seriesLen, w.globals.gridHeight, 0, bcolor, this.barOptions.colors.backgroundBarOpacity ) elSeries.add(rect) rect.node.classList.add('apexcharts-backgroundBar') } return { pathTo, pathFrom, x, y, barXPosition } } /** getPathFrom 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 **/ getPathFrom(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.length > 0 && parseInt(gpp.realIndex) === parseInt(realIndex) ) { if (typeof w.globals.previousPaths[pp].paths[j] !== 'undefined') { pathFrom = w.globals.previousPaths[pp].paths[j].d } } } return pathFrom } /** calculateBarDataLabels is used to calculate the positions for the data-labels * It also sets the element's data attr for bars and calls drawCalculatedBarDataLabels() * @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 **/ calculateDataLabelsPos({ x, y, i, j, realIndex, series, barHeight, barWidth, visibleSeries, renderedPath }) { let w = this.w let graphics = new Graphics(this.ctx) let strokeWidth = Array.isArray(this.strokeWidth) ? this.strokeWidth[realIndex] : this.strokeWidth let bcx = x + parseFloat(barWidth * visibleSeries) let bcy = y + parseFloat(barHeight * visibleSeries) if (w.globals.isXNumeric) { bcx = x + parseFloat(barWidth * (visibleSeries + 1)) - strokeWidth 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.barOptions.dataLabels const offX = dataLabelsConfig.offsetX const offY = dataLabelsConfig.offsetY let textRects = { width: 0, height: 0 } if (w.config.dataLabels.enabled) { textRects = graphics.getTextRects( w.globals.yLabelFormatters[0](w.globals.maxY), parseInt(dataLabelsConfig.style.fontSize) ) } if (this.isHorizontal) { dataLabelsPos = this.calculateBarsDataLabelsPosition({ x, y, i, j, renderedPath, bcy, barHeight, textRects, strokeWidth, dataLabelsX, dataLabelsY, barDataLabelsConfig, offX, offY }) } else { dataLabelsPos = this.calculateColumnsDataLabelsPosition({ x, y, i, j, renderedPath, realIndex, bcx, bcy, barHeight, barWidth, textRects, strokeWidth, dataLabelsY, barDataLabelsConfig, offX, offY }) } renderedPath.attr({ cy: dataLabelsPos.bcy, cx: dataLabelsPos.bcx, j: j, val: series[i][j], barHeight: barHeight, barWidth: barWidth }) dataLabels = this.drawCalculatedDataLabels({ x: dataLabelsPos.dataLabelsX, y: dataLabelsPos.dataLabelsY, val: series[i][j], i: realIndex, j: j, dataLabelsConfig }) return dataLabels } calculateColumnsDataLabelsPosition(opts) { const w = this.w let { i, j, y, bcx, barWidth, textRects, dataLabelsY, barDataLabelsConfig, strokeWidth, offX, offY } = opts let dataLabelsX let barHeight = this.series[i][j] / this.yRatio[this.yaxisIndex] let dataPointsDividedWidth = w.globals.gridWidth / w.globals.dataPoints bcx = bcx - strokeWidth / 2 if (w.globals.isXNumeric) { dataLabelsX = bcx - barWidth / 2 + offX } else { dataLabelsX = bcx - dataPointsDividedWidth + barWidth / 2 + offX } let valIsNegative = this.series[i][j] <= 0 if (this.isReversed) { y = y - barHeight } switch (barDataLabelsConfig.position) { case 'center': if (valIsNegative) { dataLabelsY = y + barHeight / 2 + textRects.height / 2 + offY } else { dataLabelsY = y + barHeight / 2 + textRects.height / 2 - offY } break case 'bottom': if (valIsNegative) { dataLabelsY = y + barHeight + textRects.height + strokeWidth + offY } else { dataLabelsY = y + barHeight - textRects.height / 2 + strokeWidth - offY } break case 'top': if (valIsNegative) { dataLabelsY = y - textRects.height / 2 - offY } else { dataLabelsY = y + textRects.height + offY } break } return { bcx, bcy: y, dataLabelsX, dataLabelsY } } calculateBarsDataLabelsPosition(opts) { const w = this.w let { x, i, j, bcy, barHeight, textRects, dataLabelsX, strokeWidth, barDataLabelsConfig, offX, offY } = opts let dataPointsDividedHeight = w.globals.gridHeight / w.globals.dataPoints let dataLabelsY = bcy - dataPointsDividedHeight + barHeight / 2 + textRects.height / 2 + offY - 3 let barWidth = this.series[i][j] / this.invertedYRatio let valIsNegative = this.series[i][j] <= 0 if (this.isReversed) { x = x + barWidth } switch (barDataLabelsConfig.position) { case 'center': if (valIsNegative) { dataLabelsX = x - barWidth / 2 - offX } else { dataLabelsX = x - barWidth / 2 + offX } break case 'bottom': if (valIsNegative) { dataLabelsX = x - barWidth - strokeWidth - Math.round(textRects.width / 2) - offX } else { dataLabelsX = x - barWidth + strokeWidth + Math.round(textRects.width / 2) + offX } break case 'top': if (valIsNegative) { dataLabelsX = x - strokeWidth + Math.round(textRects.width / 2) - offX } else { dataLabelsX = x - strokeWidth - Math.round(textRects.width / 2) + offX } break } if (dataLabelsX < 0) { dataLabelsX = textRects.width + strokeWidth } else if (dataLabelsX + textRects.width / 2 > w.globals.gridWidth) { dataLabelsX = dataLabelsX - textRects.width - strokeWidth } return { bcx: x, bcy, dataLabelsX, dataLabelsY } } drawCalculatedDataLabels({ x, y, val, i, j, dataLabelsConfig }) { const w = this.w const dataLabels = new DataLabels(this.ctx) const graphics = new Graphics(this.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' }) let text = '' if (typeof val !== 'undefined' && val !== null) { text = formatter(val, { seriesIndex: i, dataPointIndex: j, w }) } dataLabels.plotDataLabelsText({ x, y, text, i, j, parent: elDataLabelsWrap, dataLabelsConfig, alwaysDrawDataLabel: true, offsetCorrection: true }) } return elDataLabelsWrap } /** barEndingShape draws the various shapes on top of 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} path - ending shape whether round/arrow * ending_p_from - similar to pathFrom * newY - which is calculated from existing y and new shape's top **/ barEndingShape(w, opts, series, i, j) { let graphics = new Graphics(this.ctx) if (this.isHorizontal) { let endingShape = null let endingShapeFrom = '' let x = opts.x if (typeof series[i][j] !== 'undefined' || series[i][j] !== null) { let inverse = series[i][j] < 0 let eX = opts.barHeight / 2 - opts.strokeWidth if (inverse) eX = -opts.barHeight / 2 - opts.strokeWidth if (!w.config.chart.stacked) { if (this.barOptions.endingShape === 'rounded') { x = opts.x - eX / 2 } } switch (this.barOptions.endingShape) { case 'flat': endingShape = graphics.line( x, opts.barYPosition + opts.barHeight - opts.strokeWidth ) break case 'rounded': endingShape = graphics.quadraticCurve( x + eX, opts.barYPosition + (opts.barHeight - opts.strokeWidth) / 2, x, opts.barYPosition + opts.barHeight - opts.strokeWidth ) break } } return { path: endingShape, ending_p_from: endingShapeFrom, newX: x } } else { let endingShape = null let endingShapeFrom = '' let y = opts.y if (typeof series[i][j] !== 'undefined' || series[i][j] !== null) { let inverse = series[i][j] < 0 let eY = opts.barWidth / 2 - opts.strokeWidth if (inverse) eY = -opts.barWidth / 2 - opts.strokeWidth if (!w.config.chart.stacked) { // the shape exceeds the chart height, hence reduce y if (this.barOptions.endingShape === 'rounded') { y = y + eY / 2 } } switch (this.barOptions.endingShape) { case 'flat': endingShape = graphics.line( opts.barXPosition + opts.barWidth - opts.strokeWidth, y ) break case 'rounded': endingShape = graphics.quadraticCurve( opts.barXPosition + (opts.barWidth - opts.strokeWidth) / 2, y - eY, opts.barXPosition + opts.barWidth - opts.strokeWidth, y ) break } } return { path: endingShape, ending_p_from: endingShapeFrom, newY: y } } } } export default Bar