@icen.ai/ui 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +49 -6
- package/README.md +12 -9
- package/dist/behaviors/ai-branch.d.mts +19 -0
- package/dist/behaviors/ai-branch.mjs +217 -0
- package/dist/behaviors/ai-chat.d.mts +8 -6
- package/dist/behaviors/ai-chat.mjs +68 -8
- package/dist/behaviors/ai-composer.mjs +108 -20
- package/dist/behaviors/ai-feedback.d.mts +26 -0
- package/dist/behaviors/ai-feedback.mjs +303 -0
- package/dist/behaviors/ai-panel.d.mts +15 -1
- package/dist/behaviors/ai-panel.mjs +26 -0
- package/dist/behaviors/ai-threads.d.mts +38 -0
- package/dist/behaviors/ai-threads.mjs +319 -0
- package/dist/behaviors/ai-tool.mjs +5 -0
- package/dist/behaviors/ai-tools.mjs +5 -0
- package/dist/behaviors/charts.d.mts +103 -1
- package/dist/behaviors/charts.mjs +795 -0
- package/dist/behaviors/events.d.mts +193 -0
- package/dist/behaviors/kb-access.d.mts +34 -0
- package/dist/behaviors/kb-access.mjs +569 -0
- package/dist/behaviors/kb-acl.d.mts +50 -0
- package/dist/behaviors/kb-acl.mjs +405 -0
- package/dist/behaviors/kb-answer.d.mts +38 -0
- package/dist/behaviors/kb-answer.mjs +2055 -0
- package/dist/behaviors/kb-audit.d.mts +23 -0
- package/dist/behaviors/kb-audit.mjs +375 -0
- package/dist/behaviors/kb-canvas.d.mts +44 -0
- package/dist/behaviors/kb-canvas.mjs +589 -0
- package/dist/behaviors/kb-chain.d.mts +15 -0
- package/dist/behaviors/kb-chain.mjs +270 -0
- package/dist/behaviors/kb-checkpoint.d.mts +16 -0
- package/dist/behaviors/kb-checkpoint.mjs +342 -0
- package/dist/behaviors/kb-chunks.d.mts +31 -0
- package/dist/behaviors/kb-chunks.mjs +475 -0
- package/dist/behaviors/kb-citation.d.mts +38 -0
- package/dist/behaviors/kb-citation.mjs +677 -0
- package/dist/behaviors/kb-clarify.d.mts +20 -0
- package/dist/behaviors/kb-clarify.mjs +248 -0
- package/dist/behaviors/kb-conflict.d.mts +22 -0
- package/dist/behaviors/kb-conflict.mjs +284 -0
- package/dist/behaviors/kb-connector.d.mts +17 -0
- package/dist/behaviors/kb-connector.mjs +375 -0
- package/dist/behaviors/kb-core.d.mts +529 -0
- package/dist/behaviors/kb-core.mjs +890 -0
- package/dist/behaviors/kb-eval.d.mts +22 -0
- package/dist/behaviors/kb-eval.mjs +408 -0
- package/dist/behaviors/kb-explain.d.mts +11 -0
- package/dist/behaviors/kb-explain.mjs +243 -0
- package/dist/behaviors/kb-filter.d.mts +31 -0
- package/dist/behaviors/kb-filter.mjs +587 -0
- package/dist/behaviors/kb-gap.d.mts +27 -0
- package/dist/behaviors/kb-gap.mjs +273 -0
- package/dist/behaviors/kb-hittest.d.mts +34 -0
- package/dist/behaviors/kb-hittest.mjs +409 -0
- package/dist/behaviors/kb-metadata.d.mts +29 -0
- package/dist/behaviors/kb-metadata.mjs +326 -0
- package/dist/behaviors/kb-passage.d.mts +41 -0
- package/dist/behaviors/kb-passage.mjs +529 -0
- package/dist/behaviors/kb-pipeline.d.mts +18 -0
- package/dist/behaviors/kb-pipeline.mjs +381 -0
- package/dist/behaviors/kb-qa.d.mts +26 -0
- package/dist/behaviors/kb-qa.mjs +332 -0
- package/dist/behaviors/kb-rerank.d.mts +20 -0
- package/dist/behaviors/kb-rerank.mjs +315 -0
- package/dist/behaviors/kb-retrieval.d.mts +49 -0
- package/dist/behaviors/kb-retrieval.mjs +746 -0
- package/dist/behaviors/kb-review.d.mts +44 -0
- package/dist/behaviors/kb-review.mjs +451 -0
- package/dist/behaviors/kb-sandbox.d.mts +33 -0
- package/dist/behaviors/kb-sandbox.mjs +325 -0
- package/dist/behaviors/kb-segment.d.mts +32 -0
- package/dist/behaviors/kb-segment.mjs +499 -0
- package/dist/behaviors/kb-sources.d.mts +27 -0
- package/dist/behaviors/kb-sources.mjs +373 -0
- package/dist/behaviors/kb-sql.d.mts +43 -0
- package/dist/behaviors/kb-sql.mjs +948 -0
- package/dist/behaviors/kb-trace.d.mts +16 -0
- package/dist/behaviors/kb-trace.mjs +414 -0
- package/dist/behaviors/kb-visibility.d.mts +44 -0
- package/dist/behaviors/kb-visibility.mjs +403 -0
- package/dist/behaviors/kb-who-can.d.mts +32 -0
- package/dist/behaviors/kb-who-can.mjs +379 -0
- package/dist/components/ai-branch.mjs +3 -0
- package/dist/components/ai-chat.css +295 -0
- package/dist/components/ai-feedback.mjs +3 -0
- package/dist/components/ai-panel.css +217 -0
- package/dist/components/ai-threads.mjs +3 -0
- package/dist/components/chart-graph.css +57 -0
- package/dist/components/chart-graph.mjs +4 -0
- package/dist/components/chart-map.mjs +3 -0
- package/dist/components/chart-scatter.mjs +1 -1
- package/dist/components/charts.css +32 -0
- package/dist/components/charts.mjs +1 -0
- package/dist/components/kb-access.mjs +4 -0
- package/dist/components/kb-acl.mjs +4 -0
- package/dist/components/kb-agent.css +481 -0
- package/dist/components/kb-answer.mjs +4 -0
- package/dist/components/kb-audit.mjs +4 -0
- package/dist/components/kb-canvas.mjs +4 -0
- package/dist/components/kb-chain.mjs +4 -0
- package/dist/components/kb-checkpoint.mjs +4 -0
- package/dist/components/kb-chunks.mjs +4 -0
- package/dist/components/kb-citation.mjs +4 -0
- package/dist/components/kb-clarify.mjs +4 -0
- package/dist/components/kb-conflict.mjs +4 -0
- package/dist/components/kb-connector.mjs +4 -0
- package/dist/components/kb-data.css +649 -0
- package/dist/components/kb-eval.mjs +4 -0
- package/dist/components/kb-explain.mjs +4 -0
- package/dist/components/kb-filter.mjs +4 -0
- package/dist/components/kb-gap.mjs +4 -0
- package/dist/components/kb-ground.css +566 -0
- package/dist/components/kb-hittest.mjs +4 -0
- package/dist/components/kb-ingest.css +898 -0
- package/dist/components/kb-metadata.mjs +4 -0
- package/dist/components/kb-ops.css +548 -0
- package/dist/components/kb-passage.mjs +4 -0
- package/dist/components/kb-perm.css +444 -0
- package/dist/components/kb-pipeline.mjs +4 -0
- package/dist/components/kb-qa.mjs +4 -0
- package/dist/components/kb-rerank.mjs +4 -0
- package/dist/components/kb-retrieval.mjs +4 -0
- package/dist/components/kb-review.mjs +4 -0
- package/dist/components/kb-sandbox.mjs +4 -0
- package/dist/components/kb-search.css +940 -0
- package/dist/components/kb-segment.mjs +4 -0
- package/dist/components/kb-sources.mjs +4 -0
- package/dist/components/kb-sql.mjs +4 -0
- package/dist/components/kb-trace.mjs +4 -0
- package/dist/components/kb-visibility.mjs +4 -0
- package/dist/components/kb-who-can.mjs +4 -0
- package/dist/components/kb.css +252 -0
- package/dist/components/kb.mjs +9 -0
- package/dist/index.d.mts +37 -2
- package/dist/index.mjs +8555 -205
- package/dist/registry.json +325 -3
- package/dist/ui.css +5451 -45
- package/docs/research/2026-10-03-ai-kb-components.md +392 -0
- package/docs/research/2026-10-03-kb-permissions.md +992 -0
- package/docs/spec/kb-family.md +443 -0
- package/package.json +2 -2
|
@@ -1525,10 +1525,803 @@ function renderChart(el, raw) {
|
|
|
1525
1525
|
}
|
|
1526
1526
|
};
|
|
1527
1527
|
}
|
|
1528
|
+
var GRAPH_CHART_TYPE = "graph";
|
|
1529
|
+
var MAP_CHART_TYPE = "map";
|
|
1530
|
+
function buildLayoutState(nodes, edges, width, height) {
|
|
1531
|
+
const ids = [];
|
|
1532
|
+
const index = /* @__PURE__ */ new Map();
|
|
1533
|
+
for (const node of nodes) {
|
|
1534
|
+
const id = String(node?.id ?? "").trim();
|
|
1535
|
+
if (!id || index.has(id)) continue;
|
|
1536
|
+
index.set(id, ids.length);
|
|
1537
|
+
ids.push(id);
|
|
1538
|
+
}
|
|
1539
|
+
const pairs = [];
|
|
1540
|
+
for (const edge of edges) {
|
|
1541
|
+
const a = index.get(String(edge?.source ?? "").trim());
|
|
1542
|
+
const b = index.get(String(edge?.target ?? "").trim());
|
|
1543
|
+
if (a == null || b == null || a === b) continue;
|
|
1544
|
+
pairs.push([a, b]);
|
|
1545
|
+
}
|
|
1546
|
+
const n = ids.length;
|
|
1547
|
+
return {
|
|
1548
|
+
ids,
|
|
1549
|
+
index,
|
|
1550
|
+
x: new Float64Array(n),
|
|
1551
|
+
y: new Float64Array(n),
|
|
1552
|
+
pairs,
|
|
1553
|
+
width,
|
|
1554
|
+
height,
|
|
1555
|
+
k: n > 0 ? Math.sqrt(width * height / n) : 1
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function relaxLayout(state, rounds, pinned = -1, tempScale = 1) {
|
|
1559
|
+
const { x, y, pairs, width, height, k } = state;
|
|
1560
|
+
const n = x.length;
|
|
1561
|
+
if (n <= 1 || rounds <= 0) return;
|
|
1562
|
+
const cx = width / 2;
|
|
1563
|
+
const cy = height / 2;
|
|
1564
|
+
const gravity = 0.015;
|
|
1565
|
+
const tempStart = Math.min(width, height) / 6 * tempScale;
|
|
1566
|
+
const dx = new Float64Array(n);
|
|
1567
|
+
const dy = new Float64Array(n);
|
|
1568
|
+
for (let round = 0; round < rounds; round++) {
|
|
1569
|
+
dx.fill(0);
|
|
1570
|
+
dy.fill(0);
|
|
1571
|
+
const temp = Math.max(0.5, tempStart * (1 - round / rounds));
|
|
1572
|
+
for (let i = 0; i < n; i++) {
|
|
1573
|
+
for (let j = i + 1; j < n; j++) {
|
|
1574
|
+
let vx = x[j] - x[i];
|
|
1575
|
+
let vy = y[j] - y[i];
|
|
1576
|
+
let d2 = vx * vx + vy * vy;
|
|
1577
|
+
if (d2 < 0.01) {
|
|
1578
|
+
vx = 0.5 + (j - i) % 7 * 0.25;
|
|
1579
|
+
vy = (i * 3 + j) % 5 * 0.3 - 0.6;
|
|
1580
|
+
d2 = vx * vx + vy * vy;
|
|
1581
|
+
}
|
|
1582
|
+
const d = Math.sqrt(d2);
|
|
1583
|
+
const f = k * k / d;
|
|
1584
|
+
const ux = vx / d * f;
|
|
1585
|
+
const uy = vy / d * f;
|
|
1586
|
+
dx[i] -= ux;
|
|
1587
|
+
dy[i] -= uy;
|
|
1588
|
+
dx[j] += ux;
|
|
1589
|
+
dy[j] += uy;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
for (const [a, b] of pairs) {
|
|
1593
|
+
let vx = x[b] - x[a];
|
|
1594
|
+
let vy = y[b] - y[a];
|
|
1595
|
+
let d = Math.sqrt(vx * vx + vy * vy);
|
|
1596
|
+
if (d < 0.01) {
|
|
1597
|
+
vx = 1;
|
|
1598
|
+
vy = 0;
|
|
1599
|
+
d = 1;
|
|
1600
|
+
}
|
|
1601
|
+
const f = Math.min(d * d / k, k * 3);
|
|
1602
|
+
const ux = vx / d * f;
|
|
1603
|
+
const uy = vy / d * f;
|
|
1604
|
+
dx[a] += ux;
|
|
1605
|
+
dy[a] += uy;
|
|
1606
|
+
dx[b] -= ux;
|
|
1607
|
+
dy[b] -= uy;
|
|
1608
|
+
}
|
|
1609
|
+
for (let i = 0; i < n; i++) {
|
|
1610
|
+
if (i === pinned) continue;
|
|
1611
|
+
dx[i] += (cx - x[i]) * gravity;
|
|
1612
|
+
dy[i] += (cy - y[i]) * gravity;
|
|
1613
|
+
const dm = Math.sqrt(dx[i] * dx[i] + dy[i] * dy[i]);
|
|
1614
|
+
if (dm <= 0) continue;
|
|
1615
|
+
const v = Math.min(dm, temp) / dm;
|
|
1616
|
+
x[i] = Math.min(width, Math.max(0, x[i] + dx[i] * v));
|
|
1617
|
+
y[i] = Math.min(height, Math.max(0, y[i] + dy[i] * v));
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function fitLayout(state, margin) {
|
|
1622
|
+
const { x, y, width, height } = state;
|
|
1623
|
+
const n = x.length;
|
|
1624
|
+
if (n === 0) return;
|
|
1625
|
+
let minX = Infinity;
|
|
1626
|
+
let maxX = -Infinity;
|
|
1627
|
+
let minY = Infinity;
|
|
1628
|
+
let maxY = -Infinity;
|
|
1629
|
+
for (let i = 0; i < n; i++) {
|
|
1630
|
+
if (x[i] < minX) minX = x[i];
|
|
1631
|
+
if (x[i] > maxX) maxX = x[i];
|
|
1632
|
+
if (y[i] < minY) minY = y[i];
|
|
1633
|
+
if (y[i] > maxY) maxY = y[i];
|
|
1634
|
+
}
|
|
1635
|
+
const spanX = Math.max(1e-6, maxX - minX);
|
|
1636
|
+
const spanY = Math.max(1e-6, maxY - minY);
|
|
1637
|
+
const scale = Math.min((width - margin * 2) / spanX, (height - margin * 2) / spanY, 4);
|
|
1638
|
+
const offX = (width - spanX * scale) / 2 - minX * scale;
|
|
1639
|
+
const offY = (height - spanY * scale) / 2 - minY * scale;
|
|
1640
|
+
for (let i = 0; i < n; i++) {
|
|
1641
|
+
x[i] = x[i] * scale + offX;
|
|
1642
|
+
y[i] = y[i] * scale + offY;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function initRingLayout(state, seed = 0) {
|
|
1646
|
+
const { ids, index, x, y, width, height } = state;
|
|
1647
|
+
const n = ids.length;
|
|
1648
|
+
if (n === 0) return;
|
|
1649
|
+
if (n === 1) {
|
|
1650
|
+
x[0] = width / 2;
|
|
1651
|
+
y[0] = height / 2;
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const order = [...ids].sort();
|
|
1655
|
+
const radius = Math.min(width, height) / 2 * 0.76;
|
|
1656
|
+
const deg = (seed % 360 + 360) % 360;
|
|
1657
|
+
const seedAngle = deg * Math.PI / 180;
|
|
1658
|
+
for (let rank = 0; rank < n; rank++) {
|
|
1659
|
+
const i = index.get(order[rank] ?? "") ?? -1;
|
|
1660
|
+
if (i < 0) continue;
|
|
1661
|
+
const angle = seedAngle - Math.PI / 2 + rank * 2 * Math.PI / n;
|
|
1662
|
+
x[i] = width / 2 + radius * Math.cos(angle);
|
|
1663
|
+
y[i] = height / 2 + radius * Math.sin(angle);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function layoutGraph(nodes, edges, opts) {
|
|
1667
|
+
const width = Math.max(16, opts?.width ?? 320);
|
|
1668
|
+
const height = Math.max(16, opts?.height ?? 200);
|
|
1669
|
+
const state = buildLayoutState(nodes, edges, width, height);
|
|
1670
|
+
if (state.ids.length === 0) return /* @__PURE__ */ new Map();
|
|
1671
|
+
initRingLayout(state, opts?.seed ?? 0);
|
|
1672
|
+
relaxLayout(state, Math.max(1, Math.round(opts?.iterations ?? 300)));
|
|
1673
|
+
fitLayout(state, Math.max(8, Math.min(width, height) * 0.08));
|
|
1674
|
+
const out = /* @__PURE__ */ new Map();
|
|
1675
|
+
state.ids.forEach((id, i) => out.set(id, { x: state.x[i], y: state.y[i] }));
|
|
1676
|
+
return out;
|
|
1677
|
+
}
|
|
1678
|
+
var datumMarkInfo = /* @__PURE__ */ new WeakMap();
|
|
1679
|
+
var datumEventControl = /* @__PURE__ */ new WeakMap();
|
|
1680
|
+
function dispatchDatumEvent(root, type, mark, phase, pointer) {
|
|
1681
|
+
const numOr = (v) => v == null ? void 0 : Number(v);
|
|
1682
|
+
const detail = {
|
|
1683
|
+
type,
|
|
1684
|
+
phase,
|
|
1685
|
+
chart: chartDelegateMeta.get(root)?.type() ?? "vbar",
|
|
1686
|
+
index: numOr(mark.getAttribute("data-chart-index")),
|
|
1687
|
+
seriesIndex: numOr(mark.getAttribute("data-chart-series")),
|
|
1688
|
+
seriesName: mark.getAttribute("data-chart-series-name") ?? void 0,
|
|
1689
|
+
label: mark.getAttribute("data-chart-label") ?? void 0,
|
|
1690
|
+
value: numOr(mark.getAttribute("data-chart-value")),
|
|
1691
|
+
target: mark,
|
|
1692
|
+
pointerX: pointer?.x,
|
|
1693
|
+
pointerY: pointer?.y,
|
|
1694
|
+
datum: datumMarkInfo.get(mark)?.datum
|
|
1695
|
+
};
|
|
1696
|
+
emitIcen(root, `icen:chart-${type}`, detail);
|
|
1697
|
+
}
|
|
1698
|
+
function bindDatumEvents(root) {
|
|
1699
|
+
const existing = datumEventControl.get(root);
|
|
1700
|
+
if (existing) return existing;
|
|
1701
|
+
const tipEnabled = () => chartDelegateMeta.get(root)?.tooltip() ?? true;
|
|
1702
|
+
const markOf = (e) => {
|
|
1703
|
+
const t = e.target;
|
|
1704
|
+
return t instanceof Element ? t.closest("[data-chart-mark]") : null;
|
|
1705
|
+
};
|
|
1706
|
+
let hoverMark = null;
|
|
1707
|
+
let suppressClick = false;
|
|
1708
|
+
const bindings = [];
|
|
1709
|
+
const on = (type, fn) => {
|
|
1710
|
+
const listener = (evt) => fn(evt);
|
|
1711
|
+
root.addEventListener(type, listener);
|
|
1712
|
+
bindings.push([type, listener]);
|
|
1713
|
+
};
|
|
1714
|
+
const enterMark = (mark, px, py) => {
|
|
1715
|
+
hoverMark = mark;
|
|
1716
|
+
mark.classList.add("is-hover");
|
|
1717
|
+
dispatchDatumEvent(root, "hover", mark, "enter", { x: px, y: py });
|
|
1718
|
+
if (tipEnabled()) {
|
|
1719
|
+
const tip = chartTooltip();
|
|
1720
|
+
tip.textContent = datumMarkInfo.get(mark)?.tip ?? tooltipText(mark);
|
|
1721
|
+
applyTooltipSizing(mark);
|
|
1722
|
+
tip.hidden = false;
|
|
1723
|
+
moveTooltip(px, py);
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
const leaveMark = (pointer) => {
|
|
1727
|
+
if (!hoverMark) return;
|
|
1728
|
+
hoverMark.classList.remove("is-hover");
|
|
1729
|
+
dispatchDatumEvent(root, "hover", hoverMark, "leave", pointer);
|
|
1730
|
+
hoverMark = null;
|
|
1731
|
+
chartTooltip().hidden = true;
|
|
1732
|
+
};
|
|
1733
|
+
on("pointerover", (e) => {
|
|
1734
|
+
const mark = markOf(e);
|
|
1735
|
+
if (!mark || mark === hoverMark) return;
|
|
1736
|
+
leaveMark({ x: e.clientX, y: e.clientY });
|
|
1737
|
+
enterMark(mark, e.clientX, e.clientY);
|
|
1738
|
+
});
|
|
1739
|
+
on("pointermove", (e) => {
|
|
1740
|
+
if (!hoverMark) return;
|
|
1741
|
+
dispatchDatumEvent(root, "hover", hoverMark, "move", { x: e.clientX, y: e.clientY });
|
|
1742
|
+
if (tipEnabled()) moveTooltip(e.clientX, e.clientY);
|
|
1743
|
+
});
|
|
1744
|
+
on("pointerout", (e) => {
|
|
1745
|
+
const mark = markOf(e);
|
|
1746
|
+
if (mark && mark === hoverMark) leaveMark({ x: e.clientX, y: e.clientY });
|
|
1747
|
+
});
|
|
1748
|
+
on("pointerleave", () => leaveMark());
|
|
1749
|
+
on("click", (e) => {
|
|
1750
|
+
if (suppressClick) {
|
|
1751
|
+
suppressClick = false;
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const mark = markOf(e);
|
|
1755
|
+
if (mark) dispatchDatumEvent(root, "click", mark, void 0, { x: e.clientX, y: e.clientY });
|
|
1756
|
+
});
|
|
1757
|
+
on("dblclick", (e) => {
|
|
1758
|
+
const mark = markOf(e);
|
|
1759
|
+
if (mark) dispatchDatumEvent(root, "dblclick", mark, void 0, { x: e.clientX, y: e.clientY });
|
|
1760
|
+
});
|
|
1761
|
+
on("contextmenu", (e) => {
|
|
1762
|
+
const mark = markOf(e);
|
|
1763
|
+
if (!mark) return;
|
|
1764
|
+
e.preventDefault();
|
|
1765
|
+
dispatchDatumEvent(root, "contextmenu", mark, void 0, { x: e.clientX, y: e.clientY });
|
|
1766
|
+
});
|
|
1767
|
+
const control = {
|
|
1768
|
+
unbind() {
|
|
1769
|
+
for (const [type, listener] of bindings) root.removeEventListener(type, listener);
|
|
1770
|
+
bindings.length = 0;
|
|
1771
|
+
if (hoverMark) {
|
|
1772
|
+
hoverMark.classList.remove("is-hover");
|
|
1773
|
+
hoverMark = null;
|
|
1774
|
+
}
|
|
1775
|
+
chartTooltip().hidden = true;
|
|
1776
|
+
datumEventControl.delete(root);
|
|
1777
|
+
},
|
|
1778
|
+
suppressNextClick() {
|
|
1779
|
+
suppressClick = true;
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
datumEventControl.set(root, control);
|
|
1783
|
+
return control;
|
|
1784
|
+
}
|
|
1785
|
+
function buildClusterModel(keys, labels) {
|
|
1786
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1787
|
+
for (const key of keys) {
|
|
1788
|
+
if (typeof key === "string" && key !== "") seen.add(key);
|
|
1789
|
+
}
|
|
1790
|
+
if (seen.size === 0) return null;
|
|
1791
|
+
const list = [...seen].sort();
|
|
1792
|
+
return {
|
|
1793
|
+
keys: list,
|
|
1794
|
+
indexOf: new Map(list.map((k, i) => [k, i])),
|
|
1795
|
+
label: (key) => labels?.[key] ?? key,
|
|
1796
|
+
color: (index) => paletteVar(index + 1)
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function registerDatumMark(mark, datum, tip) {
|
|
1800
|
+
datumMarkInfo.set(mark, { datum, tip });
|
|
1801
|
+
}
|
|
1802
|
+
function truncateText(text, max = 8) {
|
|
1803
|
+
const chars = Array.from(text);
|
|
1804
|
+
return chars.length > max ? `${chars.slice(0, max).join("")}\u2026` : text;
|
|
1805
|
+
}
|
|
1806
|
+
function svgClientPoint(svg, clientX, clientY, viewBoxWidth, viewBoxHeight) {
|
|
1807
|
+
const rect = svg.getBoundingClientRect();
|
|
1808
|
+
const scale = Math.min(rect.width / viewBoxWidth, rect.height / viewBoxHeight);
|
|
1809
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
1810
|
+
return { x: viewBoxWidth / 2, y: viewBoxHeight / 2 };
|
|
1811
|
+
}
|
|
1812
|
+
return {
|
|
1813
|
+
x: (clientX - rect.left - (rect.width - viewBoxWidth * scale) / 2) / scale,
|
|
1814
|
+
y: (clientY - rect.top - (rect.height - viewBoxHeight * scale) / 2) / scale
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
function normalizeGraphSpec(raw) {
|
|
1818
|
+
let src = raw;
|
|
1819
|
+
if (typeof src === "string") {
|
|
1820
|
+
try {
|
|
1821
|
+
src = JSON.parse(src);
|
|
1822
|
+
} catch {
|
|
1823
|
+
src = {};
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (!src || typeof src !== "object" || Array.isArray(src)) src = {};
|
|
1827
|
+
const s = src;
|
|
1828
|
+
const spec = { ...normalizeChartSpec(s), type: "graph", nodes: [], edges: [] };
|
|
1829
|
+
if (Array.isArray(s.nodes)) {
|
|
1830
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1831
|
+
for (const v of s.nodes) {
|
|
1832
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
1833
|
+
const r = v;
|
|
1834
|
+
const id = String(r.id ?? "").trim();
|
|
1835
|
+
if (!id || seen.has(id)) continue;
|
|
1836
|
+
seen.add(id);
|
|
1837
|
+
const node = { id };
|
|
1838
|
+
if (r.label != null) node.label = String(r.label);
|
|
1839
|
+
if (typeof r.cluster === "string" && r.cluster !== "") node.cluster = r.cluster;
|
|
1840
|
+
const weight = toNum(r.weight);
|
|
1841
|
+
if (weight != null) node.weight = weight;
|
|
1842
|
+
if (r.meta && typeof r.meta === "object" && !Array.isArray(r.meta)) {
|
|
1843
|
+
node.meta = { ...r.meta };
|
|
1844
|
+
}
|
|
1845
|
+
spec.nodes.push(node);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
const known = new Set(spec.nodes.map((n) => n.id));
|
|
1849
|
+
if (Array.isArray(s.edges)) {
|
|
1850
|
+
for (const v of s.edges) {
|
|
1851
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
1852
|
+
const r = v;
|
|
1853
|
+
const source = String(r.source ?? "").trim();
|
|
1854
|
+
const target = String(r.target ?? "").trim();
|
|
1855
|
+
if (!known.has(source) || !known.has(target) || source === target) continue;
|
|
1856
|
+
const edge = { source, target };
|
|
1857
|
+
const weight = toNum(r.weight);
|
|
1858
|
+
if (weight != null) edge.weight = weight;
|
|
1859
|
+
spec.edges.push(edge);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (s.directed === true) spec.directed = true;
|
|
1863
|
+
if (s.clusterLabels && typeof s.clusterLabels === "object" && !Array.isArray(s.clusterLabels)) {
|
|
1864
|
+
const labels = {};
|
|
1865
|
+
for (const [key, value] of Object.entries(s.clusterLabels)) {
|
|
1866
|
+
if (typeof value === "string" && value !== "") labels[key] = value;
|
|
1867
|
+
}
|
|
1868
|
+
spec.clusterLabels = labels;
|
|
1869
|
+
}
|
|
1870
|
+
return spec;
|
|
1871
|
+
}
|
|
1872
|
+
function normalizeMapSpec(raw) {
|
|
1873
|
+
let src = raw;
|
|
1874
|
+
if (typeof src === "string") {
|
|
1875
|
+
try {
|
|
1876
|
+
src = JSON.parse(src);
|
|
1877
|
+
} catch {
|
|
1878
|
+
src = {};
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (!src || typeof src !== "object" || Array.isArray(src)) src = {};
|
|
1882
|
+
const s = src;
|
|
1883
|
+
const spec = { ...normalizeChartSpec(s), type: "map", points: [] };
|
|
1884
|
+
if (Array.isArray(s.points)) {
|
|
1885
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1886
|
+
for (const v of s.points) {
|
|
1887
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
1888
|
+
const r = v;
|
|
1889
|
+
const id = String(r.id ?? "").trim();
|
|
1890
|
+
if (!id || seen.has(id)) continue;
|
|
1891
|
+
seen.add(id);
|
|
1892
|
+
const point = { id, x: toNum(r.x) ?? 0, y: toNum(r.y) ?? 0 };
|
|
1893
|
+
if (typeof r.cluster === "string" && r.cluster !== "") point.cluster = r.cluster;
|
|
1894
|
+
if (r.label != null) point.label = String(r.label);
|
|
1895
|
+
if (r.meta && typeof r.meta === "object" && !Array.isArray(r.meta)) {
|
|
1896
|
+
point.meta = { ...r.meta };
|
|
1897
|
+
}
|
|
1898
|
+
spec.points.push(point);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (s.clusters && typeof s.clusters === "object" && !Array.isArray(s.clusters)) {
|
|
1902
|
+
const labels = {};
|
|
1903
|
+
for (const [key, value] of Object.entries(s.clusters)) {
|
|
1904
|
+
if (typeof value === "string" && value !== "") labels[key] = value;
|
|
1905
|
+
}
|
|
1906
|
+
spec.clusters = labels;
|
|
1907
|
+
}
|
|
1908
|
+
return spec;
|
|
1909
|
+
}
|
|
1910
|
+
var GRAPH_W = 360;
|
|
1911
|
+
var GRAPH_H = 260;
|
|
1912
|
+
var GRAPH_PAD = 28;
|
|
1913
|
+
var GRAPH_LW = GRAPH_W - GRAPH_PAD * 2;
|
|
1914
|
+
var GRAPH_LH = GRAPH_H - GRAPH_PAD * 2;
|
|
1915
|
+
var graphMarkerSeq = 0;
|
|
1916
|
+
function renderGraph(el, raw) {
|
|
1917
|
+
const noop = () => void 0;
|
|
1918
|
+
let current = normalizeGraphSpec(raw);
|
|
1919
|
+
if (typeof document === "undefined") {
|
|
1920
|
+
return { el, spec: current, update: noop, on: () => noop, destroy: noop };
|
|
1921
|
+
}
|
|
1922
|
+
const legendState = { hidden: null };
|
|
1923
|
+
let layout = null;
|
|
1924
|
+
let svgRef = null;
|
|
1925
|
+
let nodeEls = [];
|
|
1926
|
+
let edgeList = [];
|
|
1927
|
+
let nodeRadii = [];
|
|
1928
|
+
let directed = false;
|
|
1929
|
+
let arrowId = "";
|
|
1930
|
+
const edgeEnds = (from, to) => {
|
|
1931
|
+
const ax = GRAPH_PAD + (layout ? layout.x[from] : 0);
|
|
1932
|
+
const ay = GRAPH_PAD + (layout ? layout.y[from] : 0);
|
|
1933
|
+
const bx = GRAPH_PAD + (layout ? layout.x[to] : 0);
|
|
1934
|
+
const by = GRAPH_PAD + (layout ? layout.y[to] : 0);
|
|
1935
|
+
if (!directed) {
|
|
1936
|
+
return { x1: ax.toFixed(1), y1: ay.toFixed(1), x2: bx.toFixed(1), y2: by.toFixed(1) };
|
|
1937
|
+
}
|
|
1938
|
+
const dx = bx - ax;
|
|
1939
|
+
const dy = by - ay;
|
|
1940
|
+
const d = Math.hypot(dx, dy) || 1;
|
|
1941
|
+
const cut = (nodeRadii[to] ?? 6) + 4;
|
|
1942
|
+
return {
|
|
1943
|
+
x1: ax.toFixed(1),
|
|
1944
|
+
y1: ay.toFixed(1),
|
|
1945
|
+
x2: (bx - dx / d * cut).toFixed(1),
|
|
1946
|
+
y2: (by - dy / d * cut).toFixed(1)
|
|
1947
|
+
};
|
|
1948
|
+
};
|
|
1949
|
+
const paint = (body) => {
|
|
1950
|
+
directed = current.directed === true;
|
|
1951
|
+
layout = null;
|
|
1952
|
+
svgRef = null;
|
|
1953
|
+
nodeEls = [];
|
|
1954
|
+
edgeList = [];
|
|
1955
|
+
arrowId = "";
|
|
1956
|
+
const nodes = current.nodes;
|
|
1957
|
+
if (nodes.length === 0) {
|
|
1958
|
+
nodeRadii = [];
|
|
1959
|
+
renderEmpty(body, current.emptyLabel);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
const clusters = buildClusterModel(nodes.map((n) => n.cluster), current.clusterLabels);
|
|
1963
|
+
const tone = toneVar(current.tone);
|
|
1964
|
+
const idOf = new Map(nodes.map((n, i) => [n.id, i]));
|
|
1965
|
+
const degree = nodes.map(() => 0);
|
|
1966
|
+
for (const e of current.edges) {
|
|
1967
|
+
const a = idOf.get(e.source);
|
|
1968
|
+
const b = idOf.get(e.target);
|
|
1969
|
+
if (a == null || b == null || a === b) continue;
|
|
1970
|
+
degree[a] += 1;
|
|
1971
|
+
degree[b] += 1;
|
|
1972
|
+
}
|
|
1973
|
+
const weights = nodes.map((n, i) => n.weight ?? degree[i]);
|
|
1974
|
+
let wMin = Infinity;
|
|
1975
|
+
let wMax = -Infinity;
|
|
1976
|
+
for (const w of weights) {
|
|
1977
|
+
if (w < wMin) wMin = w;
|
|
1978
|
+
if (w > wMax) wMax = w;
|
|
1979
|
+
}
|
|
1980
|
+
nodeRadii = weights.map((w) => 6 + (wMax > wMin ? (w - wMin) / (wMax - wMin) : 0.5) * 12);
|
|
1981
|
+
let eMin = Infinity;
|
|
1982
|
+
let eMax = -Infinity;
|
|
1983
|
+
for (const e of current.edges) {
|
|
1984
|
+
if (e.weight == null) continue;
|
|
1985
|
+
if (e.weight < eMin) eMin = e.weight;
|
|
1986
|
+
if (e.weight > eMax) eMax = e.weight;
|
|
1987
|
+
}
|
|
1988
|
+
const edgeWidth = (e) => e.weight == null ? 1 : 0.6 + (eMax > eMin ? (e.weight - eMin) / (eMax - eMin) : 0.5) * 1.9;
|
|
1989
|
+
const wrap = document.createElement("div");
|
|
1990
|
+
wrap.className = "chart-graph";
|
|
1991
|
+
if (current.tone) wrap.style.setProperty("--chart-tone", toneVar(current.tone));
|
|
1992
|
+
const svg = svgEl("svg", { viewBox: `0 0 ${GRAPH_W} ${GRAPH_H}`, role: "img" });
|
|
1993
|
+
svgRef = svg;
|
|
1994
|
+
svg.addEventListener("pointerdown", onNodePointerDown);
|
|
1995
|
+
const defs = svgEl("defs", {});
|
|
1996
|
+
if (directed) {
|
|
1997
|
+
arrowId = `icen-chart-graph-arrow-${++graphMarkerSeq}`;
|
|
1998
|
+
const marker = svgEl("marker", {
|
|
1999
|
+
id: arrowId,
|
|
2000
|
+
viewBox: "0 0 8 8",
|
|
2001
|
+
refX: "7",
|
|
2002
|
+
refY: "4",
|
|
2003
|
+
markerWidth: "6",
|
|
2004
|
+
markerHeight: "6",
|
|
2005
|
+
orient: "auto"
|
|
2006
|
+
});
|
|
2007
|
+
marker.appendChild(svgEl("path", { d: "M0.5,0.8 L7.5,4 L0.5,7.2 Z", fill: "var(--token-text-faint)" }));
|
|
2008
|
+
defs.appendChild(marker);
|
|
2009
|
+
}
|
|
2010
|
+
layout = buildLayoutState(nodes, current.edges, GRAPH_LW, GRAPH_LH);
|
|
2011
|
+
initRingLayout(layout, 0);
|
|
2012
|
+
relaxLayout(layout, 300);
|
|
2013
|
+
fitLayout(layout, 20);
|
|
2014
|
+
const edgeLayer = svgEl("g", { class: "chart-graph-edges" });
|
|
2015
|
+
const nodeLayer = svgEl("g", { class: "chart-graph-nodes" });
|
|
2016
|
+
for (const e of current.edges) {
|
|
2017
|
+
const from = idOf.get(e.source);
|
|
2018
|
+
const to = idOf.get(e.target);
|
|
2019
|
+
if (from == null || to == null || from === to) continue;
|
|
2020
|
+
const line = svgEl("line", {
|
|
2021
|
+
...edgeEnds(from, to),
|
|
2022
|
+
stroke: "var(--token-line-soft)",
|
|
2023
|
+
"stroke-width": edgeWidth(e).toFixed(2),
|
|
2024
|
+
...directed ? { "marker-end": `url(#${arrowId})` } : {}
|
|
2025
|
+
});
|
|
2026
|
+
line.classList.add("chart-graph-edge");
|
|
2027
|
+
const fromKey = nodes[from]?.cluster;
|
|
2028
|
+
const toKey = nodes[to]?.cluster;
|
|
2029
|
+
if (fromKey) line.setAttribute("data-edge-cluster-from", fromKey);
|
|
2030
|
+
if (toKey) line.setAttribute("data-edge-cluster-to", toKey);
|
|
2031
|
+
edgeList.push({ el: line, from, to });
|
|
2032
|
+
edgeLayer.appendChild(line);
|
|
2033
|
+
}
|
|
2034
|
+
nodes.forEach((node, i) => {
|
|
2035
|
+
const clusterKey = node.cluster;
|
|
2036
|
+
const ci = clusterKey != null && clusters ? clusters.indexOf.get(clusterKey) : void 0;
|
|
2037
|
+
const clusterName = ci != null && clusters && clusterKey != null ? clusters.label(clusterKey) : void 0;
|
|
2038
|
+
const fill = clusters ? ci != null ? clusters.color(ci) : "var(--token-text-faint)" : tone;
|
|
2039
|
+
const pos = { x: GRAPH_PAD + layout.x[i], y: GRAPH_PAD + layout.y[i] };
|
|
2040
|
+
const g = svgEl("g", {
|
|
2041
|
+
class: "chart-graph-node",
|
|
2042
|
+
transform: `translate(${pos.x.toFixed(1)},${pos.y.toFixed(1)})`
|
|
2043
|
+
});
|
|
2044
|
+
g.setAttribute("data-chart-node-id", node.id);
|
|
2045
|
+
setMarkData(g, {
|
|
2046
|
+
index: i,
|
|
2047
|
+
seriesIndex: ci,
|
|
2048
|
+
seriesName: clusterName,
|
|
2049
|
+
label: node.label ?? node.id,
|
|
2050
|
+
value: weights[i]
|
|
2051
|
+
});
|
|
2052
|
+
const dot = svgEl("circle", {
|
|
2053
|
+
class: "chart-graph-dot",
|
|
2054
|
+
r: (nodeRadii[i] ?? 6).toFixed(1),
|
|
2055
|
+
fill,
|
|
2056
|
+
"fill-opacity": "0.92",
|
|
2057
|
+
stroke: "var(--token-card)",
|
|
2058
|
+
"stroke-width": "1.5"
|
|
2059
|
+
});
|
|
2060
|
+
const hit = svgEl("circle", {
|
|
2061
|
+
class: "chart-graph-hit",
|
|
2062
|
+
r: String(Math.max((nodeRadii[i] ?? 6) + 4, 11)),
|
|
2063
|
+
fill: "transparent",
|
|
2064
|
+
"data-chart-hit": "1"
|
|
2065
|
+
});
|
|
2066
|
+
const labelText = truncateText(node.label ?? node.id);
|
|
2067
|
+
const head = clusterName ? `${clusterName} \xB7 ${labelText}` : labelText;
|
|
2068
|
+
const tip = weights[i] > 0 ? `${head}\uFF1A${weights[i]}` : head;
|
|
2069
|
+
const title = svgEl("title", {});
|
|
2070
|
+
title.textContent = tip;
|
|
2071
|
+
g.append(dot, hit, title);
|
|
2072
|
+
registerDatumMark(g, { id: node.id, label: node.label ?? node.id, cluster: clusterKey, meta: node.meta }, tip);
|
|
2073
|
+
nodeEls.push(g);
|
|
2074
|
+
nodeLayer.appendChild(g);
|
|
2075
|
+
});
|
|
2076
|
+
svg.append(defs, edgeLayer, nodeLayer);
|
|
2077
|
+
wrap.appendChild(svg);
|
|
2078
|
+
body.appendChild(wrap);
|
|
2079
|
+
if (clusters) {
|
|
2080
|
+
const legend = toggleLegend(
|
|
2081
|
+
clusters.keys.map((key, ci) => ({ key, label: clusters.label(key), tone: ci + 1, si: ci })),
|
|
2082
|
+
wrap
|
|
2083
|
+
);
|
|
2084
|
+
body.appendChild(legend);
|
|
2085
|
+
const syncEdges = () => {
|
|
2086
|
+
const hiddenOf = (key) => key != null && legend.querySelector(`[data-chart-key="${CSS.escape(key)}"]`)?.classList.contains("is-off") === true;
|
|
2087
|
+
for (const edge of edgeList) {
|
|
2088
|
+
edge.el.classList.toggle(
|
|
2089
|
+
"is-off",
|
|
2090
|
+
hiddenOf(edge.el.getAttribute("data-edge-cluster-from")) || hiddenOf(edge.el.getAttribute("data-edge-cluster-to"))
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
legend.addEventListener("click", syncEdges);
|
|
2095
|
+
legend.addEventListener("keydown", (e) => {
|
|
2096
|
+
if (e.key === "Enter" || e.key === " ") syncEdges();
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
const paintPositions = () => {
|
|
2101
|
+
if (!layout) return;
|
|
2102
|
+
for (let i = 0; i < nodeEls.length; i++) {
|
|
2103
|
+
nodeEls[i]?.setAttribute(
|
|
2104
|
+
"transform",
|
|
2105
|
+
`translate(${(GRAPH_PAD + layout.x[i]).toFixed(1)},${(GRAPH_PAD + layout.y[i]).toFixed(1)})`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
for (const edge of edgeList) {
|
|
2109
|
+
const ends = edgeEnds(edge.from, edge.to);
|
|
2110
|
+
edge.el.setAttribute("x1", ends.x1);
|
|
2111
|
+
edge.el.setAttribute("y1", ends.y1);
|
|
2112
|
+
edge.el.setAttribute("x2", ends.x2);
|
|
2113
|
+
edge.el.setAttribute("y2", ends.y2);
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
let drag = null;
|
|
2117
|
+
const applyDragFrame = () => {
|
|
2118
|
+
if (!drag || !layout || !svgRef) return;
|
|
2119
|
+
const p = svgClientPoint(svgRef, drag.px, drag.py, GRAPH_W, GRAPH_H);
|
|
2120
|
+
const i = drag.i;
|
|
2121
|
+
layout.x[i] = Math.min(GRAPH_LW, Math.max(0, p.x - GRAPH_PAD));
|
|
2122
|
+
layout.y[i] = Math.min(GRAPH_LH, Math.max(0, p.y - GRAPH_PAD));
|
|
2123
|
+
relaxLayout(layout, 20, i, 0.4);
|
|
2124
|
+
paintPositions();
|
|
2125
|
+
drag.raf = 0;
|
|
2126
|
+
};
|
|
2127
|
+
const onDragMove = (e) => {
|
|
2128
|
+
if (!drag) return;
|
|
2129
|
+
if (Math.abs(e.clientX - drag.px) + Math.abs(e.clientY - drag.py) > 2) drag.moved = true;
|
|
2130
|
+
drag.px = e.clientX;
|
|
2131
|
+
drag.py = e.clientY;
|
|
2132
|
+
if (drag.raf === 0) drag.raf = requestAnimationFrame(applyDragFrame);
|
|
2133
|
+
};
|
|
2134
|
+
const endDrag = () => {
|
|
2135
|
+
if (!drag) return;
|
|
2136
|
+
if (drag.raf !== 0) cancelAnimationFrame(drag.raf);
|
|
2137
|
+
nodeEls[drag.i]?.classList.remove("is-drag");
|
|
2138
|
+
svgRef?.classList.remove("is-dragging");
|
|
2139
|
+
if (drag.moved) eventsControl.suppressNextClick();
|
|
2140
|
+
drag = null;
|
|
2141
|
+
window.removeEventListener("pointermove", onDragMove);
|
|
2142
|
+
window.removeEventListener("pointerup", endDrag);
|
|
2143
|
+
window.removeEventListener("pointercancel", endDrag);
|
|
2144
|
+
};
|
|
2145
|
+
const onNodePointerDown = (e) => {
|
|
2146
|
+
if (e.button !== 0 || !layout) return;
|
|
2147
|
+
const target = e.target;
|
|
2148
|
+
const g = target instanceof Element ? target.closest(".chart-graph-node") : null;
|
|
2149
|
+
if (!g) return;
|
|
2150
|
+
const i = layout.index.get(g.getAttribute("data-chart-node-id") ?? "");
|
|
2151
|
+
if (i == null) return;
|
|
2152
|
+
e.preventDefault();
|
|
2153
|
+
endDrag();
|
|
2154
|
+
drag = { i, moved: false, raf: 0, px: e.clientX, py: e.clientY };
|
|
2155
|
+
g.classList.add("is-drag");
|
|
2156
|
+
svgRef?.classList.add("is-dragging");
|
|
2157
|
+
window.addEventListener("pointermove", onDragMove);
|
|
2158
|
+
window.addEventListener("pointerup", endDrag);
|
|
2159
|
+
window.addEventListener("pointercancel", endDrag);
|
|
2160
|
+
};
|
|
2161
|
+
chartDelegateMeta.set(el, { type: () => GRAPH_CHART_TYPE, tooltip: () => current.tooltip !== false });
|
|
2162
|
+
const eventsControl = bindDatumEvents(el);
|
|
2163
|
+
const render = () => {
|
|
2164
|
+
if (drag) endDrag();
|
|
2165
|
+
const clusters = buildClusterModel(current.nodes.map((n) => n.cluster), current.clusterLabels);
|
|
2166
|
+
if (current.legend !== void 0) legendState.hidden = current.legend === false;
|
|
2167
|
+
withChrome(el, current, clusters != null, paint, legendState);
|
|
2168
|
+
};
|
|
2169
|
+
render();
|
|
2170
|
+
return {
|
|
2171
|
+
el,
|
|
2172
|
+
spec: current,
|
|
2173
|
+
update(next) {
|
|
2174
|
+
current = normalizeGraphSpec(next);
|
|
2175
|
+
render();
|
|
2176
|
+
},
|
|
2177
|
+
on(name, listener) {
|
|
2178
|
+
const handler = (event) => {
|
|
2179
|
+
listener(event.detail, event);
|
|
2180
|
+
};
|
|
2181
|
+
el.addEventListener(`icen:chart-${name}`, handler);
|
|
2182
|
+
return () => el.removeEventListener(`icen:chart-${name}`, handler);
|
|
2183
|
+
},
|
|
2184
|
+
destroy() {
|
|
2185
|
+
if (drag) endDrag();
|
|
2186
|
+
el.textContent = "";
|
|
2187
|
+
eventsControl.unbind();
|
|
2188
|
+
chartTooltip().hidden = true;
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
var MAP_W = 360;
|
|
2193
|
+
var MAP_H = 220;
|
|
2194
|
+
var MAP_PAD = 14;
|
|
2195
|
+
function renderMap(el, raw) {
|
|
2196
|
+
const noop = () => void 0;
|
|
2197
|
+
let current = normalizeMapSpec(raw);
|
|
2198
|
+
if (typeof document === "undefined") {
|
|
2199
|
+
return { el, spec: current, update: noop, on: () => noop, destroy: noop };
|
|
2200
|
+
}
|
|
2201
|
+
const legendState = { hidden: null };
|
|
2202
|
+
const paint = (body) => {
|
|
2203
|
+
const points = current.points.filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
2204
|
+
if (points.length === 0) {
|
|
2205
|
+
renderEmpty(body, current.emptyLabel);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const clusters = buildClusterModel(points.map((p) => p.cluster), current.clusters);
|
|
2209
|
+
const tone = toneVar(current.tone);
|
|
2210
|
+
let xMin = Infinity;
|
|
2211
|
+
let xMax = -Infinity;
|
|
2212
|
+
let yMin = Infinity;
|
|
2213
|
+
let yMax = -Infinity;
|
|
2214
|
+
for (const p of points) {
|
|
2215
|
+
if (p.x < xMin) xMin = p.x;
|
|
2216
|
+
if (p.x > xMax) xMax = p.x;
|
|
2217
|
+
if (p.y < yMin) yMin = p.y;
|
|
2218
|
+
if (p.y > yMax) yMax = p.y;
|
|
2219
|
+
}
|
|
2220
|
+
if (xMax === xMin) xMax = xMin + 1;
|
|
2221
|
+
if (yMax === yMin) yMax = yMin + 1;
|
|
2222
|
+
const xAt = (x) => MAP_PAD + (x - xMin) / (xMax - xMin) * (MAP_W - MAP_PAD * 2);
|
|
2223
|
+
const yAt = (y) => MAP_H - MAP_PAD - (y - yMin) / (yMax - yMin) * (MAP_H - MAP_PAD * 2);
|
|
2224
|
+
const wrap = document.createElement("div");
|
|
2225
|
+
wrap.className = "chart-map";
|
|
2226
|
+
if (current.tone) wrap.style.setProperty("--chart-tone", toneVar(current.tone));
|
|
2227
|
+
const svg = svgEl("svg", { viewBox: `0 0 ${MAP_W} ${MAP_H}`, role: "img" });
|
|
2228
|
+
for (let i = 0; i <= 3; i++) {
|
|
2229
|
+
const y = MAP_PAD + i * (MAP_H - MAP_PAD * 2) / 3;
|
|
2230
|
+
svg.appendChild(svgEl("line", {
|
|
2231
|
+
x1: String(MAP_PAD),
|
|
2232
|
+
x2: String(MAP_W - MAP_PAD),
|
|
2233
|
+
y1: String(y),
|
|
2234
|
+
y2: String(y),
|
|
2235
|
+
stroke: "var(--token-line-soft)",
|
|
2236
|
+
"stroke-width": "0.75",
|
|
2237
|
+
"stroke-dasharray": "2 4"
|
|
2238
|
+
}));
|
|
2239
|
+
}
|
|
2240
|
+
for (let i = 0; i <= 4; i++) {
|
|
2241
|
+
const x = MAP_PAD + i * (MAP_W - MAP_PAD * 2) / 4;
|
|
2242
|
+
svg.appendChild(svgEl("line", {
|
|
2243
|
+
x1: String(x),
|
|
2244
|
+
x2: String(x),
|
|
2245
|
+
y1: String(MAP_PAD),
|
|
2246
|
+
y2: String(MAP_H - MAP_PAD),
|
|
2247
|
+
stroke: "var(--token-line-soft)",
|
|
2248
|
+
"stroke-width": "0.75",
|
|
2249
|
+
"stroke-dasharray": "2 4"
|
|
2250
|
+
}));
|
|
2251
|
+
}
|
|
2252
|
+
points.forEach((p, i) => {
|
|
2253
|
+
const ci = p.cluster != null && clusters ? clusters.indexOf.get(p.cluster) : void 0;
|
|
2254
|
+
const clusterName = ci != null && clusters && p.cluster != null ? clusters.label(p.cluster) : void 0;
|
|
2255
|
+
const color = clusters ? ci != null ? clusters.color(ci) : "var(--token-text-faint)" : tone;
|
|
2256
|
+
const label = p.label ?? p.id;
|
|
2257
|
+
const labelText = truncateText(label);
|
|
2258
|
+
const tip = clusterName ? `${clusterName} \xB7 ${labelText}` : labelText;
|
|
2259
|
+
const g = svgEl("g", {
|
|
2260
|
+
class: "chart-map-point",
|
|
2261
|
+
transform: `translate(${xAt(p.x).toFixed(1)},${yAt(p.y).toFixed(1)})`
|
|
2262
|
+
});
|
|
2263
|
+
g.setAttribute("data-chart-point-id", p.id);
|
|
2264
|
+
setMarkData(g, { index: i, seriesIndex: ci, seriesName: clusterName, label });
|
|
2265
|
+
const dot = svgEl("circle", {
|
|
2266
|
+
class: "chart-map-dot",
|
|
2267
|
+
r: "4",
|
|
2268
|
+
fill: color,
|
|
2269
|
+
"fill-opacity": "0.7",
|
|
2270
|
+
stroke: color,
|
|
2271
|
+
"stroke-width": "1"
|
|
2272
|
+
});
|
|
2273
|
+
const hit = svgEl("circle", { class: "chart-map-hit", r: "9", fill: "transparent", "data-chart-hit": "1" });
|
|
2274
|
+
const title = svgEl("title", {});
|
|
2275
|
+
title.textContent = tip;
|
|
2276
|
+
g.append(dot, hit, title);
|
|
2277
|
+
registerDatumMark(g, { id: p.id, label, cluster: p.cluster, meta: p.meta }, tip);
|
|
2278
|
+
svg.appendChild(g);
|
|
2279
|
+
});
|
|
2280
|
+
wrap.appendChild(svg);
|
|
2281
|
+
body.appendChild(wrap);
|
|
2282
|
+
if (clusters) {
|
|
2283
|
+
body.appendChild(
|
|
2284
|
+
toggleLegend(
|
|
2285
|
+
clusters.keys.map((key, ci) => ({ key, label: clusters.label(key), tone: ci + 1, si: ci })),
|
|
2286
|
+
wrap
|
|
2287
|
+
)
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
chartDelegateMeta.set(el, { type: () => MAP_CHART_TYPE, tooltip: () => current.tooltip !== false });
|
|
2292
|
+
const eventsControl = bindDatumEvents(el);
|
|
2293
|
+
const render = () => {
|
|
2294
|
+
const clusters = buildClusterModel(current.points.map((p) => p.cluster), current.clusters);
|
|
2295
|
+
if (current.legend !== void 0) legendState.hidden = current.legend === false;
|
|
2296
|
+
withChrome(el, current, clusters != null, paint, legendState);
|
|
2297
|
+
};
|
|
2298
|
+
render();
|
|
2299
|
+
return {
|
|
2300
|
+
el,
|
|
2301
|
+
spec: current,
|
|
2302
|
+
update(next) {
|
|
2303
|
+
current = normalizeMapSpec(next);
|
|
2304
|
+
render();
|
|
2305
|
+
},
|
|
2306
|
+
on(name, listener) {
|
|
2307
|
+
const handler = (event) => {
|
|
2308
|
+
listener(event.detail, event);
|
|
2309
|
+
};
|
|
2310
|
+
el.addEventListener(`icen:chart-${name}`, handler);
|
|
2311
|
+
return () => el.removeEventListener(`icen:chart-${name}`, handler);
|
|
2312
|
+
},
|
|
2313
|
+
destroy() {
|
|
2314
|
+
el.textContent = "";
|
|
2315
|
+
eventsControl.unbind();
|
|
2316
|
+
chartTooltip().hidden = true;
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
1528
2320
|
export {
|
|
1529
2321
|
bindChartEvents,
|
|
1530
2322
|
chartFormatValue,
|
|
1531
2323
|
inferChartType,
|
|
2324
|
+
layoutGraph,
|
|
1532
2325
|
normalizeChartSpec,
|
|
1533
2326
|
registerChartTone,
|
|
1534
2327
|
renderArea,
|
|
@@ -1536,9 +2329,11 @@ export {
|
|
|
1536
2329
|
renderChart,
|
|
1537
2330
|
renderDonut,
|
|
1538
2331
|
renderGauge,
|
|
2332
|
+
renderGraph,
|
|
1539
2333
|
renderHBar,
|
|
1540
2334
|
renderHeatmap,
|
|
1541
2335
|
renderLine,
|
|
2336
|
+
renderMap,
|
|
1542
2337
|
renderRadar,
|
|
1543
2338
|
renderScatter,
|
|
1544
2339
|
renderSparkline,
|