UNPKG

apexcharts

Version:

A JavaScript Chart Library

656 lines (655 loc) 20 kB
/*! * ApexCharts v5.10.6 * (c) 2018-2026 ApexCharts */ import * as _core from "apexcharts/core"; import _core__default from "apexcharts/core"; import { default as default2 } from "apexcharts/core"; function normalize(data, area) { let sum = 0; for (let i = 0; i < data.length; i++) { sum += data[i]; } const multiplier = area / sum; const result = new Array(data.length); for (let i = 0; i < data.length; i++) { result[i] = data[i] * multiplier; } return result; } function calculateRatio(rowMin, rowMax, rowSum, length) { const lengthSq = length * length; const sumSq = rowSum * rowSum; return Math.max( lengthSq * rowMax / sumSq, sumSq / (lengthSq * rowMin) ); } function improvesRatio(rowLen, rowMin, rowMax, rowSum, nextNode, length) { if (rowLen === 0) return true; const currentRatio = calculateRatio(rowMin, rowMax, rowSum, length); const newRatio = calculateRatio( Math.min(rowMin, nextNode), Math.max(rowMax, nextNode), rowSum + nextNode, length ); return currentRatio >= newRatio; } function emitCoordinates(coords, row, rowLen, rowSum, xoffset, yoffset, width, height) { if (width >= height) { const areaWidth = rowSum / height; let subY = yoffset; for (let i = 0; i < rowLen; i++) { const h = row[i] / areaWidth; coords.push([xoffset, subY, xoffset + areaWidth, subY + h]); subY += h; } } else { const areaHeight = rowSum / width; let subX = xoffset; for (let i = 0; i < rowLen; i++) { const w = row[i] / areaHeight; coords.push([subX, yoffset, subX + w, yoffset + areaHeight]); subX += w; } } } function squarify(data, xoffset, yoffset, width, height) { const coords = []; const n = data.length; if (n === 0) return coords; const row = new Array(n); let rowLen = 0; let rowSum = 0; let rowMin = Infinity; let rowMax = -Infinity; let i = 0; while (i < n) { const length = Math.min(width, height); const val = data[i]; if (improvesRatio(rowLen, rowMin, rowMax, rowSum, val, length)) { row[rowLen] = val; rowLen++; rowSum += val; if (val < rowMin) rowMin = val; if (val > rowMax) rowMax = val; i++; } else { emitCoordinates(coords, row, rowLen, rowSum, xoffset, yoffset, width, height); if (width >= height) { const areaWidth = rowSum / height; xoffset += areaWidth; width -= areaWidth; } else { const areaHeight = rowSum / width; yoffset += areaHeight; height -= areaHeight; } rowLen = 0; rowSum = 0; rowMin = Infinity; rowMax = -Infinity; } } if (rowLen > 0) { emitCoordinates(coords, row, rowLen, rowSum, xoffset, yoffset, width, height); } return coords; } function generate(data, width, height) { const n = data.length; const sums = new Array(n); for (let i = 0; i < n; i++) { let s = 0; const series = data[i]; for (let j = 0; j < series.length; j++) { s += series[j]; } sums[i] = s; } const seriesRects = squarify( normalize(sums, width * height), 0, 0, width, height ); const results = new Array(n); for (let i = 0; i < n; i++) { const rect = seriesRects[i]; const rx = rect[0]; const ry = rect[1]; const rw = rect[2] - rx; const rh = rect[3] - ry; results[i] = squarify( normalize(data[i], rw * rh), rx, ry, rw, rh ); } return results; } const TreemapSquared = { generate }; const Graphics = _core.__apex_Graphics; const Animations = _core.__apex_Animations; const Fill = _core.__apex_Fill; const Utils = _core.__apex_Utils; const DataLabels = _core.__apex_DataLabels; class TreemapHelpers { /** * @param {import('../../../types/internal').ChartStateW} w * @param {import('../../../types/internal').ChartContext} ctx */ constructor(w, ctx) { this.ctx = ctx; this.w = w; } checkColorRange() { const w = this.w; let negRange = false; const chartOpts = w.config.plotOptions[w.config.chart.type]; if (chartOpts.colorScale.ranges.length > 0) { chartOpts.colorScale.ranges.map((range) => { if (range.from <= 0) { negRange = true; } }); } return negRange; } /** * @param {string} chartType * @param {number} i * @param {number} j * @param {any} negRange */ getShadeColor(chartType, i, j, negRange) { const w = this.w; let colorShadePercent = 1; const shadeIntensity = w.config.plotOptions[chartType].shadeIntensity; const colorProps = this.determineColor(chartType, i, j); if ( /** @type {any} */ w.globals.hasNegs || negRange ) { if (w.config.plotOptions[chartType].reverseNegativeShade) { if (colorProps.percent < 0) { colorShadePercent = colorProps.percent / 100 * (shadeIntensity * 1.25); } else { colorShadePercent = (1 - colorProps.percent / 100) * (shadeIntensity * 1.25); } } else { if (colorProps.percent <= 0) { colorShadePercent = 1 - (1 + colorProps.percent / 100) * shadeIntensity; } else { colorShadePercent = (1 - colorProps.percent / 100) * shadeIntensity; } } } else { colorShadePercent = 1 - colorProps.percent / 100; if (chartType === "treemap") { colorShadePercent = (1 - colorProps.percent / 100) * (shadeIntensity * 1.25); } } let color = colorProps.color; const utils = new Utils(); if (w.config.plotOptions[chartType].enableShades) { if (this.w.config.theme.mode === "dark") { const shadeColor = utils.shadeColor( colorShadePercent * -1, colorProps.color ); color = Utils.hexToRgba( Utils.isColorHex(shadeColor) ? shadeColor : Utils.rgb2hex(shadeColor), w.config.fill.opacity ); } else { const shadeColor = utils.shadeColor(colorShadePercent, colorProps.color); color = Utils.hexToRgba( Utils.isColorHex(shadeColor) ? shadeColor : Utils.rgb2hex(shadeColor), w.config.fill.opacity ); } } return { color, colorProps }; } /** * @param {string} chartType * @param {number} i * @param {number} j */ determineColor(chartType, i, j) { const w = this.w; const val = w.seriesData.series[i][j]; const chartOpts = w.config.plotOptions[chartType]; let seriesNumber = chartOpts.colorScale.inverse ? j : i; if (chartOpts.distributed && w.config.chart.type === "treemap") { seriesNumber = j; } let color = w.globals.colors[seriesNumber]; let foreColor = null; let min = Math.min(...w.seriesData.series[i]); let max = Math.max(...w.seriesData.series[i]); if (!chartOpts.distributed && chartType === "heatmap") { min = w.globals.minY; max = w.globals.maxY; } if (typeof chartOpts.colorScale.min !== "undefined") { min = chartOpts.colorScale.min < w.globals.minY ? chartOpts.colorScale.min : w.globals.minY; max = chartOpts.colorScale.max > w.globals.maxY ? chartOpts.colorScale.max : w.globals.maxY; } const total = Math.abs(max) + Math.abs(min); let percent = 100 * val / (total === 0 ? total - 1e-6 : total); if (chartOpts.colorScale.ranges.length > 0) { const colorRange = chartOpts.colorScale.ranges; colorRange.map((range) => { if (val >= range.from && val <= range.to) { color = range.color; foreColor = range.foreColor ? range.foreColor : null; min = range.from; max = range.to; const rTotal = Math.abs(max) + Math.abs(min); percent = 100 * val / (rTotal === 0 ? rTotal - 1e-6 : rTotal); } }); } return { color, foreColor, percent }; } /** @param {{ text?: any, x?: any, y?: any, i?: any, j?: any, colorProps?: any, fontSize?: any, series?: any }} opts */ calculateDataLabels({ text, x, y, i, j, colorProps, fontSize }) { const w = this.w; const dataLabelsConfig = w.config.dataLabels; const graphics = new Graphics(this.w); const dataLabels = new DataLabels(this.w, this.ctx); let elDataLabelsWrap = null; if (dataLabelsConfig.enabled) { elDataLabelsWrap = graphics.group({ class: "apexcharts-data-labels" }); const offX = dataLabelsConfig.offsetX; const offY = dataLabelsConfig.offsetY; const dataLabelsX = x + offX; const dataLabelsY = y + parseFloat(dataLabelsConfig.style.fontSize) / 3 + offY; dataLabels.plotDataLabelsText({ x: dataLabelsX, y: dataLabelsY, text, i, j, color: colorProps.foreColor, parent: elDataLabelsWrap, fontSize, dataLabelsConfig }); } return elDataLabelsWrap; } } const Filters = _core.__apex_Filters; class TreemapChart { /** * @param {import('../types/internal').ChartStateW} w * @param {import('../types/internal').ChartContext} ctx */ constructor(w, ctx) { this.ctx = ctx; this.w = w; this.strokeWidth = this.w.config.stroke.width; this.helpers = new TreemapHelpers(w, ctx); this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation; this.labels = []; } /** * @param {any[]} series */ draw(series) { const w = this.w; const graphics = new Graphics(this.w, this.ctx); const fill = new Fill(this.w); const ret = graphics.group({ class: "apexcharts-treemap" }); if (w.globals.noData) return ret; const ser = []; series.forEach((s) => { const d = s.map((v) => { return Math.abs(v); }); ser.push(d); }); this.negRange = this.helpers.checkColorRange(); w.config.series.forEach((s, i) => { s.data.forEach((l) => { if (!Array.isArray(this.labels[i])) this.labels[i] = []; this.labels[i].push(l.x); }); }); const nodes = TreemapSquared.generate( ser, w.layout.gridWidth, w.layout.gridHeight ); nodes.forEach((node, i) => { var _a; const elSeries = graphics.group({ class: `apexcharts-series apexcharts-treemap-series`, seriesName: Utils.escapeString(w.seriesData.seriesNames[i]), rel: i + 1, "data:realIndex": i }); graphics.setupEventDelegation(elSeries, ".apexcharts-treemap-rect"); if (w.config.chart.dropShadow.enabled) { const shadow = w.config.chart.dropShadow; const filters = new Filters(this.w); filters.dropShadow(ret, shadow, i); } const elDataLabelWrap = graphics.group({ class: "apexcharts-data-labels" }); const bounds = { xMin: Infinity, yMin: Infinity, xMax: -Infinity, yMax: -Infinity }; node.forEach((r, j) => { const x1 = r[0]; const y1 = r[1]; const x2 = r[2]; const y2 = r[3]; bounds.xMin = Math.min(bounds.xMin, x1); bounds.yMin = Math.min(bounds.yMin, y1); bounds.xMax = Math.max(bounds.xMax, x2); bounds.yMax = Math.max(bounds.yMax, y2); const colorProps = this.helpers.getShadeColor( w.config.chart.type, i, j, this.negRange ); const color = colorProps.color; const pathFill = fill.fillPath({ color, seriesNumber: i, dataPointIndex: j }); const elRect = graphics.drawRect( x1, y1, x2 - x1, y2 - y1, w.config.plotOptions.treemap.borderRadius, "#fff", 1, this.strokeWidth, w.config.plotOptions.treemap.useFillColorAsStroke ? color : w.globals.stroke.colors[i] ); elRect.attr({ cx: x1, cy: y1, index: i, i, j, width: x2 - x1, height: y2 - y1, fill: pathFill }); elRect.node.classList.add("apexcharts-treemap-rect"); let fromRect = { x: x1 + (x2 - x1) / 2, y: y1 + (y2 - y1) / 2, width: 0, height: 0 }; const toRect = { x: x1, y: y1, width: x2 - x1, height: y2 - y1 }; if (w.config.chart.animations.enabled && !w.globals.dataChanged) { let speed = 1; if (!w.globals.resized) { speed = w.config.chart.animations.speed; } this.animateTreemap(elRect, fromRect, toRect, speed); } if (w.globals.dataChanged) { let speed = 1; if (this.dynamicAnim.enabled && w.globals.shouldAnimate) { speed = this.dynamicAnim.speed; if (w.globals.previousPaths[i] && /** @type {Record<string,any>} */ w.globals.previousPaths[i][j] && /** @type {Record<string,any>} */ w.globals.previousPaths[i][j].rect) { fromRect = /** @type {Record<string,any>} */ w.globals.previousPaths[i][j].rect; } this.animateTreemap(elRect, fromRect, toRect, speed); } } let fontSize = this.getFontSize(r); let formattedText = w.config.dataLabels.formatter(this.labels[i][j], { value: w.seriesData.series[i][j], seriesIndex: i, dataPointIndex: j, w }); if (w.config.plotOptions.treemap.dataLabels.format === "truncate") { fontSize = parseInt(String(w.config.dataLabels.style.fontSize), 10); formattedText = this.truncateLabels( String(formattedText), fontSize, x1, y1, x2, y2 ); } let dataLabels = null; if (w.seriesData.series[i][j]) { dataLabels = this.helpers.calculateDataLabels({ text: formattedText, x: (x1 + x2) / 2, y: (y1 + y2) / 2 + this.strokeWidth / 2 + fontSize / 3, i, j, colorProps, fontSize, series }); } if (w.config.dataLabels.enabled && dataLabels) { this.rotateToFitLabel( dataLabels, fontSize, formattedText, x1, y1, x2, y2 ); } elSeries.add(elRect); if (dataLabels !== null) { elSeries.add(dataLabels); } }); const seriesTitle = w.config.plotOptions.treemap.seriesTitle; if (w.config.series.length > 1 && seriesTitle && seriesTitle.show) { const sName = ( /** @type {Record<string,any>} */ w.config.series[i].name || "" ); if (sName && bounds.xMin < Infinity && bounds.yMin < Infinity) { const { offsetX, offsetY, borderColor, borderWidth, borderRadius, style } = seriesTitle; const textColor = style.color || w.config.chart.foreColor; const padding = { left: style.padding.left, right: style.padding.right, top: style.padding.top, bottom: style.padding.bottom }; const textSize = graphics.getTextRects( sName, style.fontSize, style.fontFamily ); const labelRectWidth = textSize.width + padding.left + padding.right; const labelRectHeight = textSize.height + padding.top + padding.bottom; const labelX = bounds.xMin + (offsetX || 0); const labelY = bounds.yMin + (offsetY || 0); const elLabelRect = graphics.drawRect( labelX, labelY, labelRectWidth, labelRectHeight, borderRadius, style.background, 1, borderWidth, borderColor ); const elLabelText = graphics.drawText({ x: labelX + padding.left, y: labelY + padding.top + ((_a = textSize == null ? void 0 : textSize.height) != null ? _a : 0) * 0.75, text: sName, fontSize: style.fontSize, fontFamily: style.fontFamily, fontWeight: style.fontWeight, foreColor: textColor, cssClass: style.cssClass || "" }); elSeries.add(elLabelRect); elSeries.add(elLabelText); } } elSeries.add(elDataLabelWrap); ret.add(elSeries); }); return ret; } // This calculates a font-size based upon // average label length and the size of the box /** * @param {number[]} coordinates */ getFontSize(coordinates) { const w = this.w; function totalLabelLength(arr) { let i, total = 0; if (Array.isArray(arr[0])) { for (i = 0; i < arr.length; i++) { total += totalLabelLength(arr[i]); } } else { for (i = 0; i < arr.length; i++) { total += arr[i].length; } } return total; } function countLabels(arr) { let i, total = 0; if (Array.isArray(arr[0])) { for (i = 0; i < arr.length; i++) { total += countLabels(arr[i]); } } else { for (i = 0; i < arr.length; i++) { total += 1; } } return total; } const averagelabelsize = totalLabelLength(this.labels) / countLabels(this.labels); function fontSize(width, height) { const area = width * height; const arearoot = Math.pow(area, 0.5); return Math.min( arearoot / averagelabelsize, parseInt(w.config.dataLabels.style.fontSize, 10) ); } return fontSize( coordinates[2] - coordinates[0], coordinates[3] - coordinates[1] ); } /** * @param {any} elText * @param {string | number} fontSize * @param {string} text * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 */ rotateToFitLabel(elText, fontSize, text, x1, y1, x2, y2) { const graphics = new Graphics(this.w); const textRect = graphics.getTextRects(text, String(fontSize)); if (textRect.width + this.w.config.stroke.width + 5 > x2 - x1 && textRect.width <= y2 - y1) { const labelRotatingCenter = graphics.rotateAroundCenter(elText.node); elText.node.setAttribute( "transform", `rotate(-90 ${labelRotatingCenter.x} ${labelRotatingCenter.y}) translate(${textRect.height / 3})` ); } } // This is an alternative label formatting method that uses a // consistent font size, and trims the edge of long labels /** * @param {string} text * @param {number} fontSize * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 */ truncateLabels(text, fontSize, x1, y1, x2, y2) { const graphics = new Graphics(this.w); const textRect = graphics.getTextRects(text, String(fontSize)); const labelMaxWidth = textRect.width + this.w.config.stroke.width + 5 > x2 - x1 && y2 - y1 > x2 - x1 ? y2 - y1 : x2 - x1; const truncatedText = graphics.getTextBasedOnMaxWidth({ text, maxWidth: labelMaxWidth, fontSize }); if (text.length !== truncatedText.length && labelMaxWidth / fontSize < 5) { return ""; } else { return truncatedText; } } /** * @param {any} el * @param {Record<string, any>} fromRect * @param {Record<string, any>} toRect * @param {number} speed */ animateTreemap(el, fromRect, toRect, speed) { const animations = new Animations(this.w); animations.animateRect(el, fromRect, toRect, speed, () => { animations.animationCompleted(el); }); } } _core__default.use({ treemap: TreemapChart }); export { default2 as default };