UNPKG

apexcharts

Version:

A JavaScript Chart Library

599 lines (499 loc) 15.5 kB
// @ts-check 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 { /** * @param {import('../types/internal').ChartStateW} w * @param {import('../types/internal').ChartContext} ctx */ constructor(w, ctx) { this.ctx = ctx this.w = 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 this.graphics = new Graphics(this.w) this.lineColorArr = w.globals.stroke.colors !== undefined ? w.globals.stroke.colors : w.globals.colors this.defaultSize = w.globals.svgHeight < w.globals.svgWidth ? w.layout.gridHeight : w.layout.gridWidth this.isLog = w.config.yaxis[0].logarithmic this.logBase = w.config.yaxis[0].logBase this.coreUtils = new CoreUtils(this.w) this.maxValue = this.isLog ? this.coreUtils.getLogVal(this.logBase, w.globals.maxY, 0) : w.globals.maxY this.minValue = this.isLog ? this.coreUtils.getLogVal(this.logBase, 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.layout.xAxisLabelsWidth / 1.75 } if (w.config.plotOptions.radar.size !== undefined) { this.size = w.config.plotOptions.radar.size } this.dataRadiusOfPercent = /** @type {any} */ ([]) this.dataRadius = /** @type {any} */ ([]) this.angleArr = /** @type {any} */ ([]) this.dataPointsLen = 0 this.disAngle = 0 /** @type {any} */ /** @type {any[]} */ this.yaxisLabelsTextsPos = [] } /** * @param {any[]} series */ draw(series) { const w = this.w const fill = new Fill(this.w) /** @type {any[]} */ const allSeries = [] const dataLabels = new DataLabels(this.w, this.ctx) if (series.length) { this.dataPointsLen = series[w.globals.maxValsInArrayIndex].length } this.disAngle = (Math.PI * 2) / this.dataPointsLen const halfW = w.layout.gridWidth / 2 const halfH = w.layout.gridHeight / 2 const translateX = halfW + w.config.plotOptions.radar.offsetX const translateY = halfH + w.config.plotOptions.radar.offsetY const ret = this.graphics.group({ class: 'apexcharts-radar-series apexcharts-plot-series', transform: `translate(${translateX || 0}, ${translateY || 0})`, }) /** @type {any[]} */ let dataPointsPos = [] /** @type {any | null} */ let elPointsMain = null /** @type {any | null} */ let elDataPointsMain = null this.yaxisLabels = this.graphics.group({ class: 'apexcharts-yaxis', }) /** * @param {number[]} s * @param {number} i */ series.forEach((s, i) => { const longestSeries = s.length === w.globals.dataPoints // el to which series will be drawn const elSeries = this.graphics.group().attr({ class: `apexcharts-series`, 'data:longestSeries': longestSeries, seriesName: Utils.escapeString(w.seriesData.seriesNames[i]), rel: i + 1, 'data:realIndex': i, }) this.dataRadiusOfPercent[i] = [] this.dataRadius[i] = [] this.angleArr[i] = [] /** * @param {number} dv * @param {number} j */ s.forEach((/** @type {any} */ dv, /** @type {any} */ j) => { const range = Math.abs(this.maxValue - this.minValue) dv = dv - this.minValue if (this.isLog) { dv = this.coreUtils.getLogVal(this.logBase, 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++) { const 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) const pathFill = fill.fillPath({ seriesNumber: i, }) const 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.w) const shadow = w.config.chart.dropShadow filters.dropShadow( renderedAreaPath, Object.assign({}, shadow, { noUserSpaceOnUse: true }), i, ) } elSeries.add(renderedAreaPath) } /** * @param {any} sj * @param {number} j */ s.forEach((/** @type {any} */ sj, /** @type {any} */ j) => { const markers = new Markers(this.w, this.ctx) const opts = markers.getMarkerConfig({ cssClass: 'apexcharts-marker', seriesIndex: i, dataPointIndex: j, }) const 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) const 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) { const text = dataLabelsConfig.formatter(w.seriesData.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 } /** * @param {Record<string, any>} opts */ drawPolygons(opts) { const w = this.w const { parent } = opts const helpers = new Helpers(this.w) const yaxisTexts = w.globals.yAxisScale[0].result.reverse() const layers = yaxisTexts.length const radiusSizes = [] const layerDis = this.size / (layers - 1) for (let i = 0; i < layers; i++) { radiusSizes[i] = layerDis * i } radiusSizes.reverse() /** @type {any[]} */ const polygonStrings = [] /** @type {any[]} */ const 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( (/** @type {any} */ p, /** @type {any} */ 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 const elXAxisWrap = this.graphics.group({ class: 'apexcharts-xaxis', }) const polygonPos = Utils.getPolygonPos(this.size, this.dataPointsLen) /** * @param {string} label * @param {number} i */ w.labelData.labels.forEach((label, i) => { const formatter = w.config.xaxis.labels.formatter const dataLabels = new DataLabels(this.w, this.ctx) if (polygonPos[i]) { const textPos = this.getTextPos(polygonPos[i], this.size) const text = formatter(label, { seriesIndex: -1, dataPointIndex: i, w, }) const dataLabelText = dataLabels.plotDataLabelsText({ x: textPos.newX, y: textPos.newY, text, textAnchor: textPos.textAnchor, i, j: i, parent: elXAxisWrap, className: 'apexcharts-xaxis-label', 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, }) /** * @param {Event} e */ dataLabelText.on('click', (/** @type {any} */ e) => { if (typeof w.config.chart.events.xAxisLabelClick === 'function') { const opts = Object.assign({}, w, { labelIndex: i, }) w.config.chart.events.xAxisLabelClick(e, this.ctx, opts) } }) } }) return elXAxisWrap } /** * @param {Array<Record<string, any>>} pos * @param {Record<string, any>} origin */ createPaths(pos, origin) { const linePathsTo = [] /** @type {any[]} */ let linePathsFrom = [] const areaPathsTo = [] /** @type {any[]} */ 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) /** * @param {number} p * @param {number} i */ pos.forEach((/** @type {any} */ p, /** @type {any} */ 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, } } /** * @param {Record<string, any>} pos * @param {number} polygonSize */ getTextPos(pos, polygonSize) { const 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, } } /** * @param {number} realIndex */ getPreviousPath(realIndex) { const w = this.w let pathFrom = null for (let pp = 0; pp < w.globals.previousPaths.length; pp++) { const gpp = w.globals.previousPaths[pp] if ( gpp.paths.length > 0 && parseInt(gpp.realIndex, 10) === parseInt(String(realIndex), 10) ) { if (typeof w.globals.previousPaths[pp].paths[0] !== 'undefined') { pathFrom = w.globals.previousPaths[pp].paths[0].d } } } return pathFrom } /** * @param {any[]} dataRadiusArr * @param {any[]} angleArr */ getDataPointsPos( dataRadiusArr, angleArr, dataPointsLen = this.dataPointsLen, ) { dataRadiusArr = dataRadiusArr || [] angleArr = angleArr || [] const dataPointsPosArray = [] for (let j = 0; j < dataPointsLen; j++) { const 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