UNPKG

apexcharts

Version:

A JavaScript Chart Library

879 lines (755 loc) 25.3 kB
import Bar from '../charts/Bar' import BarStacked from '../charts/BarStacked' import CandleStick from '../charts/CandleStick' import CoreUtils from './CoreUtils' import Crosshairs from './Crosshairs' import DateTime from './../utils/DateTime' import HeatMap from '../charts/HeatMap' import Pie from '../charts/Pie' import Radar from '../charts/Radar' import Radial from '../charts/Radial' import Line from '../charts/Line' import Graphics from './Graphics' import XAxis from './axes/XAxis' import YAxis from './axes/YAxis' import Range from './Range' import Utils from '../utils/Utils' import Series from './Series' import TimeScale from './TimeScale' /** * ApexCharts Core Class responsible for major calculations and creating elements. * * @module Core **/ export default class Core { constructor (el, ctx) { this.ctx = ctx this.w = ctx.w this.el = el this.coreUtils = new CoreUtils(this.ctx) this.twoDSeries = [] this.threeDSeries = [] this.twoDSeriesX = [] } // get data and store into appropriate vars setupElements () { let gl = this.w.globals let cnf = this.w.config // const graphics = new Graphics(this.ctx) let ct = cnf.chart.type let axisChartsArrTypes = [ 'line', 'area', 'bar', 'candlestick', 'radar', 'scatter', 'bubble', 'heatmap' ] let xyChartsArrTypes = [ 'line', 'area', 'bar', 'candlestick', 'scatter', 'bubble' ] gl.axisCharts = axisChartsArrTypes.indexOf(ct) > -1 gl.xyCharts = xyChartsArrTypes.indexOf(ct) > -1 gl.chartClass = '.apexcharts' + gl.cuid gl.dom.baseEl = this.el gl.dom.elWrap = document.createElement('div') Graphics.setAttrs(gl.dom.elWrap, { id: gl.chartClass.substring(1), class: 'apexcharts-canvas ' + gl.chartClass.substring(1) }) this.el.appendChild(gl.dom.elWrap) gl.dom.Paper = new window.SVG.Doc(gl.dom.elWrap) gl.dom.Paper.attr({ class: 'apexcharts-svg', 'xmlns:data': 'ApexChartsNS', transform: `translate(${cnf.chart.offsetX}, ${cnf.chart.offsetY})` }) gl.dom.Paper.node.style.background = cnf.chart.background this.setSVGDimensions() gl.dom.elGraphical = gl.dom.Paper.group().attr({ class: 'apexcharts-inner apexcharts-graphical' }) gl.dom.elDefs = gl.dom.Paper.defs() gl.dom.elLegendWrap = document.createElement('div') gl.dom.elLegendWrap.classList.add('apexcharts-legend') gl.dom.elWrap.appendChild(gl.dom.elLegendWrap) // gl.dom.Paper.add(gl.dom.elLegendWrap) gl.dom.Paper.add(gl.dom.elGraphical) gl.dom.elGraphical.add(gl.dom.elDefs) } plotChartType (ser, xyRatios) { const w = this.w const cnf = w.config const gl = w.globals let lineSeries = { series: [], i: [] } let areaSeries = { series: [], i: [] } let scatterSeries = { series: [], i: [] } let columnSeries = { series: [], i: [] } let candlestickSeries = { series: [], i: [] } gl.series.map((series, st) => { // if user has specified a particular type for particular series if (typeof ser[st].type !== 'undefined') { if (ser[st].type === 'column' || ser[st].type === 'bar') { w.config.plotOptions.bar.horizontal = false // bar not supported in mixed charts columnSeries.series.push(series) columnSeries.i.push(st) } else if (ser[st].type === 'area') { areaSeries.series.push(series) areaSeries.i.push(st) } else if (ser[st].type === 'line') { lineSeries.series.push(series) lineSeries.i.push(st) } else if (ser[st].type === 'scatter') { scatterSeries.series.push(series) scatterSeries.i.push(st) } else if (ser[st].type === 'candlestick') { candlestickSeries.series.push(series) candlestickSeries.i.push(st) } else { // user has specified type, but it is not valid (other than line/area/column) throw new Error('You have specified an unrecognized chart type. Available types for this propery are line/area/column/bar') } gl.comboCharts = true } else { lineSeries.series.push(series) lineSeries.i.push(st) } }) let line = new Line(this.ctx, xyRatios) let candlestick = new CandleStick(this.ctx, xyRatios) let pie = new Pie(this.ctx) let radialBar = new Radial(this.ctx) let radar = new Radar(this.ctx) let elGraph = [] if (gl.comboCharts) { if (areaSeries.series.length > 0) { elGraph.push( line.draw(areaSeries.series, 'area', areaSeries.i) ) } if (columnSeries.series.length > 0) { if (w.config.chart.stacked) { let barStacked = new BarStacked(this.ctx, xyRatios) elGraph.push(barStacked.draw(columnSeries.series, columnSeries.i)) } else { let bar = new Bar(this.ctx, xyRatios) elGraph.push(bar.draw(columnSeries.series, columnSeries.i)) } } if (lineSeries.series.length > 0) { elGraph.push( line.draw(lineSeries.series, 'line', lineSeries.i) ) } if (candlestickSeries.series.length > 0) { elGraph.push( candlestick.draw(candlestickSeries.series, candlestickSeries.i) ) } if (scatterSeries.series.length > 0) { const scatterLine = new Line(this.ctx, xyRatios, true) elGraph.push( scatterLine.draw(scatterSeries.series, 'scatter', scatterSeries.i) ) } } else { switch (cnf.chart.type) { case 'line': elGraph = line.draw(gl.series, 'line') break case 'area': elGraph = line.draw(gl.series, 'area') break case 'bar': if (cnf.chart.stacked) { let barStacked = new BarStacked(this.ctx, xyRatios) elGraph = barStacked.draw(gl.series) } else { let bar = new Bar(this.ctx, xyRatios) elGraph = bar.draw(gl.series) } break case 'candlestick': let candleStick = new CandleStick(this.ctx, xyRatios) elGraph = candleStick.draw(gl.series) break case 'heatmap': let heatmap = new HeatMap(this.ctx, xyRatios) elGraph = heatmap.draw(gl.series) break case 'pie': case 'donut': elGraph = pie.draw(gl.series) break case 'radialBar': elGraph = radialBar.draw(gl.series) break case 'radar': elGraph = radar.draw(gl.series) break default: elGraph = line.draw(gl.series) } } return elGraph } setSVGDimensions () { let gl = this.w.globals let cnf = this.w.config gl.svgWidth = cnf.chart.width gl.svgHeight = cnf.chart.height let elDim = Utils.getDimensions(this.el) let widthUnit = cnf.chart.width.toString().split(/[0-9]+/g).pop() if (widthUnit === '%') { if (Utils.isNumber(elDim[0])) { if (elDim[0].width === 0) { elDim = Utils.getDimensions(this.el.parentNode) } gl.svgWidth = (elDim[0] * parseInt(cnf.chart.width) / 100) } } else if (widthUnit === 'px' || widthUnit === '') { gl.svgWidth = parseInt(cnf.chart.width) } if (gl.svgHeight !== 'auto' && gl.svgHeight !== '') { let heightUnit = cnf.chart.height.toString().split(/[0-9]+/g).pop() if (heightUnit === '%') { let elParentDim = Utils.getDimensions(this.el.parentNode) gl.svgHeight = elParentDim[1] * parseInt(cnf.chart.height) / 100 } else { gl.svgHeight = parseInt(cnf.chart.height) } } else { if (gl.axisCharts) { gl.svgHeight = gl.svgWidth / 1.61 } else { gl.svgHeight = gl.svgWidth } } Graphics.setAttrs(gl.dom.Paper.node, { width: gl.svgWidth, height: gl.svgHeight }) // gl.dom.Paper.node.parentNode.parentNode.style.minWidth = gl.svgWidth + "px"; let offsetY = cnf.chart.sparkline.enabled ? 0 : gl.axisCharts ? 14 : 5 gl.dom.Paper.node.parentNode.parentNode.style.minHeight = gl.svgHeight + offsetY + 'px' gl.dom.elWrap.style.width = gl.svgWidth + 'px' gl.dom.elWrap.style.height = gl.svgHeight + 'px' } shiftGraphPosition () { let gl = this.w.globals let tY = gl.translateY let tX = gl.translateX let scalingAttrs = { transform: 'translate(' + tX + ', ' + tY + ')' } Graphics.setAttrs(gl.dom.elGraphical.node, scalingAttrs) } /* ** All the calculations for setting range in charts will be done here */ coreCalculations () { const range = new Range(this.ctx) range.init() } resetGlobals () { let gl = this.w.globals gl.series = [] gl.seriesCandleO = [] gl.seriesCandleH = [] gl.seriesCandleL = [] gl.seriesCandleC = [] gl.seriesPercent = [] gl.seriesX = [] gl.seriesZ = [] gl.seriesNames = [] gl.seriesTotals = [] gl.stackedSeriesTotals = [] gl.labels = [] gl.timelineLabels = [] gl.noLabelsProvided = false gl.timescaleTicks = [] gl.resizeTimer = null gl.selectionResizeTimer = null gl.seriesXvalues = (() => { return this.w.config.series.map((s) => { return [] }) })() gl.seriesYvalues = (() => { return this.w.config.series.map((s) => { return [] }) })() gl.delayedElements = [] gl.pointsArray = [] gl.dataLabelsRects = [] gl.isXNumeric = false gl.isDataXYZ = false gl.maxY = -Number.MAX_VALUE gl.minY = Number.MIN_VALUE gl.minYArr = [] gl.maxYArr = [] gl.maxX = -Number.MAX_VALUE gl.minX = Number.MAX_VALUE gl.initialmaxX = -Number.MAX_VALUE gl.initialminX = Number.MAX_VALUE gl.maxDate = 0 gl.minDate = Number.MAX_VALUE gl.minZ = Number.MAX_VALUE gl.maxZ = -Number.MAX_VALUE gl.yAxisScale = [] gl.xAxisScale = null gl.xAxisTicksPositions = [] gl.yLabelsCoords = [] gl.yTitleCoords = [] gl.xRange = 0 gl.yRange = [] gl.zRange = 0 gl.dataPoints = 0 } isMultipleY () { // user has supplied an array in yaxis property. So, turn on multipleYAxis flag if (this.w.config.yaxis.constructor === Array && this.w.config.yaxis.length > 1) { // first, turn off stacking if multiple y axis this.w.config.chart.stacked = false this.w.globals.isMultipleYAxis = true return true } } excludeCollapsedSeriesInYAxis () { const w = this.w w.globals.ignoreYAxisIndexes = w.globals.collapsedSeries.map((collapsed, i) => { if (this.w.globals.isMultipleYAxis) { return collapsed.index } }) } isMultiFormat () { return this.isFormatXY() || this.isFormat2DArray() } // given format is [{x, y}, {x, y}] isFormatXY () { const series = this.w.config.series.slice() const sr = new Series(this.ctx) const activeSeriesIndex = sr.getActiveConfigSeriesIndex() if (typeof series[activeSeriesIndex].data !== 'undefined' && series[activeSeriesIndex].data.length > 0 && series[activeSeriesIndex].data[0] !== null && typeof series[activeSeriesIndex].data[0].x !== 'undefined' && series[activeSeriesIndex].data[0] !== null) { return true } } // given format is [[x, y], [x, y]] isFormat2DArray () { const series = this.w.config.series.slice() const sr = new Series(this.ctx) const activeSeriesIndex = sr.getActiveConfigSeriesIndex() if (typeof series[activeSeriesIndex].data !== 'undefined' && series[activeSeriesIndex].data.length > 0 && typeof series[activeSeriesIndex].data[0] !== 'undefined' && series[activeSeriesIndex].data[0] !== null && series[activeSeriesIndex].data[0].constructor === Array) { return true } } handleFormat2DArray (ser, i) { const cnf = this.w.config const gl = this.w.globals for (let j = 0; j < ser[i].data.length; j++) { if (typeof ser[i].data[j][1] !== 'undefined') { if (Array.isArray(ser[i].data[j][1]) && ser[i].data[j][1].length === 4) { this.twoDSeries.push(ser[i].data[j][1][3]) } else { this.twoDSeries.push(ser[i].data[j][1]) } } if (cnf.xaxis.type === 'datetime') { // if timestamps are provided and xaxis type is datettime, let ts = new Date(ser[i].data[j][0]) ts = new Date(ts).getTime() this.twoDSeriesX.push(ts) } else { this.twoDSeriesX.push(ser[i].data[j][0]) } } for (let j = 0; j < ser[i].data.length; j++) { if (typeof ser[i].data[j][2] !== 'undefined') { this.threeDSeries.push(ser[i].data[j][2]) gl.isDataXYZ = true } } } handleFormatXY (ser, i) { const cnf = this.w.config const gl = this.w.globals const series = this.w.config.series.slice() const dt = new DateTime(this.ctx) for (let j = 0; j < ser[i].data.length; j++) { if (typeof ser[i].data[j].y !== 'undefined') { if (Array.isArray(ser[i].data[j].y) && ser[i].data[j].y.length === 4) { this.twoDSeries.push(ser[i].data[j].y[3]) } else { this.twoDSeries.push(ser[i].data[j].y) } } const isXString = typeof ser[i].data[j].x === 'string' const isXDate = !!dt.isValidDate(ser[i].data[j].x.toString()) if (isXString || isXDate) { // user supplied '01/01/2017' or a date string (a JS date object is not supported) if (isXString) { if (cnf.xaxis.type === 'datetime') { this.twoDSeriesX.push(dt.parseDate(ser[i].data[j].x)) } else { // a category and not a numeric x value this.fallbackToCategory = true this.twoDSeriesX.push((ser[i].data[j].x)) } } else { if (cnf.xaxis.type === 'datetime') { this.twoDSeriesX.push(dt.parseDate(ser[i].data[j].x.toString())) } else { this.twoDSeriesX.push(parseFloat(ser[i].data[j].x)) } } } else { // a numeric value in x property this.twoDSeriesX.push(ser[i].data[j].x) } } if (series[i].data[0] && typeof series[i].data[0].z !== 'undefined') { for (let t = 0; t < series[i].data.length; t++) { this.threeDSeries.push(series[i].data[t].z) } gl.isDataXYZ = true } } handleCandleStickData (ser, i) { const gl = this.w.globals let ohlc = {} if (this.isFormat2DArray()) { ohlc = this.handleCandleStickDataFormat('array', ser, i) } else if (this.isFormatXY()) { ohlc = this.handleCandleStickDataFormat('xy', ser, i) } gl.seriesCandleO.push(ohlc.o) gl.seriesCandleH.push(ohlc.h) gl.seriesCandleL.push(ohlc.l) gl.seriesCandleC.push(ohlc.c) return ohlc } handleCandleStickDataFormat (format, ser, i) { const serO = [] const serH = [] const serL = [] const serC = [] const err = 'Please provide [Open, High, Low and Close] values in valid format. Read more https://apexcharts.com/docs/series/#candlestick' if (format === 'array') { if (ser[i].data[0][1].length !== 4) { throw new Error(err) } for (let j = 0; j < ser[i].data.length; j++) { serO.push(ser[i].data[j][1][0]) serH.push(ser[i].data[j][1][1]) serL.push(ser[i].data[j][1][2]) serC.push(ser[i].data[j][1][3]) } } else if (format === 'xy') { if (ser[i].data[0].y.length !== 4) { throw new Error(err) } for (let j = 0; j < ser[i].data.length; j++) { serO.push(ser[i].data[j].y[0]) serH.push(ser[i].data[j].y[1]) serL.push(ser[i].data[j].y[2]) serC.push(ser[i].data[j].y[3]) } } return { o: serO, h: serH, l: serL, c: serC } } parseDataAxisCharts (ser, series, ctx = this.ctx) { const cnf = this.w.config const gl = this.w.globals const dt = new DateTime(ctx) for (let i = 0; i < series.length; i++) { this.twoDSeries = [] this.twoDSeriesX = [] this.threeDSeries = [] if (typeof series[i].data === 'undefined') { console.error("It is a possibility that you may have not included 'data' property in series.") return } if (this.isMultiFormat()) { if (this.isFormat2DArray()) { this.handleFormat2DArray(ser, i) } else if (this.isFormatXY()) { this.handleFormatXY(ser, i) } if (cnf.chart.type === 'candlestick' || ser[i].type === 'candlestick') { this.handleCandleStickData(ser, i) } gl.series.push(this.twoDSeries) gl.labels.push(this.twoDSeriesX) gl.seriesX.push(this.twoDSeriesX) if (!this.fallbackToCategory) { gl.isXNumeric = true } } else { if (cnf.xaxis.type === 'datetime') { // user didn't supplied [{x,y}] or [[x,y]], but single array in data. // Also labels/categories were supplied differently gl.isXNumeric = true const dates = cnf.labels.length > 0 ? cnf.labels.slice() : cnf.xaxis.categories.slice() for (let j = 0; j < dates.length; j++) { if (typeof (dates[j]) === 'string') { let isDate = dt.isValidDate(dates[j]) if (isDate) { this.twoDSeriesX.push(dt.parseDate(dates[j])) } else { throw new Error('You have provided invalid Date format. Please provide a valid JavaScript Date') } } } gl.seriesX.push(this.twoDSeriesX) } else if (cnf.xaxis.type === 'numeric') { gl.isXNumeric = true let x = cnf.labels.length > 0 ? cnf.labels.slice() : cnf.xaxis.categories.slice() if (x.length > 0) { this.twoDSeriesX = x gl.seriesX.push(this.twoDSeriesX) } } gl.labels.push(this.twoDSeriesX) gl.series.push(ser[i].data) } gl.seriesZ.push(this.threeDSeries) // gl.series.push(ser[i].data) if (ser[i].name !== undefined) { gl.seriesNames.push(ser[i].name) } else { gl.seriesNames.push('series-' + parseInt(i + 1)) } } return this.w } parseDataNonAxisCharts (ser) { const gl = this.w.globals const cnf = this.w.config gl.series = ser.slice() gl.seriesNames = cnf.labels.slice() for (let i = 0; i < gl.series.length; i++) { if (gl.seriesNames[i] === undefined) { gl.seriesNames.push('series-' + (i + 1)) } } return this.w } handleExternalLabelsData (ser) { const cnf = this.w.config const gl = this.w.globals // user provided labels in category axis if (cnf.xaxis.categories.length > 0) { gl.labels = cnf.xaxis.categories } else if (cnf.labels.length > 0) { gl.labels = cnf.labels.slice() } else if (this.fallbackToCategory) { gl.labels = gl.labels[0] } else { // user didn't provided labels, fallback to 1-2-3-4-5 let labelArr = [] if (gl.axisCharts) { for (let i = 0; i < gl.series[gl.maxValsInArrayIndex].length; i++) { labelArr.push(i + 1) } for (let i = 0; i < ser.length; i++) { gl.seriesX.push(labelArr) } gl.isXNumeric = true } // no series to pull labels from, put a 0-10 series if (labelArr.length === 0) { labelArr = [0, 10] for (let i = 0; i < ser.length; i++) { gl.seriesX.push(labelArr) } } gl.labels = labelArr gl.noLabelsProvided = true if (cnf.xaxis.type === 'category') { gl.isXNumeric = false } } } // Segregate user provided data into appropriate vars parseData (ser) { let w = this.w let cnf = w.config let gl = w.globals this.excludeCollapsedSeriesInYAxis() // to determine whether data is in XY format or array format, we use original config const configSeries = cnf.series.slice() this.fallbackToCategory = false this.resetGlobals() this.isMultipleY() if (gl.axisCharts) { this.parseDataAxisCharts(ser, configSeries) } else { this.parseDataNonAxisCharts(ser) } this.coreUtils.getLargestSeries() // set Null values to 0 in all series when user hides/shows some series if (cnf.chart.type === 'bar' && cnf.chart.stacked) { const series = new Series(this.ctx) gl.series = series.setNullSeriesToZeroValues(gl.series) } this.coreUtils.getSeriesTotals() if (gl.axisCharts) { this.coreUtils.getStackedSeriesTotals() } this.coreUtils.getPercentSeries() // user didn't provide a [[x,y],[x,y]] series, but a named series if (!gl.isXNumeric || (cnf.xaxis.type === 'numeric' && cnf.labels.length === 0 && cnf.xaxis.categories.length === 0)) { this.handleExternalLabelsData(ser) } } xySettings () { let xyRatios = null const w = this.w if (w.globals.axisCharts) { if (w.config.xaxis.crosshairs.position === 'back') { const crosshairs = new Crosshairs(this.ctx) crosshairs.drawXCrosshairs() } if (w.config.yaxis[0].crosshairs.position === 'back') { const crosshairs = new Crosshairs(this.ctx) crosshairs.drawYCrosshairs() } xyRatios = this.coreUtils.getCalculatedRatios() if (w.config.xaxis.type === 'datetime' && w.config.xaxis.labels.formatter === undefined && isFinite(w.globals.minX) && isFinite(w.globals.maxX)) { let ts = new TimeScale(this.ctx) const formattedTimeScale = ts.calculateTimeScaleTicks(w.globals.minX, w.globals.maxX) ts.recalcDimensionsBasedOnFormat(formattedTimeScale) } } return xyRatios } drawAxis (type, xyRatios) { let gl = this.w.globals let cnf = this.w.config let xAxis = new XAxis(this.ctx) let yAxis = new YAxis(this.ctx) if (gl.axisCharts && type !== 'radar') { let elXaxis, elYaxis if (type === 'bar' && cnf.plotOptions.bar.horizontal) { elYaxis = yAxis.drawYaxisInversed(0) elXaxis = xAxis.drawXaxisInversed(0) gl.dom.elGraphical.add(elXaxis) gl.dom.elGraphical.add(elYaxis) } else { elXaxis = xAxis.drawXaxis() gl.dom.elGraphical.add(elXaxis) cnf.yaxis.map((yaxe, index) => { if (gl.ignoreYAxisIndexes.indexOf(index) === -1) { elYaxis = yAxis.drawYaxis(xyRatios, index) gl.dom.Paper.add(elYaxis) } }) } } cnf.yaxis.map((yaxe, index) => { if (gl.ignoreYAxisIndexes.indexOf(index) === -1) { yAxis.yAxisTitleRotate(index, yaxe.opposite) } }) } setupBrushHandler () { const w = this.w // only for brush charts if (!w.config.chart.brush.enabled) { return } // if user has not defined a custom function for selection - we handle the brush chart // otherwise we leave it to the user to define the functionality for selection if (typeof w.config.chart.events.selection !== 'function') { const targetChart = ApexCharts.getChartByID(w.config.chart.brush.target) targetChart.w.globals.brushSource = this.ctx const updateSourceChart = () => { this.ctx._updateOptions({ chart: { selection: { xaxis: { min: targetChart.w.globals.minX, max: targetChart.w.globals.maxX } } } }, false, false) } if (typeof targetChart.w.config.chart.events.zoomed !== 'function') { targetChart.w.config.chart.events.zoomed = () => { updateSourceChart() } } if (typeof targetChart.w.config.chart.events.scrolled !== 'function') { targetChart.w.config.chart.events.scrolled = () => { updateSourceChart() } } w.config.chart.events.selection = (chart, e) => { let min, max if (w.config.chart.brush.autoScaleYaxis) { let first = targetChart.w.config.series[0].data.filter(x => x[0] >= e.xaxis.min)[0] let firstValue = first[1] max = min = firstValue targetChart.w.config.series.forEach(serie => { serie.data.forEach(data => { if (data[0] <= e.xaxis.max && data[0] >= e.xaxis.min) { if (data[1] > max && data[1] !== null) max = data[1] if (data[1] < min && data[1] !== null) min = data[1] } }) }) min *= 0.95 max *= 1.05 } let yaxis = {min, max} targetChart._updateOptions({ xaxis: { min: e.xaxis.min, max: e.xaxis.max }, yaxis: { min: yaxis.min, max: yaxis.max } }, false, false, false) } } } }