apexcharts
Version:
A JavaScript Chart Library
1,051 lines (1,050 loc) • 39.4 kB
JavaScript
/*!
* ApexCharts v6.6.1
* (c) 2018-2026 ApexCharts
*/
import * as _core from "apexcharts/core";
import _core__default from "apexcharts/core";
import { default as default2 } from "apexcharts/core";
const Utils = _core.__apex_Utils;
const Options = _core.__apex_Options;
const DRAG_CLASS = "apexcharts-ink-draggable";
const TYPES = ["point", "xaxis", "yaxis"];
const EDGE_PX = 8;
const CLICK_SLOP_PX = 2;
const FONT_STEPS = [10, 11, 12, 14, 17, 20];
const MARKER_SHAPES = ["circle", "square", "diamond", "triangle"];
const SHAPE_GLYPHS = {
circle: "●",
square: "■",
diamond: "◆",
triangle: "▲"
};
const NOTE_COLORS = [
"#ffffff",
"#334155",
"#2563eb",
"#16a34a",
"#d97706",
"#dc2626"
];
const TRASH_ICON = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>';
class InkLayer {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w, ctx) {
this.w = w;
this.ctx = ctx;
this._wired = false;
this._drag = null;
this._editor = null;
this._creating = false;
this._createSeq = 0;
this._attach = this._attach.bind(this);
this._onRerender = this._onRerender.bind(this);
this._onMove = this._onMove.bind(this);
this._onUp = this._onUp.bind(this);
this._onCreateClick = this._onCreateClick.bind(this);
this._onDocDownEditor = this._onDocDownEditor.bind(this);
if (this._enabledGlobally() || this._hasDraggable() || this._paletteEnabled()) {
this._wire();
}
}
_enabledGlobally() {
const ink = this.w.config.chart.ink;
return !!(ink && ink.enabled);
}
_paletteEnabled() {
const ink = this.w.config.chart.ink;
return !!(ink && ink.palette);
}
_snapEnabled() {
const ink = this.w.config.chart.ink;
return !!(ink && ink.snap);
}
// ─── P5: snap to gridlines ────────────────────────────────────────────────
/** @param {number} v @param {number[]} ticks @returns {number} nearest tick */
_nearest(v, ticks) {
let best = v;
let bd = Infinity;
for (let i = 0; i < ticks.length; i++) {
const d = Math.abs(ticks[i] - v);
if (d < bd) {
bd = d;
best = ticks[i];
}
}
return best;
}
/**
* Snap an x value to the nearest x gridline (numeric axes only).
* @param {number} x
*/
_snapX(x) {
if (!this._snapEnabled() || typeof x !== "number") return x;
const s = this.w.globals.xAxisScale;
return s && Array.isArray(s.result) && s.result.length ? this._nearest(x, s.result) : x;
}
/**
* Snap a y value to the nearest y gridline.
* @param {number} y @param {number} si
*/
_snapY(y, si) {
if (!this._snapEnabled() || typeof y !== "number") return y;
const scales = this.w.globals.yAxisScale;
const s = scales && scales[si];
return s && Array.isArray(s.result) && s.result.length ? this._nearest(y, s.result) : y;
}
/** @param {string} type @returns {any[]} the config annotations of a type */
_annoList(type) {
const a = this.w.config.annotations;
if (!a) return [];
const key = type === "point" ? "points" : type;
return Array.isArray(a[key]) ? a[key] : [];
}
/** @param {any} anno */
_isDraggable(anno) {
if (!anno) return false;
if (anno.draggable === true) return true;
if (anno.draggable === false) return false;
return this._enabledGlobally();
}
_hasDraggable() {
return TYPES.some((t) => this._annoList(t).some((p) => this._isDraggable(p)));
}
_wire() {
if (this._wired) return;
this._wired = true;
this.ctx.addEventListener("mounted", this._onRerender);
this.ctx.addEventListener("updated", this._onRerender);
}
/**
* A full (re)render rebuilds the SVG and may swap the annotations config
* (updateOptions, undo restore), so an open editor card points at stale
* state: drop it (without committing) before rebinding handlers. Targeted
* redraws call _attach() directly and keep the card open.
*/
_onRerender() {
this._closeEditor(false);
this._attach();
}
/**
* After each (re)render, bind drag + edit handlers to every draggable
* annotation's elements. Idempotent via a per-node flag so a targeted redraw
* re-runs this without double-binding the untouched annotations.
*/
_attach() {
const w = this.w;
const baseEl = w.dom.baseEl;
if (!baseEl) return;
TYPES.forEach((type) => {
this._annoList(type).forEach((anno, index) => {
if (!this._isDraggable(anno)) return;
if (!anno.id) {
anno.id = "apexcharts-ink-" + type + "-" + index + "-" + w.globals.chartID;
}
baseEl.querySelectorAll("." + anno.id).forEach((el) => {
if (el.__inkBound) return;
el.__inkBound = true;
el.style.cursor = "move";
el.classList.add(DRAG_CLASS);
el.addEventListener(
"mousedown",
(e) => this._onDown(e, type, index)
);
el.addEventListener(
"touchstart",
(e) => this._onDown(e, type, index)
);
el.addEventListener("dblclick", (e) => {
e.preventDefault();
e.stopPropagation();
this._startEdit(type, index, { select: true });
});
});
});
});
if (this._paletteEnabled()) this._renderPalette();
}
// ─── drag / resize ────────────────────────────────────────────────────────
/**
* @param {any} e @param {string} type @param {number} index
*/
_onDown(e, type, index) {
if (e.button && e.button !== 0) return;
const w = this.w;
const doc = w.dom.baseEl && w.dom.baseEl.ownerDocument;
if (!doc) return;
e.stopPropagation();
if (e.cancelable) e.preventDefault();
const isTouch = e.type === "touchstart";
const ev = isTouch ? e.touches[0] : e;
const svgRoot = w.dom.Paper && w.dom.Paper.node;
const ctm = svgRoot && svgRoot.getScreenCTM ? svgRoot.getScreenCTM() : null;
const anno = this._annoList(type)[index];
let mode = "move";
let rect = null;
let origX = 0;
let origW = 0;
if (type === "xaxis" && anno.x2 != null) {
rect = w.dom.baseEl.querySelector(".apexcharts-annotation-rect." + anno.id);
if (rect) {
const r = rect.getBoundingClientRect();
if (Math.abs(ev.clientX - r.left) <= EDGE_PX) mode = "resize-x1";
else if (Math.abs(ev.clientX - r.right) <= EDGE_PX) mode = "resize-x2";
origX = parseFloat(rect.getAttribute("x")) || 0;
origW = parseFloat(rect.getAttribute("width")) || 0;
}
}
this._drag = {
type,
index,
anno,
els: Array.from(w.dom.baseEl.querySelectorAll("." + anno.id)),
mode,
rect,
origX,
origW,
startX: ev.clientX,
startY: ev.clientY,
scaleX: ctm && ctm.a ? ctm.a : 1,
scaleY: ctm && ctm.d ? ctm.d : 1,
dxPixel: 0,
dyPixel: 0,
moved: false
};
doc.addEventListener("mousemove", this._onMove);
doc.addEventListener("touchmove", this._onMove, { passive: false });
doc.addEventListener("mouseup", this._onUp);
doc.addEventListener("touchend", this._onUp);
}
/** @param {any} me */
_onMove(me) {
const d = this._drag;
if (!d) return;
if (me.cancelable) me.preventDefault();
const mev = me.type === "touchmove" ? me.touches[0] : me;
d.dxPixel = (mev.clientX - d.startX) / d.scaleX;
d.dyPixel = (mev.clientY - d.startY) / d.scaleY;
if (Math.abs(d.dxPixel) > CLICK_SLOP_PX || Math.abs(d.dyPixel) > CLICK_SLOP_PX) {
d.moved = true;
}
if (d.mode === "move") {
const t = `translate(${d.dxPixel} ${d.dyPixel})`;
d.els.forEach((el) => el.setAttribute("transform", t));
} else if (d.rect) {
if (d.mode === "resize-x1") {
d.rect.setAttribute("x", d.origX + d.dxPixel);
d.rect.setAttribute("width", Math.max(1, d.origW - d.dxPixel));
} else if (d.mode === "resize-x2") {
d.rect.setAttribute("width", Math.max(1, d.origW + d.dxPixel));
}
}
}
_onUp() {
const d = this._drag;
this._drag = null;
this._teardownDocListeners();
if (!d || !d.moved) {
if (d) {
d.els.forEach((el) => el.removeAttribute("transform"));
this._startEdit(d.type, d.index);
}
return;
}
const anno = this._annoList(d.type)[d.index];
if (!anno) return;
this._applyDelta(d, anno);
d.els.forEach((el) => el.removeAttribute("transform"));
this._redrawAnno(d.type, anno, d.index);
this._checkpoint("ink:drag");
this._fireDragged(d.type, anno, d.index);
}
/**
* Record a Rewind (undo) checkpoint for an ink edit. Targeted redraws fire no
* 'updated' event, so History would otherwise miss them. No-op when the
* history feature is absent or disabled.
* @param {string} label
*/
_checkpoint(label) {
var _a, _b;
(_b = (_a = this.ctx.history) == null ? void 0 : _a.snapshot) == null ? void 0 : _b.call(_a, label);
}
/**
* Mutate the annotation's config from the pixel drag delta (type + mode aware).
* @param {any} d @param {any} anno
*/
_applyDelta(d, anno) {
const w = this.w;
const dxData = w.layout.gridWidth ? d.dxPixel * (w.globals.xRange / w.layout.gridWidth) : 0;
if (d.type === "point") {
const { newX, newY } = this._invertPoint(anno, d.dxPixel, d.dyPixel);
anno.x = this._snapX(newX);
if (newY != null) {
const yi = anno.yAxisIndex || 0;
const map = w.globals.seriesYAxisMap;
anno.y = this._snapY(newY, map && map[yi] ? map[yi][0] : 0);
}
return;
}
if (d.type === "xaxis") {
if (typeof anno.x !== "number") return;
if (d.mode === "move") {
if (typeof anno.x2 === "number") {
anno.x += dxData;
anno.x2 += dxData;
} else {
anno.x = this._snapX(anno.x + dxData);
}
} else if (d.mode === "resize-x1" || d.mode === "resize-x2") {
const xIsLeft = anno.x2 == null || anno.x <= anno.x2;
const grow = d.mode === "resize-x2" ? !xIsLeft : xIsLeft;
if (grow) anno.x = this._snapX(anno.x + dxData);
else if (typeof anno.x2 === "number") anno.x2 = this._snapX(anno.x2 + dxData);
}
return;
}
if (d.type === "yaxis") {
const yi = anno.yAxisIndex || 0;
const map = w.globals.seriesYAxisMap;
const si = map && map[yi] ? map[yi][0] : 0;
const yRange = w.globals.yRange ? w.globals.yRange[si] : null;
if (yRange == null || !w.layout.gridHeight) return;
const dyData = -d.dyPixel * (yRange / w.layout.gridHeight);
if (typeof anno.y2 === "number") {
if (typeof anno.y === "number") anno.y += dyData;
anno.y2 += dyData;
} else if (typeof anno.y === "number") {
anno.y = this._snapY(anno.y + dyData, si);
}
}
}
/**
* Invert a pixel drag delta to a point annotation's data x/y.
* @param {any} anno @param {number} dxPixel @param {number} dyPixel
* @returns {{newX:any, newY:any}}
*/
_invertPoint(anno, dxPixel, dyPixel) {
const w = this.w;
const categoryX = (w.config.xaxis.type === "category" || w.config.xaxis.convertedCatToNumeric) && !w.axisFlags.dataFormatXNumeric;
let newX = anno.x;
if (!categoryX && typeof anno.x === "number" && w.layout.gridWidth) {
newX = anno.x + dxPixel * (w.globals.xRange / w.layout.gridWidth);
}
let newY = anno.y;
const yi = anno.yAxisIndex || 0;
const map = w.globals.seriesYAxisMap;
const si = map && map[yi] ? map[yi][0] : 0;
const yRange = w.globals.yRange ? w.globals.yRange[si] : null;
const logY = w.config.yaxis[yi] && w.config.yaxis[yi].logarithmic;
if (typeof anno.y === "number" && yRange != null && !logY && w.layout.gridHeight) {
newY = anno.y - dyPixel * (yRange / w.layout.gridHeight);
}
return { newX, newY };
}
/**
* Targeted redraw of one annotation: drop its elements and re-add the shape +
* label + label background at the current config coordinates (no full chart
* re-render, and repeat-safe unlike updateOptions({})).
* @param {string} type @param {any} anno @param {number} index
*/
_redrawAnno(type, anno, index) {
const w = this.w;
const baseEl = w.dom.baseEl;
const annotations = this.ctx.annotations;
if (!baseEl || !annotations) return;
baseEl.querySelectorAll("." + anno.id).forEach((el) => el.remove());
const group = baseEl.querySelector(".apexcharts-" + type + "-annotations");
if (!group) return;
if (type === "point" && annotations.pointsAnnotations) {
annotations.pointsAnnotations.addPointAnnotation(anno, group, index);
} else if (type === "xaxis" && annotations.xAxisAnnotations) {
annotations.xAxisAnnotations.addXaxisAnnotation(anno, group, index);
} else if (type === "yaxis" && annotations.yAxisAnnotations) {
annotations.yAxisAnnotations.addYaxisAnnotation(anno, group, index);
}
const labelEl = baseEl.querySelector(
".apexcharts-" + type + "-annotation-label." + anno.id
);
if (labelEl && annotations.helpers && anno.label && anno.label.text) {
const elRect = annotations.helpers.addBackgroundToAnno(labelEl, anno);
if (elRect && labelEl.parentNode) {
labelEl.parentNode.insertBefore(elRect.node, labelEl);
}
}
this._attach();
}
/** @param {string} type @param {any} anno @param {number} index */
_fireDragged(type, anno, index) {
var _a;
const args = { type, id: anno.id, index, x: anno.x, y: anno.y };
if (anno.x2 != null) args.x2 = anno.x2;
if (anno.y2 != null) args.y2 = anno.y2;
const events = this.w.config.chart.events;
if (typeof events.annotationDragged === "function") {
events.annotationDragged(this.ctx, args);
}
(_a = this.ctx.events) == null ? void 0 : _a.fireEvent("annotationDragged", [this.ctx, args]);
}
// ─── P3: click-to-create ─────────────────────────────────────────────────
/**
* Enter create mode: the next click on the plot area drops a new draggable
* point annotation there and opens its label editor.
*/
startCreate() {
if (this._creating) return;
const svg = this.w.dom.Paper && this.w.dom.Paper.node;
if (!svg) return;
this._creating = true;
svg.style.cursor = "crosshair";
svg.addEventListener("click", this._onCreateClick, true);
this._syncPalette();
}
/** Leave create mode. */
stopCreate() {
if (!this._creating) return;
this._creating = false;
const svg = this.w.dom.Paper && this.w.dom.Paper.node;
if (svg) {
svg.style.cursor = "";
svg.removeEventListener("click", this._onCreateClick, true);
}
this._syncPalette();
}
/** @param {any} e */
_onCreateClick(e) {
if (!this._creating) return;
e.preventDefault();
e.stopPropagation();
const pos = this._pixelToData(e.clientX, e.clientY);
this.stopCreate();
if (!pos) return;
this.createAt(pos.x, pos.y);
}
/**
* Create a draggable note at data coordinates and open its editor card.
* Public: the context menu's "Add note here" routes here so its notes are
* config-backed too, and thus draggable, editable, persistable and undoable.
* @param {any} x @param {any} y @param {{text?: string}} [opts]
* @returns {any} the created annotation config
*/
createAt(x, y, opts = {}) {
const w = this.w;
this._wire();
this._createSeq += 1;
const id = "apexcharts-ink-new-" + this._createSeq + "-" + w.globals.chartID;
const anno = Utils.extend(new Options().pointAnnotation, {
x,
y,
id,
draggable: true,
label: { text: opts.text || "Note" }
});
if (!w.config.annotations) w.config.annotations = {};
if (!Array.isArray(w.config.annotations.points)) w.config.annotations.points = [];
w.config.annotations.points.push(anno);
const index = w.config.annotations.points.length - 1;
this._redrawAnno("point", anno, index);
this._checkpoint("ink:create");
this._fireCreated("point", anno, index);
this._startEdit("point", index, { select: true });
return anno;
}
/**
* Create a draggable dashed LINE annotation at a data value and open its
* editor card: axis 'x' drops a vertical line at a data x, axis 'y' a
* horizontal line at a data y. Public: the context menu's "Annotate here"
* routes here so its lines are config-backed too, and thus draggable,
* editable, persistable and undoable. Lines only: x2/y2 are never set, so
* this can never produce a range rectangle.
* @param {'x'|'y'} axis @param {any} val
* @param {{text?: string, strokeDashArray?: number, color?: string, select?: boolean}} [opts]
* @returns {any} the created annotation config
*/
createLineAt(axis, val, opts = {}) {
const w = this.w;
this._wire();
this._createSeq += 1;
const id = "apexcharts-ink-new-" + this._createSeq + "-" + w.globals.chartID;
const type = axis === "y" ? "yaxis" : "xaxis";
const defaults = type === "yaxis" ? new Options().yAxisAnnotation : new Options().xAxisAnnotation;
const over = {
id,
draggable: true,
strokeDashArray: opts.strokeDashArray != null ? opts.strokeDashArray : 4,
label: { text: opts.text || "" }
};
if (opts.color) {
over.borderColor = opts.color;
over.label.borderColor = opts.color;
}
if (type === "yaxis") over.y = val;
else over.x = val;
const anno = Utils.extend(defaults, over);
if (!w.config.annotations) w.config.annotations = {};
if (!Array.isArray(w.config.annotations[type])) w.config.annotations[type] = [];
w.config.annotations[type].push(anno);
const index = w.config.annotations[type].length - 1;
this._redrawAnno(type, anno, index);
this._checkpoint("ink:create");
this._fireCreated(type, anno, index);
if (opts.select !== false) this._startEdit(type, index, { select: true });
return anno;
}
/**
* Convert a client-space point to data coordinates (absolute, for create).
* @param {number} clientX @param {number} clientY
* @returns {{x:any, y:any}|null}
*/
_pixelToData(clientX, clientY) {
const w = this.w;
const gridEl = w.dom.baseEl && w.dom.baseEl.querySelector(".apexcharts-grid");
if (!gridEl) return null;
const g = gridEl.getBoundingClientRect();
if (!g.width || !g.height) return null;
const fx = (clientX - g.left) / g.width;
const fy = (clientY - g.top) / g.height;
const minX = w.globals.minX;
const xRange = w.globals.xRange;
const minY = w.globals.minYArr && w.globals.minYArr[0] != null ? w.globals.minYArr[0] : w.globals.minY;
const yRange = w.globals.yRange && w.globals.yRange[0] != null ? w.globals.yRange[0] : w.globals.maxY - w.globals.minY;
let x = minX + fx * xRange;
const y = minY + (1 - fy) * yRange;
const categoryX = (w.config.xaxis.type === "category" || w.config.xaxis.convertedCatToNumeric) && !w.axisFlags.dataFormatXNumeric;
if (categoryX) x = Math.round(x);
return { x, y };
}
/** @param {string} type @param {any} anno @param {number} index */
_fireCreated(type, anno, index) {
var _a;
const args = { type, id: anno.id, index };
if (typeof anno.x !== "undefined") args.x = anno.x;
if (typeof anno.y !== "undefined") args.y = anno.y;
const events = this.w.config.chart.events;
if (typeof events.annotationCreated === "function") {
events.annotationCreated(this.ctx, args);
}
(_a = this.ctx.events) == null ? void 0 : _a.fireEvent("annotationCreated", [this.ctx, args]);
}
// ─── P3: tool palette ────────────────────────────────────────────────────
/** Render a minimal "add note" toggle into the chart wrap (once per render). */
_renderPalette() {
const w = this.w;
const elWrap = w.dom.elWrap;
if (!elWrap || elWrap.querySelector(".apexcharts-ink-palette")) return;
const doc = elWrap.ownerDocument;
const bar = doc.createElement("div");
bar.className = "apexcharts-ink-palette";
const s = bar.style;
s.position = "absolute";
s.top = "6px";
s.left = "6px";
s.zIndex = "15";
const btn = doc.createElement("button");
btn.type = "button";
btn.className = "apexcharts-ink-add";
btn.textContent = "+ Note";
const bs = btn.style;
bs.cursor = "pointer";
bs.font = "12px sans-serif";
bs.padding = "4px 9px";
bs.borderRadius = "5px";
bs.border = "1px solid #6366f1";
bs.color = "#4338ca";
bs.background = "#fff";
btn.addEventListener("click", (e) => {
e.stopPropagation();
if (this._creating) this.stopCreate();
else this.startCreate();
});
bar.appendChild(btn);
elWrap.appendChild(bar);
this._syncPalette();
}
/** Reflect create-mode state on the palette button. */
_syncPalette() {
const elWrap = this.w.dom.elWrap;
const btn = (
/** @type {any} */
elWrap && elWrap.querySelector(".apexcharts-ink-add")
);
if (!btn) return;
if (this._creating) {
btn.style.background = "#6366f1";
btn.style.color = "#fff";
btn.textContent = "Click chart...";
} else {
btn.style.background = "#fff";
btn.style.color = "#4338ca";
btn.textContent = "+ Note";
}
}
// ─── P2 + P6: the floating note editor card ──────────────────────────────
// Click (or double-click) an ink-managed annotation to open a small card
// anchored to it: rename inline, recolor via accent swatches, toggle bold,
// step the font size, size/reshape the marker (points), or delete the note.
/** @returns {string[]} the accent swatches offered by the editor */
_noteColors() {
const ink = this.w.config.chart.ink;
return ink && Array.isArray(ink.noteColors) && ink.noteColors.length ? ink.noteColors : NOTE_COLORS;
}
/**
* Perceived-luminance check so text/border contrast follows the accent.
* @param {string} hex
*/
static _isLight(hex) {
const h = String(hex || "").replace("#", "");
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
const n = parseInt(full, 16);
if (isNaN(n) || full.length !== 6) return false;
const r = n >> 16 & 255;
const g = n >> 8 & 255;
const b = n & 255;
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.72;
}
/** @param {any} style */
static _isBold(style) {
const fw = style && style.fontWeight;
return fw === "bold" || parseInt(String(fw), 10) >= 600;
}
/**
* Small icon/text button for the editor card. mousedown is prevented so the
* text input keeps focus while formatting.
* @param {any} doc @param {string} content @param {string} title
* @param {Function} onClick @param {string} [extraClass] @param {boolean} [isSvg]
*/
_cardBtn(doc, content, title, onClick, extraClass, isSvg) {
const b = doc.createElement("button");
b.type = "button";
b.className = "apexcharts-ink-btn" + (extraClass ? " " + extraClass : "");
b.title = title;
b.setAttribute("aria-label", title);
if (isSvg) b.innerHTML = content;
else b.textContent = content;
b.addEventListener("mousedown", (e) => e.preventDefault());
b.addEventListener("click", (e) => {
e.stopPropagation();
onClick();
});
return b;
}
/**
* Uppercase row caption ("Label" / "Line" / "Marker") for the editor card.
* @param {any} doc @param {string} text
*/
_cardLabel(doc, text) {
const lab = doc.createElement("span");
lab.className = "apexcharts-ink-cardlabel";
lab.textContent = text;
return lab;
}
/**
* Color swatch button for the editor card. mousedown is prevented so the
* text input keeps focus while restyling.
* @param {any} doc @param {string} c
* @param {(c: string) => void} pick @param {string} [extraClass]
*/
_mkSwatch(doc, c, pick, extraClass) {
const sw = doc.createElement("button");
sw.type = "button";
sw.className = "apexcharts-ink-swatch" + (extraClass ? " " + extraClass : "");
sw.title = c;
sw.setAttribute("aria-label", "Color " + c);
sw.dataset.color = c;
sw.style.background = c;
sw.addEventListener("mousedown", (e) => e.preventDefault());
sw.addEventListener("click", (e) => {
e.stopPropagation();
pick(c);
});
return sw;
}
/**
* Open the floating editor card for an annotation.
* @param {string} type @param {number} index
* @param {{select?: boolean}} [opts] select: preselect the text (create / dblclick)
*/
_startEdit(type, index, opts = {}) {
const w = this.w;
const anno = this._annoList(type)[index];
const baseEl = w.dom.baseEl;
const elWrap = w.dom.elWrap;
if (!anno || !anno.id || !baseEl || !elWrap) return;
this._closeEditor(false);
const doc = baseEl.ownerDocument;
const card = doc.createElement("div");
card.className = "apexcharts-ink-card";
card.setAttribute("role", "dialog");
card.setAttribute("aria-label", "Edit note");
card.style.visibility = "hidden";
const rowText = doc.createElement("div");
rowText.className = "apexcharts-ink-card-row";
const input = doc.createElement("input");
input.type = "text";
input.className = "apexcharts-ink-editor";
input.placeholder = "Note text";
input.value = anno.label && anno.label.text || "";
rowText.appendChild(input);
rowText.appendChild(
this._cardBtn(
doc,
TRASH_ICON,
"Delete note",
() => this._deleteAnno(),
"apexcharts-ink-btn--delete",
true
)
);
card.appendChild(rowText);
const rowStyle = doc.createElement("div");
rowStyle.className = "apexcharts-ink-card-row";
if (type !== "point") {
rowStyle.appendChild(this._cardLabel(doc, "Label"));
}
this._noteColors().forEach((c) => {
rowStyle.appendChild(this._mkSwatch(doc, c, (col) => this._applyColor(col)));
});
const sep = doc.createElement("span");
sep.className = "apexcharts-ink-sep";
rowStyle.appendChild(sep);
rowStyle.appendChild(
this._cardBtn(doc, "B", "Bold", () => this._toggleBold(), "apexcharts-ink-btn--bold")
);
rowStyle.appendChild(this._cardBtn(doc, "A-", "Smaller text", () => this._stepFont(-1)));
rowStyle.appendChild(this._cardBtn(doc, "A+", "Larger text", () => this._stepFont(1)));
card.appendChild(rowStyle);
if (type !== "point") {
const rowLine = doc.createElement("div");
rowLine.className = "apexcharts-ink-card-row";
rowLine.appendChild(this._cardLabel(doc, "Line"));
this._noteColors().forEach((c) => {
rowLine.appendChild(
this._mkSwatch(
doc,
c,
(col) => this._applyLineColor(col),
"apexcharts-ink-swatch--line"
)
);
});
card.appendChild(rowLine);
}
if (type === "point") {
const rowMarker = doc.createElement("div");
rowMarker.className = "apexcharts-ink-card-row";
rowMarker.appendChild(this._cardLabel(doc, "Marker"));
rowMarker.appendChild(
this._cardBtn(doc, "-", "Smaller marker", () => this._stepMarker(-1))
);
const sizeOut = doc.createElement("span");
sizeOut.className = "apexcharts-ink-marker-size";
rowMarker.appendChild(sizeOut);
rowMarker.appendChild(
this._cardBtn(doc, "+", "Larger marker", () => this._stepMarker(1))
);
rowMarker.appendChild(
this._cardBtn(
doc,
SHAPE_GLYPHS.circle,
"Marker shape",
() => this._cycleShape(),
"apexcharts-ink-btn--shape"
)
);
card.appendChild(rowMarker);
}
elWrap.appendChild(card);
this._editor = { card, input, type, index };
this._positionCard();
this._syncCard();
card.style.visibility = "";
input.focus();
if (opts.select) input.select();
card.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
this._closeEditor(false);
} else if (e.key === "Enter" && e.target === input) {
e.preventDefault();
this._closeEditor(true);
}
});
doc.addEventListener("mousedown", this._onDocDownEditor, true);
doc.addEventListener("touchstart", this._onDocDownEditor, true);
}
/**
* Commit + close on any press outside the card.
* @param {any} e
*/
_onDocDownEditor(e) {
const ed = this._editor;
if (!ed || ed.card.contains(e.target)) return;
this._closeEditor(true);
}
/**
* Anchor the card to the annotation's label (below it, or above when there
* is no room), clamped inside the chart wrap. Re-run after each restyle
* since the label rect changes.
*/
_positionCard() {
const ed = this._editor;
if (!ed) return;
const w = this.w;
const baseEl = w.dom.baseEl;
const elWrap = w.dom.elWrap;
const anno = this._annoList(ed.type)[ed.index];
if (!baseEl || !elWrap || !anno) return;
const anchor = baseEl.querySelector(
".apexcharts-" + ed.type + "-annotation-label." + anno.id
) || baseEl.querySelector("." + anno.id);
if (!anchor) return;
const wrapRect = elWrap.getBoundingClientRect();
const aRect = anchor.getBoundingClientRect();
const cw = ed.card.offsetWidth;
const ch = ed.card.offsetHeight;
let left = Math.round(aRect.left - wrapRect.left);
let top = Math.round(aRect.bottom - wrapRect.top) + 8;
if (top + ch > elWrap.clientHeight - 4) {
top = Math.round(aRect.top - wrapRect.top) - ch - 8;
}
if (left + cw > elWrap.clientWidth - 4) left = elWrap.clientWidth - cw - 4;
ed.card.style.left = Math.max(4, left) + "px";
ed.card.style.top = Math.max(4, top) + "px";
}
/** Reflect the annotation's current style on the card controls. */
_syncCard() {
const ed = this._editor;
if (!ed) return;
const anno = this._annoList(ed.type)[ed.index];
if (!anno) return;
const style = anno.label && anno.label.style || {};
const bg = String(style.background || "").toLowerCase();
ed.card.querySelectorAll(".apexcharts-ink-swatch:not(.apexcharts-ink-swatch--line)").forEach((sw) => {
sw.classList.toggle(
"apexcharts-ink-swatch--active",
(sw.dataset.color || "").toLowerCase() === bg
);
});
const stroke = String(anno.borderColor || "").toLowerCase();
ed.card.querySelectorAll(".apexcharts-ink-swatch--line").forEach((sw) => {
sw.classList.toggle(
"apexcharts-ink-swatch--active",
(sw.dataset.color || "").toLowerCase() === stroke
);
});
const boldBtn = ed.card.querySelector(".apexcharts-ink-btn--bold");
if (boldBtn) {
boldBtn.classList.toggle("apexcharts-ink-btn--active", InkLayer._isBold(style));
}
const m = anno.marker || {};
const sizeOut = ed.card.querySelector(".apexcharts-ink-marker-size");
if (sizeOut) {
sizeOut.textContent = String(typeof m.size === "number" ? m.size : 4);
}
const shapeBtn = ed.card.querySelector(".apexcharts-ink-btn--shape");
if (shapeBtn) {
shapeBtn.textContent = SHAPE_GLYPHS[m.shape] || SHAPE_GLYPHS.circle;
}
}
/**
* Commit the input's text into the annotation (the card stays open). Also
* runs before any style apply so typed-but-unconfirmed text survives the
* redraw.
* @param {any} ed
*/
_commitTextOf(ed) {
const anno = this._annoList(ed.type)[ed.index];
if (!anno) return;
const text = ed.input.value;
if (!anno.label) anno.label = {};
if (anno.label.text === text) return;
anno.label.text = text;
this._redrawAnno(ed.type, anno, ed.index);
this._checkpoint("ink:edit");
this._fireEdited(ed.type, anno, ed.index);
}
/**
* Close the editor card. commit=true also commits the pending text. Style
* edits apply immediately and are not rolled back by Escape; use undo.
* @param {boolean} commit
*/
_closeEditor(commit) {
const ed = this._editor;
if (!ed) return;
this._editor = null;
const doc = this.w.dom.baseEl && this.w.dom.baseEl.ownerDocument;
if (doc) {
doc.removeEventListener("mousedown", this._onDocDownEditor, true);
doc.removeEventListener("touchstart", this._onDocDownEditor, true);
}
if (ed.card.parentNode) ed.card.parentNode.removeChild(ed.card);
if (commit) this._commitTextOf(ed);
}
/**
* Apply a config mutation from a card control: commit pending text, mutate,
* redraw, checkpoint for undo, then refresh + re-anchor the card.
* @param {string} label @param {(anno: any) => void} mutate
*/
_applyStyle(label, mutate) {
const ed = this._editor;
if (!ed) return;
const anno = this._annoList(ed.type)[ed.index];
if (!anno) return;
this._commitTextOf(ed);
if (!anno.label) anno.label = {};
if (!anno.label.style) anno.label.style = {};
mutate(anno);
this._redrawAnno(ed.type, anno, ed.index);
this._checkpoint(label);
this._fireStyled(ed.type, anno, ed.index);
this._syncCard();
this._positionCard();
}
/**
* Apply an accent color: label chip + marker (points) or line/range fill
* (axis annotations), with text/border contrast following the luminance.
* @param {string} c
*/
_applyColor(c) {
const ed = this._editor;
if (!ed) return;
const light = InkLayer._isLight(c);
this._applyStyle("ink:style", (anno) => {
anno.label.style.background = c;
anno.label.style.color = light ? "#334155" : "#ffffff";
anno.label.borderColor = light ? "#cbd5e1" : c;
if (ed.type === "point") {
if (!anno.marker) anno.marker = {};
anno.marker.strokeColor = light ? "#334155" : c;
anno.marker.fillColor = light ? "#ffffff" : c;
}
});
}
/**
* Line-stroke color for axis annotations, separate from the label chip.
* @param {string} c
*/
_applyLineColor(c) {
const ed = this._editor;
if (!ed || ed.type === "point") return;
this._applyStyle("ink:style", (anno) => {
anno.borderColor = c;
if (anno.x2 != null || anno.y2 != null) anno.fillColor = c;
});
}
/**
* Step the label font size through the preset scale.
* @param {number} dir
*/
_stepFont(dir) {
this._applyStyle("ink:style", (anno) => {
const cur = parseFloat(anno.label.style.fontSize) || 11;
let i = 0;
for (let k = 1; k < FONT_STEPS.length; k++) {
if (Math.abs(FONT_STEPS[k] - cur) < Math.abs(FONT_STEPS[i] - cur)) i = k;
}
i = Math.min(FONT_STEPS.length - 1, Math.max(0, i + dir));
anno.label.style.fontSize = FONT_STEPS[i] + "px";
});
}
_toggleBold() {
this._applyStyle("ink:style", (anno) => {
anno.label.style.fontWeight = InkLayer._isBold(anno.label.style) ? 400 : 700;
});
}
/**
* Grow/shrink the point marker.
* @param {number} dir
*/
_stepMarker(dir) {
this._applyStyle("ink:style", (anno) => {
if (!anno.marker) anno.marker = {};
const cur = typeof anno.marker.size === "number" ? anno.marker.size : 4;
anno.marker.size = Math.min(14, Math.max(2, cur + dir));
});
}
/** Cycle the point marker shape (circle, square, diamond, triangle). */
_cycleShape() {
this._applyStyle("ink:style", (anno) => {
if (!anno.marker) anno.marker = {};
const i = MARKER_SHAPES.indexOf(anno.marker.shape);
anno.marker.shape = MARKER_SHAPES[(i + 1) % MARKER_SHAPES.length];
});
}
/** Delete the annotation the editor is open on (undoable via Rewind). */
_deleteAnno() {
const ed = this._editor;
if (!ed) return;
const list = this._annoList(ed.type);
const anno = list[ed.index];
this._closeEditor(false);
if (!anno) return;
const baseEl = this.w.dom.baseEl;
if (baseEl && anno.id) {
baseEl.querySelectorAll("." + anno.id).forEach((el) => el.remove());
}
list.splice(ed.index, 1);
for (let i = ed.index; i < list.length; i++) {
this._redrawAnno(ed.type, list[i], i);
}
this._checkpoint("ink:delete");
this._fireDeleted(ed.type, anno, ed.index);
}
/** @param {string} type @param {any} anno @param {number} index */
_fireEdited(type, anno, index) {
var _a;
const args = { type, id: anno.id, index, text: anno.label ? anno.label.text : "" };
const events = this.w.config.chart.events;
if (typeof events.annotationEdited === "function") {
events.annotationEdited(this.ctx, args);
}
(_a = this.ctx.events) == null ? void 0 : _a.fireEvent("annotationEdited", [this.ctx, args]);
}
/** @param {string} type @param {any} anno @param {number} index */
_fireStyled(type, anno, index) {
var _a;
const args = { type, id: anno.id, index, label: anno.label, marker: anno.marker };
const events = this.w.config.chart.events;
if (typeof events.annotationStyled === "function") {
events.annotationStyled(this.ctx, args);
}
(_a = this.ctx.events) == null ? void 0 : _a.fireEvent("annotationStyled", [this.ctx, args]);
}
/** @param {string} type @param {any} anno @param {number} index */
_fireDeleted(type, anno, index) {
var _a;
const args = { type, id: anno.id, index };
const events = this.w.config.chart.events;
if (typeof events.annotationDeleted === "function") {
events.annotationDeleted(this.ctx, args);
}
(_a = this.ctx.events) == null ? void 0 : _a.fireEvent("annotationDeleted", [this.ctx, args]);
}
// ─── lifecycle ────────────────────────────────────────────────────────────
_teardownDocListeners() {
const doc = this.w.dom.baseEl && this.w.dom.baseEl.ownerDocument;
if (!doc) return;
doc.removeEventListener("mousemove", this._onMove);
doc.removeEventListener("touchmove", this._onMove);
doc.removeEventListener("mouseup", this._onUp);
doc.removeEventListener("touchend", this._onUp);
}
teardown() {
var _a, _b, _c, _d;
this._teardownDocListeners();
this._closeEditor(false);
this.stopCreate();
this._drag = null;
if (this._wired) {
(_b = (_a = this.ctx).removeEventListener) == null ? void 0 : _b.call(_a, "mounted", this._onRerender);
(_d = (_c = this.ctx).removeEventListener) == null ? void 0 : _d.call(_c, "updated", this._onRerender);
this._wired = false;
}
}
}
_core__default.registerFeatures({ ink: InkLayer });
export {
default2 as default
};