apexcharts
Version:
A JavaScript Chart Library
284 lines (243 loc) • 7.52 kB
JavaScript
// @ts-check
import Animations from '../modules/Animations'
import Graphics from '../modules/Graphics'
import Fill from '../modules/Fill'
import Series from '../modules/Series'
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 {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
* @param {import('../types/internal').XYRatios} xyRatios
*/
constructor(w, ctx, xyRatios) {
this.ctx = ctx
this.w = w
this.xRatio = xyRatios.xRatio
this.yRatio = xyRatios.yRatio
this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation
this.helpers = new Helpers(w, ctx)
this.rectRadius = this.w.config.plotOptions.heatmap.radius
this.strokeWidth = this.w.config.stroke.show
? this.w.config.stroke.width
: 0
}
/**
* @param {any[]} series
*/
draw(series) {
const w = this.w
const graphics = new Graphics(this.w, this.ctx)
const ret = graphics.group({
class: 'apexcharts-heatmap',
})
ret.attr('clip-path', `url(#gridRectMask${w.globals.cuid})`)
// width divided into equal parts
const xDivision = w.layout.gridWidth / w.globals.dataPoints
const yDivision = w.layout.gridHeight / w.seriesData.series.length
let y1 = 0
let rev = false
this.negRange = this.helpers.checkColorRange()
const 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
const elSeries = graphics.group({
class: `apexcharts-series apexcharts-heatmap-series`,
seriesName: Utils.escapeString(w.seriesData.seriesNames[i]),
rel: i + 1,
'data:realIndex': i,
})
Series.addCollapsedClassToSeries(this.w, elSeries, i)
// Set up event delegation once per series group instead of per-cell listeners
graphics.setupEventDelegation(elSeries, '.apexcharts-heatmap-rect')
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow
const filters = new Filters(this.w)
filters.dropShadow(elSeries, shadow, i)
}
let x1 = 0
const shadeIntensity = w.config.plotOptions.heatmap.shadeIntensity
let j = 0
for (let dIndex = 0; dIndex < w.globals.dataPoints; dIndex++) {
// Recognize gaps and align values based on x axis
if (w.seriesData.seriesX.length && !w.globals.allSeriesHasEqualX) {
if (
w.globals.minX + w.globals.minXDiff * dIndex <
w.seriesData.seriesX[i][j]
) {
x1 = x1 + xDivision
continue
}
}
// Stop loop if index is out of array length
if (j >= heatSeries[i].length) break
const heatColor = this.helpers.getShadeColor(
w.config.chart.type,
i,
j,
this.negRange,
)
let color = heatColor.color
const heatColorProps = heatColor.colorProps
if (w.config.fill.type === 'image') {
const fill = new Fill(this.w)
color = fill.fillPath({
seriesNumber: i,
dataPointIndex: j,
opacity: /** @type {any} */ (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,
})
}
const radius = this.rectRadius
const 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: series[i][j],
'stroke-width': this.strokeWidth,
stroke: w.config.plotOptions.heatmap.useFillColorAsStroke
? color
: w.globals.stroke.colors[0],
color,
})
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,
)
}
}
const formatter = w.config.dataLabels.formatter
const formattedText = formatter(w.seriesData.series[i][j], {
value: w.seriesData.series[i][j],
seriesIndex: i,
dataPointIndex: j,
w,
})
const 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
j++
}
y1 = y1 + yDivision
ret.add(elSeries)
}
// adjust yaxis labels for heatmap
const yAxisScale = /** @type {any[]} */ (
w.globals.yAxisScale[0].result.slice()
)
if (w.config.yaxis[0].reversed) {
yAxisScale.unshift('')
} else {
yAxisScale.push('')
}
w.globals.yAxisScale[0].result = yAxisScale
return ret
}
/**
* @param {any} el
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {number} speed
*/
animateHeatMap(el, x, y, width, height, speed) {
const animations = new Animations(this.w)
animations.animateRect(
el,
{
x: x + width / 2,
y: y + height / 2,
width: 0,
height: 0,
},
{
x,
y,
width,
height,
},
speed,
() => {
animations.animationCompleted(el)
},
)
}
/**
* @param {any} el
* @param {string} colorFrom
* @param {string} colorTo
* @param {number} speed
*/
animateHeatColor(el, colorFrom, colorTo, speed) {
el.attr({
fill: colorFrom,
})
.animate(speed)
.attr({
fill: colorTo,
})
}
}