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