apexcharts
Version:
A JavaScript Chart Library
314 lines (313 loc) • 10.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 Environment = _core.__apex_Environment_Environment;
const Utils = _core.__apex_Utils;
class ContextMenu {
/**
* @param {import('../../types/internal').ChartStateW} w
* @param {import('../../types/internal').ChartContext} ctx
*/
constructor(w, ctx) {
this.w = w;
this.ctx = ctx;
this.menu = null;
this._items = [];
this._focusIndex = -1;
this._trigger = null;
this._onContext = this._onContext.bind(this);
this._onDocDown = this._onDocDown.bind(this);
this._onKey = this._onKey.bind(this);
this._afterRender = this._afterRender.bind(this);
ctx.addEventListener("mounted", this._afterRender);
ctx.addEventListener("updated", this._afterRender);
}
_cfg() {
return this.w.config.chart.contextMenu || {};
}
_enabled() {
return this._cfg().enabled === true;
}
_doc() {
return this.w.dom.baseEl && this.w.dom.baseEl.ownerDocument;
}
/** (Re)attach the contextmenu trigger to the freshly (re)built SVG. */
_afterRender() {
this._detachTrigger();
this.close();
if (!this._enabled() || !Environment.isBrowser()) return;
const svg = this.w.dom.Paper && this.w.dom.Paper.node;
if (!svg) return;
svg.addEventListener("contextmenu", this._onContext);
this._trigger = svg;
}
_detachTrigger() {
if (this._trigger) {
this._trigger.removeEventListener("contextmenu", this._onContext);
this._trigger = null;
}
}
/** @param {any} e */
_onContext(e) {
if (!this._enabled()) return;
e.preventDefault();
this.open(e.clientX, e.clientY);
}
/**
* Client pixel -> data {x,y} via the grid client-rect fraction (scale
* independent). Null when the grid is not measurable.
* @param {number} cx @param {number} cy
* @returns {{x:number,y:number}|null}
*/
_clientToData(cx, cy) {
const w = this.w;
const grid = w.dom.baseEl && w.dom.baseEl.querySelector(".apexcharts-grid");
if (!grid) return null;
const r = grid.getBoundingClientRect();
if (!r.width || !r.height) return null;
const clamp = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
const fx = clamp((cx - r.left) / r.width);
const fy = clamp((cy - r.top) / r.height);
const x = w.globals.minX + fx * (w.globals.maxX - w.globals.minX);
const s = w.globals.yAxisScale && w.globals.yAxisScale[0];
const ymin = s && isFinite(s.niceMin) ? s.niceMin : w.globals.minY;
const ymax = s && isFinite(s.niceMax) ? s.niceMax : w.globals.maxY;
const y = ymax - fy * (ymax - ymin);
return { x, y };
}
/**
* Resolve the configured items into runnable entries, dropping built-ins
* whose dependency is absent (e.g. measure not enabled).
* @param {any} context
* @returns {Array<{id:string,label:string,run:Function}>}
*/
_resolveItems(context) {
const cfg = this._cfg();
const raw = Array.isArray(cfg.items) && cfg.items.length ? cfg.items : ["annotate", "xline", "yline", "measure"];
const labels = cfg.labels || {};
const out = [];
raw.forEach((it) => {
if (typeof it === "string") {
if (it === "annotate") {
out.push({
id: "annotate",
label: labels.annotate || "Add note here",
run: () => this._annotate(context)
});
} else if (it === "xline") {
out.push({
id: "xline",
label: labels.xline || "Annotate here",
run: () => this._line(context, "x")
});
} else if (it === "yline") {
out.push({
id: "yline",
label: labels.yline || "Mark this level",
run: () => this._line(context, "y")
});
} else if (it === "measure") {
const m = this.ctx.measure;
const on = m && this.w.config.chart.measure && this.w.config.chart.measure.enabled;
if (on) {
out.push({
id: "measure",
label: labels.measure || "Measure from here",
run: () => m.seedFromClient(context.clientX, context.clientY)
});
}
}
} else if (it && typeof it === "object" && typeof it.onClick === "function") {
out.push({
id: it.id || "custom",
label: it.label || "Action",
run: () => it.onClick(this.ctx, context)
});
}
});
return out;
}
/** @param {any} context */
_annotate(context) {
if (context.x == null || context.y == null) return;
const cfg = this._cfg();
const ink = this.ctx.ink;
if (ink && typeof ink.createAt === "function") {
ink.createAt(context.x, context.y, { text: cfg.noteText || "Note" });
return;
}
this.ctx.addPointAnnotation(
{
x: context.x,
y: context.y,
marker: { size: 5 },
label: {
text: cfg.noteText || "Note",
style: { background: "#fff", color: "#334155" }
}
},
true
);
}
/**
* The 'xline' / 'yline' items: drop a dashed line annotation at the clicked
* data point ('x' = vertical line at the clicked x, 'y' = horizontal line
* at the clicked y). Lines only: no x2/y2 is ever set, so this never
* creates a range rectangle. Both items share chart.contextMenu.line
* ({ text, strokeDashArray, color }) for styling.
* @param {any} context @param {'x'|'y'} axis
*/
_line(context, axis) {
const lc = this._cfg().line || {};
const val = axis === "x" ? context.x : context.y;
if (val == null) return;
const ink = this.ctx.ink;
if (ink && typeof ink.createLineAt === "function") {
ink.createLineAt(axis, val, {
text: lc.text,
strokeDashArray: lc.strokeDashArray,
color: lc.color
});
return;
}
const anno = {
strokeDashArray: lc.strokeDashArray != null ? lc.strokeDashArray : 4
};
if (lc.text) anno.label = { text: lc.text };
if (lc.color) {
anno.borderColor = lc.color;
anno.label = Utils.extend(anno.label || {}, { borderColor: lc.color });
}
if (axis === "x") {
this.ctx.addXaxisAnnotation(Utils.extend(anno, { x: val }), true);
} else {
this.ctx.addYaxisAnnotation(Utils.extend(anno, { y: val }), true);
}
}
/**
* Open the menu at a client-space point.
* @param {number} clientX @param {number} clientY
*/
open(clientX, clientY) {
this.close();
const w = this.w;
const elWrap = w.dom.elWrap;
const doc = this._doc();
if (!elWrap || !doc) return;
const data = this._clientToData(clientX, clientY);
const g = w.globals;
const context = {
x: data ? data.x : null,
y: data ? data.y : null,
seriesIndex: g.capturedSeriesIndex >= 0 ? g.capturedSeriesIndex : null,
dataPointIndex: g.capturedDataPointIndex >= 0 ? g.capturedDataPointIndex : null,
clientX,
clientY
};
const items = this._resolveItems(context);
if (!items.length) return;
this._items = items;
const menu = doc.createElement("div");
menu.className = "apexcharts-context-menu";
menu.setAttribute("role", "menu");
menu.style.position = "absolute";
menu.style.visibility = "hidden";
items.forEach((it, i) => {
const btn = doc.createElement("button");
btn.type = "button";
btn.className = "apexcharts-context-menu-item";
btn.setAttribute("role", "menuitem");
btn.tabIndex = -1;
btn.textContent = it.label;
btn.addEventListener("click", (ev) => {
ev.stopPropagation();
this._activate(i);
});
btn.addEventListener("mouseenter", () => this._focus(i));
menu.appendChild(btn);
});
elWrap.appendChild(menu);
this.menu = menu;
const wrapRect = elWrap.getBoundingClientRect();
let left = clientX - wrapRect.left;
let top = clientY - wrapRect.top;
const mw = menu.offsetWidth;
const mh = menu.offsetHeight;
const maxLeft = Math.max(0, elWrap.clientWidth - mw);
const maxTop = Math.max(0, elWrap.clientHeight - mh);
if (left > maxLeft) left = maxLeft;
if (top > maxTop) top = maxTop;
menu.style.left = Math.max(0, left) + "px";
menu.style.top = Math.max(0, top) + "px";
menu.style.visibility = "visible";
doc.addEventListener("mousedown", this._onDocDown, true);
doc.addEventListener("keydown", this._onKey, true);
this._focus(0);
}
/** @param {number} i */
_focus(i) {
if (!this.menu) return;
const btns = this.menu.querySelectorAll(".apexcharts-context-menu-item");
if (this._focusIndex >= 0 && btns[this._focusIndex]) {
btns[this._focusIndex].classList.remove("apexcharts-context-menu-item--active");
}
this._focusIndex = i;
if (btns[i]) {
btns[i].classList.add("apexcharts-context-menu-item--active");
if (typeof btns[i].focus === "function") btns[i].focus();
}
}
/** @param {number} i */
_activate(i) {
const it = this._items[i];
this.close();
if (it) it.run();
}
/** @param {any} e */
_onDocDown(e) {
if (this.menu && this.menu.contains(e.target)) return;
this.close();
}
/** @param {any} e */
_onKey(e) {
if (!this.menu || !this._items.length) return;
if (e.key === "Escape") {
e.preventDefault();
this.close();
} else if (e.key === "ArrowDown") {
e.preventDefault();
this._focus((this._focusIndex + 1) % this._items.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
this._focus((this._focusIndex - 1 + this._items.length) % this._items.length);
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this._activate(this._focusIndex < 0 ? 0 : this._focusIndex);
}
}
close() {
const doc = this._doc();
if (doc) {
doc.removeEventListener("mousedown", this._onDocDown, true);
doc.removeEventListener("keydown", this._onKey, true);
}
if (this.menu && this.menu.parentNode) {
this.menu.parentNode.removeChild(this.menu);
}
this.menu = null;
this._items = [];
this._focusIndex = -1;
}
teardown() {
this.close();
this._detachTrigger();
}
}
_core__default.registerFeatures({ contextMenu: ContextMenu });
export {
default2 as default
};