@frontera-sdk/chat-ui 1.50.79 → 1.50.80
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/dist/agent-picker.js +6 -6
- package/dist/{capture-NO5ZIGP6.js → capture-G4CDPSBH.js} +1 -1
- package/dist/chat.d.ts +1 -1
- package/dist/chat.js +4594 -941
- package/dist/{chunk-V44YVBRX.js → chunk-2P4IEVOQ.js} +29 -12
- package/dist/{chunk-WMN6BPPS.js → chunk-4FRTPVJT.js} +2 -2
- package/dist/{chunk-UTKQ6G2A.js → chunk-5B25DNOG.js} +7 -7
- package/dist/{chunk-FTLVGUG5.js → chunk-7AT2LRVW.js} +2640 -153
- package/dist/{chunk-FNIHHKOZ.js → chunk-C2TZSGKK.js} +1374 -186
- package/dist/{chunk-BDJFVRYR.js → chunk-C3HW5S2Z.js} +57 -18
- package/dist/chunk-CWAOLMFS.js +32 -0
- package/dist/{chunk-26L3ST3H.js → chunk-EFFOOCIK.js} +5975 -639
- package/dist/{chunk-J7EWRXEC.js → chunk-EXPHCRFP.js} +3 -3
- package/dist/{chunk-PH7AYVSS.js → chunk-FLOAEPRU.js} +4 -4
- package/dist/chunk-LGVMMBL3.js +33 -0
- package/dist/{chunk-IQXJEZM7.js → chunk-LOBDKA2D.js} +328 -32
- package/dist/{chunk-FDGBZFGN.js → chunk-PKNEC4LJ.js} +528 -12
- package/dist/{chunk-TSKFBMGC.js → chunk-PUMRNN3R.js} +117 -66
- package/dist/{chunk-S3ECTKA3.js → chunk-QBV7MAFV.js} +30 -4
- package/dist/{chunk-F6PEZE7A.js → chunk-T5SANYSX.js} +3 -10
- package/dist/{chunk-AFVU5VT6.js → chunk-TUDJ35AB.js} +1071 -87
- package/dist/{chunk-ROJM44GB.js → chunk-UWKO24VT.js} +28 -2
- package/dist/{chunk-KHF7OM3F.js → chunk-UZ6RDWCT.js} +4 -4
- package/dist/chunk-X3QVICX7.js +32 -0
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/header.d.ts +1 -1
- package/dist/header.js +15 -15
- package/dist/message.js +14 -15
- package/dist/new-session.d.ts +1 -1
- package/dist/new-session.js +7 -8
- package/dist/parts.js +6 -6
- package/dist/{pdf-deck-view-inner-AY4FYDAJ.js → pdf-deck-view-inner-KW6RIALH.js} +7 -8
- package/dist/{pdf-document-view-inner-NC5MBN7X.js → pdf-document-view-inner-AJEP5IJC.js} +6 -7
- package/dist/{pdf-viewer-inner-GPES3KG2.js → pdf-viewer-inner-CXWPRACH.js} +2 -2
- package/dist/pending-cards.js +7 -7
- package/dist/platform.css +1 -1
- package/package.json +7 -4
- package/dist/chunk-6BPHEBCF.js +0 -639
- package/dist/chunk-IXDM5R55.js +0 -23
- package/dist/chunk-XLX3L3P4.js +0 -43
- package/dist/vega-chart-block-NQADYNWQ.js +0 -2857
- package/dist/{chunk-7ADM3CKD.js → chunk-6JLWE7M3.js} +0 -0
- package/dist/{pdf-cover-thumbnail-inner-4N2Q5AAG.js → pdf-cover-thumbnail-inner-B2P6S25B.js} +0 -0
- package/dist/{types-BDqLaBfH.d.ts → types-BZhoOcz2.d.ts} +3 -3
- /package/dist/{univer-workbook-LSBVETAQ.js → univer-workbook-PENO65OQ.js} +0 -0
|
@@ -1,2857 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { ChartBlockSkeleton, RawJsonFallback, ChartReadout, ChartErrorBoundary, buildVegaTheme, PRIMARY_COLOR, format } from './chunk-6BPHEBCF.js';
|
|
3
|
-
import { useIsMobile } from './chunk-IXDM5R55.js';
|
|
4
|
-
import { ArrowsShowIcon } from './chunk-AFVU5VT6.js';
|
|
5
|
-
import './chunk-PR4QN5HX.js';
|
|
6
|
-
import { memo, useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
|
7
|
-
import vegaEmbed from 'vega-embed';
|
|
8
|
-
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
9
|
-
|
|
10
|
-
// src/vendor/lib/charts/util.ts
|
|
11
|
-
var FALLBACK_PALETTE = [
|
|
12
|
-
"#5b8dee",
|
|
13
|
-
"#3dab82",
|
|
14
|
-
"#e8883e",
|
|
15
|
-
"#9b7ef5",
|
|
16
|
-
"#e05c78",
|
|
17
|
-
"#3ab5cc",
|
|
18
|
-
"#a4b83a",
|
|
19
|
-
"#d47a4a",
|
|
20
|
-
"#748cd4",
|
|
21
|
-
"#4cad6a",
|
|
22
|
-
"#c47eb5",
|
|
23
|
-
"#d4a43e",
|
|
24
|
-
"#e87860",
|
|
25
|
-
"#4890c4",
|
|
26
|
-
"#8eb43e",
|
|
27
|
-
"#b45e78",
|
|
28
|
-
"#3ea8a0",
|
|
29
|
-
"#d4784a",
|
|
30
|
-
"#7858c4",
|
|
31
|
-
"#58a458",
|
|
32
|
-
"#e08898",
|
|
33
|
-
"#c4a858",
|
|
34
|
-
"#5888d4",
|
|
35
|
-
"#c46848"
|
|
36
|
-
];
|
|
37
|
-
function resolveCssVar(name, fallback = "") {
|
|
38
|
-
if (typeof window === "undefined" || typeof document === "undefined") return fallback;
|
|
39
|
-
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
40
|
-
return value || fallback;
|
|
41
|
-
}
|
|
42
|
-
function resolveCategoryPalette() {
|
|
43
|
-
const colors = [];
|
|
44
|
-
for (let i = 1; i <= 24; i++) {
|
|
45
|
-
const resolved = resolveCssVar(`--chart-${i}`);
|
|
46
|
-
colors.push(resolved || FALLBACK_PALETTE[i - 1]);
|
|
47
|
-
}
|
|
48
|
-
return colors;
|
|
49
|
-
}
|
|
50
|
-
function resolveHeatmapRange() {
|
|
51
|
-
const resolved = [];
|
|
52
|
-
for (let i = 1; i <= 9; i++) {
|
|
53
|
-
const v = resolveCssVar(`--chart-heatmap-${i}`);
|
|
54
|
-
if (v) resolved.push(v);
|
|
55
|
-
}
|
|
56
|
-
if (resolved.length > 0) return resolved;
|
|
57
|
-
return ["#eef2ff", "#e0e7ff", "#c7d2fe", "#a5b4fc", "#818cf8", "#6366f1", "#4f46e5", "#4338ca", "#3730a3"];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/vendor/lib/charts/vega/theme.ts
|
|
61
|
-
function isDarkBackground(hex) {
|
|
62
|
-
const clean = hex.replace("#", "");
|
|
63
|
-
if (clean.length !== 6) return false;
|
|
64
|
-
const r = parseInt(clean.slice(0, 2), 16);
|
|
65
|
-
const g = parseInt(clean.slice(2, 4), 16);
|
|
66
|
-
const b = parseInt(clean.slice(4, 6), 16);
|
|
67
|
-
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
68
|
-
return luminance < 0.5;
|
|
69
|
-
}
|
|
70
|
-
function resolveSebatiTheme() {
|
|
71
|
-
const foreground = resolveCssVar("--foreground", "#111111");
|
|
72
|
-
const mutedForeground = resolveCssVar("--muted-foreground", "#666666");
|
|
73
|
-
const border = resolveCssVar("--border", "#e5e7eb");
|
|
74
|
-
const background = resolveCssVar("--background", "#ffffff");
|
|
75
|
-
return {
|
|
76
|
-
background,
|
|
77
|
-
foreground,
|
|
78
|
-
mutedForeground,
|
|
79
|
-
border,
|
|
80
|
-
grid: border,
|
|
81
|
-
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
82
|
-
category: resolveCategoryPalette(),
|
|
83
|
-
heatmapRange: resolveHeatmapRange(),
|
|
84
|
-
isDark: isDarkBackground(background)
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
function getSebatiVegaConfig(theme) {
|
|
88
|
-
return {
|
|
89
|
-
background: "transparent",
|
|
90
|
-
font: theme.fontFamily,
|
|
91
|
-
padding: { top: 8, bottom: 8, left: 4, right: 12 },
|
|
92
|
-
arc: { fill: theme.category[0] },
|
|
93
|
-
bar: { fill: theme.category[0] },
|
|
94
|
-
line: { stroke: theme.category[0], strokeWidth: 1.5 },
|
|
95
|
-
area: { fillOpacity: 0.5, line: true, color: theme.category[0] },
|
|
96
|
-
point: { filled: true, size: 60 },
|
|
97
|
-
rect: { stroke: theme.background, strokeWidth: 1 },
|
|
98
|
-
axisY: {
|
|
99
|
-
gridColor: theme.grid,
|
|
100
|
-
labelColor: theme.mutedForeground,
|
|
101
|
-
tickColor: theme.border,
|
|
102
|
-
domain: false,
|
|
103
|
-
titleColor: theme.foreground,
|
|
104
|
-
titleFontWeight: 500
|
|
105
|
-
},
|
|
106
|
-
axisX: {
|
|
107
|
-
grid: false,
|
|
108
|
-
labelColor: theme.mutedForeground,
|
|
109
|
-
tickColor: theme.border,
|
|
110
|
-
domainColor: theme.border,
|
|
111
|
-
titleColor: theme.foreground,
|
|
112
|
-
titleFontWeight: 500
|
|
113
|
-
},
|
|
114
|
-
legend: {
|
|
115
|
-
labelColor: theme.foreground,
|
|
116
|
-
titleColor: theme.foreground,
|
|
117
|
-
symbolStrokeWidth: 0,
|
|
118
|
-
orient: "top"
|
|
119
|
-
},
|
|
120
|
-
view: { stroke: "transparent" },
|
|
121
|
-
range: {
|
|
122
|
-
category: theme.category,
|
|
123
|
-
heatmap: theme.heatmapRange
|
|
124
|
-
},
|
|
125
|
-
numberFormat: "s",
|
|
126
|
-
customFormatTypes: true
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// src/vendor/lib/charts/vega/tooltip.ts
|
|
131
|
-
var TOOLTIP_ID = "sebati-chart-tooltip";
|
|
132
|
-
var STYLE_ID = "sebati-chart-tooltip-style";
|
|
133
|
-
var TOOLTIP_CSS = `
|
|
134
|
-
#${TOOLTIP_ID} {
|
|
135
|
-
visibility: hidden;
|
|
136
|
-
position: fixed;
|
|
137
|
-
z-index: 10000;
|
|
138
|
-
pointer-events: none;
|
|
139
|
-
padding: 0;
|
|
140
|
-
font-family: inherit;
|
|
141
|
-
font-size: 12px;
|
|
142
|
-
line-height: 1.4;
|
|
143
|
-
border-radius: 6px;
|
|
144
|
-
box-shadow: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -2px rgba(0,0,0,.1);
|
|
145
|
-
background-color: var(--surface-overlay, var(--popover, #1a1a1a));
|
|
146
|
-
color: var(--popover-foreground, #e5e5e5);
|
|
147
|
-
border: 1px solid var(--border, #333);
|
|
148
|
-
max-width: 300px;
|
|
149
|
-
display: flex;
|
|
150
|
-
flex-direction: column;
|
|
151
|
-
overflow: hidden;
|
|
152
|
-
}
|
|
153
|
-
#${TOOLTIP_ID}.visible { visibility: visible; }
|
|
154
|
-
/* When pinned, the tooltip stays put and accepts clicks so users can copy
|
|
155
|
-
values or dismiss via the close button. The ring signals "this is locked
|
|
156
|
-
in" without shouting for attention. */
|
|
157
|
-
#${TOOLTIP_ID}.pinned {
|
|
158
|
-
pointer-events: auto;
|
|
159
|
-
box-shadow: 0 0 0 2px var(--ring, #4C40F8), 0 8px 24px -6px rgba(0,0,0,.25);
|
|
160
|
-
}
|
|
161
|
-
#${TOOLTIP_ID} .tt-close {
|
|
162
|
-
position: absolute;
|
|
163
|
-
top: 4px;
|
|
164
|
-
right: 4px;
|
|
165
|
-
width: 18px;
|
|
166
|
-
height: 18px;
|
|
167
|
-
display: none;
|
|
168
|
-
align-items: center;
|
|
169
|
-
justify-content: center;
|
|
170
|
-
border: 0;
|
|
171
|
-
padding: 0;
|
|
172
|
-
background: transparent;
|
|
173
|
-
color: var(--muted-foreground, #999);
|
|
174
|
-
border-radius: 4px;
|
|
175
|
-
cursor: pointer;
|
|
176
|
-
font-size: 14px;
|
|
177
|
-
line-height: 1;
|
|
178
|
-
}
|
|
179
|
-
#${TOOLTIP_ID}.pinned .tt-close { display: flex; }
|
|
180
|
-
#${TOOLTIP_ID}.pinned .tt-header { padding-right: 28px; }
|
|
181
|
-
#${TOOLTIP_ID} .tt-close:hover {
|
|
182
|
-
background: var(--surface-hover, rgba(255,255,255,.06));
|
|
183
|
-
color: var(--foreground, #e5e5e5);
|
|
184
|
-
}
|
|
185
|
-
#${TOOLTIP_ID} .tt-header {
|
|
186
|
-
font-weight: 500;
|
|
187
|
-
flex-shrink: 0;
|
|
188
|
-
padding: 8px 12px 4px;
|
|
189
|
-
border-bottom: 1px solid var(--border, #333);
|
|
190
|
-
}
|
|
191
|
-
#${TOOLTIP_ID} .tt-body {
|
|
192
|
-
overflow-y: auto;
|
|
193
|
-
padding: 4px 12px 8px;
|
|
194
|
-
scrollbar-width: thin;
|
|
195
|
-
}
|
|
196
|
-
#${TOOLTIP_ID} .tt-row {
|
|
197
|
-
display: flex;
|
|
198
|
-
align-items: center;
|
|
199
|
-
gap: 8px;
|
|
200
|
-
padding: 1px 0;
|
|
201
|
-
}
|
|
202
|
-
#${TOOLTIP_ID} .tt-dot {
|
|
203
|
-
width: 10px; height: 10px;
|
|
204
|
-
border-radius: 50%;
|
|
205
|
-
flex-shrink: 0;
|
|
206
|
-
}
|
|
207
|
-
#${TOOLTIP_ID} .tt-key { color: var(--muted-foreground, #999); }
|
|
208
|
-
#${TOOLTIP_ID} .tt-value { font-weight: 500; margin-left: auto; }
|
|
209
|
-
`;
|
|
210
|
-
function ensureStyle() {
|
|
211
|
-
if (typeof document === "undefined") return;
|
|
212
|
-
if (document.getElementById(STYLE_ID)) return;
|
|
213
|
-
const s = document.createElement("style");
|
|
214
|
-
s.id = STYLE_ID;
|
|
215
|
-
s.textContent = TOOLTIP_CSS;
|
|
216
|
-
document.head.appendChild(s);
|
|
217
|
-
}
|
|
218
|
-
function getTooltipEl() {
|
|
219
|
-
if (typeof document === "undefined") return null;
|
|
220
|
-
let el = document.getElementById(TOOLTIP_ID);
|
|
221
|
-
if (!el) {
|
|
222
|
-
el = document.createElement("div");
|
|
223
|
-
el.id = TOOLTIP_ID;
|
|
224
|
-
document.body.appendChild(el);
|
|
225
|
-
}
|
|
226
|
-
return el;
|
|
227
|
-
}
|
|
228
|
-
var fmtCache = /* @__PURE__ */ new Map();
|
|
229
|
-
function getD3Fmt(spec) {
|
|
230
|
-
let fn = fmtCache.get(spec);
|
|
231
|
-
if (!fn) {
|
|
232
|
-
try {
|
|
233
|
-
fn = format(spec);
|
|
234
|
-
} catch {
|
|
235
|
-
fn = (n) => n.toLocaleString();
|
|
236
|
-
}
|
|
237
|
-
fmtCache.set(spec, fn);
|
|
238
|
-
}
|
|
239
|
-
return fn;
|
|
240
|
-
}
|
|
241
|
-
function fmtValue(v, fieldType, format2) {
|
|
242
|
-
if (v == null) return "\u2014";
|
|
243
|
-
if (typeof v === "number" && Number.isNaN(v)) return "\u2014";
|
|
244
|
-
if (typeof v === "number") {
|
|
245
|
-
if (format2) return getD3Fmt(format2)(v);
|
|
246
|
-
if (fieldType === "temporal" || v > 9466848e5 && v < 41024448e5) {
|
|
247
|
-
try {
|
|
248
|
-
const d = new Date(v);
|
|
249
|
-
if (!isNaN(d.getTime())) return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
250
|
-
} catch {
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const abs = Math.abs(v);
|
|
254
|
-
if (abs >= 1e12) return (v / 1e12).toLocaleString(void 0, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + "T";
|
|
255
|
-
if (abs >= 1e9) return (v / 1e9).toLocaleString(void 0, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + "B";
|
|
256
|
-
if (abs >= 1e6) return (v / 1e6).toLocaleString(void 0, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + "M";
|
|
257
|
-
if (abs >= 1e3) return (v / 1e3).toLocaleString(void 0, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + "K";
|
|
258
|
-
if (Number.isInteger(v)) return v.toLocaleString();
|
|
259
|
-
return parseFloat(v.toPrecision(4)).toLocaleString();
|
|
260
|
-
}
|
|
261
|
-
if (typeof v === "string") {
|
|
262
|
-
if (/^\d{4}-\d{2}-\d{2}/.test(v)) {
|
|
263
|
-
try {
|
|
264
|
-
const d = new Date(v);
|
|
265
|
-
if (!isNaN(d.getTime())) return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
266
|
-
} catch {
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const siMatch = v.match(/^([+-]?[\d.]+)\s*([mµnpfakMGTPEZY])?$/);
|
|
270
|
-
if (siMatch && (siMatch[2] || siMatch[1].includes("."))) {
|
|
271
|
-
const SI = {
|
|
272
|
-
f: 1e-15,
|
|
273
|
-
p: 1e-12,
|
|
274
|
-
n: 1e-9,
|
|
275
|
-
\u00B5: 1e-6,
|
|
276
|
-
m: 1e-3,
|
|
277
|
-
k: 1e3,
|
|
278
|
-
M: 1e6,
|
|
279
|
-
G: 1e9,
|
|
280
|
-
T: 1e12,
|
|
281
|
-
P: 1e15,
|
|
282
|
-
E: 1e18
|
|
283
|
-
};
|
|
284
|
-
const base = parseFloat(siMatch[1]);
|
|
285
|
-
const multiplier = siMatch[2] ? SI[siMatch[2]] ?? 1 : 1;
|
|
286
|
-
const num = base * multiplier;
|
|
287
|
-
if (!isNaN(num)) return fmtValue(num, fieldType, format2);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return String(v);
|
|
291
|
-
}
|
|
292
|
-
function positionTooltip(el, event) {
|
|
293
|
-
const x = event.clientX + 12;
|
|
294
|
-
const y = event.clientY + 12;
|
|
295
|
-
const maxX = window.innerWidth - el.offsetWidth - 8;
|
|
296
|
-
const maxY = window.innerHeight - el.offsetHeight - 8;
|
|
297
|
-
el.style.left = `${Math.min(x, maxX)}px`;
|
|
298
|
-
el.style.top = `${Math.min(y, maxY)}px`;
|
|
299
|
-
}
|
|
300
|
-
var SCENE_KEYS = /* @__PURE__ */ new Set([
|
|
301
|
-
"datum",
|
|
302
|
-
"mark",
|
|
303
|
-
"bounds",
|
|
304
|
-
"stroke",
|
|
305
|
-
"strokeDash",
|
|
306
|
-
"strokeWidth",
|
|
307
|
-
"strokeOpacity",
|
|
308
|
-
"fill",
|
|
309
|
-
"fillOpacity",
|
|
310
|
-
"opacity",
|
|
311
|
-
"tooltip",
|
|
312
|
-
"description",
|
|
313
|
-
"exit",
|
|
314
|
-
"enter",
|
|
315
|
-
"update",
|
|
316
|
-
"x",
|
|
317
|
-
"y",
|
|
318
|
-
"x2",
|
|
319
|
-
"y2",
|
|
320
|
-
"width",
|
|
321
|
-
"height",
|
|
322
|
-
"size",
|
|
323
|
-
"shape",
|
|
324
|
-
"path",
|
|
325
|
-
"items",
|
|
326
|
-
"marktype",
|
|
327
|
-
"role",
|
|
328
|
-
"interactive",
|
|
329
|
-
"clip",
|
|
330
|
-
"zindex",
|
|
331
|
-
"_vgsid_"
|
|
332
|
-
]);
|
|
333
|
-
function pickDatum(item, value) {
|
|
334
|
-
let rawDatum = null;
|
|
335
|
-
if (item && typeof item === "object") {
|
|
336
|
-
const i = item;
|
|
337
|
-
if (i.datum && typeof i.datum === "object") {
|
|
338
|
-
const d = i.datum;
|
|
339
|
-
rawDatum = d.datum && typeof d.datum === "object" && !Array.isArray(d.datum) ? d.datum : i.datum;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
let tooltipValue = null;
|
|
343
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
344
|
-
const v = value;
|
|
345
|
-
if (!("mark" in v) && !("bounds" in v) && !("items" in v)) tooltipValue = v;
|
|
346
|
-
}
|
|
347
|
-
if (!tooltipValue && !rawDatum) return null;
|
|
348
|
-
if (!tooltipValue) return rawDatum;
|
|
349
|
-
if (!rawDatum) return tooltipValue;
|
|
350
|
-
const merged = { ...tooltipValue };
|
|
351
|
-
for (const key of Object.keys(tooltipValue)) {
|
|
352
|
-
if (key in rawDatum && typeof rawDatum[key] === "number") {
|
|
353
|
-
merged[key] = rawDatum[key];
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return merged;
|
|
357
|
-
}
|
|
358
|
-
function buildHtml(datum, color, config, fields, colorScale) {
|
|
359
|
-
const xField = config.x?.field;
|
|
360
|
-
const yFields = [config.y, config.y2].filter(Boolean).map((f) => f.field);
|
|
361
|
-
const colorField = config.color?.field;
|
|
362
|
-
const fallbackDot = color || "var(--chart-1)";
|
|
363
|
-
let header = "";
|
|
364
|
-
if (xField && xField in datum) {
|
|
365
|
-
const xMeta = fields?.[xField];
|
|
366
|
-
header = `<div class="tt-header">${fmtValue(datum[xField], config.x?.type, xMeta?.format)}</div>`;
|
|
367
|
-
}
|
|
368
|
-
let body = "";
|
|
369
|
-
if (colorField && colorField in datum && colorField !== xField) {
|
|
370
|
-
const label = fmtValue(datum[colorField]);
|
|
371
|
-
const dot = colorScale ? colorScale(String(datum[colorField])) || fallbackDot : fallbackDot;
|
|
372
|
-
body += `<div class="tt-row"><span class="tt-dot" style="background:${dot}"></span><span class="tt-key">${label}</span></div>`;
|
|
373
|
-
}
|
|
374
|
-
for (const yf of yFields) {
|
|
375
|
-
if (!(yf in datum)) continue;
|
|
376
|
-
const meta = fields?.[yf];
|
|
377
|
-
const label = meta?.displayName ?? config.y?.label ?? yf.replace(/_/g, " ");
|
|
378
|
-
body += `<div class="tt-row"><span class="tt-dot" style="background:${fallbackDot}"></span><span class="tt-key">${label}:</span><span class="tt-value">${fmtValue(datum[yf], config.y?.type, meta?.format)}</span></div>`;
|
|
379
|
-
}
|
|
380
|
-
if (!body) {
|
|
381
|
-
const skipFields = new Set([xField, colorField].filter(Boolean));
|
|
382
|
-
const keys = Object.keys(datum).filter(
|
|
383
|
-
(k) => !SCENE_KEYS.has(k) && !k.startsWith("_") && !k.startsWith("__") && !skipFields.has(k)
|
|
384
|
-
).sort((a, b) => {
|
|
385
|
-
const av = datum[a], bv = datum[b];
|
|
386
|
-
if (typeof av === "number" && typeof bv === "number") return bv - av;
|
|
387
|
-
if (typeof av === "number") return -1;
|
|
388
|
-
if (typeof bv === "number") return 1;
|
|
389
|
-
return 0;
|
|
390
|
-
});
|
|
391
|
-
for (const k of keys) {
|
|
392
|
-
const raw = datum[k];
|
|
393
|
-
if (raw === null || raw === void 0 || typeof raw === "object" && !(raw instanceof Date)) continue;
|
|
394
|
-
const meta = fields?.[k];
|
|
395
|
-
const label = meta?.displayName ?? k.replace(/_/g, " ");
|
|
396
|
-
const dot = colorScale ? colorScale(k) || fallbackDot : fallbackDot;
|
|
397
|
-
body += `<div class="tt-row"><span class="tt-dot" style="background:${dot}"></span><span class="tt-key">${label}:</span><span class="tt-value">${fmtValue(raw, meta?.type, meta?.format)}</span></div>`;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (!header && !body) return "";
|
|
401
|
-
return header + (body ? `<div class="tt-body">${body}</div>` : "");
|
|
402
|
-
}
|
|
403
|
-
var SebatiChartTooltipHandler = class {
|
|
404
|
-
options;
|
|
405
|
-
vegaView = null;
|
|
406
|
-
/** Latest html rendered by the hover path — captured at pin time so the
|
|
407
|
-
* pinned tooltip keeps showing the clicked bucket even if the mouse moves
|
|
408
|
-
* over another mark afterwards. */
|
|
409
|
-
lastHtml = null;
|
|
410
|
-
pinned = false;
|
|
411
|
-
constructor(options) {
|
|
412
|
-
this.options = options;
|
|
413
|
-
}
|
|
414
|
-
/** Call after vegaEmbed resolves so the handler can read the color scale. */
|
|
415
|
-
setView(view) {
|
|
416
|
-
this.vegaView = view;
|
|
417
|
-
}
|
|
418
|
-
getColorScale() {
|
|
419
|
-
try {
|
|
420
|
-
return this.vegaView?.scale("color") ?? null;
|
|
421
|
-
} catch {
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
/** Freeze the current tooltip in place; subsequent hover updates are ignored
|
|
426
|
-
* until `unpin()` is called. Adds a small close affordance so users can
|
|
427
|
-
* dismiss without having to click the chart again. */
|
|
428
|
-
pin() {
|
|
429
|
-
const el = getTooltipEl();
|
|
430
|
-
if (!el) return;
|
|
431
|
-
if (!el.classList.contains("visible")) return;
|
|
432
|
-
this.pinned = true;
|
|
433
|
-
el.classList.add("pinned");
|
|
434
|
-
if (!el.querySelector(".tt-close")) {
|
|
435
|
-
const btn = document.createElement("button");
|
|
436
|
-
btn.type = "button";
|
|
437
|
-
btn.className = "tt-close";
|
|
438
|
-
btn.setAttribute("aria-label", "Close tooltip");
|
|
439
|
-
btn.textContent = "\xD7";
|
|
440
|
-
btn.addEventListener("click", (e) => {
|
|
441
|
-
e.stopPropagation();
|
|
442
|
-
this.unpin();
|
|
443
|
-
});
|
|
444
|
-
el.appendChild(btn);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
unpin() {
|
|
448
|
-
this.pinned = false;
|
|
449
|
-
const el = getTooltipEl();
|
|
450
|
-
if (!el) return;
|
|
451
|
-
el.classList.remove("pinned", "visible");
|
|
452
|
-
}
|
|
453
|
-
isPinned() {
|
|
454
|
-
return this.pinned;
|
|
455
|
-
}
|
|
456
|
-
call = (_handler, event, item, value) => {
|
|
457
|
-
const el = getTooltipEl();
|
|
458
|
-
if (!el) return;
|
|
459
|
-
if (this.pinned) return;
|
|
460
|
-
if (!item || value == null) {
|
|
461
|
-
el.classList.remove("visible");
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
const datum = pickDatum(item, value);
|
|
465
|
-
if (!datum) {
|
|
466
|
-
el.classList.remove("visible");
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
const pickColor = (c) => c && c !== "transparent" ? c : "";
|
|
470
|
-
const itemAny = item;
|
|
471
|
-
const color = pickColor(itemAny?.fill) || pickColor(itemAny?.stroke) || pickColor(datum.fill) || pickColor(datum.stroke) || "var(--chart-1)";
|
|
472
|
-
let html;
|
|
473
|
-
if (this.options.formatter) {
|
|
474
|
-
const values = Object.entries(datum).filter(([k, v]) => !k.startsWith("_") && !SCENE_KEYS.has(k) && v != null && typeof v !== "object").map(([k, v]) => {
|
|
475
|
-
const meta = this.options.fields?.[k];
|
|
476
|
-
return {
|
|
477
|
-
field: k,
|
|
478
|
-
title: meta?.displayName ?? k,
|
|
479
|
-
value: v,
|
|
480
|
-
formatted: fmtValue(v, meta?.type)
|
|
481
|
-
};
|
|
482
|
-
});
|
|
483
|
-
if (values.length === 0) {
|
|
484
|
-
el.classList.remove("visible");
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
html = this.options.formatter(values);
|
|
488
|
-
} else {
|
|
489
|
-
html = buildHtml(datum, color, this.options.config, this.options.fields, this.getColorScale());
|
|
490
|
-
}
|
|
491
|
-
if (!html) {
|
|
492
|
-
el.classList.remove("visible");
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
ensureStyle();
|
|
496
|
-
el.innerHTML = html;
|
|
497
|
-
this.lastHtml = html;
|
|
498
|
-
el.classList.add("visible");
|
|
499
|
-
if (event) positionTooltip(el, event);
|
|
500
|
-
};
|
|
501
|
-
hide() {
|
|
502
|
-
if (this.pinned) return;
|
|
503
|
-
getTooltipEl()?.classList.remove("visible");
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
// src/vendor/lib/charts/vega/flex-embed-options.ts
|
|
508
|
-
function createFlexEmbedOptions(input) {
|
|
509
|
-
const {
|
|
510
|
-
theme,
|
|
511
|
-
chartConfig,
|
|
512
|
-
tooltipFormatter,
|
|
513
|
-
fields,
|
|
514
|
-
embedTooltip,
|
|
515
|
-
vegaEmbedConfigReplace
|
|
516
|
-
} = input;
|
|
517
|
-
const vlConfig = vegaEmbedConfigReplace ?? getSebatiVegaConfig(theme);
|
|
518
|
-
if (embedTooltip !== void 0) {
|
|
519
|
-
return {
|
|
520
|
-
options: {
|
|
521
|
-
actions: false,
|
|
522
|
-
renderer: "svg",
|
|
523
|
-
config: vlConfig,
|
|
524
|
-
tooltip: embedTooltip
|
|
525
|
-
},
|
|
526
|
-
handler: null
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
const handler = new SebatiChartTooltipHandler({
|
|
530
|
-
config: chartConfig,
|
|
531
|
-
formatter: tooltipFormatter,
|
|
532
|
-
fields
|
|
533
|
-
});
|
|
534
|
-
return {
|
|
535
|
-
options: {
|
|
536
|
-
actions: false,
|
|
537
|
-
renderer: "svg",
|
|
538
|
-
config: vlConfig,
|
|
539
|
-
tooltip: handler.call
|
|
540
|
-
},
|
|
541
|
-
handler
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
function VegaChartFlexEmbed({
|
|
545
|
-
spec,
|
|
546
|
-
theme,
|
|
547
|
-
config,
|
|
548
|
-
tooltipFormatter,
|
|
549
|
-
fields,
|
|
550
|
-
embedTooltip,
|
|
551
|
-
vegaEmbedConfigReplace,
|
|
552
|
-
tooltipHideElementId = TOOLTIP_ID,
|
|
553
|
-
onViewReady,
|
|
554
|
-
onEmbedError,
|
|
555
|
-
className
|
|
556
|
-
}) {
|
|
557
|
-
const ref = useRef(null);
|
|
558
|
-
const viewRef = useRef(null);
|
|
559
|
-
const onViewReadyRef = useRef(onViewReady);
|
|
560
|
-
const onEmbedErrorRef = useRef(onEmbedError);
|
|
561
|
-
onViewReadyRef.current = onViewReady;
|
|
562
|
-
onEmbedErrorRef.current = onEmbedError;
|
|
563
|
-
useEffect(() => {
|
|
564
|
-
const el = ref.current;
|
|
565
|
-
if (!el) return;
|
|
566
|
-
let cancelled = false;
|
|
567
|
-
const { options, handler } = createFlexEmbedOptions({
|
|
568
|
-
theme,
|
|
569
|
-
chartConfig: config,
|
|
570
|
-
tooltipFormatter,
|
|
571
|
-
fields,
|
|
572
|
-
embedTooltip,
|
|
573
|
-
vegaEmbedConfigReplace
|
|
574
|
-
});
|
|
575
|
-
vegaEmbed(el, spec, options).then((result) => {
|
|
576
|
-
if (cancelled) {
|
|
577
|
-
result.view.finalize();
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
handler?.setView(result.view);
|
|
581
|
-
viewRef.current = result.view;
|
|
582
|
-
onEmbedErrorRef.current?.(null);
|
|
583
|
-
onViewReadyRef.current?.(result.view);
|
|
584
|
-
}).catch((err) => {
|
|
585
|
-
console.error("[VegaChartFlexEmbed] embed failed", err);
|
|
586
|
-
onEmbedErrorRef.current?.(err instanceof Error ? err : new Error(String(err)));
|
|
587
|
-
});
|
|
588
|
-
return () => {
|
|
589
|
-
cancelled = true;
|
|
590
|
-
viewRef.current?.finalize();
|
|
591
|
-
viewRef.current = null;
|
|
592
|
-
};
|
|
593
|
-
}, [
|
|
594
|
-
spec,
|
|
595
|
-
theme,
|
|
596
|
-
config,
|
|
597
|
-
tooltipFormatter,
|
|
598
|
-
fields,
|
|
599
|
-
embedTooltip,
|
|
600
|
-
vegaEmbedConfigReplace
|
|
601
|
-
]);
|
|
602
|
-
useEffect(() => {
|
|
603
|
-
const el = ref.current;
|
|
604
|
-
if (!el || typeof ResizeObserver === "undefined") return;
|
|
605
|
-
const observer = new ResizeObserver((entries) => {
|
|
606
|
-
const view = viewRef.current;
|
|
607
|
-
if (!view) return;
|
|
608
|
-
const width = entries[0]?.contentRect.width;
|
|
609
|
-
if (width != null && width > 0) {
|
|
610
|
-
view.width(width).resize().runAsync();
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
observer.observe(el);
|
|
614
|
-
return () => observer.disconnect();
|
|
615
|
-
}, []);
|
|
616
|
-
useEffect(() => {
|
|
617
|
-
const el = ref.current;
|
|
618
|
-
if (!el) return;
|
|
619
|
-
const hide = () => {
|
|
620
|
-
if (typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches) {
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
document.getElementById(tooltipHideElementId)?.classList.remove("visible");
|
|
624
|
-
};
|
|
625
|
-
el.addEventListener("mouseleave", hide);
|
|
626
|
-
return () => el.removeEventListener("mouseleave", hide);
|
|
627
|
-
}, [tooltipHideElementId]);
|
|
628
|
-
return /* @__PURE__ */ jsx(
|
|
629
|
-
"div",
|
|
630
|
-
{
|
|
631
|
-
ref,
|
|
632
|
-
className: className ? `vega-chart h-full w-full ${className}` : "vega-chart h-full w-full"
|
|
633
|
-
}
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/vendor/plugins/vega-chart/mobile-spec.ts
|
|
638
|
-
var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
639
|
-
var ARC_LABEL_FONT_SIZE = 9;
|
|
640
|
-
var SCROLL_CATEGORY_THRESHOLD = 8;
|
|
641
|
-
var SCROLL_STEP_PX = 38;
|
|
642
|
-
function encodingOf(spec) {
|
|
643
|
-
if (isObj(spec.encoding)) return spec.encoding;
|
|
644
|
-
if (Array.isArray(spec.layer)) {
|
|
645
|
-
const layer = spec.layer.find(
|
|
646
|
-
(l) => isObj(l) && isObj(l.encoding)
|
|
647
|
-
);
|
|
648
|
-
if (isObj(layer)) return layer.encoding;
|
|
649
|
-
}
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
function seriesField(spec) {
|
|
653
|
-
const enc = encodingOf(spec);
|
|
654
|
-
const color = enc && isObj(enc.color) ? enc.color : null;
|
|
655
|
-
const field = color?.field;
|
|
656
|
-
return typeof field === "string" ? field : null;
|
|
657
|
-
}
|
|
658
|
-
function inlineValues(spec) {
|
|
659
|
-
const data = isObj(spec.data) ? spec.data : null;
|
|
660
|
-
const values = data?.values;
|
|
661
|
-
return Array.isArray(values) ? values.filter(isObj) : [];
|
|
662
|
-
}
|
|
663
|
-
function categoryCount(spec) {
|
|
664
|
-
const enc = encodingOf(spec);
|
|
665
|
-
const x = enc && isObj(enc.x) ? enc.x : null;
|
|
666
|
-
const field = typeof x?.field === "string" ? x.field : null;
|
|
667
|
-
const type = typeof x?.type === "string" ? x.type : null;
|
|
668
|
-
if (!field || type === "quantitative" || type === "temporal") return 0;
|
|
669
|
-
const seen = /* @__PURE__ */ new Set();
|
|
670
|
-
for (const row of inlineValues(spec)) {
|
|
671
|
-
const v = row[field];
|
|
672
|
-
if (v != null) seen.add(String(v));
|
|
673
|
-
}
|
|
674
|
-
return seen.size;
|
|
675
|
-
}
|
|
676
|
-
function isArcSpec(spec) {
|
|
677
|
-
const markOf = (v) => {
|
|
678
|
-
if (typeof v === "string") return v;
|
|
679
|
-
if (isObj(v) && typeof v.type === "string") return v.type;
|
|
680
|
-
return null;
|
|
681
|
-
};
|
|
682
|
-
if (markOf(spec.mark) === "arc") return true;
|
|
683
|
-
if (Array.isArray(spec.layer)) {
|
|
684
|
-
return spec.layer.some(
|
|
685
|
-
(l) => isObj(l) && markOf(l.mark) === "arc"
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
function chartTitle(spec) {
|
|
691
|
-
const t = spec.title;
|
|
692
|
-
if (typeof t === "string") return t.trim() || null;
|
|
693
|
-
if (isObj(t) && typeof t.text === "string") return t.text.trim() || null;
|
|
694
|
-
return null;
|
|
695
|
-
}
|
|
696
|
-
function isFlatSummary(summary) {
|
|
697
|
-
if (!summary || summary.kind !== "buckets" || summary.buckets.length < 2) {
|
|
698
|
-
return false;
|
|
699
|
-
}
|
|
700
|
-
return summary.buckets.every((b) => Object.keys(b.values).length <= 1);
|
|
701
|
-
}
|
|
702
|
-
function fieldOf(enc, channel) {
|
|
703
|
-
const c = enc && isObj(enc[channel]) ? enc[channel] : null;
|
|
704
|
-
return typeof c?.field === "string" ? c.field : null;
|
|
705
|
-
}
|
|
706
|
-
function sortChannelField(sort, enc) {
|
|
707
|
-
if (typeof sort !== "string") return null;
|
|
708
|
-
const channel = sort.replace(/^[-+]/, "");
|
|
709
|
-
const def = isObj(enc[channel]) ? enc[channel] : null;
|
|
710
|
-
return typeof def?.field === "string" ? def.field : null;
|
|
711
|
-
}
|
|
712
|
-
function descendingSort(sort) {
|
|
713
|
-
if (sort === "descending") return true;
|
|
714
|
-
if (typeof sort === "string" && sort.startsWith("-")) return true;
|
|
715
|
-
return isObj(sort) && sort.order === "descending";
|
|
716
|
-
}
|
|
717
|
-
function orderBuckets(buckets, enc, xField, valueField) {
|
|
718
|
-
const xDef = xField != null ? Object.values(enc).find(
|
|
719
|
-
(d) => isObj(d) && d.field === xField
|
|
720
|
-
) ?? null : null;
|
|
721
|
-
const sort = xDef?.sort;
|
|
722
|
-
if (sort === null) return buckets;
|
|
723
|
-
if (Array.isArray(sort)) {
|
|
724
|
-
const order = new Map(sort.map((v, i) => [String(v), i]));
|
|
725
|
-
return [...buckets].sort(
|
|
726
|
-
(a, b) => (order.get(a.key) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.key) ?? Number.MAX_SAFE_INTEGER)
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
|
-
const dir = descendingSort(sort) || xDef && isObj(xDef.scale) && xDef.scale.reverse === true ? -1 : 1;
|
|
730
|
-
const sortField = isObj(sort) ? typeof sort.field === "string" ? sort.field : null : sortChannelField(sort, enc);
|
|
731
|
-
if (sortField != null && sortField === valueField) {
|
|
732
|
-
return [...buckets].sort((a, b) => (a.total - b.total) * dir);
|
|
733
|
-
}
|
|
734
|
-
const keyed = buckets.map((b) => ({ b, t: toTime(b.key) }));
|
|
735
|
-
if (keyed.every((k) => k.t != null)) {
|
|
736
|
-
return keyed.sort((a, b) => (a.t - b.t) * dir).map((k) => k.b);
|
|
737
|
-
}
|
|
738
|
-
return [...buckets].sort(
|
|
739
|
-
(a, b) => a.key.localeCompare(b.key, void 0, { numeric: true }) * dir
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
function summarize(spec) {
|
|
743
|
-
const enc = encodingOf(spec);
|
|
744
|
-
if (!enc) return null;
|
|
745
|
-
const rows = inlineValues(spec);
|
|
746
|
-
if (rows.length === 0) return null;
|
|
747
|
-
const series = seriesField(spec);
|
|
748
|
-
const valueField = fieldOf(enc, "theta") ?? (isObj(enc.y) && enc.y.type === "quantitative" ? fieldOf(enc, "y") : null) ?? (isObj(enc.x) && enc.x.type === "quantitative" ? fieldOf(enc, "x") : null);
|
|
749
|
-
if (!valueField) return null;
|
|
750
|
-
const valueDef = [enc.theta, enc.y, enc.x].find(
|
|
751
|
-
(d) => isObj(d) && d.field === valueField
|
|
752
|
-
);
|
|
753
|
-
const axis = isObj(valueDef?.axis) ? valueDef.axis : null;
|
|
754
|
-
const valueLabel = (typeof valueDef?.title === "string" ? valueDef.title : null) ?? (typeof axis?.title === "string" ? axis.title : null) ?? valueField;
|
|
755
|
-
const xField = fieldOf(enc, "x") === valueField ? fieldOf(enc, "y") : fieldOf(enc, "x");
|
|
756
|
-
const isComposition = !xField || fieldOf(enc, "theta") != null;
|
|
757
|
-
const num = (v) => {
|
|
758
|
-
const n = typeof v === "number" ? v : Number(v);
|
|
759
|
-
return Number.isFinite(n) ? n : 0;
|
|
760
|
-
};
|
|
761
|
-
const seen = [];
|
|
762
|
-
const byBucket = /* @__PURE__ */ new Map();
|
|
763
|
-
for (const row of rows) {
|
|
764
|
-
const bucketKey = isComposition ? "all" : String(row[xField] ?? "");
|
|
765
|
-
const name = series ? String(row[series] ?? "") : valueField;
|
|
766
|
-
if (!seen.includes(name)) seen.push(name);
|
|
767
|
-
let bucket = byBucket.get(bucketKey);
|
|
768
|
-
if (!bucket) {
|
|
769
|
-
bucket = { key: bucketKey, total: 0, values: {} };
|
|
770
|
-
byBucket.set(bucketKey, bucket);
|
|
771
|
-
}
|
|
772
|
-
const v = num(row[valueField]);
|
|
773
|
-
bucket.values[name] = (bucket.values[name] ?? 0) + v;
|
|
774
|
-
bucket.total += v;
|
|
775
|
-
}
|
|
776
|
-
const buckets = orderBuckets([...byBucket.values()], enc, xField, valueField);
|
|
777
|
-
return {
|
|
778
|
-
kind: isComposition ? "composition" : "buckets",
|
|
779
|
-
seriesField: series,
|
|
780
|
-
valueLabel,
|
|
781
|
-
bucketField: isComposition ? null : xField ?? null,
|
|
782
|
-
bucketType: isComposition || !xField ? null : channelType(enc, xField),
|
|
783
|
-
series: seen,
|
|
784
|
-
buckets,
|
|
785
|
-
total: buckets.reduce((s, b) => s + b.total, 0)
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
var DIMMED_OPACITY = 0.18;
|
|
789
|
-
function channelType(enc, field) {
|
|
790
|
-
for (const def of Object.values(enc)) {
|
|
791
|
-
if (isObj(def) && def.field === field && typeof def.type === "string") {
|
|
792
|
-
return def.type;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return null;
|
|
796
|
-
}
|
|
797
|
-
function selectionTest(enc, field, value) {
|
|
798
|
-
const ref = `datum[${JSON.stringify(field)}]`;
|
|
799
|
-
if (channelType(enc, field) === "temporal") {
|
|
800
|
-
const ms = Date.parse(value);
|
|
801
|
-
if (!Number.isNaN(ms)) return `time(${ref}) === ${ms}`;
|
|
802
|
-
}
|
|
803
|
-
return `toString(${ref}) === ${JSON.stringify(value)}`;
|
|
804
|
-
}
|
|
805
|
-
function markType(target) {
|
|
806
|
-
const m = target.mark;
|
|
807
|
-
if (typeof m === "string") return m;
|
|
808
|
-
if (isObj(m) && typeof m.type === "string") return m.type;
|
|
809
|
-
return null;
|
|
810
|
-
}
|
|
811
|
-
var BACKDROP_OPACITY = 0.28;
|
|
812
|
-
var SPANNING_MARKS = /* @__PURE__ */ new Set(["line", "area", "trail"]);
|
|
813
|
-
function withOpacity(target, field, value) {
|
|
814
|
-
const enc = isObj(target.encoding) ? target.encoding : null;
|
|
815
|
-
if (!enc) return target;
|
|
816
|
-
if (Array.isArray(target.params) && target.params.length > 0) return target;
|
|
817
|
-
const existing = isObj(enc.opacity) ? enc.opacity : null;
|
|
818
|
-
const condition = existing && isObj(existing.condition) ? existing.condition : null;
|
|
819
|
-
if (condition && typeof condition.param === "string") return target;
|
|
820
|
-
const opacity = SPANNING_MARKS.has(markType(target) ?? "") ? { value: BACKDROP_OPACITY } : {
|
|
821
|
-
condition: { test: selectionTest(enc, field, value), value: 1 },
|
|
822
|
-
value: DIMMED_OPACITY
|
|
823
|
-
};
|
|
824
|
-
return { ...target, encoding: { ...enc, opacity } };
|
|
825
|
-
}
|
|
826
|
-
function highlightSelection(spec, field, value) {
|
|
827
|
-
if (Array.isArray(spec.layer)) {
|
|
828
|
-
return {
|
|
829
|
-
...spec,
|
|
830
|
-
layer: spec.layer.map(
|
|
831
|
-
(l) => isObj(l) ? withOpacity(l, field, value) : l
|
|
832
|
-
)
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
return withOpacity(spec, field, value);
|
|
836
|
-
}
|
|
837
|
-
function adaptSpecForMobile(spec, landscape = false, cardChrome = false) {
|
|
838
|
-
const categories = categoryCount(spec);
|
|
839
|
-
const scrolls = categories > SCROLL_CATEGORY_THRESHOLD;
|
|
840
|
-
const next = { ...spec };
|
|
841
|
-
if (scrolls) {
|
|
842
|
-
next.width = { step: SCROLL_STEP_PX };
|
|
843
|
-
} else if (next.width == null || next.width === "container") {
|
|
844
|
-
next.width = "container";
|
|
845
|
-
}
|
|
846
|
-
const arc = isArcSpec(spec);
|
|
847
|
-
const capped = arc ? cardChrome ? 210 : 250 : cardChrome ? landscape ? 130 : 150 : landscape ? 200 : 220;
|
|
848
|
-
const declared = typeof next.height === "number" ? next.height : null;
|
|
849
|
-
next.height = declared != null ? Math.min(declared, capped) : capped;
|
|
850
|
-
if (arc && Array.isArray(next.layer)) {
|
|
851
|
-
const outer = next.height / 2 - 4;
|
|
852
|
-
next.layer = next.layer.map((layer) => {
|
|
853
|
-
if (!isObj(layer) || !isObj(layer.mark)) return layer;
|
|
854
|
-
const mark = { ...layer.mark };
|
|
855
|
-
if (mark.type === "arc") {
|
|
856
|
-
const declaredInner = typeof mark.innerRadius === "number" ? mark.innerRadius : null;
|
|
857
|
-
mark.outerRadius = outer;
|
|
858
|
-
mark.innerRadius = declaredInner != null ? Math.min(declaredInner, outer * 0.62) : outer * 0.58;
|
|
859
|
-
} else if (mark.type === "text") {
|
|
860
|
-
mark.fontSize = ARC_LABEL_FONT_SIZE;
|
|
861
|
-
mark.fontWeight = 500;
|
|
862
|
-
if (typeof mark.radius === "number") {
|
|
863
|
-
mark.radius = outer + 12;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
return { ...layer, mark };
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
if (arc && !Array.isArray(next.layer)) {
|
|
870
|
-
const outer = next.height / 2 - 4;
|
|
871
|
-
const markObj = isObj(next.mark) ? { ...next.mark } : { type: typeof next.mark === "string" ? next.mark : "arc" };
|
|
872
|
-
const declaredInner = typeof markObj.innerRadius === "number" ? markObj.innerRadius : null;
|
|
873
|
-
markObj.outerRadius = outer;
|
|
874
|
-
markObj.innerRadius = declaredInner != null ? Math.min(declaredInner, outer * 0.62) : outer * 0.58;
|
|
875
|
-
next.mark = markObj;
|
|
876
|
-
}
|
|
877
|
-
next.autosize = isObj(next.autosize) ? next.autosize : { type: scrolls ? "pad" : "fit-x", contains: "padding" };
|
|
878
|
-
const config = isObj(next.config) ? { ...next.config } : {};
|
|
879
|
-
config.legend = {
|
|
880
|
-
...isObj(config.legend) ? config.legend : {},
|
|
881
|
-
orient: "bottom",
|
|
882
|
-
direction: "horizontal",
|
|
883
|
-
labelLimit: 96,
|
|
884
|
-
labelFontSize: 11,
|
|
885
|
-
titleFontSize: 11,
|
|
886
|
-
symbolSize: 60,
|
|
887
|
-
offset: 6
|
|
888
|
-
};
|
|
889
|
-
const axisCommon = {
|
|
890
|
-
labelFontSize: 10.5,
|
|
891
|
-
titleFontSize: 11,
|
|
892
|
-
labelLimit: 72,
|
|
893
|
-
labelOverlap: "greedy"
|
|
894
|
-
};
|
|
895
|
-
config.axis = { ...isObj(config.axis) ? config.axis : {}, ...axisCommon };
|
|
896
|
-
config.axisX = {
|
|
897
|
-
...isObj(config.axisX) ? config.axisX : {},
|
|
898
|
-
// Horizontal labels only — rotated text at 10px is unreadable, and if the
|
|
899
|
-
// labels don't fit the chart scrolls instead.
|
|
900
|
-
labelAngle: 0,
|
|
901
|
-
...scrolls ? {} : { tickCount: 5 }
|
|
902
|
-
};
|
|
903
|
-
config.axisY = {
|
|
904
|
-
...isObj(config.axisY) ? config.axisY : {},
|
|
905
|
-
tickCount: 4,
|
|
906
|
-
labelLimit: 56
|
|
907
|
-
};
|
|
908
|
-
config.view = { ...isObj(config.view) ? config.view : {}, stroke: null };
|
|
909
|
-
if (cardChrome) {
|
|
910
|
-
next.title = null;
|
|
911
|
-
config.text = {
|
|
912
|
-
...isObj(config.text) ? config.text : {},
|
|
913
|
-
fontSize: arc ? ARC_LABEL_FONT_SIZE : 10,
|
|
914
|
-
fontWeight: 500
|
|
915
|
-
};
|
|
916
|
-
config.axis = { ...isObj(config.axis) ? config.axis : {}, title: null };
|
|
917
|
-
config.legend = { ...isObj(config.legend) ? config.legend : {}, disable: true };
|
|
918
|
-
const enc = encodingOf(next);
|
|
919
|
-
if (enc && isObj(enc.color)) {
|
|
920
|
-
const color = { ...enc.color, legend: null };
|
|
921
|
-
if (isObj(next.encoding)) next.encoding = { ...next.encoding, color };
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
next.config = config;
|
|
925
|
-
next.padding = isObj(next.padding) ? next.padding : 4;
|
|
926
|
-
return { spec: next, scrolls };
|
|
927
|
-
}
|
|
928
|
-
function dateCandidates(d) {
|
|
929
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
930
|
-
const iso = d.toISOString();
|
|
931
|
-
return [
|
|
932
|
-
String(d),
|
|
933
|
-
iso,
|
|
934
|
-
iso.slice(0, 10),
|
|
935
|
-
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
|
|
936
|
-
String(d.getTime())
|
|
937
|
-
];
|
|
938
|
-
}
|
|
939
|
-
function keyCandidates(v) {
|
|
940
|
-
if (v == null) return [];
|
|
941
|
-
if (v instanceof Date) return Number.isNaN(v.getTime()) ? [] : dateCandidates(v);
|
|
942
|
-
if (typeof v === "number" && Math.abs(v) > 1e11) {
|
|
943
|
-
const d = new Date(v);
|
|
944
|
-
if (!Number.isNaN(d.getTime())) return [String(v), ...dateCandidates(d)];
|
|
945
|
-
}
|
|
946
|
-
return [String(v)];
|
|
947
|
-
}
|
|
948
|
-
var datumMatches = (value, key) => keyCandidates(value).includes(key);
|
|
949
|
-
function keyForDatum(datum, summary) {
|
|
950
|
-
if (summary.kind === "composition") {
|
|
951
|
-
const field2 = summary.seriesField;
|
|
952
|
-
if (field2 && datum[field2] != null) {
|
|
953
|
-
const hit = summary.series.find((name) => datumMatches(datum[field2], name));
|
|
954
|
-
if (hit) return hit;
|
|
955
|
-
}
|
|
956
|
-
return summary.series.find(
|
|
957
|
-
(name) => Object.values(datum).some((v) => datumMatches(v, name))
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
const field = summary.bucketField;
|
|
961
|
-
if (field && datum[field] != null) {
|
|
962
|
-
const hit = summary.buckets.find((b) => datumMatches(datum[field], b.key));
|
|
963
|
-
if (hit) return hit.key;
|
|
964
|
-
}
|
|
965
|
-
return summary.buckets.find(
|
|
966
|
-
(b) => Object.values(datum).some((v) => datumMatches(v, b.key))
|
|
967
|
-
)?.key;
|
|
968
|
-
}
|
|
969
|
-
function toTime(v) {
|
|
970
|
-
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v.getTime();
|
|
971
|
-
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
972
|
-
if (typeof v === "string") {
|
|
973
|
-
const n = Number(v);
|
|
974
|
-
if (Number.isFinite(n) && v.trim() !== "") return n;
|
|
975
|
-
const t = Date.parse(v);
|
|
976
|
-
return Number.isNaN(t) ? null : t;
|
|
977
|
-
}
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
function bucketAtValue(summary, value) {
|
|
981
|
-
if (value == null || summary.buckets.length === 0) return void 0;
|
|
982
|
-
const exact = summary.buckets.find((b) => datumMatches(value, b.key));
|
|
983
|
-
if (exact) return exact.key;
|
|
984
|
-
const target = toTime(value);
|
|
985
|
-
if (target == null) return void 0;
|
|
986
|
-
let best;
|
|
987
|
-
let bestDistance = Infinity;
|
|
988
|
-
for (const b of summary.buckets) {
|
|
989
|
-
const t = toTime(b.key);
|
|
990
|
-
if (t == null) continue;
|
|
991
|
-
const d = Math.abs(t - target);
|
|
992
|
-
if (d < bestDistance) {
|
|
993
|
-
bestDistance = d;
|
|
994
|
-
best = b.key;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
return best;
|
|
998
|
-
}
|
|
999
|
-
function isPointerParam(p) {
|
|
1000
|
-
if (!isObj(p)) return false;
|
|
1001
|
-
const select = isObj(p.select) ? p.select : null;
|
|
1002
|
-
const on = select?.on;
|
|
1003
|
-
return typeof on === "string" && on.startsWith("pointer");
|
|
1004
|
-
}
|
|
1005
|
-
function conditionalOn(enc, removed) {
|
|
1006
|
-
return Object.values(enc).filter((def) => {
|
|
1007
|
-
if (!isObj(def) || !isObj(def.condition)) return false;
|
|
1008
|
-
const param = def.condition.param;
|
|
1009
|
-
return typeof param === "string" && removed.has(param);
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
function withoutPointerParams(target) {
|
|
1013
|
-
if (!Array.isArray(target.params)) return target;
|
|
1014
|
-
const next = { ...target };
|
|
1015
|
-
const keep = target.params.filter((p) => !isPointerParam(p));
|
|
1016
|
-
if (keep.length > 0) next.params = keep;
|
|
1017
|
-
else delete next.params;
|
|
1018
|
-
return next;
|
|
1019
|
-
}
|
|
1020
|
-
function processLayer(target, removed, selection) {
|
|
1021
|
-
let next = withoutPointerParams(target);
|
|
1022
|
-
const enc = isObj(next.encoding) ? { ...next.encoding } : null;
|
|
1023
|
-
if (!enc) return next;
|
|
1024
|
-
const conditioned = conditionalOn(enc, removed);
|
|
1025
|
-
const hoverOnly = conditioned.some((def) => def.value === 0);
|
|
1026
|
-
for (const [channel, def] of Object.entries(enc)) {
|
|
1027
|
-
if (!isObj(def) || !isObj(def.condition)) continue;
|
|
1028
|
-
const condition = def.condition;
|
|
1029
|
-
const param = condition.param;
|
|
1030
|
-
if (typeof param !== "string" || !removed.has(param)) continue;
|
|
1031
|
-
if (hoverOnly) {
|
|
1032
|
-
enc[channel] = selection ? {
|
|
1033
|
-
condition: {
|
|
1034
|
-
test: selectionTest(enc, selection.field, selection.value),
|
|
1035
|
-
..."value" in condition ? { value: condition.value } : {}
|
|
1036
|
-
},
|
|
1037
|
-
value: def.value
|
|
1038
|
-
} : { value: def.value };
|
|
1039
|
-
} else {
|
|
1040
|
-
const value = "value" in condition ? condition.value : def.value;
|
|
1041
|
-
if (value === void 0) delete enc[channel];
|
|
1042
|
-
else enc[channel] = { value };
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
next = { ...next, encoding: enc };
|
|
1046
|
-
if (hoverOnly || !selection) return next;
|
|
1047
|
-
return withOpacity(next, selection.field, selection.value);
|
|
1048
|
-
}
|
|
1049
|
-
function applyTouchInteraction(spec, selection) {
|
|
1050
|
-
const removed = /* @__PURE__ */ new Set();
|
|
1051
|
-
const collect = (holder) => {
|
|
1052
|
-
if (!Array.isArray(holder.params)) return;
|
|
1053
|
-
for (const p of holder.params) {
|
|
1054
|
-
if (isPointerParam(p) && typeof p.name === "string") removed.add(p.name);
|
|
1055
|
-
}
|
|
1056
|
-
};
|
|
1057
|
-
collect(spec);
|
|
1058
|
-
if (Array.isArray(spec.layer)) {
|
|
1059
|
-
for (const l of spec.layer) if (isObj(l)) collect(l);
|
|
1060
|
-
}
|
|
1061
|
-
if (removed.size === 0) {
|
|
1062
|
-
return selection ? highlightSelection(spec, selection.field, selection.value) : spec;
|
|
1063
|
-
}
|
|
1064
|
-
if (!Array.isArray(spec.layer)) return processLayer(spec, removed, selection);
|
|
1065
|
-
return withoutPointerParams({
|
|
1066
|
-
...spec,
|
|
1067
|
-
layer: spec.layer.map(
|
|
1068
|
-
(l) => isObj(l) ? processLayer(l, removed, selection) : l
|
|
1069
|
-
)
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// src/vendor/plugins/vega-chart/chart-tooltip.ts
|
|
1074
|
-
var TOOLTIP_STYLE_ID = "vega-tooltip-style";
|
|
1075
|
-
var tooltipEl = null;
|
|
1076
|
-
function getTooltipEl2() {
|
|
1077
|
-
if (tooltipEl && tooltipEl.isConnected) return tooltipEl;
|
|
1078
|
-
tooltipEl = document.getElementById("vg-tooltip-element");
|
|
1079
|
-
if (!tooltipEl) {
|
|
1080
|
-
tooltipEl = document.createElement("div");
|
|
1081
|
-
tooltipEl.id = "vg-tooltip-element";
|
|
1082
|
-
document.body.appendChild(tooltipEl);
|
|
1083
|
-
}
|
|
1084
|
-
if (!tooltipEl.style.left) {
|
|
1085
|
-
tooltipEl.style.left = "-9999px";
|
|
1086
|
-
tooltipEl.style.top = "-9999px";
|
|
1087
|
-
}
|
|
1088
|
-
return tooltipEl;
|
|
1089
|
-
}
|
|
1090
|
-
function ensureTooltipStyles() {
|
|
1091
|
-
if (typeof document === "undefined") return;
|
|
1092
|
-
let style = document.getElementById(TOOLTIP_STYLE_ID);
|
|
1093
|
-
if (!style) {
|
|
1094
|
-
style = document.createElement("style");
|
|
1095
|
-
style.id = TOOLTIP_STYLE_ID;
|
|
1096
|
-
document.head.appendChild(style);
|
|
1097
|
-
}
|
|
1098
|
-
style.textContent = `
|
|
1099
|
-
#vg-tooltip-element {
|
|
1100
|
-
visibility: hidden;
|
|
1101
|
-
position: fixed;
|
|
1102
|
-
z-index: 10000;
|
|
1103
|
-
pointer-events: none;
|
|
1104
|
-
padding: 0;
|
|
1105
|
-
font-family: inherit;
|
|
1106
|
-
font-size: 12px;
|
|
1107
|
-
line-height: 1.4;
|
|
1108
|
-
border-radius: 6px;
|
|
1109
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
1110
|
-
background-color: var(--surface-overlay, var(--popover, #1a1a1a));
|
|
1111
|
-
color: var(--popover-foreground, #e5e5e5);
|
|
1112
|
-
border: 1px solid var(--border, #333);
|
|
1113
|
-
max-width: 300px;
|
|
1114
|
-
max-height: 240px;
|
|
1115
|
-
display: flex;
|
|
1116
|
-
flex-direction: column;
|
|
1117
|
-
overflow: hidden;
|
|
1118
|
-
}
|
|
1119
|
-
#vg-tooltip-element.visible {
|
|
1120
|
-
visibility: visible;
|
|
1121
|
-
}
|
|
1122
|
-
#vg-tooltip-element .vg-tooltip-header {
|
|
1123
|
-
font-weight: 500;
|
|
1124
|
-
flex-shrink: 0;
|
|
1125
|
-
padding: 8px 12px 4px;
|
|
1126
|
-
padding-bottom: 4px;
|
|
1127
|
-
border-bottom: 1px solid var(--border, #333);
|
|
1128
|
-
}
|
|
1129
|
-
#vg-tooltip-element .vg-tooltip-body {
|
|
1130
|
-
overflow-y: auto;
|
|
1131
|
-
padding: 4px 12px 8px;
|
|
1132
|
-
scrollbar-width: thin;
|
|
1133
|
-
}
|
|
1134
|
-
#vg-tooltip-element .vg-tooltip-row {
|
|
1135
|
-
display: flex;
|
|
1136
|
-
align-items: center;
|
|
1137
|
-
gap: 8px;
|
|
1138
|
-
padding: 1px 0;
|
|
1139
|
-
}
|
|
1140
|
-
#vg-tooltip-element .vg-tooltip-dot {
|
|
1141
|
-
width: 10px;
|
|
1142
|
-
height: 10px;
|
|
1143
|
-
border-radius: 50%;
|
|
1144
|
-
flex-shrink: 0;
|
|
1145
|
-
}
|
|
1146
|
-
#vg-tooltip-element .vg-tooltip-key {
|
|
1147
|
-
color: var(--muted-foreground, #999);
|
|
1148
|
-
}
|
|
1149
|
-
#vg-tooltip-element .vg-tooltip-value {
|
|
1150
|
-
font-weight: 500;
|
|
1151
|
-
margin-left: auto;
|
|
1152
|
-
}
|
|
1153
|
-
/* Coarse pointers get bigger text and more room, but NOT a position. This
|
|
1154
|
-
block used to dock the tooltip to the bottom of the viewport (with
|
|
1155
|
-
!important, so it beat the computed placement) \u2014 which is where the
|
|
1156
|
-
composer and the plan bar live, so the reading landed on top of the chat
|
|
1157
|
-
chrome, a screen away from the chart it described. positionTooltip owns
|
|
1158
|
-
placement now, against the pointer.
|
|
1159
|
-
NOTE: no backticks in here. This whole stylesheet is a template literal,
|
|
1160
|
-
so one would end the string and break the parse. */
|
|
1161
|
-
@media (pointer: coarse) {
|
|
1162
|
-
#vg-tooltip-element {
|
|
1163
|
-
max-width: calc(100vw - 24px);
|
|
1164
|
-
font-size: 13px;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
/* Hide the default vega-tooltip table rendering */
|
|
1168
|
-
#vg-tooltip-element table {
|
|
1169
|
-
display: none;
|
|
1170
|
-
}
|
|
1171
|
-
#vg-tooltip-element h2 {
|
|
1172
|
-
display: none;
|
|
1173
|
-
}
|
|
1174
|
-
`;
|
|
1175
|
-
}
|
|
1176
|
-
var TIP_GAP = 14;
|
|
1177
|
-
var TIP_MARGIN = 8;
|
|
1178
|
-
function isCoarsePointer() {
|
|
1179
|
-
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
|
|
1180
|
-
}
|
|
1181
|
-
var touchDismissInstalled = false;
|
|
1182
|
-
function installTouchDismiss() {
|
|
1183
|
-
if (touchDismissInstalled || typeof document === "undefined") return;
|
|
1184
|
-
touchDismissInstalled = true;
|
|
1185
|
-
document.addEventListener(
|
|
1186
|
-
"pointerdown",
|
|
1187
|
-
() => {
|
|
1188
|
-
if (!isCoarsePointer()) return;
|
|
1189
|
-
document.getElementById("vg-tooltip-element")?.classList.remove("visible");
|
|
1190
|
-
},
|
|
1191
|
-
true
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1194
|
-
function pointerXY(event) {
|
|
1195
|
-
const e = event;
|
|
1196
|
-
const touch = e?.touches?.[0] ?? e?.changedTouches?.[0];
|
|
1197
|
-
const x = Number.isFinite(e?.clientX) ? e.clientX : touch?.clientX;
|
|
1198
|
-
const y = Number.isFinite(e?.clientY) ? e.clientY : touch?.clientY;
|
|
1199
|
-
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
|
|
1200
|
-
}
|
|
1201
|
-
function positionTooltip2(el, event, anchor) {
|
|
1202
|
-
const w = el.offsetWidth;
|
|
1203
|
-
const h = el.offsetHeight;
|
|
1204
|
-
const maxX = Math.max(TIP_MARGIN, window.innerWidth - w - TIP_MARGIN);
|
|
1205
|
-
const maxY = Math.max(TIP_MARGIN, window.innerHeight - h - TIP_MARGIN);
|
|
1206
|
-
const coarse = isCoarsePointer();
|
|
1207
|
-
const p = pointerXY(event);
|
|
1208
|
-
let x;
|
|
1209
|
-
let y;
|
|
1210
|
-
if (!p) {
|
|
1211
|
-
const rect = anchor?.getBoundingClientRect();
|
|
1212
|
-
if (!rect) return;
|
|
1213
|
-
x = rect.left + rect.width / 2 - w / 2;
|
|
1214
|
-
const above = rect.top - h - TIP_GAP;
|
|
1215
|
-
y = above >= TIP_MARGIN ? above : rect.bottom + TIP_GAP;
|
|
1216
|
-
} else if (coarse) {
|
|
1217
|
-
x = p.x - w / 2;
|
|
1218
|
-
const above = p.y - h - TIP_GAP;
|
|
1219
|
-
y = above >= TIP_MARGIN ? above : p.y + TIP_GAP;
|
|
1220
|
-
} else {
|
|
1221
|
-
x = p.x + 12;
|
|
1222
|
-
y = p.y + 12;
|
|
1223
|
-
}
|
|
1224
|
-
const left = Math.min(Math.max(x, TIP_MARGIN), maxX);
|
|
1225
|
-
const top = Math.min(Math.max(y, TIP_MARGIN), maxY);
|
|
1226
|
-
if (!Number.isFinite(left) || !Number.isFinite(top)) return;
|
|
1227
|
-
el.style.left = `${left}px`;
|
|
1228
|
-
el.style.top = `${top}px`;
|
|
1229
|
-
}
|
|
1230
|
-
function tooltipHtml(title, rows) {
|
|
1231
|
-
const head = title ? `<div class="vg-tooltip-header">${escapeTooltipText(title)}</div>` : "";
|
|
1232
|
-
const body = rows.map(
|
|
1233
|
-
(r) => `
|
|
1234
|
-
<div class="vg-tooltip-row">
|
|
1235
|
-
<span class="vg-tooltip-dot" style="background-color: ${escapeTooltipText(r.color)}"></span>
|
|
1236
|
-
<span class="vg-tooltip-key">${escapeTooltipText(r.label)}:</span>
|
|
1237
|
-
<span class="vg-tooltip-value">${escapeTooltipText(r.value)}</span>
|
|
1238
|
-
</div>`
|
|
1239
|
-
).join("");
|
|
1240
|
-
return head + `<div class="vg-tooltip-body scrollbar">${body}</div>`;
|
|
1241
|
-
}
|
|
1242
|
-
function escapeTooltipText(v) {
|
|
1243
|
-
return String(v).replace(
|
|
1244
|
-
/[&<>"]/g,
|
|
1245
|
-
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
1248
|
-
function showTooltipAt(html, clientX, clientY) {
|
|
1249
|
-
if (typeof document === "undefined") return;
|
|
1250
|
-
ensureTooltipStyles();
|
|
1251
|
-
installTouchDismiss();
|
|
1252
|
-
const el = getTooltipEl2();
|
|
1253
|
-
el.innerHTML = html;
|
|
1254
|
-
el.classList.add("visible");
|
|
1255
|
-
positionTooltip2(el, { clientX, clientY }, null);
|
|
1256
|
-
}
|
|
1257
|
-
function hideTooltip() {
|
|
1258
|
-
if (typeof document === "undefined") return;
|
|
1259
|
-
document.getElementById("vg-tooltip-element")?.classList.remove("visible");
|
|
1260
|
-
}
|
|
1261
|
-
var HOLD_TO_SCRUB_MS = 150;
|
|
1262
|
-
var HOLD_SLOP_PX = 8;
|
|
1263
|
-
var formatValue = (n) => {
|
|
1264
|
-
const abs = Math.abs(n);
|
|
1265
|
-
if (abs >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
1266
|
-
if (abs >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
1267
|
-
if (!Number.isInteger(n)) return n.toFixed(1);
|
|
1268
|
-
return n.toLocaleString();
|
|
1269
|
-
};
|
|
1270
|
-
function ChartCardMobile({
|
|
1271
|
-
title,
|
|
1272
|
-
summary,
|
|
1273
|
-
children,
|
|
1274
|
-
view,
|
|
1275
|
-
scrolls = false,
|
|
1276
|
-
selected,
|
|
1277
|
-
onSelect,
|
|
1278
|
-
onExpand
|
|
1279
|
-
}) {
|
|
1280
|
-
const colorOf = useCallback(
|
|
1281
|
-
(name) => {
|
|
1282
|
-
try {
|
|
1283
|
-
const scale = view?.scale?.("color");
|
|
1284
|
-
const c = scale?.(name);
|
|
1285
|
-
if (typeof c === "string" && c) return c;
|
|
1286
|
-
} catch {
|
|
1287
|
-
}
|
|
1288
|
-
return "var(--muted-foreground)";
|
|
1289
|
-
},
|
|
1290
|
-
[view]
|
|
1291
|
-
);
|
|
1292
|
-
const isComposition = summary?.kind === "composition";
|
|
1293
|
-
const plotRef = useRef(null);
|
|
1294
|
-
const scrubbedRef = useRef(false);
|
|
1295
|
-
const canScrub = !scrolls;
|
|
1296
|
-
const canScrubRef = useRef(canScrub);
|
|
1297
|
-
canScrubRef.current = canScrub;
|
|
1298
|
-
const armedRef = useRef(false);
|
|
1299
|
-
const startRef = useRef({ x: 0, y: 0 });
|
|
1300
|
-
const holdTimerRef = useRef(null);
|
|
1301
|
-
const selectedRef = useRef(selected);
|
|
1302
|
-
const onSelectRef = useRef(onSelect);
|
|
1303
|
-
const summaryRef = useRef(summary);
|
|
1304
|
-
selectedRef.current = selected;
|
|
1305
|
-
onSelectRef.current = onSelect;
|
|
1306
|
-
summaryRef.current = summary;
|
|
1307
|
-
const bucket = useMemo(() => {
|
|
1308
|
-
if (!summary || summary.buckets.length === 0) return null;
|
|
1309
|
-
if (summary.kind === "buckets" && selected) {
|
|
1310
|
-
return summary.buckets.find((b) => b.key === selected) ?? summary.buckets[0];
|
|
1311
|
-
}
|
|
1312
|
-
return summary.buckets[summary.buckets.length - 1];
|
|
1313
|
-
}, [summary, selected]);
|
|
1314
|
-
const slice = useMemo(() => {
|
|
1315
|
-
if (!summary || summary.kind !== "composition" || !selected || !bucket) return null;
|
|
1316
|
-
const value = bucket.values[selected];
|
|
1317
|
-
if (value == null) return null;
|
|
1318
|
-
return { name: selected, value, share: bucket.total > 0 ? value / bucket.total : 0 };
|
|
1319
|
-
}, [summary, selected, bucket]);
|
|
1320
|
-
const legendEntries = useMemo(() => {
|
|
1321
|
-
if (!summary || summary.kind !== "buckets" || summary.series.length < 2) return [];
|
|
1322
|
-
return summary.series.map((name) => ({ name, color: colorOf(name) }));
|
|
1323
|
-
}, [summary, colorOf]);
|
|
1324
|
-
const legendRef = useRef(null);
|
|
1325
|
-
const [legendOneLine, setLegendOneLine] = useState(false);
|
|
1326
|
-
useEffect(() => {
|
|
1327
|
-
const el = legendRef.current;
|
|
1328
|
-
if (!el || legendEntries.length === 0) return;
|
|
1329
|
-
const measure = () => {
|
|
1330
|
-
const kids = Array.from(el.children);
|
|
1331
|
-
if (kids.length === 0) return;
|
|
1332
|
-
const top = kids[0].offsetTop;
|
|
1333
|
-
setLegendOneLine(kids.every((k) => k.offsetTop === top));
|
|
1334
|
-
};
|
|
1335
|
-
measure();
|
|
1336
|
-
const ro = new ResizeObserver(measure);
|
|
1337
|
-
ro.observe(el);
|
|
1338
|
-
return () => ro.disconnect();
|
|
1339
|
-
}, [legendEntries]);
|
|
1340
|
-
const showTipFor = useCallback(
|
|
1341
|
-
(key, clientX, clientY) => {
|
|
1342
|
-
const s = summaryRef.current;
|
|
1343
|
-
const b = s?.buckets.find((x) => x.key === key);
|
|
1344
|
-
if (!s || !b) return;
|
|
1345
|
-
const rows2 = !isFlatSummary(s) && s.series.length ? s.series.map((name) => ({
|
|
1346
|
-
color: colorOf(name),
|
|
1347
|
-
label: name,
|
|
1348
|
-
value: formatValue(b.values[name] ?? 0)
|
|
1349
|
-
})) : [
|
|
1350
|
-
{
|
|
1351
|
-
color: colorOf(b.key),
|
|
1352
|
-
label: s.valueLabel || "Value",
|
|
1353
|
-
value: formatValue(b.total)
|
|
1354
|
-
}
|
|
1355
|
-
];
|
|
1356
|
-
showTooltipAt(tooltipHtml(b.key, rows2), clientX, clientY);
|
|
1357
|
-
},
|
|
1358
|
-
[colorOf]
|
|
1359
|
-
);
|
|
1360
|
-
const showTipRef = useRef(showTipFor);
|
|
1361
|
-
showTipRef.current = showTipFor;
|
|
1362
|
-
const rows = useMemo(() => {
|
|
1363
|
-
if (!bucket) return [];
|
|
1364
|
-
return summary?.series.map((name) => ({ name, value: bucket.values[name] ?? 0 })).sort((a, b) => b.value - a.value).map((r) => ({
|
|
1365
|
-
key: r.name,
|
|
1366
|
-
color: colorOf(r.name),
|
|
1367
|
-
label: r.name,
|
|
1368
|
-
value: formatValue(r.value),
|
|
1369
|
-
sub: bucket.total > 0 ? `${(r.value / bucket.total * 100).toFixed(1)}%` : void 0
|
|
1370
|
-
})) ?? [];
|
|
1371
|
-
}, [bucket, summary, colorOf]);
|
|
1372
|
-
useEffect(() => {
|
|
1373
|
-
if (!view) return;
|
|
1374
|
-
const v = view;
|
|
1375
|
-
const valueAtClientX = (clientX, svgFrom) => {
|
|
1376
|
-
try {
|
|
1377
|
-
const svg = svgFrom?.closest("svg") ?? plotRef.current?.querySelector("svg");
|
|
1378
|
-
if (!svg) return null;
|
|
1379
|
-
const rect = svg.getBoundingClientRect();
|
|
1380
|
-
const origin = typeof v.origin === "function" ? v.origin() : [0, 0];
|
|
1381
|
-
const x = clientX - rect.left - (origin[0] ?? 0);
|
|
1382
|
-
const scale = typeof v.scale === "function" ? v.scale("x") : null;
|
|
1383
|
-
if (!scale) return null;
|
|
1384
|
-
if (typeof scale.invert === "function") return scale.invert(x);
|
|
1385
|
-
const domain = typeof scale.domain === "function" ? scale.domain() : [];
|
|
1386
|
-
if (domain.length === 0) return null;
|
|
1387
|
-
const step = typeof scale.step === "function" ? scale.step() : Math.abs(((scale.range?.() ?? [0, 0])[1] ?? 0) - ((scale.range?.() ?? [0, 0])[0] ?? 0)) / domain.length;
|
|
1388
|
-
if (!step) return null;
|
|
1389
|
-
const i = Math.max(0, Math.min(domain.length - 1, Math.floor(x / step)));
|
|
1390
|
-
return domain[i];
|
|
1391
|
-
} catch {
|
|
1392
|
-
return null;
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
const onClick = (event, item) => {
|
|
1396
|
-
const summary2 = summaryRef.current;
|
|
1397
|
-
if (!summary2) return;
|
|
1398
|
-
if (scrubbedRef.current) {
|
|
1399
|
-
scrubbedRef.current = false;
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
const datum = item?.datum;
|
|
1403
|
-
const key = (datum ? keyForDatum(datum, summary2) : void 0) ?? (summary2.kind === "buckets" ? bucketAtValue(summary2, valueAtClientX(event.clientX, event.target)) : void 0);
|
|
1404
|
-
if (!key) return;
|
|
1405
|
-
const next = key === selectedRef.current ? null : key;
|
|
1406
|
-
onSelectRef.current(next);
|
|
1407
|
-
if (isCoarsePointer()) {
|
|
1408
|
-
if (next) showTipRef.current(next, event.clientX, event.clientY);
|
|
1409
|
-
else hideTooltip();
|
|
1410
|
-
}
|
|
1411
|
-
};
|
|
1412
|
-
v.addEventListener?.("click", onClick);
|
|
1413
|
-
const plot = plotRef.current;
|
|
1414
|
-
if (!plot || !canScrubRef.current) return () => v.removeEventListener?.("click", onClick);
|
|
1415
|
-
const selectAt = (clientX, clientY) => {
|
|
1416
|
-
const summary2 = summaryRef.current;
|
|
1417
|
-
if (!summary2 || summary2.kind !== "buckets") return;
|
|
1418
|
-
const key = bucketAtValue(summary2, valueAtClientX(clientX, null));
|
|
1419
|
-
if (!key) return;
|
|
1420
|
-
if (key !== selectedRef.current) onSelectRef.current(key);
|
|
1421
|
-
if (isCoarsePointer()) showTipRef.current(key, clientX, clientY);
|
|
1422
|
-
};
|
|
1423
|
-
const disarm = () => {
|
|
1424
|
-
if (holdTimerRef.current) clearTimeout(holdTimerRef.current);
|
|
1425
|
-
holdTimerRef.current = null;
|
|
1426
|
-
armedRef.current = false;
|
|
1427
|
-
plot.style.touchAction = "";
|
|
1428
|
-
};
|
|
1429
|
-
const onPointerDown = (e) => {
|
|
1430
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
1431
|
-
if (!canScrubRef.current || summaryRef.current?.kind !== "buckets") return;
|
|
1432
|
-
startRef.current = { x: e.clientX, y: e.clientY };
|
|
1433
|
-
holdTimerRef.current = setTimeout(() => {
|
|
1434
|
-
armedRef.current = true;
|
|
1435
|
-
scrubbedRef.current = true;
|
|
1436
|
-
plot.style.touchAction = "none";
|
|
1437
|
-
try {
|
|
1438
|
-
plot.setPointerCapture(e.pointerId);
|
|
1439
|
-
} catch {
|
|
1440
|
-
}
|
|
1441
|
-
selectAt(e.clientX, e.clientY);
|
|
1442
|
-
}, HOLD_TO_SCRUB_MS);
|
|
1443
|
-
};
|
|
1444
|
-
const onPointerMove = (e) => {
|
|
1445
|
-
if (!armedRef.current) {
|
|
1446
|
-
const { x, y } = startRef.current;
|
|
1447
|
-
if (Math.abs(e.clientY - y) > HOLD_SLOP_PX || Math.abs(e.clientX - x) > HOLD_SLOP_PX) {
|
|
1448
|
-
disarm();
|
|
1449
|
-
}
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
selectAt(e.clientX, e.clientY);
|
|
1453
|
-
};
|
|
1454
|
-
plot.addEventListener("pointerdown", onPointerDown);
|
|
1455
|
-
plot.addEventListener("pointermove", onPointerMove);
|
|
1456
|
-
plot.addEventListener("pointerup", disarm);
|
|
1457
|
-
plot.addEventListener("pointercancel", disarm);
|
|
1458
|
-
return () => {
|
|
1459
|
-
v.removeEventListener?.("click", onClick);
|
|
1460
|
-
plot.removeEventListener("pointerdown", onPointerDown);
|
|
1461
|
-
plot.removeEventListener("pointermove", onPointerMove);
|
|
1462
|
-
plot.removeEventListener("pointerup", disarm);
|
|
1463
|
-
plot.removeEventListener("pointercancel", disarm);
|
|
1464
|
-
};
|
|
1465
|
-
}, [view]);
|
|
1466
|
-
useEffect(
|
|
1467
|
-
() => () => {
|
|
1468
|
-
if (holdTimerRef.current) clearTimeout(holdTimerRef.current);
|
|
1469
|
-
},
|
|
1470
|
-
[]
|
|
1471
|
-
);
|
|
1472
|
-
const hasHeader = !!title || !!onExpand;
|
|
1473
|
-
return (
|
|
1474
|
-
// `not-prose`: this card renders inside the answer's `.prose` container, so
|
|
1475
|
-
// without it the header's <h3> inherits the typography plugin's heading
|
|
1476
|
-
// margins AND its font-size — which outranks a utility class on
|
|
1477
|
-
// specificity. That is where the header's extra vertical space came from:
|
|
1478
|
-
// the row is `min-h-11` and was rendering far taller.
|
|
1479
|
-
/* @__PURE__ */ jsxs("div", { className: "not-prose my-3 w-full overflow-hidden rounded-xl border border-border bg-surface-raised", children: [
|
|
1480
|
-
hasHeader ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1481
|
-
/* @__PURE__ */ jsxs("div", { className: "flex min-h-11 items-center gap-1 py-1 pl-3 pr-1", children: [
|
|
1482
|
-
title ? (
|
|
1483
|
-
// A <div>, not an <h3>: `not-prose` above cancels the typography
|
|
1484
|
-
// plugin's heading margins, but globals.css also carries an
|
|
1485
|
-
// unguarded `.prose h3 { text-base }` which outranks any utility
|
|
1486
|
-
// class here on specificity — the title would silently render at
|
|
1487
|
-
// 16px. Every other in-prose component in the app (the readout
|
|
1488
|
-
// band, the sheet titles) uses a span for the same reason.
|
|
1489
|
-
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 truncate text-sm font-semibold", children: title })
|
|
1490
|
-
) : /* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
1491
|
-
onExpand ? /* @__PURE__ */ jsx(
|
|
1492
|
-
"button",
|
|
1493
|
-
{
|
|
1494
|
-
type: "button",
|
|
1495
|
-
onClick: onExpand,
|
|
1496
|
-
"aria-label": "Open chart full screen",
|
|
1497
|
-
className: "flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors active:bg-surface-active",
|
|
1498
|
-
children: /* @__PURE__ */ jsx(ArrowsShowIcon, { className: "size-4" })
|
|
1499
|
-
}
|
|
1500
|
-
) : null
|
|
1501
|
-
] }),
|
|
1502
|
-
/* @__PURE__ */ jsx("div", { className: "h-px bg-border" })
|
|
1503
|
-
] }) : null,
|
|
1504
|
-
/* @__PURE__ */ jsx(
|
|
1505
|
-
"div",
|
|
1506
|
-
{
|
|
1507
|
-
ref: plotRef,
|
|
1508
|
-
className: canScrub ? "touch-pan-y px-2 py-1" : "px-2 py-1",
|
|
1509
|
-
children
|
|
1510
|
-
}
|
|
1511
|
-
),
|
|
1512
|
-
legendEntries.length > 0 ? (
|
|
1513
|
-
// Values deliberately omitted: this is a key, not a readout. What each
|
|
1514
|
-
// series is WORTH lives in the tooltip, on the tap that asks for it.
|
|
1515
|
-
// Wrapped inline, not a column grid. Equal-width columns are sized by
|
|
1516
|
-
// the LONGEST name, so "Kilat" sat in a 105px cell beside "Kilat
|
|
1517
|
-
// Khusus" and four short series took two rows with a gap you could
|
|
1518
|
-
// park a bus in. Wrapping lets every entry take only the width it
|
|
1519
|
-
// needs, so as many fit per line as actually fit — and a name too long
|
|
1520
|
-
// for the line gets one to itself, which is the single-column case
|
|
1521
|
-
// falling out for free rather than being switched on.
|
|
1522
|
-
/* @__PURE__ */ jsx(
|
|
1523
|
-
"div",
|
|
1524
|
-
{
|
|
1525
|
-
ref: legendRef,
|
|
1526
|
-
className: `flex flex-wrap gap-x-4 gap-y-1.5 px-3 pt-1 pb-3 text-xs ${legendOneLine ? "justify-center" : ""}`,
|
|
1527
|
-
children: legendEntries.map((e) => (
|
|
1528
|
-
// `max-w-full` so a name wider than the card truncates instead of
|
|
1529
|
-
// pushing the row sideways.
|
|
1530
|
-
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 max-w-full items-center gap-2", children: [
|
|
1531
|
-
/* @__PURE__ */ jsx(
|
|
1532
|
-
"span",
|
|
1533
|
-
{
|
|
1534
|
-
"aria-hidden": true,
|
|
1535
|
-
className: "size-2.5 shrink-0 rounded-full",
|
|
1536
|
-
style: { background: e.color }
|
|
1537
|
-
}
|
|
1538
|
-
),
|
|
1539
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-foreground", children: e.name })
|
|
1540
|
-
] }, e.name)
|
|
1541
|
-
))
|
|
1542
|
-
}
|
|
1543
|
-
)
|
|
1544
|
-
) : null,
|
|
1545
|
-
isComposition && rows.length > 0 ? (
|
|
1546
|
-
// ChartReadout paints `surface-raised` itself now, matching this card.
|
|
1547
|
-
/* @__PURE__ */ jsx("div", { className: "p-2 pt-0", children: /* @__PURE__ */ jsx(
|
|
1548
|
-
ChartReadout,
|
|
1549
|
-
{
|
|
1550
|
-
label: slice ? slice.name : "All series",
|
|
1551
|
-
total: /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: slice ? `${formatValue(slice.value)} \xB7 ${(slice.share * 100).toFixed(1)}%` : bucket ? formatValue(bucket.total) : "" }),
|
|
1552
|
-
rows
|
|
1553
|
-
}
|
|
1554
|
-
) })
|
|
1555
|
-
) : null
|
|
1556
|
-
] })
|
|
1557
|
-
);
|
|
1558
|
-
}
|
|
1559
|
-
var PLUGIN_VEGA_CHART_CONFIG = { type: "bar" };
|
|
1560
|
-
function formatTooltipValue(value, fieldType) {
|
|
1561
|
-
if (value == null) return "";
|
|
1562
|
-
if (typeof value === "number") {
|
|
1563
|
-
if (fieldType === "temporal" || value > 9466848e5 && value < 41024448e5) {
|
|
1564
|
-
try {
|
|
1565
|
-
const d = new Date(value);
|
|
1566
|
-
if (!isNaN(d.getTime())) {
|
|
1567
|
-
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
1568
|
-
}
|
|
1569
|
-
} catch {
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
return value.toLocaleString();
|
|
1573
|
-
}
|
|
1574
|
-
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
1575
|
-
try {
|
|
1576
|
-
const d = new Date(value);
|
|
1577
|
-
if (!isNaN(d.getTime())) {
|
|
1578
|
-
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
1579
|
-
}
|
|
1580
|
-
} catch {
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
return String(value);
|
|
1584
|
-
}
|
|
1585
|
-
function applyFoldTransforms(values, transforms) {
|
|
1586
|
-
if (!transforms) return values;
|
|
1587
|
-
let rows = values;
|
|
1588
|
-
for (const t of transforms) {
|
|
1589
|
-
if (!t.fold || !Array.isArray(t.fold)) continue;
|
|
1590
|
-
const foldFields = t.fold;
|
|
1591
|
-
const [keyAs, valueAs] = Array.isArray(t.as) ? t.as : ["key", "value"];
|
|
1592
|
-
rows = rows.flatMap((row) => {
|
|
1593
|
-
return foldFields.map((field) => {
|
|
1594
|
-
const { ...rest } = row;
|
|
1595
|
-
for (const f of foldFields) delete rest[f];
|
|
1596
|
-
return { ...rest, [keyAs]: field, [valueAs]: row[field] };
|
|
1597
|
-
});
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
return rows;
|
|
1601
|
-
}
|
|
1602
|
-
function extractChartMeta(spec) {
|
|
1603
|
-
if (spec.layer) {
|
|
1604
|
-
const layers = spec.layer;
|
|
1605
|
-
const arcLayer = layers.find((l) => {
|
|
1606
|
-
const m = l.mark;
|
|
1607
|
-
return (typeof m === "string" ? m : m?.type) === "arc";
|
|
1608
|
-
});
|
|
1609
|
-
if (arcLayer) {
|
|
1610
|
-
const encoding2 = arcLayer.encoding;
|
|
1611
|
-
if (encoding2) {
|
|
1612
|
-
const colorEnc2 = encoding2.color;
|
|
1613
|
-
const thetaEnc2 = encoding2.theta;
|
|
1614
|
-
const data2 = spec.data;
|
|
1615
|
-
const values2 = data2?.values || [];
|
|
1616
|
-
return {
|
|
1617
|
-
markType: "arc",
|
|
1618
|
-
categoryField: colorEnc2?.field,
|
|
1619
|
-
valueFields: thetaEnc2?.field ? [thetaEnc2.field] : [],
|
|
1620
|
-
data: values2
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
const lineAreaLayer = layers.find((l) => {
|
|
1625
|
-
const m = l.mark;
|
|
1626
|
-
const mt = typeof m === "string" ? m : m?.type;
|
|
1627
|
-
return mt === "line" || mt === "area";
|
|
1628
|
-
});
|
|
1629
|
-
if (lineAreaLayer) {
|
|
1630
|
-
const encoding2 = lineAreaLayer.encoding || spec.encoding;
|
|
1631
|
-
if (encoding2) {
|
|
1632
|
-
const xEnc2 = encoding2.x;
|
|
1633
|
-
const yEnc2 = encoding2.y;
|
|
1634
|
-
const colorEnc2 = encoding2.color;
|
|
1635
|
-
const data2 = spec.data;
|
|
1636
|
-
const rawValues2 = data2?.values || [];
|
|
1637
|
-
const values2 = applyFoldTransforms(rawValues2, spec.transform);
|
|
1638
|
-
const categoryField2 = xEnc2?.field;
|
|
1639
|
-
const categoryType2 = xEnc2?.type;
|
|
1640
|
-
const seriesField3 = colorEnc2?.field && colorEnc2.field !== categoryField2 ? colorEnc2.field : void 0;
|
|
1641
|
-
const valueFields2 = [];
|
|
1642
|
-
if (yEnc2?.field) valueFields2.push(yEnc2.field);
|
|
1643
|
-
return { markType: "line", categoryField: categoryField2, categoryType: categoryType2, seriesField: seriesField3, valueFields: valueFields2, data: values2 };
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
return null;
|
|
1647
|
-
}
|
|
1648
|
-
if (spec.hconcat || spec.vconcat || spec.concat) return null;
|
|
1649
|
-
const encoding = spec.encoding;
|
|
1650
|
-
if (!encoding) return null;
|
|
1651
|
-
const mark = spec.mark;
|
|
1652
|
-
if (!mark) return null;
|
|
1653
|
-
const markType2 = typeof mark === "string" ? mark : mark?.type;
|
|
1654
|
-
if (!markType2) return null;
|
|
1655
|
-
const xEnc = encoding.x;
|
|
1656
|
-
const yEnc = encoding.y;
|
|
1657
|
-
const colorEnc = encoding.color;
|
|
1658
|
-
const thetaEnc = encoding.theta;
|
|
1659
|
-
const data = spec.data;
|
|
1660
|
-
const rawValues = data?.values || [];
|
|
1661
|
-
const values = applyFoldTransforms(rawValues, spec.transform);
|
|
1662
|
-
if (markType2 === "arc") {
|
|
1663
|
-
return {
|
|
1664
|
-
markType: markType2,
|
|
1665
|
-
categoryField: colorEnc?.field,
|
|
1666
|
-
valueFields: thetaEnc?.field ? [thetaEnc.field] : [],
|
|
1667
|
-
data: values
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
const xType = xEnc?.type;
|
|
1671
|
-
const yType = yEnc?.type;
|
|
1672
|
-
const isHorizontal = markType2 === "bar" && xType === "quantitative" && (yType === "nominal" || yType === "ordinal");
|
|
1673
|
-
const catEnc = isHorizontal ? yEnc : xEnc;
|
|
1674
|
-
const valEnc = isHorizontal ? xEnc : yEnc;
|
|
1675
|
-
const categoryField = catEnc?.field;
|
|
1676
|
-
const categoryType = catEnc?.type;
|
|
1677
|
-
const colorType = colorEnc?.type;
|
|
1678
|
-
const isColorCategorical = colorType === "nominal" || colorType === "ordinal";
|
|
1679
|
-
const seriesField2 = colorEnc?.field && colorEnc.field !== categoryField && isColorCategorical ? colorEnc.field : void 0;
|
|
1680
|
-
const valueFields = [];
|
|
1681
|
-
if (valEnc?.field) valueFields.push(valEnc.field);
|
|
1682
|
-
if (markType2 === "rect" && colorEnc?.field && colorType === "quantitative") {
|
|
1683
|
-
const colorField = colorEnc.field;
|
|
1684
|
-
if (!valueFields.includes(colorField)) {
|
|
1685
|
-
valueFields.push(colorField);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return {
|
|
1689
|
-
markType: markType2,
|
|
1690
|
-
categoryField,
|
|
1691
|
-
categoryType,
|
|
1692
|
-
seriesField: seriesField2,
|
|
1693
|
-
valueFields,
|
|
1694
|
-
data: values
|
|
1695
|
-
};
|
|
1696
|
-
}
|
|
1697
|
-
function createCustomTooltipHandler(getView, chartMeta) {
|
|
1698
|
-
return function handler(_handler, event, item, value) {
|
|
1699
|
-
const el = getTooltipEl2();
|
|
1700
|
-
if (item?.datum) {
|
|
1701
|
-
const rawDatum2 = item.datum || {};
|
|
1702
|
-
rawDatum2.datum && typeof rawDatum2.datum === "object" && !Array.isArray(rawDatum2.datum) ? rawDatum2.datum : rawDatum2;
|
|
1703
|
-
}
|
|
1704
|
-
if (!item || !value && !item.datum) {
|
|
1705
|
-
if (!isCoarsePointer()) el.classList.remove("visible");
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
const rawDatum = item.datum || {};
|
|
1709
|
-
const datum = rawDatum.datum && typeof rawDatum.datum === "object" && !Array.isArray(rawDatum.datum) ? rawDatum.datum : rawDatum;
|
|
1710
|
-
const tooltipValue = value && Object.keys(value).length > 0 ? value : null;
|
|
1711
|
-
const pickColor = (c) => c && c !== "transparent" ? c : "";
|
|
1712
|
-
const color = pickColor(item.fill) || pickColor(item.stroke) || pickColor(datum.fill) || pickColor(datum.stroke) || "var(--chart-1)";
|
|
1713
|
-
let html = "";
|
|
1714
|
-
let colorScale = null;
|
|
1715
|
-
try {
|
|
1716
|
-
colorScale = getView()?.scale("color");
|
|
1717
|
-
} catch {
|
|
1718
|
-
}
|
|
1719
|
-
if (chartMeta) {
|
|
1720
|
-
const { markType: markType2, categoryField, categoryType, seriesField: seriesField2, valueFields, data } = chartMeta;
|
|
1721
|
-
if (markType2 === "arc") {
|
|
1722
|
-
const sliceName = categoryField ? datum[categoryField] : null;
|
|
1723
|
-
const sliceValue = valueFields[0] ? datum[valueFields[0]] : null;
|
|
1724
|
-
if (sliceName != null) {
|
|
1725
|
-
html += `<div class="vg-tooltip-header">${formatTooltipValue(sliceName)}</div>`;
|
|
1726
|
-
}
|
|
1727
|
-
if (sliceValue != null && valueFields[0]) {
|
|
1728
|
-
html += `
|
|
1729
|
-
<div class="vg-tooltip-row">
|
|
1730
|
-
<span class="vg-tooltip-dot" style="background-color: ${color}"></span>
|
|
1731
|
-
<span class="vg-tooltip-key">${valueFields[0].replace(/_/g, " ")}:</span>
|
|
1732
|
-
<span class="vg-tooltip-value">${formatTooltipValue(sliceValue)}</span>
|
|
1733
|
-
</div>
|
|
1734
|
-
`;
|
|
1735
|
-
}
|
|
1736
|
-
const pctValue = datum["__pct"] ?? datum["pct"];
|
|
1737
|
-
if (pctValue != null) {
|
|
1738
|
-
html += `
|
|
1739
|
-
<div class="vg-tooltip-row">
|
|
1740
|
-
<span class="vg-tooltip-dot" style="background-color: transparent"></span>
|
|
1741
|
-
<span class="vg-tooltip-key">Share:</span>
|
|
1742
|
-
<span class="vg-tooltip-value">${typeof pctValue === "number" ? (pctValue * 100).toFixed(1) + "%" : pctValue}</span>
|
|
1743
|
-
</div>
|
|
1744
|
-
`;
|
|
1745
|
-
}
|
|
1746
|
-
} else if (seriesField2 && categoryField && data.length > 0) {
|
|
1747
|
-
const categoryValue = datum[categoryField];
|
|
1748
|
-
const toEpoch = (v) => {
|
|
1749
|
-
if (v == null) return null;
|
|
1750
|
-
if (typeof v === "number") return v;
|
|
1751
|
-
if (typeof v === "string") {
|
|
1752
|
-
const d = new Date(v);
|
|
1753
|
-
return isNaN(d.getTime()) ? null : d.getTime();
|
|
1754
|
-
}
|
|
1755
|
-
return null;
|
|
1756
|
-
};
|
|
1757
|
-
let siblings;
|
|
1758
|
-
if (categoryType === "temporal") {
|
|
1759
|
-
const hoveredEpoch = toEpoch(categoryValue);
|
|
1760
|
-
siblings = hoveredEpoch != null ? data.filter((d) => toEpoch(d[categoryField]) === hoveredEpoch) : [];
|
|
1761
|
-
} else {
|
|
1762
|
-
siblings = data.filter((d) => d[categoryField] === categoryValue);
|
|
1763
|
-
}
|
|
1764
|
-
html += `<div class="vg-tooltip-header">${formatTooltipValue(categoryValue, categoryType)}</div>`;
|
|
1765
|
-
if (siblings.length > 0) {
|
|
1766
|
-
const sortedSiblings = [...siblings].sort((a, b) => {
|
|
1767
|
-
const av = valueFields[0] ? a[valueFields[0]] : null;
|
|
1768
|
-
const bv = valueFields[0] ? b[valueFields[0]] : null;
|
|
1769
|
-
if (typeof av === "number" && typeof bv === "number") return bv - av;
|
|
1770
|
-
return 0;
|
|
1771
|
-
});
|
|
1772
|
-
for (const sib of sortedSiblings) {
|
|
1773
|
-
const seriesName = String(sib[seriesField2] || "");
|
|
1774
|
-
const seriesValue = valueFields[0] ? sib[valueFields[0]] : null;
|
|
1775
|
-
const seriesColor = colorScale ? colorScale(seriesName) : color;
|
|
1776
|
-
html += `
|
|
1777
|
-
<div class="vg-tooltip-row">
|
|
1778
|
-
<span class="vg-tooltip-dot" style="background-color: ${seriesColor}"></span>
|
|
1779
|
-
<span class="vg-tooltip-key">${seriesName}:</span>
|
|
1780
|
-
<span class="vg-tooltip-value">${formatTooltipValue(seriesValue)}</span>
|
|
1781
|
-
</div>
|
|
1782
|
-
`;
|
|
1783
|
-
}
|
|
1784
|
-
} else {
|
|
1785
|
-
const seriesName = String(datum[seriesField2] || "");
|
|
1786
|
-
const seriesValue = valueFields[0] ? datum[valueFields[0]] : null;
|
|
1787
|
-
html += `
|
|
1788
|
-
<div class="vg-tooltip-row">
|
|
1789
|
-
<span class="vg-tooltip-dot" style="background-color: ${color}"></span>
|
|
1790
|
-
<span class="vg-tooltip-key">${seriesName}:</span>
|
|
1791
|
-
<span class="vg-tooltip-value">${formatTooltipValue(seriesValue)}</span>
|
|
1792
|
-
</div>
|
|
1793
|
-
`;
|
|
1794
|
-
}
|
|
1795
|
-
} else if (categoryField) {
|
|
1796
|
-
const categoryValue = datum[categoryField];
|
|
1797
|
-
html += `<div class="vg-tooltip-header">${formatTooltipValue(categoryValue, categoryType)}</div>`;
|
|
1798
|
-
for (const vf of valueFields) {
|
|
1799
|
-
const val = datum[vf];
|
|
1800
|
-
if (val != null) {
|
|
1801
|
-
html += `
|
|
1802
|
-
<div class="vg-tooltip-row">
|
|
1803
|
-
<span class="vg-tooltip-dot" style="background-color: ${color}"></span>
|
|
1804
|
-
<span class="vg-tooltip-key">${vf.replace(/_/g, " ")}:</span>
|
|
1805
|
-
<span class="vg-tooltip-value">${formatTooltipValue(val)}</span>
|
|
1806
|
-
</div>
|
|
1807
|
-
`;
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
if (!html) {
|
|
1813
|
-
const source = tooltipValue || datum;
|
|
1814
|
-
const entries = Object.entries(source).filter(
|
|
1815
|
-
([key]) => !key.startsWith("__") && !key.startsWith("sum_") && key !== "Symbol(vega_id)"
|
|
1816
|
-
);
|
|
1817
|
-
for (const [key, val] of entries) {
|
|
1818
|
-
html += `
|
|
1819
|
-
<div class="vg-tooltip-row">
|
|
1820
|
-
<span class="vg-tooltip-dot" style="background-color: ${color}"></span>
|
|
1821
|
-
<span class="vg-tooltip-key">${key.replace(/_/g, " ")}:</span>
|
|
1822
|
-
<span class="vg-tooltip-value">${formatTooltipValue(val)}</span>
|
|
1823
|
-
</div>
|
|
1824
|
-
`;
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
const headerMatch = html.match(/^(<div class="vg-tooltip-header">[\s\S]*?<\/div>)/);
|
|
1828
|
-
if (headerMatch) {
|
|
1829
|
-
const headerHtml = headerMatch[1];
|
|
1830
|
-
const bodyHtml = html.slice(headerMatch[0].length);
|
|
1831
|
-
el.innerHTML = headerHtml + `<div class="vg-tooltip-body scrollbar">${bodyHtml}</div>`;
|
|
1832
|
-
} else {
|
|
1833
|
-
el.innerHTML = `<div class="vg-tooltip-body scrollbar">${html}</div>`;
|
|
1834
|
-
}
|
|
1835
|
-
el.classList.add("visible");
|
|
1836
|
-
let container = null;
|
|
1837
|
-
try {
|
|
1838
|
-
container = getView()?.container() ?? null;
|
|
1839
|
-
} catch {
|
|
1840
|
-
}
|
|
1841
|
-
positionTooltip2(el, event, container);
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
function findClosestField(missingField, dataKeys, sampleRow, encType) {
|
|
1845
|
-
const lower = missingField.toLowerCase();
|
|
1846
|
-
const keys = Array.from(dataKeys);
|
|
1847
|
-
const ciMatch = keys.find((k) => k.toLowerCase() === lower);
|
|
1848
|
-
if (ciMatch) return ciMatch;
|
|
1849
|
-
const substringMatches = keys.filter(
|
|
1850
|
-
(k) => k.toLowerCase().includes(lower) || lower.includes(k.toLowerCase())
|
|
1851
|
-
);
|
|
1852
|
-
if (substringMatches.length === 1) return substringMatches[0];
|
|
1853
|
-
if (substringMatches.length > 1) {
|
|
1854
|
-
substringMatches.sort(
|
|
1855
|
-
(a, b) => Math.abs(a.length - missingField.length) - Math.abs(b.length - missingField.length)
|
|
1856
|
-
);
|
|
1857
|
-
return substringMatches[0];
|
|
1858
|
-
}
|
|
1859
|
-
if (encType === "quantitative") {
|
|
1860
|
-
const numericKey = keys.find((k) => typeof sampleRow[k] === "number");
|
|
1861
|
-
if (numericKey) return numericKey;
|
|
1862
|
-
}
|
|
1863
|
-
if (encType === "nominal" || encType === "ordinal") {
|
|
1864
|
-
const stringKey = keys.find((k) => typeof sampleRow[k] === "string");
|
|
1865
|
-
if (stringKey) return stringKey;
|
|
1866
|
-
}
|
|
1867
|
-
return null;
|
|
1868
|
-
}
|
|
1869
|
-
function sanitizeSpec(spec) {
|
|
1870
|
-
const data = spec.data;
|
|
1871
|
-
const values = data?.values;
|
|
1872
|
-
if (!values || values.length === 0) return spec;
|
|
1873
|
-
const sampleRow = values[0];
|
|
1874
|
-
const dataKeys = new Set(Object.keys(sampleRow));
|
|
1875
|
-
function collectTransformKeys(transforms, into) {
|
|
1876
|
-
if (!transforms) return;
|
|
1877
|
-
for (const t of transforms) {
|
|
1878
|
-
if (t.fold) {
|
|
1879
|
-
const foldAs = Array.isArray(t.as) ? t.as : ["key", "value"];
|
|
1880
|
-
for (const f of foldAs) into.add(f);
|
|
1881
|
-
}
|
|
1882
|
-
if (t.calculate && typeof t.as === "string") into.add(t.as);
|
|
1883
|
-
if (t.aggregate && Array.isArray(t.aggregate)) {
|
|
1884
|
-
for (const agg of t.aggregate) {
|
|
1885
|
-
if (typeof agg.as === "string") into.add(agg.as);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
if (typeof t.as === "string" && t.lookup) into.add(t.as);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
function fixEncoding(encoding, keys = dataKeys) {
|
|
1892
|
-
const fixed = {};
|
|
1893
|
-
for (const [channel, enc] of Object.entries(encoding)) {
|
|
1894
|
-
if (!enc || typeof enc !== "object" || !("field" in enc)) {
|
|
1895
|
-
fixed[channel] = enc;
|
|
1896
|
-
continue;
|
|
1897
|
-
}
|
|
1898
|
-
const e = { ...enc };
|
|
1899
|
-
const field = e.field;
|
|
1900
|
-
if (field && keys.has(field)) {
|
|
1901
|
-
const sampleValue = sampleRow[field];
|
|
1902
|
-
if (sampleValue !== void 0 && typeof sampleValue === "string" && e.type === "quantitative") {
|
|
1903
|
-
e.type = "nominal";
|
|
1904
|
-
}
|
|
1905
|
-
} else if (field && !keys.has(field)) {
|
|
1906
|
-
const match = findClosestField(field, keys, sampleRow, e.type);
|
|
1907
|
-
if (match) {
|
|
1908
|
-
e.field = match;
|
|
1909
|
-
const matchedValue = sampleRow[match];
|
|
1910
|
-
if (typeof matchedValue === "string" && e.type === "quantitative") {
|
|
1911
|
-
e.type = "nominal";
|
|
1912
|
-
} else if (typeof matchedValue === "number" && (e.type === "nominal" || e.type === "ordinal")) {
|
|
1913
|
-
e.type = "quantitative";
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
fixed[channel] = e;
|
|
1918
|
-
}
|
|
1919
|
-
return fixed;
|
|
1920
|
-
}
|
|
1921
|
-
collectTransformKeys(spec.transform, dataKeys);
|
|
1922
|
-
let result = { ...spec };
|
|
1923
|
-
if (spec.encoding) {
|
|
1924
|
-
result.encoding = fixEncoding(spec.encoding);
|
|
1925
|
-
}
|
|
1926
|
-
if (result.encoding) {
|
|
1927
|
-
const enc = result.encoding;
|
|
1928
|
-
const colorEnc = enc.color;
|
|
1929
|
-
if (colorEnc?.scale && colorEnc.scale.range) {
|
|
1930
|
-
const { range: _range, ...scaleWithoutRange } = colorEnc.scale;
|
|
1931
|
-
result = {
|
|
1932
|
-
...result,
|
|
1933
|
-
encoding: {
|
|
1934
|
-
...enc,
|
|
1935
|
-
color: {
|
|
1936
|
-
...colorEnc,
|
|
1937
|
-
scale: Object.keys(scaleWithoutRange).length > 0 ? scaleWithoutRange : void 0
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
if (spec.layer) {
|
|
1944
|
-
const layers = spec.layer;
|
|
1945
|
-
result.layer = layers.map((layer) => {
|
|
1946
|
-
if (!layer.encoding) return layer;
|
|
1947
|
-
const layerKeys = new Set(dataKeys);
|
|
1948
|
-
collectTransformKeys(layer.transform, layerKeys);
|
|
1949
|
-
return { ...layer, encoding: fixEncoding(layer.encoding, layerKeys) };
|
|
1950
|
-
});
|
|
1951
|
-
}
|
|
1952
|
-
if (result.encoding) {
|
|
1953
|
-
const enc = result.encoding;
|
|
1954
|
-
const xEnc = enc.x;
|
|
1955
|
-
if (xEnc?.type === "temporal" && !xEnc.timeUnit) {
|
|
1956
|
-
const xField = xEnc.field;
|
|
1957
|
-
if (xField && values && values.length > 0) {
|
|
1958
|
-
const sample = values[0][xField];
|
|
1959
|
-
if (typeof sample === "string" && /^\d{4}-\d{2}$/.test(sample)) {
|
|
1960
|
-
result = {
|
|
1961
|
-
...result,
|
|
1962
|
-
encoding: { ...enc, x: { ...xEnc, timeUnit: "yearmonth" } }
|
|
1963
|
-
};
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
const markType2 = typeof result.mark === "string" ? result.mark : result.mark?.type;
|
|
1969
|
-
if (markType2 === "bar") {
|
|
1970
|
-
const enc = result.encoding;
|
|
1971
|
-
if (enc) {
|
|
1972
|
-
const newEnc = { ...enc };
|
|
1973
|
-
const xField = newEnc.x?.field;
|
|
1974
|
-
for (const ch of Object.keys(newEnc)) {
|
|
1975
|
-
const chEnc = newEnc[ch];
|
|
1976
|
-
if (!chEnc || typeof chEnc !== "object") continue;
|
|
1977
|
-
if (chEnc.type !== "temporal") continue;
|
|
1978
|
-
const updated = { ...chEnc };
|
|
1979
|
-
if (ch === "x") {
|
|
1980
|
-
if (!updated.timeUnit) updated.timeUnit = "yearmonth";
|
|
1981
|
-
updated.scale = { ...updated.scale ?? {}, nice: false };
|
|
1982
|
-
const field = updated.field;
|
|
1983
|
-
if (field && values && updated.timeUnit === "yearmonth") {
|
|
1984
|
-
const midpoints = Array.from(
|
|
1985
|
-
new Set(values.map((r) => r[field]).filter((v) => v != null))
|
|
1986
|
-
).map((v) => {
|
|
1987
|
-
const d = new Date(String(v));
|
|
1988
|
-
if (isNaN(d.getTime())) return v;
|
|
1989
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-15`;
|
|
1990
|
-
});
|
|
1991
|
-
if (midpoints.length > 0) {
|
|
1992
|
-
updated.axis = { ...updated.axis ?? {}, values: midpoints };
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
} else if (updated.timeUnit && updated.field === xField) {
|
|
1996
|
-
const { timeUnit: _tu, ...rest } = updated;
|
|
1997
|
-
newEnc[ch] = rest;
|
|
1998
|
-
continue;
|
|
1999
|
-
}
|
|
2000
|
-
newEnc[ch] = updated;
|
|
2001
|
-
}
|
|
2002
|
-
result = { ...result, encoding: newEnc };
|
|
2003
|
-
}
|
|
2004
|
-
const yEnc = result.encoding?.y;
|
|
2005
|
-
if (yEnc?.type === "quantitative" && yEnc.scale?.domain) {
|
|
2006
|
-
const { domain: _domain, ...scaleWithoutDomain } = yEnc.scale;
|
|
2007
|
-
const newYEnc = Object.keys(scaleWithoutDomain).length === 0 ? (({ scale: _scale, ...rest }) => rest)(yEnc) : { ...yEnc, scale: scaleWithoutDomain };
|
|
2008
|
-
result = {
|
|
2009
|
-
...result,
|
|
2010
|
-
encoding: {
|
|
2011
|
-
...result.encoding,
|
|
2012
|
-
y: newYEnc
|
|
2013
|
-
}
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
return result;
|
|
2018
|
-
}
|
|
2019
|
-
function isFullVega(spec) {
|
|
2020
|
-
const schema = typeof spec.$schema === "string" ? spec.$schema : "";
|
|
2021
|
-
if (schema.includes("vega-lite")) return false;
|
|
2022
|
-
if (schema.includes("vega/v")) return true;
|
|
2023
|
-
if ("marks" in spec) return true;
|
|
2024
|
-
return false;
|
|
2025
|
-
}
|
|
2026
|
-
var COMPACT_AXIS_EXPR = [
|
|
2027
|
-
"datum.value >= 1e12 ? format(datum.value / 1e12, ',.1f') + 'T'",
|
|
2028
|
-
"datum.value >= 1e9 ? format(datum.value / 1e9, ',.1f') + 'B'",
|
|
2029
|
-
"datum.value >= 1e6 ? format(datum.value / 1e6, ',.1f') + 'M'",
|
|
2030
|
-
"datum.value >= 1e3 ? format(datum.value / 1e3, ',.1f') + 'K'",
|
|
2031
|
-
"format(datum.value, ',.0f')"
|
|
2032
|
-
].join(" : ");
|
|
2033
|
-
function injectAxisFormat(spec) {
|
|
2034
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return spec;
|
|
2035
|
-
const encoding = spec.encoding;
|
|
2036
|
-
if (!encoding) return spec;
|
|
2037
|
-
const xEnc = encoding.x;
|
|
2038
|
-
const yEnc = encoding.y;
|
|
2039
|
-
const newEncoding = { ...encoding };
|
|
2040
|
-
if (xEnc?.type === "quantitative" && !xEnc.axis) {
|
|
2041
|
-
newEncoding.x = { ...xEnc, axis: { labelExpr: COMPACT_AXIS_EXPR } };
|
|
2042
|
-
}
|
|
2043
|
-
if (yEnc?.type === "quantitative" && !yEnc.axis) {
|
|
2044
|
-
newEncoding.y = { ...yEnc, axis: { labelExpr: COMPACT_AXIS_EXPR } };
|
|
2045
|
-
}
|
|
2046
|
-
return { ...spec, encoding: newEncoding };
|
|
2047
|
-
}
|
|
2048
|
-
function injectAreaGradient(spec) {
|
|
2049
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return spec;
|
|
2050
|
-
const mark = spec.mark;
|
|
2051
|
-
if (!mark) return spec;
|
|
2052
|
-
const markType2 = typeof mark === "string" ? mark : mark?.type;
|
|
2053
|
-
if (markType2 !== "line") return spec;
|
|
2054
|
-
const encoding = spec.encoding;
|
|
2055
|
-
if (!encoding) return spec;
|
|
2056
|
-
const xEnc = encoding.x;
|
|
2057
|
-
if (!xEnc || xEnc.type !== "temporal") return spec;
|
|
2058
|
-
const colorEnc = encoding.color;
|
|
2059
|
-
const { mark: _mark, ...rest } = spec;
|
|
2060
|
-
const markObj = typeof mark === "string" ? {} : { ...mark };
|
|
2061
|
-
delete markObj.type;
|
|
2062
|
-
delete markObj.point;
|
|
2063
|
-
const hoverParams = [
|
|
2064
|
-
{
|
|
2065
|
-
name: "hover",
|
|
2066
|
-
select: { type: "point", on: "pointerover", nearest: true, clear: "pointerout" },
|
|
2067
|
-
empty: false
|
|
2068
|
-
}
|
|
2069
|
-
];
|
|
2070
|
-
const tooltipFields = [];
|
|
2071
|
-
for (const [channel, enc] of Object.entries(encoding)) {
|
|
2072
|
-
if (enc && typeof enc === "object" && "field" in enc) {
|
|
2073
|
-
const e = enc;
|
|
2074
|
-
tooltipFields.push({ field: e.field, type: e.type || "nominal" });
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
const crosshairColor = typeof document !== "undefined" ? getComputedStyle(document.documentElement).getPropertyValue("--muted-foreground").trim() || "#6b7280" : "#6b7280";
|
|
2078
|
-
const crosshairRule = {
|
|
2079
|
-
mark: { type: "rule", strokeWidth: 1, strokeDash: [4, 4] },
|
|
2080
|
-
encoding: {
|
|
2081
|
-
x: xEnc,
|
|
2082
|
-
opacity: { condition: { param: "hover", value: 0.5 }, value: 0 },
|
|
2083
|
-
color: { value: crosshairColor }
|
|
2084
|
-
}
|
|
2085
|
-
};
|
|
2086
|
-
if (colorEnc) {
|
|
2087
|
-
return {
|
|
2088
|
-
...rest,
|
|
2089
|
-
layer: [
|
|
2090
|
-
{
|
|
2091
|
-
mark: { type: "area", opacity: 0.15, line: false, ...markObj },
|
|
2092
|
-
encoding: { ...encoding, y: { ...encoding.y ?? {}, stack: null }, tooltip: tooltipFields }
|
|
2093
|
-
},
|
|
2094
|
-
{
|
|
2095
|
-
mark: { type: "line", strokeWidth: 2, ...markObj },
|
|
2096
|
-
encoding: { ...encoding, tooltip: tooltipFields }
|
|
2097
|
-
},
|
|
2098
|
-
crosshairRule,
|
|
2099
|
-
{
|
|
2100
|
-
params: hoverParams,
|
|
2101
|
-
mark: { type: "point", filled: true, size: 20, ...markObj },
|
|
2102
|
-
encoding: {
|
|
2103
|
-
...encoding,
|
|
2104
|
-
tooltip: tooltipFields,
|
|
2105
|
-
opacity: { condition: { param: "hover", value: 1 }, value: 0 }
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
]
|
|
2109
|
-
};
|
|
2110
|
-
}
|
|
2111
|
-
return {
|
|
2112
|
-
...rest,
|
|
2113
|
-
layer: [
|
|
2114
|
-
{
|
|
2115
|
-
mark: {
|
|
2116
|
-
type: "area",
|
|
2117
|
-
opacity: 0.3,
|
|
2118
|
-
line: false,
|
|
2119
|
-
color: {
|
|
2120
|
-
x1: 1,
|
|
2121
|
-
y1: 1,
|
|
2122
|
-
x2: 1,
|
|
2123
|
-
y2: 0,
|
|
2124
|
-
gradient: "linear",
|
|
2125
|
-
stops: [
|
|
2126
|
-
{ offset: 0, color: "transparent" },
|
|
2127
|
-
{ offset: 1, color: PRIMARY_COLOR }
|
|
2128
|
-
]
|
|
2129
|
-
},
|
|
2130
|
-
...markObj
|
|
2131
|
-
},
|
|
2132
|
-
encoding: { ...encoding, tooltip: tooltipFields }
|
|
2133
|
-
},
|
|
2134
|
-
{
|
|
2135
|
-
mark: {
|
|
2136
|
-
type: "line",
|
|
2137
|
-
strokeWidth: 2,
|
|
2138
|
-
color: PRIMARY_COLOR,
|
|
2139
|
-
...markObj
|
|
2140
|
-
},
|
|
2141
|
-
encoding: { ...encoding, tooltip: tooltipFields }
|
|
2142
|
-
},
|
|
2143
|
-
crosshairRule,
|
|
2144
|
-
{
|
|
2145
|
-
params: hoverParams,
|
|
2146
|
-
mark: { type: "point", filled: true, size: 60, color: PRIMARY_COLOR },
|
|
2147
|
-
encoding: {
|
|
2148
|
-
...encoding,
|
|
2149
|
-
tooltip: tooltipFields,
|
|
2150
|
-
opacity: { condition: { param: "hover", value: 1 }, value: 0 }
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
]
|
|
2154
|
-
};
|
|
2155
|
-
}
|
|
2156
|
-
function injectAreaHover(spec) {
|
|
2157
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return spec;
|
|
2158
|
-
const mark = spec.mark;
|
|
2159
|
-
if (!mark) return spec;
|
|
2160
|
-
const markType2 = typeof mark === "string" ? mark : mark?.type;
|
|
2161
|
-
if (markType2 !== "area") return spec;
|
|
2162
|
-
const encoding = spec.encoding;
|
|
2163
|
-
if (!encoding) return spec;
|
|
2164
|
-
if (spec.params || spec.selection) return spec;
|
|
2165
|
-
const xEnc = encoding.x;
|
|
2166
|
-
if (!xEnc) return spec;
|
|
2167
|
-
const { mark: originalMark, ...rest } = spec;
|
|
2168
|
-
const tooltipFields = [];
|
|
2169
|
-
for (const [channel, enc] of Object.entries(encoding)) {
|
|
2170
|
-
if (enc && typeof enc === "object" && "field" in enc) {
|
|
2171
|
-
const e = enc;
|
|
2172
|
-
tooltipFields.push({ field: e.field, type: e.type || "nominal" });
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
const crosshairColor = typeof document !== "undefined" ? getComputedStyle(document.documentElement).getPropertyValue("--muted-foreground").trim() || "#6b7280" : "#6b7280";
|
|
2176
|
-
return {
|
|
2177
|
-
...rest,
|
|
2178
|
-
layer: [
|
|
2179
|
-
// Original area mark
|
|
2180
|
-
{ mark: originalMark, encoding: { ...encoding, tooltip: tooltipFields } },
|
|
2181
|
-
// Vertical crosshair rule
|
|
2182
|
-
{
|
|
2183
|
-
mark: { type: "rule", strokeWidth: 1, strokeDash: [4, 4] },
|
|
2184
|
-
encoding: {
|
|
2185
|
-
x: xEnc,
|
|
2186
|
-
opacity: { condition: { param: "hover", value: 0.5 }, value: 0 },
|
|
2187
|
-
color: { value: crosshairColor }
|
|
2188
|
-
}
|
|
2189
|
-
},
|
|
2190
|
-
// Invisible point overlay — voronoi covers entire chart area
|
|
2191
|
-
{
|
|
2192
|
-
params: [
|
|
2193
|
-
{
|
|
2194
|
-
name: "hover",
|
|
2195
|
-
select: { type: "point", on: "pointerover", nearest: true, clear: "pointerout" }
|
|
2196
|
-
}
|
|
2197
|
-
],
|
|
2198
|
-
mark: { type: "point", filled: true, size: 60 },
|
|
2199
|
-
encoding: {
|
|
2200
|
-
...encoding,
|
|
2201
|
-
tooltip: tooltipFields,
|
|
2202
|
-
opacity: { condition: { param: "hover", value: 1 }, value: 0 }
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
]
|
|
2206
|
-
};
|
|
2207
|
-
}
|
|
2208
|
-
function injectBarHighlight(spec) {
|
|
2209
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return spec;
|
|
2210
|
-
const mark = spec.mark;
|
|
2211
|
-
if (!mark) return spec;
|
|
2212
|
-
const markType2 = typeof mark === "string" ? mark : mark?.type;
|
|
2213
|
-
if (markType2 !== "bar") return spec;
|
|
2214
|
-
const encoding = spec.encoding;
|
|
2215
|
-
if (!encoding) return spec;
|
|
2216
|
-
if (spec.params || spec.selection) return spec;
|
|
2217
|
-
if (encoding.opacity) return spec;
|
|
2218
|
-
const colorEnc = encoding.color;
|
|
2219
|
-
const colorIsCategorical = colorEnc && (colorEnc.type === "nominal" || colorEnc.type === "ordinal");
|
|
2220
|
-
const xEnc = encoding.x;
|
|
2221
|
-
const yEnc = encoding.y;
|
|
2222
|
-
const isHorizontalBar = xEnc?.type === "quantitative" && (yEnc?.type === "nominal" || yEnc?.type === "ordinal");
|
|
2223
|
-
const categoryAxis = isHorizontalBar ? "y" : "x";
|
|
2224
|
-
return {
|
|
2225
|
-
...spec,
|
|
2226
|
-
params: [
|
|
2227
|
-
{
|
|
2228
|
-
name: "barHover",
|
|
2229
|
-
select: {
|
|
2230
|
-
type: "point",
|
|
2231
|
-
on: "pointerover",
|
|
2232
|
-
clear: "pointerout",
|
|
2233
|
-
...colorIsCategorical ? { encodings: [categoryAxis] } : {}
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
],
|
|
2237
|
-
encoding: {
|
|
2238
|
-
...encoding,
|
|
2239
|
-
opacity: {
|
|
2240
|
-
condition: { param: "barHover", empty: true, value: 1 },
|
|
2241
|
-
value: 0.3
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
function injectPieLabels(spec) {
|
|
2247
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return spec;
|
|
2248
|
-
const mark = spec.mark;
|
|
2249
|
-
if (!mark) return spec;
|
|
2250
|
-
const markType2 = typeof mark === "string" ? mark : mark.type;
|
|
2251
|
-
if (markType2 !== "arc") return spec;
|
|
2252
|
-
const encoding = spec.encoding;
|
|
2253
|
-
if (!encoding) return spec;
|
|
2254
|
-
const thetaEnc = encoding.theta;
|
|
2255
|
-
const colorEnc = encoding.color;
|
|
2256
|
-
if (!thetaEnc?.field) return spec;
|
|
2257
|
-
const valueField = thetaEnc.field;
|
|
2258
|
-
const labelField = colorEnc?.field || valueField;
|
|
2259
|
-
const markObj = typeof mark === "string" ? { type: mark } : { ...mark };
|
|
2260
|
-
markObj.padAngle ??= 0.03;
|
|
2261
|
-
markObj.cornerRadius ??= 4;
|
|
2262
|
-
markObj.stroke ??= "var(--surface-raised, #1a1a1a)";
|
|
2263
|
-
markObj.strokeWidth ??= 2;
|
|
2264
|
-
const outerRadius = markObj.outerRadius ?? 120;
|
|
2265
|
-
const innerRadius = markObj.innerRadius ?? 0;
|
|
2266
|
-
const midRadius = Math.round((innerRadius + outerRadius) / 2);
|
|
2267
|
-
const labelRadius = outerRadius + 44;
|
|
2268
|
-
const { mark: _mark, encoding: arcEncoding, ...rest } = spec;
|
|
2269
|
-
const thetaStackEnc = { ...thetaEnc, stack: true };
|
|
2270
|
-
return {
|
|
2271
|
-
...rest,
|
|
2272
|
-
__pieLabelsInjected: true,
|
|
2273
|
-
// sentinel — prevents injectLayeredArcTooltip from stripping text layers
|
|
2274
|
-
transform: [
|
|
2275
|
-
...spec.transform || [],
|
|
2276
|
-
{ window: [{ op: "sum", field: valueField, as: "__total" }], frame: [null, null] },
|
|
2277
|
-
{ calculate: `datum['${valueField}'] / datum.__total`, as: "__pct" },
|
|
2278
|
-
{ calculate: `format(datum.__pct, '.1%')`, as: "__pctLabel" }
|
|
2279
|
-
],
|
|
2280
|
-
layer: [
|
|
2281
|
-
// 1. Arc slices with hover-dim
|
|
2282
|
-
{
|
|
2283
|
-
params: [{ name: "pieHover", select: { type: "point", on: "pointerover", clear: "pointerout" } }],
|
|
2284
|
-
mark: markObj,
|
|
2285
|
-
encoding: {
|
|
2286
|
-
...arcEncoding,
|
|
2287
|
-
theta: thetaStackEnc,
|
|
2288
|
-
tooltip: [
|
|
2289
|
-
{ field: labelField, type: colorEnc?.type || "nominal" },
|
|
2290
|
-
{ field: valueField, type: "quantitative" },
|
|
2291
|
-
{ field: "__pct", type: "quantitative", title: "Share", format: ".1%" }
|
|
2292
|
-
],
|
|
2293
|
-
opacity: {
|
|
2294
|
-
condition: { param: "pieHover", empty: true, value: 1 },
|
|
2295
|
-
value: 0.65
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
},
|
|
2299
|
-
// 3. Inner percentage label — hidden for slices < 4% (too small to read)
|
|
2300
|
-
{
|
|
2301
|
-
mark: {
|
|
2302
|
-
type: "text",
|
|
2303
|
-
radius: midRadius,
|
|
2304
|
-
fontSize: 11,
|
|
2305
|
-
fontWeight: "bold",
|
|
2306
|
-
align: "center",
|
|
2307
|
-
baseline: "middle",
|
|
2308
|
-
fill: "white"
|
|
2309
|
-
},
|
|
2310
|
-
encoding: {
|
|
2311
|
-
text: { field: "__pctLabel", type: "nominal" },
|
|
2312
|
-
theta: thetaStackEnc,
|
|
2313
|
-
...colorEnc ? { color: { ...colorEnc, legend: null } } : {},
|
|
2314
|
-
opacity: { condition: { test: "datum.__pct >= 0.04", value: 1 }, value: 0 }
|
|
2315
|
-
}
|
|
2316
|
-
},
|
|
2317
|
-
// 4. Outer category label — hidden for slices < 4%, colored to match slice
|
|
2318
|
-
{
|
|
2319
|
-
mark: { type: "text", radius: labelRadius, fontSize: 11, align: "center", baseline: "middle", limit: 120 },
|
|
2320
|
-
encoding: {
|
|
2321
|
-
text: { field: labelField, type: colorEnc?.type || "nominal" },
|
|
2322
|
-
theta: thetaStackEnc,
|
|
2323
|
-
color: colorEnc ?? { value: "#888" },
|
|
2324
|
-
opacity: { condition: { test: "datum.__pct >= 0.04", value: 1 }, value: 0 }
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
]
|
|
2328
|
-
};
|
|
2329
|
-
}
|
|
2330
|
-
function injectLayeredArcTooltip(spec) {
|
|
2331
|
-
const layers = spec.layer;
|
|
2332
|
-
if (!layers || layers.length === 0) return spec;
|
|
2333
|
-
if (spec.__pieLabelsInjected) return spec;
|
|
2334
|
-
const arcLayer = layers.find((layer) => {
|
|
2335
|
-
const m = layer.mark;
|
|
2336
|
-
return (typeof m === "string" ? m : m?.type) === "arc";
|
|
2337
|
-
});
|
|
2338
|
-
if (!arcLayer) return spec;
|
|
2339
|
-
const arcEncoding = arcLayer.encoding;
|
|
2340
|
-
if (!arcEncoding) return spec;
|
|
2341
|
-
const thetaEnc = arcEncoding.theta;
|
|
2342
|
-
const colorEnc = arcEncoding.color;
|
|
2343
|
-
const valueField = thetaEnc?.field;
|
|
2344
|
-
const labelField = colorEnc?.field || valueField;
|
|
2345
|
-
const existingTransforms = (spec.transform || []).filter((t) => {
|
|
2346
|
-
if (t.calculate && (String(t.calculate).includes("/ datum") || String(t.as).includes("pct"))) return false;
|
|
2347
|
-
if (t.window && Array.isArray(t.window) && t.window.some((w) => String(w.as).includes("total"))) return false;
|
|
2348
|
-
return true;
|
|
2349
|
-
});
|
|
2350
|
-
const newTransforms = valueField ? [
|
|
2351
|
-
...existingTransforms,
|
|
2352
|
-
{ window: [{ op: "sum", field: valueField, as: "__total" }], frame: [null, null] },
|
|
2353
|
-
{ calculate: `datum['${valueField}'] / datum.__total`, as: "__pct" },
|
|
2354
|
-
{ calculate: `format(datum.__pct, '.1%')`, as: "__pctLabel" }
|
|
2355
|
-
] : existingTransforms;
|
|
2356
|
-
const hoverParam = {
|
|
2357
|
-
name: "pieHover",
|
|
2358
|
-
select: { type: "point", on: "pointerover", clear: "pointerout" }
|
|
2359
|
-
};
|
|
2360
|
-
let paramsAdded = false;
|
|
2361
|
-
const newLayers = layers.map((layer) => {
|
|
2362
|
-
const m = layer.mark;
|
|
2363
|
-
const markType2 = typeof m === "string" ? m : m?.type;
|
|
2364
|
-
const encoding = layer.encoding;
|
|
2365
|
-
if (markType2 === "text") {
|
|
2366
|
-
return null;
|
|
2367
|
-
}
|
|
2368
|
-
if (markType2 !== "arc" || !encoding) return layer;
|
|
2369
|
-
const result = { ...layer };
|
|
2370
|
-
if (!paramsAdded) {
|
|
2371
|
-
result.params = [...layer.params || [], hoverParam];
|
|
2372
|
-
paramsAdded = true;
|
|
2373
|
-
}
|
|
2374
|
-
const newEncoding = { ...encoding };
|
|
2375
|
-
const tooltipFields = [];
|
|
2376
|
-
if (labelField) tooltipFields.push({ field: labelField, type: colorEnc?.type || "nominal" });
|
|
2377
|
-
if (valueField) tooltipFields.push({ field: valueField, type: "quantitative" });
|
|
2378
|
-
if (valueField) tooltipFields.push({ field: "__pct", type: "quantitative", title: "Share", format: ".1%" });
|
|
2379
|
-
if (tooltipFields.length > 0) newEncoding.tooltip = tooltipFields;
|
|
2380
|
-
newEncoding.opacity = {
|
|
2381
|
-
condition: { param: "pieHover", empty: true, value: 1 },
|
|
2382
|
-
value: 0.3
|
|
2383
|
-
};
|
|
2384
|
-
result.encoding = newEncoding;
|
|
2385
|
-
return result;
|
|
2386
|
-
});
|
|
2387
|
-
return { ...spec, transform: newTransforms, layer: newLayers.filter(Boolean) };
|
|
2388
|
-
}
|
|
2389
|
-
function injectScatterHover(spec) {
|
|
2390
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return spec;
|
|
2391
|
-
const mark = spec.mark;
|
|
2392
|
-
if (!mark) return spec;
|
|
2393
|
-
const markType2 = typeof mark === "string" ? mark : mark?.type;
|
|
2394
|
-
if (markType2 !== "point" && markType2 !== "circle") return spec;
|
|
2395
|
-
const encoding = spec.encoding;
|
|
2396
|
-
if (!encoding) return spec;
|
|
2397
|
-
if (spec.params || spec.selection) return spec;
|
|
2398
|
-
if (encoding.size || encoding.opacity) return spec;
|
|
2399
|
-
return {
|
|
2400
|
-
...spec,
|
|
2401
|
-
params: [
|
|
2402
|
-
{
|
|
2403
|
-
name: "ptHover",
|
|
2404
|
-
select: { type: "point", on: "pointerover", nearest: true, clear: "pointerout" }
|
|
2405
|
-
}
|
|
2406
|
-
],
|
|
2407
|
-
encoding: {
|
|
2408
|
-
...encoding,
|
|
2409
|
-
size: {
|
|
2410
|
-
condition: { param: "ptHover", value: 200 },
|
|
2411
|
-
value: 60
|
|
2412
|
-
},
|
|
2413
|
-
opacity: {
|
|
2414
|
-
condition: { param: "ptHover", empty: true, value: 1 },
|
|
2415
|
-
value: 0.5
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
};
|
|
2419
|
-
}
|
|
2420
|
-
function injectHeatmapHover(spec) {
|
|
2421
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return spec;
|
|
2422
|
-
const mark = spec.mark;
|
|
2423
|
-
if (!mark) return spec;
|
|
2424
|
-
const markType2 = typeof mark === "string" ? mark : mark?.type;
|
|
2425
|
-
if (markType2 !== "rect") return spec;
|
|
2426
|
-
const encoding = spec.encoding;
|
|
2427
|
-
if (!encoding) return spec;
|
|
2428
|
-
const colorEnc = encoding.color;
|
|
2429
|
-
if (!colorEnc || colorEnc.type !== "quantitative") return spec;
|
|
2430
|
-
if (spec.params || spec.selection) return spec;
|
|
2431
|
-
return {
|
|
2432
|
-
...spec,
|
|
2433
|
-
params: [
|
|
2434
|
-
{
|
|
2435
|
-
name: "cellHover",
|
|
2436
|
-
select: { type: "point", on: "pointerover", clear: "pointerout" }
|
|
2437
|
-
}
|
|
2438
|
-
],
|
|
2439
|
-
encoding: {
|
|
2440
|
-
...encoding,
|
|
2441
|
-
stroke: {
|
|
2442
|
-
condition: { param: "cellHover", empty: false, value: "#fff" },
|
|
2443
|
-
value: "transparent"
|
|
2444
|
-
},
|
|
2445
|
-
strokeWidth: {
|
|
2446
|
-
condition: { param: "cellHover", empty: false, value: 2 },
|
|
2447
|
-
value: 0
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
};
|
|
2451
|
-
}
|
|
2452
|
-
function injectLayeredLineHover(spec) {
|
|
2453
|
-
const layers = spec.layer;
|
|
2454
|
-
if (!layers || layers.length === 0) return spec;
|
|
2455
|
-
const lineLayer = layers.find((layer) => {
|
|
2456
|
-
const m = layer.mark;
|
|
2457
|
-
const mt = typeof m === "string" ? m : m?.type;
|
|
2458
|
-
return mt === "line" || mt === "area";
|
|
2459
|
-
});
|
|
2460
|
-
if (!lineLayer) return spec;
|
|
2461
|
-
const hasHoverParams = layers.some((layer) => {
|
|
2462
|
-
const params = layer.params;
|
|
2463
|
-
return params?.some((p) => p.select?.nearest === true);
|
|
2464
|
-
});
|
|
2465
|
-
if (hasHoverParams) return spec;
|
|
2466
|
-
const hasArc = layers.some((layer) => {
|
|
2467
|
-
const m = layer.mark;
|
|
2468
|
-
return (typeof m === "string" ? m : m?.type) === "arc";
|
|
2469
|
-
});
|
|
2470
|
-
if (hasArc) return spec;
|
|
2471
|
-
const encoding = lineLayer.encoding || spec.encoding;
|
|
2472
|
-
if (!encoding) return spec;
|
|
2473
|
-
const xEnc = encoding.x;
|
|
2474
|
-
if (!xEnc) return spec;
|
|
2475
|
-
const tooltipFields = [];
|
|
2476
|
-
for (const [, enc] of Object.entries(encoding)) {
|
|
2477
|
-
if (enc && typeof enc === "object" && "field" in enc) {
|
|
2478
|
-
const e = enc;
|
|
2479
|
-
tooltipFields.push({ field: e.field, type: e.type || "nominal" });
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
const crosshairColor = typeof document !== "undefined" ? getComputedStyle(document.documentElement).getPropertyValue("--muted-foreground").trim() || "#6b7280" : "#6b7280";
|
|
2483
|
-
const updatedLayers = layers.map((layer) => {
|
|
2484
|
-
const layerEnc = layer.encoding;
|
|
2485
|
-
if (layerEnc && !("tooltip" in layerEnc)) {
|
|
2486
|
-
return { ...layer, encoding: { ...layerEnc, tooltip: tooltipFields } };
|
|
2487
|
-
}
|
|
2488
|
-
return layer;
|
|
2489
|
-
});
|
|
2490
|
-
return {
|
|
2491
|
-
...spec,
|
|
2492
|
-
layer: [
|
|
2493
|
-
...updatedLayers,
|
|
2494
|
-
// Crosshair rule
|
|
2495
|
-
{
|
|
2496
|
-
mark: { type: "rule", strokeWidth: 1, strokeDash: [4, 4] },
|
|
2497
|
-
encoding: {
|
|
2498
|
-
x: xEnc,
|
|
2499
|
-
opacity: { condition: { param: "hover", value: 0.5 }, value: 0 },
|
|
2500
|
-
color: { value: crosshairColor }
|
|
2501
|
-
}
|
|
2502
|
-
},
|
|
2503
|
-
// Voronoi point overlay for hover-anywhere
|
|
2504
|
-
{
|
|
2505
|
-
params: [{ name: "hover", select: { type: "point", on: "pointerover", nearest: true, clear: "pointerout" } }],
|
|
2506
|
-
mark: { type: "point", filled: true, size: 60 },
|
|
2507
|
-
encoding: {
|
|
2508
|
-
...encoding,
|
|
2509
|
-
tooltip: tooltipFields,
|
|
2510
|
-
opacity: { condition: { param: "hover", value: 1 }, value: 0 }
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
]
|
|
2514
|
-
};
|
|
2515
|
-
}
|
|
2516
|
-
function injectTooltip(spec) {
|
|
2517
|
-
const encoding = spec.encoding;
|
|
2518
|
-
if (!encoding) return spec;
|
|
2519
|
-
if ("tooltip" in encoding) return spec;
|
|
2520
|
-
const tooltipFields = [];
|
|
2521
|
-
for (const [channel, enc] of Object.entries(encoding)) {
|
|
2522
|
-
if (channel === "tooltip") continue;
|
|
2523
|
-
if (enc && typeof enc === "object" && "field" in enc) {
|
|
2524
|
-
const e = enc;
|
|
2525
|
-
tooltipFields.push({
|
|
2526
|
-
field: e.field,
|
|
2527
|
-
type: e.type || "nominal",
|
|
2528
|
-
...e.title ? { title: e.title } : {}
|
|
2529
|
-
});
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
if (tooltipFields.length === 0) return spec;
|
|
2533
|
-
const mark = spec.mark;
|
|
2534
|
-
const markType2 = typeof mark === "string" ? mark : mark?.type;
|
|
2535
|
-
const isLineOrArea = markType2 === "line" || markType2 === "area";
|
|
2536
|
-
let result = {
|
|
2537
|
-
...spec,
|
|
2538
|
-
encoding: {
|
|
2539
|
-
...encoding,
|
|
2540
|
-
tooltip: tooltipFields
|
|
2541
|
-
}
|
|
2542
|
-
};
|
|
2543
|
-
if (isLineOrArea && !spec.selection && !spec.params && !spec.layer) {
|
|
2544
|
-
const { mark: originalMark, ...restSpec } = spec;
|
|
2545
|
-
return {
|
|
2546
|
-
...restSpec,
|
|
2547
|
-
layer: [
|
|
2548
|
-
// Visual layer (line or area)
|
|
2549
|
-
{ mark: originalMark, encoding: { ...encoding, tooltip: tooltipFields } },
|
|
2550
|
-
// Invisible point layer — voronoi covers entire chart area for hover-anywhere
|
|
2551
|
-
{
|
|
2552
|
-
params: [
|
|
2553
|
-
{
|
|
2554
|
-
name: "hover",
|
|
2555
|
-
select: { type: "point", on: "pointerover", nearest: true, clear: "pointerout" }
|
|
2556
|
-
}
|
|
2557
|
-
],
|
|
2558
|
-
mark: { type: "point", filled: true },
|
|
2559
|
-
encoding: {
|
|
2560
|
-
...encoding,
|
|
2561
|
-
tooltip: tooltipFields,
|
|
2562
|
-
size: { condition: { param: "hover", value: 60 }, value: 0 },
|
|
2563
|
-
opacity: { condition: { param: "hover", value: 1 }, value: 0 }
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
]
|
|
2567
|
-
};
|
|
2568
|
-
}
|
|
2569
|
-
return result;
|
|
2570
|
-
}
|
|
2571
|
-
function isSimpleStandaloneBarSpec(spec) {
|
|
2572
|
-
if (spec.layer || spec.hconcat || spec.vconcat || spec.concat) return false;
|
|
2573
|
-
if (spec.facet || spec.repeat) return false;
|
|
2574
|
-
const mark = spec.mark;
|
|
2575
|
-
if (mark == null) return false;
|
|
2576
|
-
const markType2 = typeof mark === "string" ? mark : mark.type;
|
|
2577
|
-
if (markType2 !== "bar") return false;
|
|
2578
|
-
const enc = spec.encoding;
|
|
2579
|
-
if (!enc || typeof enc !== "object" || Array.isArray(enc)) return false;
|
|
2580
|
-
return true;
|
|
2581
|
-
}
|
|
2582
|
-
function enhancePluginVegaLiteSpec(spec) {
|
|
2583
|
-
const sanitized = sanitizeSpec(spec);
|
|
2584
|
-
const chartMeta = extractChartMeta(sanitized);
|
|
2585
|
-
try {
|
|
2586
|
-
if (isSimpleStandaloneBarSpec(sanitized)) {
|
|
2587
|
-
const specWithFormat2 = injectAxisFormat(sanitized);
|
|
2588
|
-
const specWithBarHL2 = injectBarHighlight(specWithFormat2);
|
|
2589
|
-
const specWithTooltip2 = injectTooltip(specWithBarHL2);
|
|
2590
|
-
return { vlSpec: specWithTooltip2, chartMeta };
|
|
2591
|
-
}
|
|
2592
|
-
const specWithFormat = injectAxisFormat(sanitized);
|
|
2593
|
-
const specWithGradient = injectAreaGradient(specWithFormat);
|
|
2594
|
-
const specWithAreaHover = injectAreaHover(specWithGradient);
|
|
2595
|
-
const specWithLayeredLine = injectLayeredLineHover(specWithAreaHover);
|
|
2596
|
-
const specWithBarHL = injectBarHighlight(specWithLayeredLine);
|
|
2597
|
-
const specWithScatter = injectScatterHover(specWithBarHL);
|
|
2598
|
-
const specWithHeatmap = injectHeatmapHover(specWithScatter);
|
|
2599
|
-
const specWithLabels = injectPieLabels(specWithHeatmap);
|
|
2600
|
-
const specWithArcTooltip = injectLayeredArcTooltip(specWithLabels);
|
|
2601
|
-
const specWithTooltip = injectTooltip(specWithArcTooltip);
|
|
2602
|
-
return { vlSpec: specWithTooltip, chartMeta };
|
|
2603
|
-
} catch (enhanceErr) {
|
|
2604
|
-
console.warn("[VegaChartBlock] Enhanced spec failed, using sanitized spec:", enhanceErr);
|
|
2605
|
-
return { vlSpec: sanitized, chartMeta };
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
function PluginVegaEmbed({
|
|
2609
|
-
spec,
|
|
2610
|
-
isVegaLite,
|
|
2611
|
-
legendOrientation = "top",
|
|
2612
|
-
showLegend = true,
|
|
2613
|
-
showTitle = true,
|
|
2614
|
-
onView,
|
|
2615
|
-
disableTooltip = false,
|
|
2616
|
-
highlight,
|
|
2617
|
-
stripHover = false
|
|
2618
|
-
}) {
|
|
2619
|
-
const [error, setError] = useState(null);
|
|
2620
|
-
const theme = useMemo(() => resolveSebatiTheme(), []);
|
|
2621
|
-
const viewRef = useRef(null);
|
|
2622
|
-
const vegaThemeOpts = useMemo(
|
|
2623
|
-
() => ({ legendOrientation, showLegend, showTitle }),
|
|
2624
|
-
[legendOrientation, showLegend, showTitle]
|
|
2625
|
-
);
|
|
2626
|
-
const { embedSpec, chartMeta, vegaEmbedConfigReplace } = useMemo(() => {
|
|
2627
|
-
const vlConfig = buildVegaTheme(vegaThemeOpts);
|
|
2628
|
-
if (!isVegaLite) {
|
|
2629
|
-
return {
|
|
2630
|
-
embedSpec: spec,
|
|
2631
|
-
chartMeta: null,
|
|
2632
|
-
vegaEmbedConfigReplace: vlConfig
|
|
2633
|
-
};
|
|
2634
|
-
}
|
|
2635
|
-
const { vlSpec, chartMeta: chartMeta2 } = enhancePluginVegaLiteSpec(spec);
|
|
2636
|
-
const highlighted = stripHover ? applyTouchInteraction(vlSpec, highlight ?? null) : vlSpec;
|
|
2637
|
-
return {
|
|
2638
|
-
embedSpec: highlighted,
|
|
2639
|
-
chartMeta: chartMeta2,
|
|
2640
|
-
vegaEmbedConfigReplace: vlConfig
|
|
2641
|
-
};
|
|
2642
|
-
}, [spec, isVegaLite, vegaThemeOpts, highlight, stripHover]);
|
|
2643
|
-
const embedTooltip = useMemo(
|
|
2644
|
-
() => createCustomTooltipHandler(() => viewRef.current, chartMeta),
|
|
2645
|
-
[chartMeta]
|
|
2646
|
-
);
|
|
2647
|
-
const onEmbedError = useCallback((err) => {
|
|
2648
|
-
setError(err ? err.message : null);
|
|
2649
|
-
}, []);
|
|
2650
|
-
const handleViewReady = useCallback(
|
|
2651
|
-
(v) => {
|
|
2652
|
-
viewRef.current = v;
|
|
2653
|
-
onView?.(v);
|
|
2654
|
-
},
|
|
2655
|
-
[onView]
|
|
2656
|
-
);
|
|
2657
|
-
useEffect(() => {
|
|
2658
|
-
setError(null);
|
|
2659
|
-
}, [embedSpec]);
|
|
2660
|
-
useEffect(() => {
|
|
2661
|
-
ensureTooltipStyles();
|
|
2662
|
-
installTouchDismiss();
|
|
2663
|
-
}, []);
|
|
2664
|
-
if (error) {
|
|
2665
|
-
return /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-destructive/10 p-3 text-sm text-destructive", children: [
|
|
2666
|
-
"Chart render error: ",
|
|
2667
|
-
error
|
|
2668
|
-
] });
|
|
2669
|
-
}
|
|
2670
|
-
return /* @__PURE__ */ jsx(
|
|
2671
|
-
VegaChartFlexEmbed,
|
|
2672
|
-
{
|
|
2673
|
-
spec: embedSpec,
|
|
2674
|
-
theme,
|
|
2675
|
-
config: PLUGIN_VEGA_CHART_CONFIG,
|
|
2676
|
-
embedTooltip: disableTooltip ? false : embedTooltip,
|
|
2677
|
-
vegaEmbedConfigReplace,
|
|
2678
|
-
tooltipHideElementId: "vg-tooltip-element",
|
|
2679
|
-
onViewReady: handleViewReady,
|
|
2680
|
-
onEmbedError,
|
|
2681
|
-
className: "w-full [&>svg]:w-full"
|
|
2682
|
-
}
|
|
2683
|
-
);
|
|
2684
|
-
}
|
|
2685
|
-
function isVegaSpec(obj) {
|
|
2686
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return false;
|
|
2687
|
-
const o = obj;
|
|
2688
|
-
return !!(o.$schema || o.mark || o.marks || o.layer || o.data || o.encoding);
|
|
2689
|
-
}
|
|
2690
|
-
function fixMisnestedSpec(obj) {
|
|
2691
|
-
const data = obj.data;
|
|
2692
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) return obj;
|
|
2693
|
-
const hoistKeys = ["mark", "encoding", "layer", "transform", "params", "selection"];
|
|
2694
|
-
const misnestedKeys = hoistKeys.filter((k) => k in data && !(k in obj));
|
|
2695
|
-
if (misnestedKeys.length === 0) return obj;
|
|
2696
|
-
const fixedData = {};
|
|
2697
|
-
const hoisted = {};
|
|
2698
|
-
for (const [k, v] of Object.entries(data)) {
|
|
2699
|
-
if (misnestedKeys.includes(k)) hoisted[k] = v;
|
|
2700
|
-
else fixedData[k] = v;
|
|
2701
|
-
}
|
|
2702
|
-
return { ...obj, data: fixedData, ...hoisted };
|
|
2703
|
-
}
|
|
2704
|
-
function parseVegaJson(content) {
|
|
2705
|
-
const trimmed = content.trim();
|
|
2706
|
-
try {
|
|
2707
|
-
const obj = JSON.parse(trimmed);
|
|
2708
|
-
if (isVegaSpec(obj)) return fixMisnestedSpec(obj);
|
|
2709
|
-
} catch {
|
|
2710
|
-
}
|
|
2711
|
-
const start = trimmed.indexOf("{");
|
|
2712
|
-
if (start < 0) return null;
|
|
2713
|
-
let depth = 0;
|
|
2714
|
-
let inString = false;
|
|
2715
|
-
let escape = false;
|
|
2716
|
-
for (let i = start; i < trimmed.length; i++) {
|
|
2717
|
-
const ch = trimmed[i];
|
|
2718
|
-
if (escape) {
|
|
2719
|
-
escape = false;
|
|
2720
|
-
continue;
|
|
2721
|
-
}
|
|
2722
|
-
if (ch === "\\") {
|
|
2723
|
-
escape = true;
|
|
2724
|
-
continue;
|
|
2725
|
-
}
|
|
2726
|
-
if (ch === '"') {
|
|
2727
|
-
inString = !inString;
|
|
2728
|
-
continue;
|
|
2729
|
-
}
|
|
2730
|
-
if (inString) continue;
|
|
2731
|
-
if (ch === "{") depth++;
|
|
2732
|
-
else if (ch === "}") {
|
|
2733
|
-
depth--;
|
|
2734
|
-
if (depth === 0) {
|
|
2735
|
-
const extracted = trimmed.substring(start, i + 1);
|
|
2736
|
-
try {
|
|
2737
|
-
const obj = JSON.parse(extracted);
|
|
2738
|
-
if (isVegaSpec(obj)) return fixMisnestedSpec(obj);
|
|
2739
|
-
} catch {
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
for (let extra = 1; extra <= 3; extra++) {
|
|
2745
|
-
try {
|
|
2746
|
-
const obj = JSON.parse(trimmed + "}".repeat(extra));
|
|
2747
|
-
if (isVegaSpec(obj)) return fixMisnestedSpec(obj);
|
|
2748
|
-
} catch {
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
let cleaned = trimmed;
|
|
2752
|
-
for (let i = 0; i < 10; i++) {
|
|
2753
|
-
cleaned = cleaned.replace(/[}"'\s]$/, "");
|
|
2754
|
-
for (let extra = 0; extra <= 3; extra++) {
|
|
2755
|
-
try {
|
|
2756
|
-
const obj = JSON.parse(cleaned + "}".repeat(1 + extra));
|
|
2757
|
-
if (isVegaSpec(obj)) return fixMisnestedSpec(obj);
|
|
2758
|
-
} catch {
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
return null;
|
|
2763
|
-
}
|
|
2764
|
-
function VegaChartBlockInner({
|
|
2765
|
-
content,
|
|
2766
|
-
vegaSpec,
|
|
2767
|
-
isStreaming,
|
|
2768
|
-
onExpand,
|
|
2769
|
-
legendOrientation,
|
|
2770
|
-
showLegend,
|
|
2771
|
-
showTitle
|
|
2772
|
-
}) {
|
|
2773
|
-
const isMobile = useIsMobile();
|
|
2774
|
-
const [view, setView] = useState(null);
|
|
2775
|
-
const [selected, setSelected] = useState(null);
|
|
2776
|
-
const parsed = useMemo(() => {
|
|
2777
|
-
if (vegaSpec && typeof vegaSpec === "object" && !Array.isArray(vegaSpec)) {
|
|
2778
|
-
return vegaSpec;
|
|
2779
|
-
}
|
|
2780
|
-
if (content != null && content !== "") {
|
|
2781
|
-
return parseVegaJson(content);
|
|
2782
|
-
}
|
|
2783
|
-
return null;
|
|
2784
|
-
}, [vegaSpec, content]);
|
|
2785
|
-
const rawFallback = content ?? (vegaSpec ? JSON.stringify(vegaSpec) : "");
|
|
2786
|
-
const summary = useMemo(
|
|
2787
|
-
() => parsed && !isFullVega(parsed) ? summarize(parsed) : null,
|
|
2788
|
-
[parsed]
|
|
2789
|
-
);
|
|
2790
|
-
const title = useMemo(() => parsed ? chartTitle(parsed) : null, [parsed]);
|
|
2791
|
-
const highlight = useMemo(() => {
|
|
2792
|
-
if (!selected || !summary) return null;
|
|
2793
|
-
const field = summary.kind === "composition" ? summary.seriesField : summary.bucketField;
|
|
2794
|
-
return field ? { field, value: selected } : null;
|
|
2795
|
-
}, [selected, summary]);
|
|
2796
|
-
useEffect(() => setSelected(null), [parsed]);
|
|
2797
|
-
const useCard = isMobile && !!summary;
|
|
2798
|
-
const cardHasBand = useCard && summary?.kind === "composition";
|
|
2799
|
-
const spec = useMemo(() => {
|
|
2800
|
-
if (!parsed) return null;
|
|
2801
|
-
const base = {
|
|
2802
|
-
...parsed,
|
|
2803
|
-
width: parsed.width ?? "container",
|
|
2804
|
-
height: parsed.height ?? 300
|
|
2805
|
-
};
|
|
2806
|
-
if (!isMobile) return base;
|
|
2807
|
-
return adaptSpecForMobile(base, false, !!summarize(base)).spec;
|
|
2808
|
-
}, [parsed, isMobile]);
|
|
2809
|
-
const scrolls = useMemo(
|
|
2810
|
-
() => isMobile && parsed ? adaptSpecForMobile(parsed).scrolls : false,
|
|
2811
|
-
[isMobile, parsed]
|
|
2812
|
-
);
|
|
2813
|
-
if (isStreaming && !parsed) {
|
|
2814
|
-
return /* @__PURE__ */ jsx(ChartBlockSkeleton, { type: "vega-chart" });
|
|
2815
|
-
}
|
|
2816
|
-
if (!parsed || !spec) {
|
|
2817
|
-
return /* @__PURE__ */ jsx(RawJsonFallback, { content: rawFallback });
|
|
2818
|
-
}
|
|
2819
|
-
const fullVega = isFullVega(parsed);
|
|
2820
|
-
const plot = /* @__PURE__ */ jsx(ChartErrorBoundary, { fallback: /* @__PURE__ */ jsx(RawJsonFallback, { content: rawFallback }), children: /* @__PURE__ */ jsx("div", { className: scrolls ? "overflow-x-auto overscroll-x-contain scrollbar" : void 0, children: /* @__PURE__ */ jsx(
|
|
2821
|
-
PluginVegaEmbed,
|
|
2822
|
-
{
|
|
2823
|
-
spec,
|
|
2824
|
-
isVegaLite: !fullVega,
|
|
2825
|
-
legendOrientation,
|
|
2826
|
-
showLegend: useCard ? false : showLegend,
|
|
2827
|
-
showTitle: useCard ? false : showTitle,
|
|
2828
|
-
onView: setView,
|
|
2829
|
-
disableTooltip: cardHasBand,
|
|
2830
|
-
highlight: useCard ? highlight : null,
|
|
2831
|
-
stripHover: isMobile
|
|
2832
|
-
}
|
|
2833
|
-
) }) });
|
|
2834
|
-
if (useCard) {
|
|
2835
|
-
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
|
|
2836
|
-
ChartCardMobile,
|
|
2837
|
-
{
|
|
2838
|
-
title,
|
|
2839
|
-
summary,
|
|
2840
|
-
view,
|
|
2841
|
-
scrolls,
|
|
2842
|
-
selected,
|
|
2843
|
-
onSelect: setSelected,
|
|
2844
|
-
onExpand,
|
|
2845
|
-
children: plot
|
|
2846
|
-
}
|
|
2847
|
-
) });
|
|
2848
|
-
}
|
|
2849
|
-
return /* @__PURE__ */ jsx("div", { className: "my-4 w-full rounded-xl border bg-surface-raised p-4 max-md:px-2 max-md:pb-2", children: plot });
|
|
2850
|
-
}
|
|
2851
|
-
function vegaChartBlockPropsEqual(a, b) {
|
|
2852
|
-
return a.content === b.content && a.vegaSpec === b.vegaSpec && a.isStreaming === b.isStreaming && a.onExpand === b.onExpand && a.legendOrientation === b.legendOrientation && a.showLegend === b.showLegend && a.showTitle === b.showTitle;
|
|
2853
|
-
}
|
|
2854
|
-
var VegaChartBlock = memo(VegaChartBlockInner, vegaChartBlockPropsEqual);
|
|
2855
|
-
VegaChartBlockInner.displayName = "VegaChartBlock";
|
|
2856
|
-
|
|
2857
|
-
export { VegaChartBlock, enhancePluginVegaLiteSpec, isSimpleStandaloneBarSpec };
|