apexcharts
Version:
A JavaScript Chart Library
599 lines (499 loc) • 15.5 kB
JavaScript
// @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