@icen.ai/ui 0.5.0 → 0.8.1

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.
Files changed (151) hide show
  1. package/AGENTS.md +262 -0
  2. package/README.md +52 -13
  3. package/dist/base.css +33 -5
  4. package/dist/behaviors/accordion.d.mts +7 -0
  5. package/dist/behaviors/accordion.mjs +37 -11
  6. package/dist/behaviors/ai-chat.d.mts +59 -0
  7. package/dist/behaviors/ai-chat.mjs +580 -0
  8. package/dist/behaviors/ai-composer.d.mts +137 -0
  9. package/dist/behaviors/ai-composer.mjs +2114 -0
  10. package/dist/behaviors/ai-core.d.mts +156 -0
  11. package/dist/behaviors/ai-core.mjs +504 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +413 -0
  14. package/dist/behaviors/ai-panel.d.mts +101 -0
  15. package/dist/behaviors/ai-panel.mjs +1087 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1061 -0
  18. package/dist/behaviors/ai-tool.d.mts +26 -0
  19. package/dist/behaviors/ai-tool.mjs +2173 -0
  20. package/dist/behaviors/ai-tools.d.mts +84 -0
  21. package/dist/behaviors/ai-tools.mjs +1762 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +64 -32
  24. package/dist/behaviors/carousel.d.mts +7 -0
  25. package/dist/behaviors/carousel.mjs +72 -27
  26. package/dist/behaviors/charts.d.mts +296 -0
  27. package/dist/behaviors/charts.mjs +1364 -440
  28. package/dist/behaviors/command-palette.d.mts +42 -0
  29. package/dist/behaviors/command-palette.mjs +167 -27
  30. package/dist/behaviors/context-menu.d.mts +53 -0
  31. package/dist/behaviors/context-menu.mjs +127 -39
  32. package/dist/behaviors/controls.d.mts +59 -0
  33. package/dist/behaviors/controls.mjs +601 -0
  34. package/dist/behaviors/copy.d.mts +7 -0
  35. package/dist/behaviors/copy.mjs +41 -15
  36. package/dist/behaviors/datatable.d.mts +90 -0
  37. package/dist/behaviors/datatable.mjs +279 -53
  38. package/dist/behaviors/date-picker.d.mts +4 -0
  39. package/dist/behaviors/date-picker.mjs +276 -69
  40. package/dist/behaviors/dropdown.d.mts +8 -0
  41. package/dist/behaviors/dropdown.mjs +171 -28
  42. package/dist/behaviors/events.d.mts +330 -0
  43. package/dist/behaviors/events.mjs +208 -0
  44. package/dist/behaviors/input.d.mts +4 -0
  45. package/dist/behaviors/input.mjs +52 -32
  46. package/dist/behaviors/modal.d.mts +30 -0
  47. package/dist/behaviors/modal.mjs +87 -16
  48. package/dist/behaviors/nav.d.mts +7 -0
  49. package/dist/behaviors/nav.mjs +57 -15
  50. package/dist/behaviors/notification.d.mts +188 -0
  51. package/dist/behaviors/notification.mjs +274 -140
  52. package/dist/behaviors/popover.d.mts +82 -0
  53. package/dist/behaviors/popover.mjs +92 -5
  54. package/dist/behaviors/select.d.mts +6 -0
  55. package/dist/behaviors/select.mjs +287 -45
  56. package/dist/behaviors/sidebar.d.mts +7 -0
  57. package/dist/behaviors/sidebar.mjs +44 -13
  58. package/dist/behaviors/slider.d.mts +7 -0
  59. package/dist/behaviors/slider.mjs +55 -43
  60. package/dist/behaviors/split-pane.d.mts +8 -0
  61. package/dist/behaviors/split-pane.mjs +70 -29
  62. package/dist/behaviors/tabs.d.mts +9 -0
  63. package/dist/behaviors/tabs.mjs +102 -26
  64. package/dist/behaviors/tag-input.d.mts +7 -0
  65. package/dist/behaviors/tag-input.mjs +66 -17
  66. package/dist/behaviors/theme.d.mts +36 -0
  67. package/dist/behaviors/theme.mjs +51 -5
  68. package/dist/behaviors/toast.d.mts +48 -0
  69. package/dist/behaviors/toast.mjs +98 -25
  70. package/dist/behaviors/tree.d.mts +7 -0
  71. package/dist/behaviors/tree.mjs +39 -15
  72. package/dist/behaviors/upload.d.mts +4 -0
  73. package/dist/behaviors/upload.mjs +79 -44
  74. package/dist/cli.mjs +1 -1
  75. package/dist/components/ai-chat.css +1037 -0
  76. package/dist/components/ai-chat.mjs +3 -0
  77. package/dist/components/ai-composer.mjs +3 -0
  78. package/dist/components/ai-context.mjs +2 -0
  79. package/dist/components/ai-diff.css +311 -0
  80. package/dist/components/ai-diff.mjs +3 -0
  81. package/dist/components/ai-files.mjs +3 -0
  82. package/dist/components/ai-message.mjs +3 -0
  83. package/dist/components/ai-panel.css +682 -0
  84. package/dist/components/ai-reasoning.mjs +3 -0
  85. package/dist/components/ai-subagent.mjs +2 -0
  86. package/dist/components/ai-todo.mjs +2 -0
  87. package/dist/components/ai-tool-call.mjs +2 -0
  88. package/dist/components/ai-tool.css +532 -0
  89. package/dist/components/ai-usage.mjs +3 -0
  90. package/dist/components/badge.css +5 -9
  91. package/dist/components/breadcrumb.css +0 -36
  92. package/dist/components/btn.css +4 -7
  93. package/dist/components/card.css +3 -6
  94. package/dist/components/chart-bar.css +10 -0
  95. package/dist/components/chart-calendar.mjs +4 -0
  96. package/dist/components/chart-gauge.css +1 -1
  97. package/dist/components/chart-heatmap.css +40 -0
  98. package/dist/components/chart-pie.css +0 -6
  99. package/dist/components/chart-scatter.css +24 -0
  100. package/dist/components/chart-scatter.mjs +4 -0
  101. package/dist/components/charts.css +87 -2
  102. package/dist/components/charts.mjs +1 -0
  103. package/dist/components/command-palette.css +2 -2
  104. package/dist/components/content.css +11 -25
  105. package/dist/components/copy.css +8 -8
  106. package/dist/components/datatable.css +5 -9
  107. package/dist/components/date-picker.css +4 -8
  108. package/dist/components/empty.css +0 -4
  109. package/dist/components/feedback.css +10 -17
  110. package/dist/components/form.css +13 -14
  111. package/dist/components/input.css +2 -2
  112. package/dist/components/layout.css +36 -36
  113. package/dist/components/media.css +12 -8
  114. package/dist/components/menu.css +2 -2
  115. package/dist/components/modal.css +1 -1
  116. package/dist/components/nav.css +11 -6
  117. package/dist/components/notification.css +27 -28
  118. package/dist/components/pagination.css +4 -2
  119. package/dist/components/pagination.mjs +1 -0
  120. package/dist/components/panel.css +16 -16
  121. package/dist/components/pill.css +15 -15
  122. package/dist/components/rating.mjs +1 -0
  123. package/dist/components/segmented.css +14 -6
  124. package/dist/components/segmented.mjs +1 -0
  125. package/dist/components/select.css +79 -24
  126. package/dist/components/sidebar.css +1 -1
  127. package/dist/components/split-pane.css +0 -1
  128. package/dist/components/stat.css +14 -18
  129. package/dist/components/steps.css +15 -5
  130. package/dist/components/steps.mjs +1 -0
  131. package/dist/components/switch.css +11 -13
  132. package/dist/components/switch.mjs +1 -0
  133. package/dist/components/table.css +46 -47
  134. package/dist/components/table.mjs +1 -0
  135. package/dist/components/tabs.css +57 -45
  136. package/dist/components/tag-input.css +5 -10
  137. package/dist/components/toast.css +16 -13
  138. package/dist/components/tooltip.css +2 -2
  139. package/dist/components/upload.css +0 -15
  140. package/dist/index.d.mts +39 -0
  141. package/dist/index.mjs +11243 -3312
  142. package/dist/registry.json +147 -9
  143. package/dist/retro-effects.css +23 -32
  144. package/dist/tokens/colors.css +4 -1
  145. package/dist/tokens/retro-effects.css +23 -32
  146. package/dist/tokens/style-profiles.css +24 -2
  147. package/dist/tokens/typography.css +2 -0
  148. package/dist/tokens.css +30 -3
  149. package/dist/ui.css +3420 -645
  150. package/docs/spec/ai-native.md +662 -0
  151. package/package.json +15 -5
@@ -1,6 +1,20 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/charts.ts
2
9
  var SVG_NS = "http://www.w3.org/2000/svg";
