@icen.ai/ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +63 -14
- package/README.md +17 -9
- package/dist/base.css +4 -3
- package/dist/behaviors/accordion.d.mts +5 -2
- package/dist/behaviors/accordion.mjs +36 -11
- package/dist/behaviors/ai-branch.d.mts +19 -0
- package/dist/behaviors/ai-branch.mjs +217 -0
- package/dist/behaviors/ai-chat.d.mts +11 -7
- package/dist/behaviors/ai-chat.mjs +134 -51
- package/dist/behaviors/ai-composer.d.mts +28 -3
- package/dist/behaviors/ai-composer.mjs +399 -229
- package/dist/behaviors/ai-core.d.mts +9 -4
- package/dist/behaviors/ai-core.mjs +21 -8
- package/dist/behaviors/ai-diff.d.mts +2 -2
- package/dist/behaviors/ai-diff.mjs +172 -41
- package/dist/behaviors/ai-feedback.d.mts +26 -0
- package/dist/behaviors/ai-feedback.mjs +303 -0
- package/dist/behaviors/ai-panel.d.mts +26 -9
- package/dist/behaviors/ai-panel.mjs +211 -110
- package/dist/behaviors/ai-provider.mjs +54 -48
- package/dist/behaviors/ai-threads.d.mts +38 -0
- package/dist/behaviors/ai-threads.mjs +319 -0
- package/dist/behaviors/ai-tool.d.mts +12 -4
- package/dist/behaviors/ai-tool.mjs +316 -263
- package/dist/behaviors/ai-tools.d.mts +9 -6
- package/dist/behaviors/ai-tools.mjs +264 -155
- package/dist/behaviors/back-top.mjs +16 -1
- package/dist/behaviors/carousel.d.mts +5 -2
- package/dist/behaviors/carousel.mjs +53 -26
- package/dist/behaviors/charts.d.mts +170 -22
- package/dist/behaviors/charts.mjs +1016 -150
- package/dist/behaviors/command-palette.d.mts +40 -6
- package/dist/behaviors/command-palette.mjs +143 -15
- package/dist/behaviors/context-menu.d.mts +21 -4
- package/dist/behaviors/context-menu.mjs +122 -38
- package/dist/behaviors/controls.d.mts +59 -0
- package/dist/behaviors/controls.mjs +601 -0
- package/dist/behaviors/copy.d.mts +5 -2
- package/dist/behaviors/copy.mjs +37 -12
- package/dist/behaviors/datatable.d.mts +12 -0
- package/dist/behaviors/datatable.mjs +226 -43
- package/dist/behaviors/date-picker.d.mts +2 -2
- package/dist/behaviors/date-picker.mjs +171 -32
- package/dist/behaviors/dropdown.d.mts +6 -2
- package/dist/behaviors/dropdown.mjs +164 -25
- package/dist/behaviors/events.d.mts +523 -0
- package/dist/behaviors/events.mjs +208 -0
- package/dist/behaviors/input.mjs +25 -12
- package/dist/behaviors/kb-access.d.mts +34 -0
- package/dist/behaviors/kb-access.mjs +569 -0
- package/dist/behaviors/kb-acl.d.mts +50 -0
- package/dist/behaviors/kb-acl.mjs +405 -0
- package/dist/behaviors/kb-answer.d.mts +38 -0
- package/dist/behaviors/kb-answer.mjs +2055 -0
- package/dist/behaviors/kb-audit.d.mts +23 -0
- package/dist/behaviors/kb-audit.mjs +375 -0
- package/dist/behaviors/kb-canvas.d.mts +44 -0
- package/dist/behaviors/kb-canvas.mjs +589 -0
- package/dist/behaviors/kb-chain.d.mts +15 -0
- package/dist/behaviors/kb-chain.mjs +270 -0
- package/dist/behaviors/kb-checkpoint.d.mts +16 -0
- package/dist/behaviors/kb-checkpoint.mjs +342 -0
- package/dist/behaviors/kb-chunks.d.mts +31 -0
- package/dist/behaviors/kb-chunks.mjs +475 -0
- package/dist/behaviors/kb-citation.d.mts +38 -0
- package/dist/behaviors/kb-citation.mjs +677 -0
- package/dist/behaviors/kb-clarify.d.mts +20 -0
- package/dist/behaviors/kb-clarify.mjs +248 -0
- package/dist/behaviors/kb-conflict.d.mts +22 -0
- package/dist/behaviors/kb-conflict.mjs +284 -0
- package/dist/behaviors/kb-connector.d.mts +17 -0
- package/dist/behaviors/kb-connector.mjs +375 -0
- package/dist/behaviors/kb-core.d.mts +529 -0
- package/dist/behaviors/kb-core.mjs +890 -0
- package/dist/behaviors/kb-eval.d.mts +22 -0
- package/dist/behaviors/kb-eval.mjs +408 -0
- package/dist/behaviors/kb-explain.d.mts +11 -0
- package/dist/behaviors/kb-explain.mjs +243 -0
- package/dist/behaviors/kb-filter.d.mts +31 -0
- package/dist/behaviors/kb-filter.mjs +587 -0
- package/dist/behaviors/kb-gap.d.mts +27 -0
- package/dist/behaviors/kb-gap.mjs +273 -0
- package/dist/behaviors/kb-hittest.d.mts +34 -0
- package/dist/behaviors/kb-hittest.mjs +409 -0
- package/dist/behaviors/kb-metadata.d.mts +29 -0
- package/dist/behaviors/kb-metadata.mjs +326 -0
- package/dist/behaviors/kb-passage.d.mts +41 -0
- package/dist/behaviors/kb-passage.mjs +529 -0
- package/dist/behaviors/kb-pipeline.d.mts +18 -0
- package/dist/behaviors/kb-pipeline.mjs +381 -0
- package/dist/behaviors/kb-qa.d.mts +26 -0
- package/dist/behaviors/kb-qa.mjs +332 -0
- package/dist/behaviors/kb-rerank.d.mts +20 -0
- package/dist/behaviors/kb-rerank.mjs +315 -0
- package/dist/behaviors/kb-retrieval.d.mts +49 -0
- package/dist/behaviors/kb-retrieval.mjs +746 -0
- package/dist/behaviors/kb-review.d.mts +44 -0
- package/dist/behaviors/kb-review.mjs +451 -0
- package/dist/behaviors/kb-sandbox.d.mts +33 -0
- package/dist/behaviors/kb-sandbox.mjs +325 -0
- package/dist/behaviors/kb-segment.d.mts +32 -0
- package/dist/behaviors/kb-segment.mjs +499 -0
- package/dist/behaviors/kb-sources.d.mts +27 -0
- package/dist/behaviors/kb-sources.mjs +373 -0
- package/dist/behaviors/kb-sql.d.mts +43 -0
- package/dist/behaviors/kb-sql.mjs +948 -0
- package/dist/behaviors/kb-trace.d.mts +16 -0
- package/dist/behaviors/kb-trace.mjs +414 -0
- package/dist/behaviors/kb-visibility.d.mts +44 -0
- package/dist/behaviors/kb-visibility.mjs +403 -0
- package/dist/behaviors/kb-who-can.d.mts +32 -0
- package/dist/behaviors/kb-who-can.mjs +379 -0
- package/dist/behaviors/modal.d.mts +24 -5
- package/dist/behaviors/modal.mjs +76 -10
- package/dist/behaviors/nav.d.mts +1 -1
- package/dist/behaviors/nav.mjs +33 -7
- package/dist/behaviors/notification.d.mts +20 -4
- package/dist/behaviors/notification.mjs +53 -73
- package/dist/behaviors/popover.d.mts +39 -3
- package/dist/behaviors/popover.mjs +90 -4
- package/dist/behaviors/select.d.mts +4 -2
- package/dist/behaviors/select.mjs +161 -24
- package/dist/behaviors/sidebar.d.mts +5 -2
- package/dist/behaviors/sidebar.mjs +43 -13
- package/dist/behaviors/slider.d.mts +5 -2
- package/dist/behaviors/slider.mjs +54 -43
- package/dist/behaviors/split-pane.d.mts +6 -2
- package/dist/behaviors/split-pane.mjs +62 -21
- package/dist/behaviors/tabs.d.mts +3 -2
- package/dist/behaviors/tabs.mjs +65 -14
- package/dist/behaviors/tag-input.d.mts +5 -2
- package/dist/behaviors/tag-input.mjs +61 -21
- package/dist/behaviors/theme.d.mts +13 -2
- package/dist/behaviors/theme.mjs +48 -4
- package/dist/behaviors/toast.d.mts +30 -8
- package/dist/behaviors/toast.mjs +74 -7
- package/dist/behaviors/tree.d.mts +5 -2
- package/dist/behaviors/tree.mjs +38 -15
- package/dist/behaviors/upload.mjs +21 -23
- package/dist/cli.mjs +1 -1
- package/dist/components/ai-branch.mjs +3 -0
- package/dist/components/ai-chat.css +295 -0
- package/dist/components/ai-feedback.mjs +3 -0
- package/dist/components/ai-panel.css +218 -1
- package/dist/components/ai-threads.mjs +3 -0
- package/dist/components/ai-tool-call.mjs +1 -1
- package/dist/components/badge.css +1 -1
- package/dist/components/breadcrumb.css +0 -32
- package/dist/components/chart-calendar.mjs +1 -2
- package/dist/components/chart-graph.css +57 -0
- package/dist/components/chart-graph.mjs +4 -0
- package/dist/components/chart-map.mjs +3 -0
- package/dist/components/chart-pie.css +0 -6
- package/dist/components/chart-scatter.mjs +1 -1
- package/dist/components/charts.css +33 -1
- package/dist/components/charts.mjs +1 -0
- package/dist/components/content.css +3 -16
- package/dist/components/datatable.css +1 -1
- package/dist/components/date-picker.css +1 -1
- package/dist/components/kb-access.mjs +4 -0
- package/dist/components/kb-acl.mjs +4 -0
- package/dist/components/kb-agent.css +481 -0
- package/dist/components/kb-answer.mjs +4 -0
- package/dist/components/kb-audit.mjs +4 -0
- package/dist/components/kb-canvas.mjs +4 -0
- package/dist/components/kb-chain.mjs +4 -0
- package/dist/components/kb-checkpoint.mjs +4 -0
- package/dist/components/kb-chunks.mjs +4 -0
- package/dist/components/kb-citation.mjs +4 -0
- package/dist/components/kb-clarify.mjs +4 -0
- package/dist/components/kb-conflict.mjs +4 -0
- package/dist/components/kb-connector.mjs +4 -0
- package/dist/components/kb-data.css +649 -0
- package/dist/components/kb-eval.mjs +4 -0
- package/dist/components/kb-explain.mjs +4 -0
- package/dist/components/kb-filter.mjs +4 -0
- package/dist/components/kb-gap.mjs +4 -0
- package/dist/components/kb-ground.css +566 -0
- package/dist/components/kb-hittest.mjs +4 -0
- package/dist/components/kb-ingest.css +898 -0
- package/dist/components/kb-metadata.mjs +4 -0
- package/dist/components/kb-ops.css +548 -0
- package/dist/components/kb-passage.mjs +4 -0
- package/dist/components/kb-perm.css +444 -0
- package/dist/components/kb-pipeline.mjs +4 -0
- package/dist/components/kb-qa.mjs +4 -0
- package/dist/components/kb-rerank.mjs +4 -0
- package/dist/components/kb-retrieval.mjs +4 -0
- package/dist/components/kb-review.mjs +4 -0
- package/dist/components/kb-sandbox.mjs +4 -0
- package/dist/components/kb-search.css +940 -0
- package/dist/components/kb-segment.mjs +4 -0
- package/dist/components/kb-sources.mjs +4 -0
- package/dist/components/kb-sql.mjs +4 -0
- package/dist/components/kb-trace.mjs +4 -0
- package/dist/components/kb-visibility.mjs +4 -0
- package/dist/components/kb-who-can.mjs +4 -0
- package/dist/components/kb.css +252 -0
- package/dist/components/kb.mjs +9 -0
- package/dist/components/notification.css +2 -3
- package/dist/components/pagination.css +3 -1
- package/dist/components/pagination.mjs +1 -0
- package/dist/components/rating.mjs +1 -0
- package/dist/components/segmented.css +9 -1
- package/dist/components/segmented.mjs +1 -1
- package/dist/components/select.css +67 -4
- package/dist/components/steps.css +13 -3
- package/dist/components/steps.mjs +1 -0
- package/dist/components/switch.css +3 -1
- package/dist/components/switch.mjs +1 -0
- package/dist/components/table.mjs +1 -0
- package/dist/components/tabs.css +23 -0
- package/dist/components/tag-input.css +1 -6
- package/dist/components/upload.css +0 -15
- package/dist/index.d.mts +48 -11
- package/dist/index.mjs +13205 -3615
- package/dist/registry.json +366 -15
- package/dist/retro-effects.css +3 -16
- package/dist/tokens/retro-effects.css +3 -16
- package/dist/tokens/style-profiles.css +3 -2
- package/dist/tokens/typography.css +2 -0
- package/dist/tokens.css +5 -2
- package/dist/ui.css +5713 -267
- package/docs/research/2026-10-03-ai-kb-components.md +392 -0
- package/docs/research/2026-10-03-kb-permissions.md +992 -0
- package/docs/spec/ai-native.md +7 -4
- package/docs/spec/kb-family.md +443 -0
- package/package.json +2 -2
|
@@ -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)";
|
|
@@ -154,17 +168,14 @@ function withChrome(el, opts, hasLegend, paint, state) {
|
|
|
154
168
|
if (state) state.hidden = hidden;
|
|
155
169
|
el.setAttribute("data-legend-hidden", String(hidden));
|
|
156
170
|
syncEye();
|
|
157
|
-
el
|
|
158
|
-
bubbles: true,
|
|
159
|
-
detail: { visible: hidden ? false : true }
|
|
160
|
-
}));
|
|
171
|
+
emitIcen(el, "icen:chart-legend-visibility", { visible: hidden ? false : true });
|
|
161
172
|
});
|
|
162
173
|
syncEye();
|
|
163
174
|
head.append(title, eye);
|
|
164
175
|
el.append(head, body);
|
|
165
176
|
paint(body);
|
|
166
177
|
}
|
|
167
|
-
function toggleLegend(items, scope
|
|
178
|
+
function toggleLegend(items, scope) {
|
|
168
179
|
const legend = document.createElement("div");
|
|
169
180
|
legend.className = "chart-legend chart-legend--toggle";
|
|
170
181
|
for (const item of items) {
|
|
@@ -183,10 +194,7 @@ function toggleLegend(items, scope, _opts) {
|
|
|
183
194
|
scope.querySelectorAll(`[data-chart-key="${CSS.escape(item.key)}"]`).forEach((m) => {
|
|
184
195
|
m.classList.toggle("is-off", off);
|
|
185
196
|
});
|
|
186
|
-
legend
|
|
187
|
-
bubbles: true,
|
|
188
|
-
detail: { key: item.key, seriesIndex: item.si, hidden: off }
|
|
189
|
-
}));
|
|
197
|
+
emitIcen(legend, "icen:chart-legend-toggle", { key: item.key, seriesIndex: item.si, hidden: off });
|
|
190
198
|
};
|
|
191
199
|
row.addEventListener("click", toggle);
|
|
192
200
|
row.addEventListener("keydown", (e) => {
|
|
@@ -205,7 +213,7 @@ function toggleLegend(items, scope, _opts) {
|
|
|
205
213
|
return legend;
|
|
206
214
|
}
|
|
207
215
|
function renderVBar(el, opts) {
|
|
208
|
-
if (typeof document === "undefined") return;
|
|
216
|
+
if (typeof document === "undefined") return el;
|
|
209
217
|
const seriesList = normalizeSeriesInput(opts);
|
|
210
218
|
const labels = opts.labels ?? [];
|
|
211
219
|
const n = Math.min(labels.length, ...seriesList.map((s) => s.values.length));
|
|
@@ -256,11 +264,13 @@ function renderVBar(el, opts) {
|
|
|
256
264
|
wrap.appendChild(track);
|
|
257
265
|
}
|
|
258
266
|
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
|
|
267
|
+
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name, label: s.name, tone: s.tone, si })), wrap));
|
|
260
268
|
});
|
|
269
|
+
attachChartInteraction(el, "vbar");
|
|
270
|
+
return el;
|
|
261
271
|
}
|
|
262
272
|
function renderHBar(el, opts) {
|
|
263
|
-
if (typeof document === "undefined") return;
|
|
273
|
+
if (typeof document === "undefined") return el;
|
|
264
274
|
withChrome(el, opts, false, (body) => {
|
|
265
275
|
const n = Math.min(opts.labels.length, opts.values.length);
|
|
266
276
|
if (n === 0) {
|
|
@@ -294,9 +304,11 @@ function renderHBar(el, opts) {
|
|
|
294
304
|
});
|
|
295
305
|
body.appendChild(wrap);
|
|
296
306
|
});
|
|
307
|
+
attachChartInteraction(el, "hbar");
|
|
308
|
+
return el;
|
|
297
309
|
}
|
|
298
310
|
function renderStack(el, opts) {
|
|
299
|
-
if (typeof document === "undefined") return;
|
|
311
|
+
if (typeof document === "undefined") return el;
|
|
300
312
|
withChrome(el, opts, opts.segments.length > 0, (body) => {
|
|
301
313
|
const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
|
|
302
314
|
if (opts.segments.length === 0 || total <= 0) {
|
|
@@ -305,13 +317,13 @@ function renderStack(el, opts) {
|
|
|
305
317
|
}
|
|
306
318
|
const bar = document.createElement("div");
|
|
307
319
|
bar.className = "chart-stack";
|
|
308
|
-
for (const seg of opts.segments) {
|
|
320
|
+
for (const [segIndex, seg] of opts.segments.entries()) {
|
|
309
321
|
const s = document.createElement("div");
|
|
310
322
|
s.className = "chart-stack-seg";
|
|
311
323
|
s.style.width = `${seg.value / total * 100}%`;
|
|
312
324
|
if (seg.tone) s.style.setProperty("--chart-tone", toneVar(seg.tone));
|
|
313
325
|
s.setAttribute("title", `${seg.label}: ${fmt(opts, seg.value)}`);
|
|
314
|
-
setMarkData(s, { index:
|
|
326
|
+
setMarkData(s, { index: segIndex, label: seg.label, value: seg.value });
|
|
315
327
|
bar.appendChild(s);
|
|
316
328
|
}
|
|
317
329
|
const legend = document.createElement("div");
|
|
@@ -321,9 +333,11 @@ function renderStack(el, opts) {
|
|
|
321
333
|
}
|
|
322
334
|
body.append(bar, legend);
|
|
323
335
|
});
|
|
336
|
+
attachChartInteraction(el, "stack");
|
|
337
|
+
return el;
|
|
324
338
|
}
|
|
325
339
|
function renderDonut(el, opts) {
|
|
326
|
-
if (typeof document === "undefined") return;
|
|
340
|
+
if (typeof document === "undefined") return el;
|
|
327
341
|
withChrome(el, opts, opts.segments.length > 0, (body) => {
|
|
328
342
|
const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
|
|
329
343
|
if (opts.segments.length === 0 || total <= 0) {
|
|
@@ -370,10 +384,12 @@ function renderDonut(el, opts) {
|
|
|
370
384
|
wrap.append(svg, legend);
|
|
371
385
|
body.appendChild(wrap);
|
|
372
386
|
});
|
|
387
|
+
attachChartInteraction(el, "donut");
|
|
388
|
+
return el;
|
|
373
389
|
}
|
|
374
390
|
var lineGradientSeq = 0;
|
|
375
391
|
function renderLine(el, opts) {
|
|
376
|
-
if (typeof document === "undefined") return;
|
|
392
|
+
if (typeof document === "undefined") return el;
|
|
377
393
|
const seriesList = normalizeSeriesInput(opts);
|
|
378
394
|
const labels = opts.labels ?? [];
|
|
379
395
|
const n = Math.min(labels.length, ...seriesList.map((s) => s.values.length));
|
|
@@ -478,11 +494,13 @@ function renderLine(el, opts) {
|
|
|
478
494
|
if (defs.firstChild) svg.insertBefore(defs, svg.firstChild);
|
|
479
495
|
wrap.append(svg, labelsRow(sampleLabels(labelsUsed)));
|
|
480
496
|
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
|
|
497
|
+
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name, label: s.name, tone: s.tone, si })), wrap));
|
|
482
498
|
});
|
|
499
|
+
attachChartInteraction(el, "line");
|
|
500
|
+
return el;
|
|
483
501
|
}
|
|
484
502
|
function renderArea(el, opts) {
|
|
485
|
-
if (typeof document === "undefined") return;
|
|
503
|
+
if (typeof document === "undefined") return el;
|
|
486
504
|
withChrome(el, opts, false, (body) => {
|
|
487
505
|
const n = Math.min(opts.labels.length, opts.values.length);
|
|
488
506
|
if (n === 0) {
|
|
@@ -551,9 +569,11 @@ function renderArea(el, opts) {
|
|
|
551
569
|
wrap.append(svg, labelsRow(sampleLabels(labels)));
|
|
552
570
|
body.appendChild(wrap);
|
|
553
571
|
});
|
|
572
|
+
attachChartInteraction(el, "area");
|
|
573
|
+
return el;
|
|
554
574
|
}
|
|
555
575
|
function renderRadar(el, opts) {
|
|
556
|
-
if (typeof document === "undefined") return;
|
|
576
|
+
if (typeof document === "undefined") return el;
|
|
557
577
|
withChrome(el, opts, opts.series.length > 1, (body) => {
|
|
558
578
|
const axes = opts.axes;
|
|
559
579
|
const n = axes.length;
|
|
@@ -655,13 +675,12 @@ function renderRadar(el, opts) {
|
|
|
655
675
|
}
|
|
656
676
|
legendItems.push({ key: s.name || `\u7CFB\u5217${si + 1}`, label: s.name || `\u7CFB\u5217${si + 1}`, tone: s.tone, si });
|
|
657
677
|
});
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
l.className = "chart-legend";
|
|
661
|
-
return l;
|
|
662
|
-
})());
|
|
678
|
+
const legend = opts.series.length > 1 ? toggleLegend(legendItems, wrap) : null;
|
|
679
|
+
wrap.append(svg, ...legend ? [legend] : []);
|
|
663
680
|
body.appendChild(wrap);
|
|
664
681
|
});
|
|
682
|
+
attachChartInteraction(el, "radar");
|
|
683
|
+
return el;
|
|
665
684
|
}
|
|
666
685
|
function niceCeil(v) {
|
|
667
686
|
if (v <= 0) return 1;
|
|
@@ -680,8 +699,41 @@ function isoDate(d) {
|
|
|
680
699
|
const day = String(d.getDate()).padStart(2, "0");
|
|
681
700
|
return `${d.getFullYear()}-${m}-${day}`;
|
|
682
701
|
}
|
|
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;
|
|
711
|
+
for (let i = 0; i < pad; i++) {
|
|
712
|
+
const blank = document.createElement("span");
|
|
713
|
+
blank.className = "chart-heatmap-cell is-blank";
|
|
714
|
+
grid.appendChild(blank);
|
|
715
|
+
}
|
|
716
|
+
return pad;
|
|
717
|
+
}
|
|
718
|
+
function heatmapLegend(labels) {
|
|
719
|
+
const legend = document.createElement("div");
|
|
720
|
+
legend.className = "chart-heatmap-legend";
|
|
721
|
+
const less = document.createElement("span");
|
|
722
|
+
less.textContent = labels?.less ?? "\u5C11";
|
|
723
|
+
const more = document.createElement("span");
|
|
724
|
+
more.textContent = labels?.more ?? "\u591A";
|
|
725
|
+
legend.appendChild(less);
|
|
726
|
+
for (let lv = 0; lv <= 4; lv++) {
|
|
727
|
+
const cell = document.createElement("span");
|
|
728
|
+
cell.className = "chart-heatmap-cell";
|
|
729
|
+
cell.dataset.level = String(lv);
|
|
730
|
+
legend.appendChild(cell);
|
|
731
|
+
}
|
|
732
|
+
legend.appendChild(more);
|
|
733
|
+
return legend;
|
|
734
|
+
}
|
|
683
735
|
function renderHeatmap(el, opts) {
|
|
684
|
-
if (typeof document === "undefined") return;
|
|
736
|
+
if (typeof document === "undefined") return el;
|
|
685
737
|
withChrome(el, opts, true, (body) => {
|
|
686
738
|
let entries;
|
|
687
739
|
if (opts.data && opts.data.length > 0) {
|
|
@@ -700,48 +752,29 @@ function renderHeatmap(el, opts) {
|
|
|
700
752
|
const weeks = opts.weeks ?? Math.ceil(entries.length / 7);
|
|
701
753
|
if (entries.length > weeks * 7) entries = entries.slice(entries.length - weeks * 7);
|
|
702
754
|
const max = maxOf(entries.map((e) => e.value));
|
|
703
|
-
const levelOf = (v) => v <= 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
|
|
704
755
|
const wrap = document.createElement("div");
|
|
705
756
|
wrap.className = "chart-heatmap";
|
|
706
757
|
if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
|
|
707
758
|
const grid = document.createElement("div");
|
|
708
759
|
grid.className = "chart-heatmap-grid";
|
|
709
760
|
grid.setAttribute("role", "img");
|
|
710
|
-
|
|
711
|
-
const
|
|
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) {
|
|
761
|
+
padFirstWeek(grid, entries[0].date, opts.weekStart === 0 ? 0 : 1);
|
|
762
|
+
for (const [i, entry] of entries.entries()) {
|
|
718
763
|
const cell = document.createElement("span");
|
|
719
764
|
cell.className = "chart-heatmap-cell";
|
|
720
|
-
cell.dataset.level = String(levelOf(entry.value));
|
|
765
|
+
cell.dataset.level = String(levelOf(entry.value, max));
|
|
721
766
|
cell.setAttribute("title", `${entry.date}\uFF1A${fmt(opts, entry.value)}`);
|
|
722
|
-
setMarkData(cell, { index:
|
|
767
|
+
setMarkData(cell, { index: i, label: entry.date, value: entry.value });
|
|
723
768
|
grid.appendChild(cell);
|
|
724
769
|
}
|
|
725
|
-
|
|
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);
|
|
770
|
+
wrap.append(grid, heatmapLegend(opts.labels));
|
|
740
771
|
body.appendChild(wrap);
|
|
741
772
|
});
|
|
773
|
+
attachChartInteraction(el, "heatmap");
|
|
774
|
+
return el;
|
|
742
775
|
}
|
|
743
776
|
function renderSparkline(el, opts) {
|
|
744
|
-
if (typeof document === "undefined") return;
|
|
777
|
+
if (typeof document === "undefined") return el;
|
|
745
778
|
withChrome(el, opts, false, (body) => {
|
|
746
779
|
const values = opts.values;
|
|
747
780
|
if (values.length === 0) {
|
|
@@ -796,9 +829,11 @@ function renderSparkline(el, opts) {
|
|
|
796
829
|
wrap.appendChild(svg);
|
|
797
830
|
body.appendChild(wrap);
|
|
798
831
|
});
|
|
832
|
+
attachChartInteraction(el, "sparkline");
|
|
833
|
+
return el;
|
|
799
834
|
}
|
|
800
835
|
function renderGauge(el, opts) {
|
|
801
|
-
if (typeof document === "undefined") return;
|
|
836
|
+
if (typeof document === "undefined") return el;
|
|
802
837
|
el.textContent = "";
|
|
803
838
|
const max = opts.max ?? 100;
|
|
804
839
|
const ratio = max > 0 ? Math.min(1, Math.max(0, opts.value / max)) : 0;
|
|
@@ -842,9 +877,11 @@ function renderGauge(el, opts) {
|
|
|
842
877
|
}
|
|
843
878
|
wrap.append(svg, center);
|
|
844
879
|
el.appendChild(wrap);
|
|
880
|
+
attachChartInteraction(el, "gauge");
|
|
881
|
+
return el;
|
|
845
882
|
}
|
|
846
883
|
function renderCalendar(el, opts) {
|
|
847
|
-
if (typeof document === "undefined") return;
|
|
884
|
+
if (typeof document === "undefined") return el;
|
|
848
885
|
withChrome(el, opts, true, (body) => {
|
|
849
886
|
let entries;
|
|
850
887
|
if (opts.data && opts.data.length > 0) {
|
|
@@ -858,6 +895,10 @@ function renderCalendar(el, opts) {
|
|
|
858
895
|
const wrap = document.createElement("div");
|
|
859
896
|
wrap.className = "chart-calendar";
|
|
860
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`;
|
|
861
902
|
const grid = document.createElement("div");
|
|
862
903
|
grid.className = "chart-heatmap-grid";
|
|
863
904
|
grid.setAttribute("role", "img");
|
|
@@ -865,20 +906,13 @@ function renderCalendar(el, opts) {
|
|
|
865
906
|
months.className = "chart-calendar-months";
|
|
866
907
|
const weekdayRow = document.createElement("div");
|
|
867
908
|
weekdayRow.className = "chart-calendar-weekdays";
|
|
868
|
-
|
|
909
|
+
weekdayLabels.forEach((w) => {
|
|
869
910
|
const s = document.createElement("span");
|
|
870
911
|
s.textContent = w;
|
|
871
912
|
weekdayRow.appendChild(s);
|
|
872
913
|
});
|
|
873
914
|
const max = maxOf(entries.map((e) => e.value));
|
|
874
|
-
const
|
|
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
|
-
}
|
|
915
|
+
const pad = padFirstWeek(grid, entries[0].date, weekStart);
|
|
882
916
|
let lastMonth = -1;
|
|
883
917
|
entries.forEach((entry, i) => {
|
|
884
918
|
const d = /* @__PURE__ */ new Date(`${entry.date}T00:00:00`);
|
|
@@ -887,39 +921,28 @@ function renderCalendar(el, opts) {
|
|
|
887
921
|
lastMonth = d.getMonth();
|
|
888
922
|
const label = document.createElement("span");
|
|
889
923
|
label.className = "chart-calendar-month";
|
|
890
|
-
label.textContent =
|
|
924
|
+
label.textContent = monthLabel(d.getMonth());
|
|
891
925
|
label.style.setProperty("--col", String(col));
|
|
892
926
|
months.appendChild(label);
|
|
893
927
|
}
|
|
894
928
|
const cell = document.createElement("span");
|
|
895
929
|
cell.className = "chart-heatmap-cell";
|
|
896
|
-
cell.dataset.level = String(levelOf(entry.value));
|
|
930
|
+
cell.dataset.level = String(levelOf(entry.value, max));
|
|
897
931
|
cell.setAttribute("title", `${entry.date}\uFF1A${fmt(opts, entry.value)}`);
|
|
898
932
|
setMarkData(cell, { index: i, label: entry.date, value: entry.value });
|
|
899
933
|
grid.appendChild(cell);
|
|
900
934
|
});
|
|
901
|
-
const legend =
|
|
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);
|
|
935
|
+
const legend = heatmapLegend(opts.labels);
|
|
915
936
|
const columns = Math.max(1, Math.ceil((pad + entries.length) / 7));
|
|
916
937
|
months.style.setProperty("--columns", String(columns));
|
|
917
938
|
wrap.append(months, weekdayRow, grid, legend);
|
|
918
939
|
body.appendChild(wrap);
|
|
919
940
|
});
|
|
941
|
+
attachChartInteraction(el, "calendar");
|
|
942
|
+
return el;
|
|
920
943
|
}
|
|
921
944
|
function renderScatter(el, opts) {
|
|
922
|
-
if (typeof document === "undefined") return;
|
|
945
|
+
if (typeof document === "undefined") return el;
|
|
923
946
|
withChrome(el, opts, false, (body) => {
|
|
924
947
|
const pts = opts.points.filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
925
948
|
if (pts.length === 0) {
|
|
@@ -964,7 +987,7 @@ function renderScatter(el, opts) {
|
|
|
964
987
|
"font-size": "8",
|
|
965
988
|
fill: "var(--token-text-faint)"
|
|
966
989
|
});
|
|
967
|
-
txt.textContent =
|
|
990
|
+
txt.textContent = fmt(opts, Math.round(tv * 10) / 10);
|
|
968
991
|
svg.appendChild(txt);
|
|
969
992
|
}
|
|
970
993
|
for (let i = 0; i <= 4; i++) {
|
|
@@ -986,7 +1009,7 @@ function renderScatter(el, opts) {
|
|
|
986
1009
|
"font-size": "8",
|
|
987
1010
|
fill: "var(--token-text-faint)"
|
|
988
1011
|
});
|
|
989
|
-
txt.textContent =
|
|
1012
|
+
txt.textContent = fmt(opts, Math.round(tv * 10) / 10);
|
|
990
1013
|
svg.appendChild(txt);
|
|
991
1014
|
}
|
|
992
1015
|
if (opts.yLabel) {
|
|
@@ -1010,7 +1033,7 @@ function renderScatter(el, opts) {
|
|
|
1010
1033
|
"stroke-width": "1"
|
|
1011
1034
|
});
|
|
1012
1035
|
const title = svgEl("title", {});
|
|
1013
|
-
title.textContent = `${p.label ?? `#${i + 1}`}: (${p.x}, ${p.y}${p.size != null ? `, size ${p.size}` : ""})`;
|
|
1036
|
+
title.textContent = `${p.label ?? `#${i + 1}`}: (${fmt(opts, p.x)}, ${fmt(opts, p.y)}${p.size != null ? `, size ${fmt(opts, p.size)}` : ""})`;
|
|
1014
1037
|
c.appendChild(title);
|
|
1015
1038
|
setMarkData(c, { index: i, label: p.label ?? `#${i + 1}`, value: p.y });
|
|
1016
1039
|
c.dataset.chartX = String(p.x);
|
|
@@ -1019,6 +1042,8 @@ function renderScatter(el, opts) {
|
|
|
1019
1042
|
wrap.appendChild(svg);
|
|
1020
1043
|
body.appendChild(wrap);
|
|
1021
1044
|
});
|
|
1045
|
+
attachChartInteraction(el, "scatter");
|
|
1046
|
+
return el;
|
|
1022
1047
|
}
|
|
1023
1048
|
function chartFormatValue(format) {
|
|
1024
1049
|
if (!format) return (v) => String(v);
|
|
@@ -1149,7 +1174,7 @@ function normalizeChartSpec(raw) {
|
|
|
1149
1174
|
}
|
|
1150
1175
|
}
|
|
1151
1176
|
if (!src || typeof src !== "object" || Array.isArray(src)) {
|
|
1152
|
-
if (Array.isArray(
|
|
1177
|
+
if (Array.isArray(src)) src = { data: src };
|
|
1153
1178
|
else return {};
|
|
1154
1179
|
}
|
|
1155
1180
|
const s = src;
|
|
@@ -1219,6 +1244,7 @@ function normalizeChartSpec(raw) {
|
|
|
1219
1244
|
}
|
|
1220
1245
|
return spec;
|
|
1221
1246
|
}
|
|
1247
|
+
var TEMPORAL_HIT_RATIO = 0.6;
|
|
1222
1248
|
function looksTemporal(labels) {
|
|
1223
1249
|
if (labels.length === 0) return false;
|
|
1224
1250
|
let hits = 0;
|
|
@@ -1232,7 +1258,7 @@ function looksTemporal(labels) {
|
|
|
1232
1258
|
else if (/^(v|ver\.?|version[ .]?)?\d+(\.\d+)+$/i.test(l)) hits++;
|
|
1233
1259
|
else if (/^\d{4}$/.test(l)) hits++;
|
|
1234
1260
|
}
|
|
1235
|
-
return hits / labels.length >=
|
|
1261
|
+
return hits / labels.length >= TEMPORAL_HIT_RATIO;
|
|
1236
1262
|
}
|
|
1237
1263
|
function inferChartType(spec) {
|
|
1238
1264
|
if (spec.points && spec.points.length > 0) return "scatter";
|
|
@@ -1267,7 +1293,7 @@ function dispatchChartEvent(root, type, mark, chart, phase, pointer) {
|
|
|
1267
1293
|
pointerX: pointer?.x,
|
|
1268
1294
|
pointerY: pointer?.y
|
|
1269
1295
|
};
|
|
1270
|
-
root
|
|
1296
|
+
emitIcen(root, `icen:chart-${type}`, detail);
|
|
1271
1297
|
}
|
|
1272
1298
|
var tooltipEl = null;
|
|
1273
1299
|
function chartTooltip() {
|
|
@@ -1280,6 +1306,12 @@ function chartTooltip() {
|
|
|
1280
1306
|
}
|
|
1281
1307
|
return tooltipEl;
|
|
1282
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
|
+
}
|
|
1283
1315
|
function tooltipText(mark) {
|
|
1284
1316
|
const label = mark.getAttribute("data-chart-label") ?? "";
|
|
1285
1317
|
const series = mark.getAttribute("data-chart-series-name");
|
|
@@ -1299,6 +1331,101 @@ function moveTooltip(x, y) {
|
|
|
1299
1331
|
tip.style.left = `${Math.round(left)}px`;
|
|
1300
1332
|
tip.style.top = `${Math.round(top)}px`;
|
|
1301
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
|
+
}
|
|
1302
1429
|
function hasLegendOf(spec) {
|
|
1303
1430
|
switch (spec.type) {
|
|
1304
1431
|
case "donut":
|
|
@@ -1321,9 +1448,6 @@ function renderChart(el, raw) {
|
|
|
1321
1448
|
return { el, spec: {}, update: noop, on: () => noop, destroy: noop };
|
|
1322
1449
|
}
|
|
1323
1450
|
const root = el;
|
|
1324
|
-
const attached = root;
|
|
1325
|
-
const wasBound = attached.__icenChartBound === true;
|
|
1326
|
-
attached.__icenChartBound = true;
|
|
1327
1451
|
let current = normalizeChartSpec(raw);
|
|
1328
1452
|
const typeOf = () => current.type ?? "vbar";
|
|
1329
1453
|
const legendState = { hidden: null };
|
|
@@ -1358,10 +1482,10 @@ function renderChart(el, raw) {
|
|
|
1358
1482
|
break;
|
|
1359
1483
|
}
|
|
1360
1484
|
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 });
|
|
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 });
|
|
1362
1486
|
break;
|
|
1363
1487
|
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 });
|
|
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 });
|
|
1365
1489
|
break;
|
|
1366
1490
|
case "sparkline":
|
|
1367
1491
|
renderSparkline(body, { labels: spec.labels ?? [], values: spec.values ?? [], tone: spec.tone, formatValue, emptyLabel: spec.emptyLabel, title: spec.title, legend: spec.legend });
|
|
@@ -1377,63 +1501,8 @@ function renderChart(el, raw) {
|
|
|
1377
1501
|
}
|
|
1378
1502
|
});
|
|
1379
1503
|
};
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
}
|
|
1504
|
+
chartDelegateMeta.set(root, { type: typeOf, tooltip: () => current.tooltip !== false });
|
|
1505
|
+
const unbindEvents = bindChartEvents(root);
|
|
1437
1506
|
render();
|
|
1438
1507
|
return {
|
|
1439
1508
|
el: root,
|
|
@@ -1451,23 +1520,820 @@ function renderChart(el, raw) {
|
|
|
1451
1520
|
},
|
|
1452
1521
|
destroy() {
|
|
1453
1522
|
root.textContent = "";
|
|
1454
|
-
|
|
1523
|
+
unbindEvents();
|
|
1524
|
+
chartTooltip().hidden = true;
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
var GRAPH_CHART_TYPE = "graph";
|
|
1529
|
+
var MAP_CHART_TYPE = "map";
|
|
1530
|
+
function buildLayoutState(nodes, edges, width, height) {
|
|
1531
|
+
const ids = [];
|
|
1532
|
+
const index = /* @__PURE__ */ new Map();
|
|
1533
|
+
for (const node of nodes) {
|
|
1534
|
+
const id = String(node?.id ?? "").trim();
|
|
1535
|
+
if (!id || index.has(id)) continue;
|
|
1536
|
+
index.set(id, ids.length);
|
|
1537
|
+
ids.push(id);
|
|
1538
|
+
}
|
|
1539
|
+
const pairs = [];
|
|
1540
|
+
for (const edge of edges) {
|
|
1541
|
+
const a = index.get(String(edge?.source ?? "").trim());
|
|
1542
|
+
const b = index.get(String(edge?.target ?? "").trim());
|
|
1543
|
+
if (a == null || b == null || a === b) continue;
|
|
1544
|
+
pairs.push([a, b]);
|
|
1545
|
+
}
|
|
1546
|
+
const n = ids.length;
|
|
1547
|
+
return {
|
|
1548
|
+
ids,
|
|
1549
|
+
index,
|
|
1550
|
+
x: new Float64Array(n),
|
|
1551
|
+
y: new Float64Array(n),
|
|
1552
|
+
pairs,
|
|
1553
|
+
width,
|
|
1554
|
+
height,
|
|
1555
|
+
k: n > 0 ? Math.sqrt(width * height / n) : 1
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function relaxLayout(state, rounds, pinned = -1, tempScale = 1) {
|
|
1559
|
+
const { x, y, pairs, width, height, k } = state;
|
|
1560
|
+
const n = x.length;
|
|
1561
|
+
if (n <= 1 || rounds <= 0) return;
|
|
1562
|
+
const cx = width / 2;
|
|
1563
|
+
const cy = height / 2;
|
|
1564
|
+
const gravity = 0.015;
|
|
1565
|
+
const tempStart = Math.min(width, height) / 6 * tempScale;
|
|
1566
|
+
const dx = new Float64Array(n);
|
|
1567
|
+
const dy = new Float64Array(n);
|
|
1568
|
+
for (let round = 0; round < rounds; round++) {
|
|
1569
|
+
dx.fill(0);
|
|
1570
|
+
dy.fill(0);
|
|
1571
|
+
const temp = Math.max(0.5, tempStart * (1 - round / rounds));
|
|
1572
|
+
for (let i = 0; i < n; i++) {
|
|
1573
|
+
for (let j = i + 1; j < n; j++) {
|
|
1574
|
+
let vx = x[j] - x[i];
|
|
1575
|
+
let vy = y[j] - y[i];
|
|
1576
|
+
let d2 = vx * vx + vy * vy;
|
|
1577
|
+
if (d2 < 0.01) {
|
|
1578
|
+
vx = 0.5 + (j - i) % 7 * 0.25;
|
|
1579
|
+
vy = (i * 3 + j) % 5 * 0.3 - 0.6;
|
|
1580
|
+
d2 = vx * vx + vy * vy;
|
|
1581
|
+
}
|
|
1582
|
+
const d = Math.sqrt(d2);
|
|
1583
|
+
const f = k * k / d;
|
|
1584
|
+
const ux = vx / d * f;
|
|
1585
|
+
const uy = vy / d * f;
|
|
1586
|
+
dx[i] -= ux;
|
|
1587
|
+
dy[i] -= uy;
|
|
1588
|
+
dx[j] += ux;
|
|
1589
|
+
dy[j] += uy;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
for (const [a, b] of pairs) {
|
|
1593
|
+
let vx = x[b] - x[a];
|
|
1594
|
+
let vy = y[b] - y[a];
|
|
1595
|
+
let d = Math.sqrt(vx * vx + vy * vy);
|
|
1596
|
+
if (d < 0.01) {
|
|
1597
|
+
vx = 1;
|
|
1598
|
+
vy = 0;
|
|
1599
|
+
d = 1;
|
|
1600
|
+
}
|
|
1601
|
+
const f = Math.min(d * d / k, k * 3);
|
|
1602
|
+
const ux = vx / d * f;
|
|
1603
|
+
const uy = vy / d * f;
|
|
1604
|
+
dx[a] += ux;
|
|
1605
|
+
dy[a] += uy;
|
|
1606
|
+
dx[b] -= ux;
|
|
1607
|
+
dy[b] -= uy;
|
|
1608
|
+
}
|
|
1609
|
+
for (let i = 0; i < n; i++) {
|
|
1610
|
+
if (i === pinned) continue;
|
|
1611
|
+
dx[i] += (cx - x[i]) * gravity;
|
|
1612
|
+
dy[i] += (cy - y[i]) * gravity;
|
|
1613
|
+
const dm = Math.sqrt(dx[i] * dx[i] + dy[i] * dy[i]);
|
|
1614
|
+
if (dm <= 0) continue;
|
|
1615
|
+
const v = Math.min(dm, temp) / dm;
|
|
1616
|
+
x[i] = Math.min(width, Math.max(0, x[i] + dx[i] * v));
|
|
1617
|
+
y[i] = Math.min(height, Math.max(0, y[i] + dy[i] * v));
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function fitLayout(state, margin) {
|
|
1622
|
+
const { x, y, width, height } = state;
|
|
1623
|
+
const n = x.length;
|
|
1624
|
+
if (n === 0) return;
|
|
1625
|
+
let minX = Infinity;
|
|
1626
|
+
let maxX = -Infinity;
|
|
1627
|
+
let minY = Infinity;
|
|
1628
|
+
let maxY = -Infinity;
|
|
1629
|
+
for (let i = 0; i < n; i++) {
|
|
1630
|
+
if (x[i] < minX) minX = x[i];
|
|
1631
|
+
if (x[i] > maxX) maxX = x[i];
|
|
1632
|
+
if (y[i] < minY) minY = y[i];
|
|
1633
|
+
if (y[i] > maxY) maxY = y[i];
|
|
1634
|
+
}
|
|
1635
|
+
const spanX = Math.max(1e-6, maxX - minX);
|
|
1636
|
+
const spanY = Math.max(1e-6, maxY - minY);
|
|
1637
|
+
const scale = Math.min((width - margin * 2) / spanX, (height - margin * 2) / spanY, 4);
|
|
1638
|
+
const offX = (width - spanX * scale) / 2 - minX * scale;
|
|
1639
|
+
const offY = (height - spanY * scale) / 2 - minY * scale;
|
|
1640
|
+
for (let i = 0; i < n; i++) {
|
|
1641
|
+
x[i] = x[i] * scale + offX;
|
|
1642
|
+
y[i] = y[i] * scale + offY;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function initRingLayout(state, seed = 0) {
|
|
1646
|
+
const { ids, index, x, y, width, height } = state;
|
|
1647
|
+
const n = ids.length;
|
|
1648
|
+
if (n === 0) return;
|
|
1649
|
+
if (n === 1) {
|
|
1650
|
+
x[0] = width / 2;
|
|
1651
|
+
y[0] = height / 2;
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const order = [...ids].sort();
|
|
1655
|
+
const radius = Math.min(width, height) / 2 * 0.76;
|
|
1656
|
+
const deg = (seed % 360 + 360) % 360;
|
|
1657
|
+
const seedAngle = deg * Math.PI / 180;
|
|
1658
|
+
for (let rank = 0; rank < n; rank++) {
|
|
1659
|
+
const i = index.get(order[rank] ?? "") ?? -1;
|
|
1660
|
+
if (i < 0) continue;
|
|
1661
|
+
const angle = seedAngle - Math.PI / 2 + rank * 2 * Math.PI / n;
|
|
1662
|
+
x[i] = width / 2 + radius * Math.cos(angle);
|
|
1663
|
+
y[i] = height / 2 + radius * Math.sin(angle);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function layoutGraph(nodes, edges, opts) {
|
|
1667
|
+
const width = Math.max(16, opts?.width ?? 320);
|
|
1668
|
+
const height = Math.max(16, opts?.height ?? 200);
|
|
1669
|
+
const state = buildLayoutState(nodes, edges, width, height);
|
|
1670
|
+
if (state.ids.length === 0) return /* @__PURE__ */ new Map();
|
|
1671
|
+
initRingLayout(state, opts?.seed ?? 0);
|
|
1672
|
+
relaxLayout(state, Math.max(1, Math.round(opts?.iterations ?? 300)));
|
|
1673
|
+
fitLayout(state, Math.max(8, Math.min(width, height) * 0.08));
|
|
1674
|
+
const out = /* @__PURE__ */ new Map();
|
|
1675
|
+
state.ids.forEach((id, i) => out.set(id, { x: state.x[i], y: state.y[i] }));
|
|
1676
|
+
return out;
|
|
1677
|
+
}
|
|
1678
|
+
var datumMarkInfo = /* @__PURE__ */ new WeakMap();
|
|
1679
|
+
var datumEventControl = /* @__PURE__ */ new WeakMap();
|
|
1680
|
+
function dispatchDatumEvent(root, type, mark, phase, pointer) {
|
|
1681
|
+
const numOr = (v) => v == null ? void 0 : Number(v);
|
|
1682
|
+
const detail = {
|
|
1683
|
+
type,
|
|
1684
|
+
phase,
|
|
1685
|
+
chart: chartDelegateMeta.get(root)?.type() ?? "vbar",
|
|
1686
|
+
index: numOr(mark.getAttribute("data-chart-index")),
|
|
1687
|
+
seriesIndex: numOr(mark.getAttribute("data-chart-series")),
|
|
1688
|
+
seriesName: mark.getAttribute("data-chart-series-name") ?? void 0,
|
|
1689
|
+
label: mark.getAttribute("data-chart-label") ?? void 0,
|
|
1690
|
+
value: numOr(mark.getAttribute("data-chart-value")),
|
|
1691
|
+
target: mark,
|
|
1692
|
+
pointerX: pointer?.x,
|
|
1693
|
+
pointerY: pointer?.y,
|
|
1694
|
+
datum: datumMarkInfo.get(mark)?.datum
|
|
1695
|
+
};
|
|
1696
|
+
emitIcen(root, `icen:chart-${type}`, detail);
|
|
1697
|
+
}
|
|
1698
|
+
function bindDatumEvents(root) {
|
|
1699
|
+
const existing = datumEventControl.get(root);
|
|
1700
|
+
if (existing) return existing;
|
|
1701
|
+
const tipEnabled = () => chartDelegateMeta.get(root)?.tooltip() ?? true;
|
|
1702
|
+
const markOf = (e) => {
|
|
1703
|
+
const t = e.target;
|
|
1704
|
+
return t instanceof Element ? t.closest("[data-chart-mark]") : null;
|
|
1705
|
+
};
|
|
1706
|
+
let hoverMark = null;
|
|
1707
|
+
let suppressClick = false;
|
|
1708
|
+
const bindings = [];
|
|
1709
|
+
const on = (type, fn) => {
|
|
1710
|
+
const listener = (evt) => fn(evt);
|
|
1711
|
+
root.addEventListener(type, listener);
|
|
1712
|
+
bindings.push([type, listener]);
|
|
1713
|
+
};
|
|
1714
|
+
const enterMark = (mark, px, py) => {
|
|
1715
|
+
hoverMark = mark;
|
|
1716
|
+
mark.classList.add("is-hover");
|
|
1717
|
+
dispatchDatumEvent(root, "hover", mark, "enter", { x: px, y: py });
|
|
1718
|
+
if (tipEnabled()) {
|
|
1719
|
+
const tip = chartTooltip();
|
|
1720
|
+
tip.textContent = datumMarkInfo.get(mark)?.tip ?? tooltipText(mark);
|
|
1721
|
+
applyTooltipSizing(mark);
|
|
1722
|
+
tip.hidden = false;
|
|
1723
|
+
moveTooltip(px, py);
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
const leaveMark = (pointer) => {
|
|
1727
|
+
if (!hoverMark) return;
|
|
1728
|
+
hoverMark.classList.remove("is-hover");
|
|
1729
|
+
dispatchDatumEvent(root, "hover", hoverMark, "leave", pointer);
|
|
1730
|
+
hoverMark = null;
|
|
1731
|
+
chartTooltip().hidden = true;
|
|
1732
|
+
};
|
|
1733
|
+
on("pointerover", (e) => {
|
|
1734
|
+
const mark = markOf(e);
|
|
1735
|
+
if (!mark || mark === hoverMark) return;
|
|
1736
|
+
leaveMark({ x: e.clientX, y: e.clientY });
|
|
1737
|
+
enterMark(mark, e.clientX, e.clientY);
|
|
1738
|
+
});
|
|
1739
|
+
on("pointermove", (e) => {
|
|
1740
|
+
if (!hoverMark) return;
|
|
1741
|
+
dispatchDatumEvent(root, "hover", hoverMark, "move", { x: e.clientX, y: e.clientY });
|
|
1742
|
+
if (tipEnabled()) moveTooltip(e.clientX, e.clientY);
|
|
1743
|
+
});
|
|
1744
|
+
on("pointerout", (e) => {
|
|
1745
|
+
const mark = markOf(e);
|
|
1746
|
+
if (mark && mark === hoverMark) leaveMark({ x: e.clientX, y: e.clientY });
|
|
1747
|
+
});
|
|
1748
|
+
on("pointerleave", () => leaveMark());
|
|
1749
|
+
on("click", (e) => {
|
|
1750
|
+
if (suppressClick) {
|
|
1751
|
+
suppressClick = false;
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const mark = markOf(e);
|
|
1755
|
+
if (mark) dispatchDatumEvent(root, "click", mark, void 0, { x: e.clientX, y: e.clientY });
|
|
1756
|
+
});
|
|
1757
|
+
on("dblclick", (e) => {
|
|
1758
|
+
const mark = markOf(e);
|
|
1759
|
+
if (mark) dispatchDatumEvent(root, "dblclick", mark, void 0, { x: e.clientX, y: e.clientY });
|
|
1760
|
+
});
|
|
1761
|
+
on("contextmenu", (e) => {
|
|
1762
|
+
const mark = markOf(e);
|
|
1763
|
+
if (!mark) return;
|
|
1764
|
+
e.preventDefault();
|
|
1765
|
+
dispatchDatumEvent(root, "contextmenu", mark, void 0, { x: e.clientX, y: e.clientY });
|
|
1766
|
+
});
|
|
1767
|
+
const control = {
|
|
1768
|
+
unbind() {
|
|
1769
|
+
for (const [type, listener] of bindings) root.removeEventListener(type, listener);
|
|
1770
|
+
bindings.length = 0;
|
|
1771
|
+
if (hoverMark) {
|
|
1772
|
+
hoverMark.classList.remove("is-hover");
|
|
1773
|
+
hoverMark = null;
|
|
1774
|
+
}
|
|
1775
|
+
chartTooltip().hidden = true;
|
|
1776
|
+
datumEventControl.delete(root);
|
|
1777
|
+
},
|
|
1778
|
+
suppressNextClick() {
|
|
1779
|
+
suppressClick = true;
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
datumEventControl.set(root, control);
|
|
1783
|
+
return control;
|
|
1784
|
+
}
|
|
1785
|
+
function buildClusterModel(keys, labels) {
|
|
1786
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1787
|
+
for (const key of keys) {
|
|
1788
|
+
if (typeof key === "string" && key !== "") seen.add(key);
|
|
1789
|
+
}
|
|
1790
|
+
if (seen.size === 0) return null;
|
|
1791
|
+
const list = [...seen].sort();
|
|
1792
|
+
return {
|
|
1793
|
+
keys: list,
|
|
1794
|
+
indexOf: new Map(list.map((k, i) => [k, i])),
|
|
1795
|
+
label: (key) => labels?.[key] ?? key,
|
|
1796
|
+
color: (index) => paletteVar(index + 1)
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function registerDatumMark(mark, datum, tip) {
|
|
1800
|
+
datumMarkInfo.set(mark, { datum, tip });
|
|
1801
|
+
}
|
|
1802
|
+
function truncateText(text, max = 8) {
|
|
1803
|
+
const chars = Array.from(text);
|
|
1804
|
+
return chars.length > max ? `${chars.slice(0, max).join("")}\u2026` : text;
|
|
1805
|
+
}
|
|
1806
|
+
function svgClientPoint(svg, clientX, clientY, viewBoxWidth, viewBoxHeight) {
|
|
1807
|
+
const rect = svg.getBoundingClientRect();
|
|
1808
|
+
const scale = Math.min(rect.width / viewBoxWidth, rect.height / viewBoxHeight);
|
|
1809
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
1810
|
+
return { x: viewBoxWidth / 2, y: viewBoxHeight / 2 };
|
|
1811
|
+
}
|
|
1812
|
+
return {
|
|
1813
|
+
x: (clientX - rect.left - (rect.width - viewBoxWidth * scale) / 2) / scale,
|
|
1814
|
+
y: (clientY - rect.top - (rect.height - viewBoxHeight * scale) / 2) / scale
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
function normalizeGraphSpec(raw) {
|
|
1818
|
+
let src = raw;
|
|
1819
|
+
if (typeof src === "string") {
|
|
1820
|
+
try {
|
|
1821
|
+
src = JSON.parse(src);
|
|
1822
|
+
} catch {
|
|
1823
|
+
src = {};
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (!src || typeof src !== "object" || Array.isArray(src)) src = {};
|
|
1827
|
+
const s = src;
|
|
1828
|
+
const spec = { ...normalizeChartSpec(s), type: "graph", nodes: [], edges: [] };
|
|
1829
|
+
if (Array.isArray(s.nodes)) {
|
|
1830
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1831
|
+
for (const v of s.nodes) {
|
|
1832
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
1833
|
+
const r = v;
|
|
1834
|
+
const id = String(r.id ?? "").trim();
|
|
1835
|
+
if (!id || seen.has(id)) continue;
|
|
1836
|
+
seen.add(id);
|
|
1837
|
+
const node = { id };
|
|
1838
|
+
if (r.label != null) node.label = String(r.label);
|
|
1839
|
+
if (typeof r.cluster === "string" && r.cluster !== "") node.cluster = r.cluster;
|
|
1840
|
+
const weight = toNum(r.weight);
|
|
1841
|
+
if (weight != null) node.weight = weight;
|
|
1842
|
+
if (r.meta && typeof r.meta === "object" && !Array.isArray(r.meta)) {
|
|
1843
|
+
node.meta = { ...r.meta };
|
|
1844
|
+
}
|
|
1845
|
+
spec.nodes.push(node);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
const known = new Set(spec.nodes.map((n) => n.id));
|
|
1849
|
+
if (Array.isArray(s.edges)) {
|
|
1850
|
+
for (const v of s.edges) {
|
|
1851
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
1852
|
+
const r = v;
|
|
1853
|
+
const source = String(r.source ?? "").trim();
|
|
1854
|
+
const target = String(r.target ?? "").trim();
|
|
1855
|
+
if (!known.has(source) || !known.has(target) || source === target) continue;
|
|
1856
|
+
const edge = { source, target };
|
|
1857
|
+
const weight = toNum(r.weight);
|
|
1858
|
+
if (weight != null) edge.weight = weight;
|
|
1859
|
+
spec.edges.push(edge);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (s.directed === true) spec.directed = true;
|
|
1863
|
+
if (s.clusterLabels && typeof s.clusterLabels === "object" && !Array.isArray(s.clusterLabels)) {
|
|
1864
|
+
const labels = {};
|
|
1865
|
+
for (const [key, value] of Object.entries(s.clusterLabels)) {
|
|
1866
|
+
if (typeof value === "string" && value !== "") labels[key] = value;
|
|
1867
|
+
}
|
|
1868
|
+
spec.clusterLabels = labels;
|
|
1869
|
+
}
|
|
1870
|
+
return spec;
|
|
1871
|
+
}
|
|
1872
|
+
function normalizeMapSpec(raw) {
|
|
1873
|
+
let src = raw;
|
|
1874
|
+
if (typeof src === "string") {
|
|
1875
|
+
try {
|
|
1876
|
+
src = JSON.parse(src);
|
|
1877
|
+
} catch {
|
|
1878
|
+
src = {};
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (!src || typeof src !== "object" || Array.isArray(src)) src = {};
|
|
1882
|
+
const s = src;
|
|
1883
|
+
const spec = { ...normalizeChartSpec(s), type: "map", points: [] };
|
|
1884
|
+
if (Array.isArray(s.points)) {
|
|
1885
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1886
|
+
for (const v of s.points) {
|
|
1887
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
1888
|
+
const r = v;
|
|
1889
|
+
const id = String(r.id ?? "").trim();
|
|
1890
|
+
if (!id || seen.has(id)) continue;
|
|
1891
|
+
seen.add(id);
|
|
1892
|
+
const point = { id, x: toNum(r.x) ?? 0, y: toNum(r.y) ?? 0 };
|
|
1893
|
+
if (typeof r.cluster === "string" && r.cluster !== "") point.cluster = r.cluster;
|
|
1894
|
+
if (r.label != null) point.label = String(r.label);
|
|
1895
|
+
if (r.meta && typeof r.meta === "object" && !Array.isArray(r.meta)) {
|
|
1896
|
+
point.meta = { ...r.meta };
|
|
1897
|
+
}
|
|
1898
|
+
spec.points.push(point);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (s.clusters && typeof s.clusters === "object" && !Array.isArray(s.clusters)) {
|
|
1902
|
+
const labels = {};
|
|
1903
|
+
for (const [key, value] of Object.entries(s.clusters)) {
|
|
1904
|
+
if (typeof value === "string" && value !== "") labels[key] = value;
|
|
1905
|
+
}
|
|
1906
|
+
spec.clusters = labels;
|
|
1907
|
+
}
|
|
1908
|
+
return spec;
|
|
1909
|
+
}
|
|
1910
|
+
var GRAPH_W = 360;
|
|
1911
|
+
var GRAPH_H = 260;
|
|
1912
|
+
var GRAPH_PAD = 28;
|
|
1913
|
+
var GRAPH_LW = GRAPH_W - GRAPH_PAD * 2;
|
|
1914
|
+
var GRAPH_LH = GRAPH_H - GRAPH_PAD * 2;
|
|
1915
|
+
var graphMarkerSeq = 0;
|
|
1916
|
+
function renderGraph(el, raw) {
|
|
1917
|
+
const noop = () => void 0;
|
|
1918
|
+
let current = normalizeGraphSpec(raw);
|
|
1919
|
+
if (typeof document === "undefined") {
|
|
1920
|
+
return { el, spec: current, update: noop, on: () => noop, destroy: noop };
|
|
1921
|
+
}
|
|
1922
|
+
const legendState = { hidden: null };
|
|
1923
|
+
let layout = null;
|
|
1924
|
+
let svgRef = null;
|
|
1925
|
+
let nodeEls = [];
|
|
1926
|
+
let edgeList = [];
|
|
1927
|
+
let nodeRadii = [];
|
|
1928
|
+
let directed = false;
|
|
1929
|
+
let arrowId = "";
|
|
1930
|
+
const edgeEnds = (from, to) => {
|
|
1931
|
+
const ax = GRAPH_PAD + (layout ? layout.x[from] : 0);
|
|
1932
|
+
const ay = GRAPH_PAD + (layout ? layout.y[from] : 0);
|
|
1933
|
+
const bx = GRAPH_PAD + (layout ? layout.x[to] : 0);
|
|
1934
|
+
const by = GRAPH_PAD + (layout ? layout.y[to] : 0);
|
|
1935
|
+
if (!directed) {
|
|
1936
|
+
return { x1: ax.toFixed(1), y1: ay.toFixed(1), x2: bx.toFixed(1), y2: by.toFixed(1) };
|
|
1937
|
+
}
|
|
1938
|
+
const dx = bx - ax;
|
|
1939
|
+
const dy = by - ay;
|
|
1940
|
+
const d = Math.hypot(dx, dy) || 1;
|
|
1941
|
+
const cut = (nodeRadii[to] ?? 6) + 4;
|
|
1942
|
+
return {
|
|
1943
|
+
x1: ax.toFixed(1),
|
|
1944
|
+
y1: ay.toFixed(1),
|
|
1945
|
+
x2: (bx - dx / d * cut).toFixed(1),
|
|
1946
|
+
y2: (by - dy / d * cut).toFixed(1)
|
|
1947
|
+
};
|
|
1948
|
+
};
|
|
1949
|
+
const paint = (body) => {
|
|
1950
|
+
directed = current.directed === true;
|
|
1951
|
+
layout = null;
|
|
1952
|
+
svgRef = null;
|
|
1953
|
+
nodeEls = [];
|
|
1954
|
+
edgeList = [];
|
|
1955
|
+
arrowId = "";
|
|
1956
|
+
const nodes = current.nodes;
|
|
1957
|
+
if (nodes.length === 0) {
|
|
1958
|
+
nodeRadii = [];
|
|
1959
|
+
renderEmpty(body, current.emptyLabel);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
const clusters = buildClusterModel(nodes.map((n) => n.cluster), current.clusterLabels);
|
|
1963
|
+
const tone = toneVar(current.tone);
|
|
1964
|
+
const idOf = new Map(nodes.map((n, i) => [n.id, i]));
|
|
1965
|
+
const degree = nodes.map(() => 0);
|
|
1966
|
+
for (const e of current.edges) {
|
|
1967
|
+
const a = idOf.get(e.source);
|
|
1968
|
+
const b = idOf.get(e.target);
|
|
1969
|
+
if (a == null || b == null || a === b) continue;
|
|
1970
|
+
degree[a] += 1;
|
|
1971
|
+
degree[b] += 1;
|
|
1972
|
+
}
|
|
1973
|
+
const weights = nodes.map((n, i) => n.weight ?? degree[i]);
|
|
1974
|
+
let wMin = Infinity;
|
|
1975
|
+
let wMax = -Infinity;
|
|
1976
|
+
for (const w of weights) {
|
|
1977
|
+
if (w < wMin) wMin = w;
|
|
1978
|
+
if (w > wMax) wMax = w;
|
|
1979
|
+
}
|
|
1980
|
+
nodeRadii = weights.map((w) => 6 + (wMax > wMin ? (w - wMin) / (wMax - wMin) : 0.5) * 12);
|
|
1981
|
+
let eMin = Infinity;
|
|
1982
|
+
let eMax = -Infinity;
|
|
1983
|
+
for (const e of current.edges) {
|
|
1984
|
+
if (e.weight == null) continue;
|
|
1985
|
+
if (e.weight < eMin) eMin = e.weight;
|
|
1986
|
+
if (e.weight > eMax) eMax = e.weight;
|
|
1987
|
+
}
|
|
1988
|
+
const edgeWidth = (e) => e.weight == null ? 1 : 0.6 + (eMax > eMin ? (e.weight - eMin) / (eMax - eMin) : 0.5) * 1.9;
|
|
1989
|
+
const wrap = document.createElement("div");
|
|
1990
|
+
wrap.className = "chart-graph";
|
|
1991
|
+
if (current.tone) wrap.style.setProperty("--chart-tone", toneVar(current.tone));
|
|
1992
|
+
const svg = svgEl("svg", { viewBox: `0 0 ${GRAPH_W} ${GRAPH_H}`, role: "img" });
|
|
1993
|
+
svgRef = svg;
|
|
1994
|
+
svg.addEventListener("pointerdown", onNodePointerDown);
|
|
1995
|
+
const defs = svgEl("defs", {});
|
|
1996
|
+
if (directed) {
|
|
1997
|
+
arrowId = `icen-chart-graph-arrow-${++graphMarkerSeq}`;
|
|
1998
|
+
const marker = svgEl("marker", {
|
|
1999
|
+
id: arrowId,
|
|
2000
|
+
viewBox: "0 0 8 8",
|
|
2001
|
+
refX: "7",
|
|
2002
|
+
refY: "4",
|
|
2003
|
+
markerWidth: "6",
|
|
2004
|
+
markerHeight: "6",
|
|
2005
|
+
orient: "auto"
|
|
2006
|
+
});
|
|
2007
|
+
marker.appendChild(svgEl("path", { d: "M0.5,0.8 L7.5,4 L0.5,7.2 Z", fill: "var(--token-text-faint)" }));
|
|
2008
|
+
defs.appendChild(marker);
|
|
2009
|
+
}
|
|
2010
|
+
layout = buildLayoutState(nodes, current.edges, GRAPH_LW, GRAPH_LH);
|
|
2011
|
+
initRingLayout(layout, 0);
|
|
2012
|
+
relaxLayout(layout, 300);
|
|
2013
|
+
fitLayout(layout, 20);
|
|
2014
|
+
const edgeLayer = svgEl("g", { class: "chart-graph-edges" });
|
|
2015
|
+
const nodeLayer = svgEl("g", { class: "chart-graph-nodes" });
|
|
2016
|
+
for (const e of current.edges) {
|
|
2017
|
+
const from = idOf.get(e.source);
|
|
2018
|
+
const to = idOf.get(e.target);
|
|
2019
|
+
if (from == null || to == null || from === to) continue;
|
|
2020
|
+
const line = svgEl("line", {
|
|
2021
|
+
...edgeEnds(from, to),
|
|
2022
|
+
stroke: "var(--token-line-soft)",
|
|
2023
|
+
"stroke-width": edgeWidth(e).toFixed(2),
|
|
2024
|
+
...directed ? { "marker-end": `url(#${arrowId})` } : {}
|
|
2025
|
+
});
|
|
2026
|
+
line.classList.add("chart-graph-edge");
|
|
2027
|
+
const fromKey = nodes[from]?.cluster;
|
|
2028
|
+
const toKey = nodes[to]?.cluster;
|
|
2029
|
+
if (fromKey) line.setAttribute("data-edge-cluster-from", fromKey);
|
|
2030
|
+
if (toKey) line.setAttribute("data-edge-cluster-to", toKey);
|
|
2031
|
+
edgeList.push({ el: line, from, to });
|
|
2032
|
+
edgeLayer.appendChild(line);
|
|
2033
|
+
}
|
|
2034
|
+
nodes.forEach((node, i) => {
|
|
2035
|
+
const clusterKey = node.cluster;
|
|
2036
|
+
const ci = clusterKey != null && clusters ? clusters.indexOf.get(clusterKey) : void 0;
|
|
2037
|
+
const clusterName = ci != null && clusters && clusterKey != null ? clusters.label(clusterKey) : void 0;
|
|
2038
|
+
const fill = clusters ? ci != null ? clusters.color(ci) : "var(--token-text-faint)" : tone;
|
|
2039
|
+
const pos = { x: GRAPH_PAD + layout.x[i], y: GRAPH_PAD + layout.y[i] };
|
|
2040
|
+
const g = svgEl("g", {
|
|
2041
|
+
class: "chart-graph-node",
|
|
2042
|
+
transform: `translate(${pos.x.toFixed(1)},${pos.y.toFixed(1)})`
|
|
2043
|
+
});
|
|
2044
|
+
g.setAttribute("data-chart-node-id", node.id);
|
|
2045
|
+
setMarkData(g, {
|
|
2046
|
+
index: i,
|
|
2047
|
+
seriesIndex: ci,
|
|
2048
|
+
seriesName: clusterName,
|
|
2049
|
+
label: node.label ?? node.id,
|
|
2050
|
+
value: weights[i]
|
|
2051
|
+
});
|
|
2052
|
+
const dot = svgEl("circle", {
|
|
2053
|
+
class: "chart-graph-dot",
|
|
2054
|
+
r: (nodeRadii[i] ?? 6).toFixed(1),
|
|
2055
|
+
fill,
|
|
2056
|
+
"fill-opacity": "0.92",
|
|
2057
|
+
stroke: "var(--token-card)",
|
|
2058
|
+
"stroke-width": "1.5"
|
|
2059
|
+
});
|
|
2060
|
+
const hit = svgEl("circle", {
|
|
2061
|
+
class: "chart-graph-hit",
|
|
2062
|
+
r: String(Math.max((nodeRadii[i] ?? 6) + 4, 11)),
|
|
2063
|
+
fill: "transparent",
|
|
2064
|
+
"data-chart-hit": "1"
|
|
2065
|
+
});
|
|
2066
|
+
const labelText = truncateText(node.label ?? node.id);
|
|
2067
|
+
const head = clusterName ? `${clusterName} \xB7 ${labelText}` : labelText;
|
|
2068
|
+
const tip = weights[i] > 0 ? `${head}\uFF1A${weights[i]}` : head;
|
|
2069
|
+
const title = svgEl("title", {});
|
|
2070
|
+
title.textContent = tip;
|
|
2071
|
+
g.append(dot, hit, title);
|
|
2072
|
+
registerDatumMark(g, { id: node.id, label: node.label ?? node.id, cluster: clusterKey, meta: node.meta }, tip);
|
|
2073
|
+
nodeEls.push(g);
|
|
2074
|
+
nodeLayer.appendChild(g);
|
|
2075
|
+
});
|
|
2076
|
+
svg.append(defs, edgeLayer, nodeLayer);
|
|
2077
|
+
wrap.appendChild(svg);
|
|
2078
|
+
body.appendChild(wrap);
|
|
2079
|
+
if (clusters) {
|
|
2080
|
+
const legend = toggleLegend(
|
|
2081
|
+
clusters.keys.map((key, ci) => ({ key, label: clusters.label(key), tone: ci + 1, si: ci })),
|
|
2082
|
+
wrap
|
|
2083
|
+
);
|
|
2084
|
+
body.appendChild(legend);
|
|
2085
|
+
const syncEdges = () => {
|
|
2086
|
+
const hiddenOf = (key) => key != null && legend.querySelector(`[data-chart-key="${CSS.escape(key)}"]`)?.classList.contains("is-off") === true;
|
|
2087
|
+
for (const edge of edgeList) {
|
|
2088
|
+
edge.el.classList.toggle(
|
|
2089
|
+
"is-off",
|
|
2090
|
+
hiddenOf(edge.el.getAttribute("data-edge-cluster-from")) || hiddenOf(edge.el.getAttribute("data-edge-cluster-to"))
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
legend.addEventListener("click", syncEdges);
|
|
2095
|
+
legend.addEventListener("keydown", (e) => {
|
|
2096
|
+
if (e.key === "Enter" || e.key === " ") syncEdges();
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
const paintPositions = () => {
|
|
2101
|
+
if (!layout) return;
|
|
2102
|
+
for (let i = 0; i < nodeEls.length; i++) {
|
|
2103
|
+
nodeEls[i]?.setAttribute(
|
|
2104
|
+
"transform",
|
|
2105
|
+
`translate(${(GRAPH_PAD + layout.x[i]).toFixed(1)},${(GRAPH_PAD + layout.y[i]).toFixed(1)})`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
for (const edge of edgeList) {
|
|
2109
|
+
const ends = edgeEnds(edge.from, edge.to);
|
|
2110
|
+
edge.el.setAttribute("x1", ends.x1);
|
|
2111
|
+
edge.el.setAttribute("y1", ends.y1);
|
|
2112
|
+
edge.el.setAttribute("x2", ends.x2);
|
|
2113
|
+
edge.el.setAttribute("y2", ends.y2);
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
let drag = null;
|
|
2117
|
+
const applyDragFrame = () => {
|
|
2118
|
+
if (!drag || !layout || !svgRef) return;
|
|
2119
|
+
const p = svgClientPoint(svgRef, drag.px, drag.py, GRAPH_W, GRAPH_H);
|
|
2120
|
+
const i = drag.i;
|
|
2121
|
+
layout.x[i] = Math.min(GRAPH_LW, Math.max(0, p.x - GRAPH_PAD));
|
|
2122
|
+
layout.y[i] = Math.min(GRAPH_LH, Math.max(0, p.y - GRAPH_PAD));
|
|
2123
|
+
relaxLayout(layout, 20, i, 0.4);
|
|
2124
|
+
paintPositions();
|
|
2125
|
+
drag.raf = 0;
|
|
2126
|
+
};
|
|
2127
|
+
const onDragMove = (e) => {
|
|
2128
|
+
if (!drag) return;
|
|
2129
|
+
if (Math.abs(e.clientX - drag.px) + Math.abs(e.clientY - drag.py) > 2) drag.moved = true;
|
|
2130
|
+
drag.px = e.clientX;
|
|
2131
|
+
drag.py = e.clientY;
|
|
2132
|
+
if (drag.raf === 0) drag.raf = requestAnimationFrame(applyDragFrame);
|
|
2133
|
+
};
|
|
2134
|
+
const endDrag = () => {
|
|
2135
|
+
if (!drag) return;
|
|
2136
|
+
if (drag.raf !== 0) cancelAnimationFrame(drag.raf);
|
|
2137
|
+
nodeEls[drag.i]?.classList.remove("is-drag");
|
|
2138
|
+
svgRef?.classList.remove("is-dragging");
|
|
2139
|
+
if (drag.moved) eventsControl.suppressNextClick();
|
|
2140
|
+
drag = null;
|
|
2141
|
+
window.removeEventListener("pointermove", onDragMove);
|
|
2142
|
+
window.removeEventListener("pointerup", endDrag);
|
|
2143
|
+
window.removeEventListener("pointercancel", endDrag);
|
|
2144
|
+
};
|
|
2145
|
+
const onNodePointerDown = (e) => {
|
|
2146
|
+
if (e.button !== 0 || !layout) return;
|
|
2147
|
+
const target = e.target;
|
|
2148
|
+
const g = target instanceof Element ? target.closest(".chart-graph-node") : null;
|
|
2149
|
+
if (!g) return;
|
|
2150
|
+
const i = layout.index.get(g.getAttribute("data-chart-node-id") ?? "");
|
|
2151
|
+
if (i == null) return;
|
|
2152
|
+
e.preventDefault();
|
|
2153
|
+
endDrag();
|
|
2154
|
+
drag = { i, moved: false, raf: 0, px: e.clientX, py: e.clientY };
|
|
2155
|
+
g.classList.add("is-drag");
|
|
2156
|
+
svgRef?.classList.add("is-dragging");
|
|
2157
|
+
window.addEventListener("pointermove", onDragMove);
|
|
2158
|
+
window.addEventListener("pointerup", endDrag);
|
|
2159
|
+
window.addEventListener("pointercancel", endDrag);
|
|
2160
|
+
};
|
|
2161
|
+
chartDelegateMeta.set(el, { type: () => GRAPH_CHART_TYPE, tooltip: () => current.tooltip !== false });
|
|
2162
|
+
const eventsControl = bindDatumEvents(el);
|
|
2163
|
+
const render = () => {
|
|
2164
|
+
if (drag) endDrag();
|
|
2165
|
+
const clusters = buildClusterModel(current.nodes.map((n) => n.cluster), current.clusterLabels);
|
|
2166
|
+
if (current.legend !== void 0) legendState.hidden = current.legend === false;
|
|
2167
|
+
withChrome(el, current, clusters != null, paint, legendState);
|
|
2168
|
+
};
|
|
2169
|
+
render();
|
|
2170
|
+
return {
|
|
2171
|
+
el,
|
|
2172
|
+
spec: current,
|
|
2173
|
+
update(next) {
|
|
2174
|
+
current = normalizeGraphSpec(next);
|
|
2175
|
+
render();
|
|
2176
|
+
},
|
|
2177
|
+
on(name, listener) {
|
|
2178
|
+
const handler = (event) => {
|
|
2179
|
+
listener(event.detail, event);
|
|
2180
|
+
};
|
|
2181
|
+
el.addEventListener(`icen:chart-${name}`, handler);
|
|
2182
|
+
return () => el.removeEventListener(`icen:chart-${name}`, handler);
|
|
2183
|
+
},
|
|
2184
|
+
destroy() {
|
|
2185
|
+
if (drag) endDrag();
|
|
2186
|
+
el.textContent = "";
|
|
2187
|
+
eventsControl.unbind();
|
|
2188
|
+
chartTooltip().hidden = true;
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
var MAP_W = 360;
|
|
2193
|
+
var MAP_H = 220;
|
|
2194
|
+
var MAP_PAD = 14;
|
|
2195
|
+
function renderMap(el, raw) {
|
|
2196
|
+
const noop = () => void 0;
|
|
2197
|
+
let current = normalizeMapSpec(raw);
|
|
2198
|
+
if (typeof document === "undefined") {
|
|
2199
|
+
return { el, spec: current, update: noop, on: () => noop, destroy: noop };
|
|
2200
|
+
}
|
|
2201
|
+
const legendState = { hidden: null };
|
|
2202
|
+
const paint = (body) => {
|
|
2203
|
+
const points = current.points.filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
2204
|
+
if (points.length === 0) {
|
|
2205
|
+
renderEmpty(body, current.emptyLabel);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const clusters = buildClusterModel(points.map((p) => p.cluster), current.clusters);
|
|
2209
|
+
const tone = toneVar(current.tone);
|
|
2210
|
+
let xMin = Infinity;
|
|
2211
|
+
let xMax = -Infinity;
|
|
2212
|
+
let yMin = Infinity;
|
|
2213
|
+
let yMax = -Infinity;
|
|
2214
|
+
for (const p of points) {
|
|
2215
|
+
if (p.x < xMin) xMin = p.x;
|
|
2216
|
+
if (p.x > xMax) xMax = p.x;
|
|
2217
|
+
if (p.y < yMin) yMin = p.y;
|
|
2218
|
+
if (p.y > yMax) yMax = p.y;
|
|
2219
|
+
}
|
|
2220
|
+
if (xMax === xMin) xMax = xMin + 1;
|
|
2221
|
+
if (yMax === yMin) yMax = yMin + 1;
|
|
2222
|
+
const xAt = (x) => MAP_PAD + (x - xMin) / (xMax - xMin) * (MAP_W - MAP_PAD * 2);
|
|
2223
|
+
const yAt = (y) => MAP_H - MAP_PAD - (y - yMin) / (yMax - yMin) * (MAP_H - MAP_PAD * 2);
|
|
2224
|
+
const wrap = document.createElement("div");
|
|
2225
|
+
wrap.className = "chart-map";
|
|
2226
|
+
if (current.tone) wrap.style.setProperty("--chart-tone", toneVar(current.tone));
|
|
2227
|
+
const svg = svgEl("svg", { viewBox: `0 0 ${MAP_W} ${MAP_H}`, role: "img" });
|
|
2228
|
+
for (let i = 0; i <= 3; i++) {
|
|
2229
|
+
const y = MAP_PAD + i * (MAP_H - MAP_PAD * 2) / 3;
|
|
2230
|
+
svg.appendChild(svgEl("line", {
|
|
2231
|
+
x1: String(MAP_PAD),
|
|
2232
|
+
x2: String(MAP_W - MAP_PAD),
|
|
2233
|
+
y1: String(y),
|
|
2234
|
+
y2: String(y),
|
|
2235
|
+
stroke: "var(--token-line-soft)",
|
|
2236
|
+
"stroke-width": "0.75",
|
|
2237
|
+
"stroke-dasharray": "2 4"
|
|
2238
|
+
}));
|
|
2239
|
+
}
|
|
2240
|
+
for (let i = 0; i <= 4; i++) {
|
|
2241
|
+
const x = MAP_PAD + i * (MAP_W - MAP_PAD * 2) / 4;
|
|
2242
|
+
svg.appendChild(svgEl("line", {
|
|
2243
|
+
x1: String(x),
|
|
2244
|
+
x2: String(x),
|
|
2245
|
+
y1: String(MAP_PAD),
|
|
2246
|
+
y2: String(MAP_H - MAP_PAD),
|
|
2247
|
+
stroke: "var(--token-line-soft)",
|
|
2248
|
+
"stroke-width": "0.75",
|
|
2249
|
+
"stroke-dasharray": "2 4"
|
|
2250
|
+
}));
|
|
2251
|
+
}
|
|
2252
|
+
points.forEach((p, i) => {
|
|
2253
|
+
const ci = p.cluster != null && clusters ? clusters.indexOf.get(p.cluster) : void 0;
|
|
2254
|
+
const clusterName = ci != null && clusters && p.cluster != null ? clusters.label(p.cluster) : void 0;
|
|
2255
|
+
const color = clusters ? ci != null ? clusters.color(ci) : "var(--token-text-faint)" : tone;
|
|
2256
|
+
const label = p.label ?? p.id;
|
|
2257
|
+
const labelText = truncateText(label);
|
|
2258
|
+
const tip = clusterName ? `${clusterName} \xB7 ${labelText}` : labelText;
|
|
2259
|
+
const g = svgEl("g", {
|
|
2260
|
+
class: "chart-map-point",
|
|
2261
|
+
transform: `translate(${xAt(p.x).toFixed(1)},${yAt(p.y).toFixed(1)})`
|
|
2262
|
+
});
|
|
2263
|
+
g.setAttribute("data-chart-point-id", p.id);
|
|
2264
|
+
setMarkData(g, { index: i, seriesIndex: ci, seriesName: clusterName, label });
|
|
2265
|
+
const dot = svgEl("circle", {
|
|
2266
|
+
class: "chart-map-dot",
|
|
2267
|
+
r: "4",
|
|
2268
|
+
fill: color,
|
|
2269
|
+
"fill-opacity": "0.7",
|
|
2270
|
+
stroke: color,
|
|
2271
|
+
"stroke-width": "1"
|
|
2272
|
+
});
|
|
2273
|
+
const hit = svgEl("circle", { class: "chart-map-hit", r: "9", fill: "transparent", "data-chart-hit": "1" });
|
|
2274
|
+
const title = svgEl("title", {});
|
|
2275
|
+
title.textContent = tip;
|
|
2276
|
+
g.append(dot, hit, title);
|
|
2277
|
+
registerDatumMark(g, { id: p.id, label, cluster: p.cluster, meta: p.meta }, tip);
|
|
2278
|
+
svg.appendChild(g);
|
|
2279
|
+
});
|
|
2280
|
+
wrap.appendChild(svg);
|
|
2281
|
+
body.appendChild(wrap);
|
|
2282
|
+
if (clusters) {
|
|
2283
|
+
body.appendChild(
|
|
2284
|
+
toggleLegend(
|
|
2285
|
+
clusters.keys.map((key, ci) => ({ key, label: clusters.label(key), tone: ci + 1, si: ci })),
|
|
2286
|
+
wrap
|
|
2287
|
+
)
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
chartDelegateMeta.set(el, { type: () => MAP_CHART_TYPE, tooltip: () => current.tooltip !== false });
|
|
2292
|
+
const eventsControl = bindDatumEvents(el);
|
|
2293
|
+
const render = () => {
|
|
2294
|
+
const clusters = buildClusterModel(current.points.map((p) => p.cluster), current.clusters);
|
|
2295
|
+
if (current.legend !== void 0) legendState.hidden = current.legend === false;
|
|
2296
|
+
withChrome(el, current, clusters != null, paint, legendState);
|
|
2297
|
+
};
|
|
2298
|
+
render();
|
|
2299
|
+
return {
|
|
2300
|
+
el,
|
|
2301
|
+
spec: current,
|
|
2302
|
+
update(next) {
|
|
2303
|
+
current = normalizeMapSpec(next);
|
|
2304
|
+
render();
|
|
2305
|
+
},
|
|
2306
|
+
on(name, listener) {
|
|
2307
|
+
const handler = (event) => {
|
|
2308
|
+
listener(event.detail, event);
|
|
2309
|
+
};
|
|
2310
|
+
el.addEventListener(`icen:chart-${name}`, handler);
|
|
2311
|
+
return () => el.removeEventListener(`icen:chart-${name}`, handler);
|
|
2312
|
+
},
|
|
2313
|
+
destroy() {
|
|
2314
|
+
el.textContent = "";
|
|
2315
|
+
eventsControl.unbind();
|
|
1455
2316
|
chartTooltip().hidden = true;
|
|
1456
2317
|
}
|
|
1457
2318
|
};
|
|
1458
2319
|
}
|
|
1459
2320
|
export {
|
|
2321
|
+
bindChartEvents,
|
|
1460
2322
|
chartFormatValue,
|
|
1461
2323
|
inferChartType,
|
|
2324
|
+
layoutGraph,
|
|
1462
2325
|
normalizeChartSpec,
|
|
2326
|
+
registerChartTone,
|
|
1463
2327
|
renderArea,
|
|
1464
2328
|
renderCalendar,
|
|
1465
2329
|
renderChart,
|
|
1466
2330
|
renderDonut,
|
|
1467
2331
|
renderGauge,
|
|
2332
|
+
renderGraph,
|
|
1468
2333
|
renderHBar,
|
|
1469
2334
|
renderHeatmap,
|
|
1470
2335
|
renderLine,
|
|
2336
|
+
renderMap,
|
|
1471
2337
|
renderRadar,
|
|
1472
2338
|
renderScatter,
|
|
1473
2339
|
renderSparkline,
|