apexcharts
Version:
A JavaScript Chart Library
2,474 lines • 89.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
/*!
* ApexCharts v7.0.0
* (c) 2018-2026 ApexCharts
*/
import * as _core from "apexcharts/core";
import _core__default from "apexcharts/core";
import { default as default2 } from "apexcharts/core";
const Animations = _core.__apex_Animations;
const Fill = _core.__apex_Fill;
const Utils = _core.__apex_Utils;
const Graphics = _core.__apex_Graphics;
const Filters = _core.__apex_Filters;
const Scales = _core.__apex_Scales;
function drawOuterLabel(w, spec) {
const {
lines,
lineHeight,
anchor,
elbow,
labelX,
labelY,
side,
connector,
style,
foreColor
} = spec;
const graphics = new Graphics(w);
const group = graphics.group({
class: spec.groupClass || "apexcharts-outer-label-group"
});
if (connector.show) {
const d = `M ${anchor.x} ${anchor.y} L ${elbow.x} ${elbow.y} L ${labelX} ${labelY}`;
const line = graphics.drawPath({
d,
stroke: connector.color,
strokeWidth: connector.width,
fill: "none",
strokeLinecap: "round"
});
line.node.classList.add(spec.connectorClass || "apexcharts-outer-label-connector");
group.add(line);
}
const textX = side === "right" ? labelX + 4 : labelX - 4;
const n = lines.length;
const startY = labelY - (n - 1) * lineHeight / 2;
const elText = graphics.drawText({
x: textX,
y: startY,
text: n === 1 ? lines[0] : lines,
textAnchor: side === "right" ? "start" : "end",
fontSize: style.fontSize,
fontFamily: style.fontFamily,
fontWeight: style.fontWeight,
foreColor,
dominantBaseline: "central",
cssClass: spec.textClass || "apexcharts-outer-label"
});
if (n > 1) {
const tspans = elText.node.getElementsByTagName("tspan");
for (let li = 0; li < tspans.length; li++) {
tspans[li].setAttribute("x", `${textX}`);
tspans[li].setAttribute("dy", li === 0 ? "0" : `${lineHeight}`);
}
}
group.add(elText);
return group;
}
function spaceOutLabels(items, minGap, maxY, minY) {
const col = items.slice().sort((a, b) => a.idealY - b.idealY);
col.forEach((l) => {
l.labelY = l.idealY;
});
for (let k = 1; k < col.length; k++) {
if (col[k].labelY - col[k - 1].labelY < minGap) {
col[k].labelY = col[k - 1].labelY + minGap;
}
}
const last = col[col.length - 1];
const overflow = last ? last.labelY - maxY : 0;
if (overflow > 0) {
for (let k = col.length - 1; k >= 0; k--) {
col[k].labelY -= overflow;
if (k < col.length - 1 && col[k + 1].labelY - col[k].labelY < minGap) {
col[k].labelY = col[k + 1].labelY - minGap;
}
}
}
}
class CircularChartsHelpers {
/**
* @param {import('../../../types/internal').ChartStateW} w
*/
constructor(w) {
this.w = w;
}
/**
* @param {number} x
* @param {number} y
* @param {number} i
* @param {string | number} text
*/
drawYAxisTexts(x, y, i, text) {
const w = this.w;
const yaxisConfig = w.config.yaxis[0];
const formatter = w.formatters.yLabelFormatters[0];
const graphics = new Graphics(this.w);
const yaxisLabel = graphics.drawText({
x: x + yaxisConfig.labels.offsetX,
y: y + yaxisConfig.labels.offsetY,
text: formatter(text, i),
textAnchor: "middle",
fontSize: yaxisConfig.labels.style.fontSize,
fontFamily: yaxisConfig.labels.style.fontFamily,
foreColor: Array.isArray(yaxisConfig.labels.style.colors) ? yaxisConfig.labels.style.colors[i] : yaxisConfig.labels.style.colors
});
return yaxisLabel;
}
/**
* Widest rendered width among the given label strings. Used to reserve
* horizontal room for outer (name) labels so the pie can shrink to fit them.
* @param {string[]} labels
* @param {{ fontSize?: string, fontFamily?: string }} style
* @returns {number}
*/
getMaxLabelWidth(labels, { fontSize, fontFamily } = {}) {
const graphics = new Graphics(this.w);
let maxWidth = 0;
labels.forEach((text) => {
if (text === null || typeof text === "undefined" || text === "") return;
const rect = graphics.getTextRects(
`${text}`,
fontSize || "12px",
fontFamily,
""
);
maxWidth = Math.max(maxWidth, rect.width);
});
return maxWidth;
}
/**
* Draw a single outer (name) label: an optional leader line from the slice
* edge (anchor -> radial elbow -> label) plus the name text (one or more
* lines, e.g. name + percent). Geometry is computed by the caller (Pie.js)
* so it can run a de-overlap pass first. The text block is vertically
* centered on `labelY`, which is where the connector terminates.
* @param {{
* lines: string[],
* lineHeight: number,
* anchor: { x: number, y: number },
* elbow: { x: number, y: number },
* labelX: number,
* labelY: number,
* side: 'left' | 'right',
* connector: { show: boolean, width: number, color: string },
* style: { fontSize?: string, fontFamily?: string, fontWeight?: string | number },
* foreColor: string,
* }} opts
*/
drawExternalLabel(opts) {
return drawOuterLabel(this.w, __spreadProps(__spreadValues({}, opts), {
groupClass: "apexcharts-pie-name-label-group",
textClass: "apexcharts-pie-name-label",
connectorClass: "apexcharts-pie-label-connector"
}));
}
}
const Environment = _core.__apex_Environment_Environment;
const D2R = Math.PI / 180;
const R2D = 180 / Math.PI;
function arcPoint(cx, cy, radius, deg) {
return {
x: cx + radius * Math.cos((deg - 90) * D2R),
y: cy + radius * Math.sin((deg - 90) * D2R)
};
}
const xy = (p) => `${p.x} ${p.y}`;
function roundedDonutSegmentPath({ cx, cy, rIn, rOut, a0, a1, r, spanDeg }) {
const ptAt = (radius, deg) => arcPoint(cx, cy, radius, deg);
const degOut = r / rOut * R2D;
const degIn = r / rIn * R2D;
const oStart = ptAt(rOut, a0 + degOut);
const oEnd = ptAt(rOut, a1 - degOut);
const largeOut = spanDeg - 2 * degOut > 180 ? 1 : 0;
const ocEnd = ptAt(rOut, a1);
const rEndOut = ptAt(rOut - r, a1);
const ocStart = ptAt(rOut, a0);
const rStartOut = ptAt(rOut - r, a0);
const iEnd = ptAt(rIn, a1 - degIn);
const iStart = ptAt(rIn, a0 + degIn);
const largeIn = spanDeg - 2 * degIn > 180 ? 1 : 0;
const icEnd = ptAt(rIn, a1);
const rEndIn = ptAt(rIn + r, a1);
const icStart = ptAt(rIn, a0);
const rStartIn = ptAt(rIn + r, a0);
return [
"M",
xy(oStart),
"A",
rOut,
rOut,
0,
largeOut,
1,
xy(oEnd),
"Q",
xy(ocEnd),
xy(rEndOut),
"L",
xy(rEndIn),
"Q",
xy(icEnd),
xy(iEnd),
"A",
rIn,
rIn,
0,
largeIn,
0,
xy(iStart),
"Q",
xy(icStart),
xy(rStartIn),
"L",
xy(rStartOut),
"Q",
xy(ocStart),
xy(oStart),
"Z"
].join(" ");
}
function roundedPieSegmentPath({ cx, cy, rOut, a0, a1, r, spanDeg }) {
const ptAt = (radius, deg) => arcPoint(cx, cy, radius, deg);
const degOut = r / rOut * R2D;
const oStart = ptAt(rOut, a0 + degOut);
const oEnd = ptAt(rOut, a1 - degOut);
const largeOut = spanDeg - 2 * degOut > 180 ? 1 : 0;
const ocEnd = ptAt(rOut, a1);
const rEndOut = ptAt(rOut - r, a1);
const ocStart = ptAt(rOut, a0);
const rStartOut = ptAt(rOut - r, a0);
return [
"M",
xy(oStart),
"A",
rOut,
rOut,
0,
largeOut,
1,
xy(oEnd),
"Q",
xy(ocEnd),
xy(rEndOut),
"L",
`${cx} ${cy}`,
"L",
xy(rStartOut),
"Q",
xy(ocStart),
xy(oStart),
"Z"
].join(" ");
}
function sharpDonutSegmentPath({ cx, cy, rIn, rOut, a0, a1, spanDeg }) {
const ptAt = (radius, deg) => arcPoint(cx, cy, radius, deg);
const largeArc = spanDeg > 180 ? 1 : 0;
const A = ptAt(rOut, a0);
const B = ptAt(rOut, a1);
const C = ptAt(rIn, a1);
const Din = ptAt(rIn, a0);
return [
"M",
xy(A),
"A",
rOut,
rOut,
0,
largeArc,
1,
xy(B),
"L",
xy(C),
"A",
rIn,
rIn,
0,
largeArc,
0,
xy(Din),
"Z"
].join(" ");
}
const SLICE_OFFSET_TRANSITION = "transform 320ms cubic-bezier(0.25, 0.8, 0.3, 1)";
const HOVER_OUTLINE_TRANSITION = "opacity 180ms ease-out";
class Pie {
/**
* @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.animBeginArr = [0];
this.animDur = 0;
this.donutDataLabels = this.w.config.plotOptions.pie.donut.labels;
this.lineColorArr = w.globals.stroke.colors !== void 0 ? w.globals.stroke.colors : w.globals.colors;
this.defaultSize = Math.min(w.layout.gridWidth, w.layout.gridHeight);
this.centerY = this.defaultSize / 2;
this.centerX = w.layout.gridWidth / 2;
if (w.config.chart.type === "radialBar") {
this.fullAngle = 360;
} else {
this.fullAngle = Math.abs(
w.config.plotOptions.pie.endAngle - w.config.plotOptions.pie.startAngle
);
}
this.initialAngle = w.config.plotOptions.pie.startAngle % this.fullAngle;
w.globals.radialSize = this.defaultSize / 2.05 - w.config.stroke.width - (!w.config.chart.sparkline.enabled ? w.config.chart.dropShadow.blur : 0);
this.externalCfg = w.config.plotOptions.pie.dataLabels.external;
const dlStyle = w.config.dataLabels.style;
this.externalLabelStyle = {
fontSize: this.externalCfg.fontSize || dlStyle.fontSize,
fontFamily: this.externalCfg.fontFamily || dlStyle.fontFamily,
fontWeight: this.externalCfg.fontWeight || dlStyle.fontWeight
};
this.externalLabels = [];
this.externalLabelMaxLines = 1;
this.externalLabelLineH = parseFloat(this.externalLabelStyle.fontSize) || 12;
w.globals.pieExternalLabelMarginY = 0;
this.showExternalLabels = this.externalCfg.show && this.chartType !== "polarArea";
if (this.showExternalLabels && !w.globals.noData) {
this.reserveExternalLabelSpace();
}
this.donutSize = w.globals.radialSize * parseInt(w.config.plotOptions.pie.donut.size, 10) / 100;
const scaleSize = w.config.plotOptions.pie.customScale;
const halfW = w.layout.gridWidth / 2;
const halfH = w.layout.gridHeight / 2;
this.translateX = halfW - halfW * scaleSize;
this.translateY = halfH - halfH * scaleSize;
this.dataLabelsGroup = new Graphics(this.w).group({
class: "apexcharts-datalabels-group",
transform: `translate(${this.translateX}, ${this.translateY}) scale(${scaleSize})`
});
this.maxY = 0;
this.sliceLabels = [];
this.sliceSizes = [];
this.prevSliceSizes = [];
this.sliceLabelGroups = {};
this.externalLabelGroups = {};
this.elHoverOutline = null;
this.elHoverOutlinePath = null;
this.hoverOutlineIndex = -1;
this.prevSectorAngleArr = [];
}
/**
* The text shown in an outer (name) label for slice `i`. Applies the
* user `name.formatter` if provided, otherwise the raw series name. The
* formatter may return a string or an array of strings (one per line, e.g.
* `[name, percent]`); normalize via `getExternalLabelLines`.
* @param {number} i
* @returns {string | string[]}
*/
getExternalLabelText(i) {
var _a, _b, _c;
const w = this.w;
const name = w.seriesData.seriesNames[i];
const fn = this.externalCfg.formatter;
if (typeof fn === "function") {
return fn(name, {
seriesIndex: i,
percent: (_b = (_a = w.globals.seriesPercent) == null ? void 0 : _a[i]) == null ? void 0 : _b[0],
value: (_c = w.globals.seriesTotals) == null ? void 0 : _c[i],
w
});
}
return name == null ? "" : `${name}`;
}
/**
* Outer label content for slice `i` normalized to an array of line strings.
* Supports a formatter returning an array, or a string with `\n` separators.
* @param {number} i
* @returns {string[]}
*/
getExternalLabelLines(i) {
const raw = this.getExternalLabelText(i);
const arr = Array.isArray(raw) ? raw : `${raw == null ? "" : raw}`.split("\n");
return arr.map((l) => l == null ? "" : `${l}`);
}
/**
* Shrink the pie radius (and reposition its center) so outer name labels and
* their connector lines fit inside the chart area without clipping. Stores
* the reserved vertical band on `w.globals.pieExternalLabelMarginY` so
* Core.resizeNonAxisCharts can grow the SVG height to match.
*/
reserveExternalLabelSpace() {
const w = this.w;
const helpers = new CircularChartsHelpers(w);
const lineSets = (w.seriesData.seriesNames || []).map(
(_, i) => this.getExternalLabelLines(i)
);
const maxLabelWidth = helpers.getMaxLabelWidth(lineSets.flat(), {
fontSize: this.externalLabelStyle.fontSize,
fontFamily: this.externalLabelStyle.fontFamily
});
this.externalLabelMaxLines = lineSets.reduce((m, s) => Math.max(m, s.length), 1);
this.externalLabelLineH = Math.round(
(parseFloat(this.externalLabelStyle.fontSize) || 12) * 1.35
);
const cn = this.externalCfg.connector;
const blockHeight = this.externalLabelMaxLines * this.externalLabelLineH;
const mh = maxLabelWidth + (cn.length || 0) + (cn.gap || 0) + 12;
const mv = blockHeight / 2 + (cn.gap || 0) + 6;
const fitted = Math.min(
w.globals.radialSize,
w.layout.gridWidth / 2 - mh,
w.layout.gridHeight / 2 - mv
);
w.globals.radialSize = Math.max(fitted, this.defaultSize * 0.15);
w.globals.pieExternalLabelMarginY = mv;
const heightStr = w.config.chart.height ? String(w.config.chart.height) : "";
const userSetFixedHeight = heightStr !== "" && heightStr !== "auto";
this.centerY = userSetFixedHeight ? w.layout.gridHeight / 2 : w.globals.radialSize + mv;
}
/**
* @param {any[]} series
*/
draw(series) {
var _a;
const self = this;
const w = this.w;
const graphics = new Graphics(this.w);
const elPie = graphics.group({
class: "apexcharts-pie"
});
if (w.globals.noData) return elPie;
let total = 0;
for (let k = 0; k < series.length; k++) {
total += Utils.negToZero(series[k]);
}
const sectorAngleArr = [];
const elSeries = graphics.group();
if (total === 0) {
total = 1e-5;
}
series.forEach((m) => {
this.maxY = Math.max(this.maxY, m);
});
if (w.config.yaxis[0].max) {
this.maxY = w.config.yaxis[0].max;
}
if (w.config.grid.position === "back" && this.chartType === "polarArea") {
this.drawPolarElements(elPie);
}
const collapsedIdx = w.globals.collapsedSeriesIndices || [];
let polarVisible = 1;
if (this.chartType === "polarArea") {
let visible = 0;
for (let k = 0; k < series.length; k++) {
if (collapsedIdx.indexOf(k) === -1) visible++;
}
polarVisible = Math.max(1, visible);
}
for (let i = 0; i < series.length; i++) {
const angle = this.fullAngle * Utils.negToZero(series[i]) / total;
sectorAngleArr.push(angle);
if (this.chartType === "polarArea") {
sectorAngleArr[i] = collapsedIdx.indexOf(i) > -1 ? 0 : this.fullAngle / polarVisible;
this.sliceSizes.push(
w.globals.radialSize * series[i] / (this.maxY || 1)
);
} else {
this.sliceSizes.push(w.globals.radialSize);
}
}
const morphActive = ((_a = this.ctx.morphTypeChange) == null ? void 0 : _a.isActive()) === true;
if (w.globals.dataChanged && !morphActive) {
if (this.chartType === "polarArea") {
const prevValues = w.globals.previousPaths;
const stash = w.globals.prevPolarAngles;
if (Array.isArray(stash) && stash.length === prevValues.length) {
this.prevSectorAngleArr = stash.slice();
} else {
for (let i = 0; i < prevValues.length; i++) {
this.prevSectorAngleArr.push(
this.fullAngle / Math.max(1, prevValues.length)
);
}
}
let prevMaxY = 0;
for (let k = 0; k < prevValues.length; k++) {
prevMaxY = Math.max(prevMaxY, Utils.negToZero(prevValues[k]));
}
if (w.config.yaxis[0].max) {
prevMaxY = w.config.yaxis[0].max;
}
this.prevSliceSizes = prevValues.map(
(v) => w.globals.radialSize * Utils.negToZero(v) / (prevMaxY || 1)
);
} else {
let prevTotal = 0;
for (let k = 0; k < w.globals.previousPaths.length; k++) {
prevTotal += Utils.negToZero(w.globals.previousPaths[k]);
}
let previousAngle;
for (let i = 0; i < w.globals.previousPaths.length; i++) {
previousAngle = this.fullAngle * Utils.negToZero(w.globals.previousPaths[i]) / prevTotal;
this.prevSectorAngleArr.push(previousAngle);
}
}
}
if (this.chartType === "polarArea") {
w.globals.prevPolarAngles = sectorAngleArr.slice();
}
if (this.donutSize < 0) {
this.donutSize = 0;
}
if (this.chartType === "donut") {
const circle = graphics.drawCircle(this.donutSize);
circle.attr({
cx: this.centerX,
cy: this.centerY,
fill: w.config.plotOptions.pie.donut.background ? w.config.plotOptions.pie.donut.background : "transparent"
});
elSeries.add(circle);
}
const elG = self.drawArcs(sectorAngleArr, series);
this.sliceLabels.forEach((s) => {
elG.add(s);
});
elSeries.attr({
transform: `translate(${this.translateX}, ${this.translateY}) scale(${w.config.plotOptions.pie.customScale})`
});
elSeries.add(elG);
elPie.add(elSeries);
if (this.donutDataLabels.show) {
const shouldFadeInLabels = this.initialAnim && !w.globals.resized && !w.globals.dataChanged && this.animDur > 0;
const dataLabels = this.renderInnerDataLabels(
this.dataLabelsGroup,
this.donutDataLabels,
{
hollowSize: this.donutSize,
centerX: this.centerX,
centerY: this.centerY,
opacity: shouldFadeInLabels ? 0 : this.donutDataLabels.show
}
);
if (shouldFadeInLabels) {
const labelsNode = this.dataLabelsGroup.node;
labelsNode.style.transition = "opacity 280ms ease-out";
setTimeout(() => {
labelsNode.style.opacity = "1";
}, this.animDur);
}
elPie.add(dataLabels);
}
if (w.config.grid.position === "front" && this.chartType === "polarArea") {
this.drawPolarElements(elPie);
}
return elPie;
}
// core function for drawing pie arcs
/**
* @param {any[]} sectorAngleArr
* @param {any[]} series
*/
drawArcs(sectorAngleArr, series) {
var _a, _b, _c, _d, _e;
const w = this.w;
const filters = new Filters(this.w);
const graphics = new Graphics(this.w);
const fill = new Fill(this.w);
const g = graphics.group({
class: "apexcharts-slices"
});
this.elHoverOutline = graphics.group({
class: "apexcharts-pie-hover-outline"
});
this.elHoverOutline.node.style.pointerEvents = "none";
this.elHoverOutline.node.style.opacity = "0";
if (w.config.chart.animations.enabled) {
this.elHoverOutline.node.style.transition = HOVER_OUTLINE_TRANSITION;
}
g.add(this.elHoverOutline);
let startAngle = this.initialAngle;
let prevStartAngle = this.initialAngle;
let endAngle = this.initialAngle;
let prevEndAngle = this.initialAngle;
this.strokeWidth = w.config.stroke.show ? w.config.stroke.width : 0;
const morphActive = ((_a = this.ctx.morphTypeChange) == null ? void 0 : _a.isActive()) === true;
for (let i = 0; i < sectorAngleArr.length; i++) {
const elPieArc = graphics.group({
class: `apexcharts-series apexcharts-pie-series`,
seriesName: Utils.escapeString(w.seriesData.seriesNames[i]),
rel: i + 1,
"data:realIndex": i
});
g.add(elPieArc);
startAngle = endAngle;
prevStartAngle = prevEndAngle;
endAngle = startAngle + sectorAngleArr[i];
prevEndAngle = prevStartAngle + this.prevSectorAngleArr[i];
const angle = endAngle < startAngle ? this.fullAngle + endAngle - startAngle : endAngle - startAngle;
const pathFill = fill.fillPath({
seriesNumber: i,
size: this.sliceSizes[i],
value: series[i]
});
const morphFrom = morphActive ? this.ctx.morphTypeChange.getInitialPathFor(i, 0) : null;
const prevSize = this.chartType === "polarArea" && w.globals.dataChanged && !morphActive ? this.prevSliceSizes[i] || 0 : void 0;
const path = morphFrom || this.getChangedPath(prevStartAngle, prevEndAngle, prevSize);
const elPath = graphics.drawPath({
d: path,
// Pie/donut/polarArea data is a single series, so a user-supplied
// `stroke.colors` shorter than the slice count is NOT padded by the
// theme engine (unlike fill colors, which cycle). Without this, only
// slice 0 gets the requested color and the rest fall back to a grey
// default. Cycle the array — matching fill-color behaviour — so a
// single `stroke.colors: ['#fff']` borders every slice as expected.
stroke: Array.isArray(this.lineColorArr) ? (_b = this.lineColorArr[i]) != null ? _b : this.lineColorArr[i % this.lineColorArr.length] : this.lineColorArr,
strokeWidth: 0,
fill: pathFill,
fillOpacity: w.config.fill.opacity,
classes: `apexcharts-pie-area apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
});
elPath.attr({
index: 0,
j: i
});
filters.setSelectionFilter(elPath, 0, i);
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow;
filters.dropShadow(elPath, shadow, i);
}
this.addListeners(elPath, this.donutDataLabels, i);
let labelPosition = {
x: 0,
y: 0
};
const midAngle = (startAngle + angle / 2) % this.fullAngle;
let arcCenter = { x: this.centerX, y: this.centerY };
if (this.chartType === "pie" || this.chartType === "polarArea") {
labelPosition = Utils.polarToCartesian(
this.centerX,
this.centerY,
w.globals.radialSize / 1.25 + w.config.plotOptions.pie.dataLabels.offset,
midAngle
);
arcCenter = Utils.polarToCartesian(
this.centerX,
this.centerY,
w.globals.radialSize / 2,
midAngle
);
} else if (this.chartType === "donut") {
labelPosition = Utils.polarToCartesian(
this.centerX,
this.centerY,
(w.globals.radialSize + this.donutSize) / 2 + w.config.plotOptions.pie.dataLabels.offset,
midAngle
);
arcCenter = Utils.polarToCartesian(
this.centerX,
this.centerY,
(w.globals.radialSize + this.donutSize) / 2,
midAngle
);
}
Graphics.setAttrs(elPath.node, {
"data:angle": angle,
"data:startAngle": startAngle,
"data:strokeWidth": this.strokeWidth,
"data:value": series[i],
"data:cx": arcCenter.x,
"data:cy": arcCenter.y
});
elPieArc.add(elPath);
let pieceClaimed = false;
if (morphActive) {
const finalD = this.getPiePath({
me: this,
startAngle,
angle,
size: this.sliceSizes[i]
});
elPath.node.setAttribute("data:pathFinal", finalD);
pieceClaimed = ((_d = (_c = this.ctx.morphTypeChange).claimsTargetMark) == null ? void 0 : _d.call(_c, i, 0)) === true;
if (pieceClaimed) {
elPath.attr({ d: finalD });
elPath.node.setAttribute("data:pathOrig", finalD);
elPath.node.setAttribute("opacity", "0");
elPath.node.setAttribute("data-piece-hidden", "1");
}
}
let dur = 0;
if (this.initialAnim && !w.globals.resized && !w.globals.dataChanged) {
dur = angle / this.fullAngle * w.config.chart.animations.speed;
if (dur === 0) dur = 1;
this.animDur = dur + this.animDur;
this.animBeginArr.push(this.animDur);
} else {
this.animBeginArr.push(0);
}
if (pieceClaimed) ;
else if (morphActive && morphFrom) {
const targetD = this.getPiePath({
me: this,
startAngle,
angle,
size: this.sliceSizes[i]
});
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
const animations = this.ctx.animations;
elPath.node.setAttribute("data:pathOrig", targetD);
const morphRunner = elPath.animate(morphSpeed).plot(targetD, "polygons").attr({ "stroke-width": this.strokeWidth });
if (morphRunner && typeof morphRunner.after === "function") {
morphRunner.after(() => animations.animationCompleted(elPath));
} else {
animations.animationCompleted(elPath);
}
} else if (this.dynamicAnim && w.globals.dataChanged) {
this.animatePaths(elPath, {
size: this.sliceSizes[i],
prevSize,
endAngle,
startAngle,
prevStartAngle,
prevEndAngle,
animateStartingPos: true,
i,
animBeginArr: this.animBeginArr,
shouldSetPrevPaths: true,
dur: w.config.chart.animations.dynamicAnimation.speed
});
} else {
this.animatePaths(elPath, {
size: this.sliceSizes[i],
endAngle,
startAngle,
i,
totalItems: sectorAngleArr.length - 1,
animBeginArr: this.animBeginArr,
dur
});
}
if (this.getExpandOffset() > 0) {
elPath.node.addEventListener("mouseup", this.pieClicked.bind(this, i));
} else if (i === 0 && Filters.drilldownBlocksSliceOffset(w)) {
(_e = this.ctx.drilldown) == null ? void 0 : _e.warnSliceOffsetDisabled();
}
if (typeof w.interact.selectedDataPoints[0] !== "undefined" && w.interact.selectedDataPoints[0].indexOf(i) > -1) {
if (this.initialAnim && !w.globals.resized && !w.globals.dataChanged && this.animDur > 0) {
const _this = this;
const _i = i;
setTimeout(() => _this.pieClicked(_i, { animate: false }), this.animDur);
} else {
this.pieClicked(i, { animate: false });
}
}
if (w.config.dataLabels.enabled) {
const xPos = labelPosition.x;
const yPos = labelPosition.y;
let text = 100 * angle / this.fullAngle + "%";
if (angle !== 0 && w.config.plotOptions.pie.dataLabels.minAngleToShowLabel < sectorAngleArr[i]) {
const formatter = w.config.dataLabels.formatter;
if (formatter !== void 0) {
text = formatter(w.globals.seriesPercent[i][0], {
seriesIndex: i,
w
});
}
const foreColor = w.globals.dataLabels.style.colors[i];
const elPieLabelWrap = graphics.group({
class: `apexcharts-datalabels`
});
const elPieLabel = graphics.drawText({
x: xPos,
y: yPos,
text,
textAnchor: "middle",
fontSize: w.config.dataLabels.style.fontSize,
fontFamily: w.config.dataLabels.style.fontFamily,
fontWeight: w.config.dataLabels.style.fontWeight,
foreColor
});
elPieLabelWrap.add(elPieLabel);
if (w.config.dataLabels.dropShadow.enabled) {
const textShadow = w.config.dataLabels.dropShadow;
filters.dropShadow(elPieLabel, textShadow);
}
elPieLabel.node.classList.add("apexcharts-pie-label");
if (w.config.chart.animations.animate && w.globals.resized === false) {
elPieLabel.node.classList.add("apexcharts-pie-label-delay");
elPieLabel.node.style.animationDelay = w.config.chart.animations.speed / 940 + "s";
}
this.sliceLabels.push(elPieLabelWrap);
this.sliceLabelGroups[i] = elPieLabelWrap.node;
}
}
if (this.showExternalLabels && angle !== 0) {
const lines = this.getExternalLabelLines(i);
if (lines.some((l) => l !== "")) {
const anchor = Utils.polarToCartesian(
this.centerX,
this.centerY,
w.globals.radialSize,
midAngle
);
const elbow = Utils.polarToCartesian(
this.centerX,
this.centerY,
w.globals.radialSize + (this.externalCfg.connector.gap || 0),
midAngle
);
const isRight = elbow.x >= this.centerX;
const baseLabelX = isRight ? elbow.x + (this.externalCfg.connector.length || 0) : elbow.x - (this.externalCfg.connector.length || 0);
this.externalLabels.push({
i,
lines,
anchor,
elbow,
side: isRight ? "right" : "left",
labelX: baseLabelX + parseFloat(this.externalCfg.offsetX || 0),
idealY: elbow.y + parseFloat(this.externalCfg.offsetY || 0),
connectorColor: this.externalCfg.connector.color || w.globals.colors[i],
foreColor: this.externalCfg.color || w.config.chart.foreColor
});
}
}
}
if (this.showExternalLabels && this.externalLabels.length) {
this.placeExternalLabels();
const revealOnAnimEnd = Environment.isBrowser() && (morphActive || this.dynamicAnim && w.globals.dataChanged || this.initialAnim && !w.globals.resized && !w.globals.dataChanged);
this.externalLabels.forEach((lbl) => {
const group = new CircularChartsHelpers(w).drawExternalLabel({
lines: lbl.lines,
lineHeight: this.externalLabelLineH,
anchor: lbl.anchor,
elbow: lbl.elbow,
labelX: lbl.labelX,
labelY: lbl.labelY,
side: lbl.side,
connector: {
show: this.externalCfg.connector.show,
width: this.externalCfg.connector.width,
color: lbl.connectorColor
},
style: this.externalLabelStyle,
foreColor: lbl.foreColor
});
if (revealOnAnimEnd) {
group.node.classList.add("apexcharts-element-hidden");
w.globals.delayedElements.push({ el: group.node });
}
this.externalLabelGroups[lbl.i] = group.node;
g.add(group);
});
}
return g;
}
/**
* Vertical de-overlap for outer (name) labels: per side, sort by ideal y and
* push neighbours apart so they keep at least one line-height of spacing.
* Mutates each entry's `labelY`. Connector lines re-route to the moved y.
*/
placeExternalLabels() {
const w = this.w;
const lineHeight = this.externalLabelMaxLines * this.externalLabelLineH + 2;
const maxY = this.centerY + w.globals.radialSize + w.globals.pieExternalLabelMarginY;
["left", "right"].forEach((side) => {
spaceOutLabels(
this.externalLabels.filter((l) => l.side === side),
lineHeight,
maxY
);
});
}
/**
* @param {any} elPath
* @param {Record<string, any>} dataLabels
* @param {number} [i] slice index, for the hover outline band
*/
addListeners(elPath, dataLabels, i) {
const graphics = new Graphics(this.w, this.ctx);
elPath.node.addEventListener(
"mouseenter",
graphics.pathMouseEnter.bind(graphics, elPath)
);
elPath.node.addEventListener(
"mouseleave",
graphics.pathMouseLeave.bind(graphics, elPath)
);
elPath.node.addEventListener(
"mouseleave",
this.revertDataLabelsInner.bind(this)
);
if (typeof i === "number") {
elPath.node.addEventListener(
"mouseenter",
this.showHoverOutline.bind(this, i)
);
elPath.node.addEventListener(
"mouseleave",
this.hideHoverOutline.bind(this)
);
}
elPath.node.addEventListener(
"mousedown",
graphics.pathMouseDown.bind(graphics, elPath)
);
if (!this.donutDataLabels.total.showAlways) {
elPath.node.addEventListener(
"mouseenter",
this.printDataLabelsInner.bind(this, elPath.node, dataLabels)
);
elPath.node.addEventListener(
"mousedown",
this.printDataLabelsInner.bind(this, elPath.node, dataLabels)
);
}
}
// This function can be used for other circle charts too
/**
* @param {any} el
* @param {Record<string, any>} opts
*/
animatePaths(el, opts) {
const w = this.w;
const me = this;
let angle = opts.endAngle < opts.startAngle ? this.fullAngle + opts.endAngle - opts.startAngle : opts.endAngle - opts.startAngle;
let prevAngle = angle;
let fromStartAngle = opts.startAngle;
const toStartAngle = opts.startAngle;
if (opts.prevStartAngle !== void 0 && opts.prevEndAngle !== void 0) {
fromStartAngle = opts.prevEndAngle;
prevAngle = opts.prevEndAngle < opts.prevStartAngle ? this.fullAngle + opts.prevEndAngle - opts.prevStartAngle : opts.prevEndAngle - opts.prevStartAngle;
}
if (opts.i === w.config.series.length - 1) {
if (angle + toStartAngle > this.fullAngle) {
opts.endAngle = opts.endAngle - (angle + toStartAngle);
} else if (angle + toStartAngle < this.fullAngle) {
opts.endAngle = opts.endAngle + (this.fullAngle - (angle + toStartAngle));
}
}
if (angle === this.fullAngle) angle = this.fullAngle - 0.01;
me.animateArc(el, fromStartAngle, toStartAngle, angle, prevAngle, opts);
}
/**
* @param {any} el
* @param {number} fromStartAngle
* @param {number} toStartAngle
* @param {number} angle
* @param {number} prevAngle
* @param {Record<string, any>} opts
*/
animateArc(el, fromStartAngle, toStartAngle, angle, prevAngle, opts) {
const me = this;
const w = this.w;
const animations = new Animations(this.w);
const size = opts.size;
let path;
if (isNaN(fromStartAngle) || isNaN(prevAngle)) {
fromStartAngle = toStartAngle;
prevAngle = angle;
opts.dur = 0;
}
let currAngle = angle;
let startAngle = toStartAngle;
const fromAngle = fromStartAngle < toStartAngle ? this.fullAngle + fromStartAngle - toStartAngle : fromStartAngle - toStartAngle;
const hasPrevSize = typeof opts.prevSize === "number";
if (w.globals.dataChanged && opts.shouldSetPrevPaths) {
if (opts.prevEndAngle) {
path = me.getPiePath({
me,
startAngle: opts.prevStartAngle,
angle: opts.prevEndAngle < opts.prevStartAngle ? this.fullAngle + opts.prevEndAngle - opts.prevStartAngle : opts.prevEndAngle - opts.prevStartAngle,
size: hasPrevSize ? opts.prevSize : size
});
el.attr({ d: path });
}
}
if (opts.dur !== 0) {
el.animate(opts.dur, opts.animBeginArr[opts.i]).after(
/** @this {any} */
function() {
if (me.chartType === "pie" || me.chartType === "donut" || me.chartType === "polarArea") {
this.animate(
w.config.chart.animations.dynamicAnimation.speed
).attr({
"stroke-width": me.strokeWidth
});
}
if (opts.i === w.config.series.length - 1) {
animations.animationCompleted(el);
}
}
).during((pos) => {
currAngle = fromAngle + (angle - fromAngle) * pos;
if (opts.animateStartingPos) {
currAngle = prevAngle + (angle - prevAngle) * pos;
startAngle = fromStartAngle - prevAngle + (toStartAngle - (fromStartAngle - prevAngle)) * pos;
}
path = me.getPiePath({
me,
startAngle,
angle: currAngle,
size: hasPrevSize ? opts.prevSize + (size - opts.prevSize) * pos : size
});
el.node.setAttribute("data:pathOrig", path);
el.attr({
d: path
});
});
} else {
path = me.getPiePath({
me,
startAngle,
angle,
size
});
if (!opts.isTrack) {
w.globals.animationEnded = true;
}
el.node.setAttribute("data:pathOrig", path);
el.attr({
d: path,
"stroke-width": me.strokeWidth
});
}
}
/**
* Toggle slice `i` in or out of the pie. Only one slice sits outside at a
* time. Bound to `mouseup` on each slice, and also reached from the
* `toggleDataPointSelection` API and from the pre-selected-slice pass in
* drawArcs (which passes `animate: false` so the slice is already parked by
* the time the first frame is painted).
* @param {number} i
* @param {{animate?: boolean}} [opts] a MouseEvent when called as a listener,
* which carries no `animate`, so real clicks animate
*/
pieClicked(i, opts) {
const w = this.w;
const me = this;
const animate = !(opts && opts.animate === false);
const elPath = w.dom.Paper.findOne(
`.apexcharts-${me.chartType.toLowerCase()}-slice-${i}`
);
if (!elPath) return;
if (elPath.attr("data:pieClicked") === "true") {
elPath.attr({
"data:pieClicked": "false"
});
this.revertDataLabelsInner();
this.offsetSlice(i, 0, animate);
return;
}
const allEls = w.dom.baseEl.getElementsByClassName("apexcharts-pie-area");
Array.prototype.forEach.call(allEls, (pieSlice) => {
const wasOut = pieSlice.getAttribute("data:pieClicked") === "true";
pieSlice.setAttribute("data:pieClicked", "false");
if (wasOut) {
this.offsetSlice(parseInt(pieSlice.getAttribute("j"), 10), 0, animate);
}
});
w.interact.capturedDataPointIndex = i;
elPath.attr("data:pieClicked", "true");
this.offsetSlice(i, this.getExpandOffset(), animate);
}
/**
* How far a clicked slice slides out, in px. 0 when the pull-out is off, when
* drilldown owns the click, and always for polarArea: there the radius
* encodes the value, so moving a slice outward would read as a bigger number.
* @returns {number}
*/
getExpandOffset() {
const pie = this.w.config.plotOptions.pie;
if (!pie.expandOnClick || this.chartType === "polarArea") return 0;
if (Filters.drilldownBlocksSliceOffset(this.w)) return 0;
const offset = Number(pie.expandOffset);
return Number.isFinite(offset) && offset > 0 ? offset : 0;
}
/**
* Every node that has to travel with slice `i`: the slice path itself plus
* its labels, which live in sibling groups so they paint above all slices.
* @param {number} i
* @returns {SVGElement[]}
*/
getSliceMovers(i) {
const elPath = this.w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
return [
elPath ? elPath.node : null,
this.sliceLabelGroups[i],
this.externalLabelGroups[i],
// The hover band, when it is this slice's: clicking a slice you are
// hovering has to take its outline along, or the band is left behind
// sitting in the gap the slice just opened.
this.hoverOutlineIndex === i && this.elHoverOutlinePath ? this.elHoverOutlinePath.node : null
].filter(Boolean);
}
/**
* Slide slice `i` `dist` px out of the pie along its own mid-angle, or back
* to its resting place when `dist` is 0.
*
* The slice is *translated*, never re-drawn: the arc keeps the exact radius
* and span it had, so the pulled-out slice still encodes the same quantity
* (growing the radius, as this used to, quietly inflates it) and a clean gap
* opens between it and the rest of the pie.
* @param {number} i
* @param {number} dist
* @param {boolean} [animate] false to park it instantly, with no transition
*/
offsetSlice(i, dist, animate = true) {
const w = this.w;
const elPath = w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
if (!elPath) return;
const { dx, dy } = this.getSliceOffsetVector(i, dist);
const transform = `translate(${dx} ${dy})`;
const transition = animate && w.config.chart.animations.enabled ? SLICE_OFFSET_TRANSITION : "";
this.getSliceMovers(i).forEach((node) => {
node.style.transition = transition;
node.setAttribute("transform", transform);
});
}
/**
* The px vector `dist` along slice `i`'s mid-angle. Zero for a slice that
* fills the pie: there is no "outside" for it to move to, and sliding it
* would just shift the whole chart sideways.
* @param {number} i
* @param {number} dist
* @returns {{dx: number, dy: number}}
*/
getSliceOffsetVector(i, dist) {
const elPath = this.w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
const angle = elPath ? parseFloat(elPath.attr("data:angle")) : NaN;
if (!dist || !Number.isFinite(angle) || angle >= this.fullAngle) {
return { dx: 0, dy: 0 };
}
const startAngle = parseFloat(elPath.attr("data:startAngle"));
const midRad = Math.PI * (startAngle + angle / 2 - 90) / 180;
return { dx: dist * Math.cos(midRad), dy: dist * Math.sin(midRad) };
}
/**
* Fade in the hover outline: a translucent band traced just outside the rim
* of slice `i`, in the slice's own colour. It replaces lightening the slice
* (see Filters.hoverOutlineOwnsHoverState) so a hovered slice keeps the
* colour the legend and the data labels claim it has.
* @param {number} i
*/
showHoverOutline(i) {
const w = this.w;
if (!Filters.hoverOutlineOwnsHoverState(w)) return;
if (!this.elHoverOutline) return;
const path = this.getHoverOutlinePath(i);
if (!path) return;
const cfg = w.config.plotOptions.pie.hoverOutline;
const fill = cfg.color || w.globals.colors[i];
if (!this.elHoverOutlinePath) {
const graphics = new Graphics(w);
this.elHoverOutlinePath = graphics.drawPath({
d: path,
fill,
strokeWidth: 0,
classes: "apexcharts-pie-hover-outline-band"
});
this.elHoverOutline.add(this.elHoverOutlinePath);
}
this.elHoverOutlinePath.attr({
d: path,
fill,
"fill-opacity": cfg.opacity
});
const { dx, dy } = this.getSliceOffsetVector(
i,
this.isSliceOut(i) ? this.getExpandOffset() : 0
);
this.elHoverOutlinePath.node.style.transition = "";
this.elHoverOutlinePath.node.setAttribute("transform", `translate(${dx} ${dy})`);
this.hoverOutlineIndex = i;
this.elHoverOutline.node.style.opacity = "1";
}
/** Fade the hover outline back out, leaving the band node in place. */
hideHoverOutline() {
if (this.elHoverOutline) {
this.elHoverOutline.node.style.opacity = "0";
}
}
/** @param {number} i @returns {boolean} */
isSliceOut(i) {
const elPath = this.w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
return !!elPath && elPath.attr("data:pieClicked") === "true";
}
/**
* Band geometry for the hover outline of slice `i`: an annulus from the
* slice rim (plus the stroke and the configured clearance) outward, over the
* same angular extent the slice is actually drawn over, so it lines up with
* both slice edges even with `spacing` insetting them. Rounded into a pill
* when the band is thick enough for the fillets to fit.
* @param {number} i
* @returns {string | null}
*/
getHoverOutlinePath(i) {
const w = this.w;
const cfg = w.config.plotOptions.pie.hoverOutline;
const elPath = w.dom.Paper.findOne(
`.apexcharts-${this.chartType.toLowerCase()}-slice-${i}`
);
if (!elPath) return null;
const angle = parseFloat(elPath.attr("data:angle"));
if (!Number.isFinite(angle) || angle <= 0) return null;
const size = this.sliceSizes[i];
const thickness = Number(cfg.size);
if (!Number.isFinite(size) || !Number.isFinite(thickness) || thickness <= 0) {
return null;
}
const { startDeg, spanDeg } = this.getSliceExtent({
me: this,
startAngle: parseFloat(elPath.attr("data:startAngle")),
angle,
size
});
if (!(spanDeg > 0)) return null;
const rIn = size + (this.strokeWidth || 0) / 2 + (Number(cfg.gap) || 0);
const rOut = rIn + thickness;
const geo = {
cx: this.centerX,
cy: this.centerY,
rIn,
rOut,
a0: startDeg,
a1: startDeg + spanDeg,
spanDeg
};
const r = Math.min(thickness / 2, spanDeg * Math.PI / 180 / 2 * rIn);
return r > 0.5 ? roundedDonutSegmentPath(__spreadProps(__spreadValues({}, geo), { r })) : sharpDonutSegmentPath(geo);
}
/**
* @param {number} prevStartAngle
* @param {number} prevEndAngle
*/
/**
* @param {number} prevStartAngle
* @param {number} prevEndAngle
* @param {number} [prevSize] - polarArea passes its previous slice radius,
* so the pre-animation frame sits at the previous VALUE too, not just the
* previous angles.
*/
getChangedPath(prevStartAngle, prevEndAngle, prevSize) {
let path = "";
if (this.dynamicAnim && this.w.globals.dataChanged) {
path = this.getPiePath({
me: this,
startAngle: prevStartAngle,
angle: prevEndAngle - prevStartAngle,
// @ts-ignore — size is set dynamically during draw()
size: typeof prevSize === "number" ? prevSize : this.size
});
}
return path;
}
/**
* The angular extent a slice is actually drawn over: the raw start / span
* clamped so a full circle never overlaps itself, then inset by
* `plotOptions.pie.spacing`. Shared by getPiePath and the hover outline, so
* the band lines up with the slice edges instead of with the un-inset angles
* cached on the path node.
* @param {{me: any, startAngle: number, angle: number, size: number}} opts
* @returns {{startDeg: number, spanDeg: number, endDeg: number}}
*/
getSliceExtent({ me, startAngle, angle, size }) {
const w = this.w;
let startDeg = startAngle;
let endDeg = angle + startAngle;
if (Math.ceil(endDeg) >= this.fullAngle + this.w.config.plotOptions.pie.startAngle % this.fullAngle) {
endDeg = this.fullAngle + this.w.config.plotOptions.pie.startAngle % this.fullAngle - 0.01;
}
let spanDeg = endDeg - startDeg;
const isSliceType = me.chartType === "pie" || me.chartType === "donut" || me.chartType === "polarArea";
const spacing = w.config.plotOptions.pie.spacing;
if (isSliceType && spacing > 0 && spanDeg > 0) {
const rRef = me.chartType === "donut" ? (size + me.donutSize) / 2 : size;
const gapDeg = rRef > 0 ? spacing / rRef * (180 / Math.PI) : 0;
const inset = Math.min(gapDeg / 2, Math.max(0, spanDeg / 2 - 0.5));
startDeg += inset;
spanDeg -= 2 * inset;
}
endDeg = startDeg + spanDeg;
if (Math.ceil(endDeg) > this.fullAngle) endDeg -= this.fullAngle;
return { startDeg, spanDeg, endDeg };
}
/** @param {{me: any, startAngle: any, angle: any, size: any}} opts */
getPiePath({ me, startAngle, angle, size }) {
let path;
const w = this.w;
const graphics = new Graphics(this.w);
const { startDeg, spanDeg, endDeg } = this.getSliceExtent({
me,
startAngle,
angle,
size
});
const isSliceType = me.chartType === "pie" || me.chartType === "donut" || me.chartType === "polarArea";
const startRadians = Math.PI * (startDeg - 90) / 180;
const borderRadius = w.config.plotOptions.pie.borderRadius;
if (borderRadius > 0 && isSliceType) {
const roundedPath = this.getRoundedSlicePath({
me,
startDeg,
spanDeg,
size,
borderRadius
});
if (roundedPath) return roundedPath;
}
const endRadians = Math.PI * (endDeg - 90) / 180;
const x1 = me.centerX + size * Math.cos(startRadians);
const y1 = me.centerY + size * Math.sin(startRadians);
const x2 = me.centerX + size * Math.cos(endRadians);
const y2 = me.centerY + size * Math.sin(endRadians);
const startInner = Utils.polarToCartesian(
me.centerX,
me.centerY,
me.donutSize,
endDeg
);
const endInner = Utils.polarToCartesian(
me.centerX,
me.centerY,
me.donutSize,
startDeg
);
const largeArc = spanDeg > 180 ? 1 : 0;
const pathBeginning = ["M", x1, y1, "A", size, size, 0, largeArc, 1, x2, y2];
if (me.chartType === "donut") {
path = [
...pathBeginning,
"L",
startInner.x,
startInner.y,
"A",
me.donutSize,
me.donutSize,
0,
largeArc,
0,
endInner.x,
endInner.y,
"L",
x1,
y1,
"z"
].join(" ");
} else if (me.chartType === "pie" || me.chartType === "polarArea") {
path = [...pathBeginning, "L", me.centerX, me.centerY, "L", x1, y1].join(
" "
);
} else {
path = [...pathBeginning].join(" ");
}
return graphics.roundPathCorners(path, this.strokeWidth * 2);
}
/**
* Build a slice path with rounded corners (plotOptions.pie.borderRadius).
*
* The generic roundPathCorners() only rounds line->line joins, but a slice
* corner is an arc<->line join, so we construct the fillets explicitly here:
* every corner is inset by the (clamped) radius along both the arc and the
* radial edge, and a quadratic Bezier with its control point at the original
* sharp corner bridges the two inset points. Donut slices round all four
* corners; pie / polarArea slices round the two outer corners and keep the
* center apex sharp.
*
* Returns null when the slice is too small to round meaningfully, so the
* caller can fall back to a sharp-corner path.
*
* @param {{me: any, startDeg: number, spanDeg: number, size: number, borderRadius: number}} opts
* @returns {string | null}
*/
getRoundedSlicePath({ me, startDeg, spanDeg, size, borderRadius }) {
if (!(spanDeg > 0)) return null;
const D2R2 = Math.PI / 180;
const cx = me.centerX;
const cy = me.centerY;
const isDonut = me.chartType === "donut";
const rOut = size;
const rIn = isDonut ? me.donutSize : 0;
const spanRad = spanDeg * D2R2;
let r = borderRadius;
r = Math.min(r, spanRad * rOut / 2);
if (isDonut) {
r = Math.min(r, spanRad * rIn / 2);
r = Math.min(r, (rOut - rIn) / 2);
} else {
r = Math.min(r, rOut / 2);
}
if (!(r > 0.5)) return null;
const a0 = startDeg;
const a1 = startDeg + spanDeg;
if (isDonut) {
return roundedDonutSegmentPath({ cx, cy, rIn, rOut, a0, a1, r, spanDeg });
}
return roundedPieSegmentPath({ cx, cy, rOut, a0, a1, r, spanDeg });
}
/**
* @param {any} parent
*/
drawPolarElements(parent) {
const w = this.w;
const scale = new Scales(this.w);
const graphics = new Graphics(this.w);
const helpers = new CircularChartsHelpers(this.w);
const gCircles = graphics.group();
const gYAxis = graphics.group();
const yScale = scale.niceScale(0, Math.ceil(this.maxY), 0);
const yTexts = yScale.result.reverse();
const len = yScale.result.length;
this.maxY = yScale.niceMax;
let circleSize = w.globals.radialSize;
const diff = circleSize / (len - 1);
for (let i = 0; i < len - 1; i++) {
const circle = graphics.drawCircle(circleSize);
circle.attr({
cx: this.centerX,
cy: this.centerY,
fill: "none",
"stroke-width": w.config.plotOptions.polarArea.rings.strokeWidth,
stroke: w.config.plotOptions.polarArea.rings.strokeColor
});
if (w.config.yaxis[0].show) {
const yLabel = helpers.drawYAxisTexts(
this.centerX,
this.centerY - circleSize + parseInt(w.config.yaxis[0].labels.style.fontSize, 10) / 2,
i,
yTexts[i]
);
gYAxis.add(yLabel);
}
gCircles.add(circle);
circleSize = circleSize - diff;
}
this.drawSpokes(parent);
parent.add(gCircles);
parent.add(gYAxis);
}
/**
* @param {any} dataLabelsGroup
* @param {Record<string, any>} dataLabelsConfig
* @param {Record<string, any>} opts
*/
renderInnerDataLabels(dataLabelsGroup, dataLabelsConfig, opts) {
const w = this.w;
const graphics = new Graphics(this.w);
const showTotal = dataLabelsConfig.total.show;
dataLabelsGroup.node.innerHTML = "";
dataLabelsGroup.node.style.opacity = opts.opacity;
const x = opts.centerX;
const y = !this.donutDataLabels.total.label ? opts.centerY - opts.centerY / 6 : opts.centerY;
let labelColor, valueColor;
if (dataLabelsConfig.name.color === void 0) {
labelColor = w.globals.colors[0];
} else {
labelColor = dataLabelsConfig.name.color;
}
let labelFontSize = dataLabelsConfig.name.fontSize;
let labelFontFamily = dataLabelsConfig.name.fontFamily;
let labelFontWeight = dataLabelsConfig.name.fontWeight;
if (dataLabelsConfig.value.color === void 0) {
valueColor = w.config.chart.foreColor;
} else {
valueColor = dataLabelsConfig.value.color;
}
const lbFormatter = dataLabelsConfig.value.formatter;
let val = "";
let name = "";
if (showTotal) {
labelColor = dataLabelsConfig.total.color;
labelFontSize = dataLabelsConfig.total.fontSize;
labelFontFamily = dataLabelsConfig.total.fontFamily;
labelFontWeight = dataLabelsConfig.total.fontWeight;
name = !this.donutDataLabels.total.label ? "" : dataLabelsConfig.total.label;
val = dataLabelsConfig.total.formatter(w);
} else {
if (w.seriesData.series.length === 1) {
val = lbFormatter(w.seriesData.series[0], w);
name = w.seriesData.seriesNames[0];
}
}
if (name) {
name = dataLabelsConfig.name.formatter(
name,
dataLabelsConfig.total.show,
w
);
}
if (dataLabelsConfig.name.show) {
const elLabel = graphics.drawText({
x,
y: y + parseFloat(dataLabelsConfig.name.offsetY),
text: name,
textAnchor: "middle",
foreColor: labelColor,
fontSize: labelFontSize,
fontWeight: labelFontWeight,
fontFamily: labelFontFamily
});
elLabel.node.classList.add("apexcharts-datalabel-label");
dataLabelsGroup.add(elLabel);
}
if (dataLabelsConfig.value.show) {
const valOffset = dataLabelsConfig.name.show ? parseFloat(dataLabelsConfig.value.offsetY) + 16 : dataLabelsConfig.value.offsetY;
const elValue = graphics.drawText({
x,
y: y + valOffset,
text: val,
textAnchor: "middle",
foreColor: valueColor,
fontWeight: dataLabelsConfig.value.fontWeight,
fontSize: dataLabelsConfig.value.fontSize,
fontFamily: dataLabelsConfig.value.fontFamily
});
elValue.node.classList.add("apexcharts-datalabel-value");
dataLabelsGroup.add(elValue);
}
return dataLabelsGroup;
}
/**
*
* @param {string} name - The name of the series
* @param {string} val - The value of that series
* @param {any} el - Optional el (indicates which series was hovered/clicked). If this param is not present, means we need to show total
* @param {Record<string, any>} labelsConfig
*/
printInnerLabels(labelsConfig, name, val, el) {
const w = this.w;
let labelColor;
if (el) {
if (labelsConfig.name.color === void 0) {
labelColor = w.globals.colors[parseInt(el.parentNode.getAttribute("rel"), 10) - 1];
} else {
labelColor = labelsConfig.name.color;
}
} else {
if (w.seriesData.series.length > 1 && labelsConfig.total.show) {
labelColor = labelsConfig.total.color;
}
}
const elLabel = w.dom.baseEl.querySelector(".apexcharts-datalabel-label");
const elValue = w.dom.baseEl.querySelector(".apexcharts-datalabel-value");
const lbFormatter = labelsConfig.value.formatter;
val = lbFormatter(val, w);
if (!el && typeof labelsConfig.total.formatter === "function") {
val = labelsConfig.total.formatter(w);
}
const isTotal = name === labelsConfig.total.label;
name = !this.donutDataLabels.total.label ? "" : labelsConfig.name.formatter(name, isTotal, w);
if (elLabel !== null) {
elLabel.textContent = name;
}
if (elValue !== null) {
elValue.textContent = val;
}
if (elLabel !== null) {
const elLabelEl = (
/** @type {HTMLElement} */
elLabel
);
elLabelEl.style.fill = labelColor;
}
}
/**
* @param {any} el
* @param {Record<string, any>} dataLabelsConfig
*/
printDataLabelsInner(el, dataLabelsConfig) {
const w = this.w;
const val = el.getAttribute("data:value");
const name = w.seriesData.seriesNames[parseInt(el.parentNode.getAttribute("rel"), 10) - 1];
if (w.seriesData.series.length > 1) {
this.printInnerLabels(dataLabelsConfig, name, val, el);
}
const dataLabelsGroup = w.dom.baseEl.querySelector(
".apexcharts-datalabels-group"
);
if (dataLabelsGroup !== null) {
const dataLabelsGroupEl = (
/** @type {HTMLElement} */
dataLabelsGroup
);
dataLabelsGroupEl.style.opacity = "1";
}
}
/**
* @param {any} parent
*/
drawSpokes(parent) {
const w = this.w;
const graphics = new Graphics(this.w);
const spokeConfig = w.config.plotOptions.polarArea.spokes;
if (spokeConfig.strokeWidth === 0) return;
const spokes = [];
const angleDivision = 360 / w.seriesData.series.length;
for (let i = 0; i < w.seriesData.series.length; i++) {
spokes.push(
Utils.polarToCartesian(
this.centerX,
this.centerY,
w.globals.radialSize,
w.config.plotOptions.pie.startAngle + angleDivision * i
)
);
}
spokes.forEach((p, i) => {
const line = graphics.drawLine(
p.x,
p.y,
this.centerX,
this.centerY,
Array.isArray(spokeConfig.connectorColors) ? spokeConfig.connectorColors[i] : spokeConfig.connectorColors
);
parent.add(line);
});
}
revertDataLabelsInner() {
const w = this.w;
if (this.donutDataLabels.show) {
const dataLabelsGroup = w.dom.Paper.findOne(
`.apexcharts-datalabels-group`
);
const dataLabels = this.renderInnerDataLabels(
dataLabelsGroup,
this.donutDataLabels,
{
hollowSize: this.donutSize,
centerX: this.centerX,
centerY: this.centerY,
opacity: this.donutDataLabels.show
}
);
const elPie = w.dom.Paper.findOne(
".apexcharts-radialbar, .apexcharts-pie"
);
elPie.add(dataLabels);
}
}
}
const Series = _core.__apex_Series;
const BrowserAPIs = _core.__apex_BrowserAPIs_BrowserAPIs;
class Radial extends Pie {
/**
* @param {import('../types/internal').ChartStateW} w
* @param {import('../types/internal').ChartContext} ctx
*/
constructor(w, ctx) {
super(w, ctx);
this.ctx = ctx;
this.w = w;
this.animBeginArr = [0];
this.animDur = 0;
this.startAngle = w.config.plotOptions.radialBar.startAngle;
this.endAngle = w.config.plotOptions.radialBar.endAngle;
this.totalAngle = Math.abs(
w.config.plotOptions.radialBar.endAngle - w.config.plotOptions.radialBar.startAngle
);
this.trackStartAngle = w.config.plotOptions.radialBar.track.startAngle;
this.trackEndAngle = w.config.plotOptions.radialBar.track.endAngle;
this.barLabels = this.w.config.plotOptions.radialBar.barLabels;
this.donutDataLabels = this.w.config.plotOptions.radialBar.dataLabels;
this.radialDataLabels = this.donutDataLabels;
if (!this.trackStartAngle) this.trackStartAngle = this.startAngle;
if (!this.trackEndAngle) this.trackEndAngle = this.endAngle;
if (this.endAngle === 360) this.endAngle = 359.99;
this.margin = parseInt(w.config.plotOptions.radialBar.track.margin, 10);
this.onBarLabelClick = this.onBarLabelClick.bind(this);
}
/**
* @param {any[]} series
*/
draw(series) {
var _a;
const w = this.w;
const graphics = new Graphics(this.w);
const ret = graphics.group({
class: "apexcharts-radialbar"
});
if (w.globals.noData) return ret;
const elSeries = graphics.group();
const centerY = this.defaultSize / 2;
const centerX = w.layout.gridWidth / 2;
let size = this.defaultSize / 2.05;
if (!w.config.chart.sparkline.enabled) {
size = size - w.config.stroke.width - w.config.chart.dropShadow.blur;
}
const colorArr = w.globals.fill.colors;
const rb = w.config.plotOptions.radialBar;
const hasBands = Array.isArray(rb.bands) && rb.bands.length > 0;
const hideTrack = hasBands && rb.bandsStyle && rb.bandsStyle.hideTrackWhenPresent;
const isNeedleShape = rb.shape === "needle";
if (rb.track.show && !hideTrack) {
const elTracks = this.drawTracks({
size,
centerX,
centerY,
colorArr,
series
});
elSeries.add(elTracks);
}
if (hasBands) {
const elBands = this.drawBands({
size,
centerX,
centerY,
series
});
elSeries.add(elBands);
}
const elG = this.drawArcs({
size,
centerX,
centerY,
colorArr,
series,
// When `needle.showValueArc` is true, render both the filled value-arc
// and the needle on top — required for gauges that want a progress
// ring with a pointer indicator (default still hides the arc when in
// needle shape, preserving prior behavior).
skipValueArc: isNeedleShape && !((_a = rb.needle) == null ? void 0 : _a.showValueArc)
});
if (rb.ticks && rb.ticks.show) {
const elTicks = this.drawTicks({
size,
centerX,
centerY,
series
});
const isInitialMount = this.initialAnim && !w.globals.dataChanged && !w.globals.resized;
if (isInitialMount && Environment.isBrowser() && w.globals.shouldAnimate) {
const ticksNode = elTicks.node;
ticksNode.style.opacity = "0";
ticksNode.style.transition = "opacity 280ms ease-out";
const sweepDur = w.config.chart.animations.speed || 800;
setTimeout(() => {
ticksNode.style.opacity = "1";
}, sweepDur);
}
elSeries.add(elTicks);
}
if (isNeedleShape) {
const elNeedle = this.drawNeedle({
size,
centerX,
centerY,
series
});
elSeries.add(elNeedle);
}
let totalAngle = 360;
if (w.config.plotOptions.radialBar.startAngle < 0) {
totalAngle = this.totalAngle;
}
const angleRatio = (360 - totalAngle) / 360;
w.globals.radialSize = size - size * angleRatio;
if (this.radialDataLabels.value.show) {
const offset = Math.max(
this.radialDataLabels.value.offsetY,
this.radialDataLabels.name.offsetY
);
w.globals.radialSize += offset * angleRatio;
}
elSeries.add(elG.g);
if (w.config.plotOptions.radialBar.hollow.position === "front") {
elG.g.add(elG.elHollow);
if (elG.dataLabels) {
elG.g.add(elG.dataLabels);
}
}
ret.add(elSeries);
return ret;
}
/**
* @param {Record<string, any>} opts
*/
drawTracks(opts) {
const w = this.w;
const graphics = new Graphics(this.w);
const g = graphics.group({
class: "apexcharts-tracks"
});
const filters = new Filters(this.w);
const fill = new Fill(this.w);
const strokeWidth = this.getStrokeWidth(opts);
opts.size = opts.size - strokeWidth / 2;
for (let i = 0; i < opts.series.length; i++) {
const elRadialBarTrack = graphics.group({
class: "apexcharts-radialbar-track apexcharts-track"
});
g.add(elRadialBarTrack);
elRadialBarTrack.attr({
rel: i + 1
});
opts.size = opts.size - strokeWidth - this.margin;
const trackConfig = w.config.plotOptions.radialBar.track;
const pathFill = fill.fillPath({
seriesNumber: 0,
size: opts.size,
fillColors: Array.isArray(trackConfig.background) ? trackConfig.background[i] : trackConfig.background,
solid: true
});
const startAngle = this.trackStartAngle;
let endAngle = this.trackEndAngle;
if (Math.abs(endAngle) + Math.abs(startAngle) >= 360)
endAngle = 360 - Math.abs(this.startAngle) - 0.1;
const elPath = graphics.drawPath({
d: "",
stroke: pathFill,
strokeWidth: strokeWidth * parseInt(trackConfig.strokeWidth, 10) / 100,
fill: "none",
strokeOpacity: trackConfig.opacity,
classes: "apexcharts-radialbar-area"
});
if (trackConfig.dropShadow.enabled) {
const shadow = trackConfig.dropShadow;
filters.dropShadow(elPath, shadow);
}
elRadialBarTrack.add(elPath);
elPath.attr("id", "apexcharts-radialbarTrack-" + i);
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: 0,
dur: 0,
isTrack: true
});
}
return g;
}
/**
* @param {Record<string, any>} opts
*/
drawArcs(opts) {
var _a;
const w = this.w;
const graphics = new Graphics(this.w);
const fill = new Fill(this.w);
const filters = new Filters(this.w);
const g = graphics.group();
const strokeWidth = this.getStrokeWidth(opts);
opts.size = opts.size - strokeWidth / 2;
let hollowFillID = w.config.plotOptions.radialBar.hollow.background;
const hollowSize = opts.size - strokeWidth * opts.series.length - this.margin * opts.series.length - strokeWidth * parseInt(w.config.plotOptions.radialBar.track.strokeWidth, 10) / 100 / 2;
const hollowRadius = hollowSize - w.config.plotOptions.radialBar.hollow.margin;
if (w.config.plotOptions.radialBar.hollow.image !== void 0) {
hollowFillID = this.drawHollowImage(opts, g, hollowSize, hollowFillID);
}
const elHollow = this.drawHollow({
size: hollowRadius,
centerX: opts.centerX,
centerY: opts.centerY,
fill: hollowFillID ? hollowFillID : "transparent"
});
if (w.config.plotOptions.radialBar.hollow.dropShadow.enabled) {
const shadow = w.config.plotOptions.radialBar.hollow.dropShadow;
filters.dropShadow(elHollow, shadow);
}
let shown = 1;
if (!this.radialDataLabels.total.show && w.seriesData.series.length > 1) {
shown = 0;
}
let dataLabels = null;
if (this.radialDataLabels.show) {
const dataLabelsGroup = w.dom.Paper.findOne(
`.apexcharts-datalabels-group`
);
dataLabels = this.renderInnerDataLabels(
dataLabelsGroup,
this.radialDataLabels,
{
hollowSize,
centerX: opts.centerX,
centerY: opts.centerY,
opacity: shown
}
);
}
if (w.config.plotOptions.radialBar.hollow.position === "back") {
g.add(elHollow);
if (dataLabels) {
g.add(dataLabels);
}
}
let reverseLoop = false;
if (w.config.plotOptions.radialBar.inverseOrder) {
reverseLoop = true;
}
const morphActive = ((_a = this.ctx.morphTypeChange) == null ? void 0 : _a.isActive()) === true;
for (let i = reverseLoop ? opts.series.length - 1 : 0; reverseLoop ? i >= 0 : i < opts.series.length; reverseLoop ? i-- : i++) {
const elRadialBarArc = graphics.group({
class: `apexcharts-series apexcharts-radial-series`,
seriesName: Utils.escapeString(w.seriesData.seriesNames[i])
});
g.add(elRadialBarArc);
elRadialBarArc.attr({
rel: i + 1,
"data:realIndex": i
});
Series.addCollapsedClassToSeries(this.w, elRadialBarArc, i);
opts.size = opts.size - strokeWidth - this.margin;
const pathFill = fill.fillPath({
seriesNumber: i,
size: opts.size,
value: opts.series[i]
});
const startAngle = this.startAngle;
let prevStartAngle;
const rb = w.config.plotOptions.radialBar;
const domainMin = typeof rb.min === "number" ? rb.min : 0;
const domainMax = typeof rb.max === "number" ? rb.max : 100;
const domainSpan = domainMax === domainMin ? 1 : domainMax - domainMin;
const valueToFraction = (v) => {
const clamped = Math.min(Math.max(v, domainMin), domainMax);
return Math.max(0, (clamped - domainMin) / domainSpan);
};
const dataValue = valueToFraction(Utils.negToZero(opts.series[i]));
let endAngle = Math.round(this.totalAngle * dataValue) + this.startAngle;
let prevEndAngle;
if (w.globals.dataChanged) {
prevStartAngle = this.startAngle;
prevEndAngle = Math.round(
this.totalAngle * valueToFraction(Utils.negToZero(w.globals.previousPaths[i]))
) + prevStartAngle;
}
const currFullAngle = Math.abs(endAngle) + Math.abs(startAngle);
if (currFullAngle > 360) {
endAngle = endAngle - 0.01;
}
const prevFullAngle = Math.abs(prevEndAngle) + Math.abs(prevStartAngle);
if (prevFullAngle > 360) {
prevEndAngle = prevEndAngle - 0.01;
}
const angle = endAngle - startAngle;
const dashArray = Array.isArray(w.config.stroke.dashArray) ? w.config.stroke.dashArray[i] : w.config.stroke.dashArray;
const morphFrom = morphActive ? this.ctx.morphTypeChange.getInitialPathFor(i, 0) : null;
const morphFromType = morphActive ? this.ctx.morphTypeChange.getFromType() : null;
const morphFromFilled = !!morphFrom && (morphFromType === "bar" || morphFromType === "funnel" || morphFromType === "pyramid" || morphFromType === "pie" || morphFromType === "donut" || morphFromType === "polarArea");
const elPath = graphics.drawPath({
d: morphFrom || "",
stroke: morphFromFilled ? "transparent" : opts.skipValueArc ? "transparent" : pathFill,
strokeWidth: morphFromFilled ? 0 : opts.skipValueArc ? 0 : strokeWidth,
fill: morphFromFilled ? pathFill : "none",
fillOpacity: w.config.fill.opacity,
classes: "apexcharts-radialbar-area apexcharts-radialbar-slice-" + i,
strokeDashArray: dashArray
});
const radialMidAngle = startAngle + angle / 2;
const radialArcCenter = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
opts.size,
radialMidAngle
);
Graphics.setAttrs(elPath.node, {
"data:angle": angle,
"data:value": opts.series[i],
"data:cx": radialArcCenter.x,
"data:cy": radialArcCenter.y
});
if (w.config.chart.dropShadow.enabled) {
const shadow = w.config.chart.dropShadow;
filters.dropShadow(elPath, shadow, i);
}
filters.setSelectionFilter(elPath, 0, i);
this.addListeners(elPath, this.radialDataLabels);
elRadialBarArc.add(elPath);
elPath.attr({
index: 0,
j: i
});
if (this.barLabels.enabled) {
const barStartCords = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
opts.size,
startAngle
);
const text = this.barLabels.formatter(w.seriesData.seriesNames[i], {
seriesIndex: i,
w
});
const classes = ["apexcharts-radialbar-label"];
if (!this.barLabels.onClick) {
classes.push("apexcharts-no-click");
}
let textColor = this.barLabels.useSeriesColors ? w.globals.colors[i] : w.config.chart.foreColor;
if (!textColor) {
textColor = w.config.chart.foreColor;
}
const x = barStartCords.x + this.barLabels.offsetX;
const y = barStartCords.y + this.barLabels.offsetY;
const elText = graphics.drawText({
x,
y,
text,
textAnchor: "end",
dominantBaseline: "middle",
fontFamily: this.barLabels.fontFamily,
fontWeight: this.barLabels.fontWeight,
fontSize: this.barLabels.fontSize,
foreColor: textColor,
cssClass: classes.join(" ")
});
elText.on("click", this.onBarLabelClick);
elText.attr({
rel: i + 1
});
if (startAngle !== 0) {
elText.attr({
"transform-origin": `${x} ${y}`,
transform: `rotate(${startAngle} 0 0)`
});
}
elRadialBarArc.add(elText);
}
let dur = 0;
if (this.initialAnim && !w.globals.resized && !w.globals.dataChanged) {
dur = w.config.chart.animations.speed;
}
if (w.globals.dataChanged) {
dur = w.config.chart.animations.dynamicAnimation.speed;
}
this.animDur = dur / (opts.series.length * 1.2) + this.animDur;
this.animBeginArr.push(this.animDur);
if (morphActive && morphFrom) {
const morphSpeed = this.ctx.morphTypeChange.getSpeed();
const actualArcD = this.getPiePath({
me: this,
startAngle,
angle,
size: opts.size
});
if (morphFromFilled) {
const targetD = this.ctx.morphTypeChange.buildRingSegmentPath(
opts.centerX,
opts.centerY,
opts.size,
strokeWidth,
startAngle,
startAngle + angle
);
elPath.animate(morphSpeed).plot(targetD, "polygons").after(
/** @this {any} */
function() {
this.attr({
d: actualArcD,
fill: "none",
stroke: opts.skipValueArc ? "transparent" : pathFill,
"stroke-width": opts.skipValueArc ? 0 : strokeWidth
});
}
);
} else {
elPath.animate(morphSpeed).plot(actualArcD, "polygons").attr({ "stroke-width": strokeWidth });
}
} else {
this.animatePaths(elPath, {
centerX: opts.centerX,
centerY: opts.centerY,
endAngle,
startAngle,
prevEndAngle,
prevStartAngle,
size: opts.size,
i,
totalItems: 2,
animBeginArr: this.animBeginArr,
dur,
shouldSetPrevPaths: true
});
}
}
return {
g,
elHollow,
dataLabels
};
}
/**
* Map a domain value (between `min` and `max`) to the corresponding angle
* in the gauge's `startAngle`..`endAngle` range. Values outside the
* domain are clamped.
*
* @param {number} value
* @returns {number}
*/
_angleAtValue(value) {
const rb = this.w.config.plotOptions.radialBar;
const min = typeof rb.min === "number" ? rb.min : 0;
const max = typeof rb.max === "number" ? rb.max : 100;
const safeMax = max === min ? min + 1 : max;
const clamped = Math.max(min, Math.min(safeMax, Number(value)));
const t = (clamped - min) / (safeMax - min);
return this.startAngle + t * (this.endAngle - this.startAngle);
}
/**
* Build an SVG arc path from `startAngle` to `endAngle` at radius `r`
* around `(cx, cy)`. Angles are in degrees, with 0° at the top.
* Used by drawBands; mirrors the `M ... A ... ` form used elsewhere.
*
* @param {number} cx
* @param {number} cy
* @param {number} r
* @param {number} startAngle
* @param {number} endAngle
* @returns {string}
*/
_describeArc(cx, cy, r, startAngle, endAngle) {
const start = Utils.polarToCartesian(cx, cy, r, endAngle);
const end = Utils.polarToCartesian(cx, cy, r, startAngle);
const sweep = endAngle - startAngle;
const largeArc = Math.abs(sweep) > 180 ? 1 : 0;
return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 0 ${end.x} ${end.y}`;
}
/**
* Draw threshold bands as colored arc segments along the gauge arc.
* Bands sit behind the value-arc and tick marks. Used for gauges that
* indicate ranges like 0-30 critical / 30-70 warning / 70-100 healthy.
*
* @param {Record<string, any>} opts
*/
drawBands(opts) {
const w = this.w;
const graphics = new Graphics(this.w);
const rb = w.config.plotOptions.radialBar;
const bands = rb.bands || [];
const g = graphics.group({
class: "apexcharts-gauge-bands"
});
const strokeWidth = this.getStrokeWidth(opts);
const radius = opts.size - strokeWidth / 2 - strokeWidth - this.margin;
const bandStroke = strokeWidth * parseInt(rb.bandsStyle.strokeWidth, 10) / 100;
const min = typeof rb.min === "number" ? rb.min : 0;
const max = typeof rb.max === "number" ? rb.max : 100;
const gapDeg = max === min ? 0 : (rb.bandsStyle.gap || 0) * ((this.endAngle - this.startAngle) / (max - min));
for (let b = 0; b < bands.length; b++) {
const band = bands[b];
if (band.from === void 0 || band.to === void 0) continue;
const a1 = this._angleAtValue(band.from);
const a2 = this._angleAtValue(band.to);
const startA = Math.min(a1, a2) + gapDeg / 2;
const endA = Math.max(a1, a2) - gapDeg / 2;
if (endA - startA <= 0) continue;
const elBand = graphics.drawPath({
d: this._describeArc(opts.centerX, opts.centerY, radius, startA, endA),
stroke: band.color || "#ccc",
strokeWidth: bandStroke,
fill: "none",
strokeLinecap: rb.bandsStyle.linecap || "butt",
classes: "apexcharts-gauge-band"
});
elBand.node.setAttribute("data-band-index", String(b));
g.add(elBand);
}
return g;
}
/**
* Draw tick marks (major + minor) along the gauge arc, with optional
* value labels at each major tick.
*
* @param {Record<string, any>} opts
*/
drawTicks(opts) {
var _a, _b, _c, _d;
const w = this.w;
const graphics = new Graphics(this.w);
const rb = w.config.plotOptions.radialBar;
const ticks = rb.ticks;
const g = graphics.group({ class: "apexcharts-gauge-ticks" });
const strokeWidth = this.getStrokeWidth(opts);
const radius = opts.size - strokeWidth / 2 - strokeWidth - this.margin;
const min = typeof rb.min === "number" ? rb.min : 0;
const max = typeof rb.max === "number" ? rb.max : 100;
const majorCount = Math.max(2, (_b = (_a = ticks.major) == null ? void 0 : _a.count) != null ? _b : 11);
const minorCount = Math.max(0, (_d = (_c = ticks.minor) == null ? void 0 : _c.count) != null ? _d : 0);
const drawTickAt = (value, cfg, isMajor) => {
var _a2, _b2, _c2;
const angle = this._angleAtValue(value);
const length = (_a2 = cfg.length) != null ? _a2 : 8;
const inner = cfg.placement === "inside" ? radius - length : radius;
const outer = cfg.placement === "inside" ? radius : radius + length;
const p1 = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
inner,
angle
);
const p2 = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
outer,
angle
);
const line = graphics.drawLine(
p1.x,
p1.y,
p2.x,
p2.y,
cfg.color || (isMajor ? "#666" : "#999"),
0,
cfg.width || (isMajor ? 2 : 1)
);
g.add(line);
if (isMajor && ((_b2 = ticks.labels) == null ? void 0 : _b2.show)) {
const labelRadius = (cfg.placement === "inside" ? inner : outer) + (cfg.placement === "inside" ? -1 : 1) * ((_c2 = ticks.labels.offset) != null ? _c2 : 6);
const labelPos = Utils.polarToCartesian(
opts.centerX,
opts.centerY,
labelRadius,
angle
);
const labelText = typeof ticks.labels.formatter === "function" ? ticks.labels.formatter(value) : String(value);
const elText = graphics.drawText({
x: labelPos.x,
y: labelPos.y,
text: labelText,
textAnchor: "middle",
dominantBaseline: "middle",
fontFamily: ticks.labels.fontFamily,
fontSize: ticks.labels.fontSize,
fontWeight: ticks.labels.fontWeight,
foreColor: ticks.labels.color,
cssClass: "apexcharts-gauge-tick-label"
});
g.add(elText);
}
};
for (let m = 0; m < majorCount; m++) {
const t = m / (majorCount - 1);
const value = min + t * (max - min);
drawTickAt(value, ticks.major || {}, true);
if (m < majorCount - 1 && minorCount > 0) {
for (let n = 1; n <= minorCount; n++) {
const tMinor = (m + n / (minorCount + 1)) / (majorCount - 1);
const minorValue = min + tMinor * (max - min);
drawTickAt(minorValue, ticks.minor || {}, false);
}
}
}
return g;
}
/**
* Draw a rotating needle pointing at the current series value. Only
* called when `plotOptions.radialBar.shape === 'needle'`. The needle is
* a tapered polygon inside a `<g>` whose rotation transform is animated
* from `startAngle` to the value's angle.
*
* Renders a single needle for the first series value (gauge use case).
* Additional series are ignored — drilled-down multi-series gauges are
* out of scope for this iteration.
*
* @param {Record<string, any>} opts
*/
drawNeedle(opts) {
var _a, _b, _c, _d, _e;
const w = this.w;
const graphics = new Graphics(this.w);
const rb = w.config.plotOptions.radialBar;
const cfg = rb.needle || {};
const g = graphics.group({ class: "apexcharts-gauge-needle" });
if (!opts.series || opts.series.length === 0) return g;
const strokeWidth = this.getStrokeWidth(opts);
const arcRadius = opts.size - strokeWidth / 2 - strokeWidth - this.margin;
const length = typeof cfg.length === "string" && cfg.length.endsWith("%") ? arcRadius * parseInt(cfg.length, 10) / 100 : Number(cfg.length || arcRadius * 0.85);
const baseW = (_a = cfg.baseWidth) != null ? _a : 4;
const tipW = (_b = cfg.tipWidth) != null ? _b : 1;
const color = cfg.color || "#333";
const cx = opts.centerX;
const needleOffsetY = Number((_c = cfg.offsetY) != null ? _c : 0);
const cy = opts.centerY + needleOffsetY;
const path = `M ${cx + baseW / 2} ${cy} A ${baseW / 2} ${baseW / 2} 0 0 1 ${cx - baseW / 2} ${cy} L ${cx - tipW / 2} ${cy - length} L ${cx + tipW / 2} ${cy - length} Z`;
const elNeedle = graphics.drawPath({
d: path,
stroke: color,
strokeWidth: 0,
fill: color,
classes: "apexcharts-gauge-needle-shape"
});
g.add(elNeedle);
const value = Number(opts.series[0]);
const targetAngle = this._angleAtValue(value);
const isInitialMount = this.initialAnim && !w.globals.dataChanged && !w.globals.resized;
const ctx = (
/** @type {any} */
this.ctx
);
const fromAngle = typeof ctx._lastNeedleAngle === "number" ? ctx._lastNeedleAngle : this.startAngle;
ctx._lastNeedleAngle = targetAngle;
const shouldAnimate = Environment.isBrowser() && w.globals.shouldAnimate && (isInitialMount || w.globals.dataChanged);
if (shouldAnimate && fromAngle !== targetAngle) {
const node = g.node;
node.setAttribute("transform-origin", `${cx} ${cy}`);
node.setAttribute("transform", `rotate(${fromAngle})`);
const speed = ((_d = cfg.animation) == null ? void 0 : _d.duration) && Number(cfg.animation.duration) || cfg.animationSpeed && Number(cfg.animationSpeed) || ((_e = w.config.chart.animations.dynamicAnimation) == null ? void 0 : _e.speed) || w.config.chart.animations.speed || 800;
const c1 = 1.70158;
const c3 = c1 + 1;
const easeOutBack = (t) => 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
const ease = isInitialMount ? easeOutBack : easeOutCubic;
if (w.globals.radialNeedleRAF != null) {
BrowserAPIs.cancelAnimationFrame(w.globals.radialNeedleRAF);
w.globals.radialNeedleRAF = null;
}
const startAt = performance.now();
const step = (now) => {
if (this.w.globals.isDestroyed) {
w.globals.radialNeedleRAF = null;
return;
}
const t = Math.max(0, Math.min(1, (now - startAt) / speed));
const angle = fromAngle + (targetAngle - fromAngle) * ease(t);
node.setAttribute("transform", `rotate(${angle})`);
if (t < 1) {
w.globals.radialNeedleRAF = BrowserAPIs.requestAnimationFrame(step);
} else {
w.globals.radialNeedleRAF = null;
}
};
w.globals.radialNeedleRAF = BrowserAPIs.requestAnimationFrame(step);
} else {
g.attr({
"transform-origin": `${cx} ${cy}`,
transform: `rotate(${targetAngle})`
});
}
return g;
}
/**
* @param {Record<string, any>} opts
*/
drawHollow(opts) {
var _a;
const graphics = new Graphics(this.w);
const hollow = this.w.config.plotOptions.radialBar.hollow;
const circle = graphics.drawCircle(opts.size * 2);
const attrs = {
class: "apexcharts-radialbar-hollow",
cx: opts.centerX,
cy: opts.centerY,
r: opts.size,
fill: opts.fill
};
if (hollow.stroke || hollow.strokeDasharray) {
attrs.stroke = hollow.stroke || "transparent";
attrs["stroke-width"] = (_a = hollow.strokeWidth) != null ? _a : 1;
if (hollow.strokeDasharray) {
attrs["stroke-dasharray"] = hollow.strokeDasharray;
}
}
circle.attr(attrs);
return circle;
}
/**
* @param {Record<string, any>} opts
* @param {any} g
* @param {number} hollowSize
* @param {string} hollowFillID
*/
drawHollowImage(opts, g, hollowSize, hollowFillID) {
const w = this.w;
const fill = new Fill(this.w);
const randID = Utils.randomId();
const hollowFillImg = w.config.plotOptions.radialBar.hollow.image;
if (w.config.plotOptions.radialBar.hollow.imageClipped) {
fill.clippedImgArea({
width: hollowSize,
height: hollowSize,
image: hollowFillImg,
patternID: `pattern${w.globals.cuid}${randID}`
});
hollowFillID = `url(#pattern${w.globals.cuid}${randID})`;
} else {
const imgWidth = w.config.plotOptions.radialBar.hollow.imageWidth;
const imgHeight = w.config.plotOptions.radialBar.hollow.imageHeight;
if (imgWidth === void 0 && imgHeight === void 0) {
const image = w.dom.Paper.image(
hollowFillImg,
/** @this {any} */
function(loader) {
this.move(
opts.centerX - loader.width / 2 + w.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY - loader.height / 2 + w.config.plotOptions.radialBar.hollow.imageOffsetY
);
}
);
g.add(image);
} else {
const image = w.dom.Paper.image(
hollowFillImg,
/** @this {any} */
function() {
this.move(
opts.centerX - imgWidth / 2 + w.config.plotOptions.radialBar.hollow.imageOffsetX,
opts.centerY - imgHeight / 2 + w.config.plotOptions.radialBar.hollow.imageOffsetY
);
this.size(imgWidth, imgHeight);
}
);
g.add(image);
}
}
return hollowFillID;
}
/**
* @param {Record<string, any>} opts
*/
getStrokeWidth(opts) {
const w = this.w;
return opts.size * (100 - parseInt(w.config.plotOptions.radialBar.hollow.size, 10)) / 100 / (opts.series.length + 1) - this.margin;
}
/**
* @param {Event} e
*/
onBarLabelClick(e) {
var _a;
const target = (
/** @type {Element} */
e.target
);
const seriesIndex = parseInt((_a = target.getAttribute("rel")) != null ? _a : "", 10) - 1;
const legendClick = this.barLabels.onClick;
const w = this.w;
if (legendClick) {
legendClick(w.seriesData.seriesNames[seriesIndex], { w, seriesIndex });
}
}
}
_core__default.use({
radialBar: Radial
});
export {
default2 as default
};