10
+ var REGISTERED_TONES = /* @__PURE__ */ new Map();
11
+ function registerChartTone(name, cssVar) {
12
+ if (!name || !cssVar) return;
13
+ REGISTERED_TONES.set(name, cssVar.startsWith("var(") ? cssVar : `var(${cssVar})`);
14
+ }
3
15
  function toneVar(tone = "accent") {
16
+ const registered = REGISTERED_TONES.get(tone);
17
+ if (registered) return registered;
4
18
  switch (tone) {
5
19
  case "success":
6
20
  return "var(--token-success)";
@@ -18,6 +32,19 @@ function toneVar(tone = "accent") {
18
32
  function fmt(opts, value) {
19
33
  return opts.formatValue ? opts.formatValue(value) : String(value);
20
34
  }
35
+ function maxOf(values) {
36
+ let max = 1;
37
+ for (const v of values) if (v > max) max = v;
38
+ return max;
39
+ }
40
+ function sampleLabels(labels, max = 7) {
41
+ if (labels.length <= max) return labels;
42
+ const out = [];
43
+ for (let i = 0; i < max; i++) {
44
+ out.push(labels[Math.round(i * (labels.length - 1) / (max - 1))] ?? "");
45
+ }
46
+ return out;
47
+ }
21
48
  function svgEl(tag, attrs) {
22
49
  const el = document.createElementNS(SVG_NS, tag);
23
50
  for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
@@ -54,383 +81,606 @@ function labelsRow(labels) {
54
81
  }
55
82
  return row;
56
83
  }
57
- function renderVBar(el, opts) {
58
- if (typeof document === "undefined") return;
59
- el.textContent = "";
60
- const n = Math.min(opts.labels.length, opts.values.length);
61
- if (n === 0) {
62
- renderEmpty(el, opts.emptyLabel);
63
- return;
84
+ function normalizeSeriesInput(opts) {
85
+ if (opts.series && opts.series.length > 0) return opts.series;
86
+ return [{ name: "", values: opts.values ?? [], tone: opts.tone }];
87
+ }
88
+ function toneOf(tone, seriesIndex) {
89
+ if (typeof tone === "string") return toneVar(tone);
90
+ if (typeof tone === "number") return paletteVar(tone);
91
+ return seriesIndex > 0 ? paletteVar((seriesIndex - 1) % 6 + 1) : void 0;
92
+ }
93
+ var PALETTE_VARS = [
94
+ "var(--token-accent)",
95
+ "var(--token-accent-cold)",
96
+ "var(--token-success)",
97
+ "var(--token-warning)",
98
+ "var(--token-info)",
99
+ "color-mix(in srgb, var(--token-text) 50%, transparent)"
100
+ ];
101
+ function paletteVar(index) {
102
+ const i = ((Math.floor(index) - 1) % 6 + 6) % 6;
103
+ return PALETTE_VARS[i];
104
+ }
105
+ function setMarkData(node, data) {
106
+ node.setAttribute("data-chart-mark", "1");
107
+ if (data.index != null) node.setAttribute("data-chart-index", String(data.index));
108
+ if (data.seriesIndex != null) node.setAttribute("data-chart-series", String(data.seriesIndex));
109
+ if (data.seriesName != null) node.setAttribute("data-chart-series-name", String(data.seriesName));
110
+ if (data.label != null) node.setAttribute("data-chart-label", String(data.label));
111
+ if (data.value != null) node.setAttribute("data-chart-value", String(data.value));
112
+ }
113
+ function copyMarkData(target, source) {
114
+ for (const attr of Array.from(source.attributes)) {
115
+ if (attr.name.startsWith("data-chart-")) target.setAttribute(attr.name, attr.value);
64
116
  }
65
- const labels = opts.labels.slice(0, n);
66
- const values = opts.values.slice(0, n);
67
- const max = Math.max(...values, 1);
68
- const wrap = document.createElement("div");
69
- wrap.className = "chart-vbar";
70
- if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
71
- values.forEach((value, i) => {
72
- const track = document.createElement("div");
73
- track.className = "chart-vbar-track";
74
- const bar = document.createElement("div");
75
- bar.className = "chart-vbar-bar";
76
- bar.style.height = `${Math.max(6, value / max * 100)}%`;
77
- bar.setAttribute("title", `${labels[i]}: ${fmt(opts, value)}`);
78
- track.appendChild(bar);
79
- wrap.appendChild(track);
80
- });
81
- el.append(wrap, labelsRow(labels));
82
117
  }
83
- function renderHBar(el, opts) {
84
- if (typeof document === "undefined") return;
85
- el.textContent = "";
86
- const n = Math.min(opts.labels.length, opts.values.length);
87
- if (n === 0) {
88
- renderEmpty(el, opts.emptyLabel);
89
- return;
118
+ var CHART_EYE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0"/><circle cx="12" cy="12" r="3"/></svg>';
119
+ var CHART_EYE_OFF_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.73 5.08A10.4 10.4 0 0 1 12 5c7 0 10 7 10 7a13.2 13.2 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.5 13.5 0 0 0 2 12s3 7 10 7a9.7 9.7 0 0 0 5.39-1.61"/><path d="M2 2l20 20"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>';
120
+ function parseChartSvg(svg) {
121
+ try {
122
+ const doc = new DOMParser().parseFromString(svg, "image/svg+xml");
123
+ const el = doc.documentElement;
124
+ return el && el.tagName.toLowerCase() === "svg" && !doc.querySelector("parsererror") ? document.importNode(el, true) : null;
125
+ } catch {
126
+ return null;
90
127
  }
91
- const labels = opts.labels.slice(0, n);
92
- const values = opts.values.slice(0, n);
93
- const max = Math.max(...values, 1);
94
- const wrap = document.createElement("div");
95
- wrap.className = "chart-hbar";
96
- if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
97
- values.forEach((value, i) => {
98
- const row = document.createElement("div");
99
- row.className = "chart-hbar-row";
100
- const label = document.createElement("span");
101
- label.className = "chart-hbar-label";
102
- label.textContent = labels[i] ?? "";
103
- const track = document.createElement("div");
104
- track.className = "chart-hbar-track";
105
- const bar = document.createElement("div");
106
- bar.className = "chart-hbar-bar";
107
- bar.style.width = `${Math.max(2, value / max * 100)}%`;
108
- const val = document.createElement("span");
109
- val.className = "chart-hbar-value";
110
- val.textContent = fmt(opts, value);
111
- track.appendChild(bar);
112
- row.append(label, track, val);
113
- wrap.appendChild(row);
114
- });
115
- el.appendChild(wrap);
116
128
  }
117
- function renderStack(el, opts) {
118
- if (typeof document === "undefined") return;
119
- el.textContent = "";
120
- const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
121
- if (opts.segments.length === 0 || total <= 0) {
122
- renderEmpty(el, opts.emptyLabel);
129
+ function withChrome(el, opts, hasLegend, paint, state) {
130
+ if (el.dataset.chartBody != null) {
131
+ el.textContent = "";
132
+ paint(el);
123
133
  return;
124
134
  }
125
- const bar = document.createElement("div");
126
- bar.className = "chart-stack";
127
- for (const seg of opts.segments) {
128
- const s = document.createElement("div");
129
- s.className = "chart-stack-seg";
130
- s.style.width = `${seg.value / total * 100}%`;
131
- if (seg.tone) s.style.setProperty("--chart-tone", toneVar(seg.tone));
132
- s.setAttribute("title", `${seg.label}: ${fmt(opts, seg.value)}`);
133
- bar.appendChild(s);
135
+ el.textContent = "";
136
+ const body = document.createElement("div");
137
+ body.className = "chart-body";
138
+ body.dataset.chartBody = "";
139
+ if (!opts.title && !hasLegend) {
140
+ el.appendChild(body);
141
+ paint(body);
142
+ return;
134
143
  }
144
+ let hidden = state && state.hidden != null ? state.hidden : opts.legend === false;
145
+ if (state) state.hidden = hidden;
146
+ el.setAttribute("data-legend-hidden", String(hidden));
147
+ const head = document.createElement("div");
148
+ head.className = "chart-head";
149
+ const title = document.createElement("div");
150
+ title.className = "chart-title";
151
+ title.textContent = opts.title ?? "";
152
+ if (!opts.title) title.setAttribute("aria-hidden", "true");
153
+ const eye = document.createElement("button");
154
+ eye.type = "button";
155
+ eye.className = "chart-legend-eye";
156
+ eye.dataset.chartLegendEye = "";
157
+ const syncEye = () => {
158
+ const nowHidden = el.getAttribute("data-legend-hidden") === "true";
159
+ eye.setAttribute("aria-pressed", String(!nowHidden));
160
+ eye.setAttribute("aria-label", nowHidden ? "\u663E\u793A\u56FE\u4F8B" : "\u9690\u85CF\u56FE\u4F8B");
161
+ eye.setAttribute("title", nowHidden ? "\u663E\u793A\u56FE\u4F8B" : "\u9690\u85CF\u56FE\u4F8B");
162
+ const icon = parseChartSvg(nowHidden ? CHART_EYE_OFF_SVG : CHART_EYE_SVG);
163
+ if (icon) eye.replaceChildren(icon);
164
+ };
165
+ eye.addEventListener("click", () => {
166
+ const nowHidden = el.getAttribute("data-legend-hidden") === "true";
167
+ hidden = !nowHidden;
168
+ if (state) state.hidden = hidden;
169
+ el.setAttribute("data-legend-hidden", String(hidden));
170
+ syncEye();
171
+ emitIcen(el, "icen:chart-legend-visibility", { visible: hidden ? false : true });
172
+ });
173
+ syncEye();
174
+ head.append(title, eye);
175
+ el.append(head, body);
176
+ paint(body);
177
+ }
178
+ function toggleLegend(items, scope) {
135
179
  const legend = document.createElement("div");
136
- legend.className = "chart-legend";
137
- for (const seg of opts.segments) {
138
- legend.appendChild(legendItem(seg.label, fmt(opts, seg.value), seg.tone));
180
+ legend.className = "chart-legend chart-legend--toggle";
181
+ for (const item of items) {
182
+ const row = legendItem(item.label, "", item.tone);
183
+ row.classList.add("chart-legend-toggle-item");
184
+ row.setAttribute("role", "button");
185
+ row.tabIndex = 0;
186
+ row.setAttribute("aria-pressed", "true");
187
+ row.dataset.chartKey = item.key;
188
+ const dot = row.querySelector(".chart-legend-dot");
189
+ const toneColor = toneOf(item.tone, item.si + 1);
190
+ if (dot && toneColor) dot.style.setProperty("--chart-tone", toneColor);
191
+ const toggle = () => {
192
+ const off = row.classList.toggle("is-off");
193
+ row.setAttribute("aria-pressed", String(!off));
194
+ scope.querySelectorAll(`[data-chart-key="${CSS.escape(item.key)}"]`).forEach((m) => {
195
+ m.classList.toggle("is-off", off);
196
+ });
197
+ emitIcen(legend, "icen:chart-legend-toggle", { key: item.key, seriesIndex: item.si, hidden: off });
198
+ };
199
+ row.addEventListener("click", toggle);
200
+ row.addEventListener("keydown", (e) => {
201
+ if (e.key === "Enter" || e.key === " ") {
202
+ e.preventDefault();
203
+ toggle();
204
+ }
205
+ });
206
+ legend.appendChild(row);
139
207
  }
140
- el.append(bar, legend);
208
+ for (const item of items) {
209
+ scope.querySelectorAll(`[data-chart-series="${item.si}"]`).forEach((m) => {
210
+ if (!m.hasAttribute("data-chart-key")) m.setAttribute("data-chart-key", item.key);
211
+ });
212
+ }
213
+ return legend;
214
+ }
215
+ function renderVBar(el, opts) {
216
+ if (typeof document === "undefined") return el;
217
+ const seriesList = normalizeSeriesInput(opts);
218
+ const labels = opts.labels ?? [];
219
+ const n = Math.min(labels.length, ...seriesList.map((s) => s.values.length));
220
+ withChrome(el, opts, seriesList.length > 1, (body) => {
221
+ if (n === 0 || seriesList.length === 0) {
222
+ renderEmpty(body, opts.emptyLabel);
223
+ return;
224
+ }
225
+ const labelsUsed = labels.slice(0, n);
226
+ let max = 1;
227
+ if (opts.stacked && seriesList.length > 1) {
228
+ for (let i = 0; i < n; i++) {
229
+ let sum = 0;
230
+ for (const s of seriesList) sum += Math.max(0, s.values[i] ?? 0);
231
+ if (sum > max) max = sum;
232
+ }
233
+ } else {
234
+ for (const s of seriesList) max = Math.max(max, maxOf(s.values.slice(0, n)));
235
+ }
236
+ const wrap = document.createElement("div");
237
+ wrap.className = "chart-vbar";
238
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
239
+ for (let i = 0; i < n; i++) {
240
+ const track = document.createElement("div");
241
+ track.className = "chart-vbar-track";
242
+ if (seriesList.length > 1) {
243
+ track.classList.add("chart-vbar-track--multi");
244
+ if (opts.stacked) track.classList.add("chart-vbar-track--stacked");
245
+ }
246
+ seriesList.forEach((s, si) => {
247
+ const value = s.values[i] ?? 0;
248
+ const bar = document.createElement("div");
249
+ bar.className = "chart-vbar-bar";
250
+ const height = value > 0 ? Math.max(6, value / max * 100) : 0;
251
+ bar.style.height = `${height}%`;
252
+ const toneColor = toneOf(s.tone, si);
253
+ if (toneColor) bar.style.setProperty("--chart-tone", toneColor);
254
+ bar.setAttribute("title", seriesList.length > 1 ? `${labelsUsed[i]} \xB7 ${s.name}: ${fmt(opts, value)}` : `${labelsUsed[i]}: ${fmt(opts, value)}`);
255
+ setMarkData(bar, {
256
+ index: i,
257
+ seriesIndex: seriesList.length > 1 ? si : void 0,
258
+ seriesName: seriesList.length > 1 ? s.name : void 0,
259
+ label: labelsUsed[i] ?? "",
260
+ value
261
+ });
262
+ track.appendChild(bar);
263
+ });
264
+ wrap.appendChild(track);
265
+ }
266
+ body.append(wrap, labelsRow(labelsUsed));
267
+ if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name, label: s.name, tone: s.tone, si })), wrap));
268
+ });
269
+ attachChartInteraction(el, "vbar");
270
+ return el;
271
+ }
272
+ function renderHBar(el, opts) {
273
+ if (typeof document === "undefined") return el;
274
+ withChrome(el, opts, false, (body) => {
275
+ const n = Math.min(opts.labels.length, opts.values.length);
276
+ if (n === 0) {
277
+ renderEmpty(body, opts.emptyLabel);
278
+ return;
279
+ }
280
+ const labels = opts.labels.slice(0, n);
281
+ const values = opts.values.slice(0, n);
282
+ const max = maxOf(values);
283
+ const wrap = document.createElement("div");
284
+ wrap.className = "chart-hbar";
285
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
286
+ values.forEach((value, i) => {
287
+ const row = document.createElement("div");
288
+ row.className = "chart-hbar-row";
289
+ const label = document.createElement("span");
290
+ label.className = "chart-hbar-label";
291
+ label.textContent = labels[i] ?? "";
292
+ const track = document.createElement("div");
293
+ track.className = "chart-hbar-track";
294
+ const bar = document.createElement("div");
295
+ bar.className = "chart-hbar-bar";
296
+ bar.style.width = value > 0 ? `${Math.max(2, value / max * 100)}%` : "0%";
297
+ setMarkData(bar, { index: i, label: labels[i] ?? "", value });
298
+ const val = document.createElement("span");
299
+ val.className = "chart-hbar-value";
300
+ val.textContent = fmt(opts, value);
301
+ track.appendChild(bar);
302
+ row.append(label, track, val);
303
+ wrap.appendChild(row);
304
+ });
305
+ body.appendChild(wrap);
306
+ });
307
+ attachChartInteraction(el, "hbar");
308
+ return el;
309
+ }
310
+ function renderStack(el, opts) {
311
+ if (typeof document === "undefined") return el;
312
+ withChrome(el, opts, opts.segments.length > 0, (body) => {
313
+ const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
314
+ if (opts.segments.length === 0 || total <= 0) {
315
+ renderEmpty(body, opts.emptyLabel);
316
+ return;
317
+ }
318
+ const bar = document.createElement("div");
319
+ bar.className = "chart-stack";
320
+ for (const [segIndex, seg] of opts.segments.entries()) {
321
+ const s = document.createElement("div");
322
+ s.className = "chart-stack-seg";
323
+ s.style.width = `${seg.value / total * 100}%`;
324
+ if (seg.tone) s.style.setProperty("--chart-tone", toneVar(seg.tone));
325
+ s.setAttribute("title", `${seg.label}: ${fmt(opts, seg.value)}`);
326
+ setMarkData(s, { index: segIndex, label: seg.label, value: seg.value });
327
+ bar.appendChild(s);
328
+ }
329
+ const legend = document.createElement("div");
330
+ legend.className = "chart-legend";
331
+ for (const seg of opts.segments) {
332
+ legend.appendChild(legendItem(seg.label, fmt(opts, seg.value), seg.tone));
333
+ }
334
+ body.append(bar, legend);
335
+ });
336
+ attachChartInteraction(el, "stack");
337
+ return el;
141
338
  }
142
339
  function renderDonut(el, opts) {
143
- if (typeof document === "undefined") return;
144
- el.textContent = "";
145
- const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
146
- if (opts.segments.length === 0 || total <= 0) {
147
- renderEmpty(el, opts.emptyLabel);
148
- return;
149
- }
150
- const radius = 42;
151
- const circumference = 2 * Math.PI * radius;
152
- const wrap = document.createElement("div");
153
- wrap.className = "chart-donut";
154
- const svg = svgEl("svg", { viewBox: "0 0 120 120", role: "img" });
155
- svg.style.transform = "rotate(-90deg)";
156
- svg.appendChild(svgEl("circle", {
157
- cx: "60",
158
- cy: "60",
159
- r: String(radius),
160
- fill: "none",
161
- stroke: "var(--token-line-soft)",
162
- "stroke-width": "14"
163
- }));
164
- let offset = 0;
165
- for (const seg of opts.segments) {
166
- const dash = seg.value / total * circumference;
340
+ if (typeof document === "undefined") return el;
341
+ withChrome(el, opts, opts.segments.length > 0, (body) => {
342
+ const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
343
+ if (opts.segments.length === 0 || total <= 0) {
344
+ renderEmpty(body, opts.emptyLabel);
345
+ return;
346
+ }
347
+ const radius = 42;
348
+ const circumference = 2 * Math.PI * radius;
349
+ const wrap = document.createElement("div");
350
+ wrap.className = "chart-donut";
351
+ const svg = svgEl("svg", { viewBox: "0 0 120 120", role: "img" });
352
+ svg.style.transform = "rotate(-90deg)";
167
353
  svg.appendChild(svgEl("circle", {
168
354
  cx: "60",
169
355
  cy: "60",
170
356
  r: String(radius),
171
357
  fill: "none",
172
- stroke: toneVar(seg.tone),
173
- "stroke-width": "14",
174
- "stroke-dasharray": `${dash} ${circumference - dash}`,
175
- "stroke-dashoffset": String(-offset),
176
- "stroke-linecap": "butt"
358
+ stroke: "var(--token-line-soft)",
359
+ "stroke-width": "14"
177
360
  }));
178
- offset += dash;
179
- }
180
- const legend = document.createElement("div");
181
- legend.className = "chart-legend";
182
- for (const seg of opts.segments) {
183
- legend.appendChild(legendItem(seg.label, `${Math.round(seg.value / total * 100)}%`, seg.tone));
184
- }
185
- wrap.append(svg, legend);
186
- el.appendChild(wrap);
361
+ let offset = 0;
362
+ opts.segments.forEach((seg, segIndex) => {
363
+ const dash = seg.value / total * circumference;
364
+ const arc = svgEl("circle", {
365
+ cx: "60",
366
+ cy: "60",
367
+ r: String(radius),
368
+ fill: "none",
369
+ stroke: toneVar(seg.tone),
370
+ "stroke-width": "14",
371
+ "stroke-dasharray": `${dash} ${circumference - dash}`,
372
+ "stroke-dashoffset": String(-offset),
373
+ "stroke-linecap": "butt"
374
+ });
375
+ setMarkData(arc, { index: segIndex, label: seg.label, value: seg.value });
376
+ svg.appendChild(arc);
377
+ offset += dash;
378
+ });
379
+ const legend = document.createElement("div");
380
+ legend.className = "chart-legend";
381
+ for (const seg of opts.segments) {
382
+ legend.appendChild(legendItem(seg.label, `${Math.round(seg.value / total * 100)}%`, seg.tone));
383
+ }
384
+ wrap.append(svg, legend);
385
+ body.appendChild(wrap);
386
+ });
387
+ attachChartInteraction(el, "donut");
388
+ return el;
187
389
  }
188
390
  var lineGradientSeq = 0;
189
391
  function renderLine(el, opts) {
190
- if (typeof document === "undefined") return;
191
- el.textContent = "";
192
- const n = Math.min(opts.labels.length, opts.values.length);
193
- if (n === 0) {
194
- renderEmpty(el, opts.emptyLabel);
195
- return;
196
- }
197
- const labels = opts.labels.slice(0, n);
198
- const values = opts.values.slice(0, n);
199
- const width = 360;
200
- const height = 150;
201
- const pad = 14;
202
- const max = Math.max(...values, 1);
203
- const tone = toneVar(opts.tone);
204
- const points = values.map((value, i) => ({
205
- x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
206
- y: height - pad - value / max * (height - pad * 2),
207
- value,
208
- label: labels[i] ?? ""
209
- }));
210
- const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
211
- const last = points[points.length - 1];
212
- const first = points[0];
213
- const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
214
- const gradientId = `icen-chart-line-${++lineGradientSeq}`;
215
- const wrap = document.createElement("div");
216
- wrap.className = "chart-line";
217
- const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
218
- const defs = svgEl("defs", {});
219
- const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
220
- gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.32" }));
221
- gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.02" }));
222
- defs.appendChild(gradient);
223
- svg.appendChild(defs);
224
- for (let i = 0; i < 4; i++) {
225
- const y = pad + i * (height - pad * 2) / 3;
226
- svg.appendChild(svgEl("line", {
227
- x1: String(pad),
228
- x2: String(width - pad),
229
- y1: String(y),
230
- y2: String(y),
231
- stroke: "var(--token-line-soft)",
232
- "stroke-width": "1"
233
- }));
234
- }
235
- svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
236
- svg.appendChild(svgEl("path", {
237
- d: line,
238
- fill: "none",
239
- stroke: tone,
240
- "stroke-width": "2.5",
241
- "stroke-linecap": "round",
242
- "stroke-linejoin": "round"
243
- }));
244
- for (const p of points) {
245
- const dot = svgEl("circle", {
246
- cx: p.x.toFixed(1),
247
- cy: p.y.toFixed(1),
248
- r: "3",
249
- fill: "var(--token-card)",
250
- stroke: tone,
251
- "stroke-width": "2"
392
+ if (typeof document === "undefined") return el;
393
+ const seriesList = normalizeSeriesInput(opts);
394
+ const labels = opts.labels ?? [];
395
+ const n = Math.min(labels.length, ...seriesList.map((s) => s.values.length));
396
+ withChrome(el, opts, seriesList.length > 1, (body) => {
397
+ if (n === 0 || seriesList.length === 0) {
398
+ renderEmpty(body, opts.emptyLabel);
399
+ return;
400
+ }
401
+ const labelsUsed = labels.slice(0, n);
402
+ const width = 360;
403
+ const height = 150;
404
+ const pad = 14;
405
+ let max = 1;
406
+ for (const s of seriesList) max = Math.max(max, maxOf(s.values.slice(0, n)));
407
+ const wrap = document.createElement("div");
408
+ wrap.className = "chart-line";
409
+ const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
410
+ const defs = svgEl("defs", {});
411
+ for (let i = 0; i < 4; i++) {
412
+ const y = pad + i * (height - pad * 2) / 3;
413
+ svg.appendChild(svgEl("line", {
414
+ x1: String(pad),
415
+ x2: String(width - pad),
416
+ y1: String(y),
417
+ y2: String(y),
418
+ stroke: "var(--token-line-soft)",
419
+ "stroke-width": "1"
420
+ }));
421
+ }
422
+ seriesList.forEach((s, si) => {
423
+ const tone = toneOf(s.tone, si) ?? toneVar(opts.tone);
424
+ const values = s.values.slice(0, n);
425
+ const points = values.map((value, i) => ({
426
+ i,
427
+ x: n <= 1 ? width / 2 : pad + i / (n - 1) * (width - pad * 2),
428
+ y: height - pad - Math.max(0, value) / max * (height - pad * 2),
429
+ value,
430
+ label: labelsUsed[i] ?? ""
431
+ }));
432
+ const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
433
+ const last = points[points.length - 1];
434
+ const first = points[0];
435
+ const fillArea = seriesList.length === 1 || opts.fill;
436
+ if (fillArea) {
437
+ const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
438
+ const gradientId = `icen-chart-line-${++lineGradientSeq}`;
439
+ const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
440
+ const strong = seriesList.length > 1 ? "0.20" : "0.32";
441
+ gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": strong }));
442
+ gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.02" }));
443
+ defs.appendChild(gradient);
444
+ const areaPath = svgEl("path", { d: area, fill: `url(#${gradientId})` });
445
+ if (seriesList.length > 1) {
446
+ setMarkData(areaPath, { seriesIndex: si, seriesName: s.name, label: s.name, value: values.reduce((a, b) => a + b, 0) });
447
+ }
448
+ svg.appendChild(areaPath);
449
+ }
450
+ const linePath = svgEl("path", {
451
+ d: line,
452
+ fill: "none",
453
+ stroke: tone,
454
+ "stroke-width": seriesList.length > 1 ? "2" : "2.5",
455
+ "stroke-linecap": "round",
456
+ "stroke-linejoin": "round"
457
+ });
458
+ const lineTitle = svgEl("title", {});
459
+ lineTitle.textContent = s.name || "\u6570\u503C";
460
+ linePath.appendChild(lineTitle);
461
+ setMarkData(linePath, { seriesIndex: si, seriesName: s.name, label: s.name, value: values[values.length - 1] ?? 0 });
462
+ svg.appendChild(linePath);
463
+ for (const p of points) {
464
+ const dot = svgEl("circle", {
465
+ cx: p.x.toFixed(1),
466
+ cy: p.y.toFixed(1),
467
+ r: "3",
468
+ fill: "var(--token-card)",
469
+ stroke: tone,
470
+ "stroke-width": "2"
471
+ });
472
+ const title = svgEl("title", {});
473
+ title.textContent = seriesList.length > 1 ? `${p.label} \xB7 ${s.name}: ${fmt(opts, p.value)}` : `${p.label}: ${fmt(opts, p.value)}`;
474
+ dot.appendChild(title);
475
+ setMarkData(dot, {
476
+ index: p.i,
477
+ seriesIndex: seriesList.length > 1 ? si : void 0,
478
+ seriesName: seriesList.length > 1 ? s.name : void 0,
479
+ label: p.label,
480
+ value: p.value
481
+ });
482
+ svg.appendChild(dot);
483
+ const hit = svgEl("circle", {
484
+ cx: p.x.toFixed(1),
485
+ cy: p.y.toFixed(1),
486
+ r: "8",
487
+ fill: "transparent",
488
+ "data-chart-hit": "1"
489
+ });
490
+ copyMarkData(hit, dot);
491
+ svg.appendChild(hit);
492
+ }
252
493
  });
253
- const title = svgEl("title", {});
254
- title.textContent = `${p.label}: ${fmt(opts, p.value)}`;
255
- dot.appendChild(title);
256
- svg.appendChild(dot);
257
- }
258
- wrap.append(svg, labelsRow(labels.slice(-7)));
259
- el.appendChild(wrap);
494
+ if (defs.firstChild) svg.insertBefore(defs, svg.firstChild);
495
+ wrap.append(svg, labelsRow(sampleLabels(labelsUsed)));
496
+ body.appendChild(wrap);
497
+ if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name, label: s.name, tone: s.tone, si })), wrap));
498
+ });
499
+ attachChartInteraction(el, "line");
500
+ return el;
260
501
  }
261
502
  function renderArea(el, opts) {
262
- if (typeof document === "undefined") return;
263
- el.textContent = "";
264
- const n = Math.min(opts.labels.length, opts.values.length);
265
- if (n === 0) {
266
- renderEmpty(el, opts.emptyLabel);
267
- return;
268
- }
269
- const labels = opts.labels.slice(0, n);
270
- const values = opts.values.slice(0, n);
271
- const width = 360;
272
- const height = 150;
273
- const pad = 14;
274
- const max = Math.max(...values, 1);
275
- const tone = toneVar(opts.tone);
276
- const points = values.map((value, i) => ({
277
- x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
278
- y: height - pad - value / max * (height - pad * 2),
279
- value,
280
- label: labels[i] ?? ""
281
- }));
282
- const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
283
- const last = points[points.length - 1];
284
- const first = points[0];
285
- const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
286
- const gradientId = `icen-chart-area-${++lineGradientSeq}`;
287
- const wrap = document.createElement("div");
288
- wrap.className = "chart-area";
289
- const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
290
- const defs = svgEl("defs", {});
291
- const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
292
- gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.5" }));
293
- gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.05" }));
294
- defs.appendChild(gradient);
295
- svg.appendChild(defs);
296
- for (let i = 0; i < 4; i++) {
297
- const y = pad + i * (height - pad * 2) / 3;
298
- svg.appendChild(svgEl("line", {
299
- x1: String(pad),
300
- x2: String(width - pad),
301
- y1: String(y),
302
- y2: String(y),
303
- stroke: "var(--token-line-soft)",
304
- "stroke-width": "1"
503
+ if (typeof document === "undefined") return el;
504
+ withChrome(el, opts, false, (body) => {
505
+ const n = Math.min(opts.labels.length, opts.values.length);
506
+ if (n === 0) {
507
+ renderEmpty(body, opts.emptyLabel);
508
+ return;
509
+ }
510
+ const labels = opts.labels.slice(0, n);
511
+ const values = opts.values.slice(0, n);
512
+ const width = 360;
513
+ const height = 150;
514
+ const pad = 14;
515
+ const max = maxOf(values);
516
+ const tone = toneVar(opts.tone);
517
+ const points = values.map((value, i) => ({
518
+ x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
519
+ y: height - pad - value / max * (height - pad * 2),
520
+ value,
521
+ label: labels[i] ?? ""
305
522
  }));
306
- }
307
- svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
308
- svg.appendChild(svgEl("path", {
309
- d: line,
310
- fill: "none",
311
- stroke: tone,
312
- "stroke-width": "1.5",
313
- "stroke-linecap": "round",
314
- "stroke-linejoin": "round"
315
- }));
316
- for (const p of points) {
317
- const title = svgEl("title", {});
318
- title.textContent = `${p.label}: ${fmt(opts, p.value)}`;
319
- const inv = svgEl("circle", {
320
- cx: p.x.toFixed(1),
321
- cy: p.y.toFixed(1),
322
- r: "2",
323
- fill: "transparent"
324
- });
325
- inv.appendChild(title);
326
- svg.appendChild(inv);
327
- }
328
- wrap.append(svg, labelsRow(labels.slice(-7)));
329
- el.appendChild(wrap);
330
- }
331
- function renderRadar(el, opts) {
332
- if (typeof document === "undefined") return;
333
- el.textContent = "";
334
- const axes = opts.axes;
335
- const n = axes.length;
336
- if (n < 3 || opts.series.length === 0) {
337
- renderEmpty(el, opts.emptyLabel);
338
- return;
339
- }
340
- let maxVal = opts.max ?? 0;
341
- if (opts.max == null) {
342
- for (const s of opts.series) {
343
- for (let i = 0; i < n; i++) maxVal = Math.max(maxVal, s.values[i] ?? 0);
344
- }
345
- maxVal = niceCeil(maxVal);
346
- }
347
- if (maxVal <= 0) maxVal = 1;
348
- const size = 200;
349
- const cx = size / 2;
350
- const cy = size / 2;
351
- const radius = 78;
352
- const levels = Math.max(2, opts.levels ?? 4);
353
- const angleOf = (i) => -Math.PI / 2 + i * 2 * Math.PI / n;
354
- const pointAt = (i, r) => ({
355
- x: cx + r * Math.cos(angleOf(i)),
356
- y: cy + r * Math.sin(angleOf(i))
357
- });
358
- const wrap = document.createElement("div");
359
- wrap.className = "chart-radar";
360
- const svg = svgEl("svg", { viewBox: `0 0 ${size} ${size}`, role: "img" });
361
- for (let lv = 1; lv <= levels; lv++) {
362
- const r = radius * lv / levels;
363
- const pts = Array.from({ length: n }, (_, i) => {
364
- const p = pointAt(i, r);
365
- return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
366
- }).join(" ");
367
- svg.appendChild(svgEl("polygon", {
368
- points: pts,
523
+ const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
524
+ const last = points[points.length - 1];
525
+ const first = points[0];
526
+ const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
527
+ const gradientId = `icen-chart-area-${++lineGradientSeq}`;
528
+ const wrap = document.createElement("div");
529
+ wrap.className = "chart-area";
530
+ const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
531
+ const defs = svgEl("defs", {});
532
+ const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
533
+ gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.5" }));
534
+ gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.05" }));
535
+ defs.appendChild(gradient);
536
+ svg.appendChild(defs);
537
+ for (let i = 0; i < 4; i++) {
538
+ const y = pad + i * (height - pad * 2) / 3;
539
+ svg.appendChild(svgEl("line", {
540
+ x1: String(pad),
541
+ x2: String(width - pad),
542
+ y1: String(y),
543
+ y2: String(y),
544
+ stroke: "var(--token-line-soft)",
545
+ "stroke-width": "1"
546
+ }));
547
+ }
548
+ svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
549
+ svg.appendChild(svgEl("path", {
550
+ d: line,
369
551
  fill: "none",
370
- stroke: "var(--token-line-soft)",
371
- "stroke-width": "1",
372
- "stroke-dasharray": lv === levels ? "0" : "3 2"
373
- }));
374
- }
375
- for (let i = 0; i < n; i++) {
376
- const edge = pointAt(i, radius);
377
- svg.appendChild(svgEl("line", {
378
- x1: String(cx),
379
- y1: String(cy),
380
- x2: edge.x.toFixed(1),
381
- y2: edge.y.toFixed(1),
382
- stroke: "var(--token-line-soft)",
383
- "stroke-width": "1"
384
- }));
385
- const labelP = pointAt(i, radius + 14);
386
- const txt = svgEl("text", {
387
- x: labelP.x.toFixed(1),
388
- y: labelP.y.toFixed(1),
389
- "text-anchor": "middle",
390
- "dominant-baseline": "middle",
391
- "font-size": "9",
392
- fill: "var(--token-text-muted)"
393
- });
394
- txt.textContent = axes[i] ?? "";
395
- svg.appendChild(txt);
396
- }
397
- const legend = document.createElement("div");
398
- legend.className = "chart-legend";
399
- for (const s of opts.series) {
400
- const tone = toneVar(s.tone);
401
- const pts = Array.from({ length: n }, (_, i) => {
402
- const v = Math.max(0, Math.min(maxVal, s.values[i] ?? 0));
403
- const p = pointAt(i, v / maxVal * radius);
404
- return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
405
- }).join(" ");
406
- const polyFill = svgEl("polygon", {
407
- points: pts,
408
- fill: tone,
409
- "fill-opacity": "0.16",
410
552
  stroke: tone,
411
- "stroke-width": "2",
553
+ "stroke-width": "1.5",
554
+ "stroke-linecap": "round",
412
555
  "stroke-linejoin": "round"
413
- });
414
- const title = svgEl("title", {});
415
- title.textContent = `${s.name}: ${s.values.map((v) => fmt(opts, v)).join(" / ")}`;
416
- polyFill.appendChild(title);
417
- svg.appendChild(polyFill);
418
- for (let i = 0; i < n; i++) {
419
- const v = Math.max(0, Math.min(maxVal, s.values[i] ?? 0));
420
- const p = pointAt(i, v / maxVal * radius);
421
- svg.appendChild(svgEl("circle", {
556
+ }));
557
+ for (const p of points) {
558
+ const title = svgEl("title", {});
559
+ title.textContent = `${p.label}: ${fmt(opts, p.value)}`;
560
+ const inv = svgEl("circle", {
422
561
  cx: p.x.toFixed(1),
423
562
  cy: p.y.toFixed(1),
424
- r: "2.5",
425
- fill: "var(--token-card)",
426
- stroke: tone,
427
- "stroke-width": "1.5"
563
+ r: "2",
564
+ fill: "transparent"
565
+ });
566
+ inv.appendChild(title);
567
+ svg.appendChild(inv);
568
+ }
569
+ wrap.append(svg, labelsRow(sampleLabels(labels)));
570
+ body.appendChild(wrap);
571
+ });
572
+ attachChartInteraction(el, "area");
573
+ return el;
574
+ }
575
+ function renderRadar(el, opts) {
576
+ if (typeof document === "undefined") return el;
577
+ withChrome(el, opts, opts.series.length > 1, (body) => {
578
+ const axes = opts.axes;
579
+ const n = axes.length;
580
+ if (n < 3 || opts.series.length === 0) {
581
+ renderEmpty(body, opts.emptyLabel);
582
+ return;
583
+ }
584
+ let maxVal = opts.max ?? 0;
585
+ if (opts.max == null) {
586
+ for (const s of opts.series) {
587
+ for (let i = 0; i < n; i++) maxVal = Math.max(maxVal, s.values[i] ?? 0);
588
+ }
589
+ maxVal = niceCeil(maxVal);
590
+ }
591
+ if (maxVal <= 0) maxVal = 1;
592
+ const size = 200;
593
+ const cx = size / 2;
594
+ const cy = size / 2;
595
+ const radius = 78;
596
+ const levels = Math.max(2, opts.levels ?? 4);
597
+ const angleOf = (i) => -Math.PI / 2 + i * 2 * Math.PI / n;
598
+ const pointAt = (i, r) => ({
599
+ x: cx + r * Math.cos(angleOf(i)),
600
+ y: cy + r * Math.sin(angleOf(i))
601
+ });
602
+ const wrap = document.createElement("div");
603
+ wrap.className = "chart-radar";
604
+ const svg = svgEl("svg", { viewBox: `0 0 ${size} ${size}`, role: "img" });
605
+ for (let lv = 1; lv <= levels; lv++) {
606
+ const r = radius * lv / levels;
607
+ const pts = Array.from({ length: n }, (_, i) => {
608
+ const p = pointAt(i, r);
609
+ return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
610
+ }).join(" ");
611
+ svg.appendChild(svgEl("polygon", {
612
+ points: pts,
613
+ fill: "none",
614
+ stroke: "var(--token-line-soft)",
615
+ "stroke-width": "1",
616
+ "stroke-dasharray": lv === levels ? "0" : "3 2"
428
617
  }));
429
618
  }
430
- legend.appendChild(legendItem(s.name, "", s.tone));
431
- }
432
- wrap.append(svg, legend);
433
- el.appendChild(wrap);
619
+ for (let i = 0; i < n; i++) {
620
+ const edge = pointAt(i, radius);
621
+ svg.appendChild(svgEl("line", {
622
+ x1: String(cx),
623
+ y1: String(cy),
624
+ x2: edge.x.toFixed(1),
625
+ y2: edge.y.toFixed(1),
626
+ stroke: "var(--token-line-soft)",
627
+ "stroke-width": "1"
628
+ }));
629
+ const labelP = pointAt(i, radius + 14);
630
+ const txt = svgEl("text", {
631
+ x: labelP.x.toFixed(1),
632
+ y: labelP.y.toFixed(1),
633
+ "text-anchor": "middle",
634
+ "dominant-baseline": "middle",
635
+ "font-size": "9",
636
+ fill: "var(--token-text-muted)"
637
+ });
638
+ txt.textContent = axes[i] ?? "";
639
+ svg.appendChild(txt);
640
+ }
641
+ const legendItems = [];
642
+ opts.series.forEach((s, si) => {
643
+ const tone = toneVar(s.tone);
644
+ const pts = Array.from({ length: n }, (_, i) => {
645
+ const v = Math.max(0, Math.min(maxVal, s.values[i] ?? 0));
646
+ const p = pointAt(i, v / maxVal * radius);
647
+ return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
648
+ }).join(" ");
649
+ const polyFill = svgEl("polygon", {
650
+ points: pts,
651
+ fill: tone,
652
+ "fill-opacity": "0.16",
653
+ stroke: tone,
654
+ "stroke-width": "2",
655
+ "stroke-linejoin": "round"
656
+ });
657
+ const title = svgEl("title", {});
658
+ title.textContent = `${s.name}: ${s.values.map((v) => fmt(opts, v)).join(" / ")}`;
659
+ polyFill.appendChild(title);
660
+ setMarkData(polyFill, { seriesIndex: si, seriesName: s.name || void 0, label: s.name, value: s.values.reduce((a, b) => a + (b || 0), 0) });
661
+ svg.appendChild(polyFill);
662
+ for (let i = 0; i < n; i++) {
663
+ const v = Math.max(0, Math.min(maxVal, s.values[i] ?? 0));
664
+ const p = pointAt(i, v / maxVal * radius);
665
+ const dot = svgEl("circle", {
666
+ cx: p.x.toFixed(1),
667
+ cy: p.y.toFixed(1),
668
+ r: "2.5",
669
+ fill: "var(--token-card)",
670
+ stroke: tone,
671
+ "stroke-width": "1.5"
672
+ });
673
+ setMarkData(dot, { index: i, seriesIndex: si, seriesName: s.name || void 0, label: axes[i] ?? "", value: s.values[i] ?? 0 });
674
+ svg.appendChild(dot);
675
+ }
676
+ legendItems.push({ key: s.name || `\u7CFB\u5217${si + 1}`, label: s.name || `\u7CFB\u5217${si + 1}`, tone: s.tone, si });
677
+ });
678
+ const legend = opts.series.length > 1 ? toggleLegend(legendItems, wrap) : null;
679
+ wrap.append(svg, ...legend ? [legend] : []);
680
+ body.appendChild(wrap);
681
+ });
682
+ attachChartInteraction(el, "radar");
683
+ return el;
434
684
  }
435
685
  function niceCeil(v) {
436
686
  if (v <= 0) return 1;
@@ -449,53 +699,29 @@ function isoDate(d) {
449
699
  const day = String(d.getDate()).padStart(2, "0");
450
700
  return `${d.getFullYear()}-${m}-${day}`;
451
701
  }
452
- function renderHeatmap(el, opts) {
453
- if (typeof document === "undefined") return;
454
- el.textContent = "";
455
- let entries;
456
- if (opts.data && opts.data.length > 0) {
457
- entries = opts.data;
458
- } else if (opts.values && opts.values.length > 0) {
459
- const today = /* @__PURE__ */ new Date();
460
- entries = opts.values.map((value, i) => {
461
- const d = new Date(today);
462
- d.setDate(d.getDate() - (opts.values.length - 1 - i));
463
- return { date: isoDate(d), value };
464
- });
465
- } else {
466
- renderEmpty(el, opts.emptyLabel);
467
- return;
468
- }
469
- const weeks = opts.weeks ?? Math.ceil(entries.length / 7);
470
- if (entries.length > weeks * 7) entries = entries.slice(entries.length - weeks * 7);
471
- const max = Math.max(...entries.map((e) => e.value), 1);
472
- const levelOf = (v) => v <= 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
473
- const wrap = document.createElement("div");
474
- wrap.className = "chart-heatmap";
475
- if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
476
- const grid = document.createElement("div");
477
- grid.className = "chart-heatmap-grid";
478
- grid.setAttribute("role", "img");
479
- const first = /* @__PURE__ */ new Date(`${entries[0].date}T00:00:00`);
480
- const pad = (first.getDay() + 6) % 7;
702
+ function weekdayLabelAt(defaults, day) {
703
+ return defaults[(day + 6) % 7] ?? "";
704
+ }
705
+ function levelOf(v, max) {
706
+ return v <= 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
707
+ }
708
+ function padFirstWeek(grid, firstDate, weekStart) {
709
+ const first = /* @__PURE__ */ new Date(`${firstDate}T00:00:00`);
710
+ const pad = (first.getDay() + 7 - weekStart) % 7;
481
711
  for (let i = 0; i < pad; i++) {
482
712
  const blank = document.createElement("span");
483
713
  blank.className = "chart-heatmap-cell is-blank";
484
714
  grid.appendChild(blank);
485
715
  }
486
- for (const entry of entries) {
487
- const cell = document.createElement("span");
488
- cell.className = "chart-heatmap-cell";
489
- cell.dataset.level = String(levelOf(entry.value));
490
- cell.setAttribute("title", `${entry.date}\uFF1A${fmt(opts, entry.value)}`);
491
- grid.appendChild(cell);
492
- }
716
+ return pad;
717
+ }
718
+ function heatmapLegend(labels) {
493
719
  const legend = document.createElement("div");
494
720
  legend.className = "chart-heatmap-legend";
495
721
  const less = document.createElement("span");
496
- less.textContent = "\u5C11";
722
+ less.textContent = labels?.less ?? "\u5C11";
497
723
  const more = document.createElement("span");
498
- more.textContent = "\u591A";
724
+ more.textContent = labels?.more ?? "\u591A";
499
725
  legend.appendChild(less);
500
726
  for (let lv = 0; lv <= 4; lv++) {
501
727
  const cell = document.createElement("span");
@@ -504,67 +730,110 @@ function renderHeatmap(el, opts) {
504
730
  legend.appendChild(cell);
505
731
  }
506
732
  legend.appendChild(more);
507
- wrap.append(grid, legend);
508
- el.appendChild(wrap);
733
+ return legend;
734
+ }
735
+ function renderHeatmap(el, opts) {
736
+ if (typeof document === "undefined") return el;
737
+ withChrome(el, opts, true, (body) => {
738
+ let entries;
739
+ if (opts.data && opts.data.length > 0) {
740
+ entries = opts.data;
741
+ } else if (opts.values && opts.values.length > 0) {
742
+ const today = /* @__PURE__ */ new Date();
743
+ entries = opts.values.map((value, i) => {
744
+ const d = new Date(today);
745
+ d.setDate(d.getDate() - (opts.values.length - 1 - i));
746
+ return { date: isoDate(d), value };
747
+ });
748
+ } else {
749
+ renderEmpty(body, opts.emptyLabel);
750
+ return;
751
+ }
752
+ const weeks = opts.weeks ?? Math.ceil(entries.length / 7);
753
+ if (entries.length > weeks * 7) entries = entries.slice(entries.length - weeks * 7);
754
+ const max = maxOf(entries.map((e) => e.value));
755
+ const wrap = document.createElement("div");
756
+ wrap.className = "chart-heatmap";
757
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
758
+ const grid = document.createElement("div");
759
+ grid.className = "chart-heatmap-grid";
760
+ grid.setAttribute("role", "img");
761
+ padFirstWeek(grid, entries[0].date, opts.weekStart === 0 ? 0 : 1);
762
+ for (const [i, entry] of entries.entries()) {
763
+ const cell = document.createElement("span");
764
+ cell.className = "chart-heatmap-cell";
765
+ cell.dataset.level = String(levelOf(entry.value, max));
766
+ cell.setAttribute("title", `${entry.date}\uFF1A${fmt(opts, entry.value)}`);
767
+ setMarkData(cell, { index: i, label: entry.date, value: entry.value });
768
+ grid.appendChild(cell);
769
+ }
770
+ wrap.append(grid, heatmapLegend(opts.labels));
771
+ body.appendChild(wrap);
772
+ });
773
+ attachChartInteraction(el, "heatmap");
774
+ return el;
509
775
  }
510
776
  function renderSparkline(el, opts) {
511
- if (typeof document === "undefined") return;
512
- el.textContent = "";
513
- const values = opts.values;
514
- if (values.length === 0) {
515
- renderEmpty(el, opts.emptyLabel);
516
- return;
517
- }
518
- const width = 96;
519
- const height = 28;
520
- const pad = 3;
521
- const max = Math.max(...values, 1);
522
- const tone = toneVar(opts.tone);
523
- const points = values.map((value, i) => ({
524
- x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
525
- y: height - pad - value / max * (height - pad * 2),
526
- value
527
- }));
528
- const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
529
- const last = points[points.length - 1];
530
- const first = points[0];
531
- const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
532
- const gradientId = `icen-chart-line-${++lineGradientSeq}`;
533
- const wrap = document.createElement("div");
534
- wrap.className = "chart-sparkline";
535
- const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
536
- const defs = svgEl("defs", {});
537
- const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
538
- gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.28" }));
539
- gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.02" }));
540
- defs.appendChild(gradient);
541
- svg.appendChild(defs);
542
- svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
543
- svg.appendChild(svgEl("path", {
544
- d: line,
545
- fill: "none",
546
- stroke: tone,
547
- "stroke-width": "1.8",
548
- "stroke-linecap": "round",
549
- "stroke-linejoin": "round"
550
- }));
551
- const dot = svgEl("circle", {
552
- cx: last.x.toFixed(1),
553
- cy: last.y.toFixed(1),
554
- r: "2.4",
555
- fill: "var(--token-card)",
556
- stroke: tone,
557
- "stroke-width": "1.6"
777
+ if (typeof document === "undefined") return el;
778
+ withChrome(el, opts, false, (body) => {
779
+ const values = opts.values;
780
+ if (values.length === 0) {
781
+ renderEmpty(body, opts.emptyLabel);
782
+ return;
783
+ }
784
+ const width = 96;
785
+ const height = 28;
786
+ const pad = 3;
787
+ const max = maxOf(values);
788
+ const tone = toneVar(opts.tone);
789
+ const points = values.map((value, i) => ({
790
+ x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
791
+ y: height - pad - value / max * (height - pad * 2),
792
+ value
793
+ }));
794
+ const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
795
+ const last = points[points.length - 1];
796
+ const first = points[0];
797
+ const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
798
+ const gradientId = `icen-chart-line-${++lineGradientSeq}`;
799
+ const wrap = document.createElement("div");
800
+ wrap.className = "chart-sparkline";
801
+ const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
802
+ const defs = svgEl("defs", {});
803
+ const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
804
+ gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.28" }));
805
+ gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.02" }));
806
+ defs.appendChild(gradient);
807
+ svg.appendChild(defs);
808
+ svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
809
+ svg.appendChild(svgEl("path", {
810
+ d: line,
811
+ fill: "none",
812
+ stroke: tone,
813
+ "stroke-width": "1.8",
814
+ "stroke-linecap": "round",
815
+ "stroke-linejoin": "round"
816
+ }));
817
+ const dot = svgEl("circle", {
818
+ cx: last.x.toFixed(1),
819
+ cy: last.y.toFixed(1),
820
+ r: "2.4",
821
+ fill: "var(--token-card)",
822
+ stroke: tone,
823
+ "stroke-width": "1.6"
824
+ });
825
+ const title = svgEl("title", {});
826
+ title.textContent = fmt(opts, last.value);
827
+ dot.appendChild(title);
828
+ svg.appendChild(dot);
829
+ wrap.appendChild(svg);
830
+ body.appendChild(wrap);
558
831
  });
559
- const title = svgEl("title", {});
560
- title.textContent = fmt(opts, last.value);
561
- dot.appendChild(title);
562
- svg.appendChild(dot);
563
- wrap.appendChild(svg);
564
- el.appendChild(wrap);
832
+ attachChartInteraction(el, "sparkline");
833
+ return el;
565
834
  }
566
835
  function renderGauge(el, opts) {
567
- if (typeof document === "undefined") return;
836
+ if (typeof document === "undefined") return el;
568
837
  el.textContent = "";
569
838
  const max = opts.max ?? 100;
570
839
  const ratio = max > 0 ? Math.min(1, Math.max(0, opts.value / max)) : 0;
@@ -608,15 +877,670 @@ function renderGauge(el, opts) {
608
877
  }
609
878
  wrap.append(svg, center);
610
879
  el.appendChild(wrap);
880
+ attachChartInteraction(el, "gauge");
881
+ return el;
882
+ }
883
+ function renderCalendar(el, opts) {
884
+ if (typeof document === "undefined") return el;
885
+ withChrome(el, opts, true, (body) => {
886
+ let entries;
887
+ if (opts.data && opts.data.length > 0) {
888
+ entries = opts.data;
889
+ } else if (opts.dates && opts.dates.length > 0 && opts.values) {
890
+ entries = opts.dates.slice(0, opts.values.length).map((date, i) => ({ date, value: opts.values[i] ?? 0 }));
891
+ } else {
892
+ renderEmpty(body, opts.emptyLabel);
893
+ return;
894
+ }
895
+ const wrap = document.createElement("div");
896
+ wrap.className = "chart-calendar";
897
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
898
+ const weekStart = opts.weekStart === 0 ? 0 : 1;
899
+ const weekdayDefaults = ["\u4E00", "", "\u4E09", "", "\u4E94", "", "\u65E5"];
900
+ const weekdayLabels = opts.labels?.weekdays ?? weekdayDefaults.map((_, i) => weekdayLabelAt(weekdayDefaults, (weekStart + i) % 7));
901
+ const monthLabel = (m) => opts.labels?.months?.[m] ?? `${m + 1}\u6708`;
902
+ const grid = document.createElement("div");
903
+ grid.className = "chart-heatmap-grid";
904
+ grid.setAttribute("role", "img");
905
+ const months = document.createElement("div");
906
+ months.className = "chart-calendar-months";
907
+ const weekdayRow = document.createElement("div");
908
+ weekdayRow.className = "chart-calendar-weekdays";
909
+ weekdayLabels.forEach((w) => {
910
+ const s = document.createElement("span");
911
+ s.textContent = w;
912
+ weekdayRow.appendChild(s);
913
+ });
914
+ const max = maxOf(entries.map((e) => e.value));
915
+ const pad = padFirstWeek(grid, entries[0].date, weekStart);
916
+ let lastMonth = -1;
917
+ entries.forEach((entry, i) => {
918
+ const d = /* @__PURE__ */ new Date(`${entry.date}T00:00:00`);
919
+ const col = Math.floor((pad + i) / 7);
920
+ if (d.getMonth() !== lastMonth) {
921
+ lastMonth = d.getMonth();
922
+ const label = document.createElement("span");
923
+ label.className = "chart-calendar-month";
924
+ label.textContent = monthLabel(d.getMonth());
925
+ label.style.setProperty("--col", String(col));
926
+ months.appendChild(label);
927
+ }
928
+ const cell = document.createElement("span");
929
+ cell.className = "chart-heatmap-cell";
930
+ cell.dataset.level = String(levelOf(entry.value, max));
931
+ cell.setAttribute("title", `${entry.date}\uFF1A${fmt(opts, entry.value)}`);
932
+ setMarkData(cell, { index: i, label: entry.date, value: entry.value });
933
+ grid.appendChild(cell);
934
+ });
935
+ const legend = heatmapLegend(opts.labels);
936
+ const columns = Math.max(1, Math.ceil((pad + entries.length) / 7));
937
+ months.style.setProperty("--columns", String(columns));
938
+ wrap.append(months, weekdayRow, grid, legend);
939
+ body.appendChild(wrap);
940
+ });
941
+ attachChartInteraction(el, "calendar");
942
+ return el;
943
+ }
944
+ function renderScatter(el, opts) {
945
+ if (typeof document === "undefined") return el;
946
+ withChrome(el, opts, false, (body) => {
947
+ const pts = opts.points.filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
948
+ if (pts.length === 0) {
949
+ renderEmpty(body, opts.emptyLabel);
950
+ return;
951
+ }
952
+ const width = 360;
953
+ const height = 220;
954
+ const pad = 30;
955
+ let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, sMax = 0;
956
+ for (const p of pts) {
957
+ if (p.x < xMin) xMin = p.x;
958
+ if (p.x > xMax) xMax = p.x;
959
+ if (p.y < yMin) yMin = p.y;
960
+ if (p.y > yMax) yMax = p.y;
961
+ if ((p.size ?? 0) > sMax) sMax = p.size ?? 0;
962
+ }
963
+ if (xMax === xMin) xMax = xMin + 1;
964
+ if (yMax === yMin) yMax = yMin + 1;
965
+ const rOf = (size) => size && sMax > 0 ? 3 + size / sMax * 7 : 4;
966
+ const wrap = document.createElement("div");
967
+ wrap.className = "chart-scatter";
968
+ const tone = toneVar(opts.tone);
969
+ const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
970
+ const xAt = (x) => pad + (x - xMin) / (xMax - xMin) * (width - pad * 2);
971
+ const yAt = (y) => height - pad - (y - yMin) / (yMax - yMin) * (height - pad * 2);
972
+ for (let i = 0; i <= 3; i++) {
973
+ const y = pad + i * (height - pad * 2) / 3;
974
+ svg.appendChild(svgEl("line", {
975
+ x1: String(pad),
976
+ x2: String(width - pad),
977
+ y1: String(y),
978
+ y2: String(y),
979
+ stroke: "var(--token-line-soft)",
980
+ "stroke-width": "1"
981
+ }));
982
+ const tv = yMin + (yMax - yMin) * (3 - i) / 3;
983
+ const txt = svgEl("text", {
984
+ x: String(pad - 4),
985
+ y: String(y + 3),
986
+ "text-anchor": "end",
987
+ "font-size": "8",
988
+ fill: "var(--token-text-faint)"
989
+ });
990
+ txt.textContent = fmt(opts, Math.round(tv * 10) / 10);
991
+ svg.appendChild(txt);
992
+ }
993
+ for (let i = 0; i <= 4; i++) {
994
+ const x = pad + i * (width - pad * 2) / 4;
995
+ svg.appendChild(svgEl("line", {
996
+ x1: String(x),
997
+ x2: String(x),
998
+ y1: String(pad),
999
+ y2: String(height - pad),
1000
+ stroke: "var(--token-line-soft)",
1001
+ "stroke-width": "1",
1002
+ "stroke-dasharray": "2 3"
1003
+ }));
1004
+ const tv = xMin + (xMax - xMin) * i / 4;
1005
+ const txt = svgEl("text", {
1006
+ x: String(x),
1007
+ y: String(height - pad + 12),
1008
+ "text-anchor": "middle",
1009
+ "font-size": "8",
1010
+ fill: "var(--token-text-faint)"
1011
+ });
1012
+ txt.textContent = fmt(opts, Math.round(tv * 10) / 10);
1013
+ svg.appendChild(txt);
1014
+ }
1015
+ if (opts.yLabel) {
1016
+ const t = svgEl("text", { x: "10", y: String(pad - 10), "font-size": "8", fill: "var(--token-text-faint)" });
1017
+ t.textContent = opts.yLabel;
1018
+ svg.appendChild(t);
1019
+ }
1020
+ if (opts.xLabel) {
1021
+ const t = svgEl("text", { x: String(width - pad), y: String(pad - 10), "text-anchor": "end", "font-size": "8", fill: "var(--token-text-faint)" });
1022
+ t.textContent = opts.xLabel;
1023
+ svg.appendChild(t);
1024
+ }
1025
+ pts.forEach((p, i) => {
1026
+ const c = svgEl("circle", {
1027
+ cx: xAt(p.x).toFixed(1),
1028
+ cy: yAt(p.y).toFixed(1),
1029
+ r: rOf(p.size).toFixed(1),
1030
+ fill: tone,
1031
+ "fill-opacity": "0.55",
1032
+ stroke: tone,
1033
+ "stroke-width": "1"
1034
+ });
1035
+ const title = svgEl("title", {});
1036
+ title.textContent = `${p.label ?? `#${i + 1}`}: (${fmt(opts, p.x)}, ${fmt(opts, p.y)}${p.size != null ? `, size ${fmt(opts, p.size)}` : ""})`;
1037
+ c.appendChild(title);
1038
+ setMarkData(c, { index: i, label: p.label ?? `#${i + 1}`, value: p.y });
1039
+ c.dataset.chartX = String(p.x);
1040
+ svg.appendChild(c);
1041
+ });
1042
+ wrap.appendChild(svg);
1043
+ body.appendChild(wrap);
1044
+ });
1045
+ attachChartInteraction(el, "scatter");
1046
+ return el;
1047
+ }
1048
+ function chartFormatValue(format) {
1049
+ if (!format) return (v) => String(v);
1050
+ const notation = format.notation ?? "raw";
1051
+ return (v) => {
1052
+ let text;
1053
+ if (notation === "compact") {
1054
+ const abs = Math.abs(v);
1055
+ text = abs >= 1e6 ? `${Math.round(v / 1e5) / 10}M` : abs >= 1e3 ? `${Math.round(v / 100) / 10}k` : String(v);
1056
+ } else if (notation === "percent") {
1057
+ text = `${Math.round(v * 10) / 10}%`;
1058
+ } else if (notation === "ms") {
1059
+ if (v <= 0) text = "0ms";
1060
+ else if (v < 1e3) text = `${Math.round(v)}ms`;
1061
+ else if (v < 6e4) text = `${Math.round(v / 100) / 10}s`;
1062
+ else text = `${Math.floor(v / 6e4)}m`;
1063
+ } else {
1064
+ text = String(Math.round(v * 100) / 100);
1065
+ }
1066
+ return format.unit ? `${text} ${format.unit}` : text;
1067
+ };
1068
+ }
1069
+ function pivotData(spec) {
1070
+ const data = spec.data;
1071
+ if (!Array.isArray(data) || data.length === 0) return {};
1072
+ const d = spec.dims ?? {};
1073
+ const labelField = d.label ?? "label";
1074
+ const seriesField = d.series ?? "series";
1075
+ const valueField = d.value ?? "value";
1076
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : Number(v) || 0;
1077
+ const str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
1078
+ if (d.x || data.some((r) => typeof r.x === "number")) {
1079
+ const xf = d.x ?? "x";
1080
+ const yf = d.y ?? "y";
1081
+ const sf = d.size ?? "size";
1082
+ const lf = d.label ?? "label";
1083
+ const points = data.filter((r) => r[xf] != null && r[yf] != null).map((r) => ({
1084
+ x: num(r[xf]),
1085
+ y: num(r[yf]),
1086
+ size: r[sf] != null ? num(r[sf]) : void 0,
1087
+ label: r[lf] != null ? str(r[lf]) : void 0
1088
+ }));
1089
+ return { points };
1090
+ }
1091
+ const labels = [];
1092
+ const seenLabel = /* @__PURE__ */ new Set();
1093
+ const hasSeries = data.some((r) => r[seriesField] != null && r[seriesField] !== "");
1094
+ const seriesNames = [];
1095
+ const seenSeries = /* @__PURE__ */ new Set();
1096
+ for (const r of data) {
1097
+ const l = str(r[labelField]);
1098
+ if (!seenLabel.has(l)) {
1099
+ seenLabel.add(l);
1100
+ labels.push(l);
1101
+ }
1102
+ if (hasSeries) {
1103
+ const sn = str(r[seriesField]);
1104
+ if (!seenSeries.has(sn)) {
1105
+ seenSeries.add(sn);
1106
+ seriesNames.push(sn);
1107
+ }
1108
+ }
1109
+ }
1110
+ const labelIndex = new Map(labels.map((l, i) => [l, i]));
1111
+ const seriesIndex = new Map(seriesNames.map((s, i) => [s, i]));
1112
+ const matrix = seriesNames.map(() => labels.map(() => NaN));
1113
+ for (const r of data) {
1114
+ const li = labelIndex.get(str(r[labelField])) ?? 0;
1115
+ const si = hasSeries ? seriesIndex.get(str(r[seriesField])) ?? 0 : 0;
1116
+ (matrix[si] ?? [])[li] = num(r[valueField]);
1117
+ }
1118
+ for (const row of matrix) for (let i = 0; i < row.length; i++) if (Number.isNaN(row[i])) row[i] = 0;
1119
+ if (hasSeries) {
1120
+ return {
1121
+ labels,
1122
+ series: seriesNames.map((name, si) => ({ name, values: matrix[si] ?? [] }))
1123
+ };
1124
+ }
1125
+ return { labels, values: matrix[0] ?? [] };
1126
+ }
1127
+ var CHART_TYPE_ALIASES = {
1128
+ bar: "vbar",
1129
+ column: "vbar",
1130
+ "vertical-bar": "vbar",
1131
+ "bar-vertical": "vbar",
1132
+ "horizontal-bar": "hbar",
1133
+ "bar-horizontal": "hbar",
1134
+ ranking: "hbar",
1135
+ pie: "donut",
1136
+ ring: "donut",
1137
+ "pie-chart": "donut",
1138
+ "stacked-bar": "stack",
1139
+ "stacked-bar-100": "stack",
1140
+ ratio: "stack",
1141
+ time: "line",
1142
+ timeline: "line",
1143
+ trend: "line",
1144
+ "trend-line": "line",
1145
+ contribution: "calendar",
1146
+ "contribution-graph": "calendar",
1147
+ "calendar-heatmap": "calendar",
1148
+ heat: "heatmap",
1149
+ matrix: "heatmap",
1150
+ bubble: "scatter",
1151
+ "scatter-plot": "scatter",
1152
+ dial: "gauge",
1153
+ progress: "gauge",
1154
+ mini: "sparkline",
1155
+ "trend-sparkline": "sparkline",
1156
+ spider: "radar",
1157
+ "radar-chart": "radar"
1158
+ };
1159
+ function toNum(v) {
1160
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1161
+ if (typeof v === "string" && v.trim() !== "") {
1162
+ const n = Number(v.replace(/[,\s%]/g, ""));
1163
+ if (Number.isFinite(n)) return n;
1164
+ }
1165
+ return void 0;
1166
+ }
1167
+ function normalizeChartSpec(raw) {
1168
+ let src = raw;
1169
+ if (typeof src === "string") {
1170
+ try {
1171
+ src = JSON.parse(src);
1172
+ } catch {
1173
+ return {};
1174
+ }
1175
+ }
1176
+ if (!src || typeof src !== "object" || Array.isArray(src)) {
1177
+ if (Array.isArray(src)) src = { data: src };
1178
+ else return {};
1179
+ }
1180
+ const s = src;
1181
+ const spec = {};
1182
+ if (typeof s.type === "string") {
1183
+ const t = s.type.trim().toLowerCase();
1184
+ spec.type = CHART_TYPE_ALIASES[t] ?? t;
1185
+ }
1186
+ for (const k of ["title", "tone", "emptyLabel"]) {
1187
+ if (typeof s[k] === "string") spec[k] = s[k];
1188
+ }
1189
+ for (const k of ["stacked", "fill", "tooltip", "legend"]) {
1190
+ if (typeof s[k] === "boolean") spec[k] = s[k];
1191
+ }
1192
+ for (const k of ["weeks", "max", "levels"]) {
1193
+ const n = toNum(s[k]);
1194
+ if (n != null) spec[k] = n;
1195
+ }
1196
+ if (Array.isArray(s.labels)) spec.labels = s.labels.map((v) => String(v ?? ""));
1197
+ if (Array.isArray(s.values)) {
1198
+ const values = s.values.map(toNum).filter((v) => v != null);
1199
+ if (values.length > 0) spec.values = values;
1200
+ }
1201
+ if (Array.isArray(s.dates)) spec.dates = s.dates.map((v) => String(v ?? ""));
1202
+ if (Array.isArray(s.axes)) spec.axes = s.axes.map((v) => String(v ?? ""));
1203
+ if (Array.isArray(s.series)) {
1204
+ const series = s.series.filter((v) => !!v && typeof v === "object").map((v, i) => {
1205
+ const values = Array.isArray(v.values) ? v.values.map(toNum).filter((n) => n != null) : [];
1206
+ const sv = { name: String(v.name ?? `\u7CFB\u5217${i + 1}`), values };
1207
+ const tone = v.tone;
1208
+ if (typeof tone === "string" || typeof tone === "number") sv.tone = tone;
1209
+ return sv;
1210
+ }).filter((v) => v.values.length > 0);
1211
+ if (series.length > 0) spec.series = series;
1212
+ }
1213
+ if (Array.isArray(s.segments)) {
1214
+ const segments = s.segments.filter((v) => !!v && typeof v === "object").map((v) => {
1215
+ const seg = { label: String(v.label ?? v.name ?? ""), value: toNum(v.value) ?? 0 };
1216
+ if (typeof v.tone === "string") seg.tone = v.tone;
1217
+ return seg;
1218
+ }).filter((v) => v.label !== "" || v.value !== 0);
1219
+ if (segments.length > 0) spec.segments = segments;
1220
+ }
1221
+ if (Array.isArray(s.points)) {
1222
+ const points = s.points.filter((v) => !!v && typeof v === "object").map((v) => ({
1223
+ x: toNum(v.x) ?? 0,
1224
+ y: toNum(v.y) ?? 0,
1225
+ size: toNum(v.size),
1226
+ label: typeof v.label === "string" ? v.label : void 0
1227
+ }));
1228
+ if (points.length > 0) spec.points = points;
1229
+ }
1230
+ if (Array.isArray(s.data)) spec.data = s.data.filter((v) => !!v && typeof v === "object");
1231
+ if (s.dims && typeof s.dims === "object" && !Array.isArray(s.dims)) spec.dims = s.dims;
1232
+ if (s.format && typeof s.format === "object") {
1233
+ const f = s.format;
1234
+ const format = {};
1235
+ if (typeof f.unit === "string") format.unit = f.unit;
1236
+ if (typeof f.notation === "string" && ["compact", "percent", "ms", "raw"].includes(f.notation)) {
1237
+ format.notation = f.notation;
1238
+ }
1239
+ spec.format = format;
1240
+ }
1241
+ const isDateRecords = spec.data?.length && spec.data[0] && "date" in spec.data[0];
1242
+ if (spec.data && spec.data.length > 0 && !isDateRecords && !spec.series && !spec.points && !spec.values && !spec.segments) {
1243
+ Object.assign(spec, pivotData(spec));
1244
+ }
1245
+ return spec;
1246
+ }
1247
+ var TEMPORAL_HIT_RATIO = 0.6;
1248
+ function looksTemporal(labels) {
1249
+ if (labels.length === 0) return false;
1250
+ let hits = 0;
1251
+ for (const l of labels) {
1252
+ if (/^\d{4}-\d{2}(-\d{2})?$/.test(l)) hits++;
1253
+ else if (/^\d{4}\/\d{1,2}(\/\d{1,2})?$/.test(l)) hits++;
1254
+ else if (/^\d{4}年/.test(l)) hits++;
1255
+ else if (/^[Qq][1-4]$/.test(l)) hits++;
1256
+ else if (/^(周一|周二|周三|周四|周五|周六|周日|Mon|Tue|Wed|Thu|Fri|Sat|Sun)/i.test(l)) hits++;
1257
+ else if (/^第?\d+[日时周月]/.test(l)) hits++;
1258
+ else if (/^(v|ver\.?|version[ .]?)?\d+(\.\d+)+$/i.test(l)) hits++;
1259
+ else if (/^\d{4}$/.test(l)) hits++;
1260
+ }
1261
+ return hits / labels.length >= TEMPORAL_HIT_RATIO;
1262
+ }
1263
+ function inferChartType(spec) {
1264
+ if (spec.points && spec.points.length > 0) return "scatter";
1265
+ if (spec.dates && spec.dates.length > 0) return "calendar";
1266
+ if (spec.segments && spec.segments.length > 0) return "donut";
1267
+ if (spec.axes && spec.axes.length >= 3) return "radar";
1268
+ const labels = spec.labels ?? [];
1269
+ const multiSeries = (spec.series?.length ?? 0) > 1;
1270
+ if (labels.length > 0) {
1271
+ if (looksTemporal(labels)) return "line";
1272
+ if (multiSeries) return "vbar";
1273
+ return labels.length > 8 ? "hbar" : "vbar";
1274
+ }
1275
+ if (multiSeries) return "line";
1276
+ if (spec.values && spec.values.length > 1) return "line";
1277
+ if (typeof spec.max === "number" && spec.values && spec.values.length === 1) return "gauge";
1278
+ return "vbar";
1279
+ }
1280
+ function dispatchChartEvent(root, type, mark, chart, phase, pointer) {
1281
+ const read = (name) => mark.getAttribute(name) ?? void 0;
1282
+ const numOr = (v) => v == null ? void 0 : Number(v);
1283
+ const detail = {
1284
+ type,
1285
+ phase,
1286
+ chart,
1287
+ index: numOr(read("data-chart-index")),
1288
+ seriesIndex: numOr(read("data-chart-series")),
1289
+ seriesName: read("data-chart-series-name"),
1290
+ label: read("data-chart-label"),
1291
+ value: numOr(read("data-chart-value")),
1292
+ target: mark,
1293
+ pointerX: pointer?.x,
1294
+ pointerY: pointer?.y
1295
+ };
1296
+ emitIcen(root, `icen:chart-${type}`, detail);
1297
+ }
1298
+ var tooltipEl = null;
1299
+ function chartTooltip() {
1300
+ if (!tooltipEl) {
1301
+ tooltipEl = document.createElement("div");
1302
+ tooltipEl.className = "chart-tooltip";
1303
+ tooltipEl.setAttribute("role", "tooltip");
1304
+ tooltipEl.hidden = true;
1305
+ document.body.appendChild(tooltipEl);
1306
+ }
1307
+ return tooltipEl;
1308
+ }
1309
+ function applyTooltipSizing(host) {
1310
+ const tip = chartTooltip();
1311
+ const raw = host?.closest(".chart, [data-chart-root]")?.getAttribute("data-panel-max") ?? null;
1312
+ const n = raw != null ? Number(raw) : NaN;
1313
+ tip.style.maxWidth = Number.isFinite(n) && n > 0 ? `${n}px` : "";
1314
+ }
1315
+ function tooltipText(mark) {
1316
+ const label = mark.getAttribute("data-chart-label") ?? "";
1317
+ const series = mark.getAttribute("data-chart-series-name");
1318
+ const value = mark.getAttribute("data-chart-value");
1319
+ const head = series ? label ? `${series} \xB7 ${label}` : series : label;
1320
+ return value != null ? `${head}\uFF1A${value}` : head;
1321
+ }
1322
+ function moveTooltip(x, y) {
1323
+ const tip = chartTooltip();
1324
+ const pad = 12;
1325
+ const w = tip.offsetWidth || 120;
1326
+ const h = tip.offsetHeight || 28;
1327
+ let left = x + pad;
1328
+ let top = y - h - pad;
1329
+ if (left + w > window.innerWidth - 8) left = x - w - pad;
1330
+ if (top < 8) top = y + pad;
1331
+ tip.style.left = `${Math.round(left)}px`;
1332
+ tip.style.top = `${Math.round(top)}px`;
1333
+ }
1334
+ var chartDelegateMeta = /* @__PURE__ */ new WeakMap();
1335
+ var chartDelegateUnbind = /* @__PURE__ */ new WeakMap();
1336
+ function bindChartEvents(root) {
1337
+ const noop = () => void 0;
1338
+ if (typeof document === "undefined") return noop;
1339
+ const attached = root;
1340
+ if (attached.__icenChartBound) {
1341
+ const prev = chartDelegateUnbind.get(root);
1342
+ if (prev) return prev;
1343
+ }
1344
+ for (let p = root.parentElement; p; p = p.parentElement) {
1345
+ if (p.__icenChartBound) return noop;
1346
+ }
1347
+ const typeNow = () => chartDelegateMeta.get(root)?.type() ?? "vbar";
1348
+ const tipEnabled = () => chartDelegateMeta.get(root)?.tooltip() ?? true;
1349
+ const markOf = (e) => {
1350
+ const t = e.target;
1351
+ return t instanceof Element ? t.closest("[data-chart-mark]") : null;
1352
+ };
1353
+ let hoverMark = null;
1354
+ const enterMark = (mark, x, y) => {
1355
+ hoverMark = mark;
1356
+ mark.classList.add("is-hover");
1357
+ dispatchChartEvent(root, "hover", mark, typeNow(), "enter", { x, y });
1358
+ if (tipEnabled()) {
1359
+ const tip = chartTooltip();
1360
+ tip.textContent = tooltipText(mark);
1361
+ applyTooltipSizing(mark);
1362
+ tip.hidden = false;
1363
+ moveTooltip(x, y);
1364
+ }
1365
+ };
1366
+ const leaveMark = (pointer) => {
1367
+ if (!hoverMark) return;
1368
+ hoverMark.classList.remove("is-hover");
1369
+ dispatchChartEvent(root, "hover", hoverMark, typeNow(), "leave", pointer);
1370
+ hoverMark = null;
1371
+ chartTooltip().hidden = true;
1372
+ };
1373
+ const bindings = [];
1374
+ const on = (type, fn) => {
1375
+ const listener = (evt) => fn(evt);
1376
+ root.addEventListener(type, listener);
1377
+ bindings.push([type, listener]);
1378
+ };
1379
+ on("pointerover", (e) => {
1380
+ const mark = markOf(e);
1381
+ if (!mark || mark === hoverMark) return;
1382
+ leaveMark({ x: e.clientX, y: e.clientY });
1383
+ enterMark(mark, e.clientX, e.clientY);
1384
+ });
1385
+ on("pointermove", (e) => {
1386
+ if (hoverMark && tipEnabled()) {
1387
+ dispatchChartEvent(root, "hover", hoverMark, typeNow(), "move", { x: e.clientX, y: e.clientY });
1388
+ moveTooltip(e.clientX, e.clientY);
1389
+ }
1390
+ });
1391
+ on("pointerout", (e) => {
1392
+ const mark = markOf(e);
1393
+ if (mark && mark === hoverMark) leaveMark({ x: e.clientX, y: e.clientY });
1394
+ });
1395
+ on("pointerleave", () => leaveMark());
1396
+ on("click", (e) => {
1397
+ const mark = markOf(e);
1398
+ if (mark) dispatchChartEvent(root, "click", mark, typeNow(), void 0, { x: e.clientX, y: e.clientY });
1399
+ });
1400
+ on("dblclick", (e) => {
1401
+ const mark = markOf(e);
1402
+ if (mark) dispatchChartEvent(root, "dblclick", mark, typeNow(), void 0, { x: e.clientX, y: e.clientY });
1403
+ });
1404
+ on("contextmenu", (e) => {
1405
+ const mark = markOf(e);
1406
+ if (!mark) return;
1407
+ e.preventDefault();
1408
+ dispatchChartEvent(root, "contextmenu", mark, typeNow(), void 0, { x: e.clientX, y: e.clientY });
1409
+ });
1410
+ const unbind = () => {
1411
+ for (const [type, listener] of bindings) root.removeEventListener(type, listener);
1412
+ bindings.length = 0;
1413
+ attached.__icenChartBound = false;
1414
+ chartDelegateUnbind.delete(root);
1415
+ if (hoverMark) {
1416
+ hoverMark.classList.remove("is-hover");
1417
+ hoverMark = null;
1418
+ }
1419
+ chartTooltip().hidden = true;
1420
+ };
1421
+ attached.__icenChartBound = true;
1422
+ chartDelegateUnbind.set(root, unbind);
1423
+ return unbind;
1424
+ }
1425
+ function attachChartInteraction(el, type) {
1426
+ chartDelegateMeta.set(el, { type: () => type, tooltip: () => true });
1427
+ bindChartEvents(el);
1428
+ }
1429
+ function hasLegendOf(spec) {
1430
+ switch (spec.type) {
1431
+ case "donut":
1432
+ case "stack":
1433
+ return (spec.segments?.length ?? 0) > 0;
1434
+ case "heatmap":
1435
+ case "calendar":
1436
+ return true;
1437
+ case "line":
1438
+ case "vbar":
1439
+ case "radar":
1440
+ return (spec.series?.length ?? 0) > 1;
1441
+ default:
1442
+ return false;
1443
+ }
1444
+ }
1445
+ function renderChart(el, raw) {
1446
+ const noop = () => void 0;
1447
+ if (typeof document === "undefined") {
1448
+ return { el, spec: {}, update: noop, on: () => noop, destroy: noop };
1449
+ }
1450
+ const root = el;
1451
+ let current = normalizeChartSpec(raw);
1452
+ const typeOf = () => current.type ?? "vbar";
1453
+ const legendState = { hidden: null };
1454
+ const render = () => {
1455
+ if (!current.type) current = { ...current, type: inferChartType(current) };
1456
+ const spec = current;
1457
+ const formatValue = chartFormatValue(spec.format);
1458
+ if (spec.legend !== void 0) legendState.hidden = spec.legend === false;
1459
+ withChrome(root, spec, hasLegendOf(spec), (body) => {
1460
+ switch (spec.type) {
1461
+ case "line":
1462
+ renderLine(body, { labels: spec.labels ?? [], values: spec.values ?? [], series: spec.series, tone: spec.tone, fill: spec.fill, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1463
+ break;
1464
+ case "area":
1465
+ renderLine(body, { labels: spec.labels ?? [], values: spec.values ?? [], series: spec.series, tone: spec.tone, fill: true, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1466
+ break;
1467
+ case "vbar":
1468
+ renderVBar(body, { labels: spec.labels ?? [], values: spec.values ?? [], series: spec.series, stacked: spec.stacked, tone: spec.tone, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1469
+ break;
1470
+ case "hbar":
1471
+ renderHBar(body, { labels: spec.labels ?? [], values: spec.values ?? [], tone: spec.tone, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1472
+ break;
1473
+ case "stack":
1474
+ renderStack(body, { segments: spec.segments ?? [], formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1475
+ break;
1476
+ case "donut":
1477
+ renderDonut(body, { segments: spec.segments ?? [], formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1478
+ break;
1479
+ case "radar": {
1480
+ const series = spec.series ? spec.series.map((s) => ({ name: s.name, values: s.values, tone: typeof s.tone === "string" ? s.tone : void 0 })) : spec.values ? [{ name: "", values: spec.values }] : [];
1481
+ renderRadar(body, { axes: spec.axes ?? spec.labels ?? [], series, max: spec.max, levels: spec.levels, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1482
+ break;
1483
+ }
1484
+ case "heatmap":
1485
+ renderHeatmap(body, { values: spec.values, data: spec.data, weeks: spec.weeks, tone: spec.tone, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend, weekStart: spec.i18n?.weekStart, labels: spec.i18n });
1486
+ break;
1487
+ case "calendar":
1488
+ renderCalendar(body, { dates: spec.dates, values: spec.values, data: spec.data, tone: spec.tone, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend, weekStart: spec.i18n?.weekStart, labels: spec.i18n });
1489
+ break;
1490
+ case "sparkline":
1491
+ renderSparkline(body, { labels: spec.labels ?? [], values: spec.values ?? [], tone: spec.tone, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1492
+ break;
1493
+ case "gauge":
1494
+ renderGauge(body, { value: spec.values?.[0] ?? 0, max: spec.max, tone: spec.tone, label: spec.labels?.[0], formatValue });
1495
+ break;
1496
+ case "scatter":
1497
+ renderScatter(body, { points: spec.points ?? [], tone: spec.tone, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
1498
+ break;
1499
+ default:
1500
+ renderEmpty(body, spec.emptyLabel);
1501
+ }
1502
+ });
1503
+ };
1504
+ chartDelegateMeta.set(root, { type: typeOf, tooltip: () => current.tooltip !== false });
1505
+ const unbindEvents = bindChartEvents(root);
1506
+ render();
1507
+ return {
1508
+ el: root,
1509
+ spec: current,
1510
+ update(next) {
1511
+ current = normalizeChartSpec(next);
1512
+ render();
1513
+ },
1514
+ on(name, listener) {
1515
+ const handler = (event) => {
1516
+ listener(event.detail, event);
1517
+ };
1518
+ root.addEventListener(`icen:chart-${name}`, handler);
1519
+ return () => root.removeEventListener(`icen:chart-${name}`, handler);
1520
+ },
1521
+ destroy() {
1522
+ root.textContent = "";
1523
+ unbindEvents();
1524
+ chartTooltip().hidden = true;
1525
+ }
1526
+ };
611
1527
  }
612
1528
  export {
1529
+ bindChartEvents,
1530
+ chartFormatValue,
1531
+ inferChartType,
1532
+ normalizeChartSpec,
1533
+ registerChartTone,
613
1534
  renderArea,
1535
+ renderCalendar,
1536
+ renderChart,
614
1537
  renderDonut,
615
1538
  renderGauge,
616
1539
  renderHBar,
617
1540
  renderHeatmap,
618
1541
  renderLine,
619
1542
  renderRadar,
1543
+ renderScatter,
620
1544
  renderSparkline,
621
1545
  renderStack,
622
1546
  renderVBar