@funnest/charts-react 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +842 -222
- package/dist/viz/annotations.d.ts +13 -2
- package/dist/viz/axes.d.ts +3 -1
- package/dist/viz/bar.d.ts +7 -1
- package/dist/viz/cartesian-chart.d.ts +28 -6
- package/dist/viz/charts.d.ts +55 -11
- package/dist/viz/context.d.ts +36 -7
- package/dist/viz/horizontal-bar.d.ts +37 -0
- package/dist/viz/index.d.ts +7 -6
- package/dist/viz/legend.d.ts +9 -4
- package/dist/viz/line.d.ts +46 -7
- package/dist/viz/period-selector.d.ts +10 -1
- package/dist/viz/scrubber.d.ts +9 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
bandScale,
|
|
73
73
|
extent,
|
|
74
74
|
linearScale,
|
|
75
|
+
logScale,
|
|
75
76
|
maxLength,
|
|
76
77
|
nearestIndex,
|
|
77
78
|
niceDomain,
|
|
@@ -104,8 +105,11 @@ var VAR = {
|
|
|
104
105
|
grid: "var(--fnv-grid, color-mix(in srgb, CanvasText 9%, transparent))",
|
|
105
106
|
line: "var(--fnv-line, color-mix(in srgb, CanvasText 32%, transparent))",
|
|
106
107
|
bg: "var(--fnv-bg, Canvas)",
|
|
107
|
-
font: "var(--fnv-font, inherit)"
|
|
108
|
+
font: "var(--fnv-font, inherit)",
|
|
109
|
+
accentRing: "color-mix(in srgb, var(--fnv-accent, #6C4DFF) 55%, transparent)"
|
|
108
110
|
};
|
|
111
|
+
var DIM_OPACITY = 0.22;
|
|
112
|
+
var dashFor = (style, width) => style === "dashed" ? `${width * 3} ${width * 2.5}` : style === "dotted" ? `0.1 ${width * 2.6}` : void 0;
|
|
109
113
|
var numberFormat = new Intl.NumberFormat(void 0, { maximumFractionDigits: 2 });
|
|
110
114
|
var compactFormat = new Intl.NumberFormat(void 0, { notation: "compact", maximumFractionDigits: 2 });
|
|
111
115
|
var formatNumber = (v) => Math.abs(v) >= 1e5 ? compactFormat.format(v) : numberFormat.format(v);
|
|
@@ -113,22 +117,36 @@ function seriesY(ctx, s, i) {
|
|
|
113
117
|
const stacked = ctx.stacked?.get(s.id);
|
|
114
118
|
if (stacked) {
|
|
115
119
|
const raw = s.data[i];
|
|
116
|
-
return raw === null || raw === void 0 ? null :
|
|
120
|
+
return raw === null || raw === void 0 ? null : s.scale(stacked.y1[i]);
|
|
117
121
|
}
|
|
118
122
|
const v = s.data[i];
|
|
119
|
-
return v === null || v === void 0 || !Number.isFinite(v) ? null :
|
|
123
|
+
return v === null || v === void 0 || !Number.isFinite(v) ? null : s.scale(v);
|
|
120
124
|
}
|
|
125
|
+
var scaleFor = (ctx, axisId) => (axisId ? ctx.yScales[axisId] : void 0) ?? ctx.y;
|
|
121
126
|
|
|
122
127
|
// src/viz/legend.tsx
|
|
123
128
|
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
124
|
-
function Legend({ items, onItemClick, onItemHover, shape = "circle", className, style }) {
|
|
125
|
-
const swatch = (color) =>
|
|
129
|
+
function Legend({ items, onItemClick, onItemHover, shape = "circle", direction = "row", justify = "flex-start", className, style }) {
|
|
130
|
+
const swatch = (color) => {
|
|
131
|
+
switch (shape) {
|
|
132
|
+
case "line":
|
|
133
|
+
return { width: 12, height: 3, borderRadius: 2, background: color };
|
|
134
|
+
case "pill":
|
|
135
|
+
return { width: 14, height: 6, borderRadius: 3, background: color };
|
|
136
|
+
case "square":
|
|
137
|
+
return { width: 8, height: 8, borderRadius: 1, background: color };
|
|
138
|
+
case "squircle":
|
|
139
|
+
return { width: 9, height: 9, borderRadius: 3, background: color };
|
|
140
|
+
default:
|
|
141
|
+
return { width: 8, height: 8, borderRadius: "50%", background: color };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
126
144
|
return /* @__PURE__ */ jsx2(
|
|
127
145
|
"div",
|
|
128
146
|
{
|
|
129
147
|
className,
|
|
130
148
|
role: onItemClick ? "group" : "list",
|
|
131
|
-
style: { display: "flex", flexWrap: "wrap", gap: "4px 14px", padding: "4px 0", fontSize: 12, color: VAR.text, fontFamily: VAR.font, ...style },
|
|
149
|
+
style: { display: "flex", flexDirection: direction, flexWrap: direction === "row" ? "wrap" : "nowrap", justifyContent: justify, gap: direction === "row" ? "4px 14px" : 8, padding: "4px 0", fontSize: 12, color: VAR.text, fontFamily: VAR.font, ...style },
|
|
132
150
|
children: items.map((item) => {
|
|
133
151
|
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
134
152
|
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", style: { flex: "none", ...swatch(item.color) } }),
|
|
@@ -157,6 +175,7 @@ function Legend({ items, onItemClick, onItemHover, shape = "circle", className,
|
|
|
157
175
|
// src/viz/cartesian-chart.tsx
|
|
158
176
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
159
177
|
var DEFAULT_HEIGHT = 240;
|
|
178
|
+
var DEFAULT_AXIS = "default";
|
|
160
179
|
function resolveInset(inset) {
|
|
161
180
|
const d = typeof inset === "number" ? inset : 8;
|
|
162
181
|
const o = typeof inset === "object" ? inset : {};
|
|
@@ -196,6 +215,7 @@ function useElementWidth(fixed) {
|
|
|
196
215
|
function prefersReducedMotion() {
|
|
197
216
|
return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
198
217
|
}
|
|
218
|
+
var normalizeLegend = (legend) => legend === true ? { position: "top" } : legend === false || legend === void 0 ? null : typeof legend === "string" ? { position: legend } : legend;
|
|
199
219
|
function CartesianChart({
|
|
200
220
|
series,
|
|
201
221
|
width: fixedWidth,
|
|
@@ -206,9 +226,11 @@ function CartesianChart({
|
|
|
206
226
|
stacked = false,
|
|
207
227
|
enableScrubbing = false,
|
|
208
228
|
onScrubberPositionChange,
|
|
229
|
+
getScrubberAccessibilityLabel,
|
|
209
230
|
animate = true,
|
|
210
231
|
legend = false,
|
|
211
232
|
legendValueFormatter,
|
|
233
|
+
emptyState,
|
|
212
234
|
palette,
|
|
213
235
|
className,
|
|
214
236
|
style,
|
|
@@ -218,11 +240,29 @@ function CartesianChart({
|
|
|
218
240
|
const id = `fnv${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
219
241
|
const [wrapRef, width] = useElementWidth(fixedWidth);
|
|
220
242
|
const [scrubIndex, setScrubIndex] = useState2(null);
|
|
243
|
+
const [focused, setFocused] = useState2(false);
|
|
221
244
|
const [motion, setMotion] = useState2(false);
|
|
245
|
+
const [hidden, setHidden] = useState2([]);
|
|
246
|
+
const [emphasis, setEmphasis] = useState2(null);
|
|
222
247
|
useEffect2(() => setMotion(animate && !prefersReducedMotion()), [animate]);
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
[
|
|
248
|
+
const legendCfg = normalizeLegend(legend);
|
|
249
|
+
const axes = useMemo(() => {
|
|
250
|
+
const list = Array.isArray(yAxis) ? yAxis : [yAxis ?? {}];
|
|
251
|
+
return (list.length ? list : [{}]).map((a, i) => ({ ...a, id: a.id ?? (i === 0 ? DEFAULT_AXIS : `axis${i}`) }));
|
|
252
|
+
}, [yAxis]);
|
|
253
|
+
const firstAxis = axes[0].id;
|
|
254
|
+
const all = useMemo(
|
|
255
|
+
() => series.map((s, index) => {
|
|
256
|
+
const hasRange = s.data.some((v) => Array.isArray(v));
|
|
257
|
+
const high = s.data.map((v) => Array.isArray(v) ? Math.max(v[0], v[1]) : v);
|
|
258
|
+
const low = hasRange ? s.data.map((v) => Array.isArray(v) ? Math.min(v[0], v[1]) : v) : null;
|
|
259
|
+
return { ...s, data: high, low, index, color: s.color ?? seriesColor(index, palette), dimmed: false, axisId: s.yAxisId ?? firstAxis };
|
|
260
|
+
}),
|
|
261
|
+
[series, palette, firstAxis]
|
|
262
|
+
);
|
|
263
|
+
const visible = useMemo(
|
|
264
|
+
() => all.filter((s) => !hidden.includes(s.id)).map((s) => ({ ...s, dimmed: emphasis !== null && emphasis !== s.id })),
|
|
265
|
+
[all, hidden, emphasis]
|
|
226
266
|
);
|
|
227
267
|
const axisSpace = useMemo(() => {
|
|
228
268
|
const acc = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
@@ -236,26 +276,33 @@ function CartesianChart({
|
|
|
236
276
|
const right = Math.max(left, width - ins.right - axisSpace.right);
|
|
237
277
|
const bottom = Math.max(top, height - ins.bottom - axisSpace.bottom);
|
|
238
278
|
const plot = { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
239
|
-
const length = Math.max(maxLength(
|
|
279
|
+
const length = Math.max(maxLength(all), xAxis?.data?.length ?? 0);
|
|
240
280
|
const isBand = xAxis?.scaleType === "band";
|
|
241
281
|
const band = isBand ? bandScale(length, [left, right], xAxis?.bandPadding ?? 0.25) : null;
|
|
242
282
|
const xLinear = linearScale([0, Math.max(1, length - 1)], [left, right]);
|
|
243
283
|
const x = band ? (i) => band.center(i) : (i) => length <= 1 ? (left + right) / 2 : xLinear(i);
|
|
244
|
-
let stackedMap = null;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
284
|
+
let stackedMap = stacked ? /* @__PURE__ */ new Map() : null;
|
|
285
|
+
const yScales = {};
|
|
286
|
+
for (const axis of axes) {
|
|
287
|
+
const on = all.filter((s) => s.axisId === axis.id);
|
|
288
|
+
let [min, max] = extent([...on, ...on.flatMap((s) => s.low ? [{ id: `${s.id}~low`, data: s.low }] : [])]) ?? [0, 1];
|
|
289
|
+
if (stacked) {
|
|
290
|
+
for (const st of stack(visible.filter((s) => s.axisId === axis.id))) stackedMap.set(st.id, st);
|
|
291
|
+
if (on.length) [min, max] = stackedExtent(stack(on));
|
|
292
|
+
}
|
|
293
|
+
const log = axis.scaleType === "log";
|
|
294
|
+
if (!log && (axis.includeZero ?? isBand)) {
|
|
295
|
+
min = Math.min(min, 0);
|
|
296
|
+
max = Math.max(max, 0);
|
|
297
|
+
}
|
|
298
|
+
if (axis.nice && !log) [min, max] = niceDomain(min, max, Math.max(2, Math.round(plot.height / 50)));
|
|
299
|
+
if (axis.domain?.min !== void 0) min = axis.domain.min;
|
|
300
|
+
if (axis.domain?.max !== void 0) max = axis.domain.max;
|
|
301
|
+
yScales[axis.id] = log ? logScale([Math.max(min, Number.MIN_VALUE), max], [bottom, top]) : linearScale([min, max], [bottom, top]);
|
|
254
302
|
}
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const y = linearScale([min, max], [bottom, top]);
|
|
303
|
+
if (stackedMap && stackedMap.size === 0) stackedMap = null;
|
|
304
|
+
const y = yScales[firstAxis];
|
|
305
|
+
const resolved = visible.map(({ axisId, ...s }) => ({ ...s, scale: yScales[axisId] ?? y }));
|
|
259
306
|
const xData = xAxis?.data;
|
|
260
307
|
const xLabel = (i) => {
|
|
261
308
|
const v = xData?.[i];
|
|
@@ -272,13 +319,15 @@ function CartesianChart({
|
|
|
272
319
|
x,
|
|
273
320
|
band,
|
|
274
321
|
y,
|
|
322
|
+
yScales,
|
|
275
323
|
stacked: stackedMap,
|
|
276
324
|
xLabel,
|
|
277
325
|
xData,
|
|
278
326
|
scrubIndex,
|
|
327
|
+
focused,
|
|
279
328
|
animate: motion
|
|
280
329
|
};
|
|
281
|
-
}, [id, width, height, inset, axisSpace,
|
|
330
|
+
}, [id, width, height, inset, axisSpace, all, visible, axes, firstAxis, xAxis, stacked, scrubIndex, focused, motion]);
|
|
282
331
|
const xsRef = useRef2([]);
|
|
283
332
|
xsRef.current = useMemo(() => Array.from({ length: ctx.length }, (_, i) => ctx.x(i)), [ctx.length, ctx.x]);
|
|
284
333
|
const updateScrub = useCallback(
|
|
@@ -291,51 +340,111 @@ function CartesianChart({
|
|
|
291
340
|
[onScrubberPositionChange]
|
|
292
341
|
);
|
|
293
342
|
const onPointerMove = (e) => {
|
|
294
|
-
if (!enableScrubbing) return;
|
|
295
343
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
296
344
|
const px = e.clientX - rect.left;
|
|
297
345
|
const i = ctx.band ? ctx.band.invert(px) : nearestIndex(xsRef.current, px);
|
|
298
346
|
updateScrub(i < 0 ? null : i);
|
|
299
347
|
};
|
|
300
|
-
const onPointerLeave = () =>
|
|
301
|
-
|
|
302
|
-
|
|
348
|
+
const onPointerLeave = () => {
|
|
349
|
+
if (!focused) updateScrub(null);
|
|
350
|
+
};
|
|
351
|
+
const onKeyDown = (e) => {
|
|
352
|
+
const last = ctx.length - 1;
|
|
353
|
+
if (last < 0) return;
|
|
354
|
+
const step = e.shiftKey ? 10 : 1;
|
|
355
|
+
const cur = scrubIndex ?? last;
|
|
356
|
+
let next;
|
|
357
|
+
if (e.key === "ArrowLeft") next = Math.max(0, (scrubIndex === null ? last + 1 : cur) - step);
|
|
358
|
+
else if (e.key === "ArrowRight") next = Math.min(last, (scrubIndex === null ? -1 : cur) + step);
|
|
359
|
+
else if (e.key === "Home") next = 0;
|
|
360
|
+
else if (e.key === "End") next = last;
|
|
361
|
+
else if (e.key === "Escape") next = null;
|
|
362
|
+
if (next === void 0) return;
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
updateScrub(next);
|
|
365
|
+
};
|
|
366
|
+
const hasData = all.some((s) => s.data.some((v) => v !== null && Number.isFinite(v)));
|
|
367
|
+
const a11yText = (i) => getScrubberAccessibilityLabel ? getScrubberAccessibilityLabel(i) : [ctx.xLabel(i), ...ctx.series.map((s) => `${s.label ?? s.id} ${s.data[i] === null || s.data[i] === void 0 ? "\u2014" : formatNumber(s.data[i])}`)].join(", ");
|
|
368
|
+
const legendEl = legendCfg && /* @__PURE__ */ jsx3(
|
|
303
369
|
Legend,
|
|
304
370
|
{
|
|
305
|
-
|
|
371
|
+
direction: legendCfg.position === "left" || legendCfg.position === "right" ? "column" : "row",
|
|
372
|
+
...legendCfg.shape ? { shape: legendCfg.shape } : {},
|
|
373
|
+
items: all.map((s) => {
|
|
306
374
|
const v = scrubIndex === null ? null : s.data[scrubIndex];
|
|
307
375
|
return {
|
|
308
376
|
id: s.id,
|
|
309
377
|
label: s.label ?? s.id,
|
|
310
378
|
color: s.color,
|
|
379
|
+
inactive: hidden.includes(s.id) || emphasis !== null && emphasis !== s.id,
|
|
311
380
|
value: v === null || v === void 0 ? void 0 : legendValueFormatter ? legendValueFormatter(v, s.id) : formatNumber(v)
|
|
312
381
|
};
|
|
313
|
-
})
|
|
382
|
+
}),
|
|
383
|
+
...legendCfg.interactive ? {
|
|
384
|
+
onItemClick: (sid) => setHidden((h) => h.includes(sid) ? h.filter((x) => x !== sid) : [...h, sid]),
|
|
385
|
+
onItemHover: (sid) => setEmphasis(sid && !hidden.includes(sid) ? sid : null)
|
|
386
|
+
} : {}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
const side = legendCfg?.position === "left" || legendCfg?.position === "right";
|
|
390
|
+
const svg = /* @__PURE__ */ jsxs2(
|
|
391
|
+
"svg",
|
|
392
|
+
{
|
|
393
|
+
width: width || "100%",
|
|
394
|
+
height,
|
|
395
|
+
role: "img",
|
|
396
|
+
"aria-label": ariaLabel,
|
|
397
|
+
tabIndex: enableScrubbing && hasData ? 0 : void 0,
|
|
398
|
+
style: {
|
|
399
|
+
display: "block",
|
|
400
|
+
overflow: "visible",
|
|
401
|
+
touchAction: enableScrubbing ? "pan-y" : void 0,
|
|
402
|
+
userSelect: "none",
|
|
403
|
+
outline: "none",
|
|
404
|
+
borderRadius: 6,
|
|
405
|
+
boxShadow: focused ? `0 0 0 2px ${VAR.accentRing}` : void 0
|
|
406
|
+
},
|
|
407
|
+
onPointerMove: enableScrubbing ? onPointerMove : void 0,
|
|
408
|
+
onPointerDown: enableScrubbing ? onPointerMove : void 0,
|
|
409
|
+
onPointerLeave: enableScrubbing ? onPointerLeave : void 0,
|
|
410
|
+
onPointerCancel: enableScrubbing ? onPointerLeave : void 0,
|
|
411
|
+
onKeyDown: enableScrubbing ? onKeyDown : void 0,
|
|
412
|
+
onFocus: enableScrubbing ? () => setFocused(true) : void 0,
|
|
413
|
+
onBlur: enableScrubbing ? () => {
|
|
414
|
+
setFocused(false);
|
|
415
|
+
updateScrub(null);
|
|
416
|
+
} : void 0,
|
|
417
|
+
children: [
|
|
418
|
+
width > 0 && hasData && /* @__PURE__ */ jsx3(CartesianContext.Provider, { value: ctx, children }),
|
|
419
|
+
width > 0 && !hasData && /* @__PURE__ */ jsx3("text", { x: width / 2, y: height / 2, textAnchor: "middle", dominantBaseline: "central", fontSize: 13, style: { fill: VAR.text, fontFamily: VAR.font }, children: typeof emptyState === "string" || emptyState === void 0 ? emptyState ?? "" : null })
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
return /* @__PURE__ */ jsxs2(
|
|
424
|
+
"div",
|
|
425
|
+
{
|
|
426
|
+
ref: wrapRef,
|
|
427
|
+
className,
|
|
428
|
+
style: {
|
|
429
|
+
position: "relative",
|
|
430
|
+
width: fixedWidth ?? "100%",
|
|
431
|
+
...side ? { display: "flex", alignItems: "center", gap: 16, flexDirection: legendCfg?.position === "left" ? "row-reverse" : "row" } : {},
|
|
432
|
+
...style
|
|
433
|
+
},
|
|
434
|
+
children: [
|
|
435
|
+
legendCfg?.position === "top" || legendCfg && !legendCfg.position ? legendEl : null,
|
|
436
|
+
side ? /* @__PURE__ */ jsx3("div", { style: { flex: 1, minWidth: 0 }, children: svg }) : svg,
|
|
437
|
+
side && legendEl,
|
|
438
|
+
legendCfg?.position === "bottom" && legendEl,
|
|
439
|
+
!hasData && emptyState !== void 0 && typeof emptyState !== "string" && /* @__PURE__ */ jsx3("div", { style: { position: "absolute", inset: 0, display: "grid", placeItems: "center" }, children: emptyState }),
|
|
440
|
+
enableScrubbing && /* @__PURE__ */ jsx3("span", { "aria-live": "polite", style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: focused && scrubIndex !== null ? a11yText(scrubIndex) : "" })
|
|
441
|
+
]
|
|
314
442
|
}
|
|
315
443
|
);
|
|
316
|
-
return /* @__PURE__ */ jsxs2("div", { ref: wrapRef, className, style: { position: "relative", width: fixedWidth ?? "100%", ...style }, children: [
|
|
317
|
-
legendPos === "top" && legendEl,
|
|
318
|
-
/* @__PURE__ */ jsx3(
|
|
319
|
-
"svg",
|
|
320
|
-
{
|
|
321
|
-
width: width || "100%",
|
|
322
|
-
height,
|
|
323
|
-
role: "img",
|
|
324
|
-
"aria-label": ariaLabel,
|
|
325
|
-
style: { display: "block", overflow: "visible", touchAction: enableScrubbing ? "pan-y" : void 0, userSelect: "none" },
|
|
326
|
-
onPointerMove: enableScrubbing ? onPointerMove : void 0,
|
|
327
|
-
onPointerDown: enableScrubbing ? onPointerMove : void 0,
|
|
328
|
-
onPointerLeave: enableScrubbing ? onPointerLeave : void 0,
|
|
329
|
-
onPointerCancel: enableScrubbing ? onPointerLeave : void 0,
|
|
330
|
-
children: width > 0 && /* @__PURE__ */ jsx3(CartesianContext.Provider, { value: ctx, children })
|
|
331
|
-
}
|
|
332
|
-
),
|
|
333
|
-
legendPos === "bottom" && legendEl
|
|
334
|
-
] });
|
|
335
444
|
}
|
|
336
445
|
|
|
337
446
|
// src/viz/axes.tsx
|
|
338
|
-
import { sampleIndices, ticks as niceTicks } from "@funnest/charts-viz";
|
|
447
|
+
import { logTicks, sampleIndices, ticks as niceTicks } from "@funnest/charts-viz";
|
|
339
448
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
340
449
|
function XAxis({
|
|
341
450
|
position = "bottom",
|
|
@@ -373,14 +482,17 @@ function YAxis({
|
|
|
373
482
|
width = 48,
|
|
374
483
|
ticks,
|
|
375
484
|
tickLabelFormatter = formatNumber,
|
|
485
|
+
axisId,
|
|
376
486
|
showGrid = true,
|
|
377
487
|
showLine = false,
|
|
378
488
|
minTickSpacing = 32
|
|
379
489
|
}) {
|
|
380
490
|
const ctx = useCartesianChart();
|
|
381
|
-
const { plot
|
|
491
|
+
const { plot } = ctx;
|
|
492
|
+
const y = scaleFor(ctx, axisId);
|
|
382
493
|
const [d0, d1] = y.domain;
|
|
383
|
-
const
|
|
494
|
+
const count = Math.max(2, Math.floor(plot.height / minTickSpacing));
|
|
495
|
+
const values = (ticks ?? (y.kind === "log" ? logTicks(d0, d1, count) : niceTicks(d0, d1, count))).filter(
|
|
384
496
|
(v) => v >= Math.min(d0, d1) - 1e-9 && v <= Math.max(d0, d1) + 1e-9
|
|
385
497
|
);
|
|
386
498
|
const right = position === "right";
|
|
@@ -396,114 +508,229 @@ YAxis.axisSpace = (p) => ({ side: p.position ?? "right", size: p.width ?? 48 });
|
|
|
396
508
|
|
|
397
509
|
// src/viz/line.tsx
|
|
398
510
|
import { areaPath, linePath, segments } from "@funnest/charts-viz";
|
|
399
|
-
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
511
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
400
512
|
function seriesRuns(ctx, s, connectNulls = false) {
|
|
401
513
|
const st = ctx.stacked?.get(s.id);
|
|
402
|
-
const
|
|
403
|
-
const
|
|
514
|
+
const y = s.scale;
|
|
515
|
+
const top = (i, v) => y(st ? st.y1[i] : v);
|
|
516
|
+
const bottom = (i) => st ? y(st.y0[i]) : s.low ? y(s.low[i] ?? s.data[i]) : null;
|
|
404
517
|
const runs = connectNulls ? [{ start: 0, values: s.data.map((v, i) => v === null ? null : i).filter((i) => i !== null) }] : segments(s.data).map((r) => ({ start: r.start, values: r.values.map((_, k) => r.start + k) }));
|
|
405
518
|
return runs.filter((r) => r.values.length > 0).map((r) => {
|
|
406
519
|
const idx = r.values;
|
|
407
520
|
const upper = idx.map((i) => ({ x: ctx.x(i), y: top(i, s.data[i]) }));
|
|
408
|
-
const lower = st ? idx.map((i) => ({ x: ctx.x(i), y: bottom(i) })) : null;
|
|
521
|
+
const lower = st || s.low ? idx.map((i) => ({ x: ctx.x(i), y: bottom(i) })) : null;
|
|
409
522
|
return { upper, lower };
|
|
410
523
|
});
|
|
411
524
|
}
|
|
412
|
-
function baselineY(ctx) {
|
|
413
|
-
const
|
|
525
|
+
function baselineY(ctx, scale, baseline) {
|
|
526
|
+
const { top, bottom } = ctx.plot;
|
|
527
|
+
if (baseline !== void 0) return Math.min(bottom, Math.max(top, scale(baseline)));
|
|
528
|
+
const [d0, d1] = scale.domain;
|
|
414
529
|
const lo = Math.min(d0, d1);
|
|
415
530
|
const hi = Math.max(d0, d1);
|
|
416
|
-
return lo <= 0 && hi >= 0 ?
|
|
531
|
+
return scale.kind === "linear" && lo <= 0 && hi >= 0 ? scale(0) : bottom;
|
|
532
|
+
}
|
|
533
|
+
function SplitClips({ id, y, ctx }) {
|
|
534
|
+
const { left, right, top, bottom } = ctx.plot;
|
|
535
|
+
const pad = 20;
|
|
536
|
+
return /* @__PURE__ */ jsxs4("defs", { children: [
|
|
537
|
+
/* @__PURE__ */ jsx5("clipPath", { id: `${id}-above`, children: /* @__PURE__ */ jsx5("rect", { x: left - pad, y: top - pad, width: right - left + pad * 2, height: Math.max(0, y - top + pad) }) }),
|
|
538
|
+
/* @__PURE__ */ jsx5("clipPath", { id: `${id}-below`, children: /* @__PURE__ */ jsx5("rect", { x: left - pad, y, width: right - left + pad * 2, height: Math.max(0, bottom - y + pad) }) })
|
|
539
|
+
] });
|
|
417
540
|
}
|
|
418
|
-
function Area({ seriesId, curve = "monotone", fill = "gradient", opacity, connectNulls }) {
|
|
541
|
+
function Area({ seriesId, curve = "monotone", fill = "gradient", opacity, connectNulls, baseline, splitColors, gradientStops }) {
|
|
419
542
|
const ctx = useCartesianChart();
|
|
420
543
|
const s = ctx.getSeries(seriesId);
|
|
421
544
|
if (!s) return null;
|
|
422
545
|
const gid = `${ctx.id}-area-${s.index}`;
|
|
423
|
-
const base = baselineY(ctx);
|
|
424
|
-
const alpha = opacity ?? (fill === "gradient" ? 0.24 : 0.16);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
546
|
+
const base = baselineY(ctx, s.scale, baseline);
|
|
547
|
+
const alpha = opacity ?? (fill === "gradient" ? 0.24 : fill === "dotted" ? 0.5 : 0.16);
|
|
548
|
+
const runs = seriesRuns(ctx, s, connectNulls);
|
|
549
|
+
const dim = s.dimmed ? DIM_OPACITY : 1;
|
|
550
|
+
const paint = (key, color, from, clip) => {
|
|
551
|
+
const fillId = `${gid}-paint-${key}`;
|
|
552
|
+
const stops = gradientStops ?? [
|
|
553
|
+
{ offset: 0, opacity: alpha },
|
|
554
|
+
{ offset: 1, opacity: 0 }
|
|
555
|
+
];
|
|
556
|
+
return /* @__PURE__ */ jsxs4("g", { clipPath: clip ? `url(#${clip})` : void 0, opacity: dim, children: [
|
|
557
|
+
/* @__PURE__ */ jsxs4("defs", { children: [
|
|
558
|
+
fill === "gradient" && /* @__PURE__ */ jsx5("linearGradient", { id: fillId, x1: "0", y1: from, x2: "0", y2: base, gradientUnits: "userSpaceOnUse", children: stops.map((st, i) => /* @__PURE__ */ jsx5("stop", { offset: st.offset, stopOpacity: st.opacity, style: { stopColor: st.color ?? color } }, i)) }),
|
|
559
|
+
fill === "dotted" && /* @__PURE__ */ jsx5("pattern", { id: fillId, width: "6", height: "6", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx5("circle", { cx: "1.5", cy: "1.5", r: "1.1", style: { fill: color } }) })
|
|
560
|
+
] }),
|
|
561
|
+
runs.map((run, k) => /* @__PURE__ */ jsx5(
|
|
562
|
+
"path",
|
|
563
|
+
{
|
|
564
|
+
d: areaPath(run.upper, run.lower ?? base, curve),
|
|
565
|
+
fillOpacity: fill === "gradient" ? void 0 : alpha,
|
|
566
|
+
stroke: "none",
|
|
567
|
+
style: { fill: fill === "solid" ? color : `url(#${fillId})` },
|
|
568
|
+
children: ctx.animate && /* @__PURE__ */ jsx5("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.6s", fill: "freeze" })
|
|
569
|
+
},
|
|
570
|
+
k
|
|
571
|
+
))
|
|
572
|
+
] }, key);
|
|
573
|
+
};
|
|
574
|
+
if (splitColors && baseline !== void 0 && !run0IsStacked(ctx, s)) {
|
|
575
|
+
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
576
|
+
/* @__PURE__ */ jsx5(SplitClips, { id: gid, y: base, ctx }),
|
|
577
|
+
paint("above", splitColors.above, ctx.plot.top, `${gid}-above`),
|
|
578
|
+
paint("below", splitColors.below, ctx.plot.bottom, `${gid}-below`)
|
|
579
|
+
] });
|
|
580
|
+
}
|
|
581
|
+
return /* @__PURE__ */ jsx5("g", { children: paint("fill", s.color, ctx.plot.top) });
|
|
442
582
|
}
|
|
583
|
+
var run0IsStacked = (ctx, s) => !!ctx.stacked?.get(s.id);
|
|
443
584
|
function Line({
|
|
444
585
|
seriesId,
|
|
445
586
|
curve = "monotone",
|
|
446
587
|
strokeWidth = 2,
|
|
588
|
+
lineStyle,
|
|
447
589
|
dash,
|
|
448
590
|
connectNulls,
|
|
449
591
|
area = "none",
|
|
450
592
|
areaOpacity,
|
|
451
|
-
opacity
|
|
593
|
+
opacity,
|
|
594
|
+
points,
|
|
595
|
+
baseline,
|
|
596
|
+
splitColors
|
|
452
597
|
}) {
|
|
453
598
|
const ctx = useCartesianChart();
|
|
454
599
|
const s = ctx.getSeries(seriesId);
|
|
455
600
|
if (!s) return null;
|
|
456
|
-
const
|
|
601
|
+
const style = lineStyle ?? s.lineStyle ?? "solid";
|
|
602
|
+
const dashArray = dash ?? dashFor(style, strokeWidth);
|
|
603
|
+
const drawIn = ctx.animate && !dashArray;
|
|
604
|
+
const runs = seriesRuns(ctx, s, connectNulls);
|
|
605
|
+
const split = splitColors && baseline !== void 0;
|
|
606
|
+
const gid = `${ctx.id}-line-${s.index}`;
|
|
607
|
+
const baseY = baselineY(ctx, s.scale, baseline);
|
|
608
|
+
const strokeOpacity = (opacity ?? 1) * (s.dimmed ? DIM_OPACITY : 1);
|
|
609
|
+
const strokes = (color, clip) => /* @__PURE__ */ jsx5("g", { clipPath: clip ? `url(#${clip})` : void 0, children: runs.map((run, k) => /* @__PURE__ */ jsx5(
|
|
610
|
+
"path",
|
|
611
|
+
{
|
|
612
|
+
d: linePath(run.upper, curve),
|
|
613
|
+
fill: "none",
|
|
614
|
+
strokeWidth,
|
|
615
|
+
strokeLinecap: "round",
|
|
616
|
+
strokeLinejoin: "round",
|
|
617
|
+
strokeDasharray: dashArray ?? (drawIn ? "1" : void 0),
|
|
618
|
+
strokeOpacity,
|
|
619
|
+
pathLength: drawIn ? 1 : void 0,
|
|
620
|
+
style: { stroke: color },
|
|
621
|
+
children: drawIn && /* @__PURE__ */ jsx5("animate", { attributeName: "stroke-dashoffset", from: "1", to: "0", dur: "0.8s", fill: "freeze", calcMode: "spline", keySplines: "0.3 0 0.2 1", keyTimes: "0;1" })
|
|
622
|
+
},
|
|
623
|
+
k
|
|
624
|
+
)) });
|
|
625
|
+
const dots = [];
|
|
626
|
+
if (points) {
|
|
627
|
+
const st = ctx.stacked?.get(s.id);
|
|
628
|
+
s.data.forEach((v, i) => {
|
|
629
|
+
if (v === null || v === void 0 || !Number.isFinite(v)) return;
|
|
630
|
+
const spec = points === true ? true : points(i, v);
|
|
631
|
+
if (!spec) return;
|
|
632
|
+
const p = spec === true ? {} : spec;
|
|
633
|
+
const x = ctx.x(i);
|
|
634
|
+
const y = s.scale(st ? st.y1[i] : v);
|
|
635
|
+
const color = p.color ?? (split ? v >= baseline ? splitColors.above : splitColors.below : s.color);
|
|
636
|
+
const r = p.radius ?? Math.max(3, strokeWidth + 1);
|
|
637
|
+
dots.push(
|
|
638
|
+
/* @__PURE__ */ jsxs4("g", { opacity: s.dimmed ? DIM_OPACITY : 1, children: [
|
|
639
|
+
/* @__PURE__ */ jsx5("circle", { cx: x, cy: y, r, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } }),
|
|
640
|
+
p.label && /* @__PURE__ */ jsx5(
|
|
641
|
+
"text",
|
|
642
|
+
{
|
|
643
|
+
x,
|
|
644
|
+
y: p.labelPosition === "bottom" ? y + r + 13 : y - r - 6,
|
|
645
|
+
textAnchor: "middle",
|
|
646
|
+
fontSize: 11,
|
|
647
|
+
fontWeight: 600,
|
|
648
|
+
strokeWidth: 3,
|
|
649
|
+
strokeLinejoin: "round",
|
|
650
|
+
paintOrder: "stroke",
|
|
651
|
+
style: { fill: color, stroke: VAR.bg },
|
|
652
|
+
children: p.label
|
|
653
|
+
}
|
|
654
|
+
)
|
|
655
|
+
] }, i)
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
457
659
|
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
458
|
-
area !== "none" && /* @__PURE__ */ jsx5(
|
|
459
|
-
|
|
460
|
-
"path",
|
|
660
|
+
area !== "none" && /* @__PURE__ */ jsx5(
|
|
661
|
+
Area,
|
|
461
662
|
{
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
},
|
|
473
|
-
|
|
474
|
-
|
|
663
|
+
seriesId,
|
|
664
|
+
curve,
|
|
665
|
+
fill: area,
|
|
666
|
+
connectNulls: connectNulls ?? false,
|
|
667
|
+
...areaOpacity === void 0 ? {} : { opacity: areaOpacity },
|
|
668
|
+
...baseline === void 0 ? {} : { baseline },
|
|
669
|
+
...splitColors ? { splitColors } : {}
|
|
670
|
+
}
|
|
671
|
+
),
|
|
672
|
+
split ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
673
|
+
/* @__PURE__ */ jsx5(SplitClips, { id: gid, y: baseY, ctx }),
|
|
674
|
+
strokes(splitColors.above, `${gid}-above`),
|
|
675
|
+
strokes(splitColors.below, `${gid}-below`)
|
|
676
|
+
] }) : strokes(s.color),
|
|
677
|
+
dots
|
|
475
678
|
] });
|
|
476
679
|
}
|
|
477
680
|
|
|
478
681
|
// src/viz/bar.tsx
|
|
479
682
|
import { barPath } from "@funnest/charts-viz";
|
|
480
683
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
481
|
-
function BarPlot({
|
|
684
|
+
function BarPlot({
|
|
685
|
+
seriesIds,
|
|
686
|
+
radius = 3,
|
|
687
|
+
roundBaseline = false,
|
|
688
|
+
groupGap = 2,
|
|
689
|
+
stackGap = 0,
|
|
690
|
+
minSize = 0,
|
|
691
|
+
getBarColor,
|
|
692
|
+
highlightScrubbed = true
|
|
693
|
+
}) {
|
|
482
694
|
const ctx = useCartesianChart();
|
|
483
695
|
const band = ctx.band;
|
|
484
696
|
if (!band) return null;
|
|
485
697
|
const list = seriesIds ? ctx.series.filter((s) => seriesIds.includes(s.id)) : ctx.series;
|
|
486
698
|
const groups = ctx.stacked ? 1 : Math.max(1, list.length);
|
|
487
699
|
const barW = Math.max(1, (band.bandwidth - groupGap * (groups - 1)) / groups);
|
|
488
|
-
const
|
|
700
|
+
const zeroOf = (scale) => {
|
|
701
|
+
const [d0, d1] = scale.domain;
|
|
702
|
+
return scale(Math.min(Math.max(0, Math.min(d0, d1)), Math.max(d0, d1)));
|
|
703
|
+
};
|
|
489
704
|
const dim = highlightScrubbed && ctx.scrubIndex !== null;
|
|
490
705
|
return /* @__PURE__ */ jsx6("g", { children: list.map((s, k) => {
|
|
491
706
|
const st = ctx.stacked?.get(s.id);
|
|
707
|
+
const y = s.scale;
|
|
708
|
+
const zero = zeroOf(y);
|
|
492
709
|
const offset = ctx.stacked ? 0 : k * (barW + groupGap);
|
|
493
710
|
const isTopOfStack = (i, v) => !ctx.stacked || !list.slice(k + 1).some((o) => {
|
|
494
711
|
const ov = o.data[i];
|
|
495
712
|
return ov !== null && ov !== void 0 && ov !== 0 && Math.sign(ov) === Math.sign(v);
|
|
496
713
|
});
|
|
497
|
-
return /* @__PURE__ */ jsx6("g", { children: s.data.map((v, i) => {
|
|
498
|
-
if (v === null || v === void 0 || !Number.isFinite(v)) return null;
|
|
714
|
+
return /* @__PURE__ */ jsx6("g", { opacity: s.dimmed ? DIM_OPACITY : 1, children: s.data.map((v, i) => {
|
|
715
|
+
if (v === null || v === void 0 || !Number.isFinite(v) || v === 0) return null;
|
|
499
716
|
const x = band(i) + offset;
|
|
500
|
-
const
|
|
501
|
-
|
|
717
|
+
const low = s.low?.[i];
|
|
718
|
+
let y0 = st ? y(st.y0[i]) : low !== null && low !== void 0 && s.low ? y(low) : zero;
|
|
719
|
+
let y1 = st ? y(st.y1[i]) : y(v);
|
|
720
|
+
const dir = Math.sign(y1 - y0) || -1;
|
|
721
|
+
const top = isTopOfStack(i, v);
|
|
722
|
+
const atBase = Math.abs(y0 - zero) <= 0.5 || !!s.low;
|
|
723
|
+
if (st && stackGap > 0) {
|
|
724
|
+
if (!atBase) y0 += dir * stackGap / 2;
|
|
725
|
+
if (!top) y1 -= dir * stackGap / 2;
|
|
726
|
+
}
|
|
727
|
+
if (minSize > 0 && Math.abs(y1 - y0) < minSize) y1 = y0 + dir * minSize;
|
|
502
728
|
const color = getBarColor?.(v, i, s.id) ?? s.color;
|
|
729
|
+
const r = top ? radius : st && stackGap > 0 ? Math.min(radius, 2) : 0;
|
|
503
730
|
return /* @__PURE__ */ jsx6(
|
|
504
731
|
"path",
|
|
505
732
|
{
|
|
506
|
-
d: barPath(x, barW, y0, y1,
|
|
733
|
+
d: barPath(x, barW, y0, y1, s.low ? radius : r, roundBaseline && atBase || !!s.low),
|
|
507
734
|
fillOpacity: dim && ctx.scrubIndex !== i ? 0.45 : 1,
|
|
508
735
|
style: { fill: color },
|
|
509
736
|
children: ctx.animate && /* @__PURE__ */ jsx6("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.5s", fill: "freeze" })
|
|
@@ -514,116 +741,354 @@ function BarPlot({ seriesIds, radius = 3, groupGap = 2, getBarColor, highlightSc
|
|
|
514
741
|
}) });
|
|
515
742
|
}
|
|
516
743
|
|
|
744
|
+
// src/viz/horizontal-bar.tsx
|
|
745
|
+
import { bandScale as bandScale2, barPath as barPath2, extent as extent2, linearScale as linearScale2, seriesColor as seriesColor2, stack as stack2, stackedExtent as stackedExtent2, ticks as niceTicks2 } from "@funnest/charts-viz";
|
|
746
|
+
import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
|
|
747
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
748
|
+
var textWidth = (t, size = 12) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? size : size * 0.6), 0);
|
|
749
|
+
function HorizontalBarChart({
|
|
750
|
+
series,
|
|
751
|
+
categories,
|
|
752
|
+
width: fixedWidth,
|
|
753
|
+
height,
|
|
754
|
+
stacked = false,
|
|
755
|
+
radius = 3,
|
|
756
|
+
groupGap = 2,
|
|
757
|
+
stackGap = 0,
|
|
758
|
+
bandPadding = 0.3,
|
|
759
|
+
getBarColor,
|
|
760
|
+
showValueAxis = true,
|
|
761
|
+
showCategoryAxis = true,
|
|
762
|
+
valueFormatter = formatNumber,
|
|
763
|
+
enableScrubbing = true,
|
|
764
|
+
legend = false,
|
|
765
|
+
palette,
|
|
766
|
+
className,
|
|
767
|
+
style,
|
|
768
|
+
"aria-label": ariaLabel
|
|
769
|
+
}) {
|
|
770
|
+
const wrapRef = useRef3(null);
|
|
771
|
+
const [measured, setMeasured] = useState3(fixedWidth ?? 0);
|
|
772
|
+
const [hover, setHover] = useState3(null);
|
|
773
|
+
useEffect3(() => {
|
|
774
|
+
if (fixedWidth !== void 0) return setMeasured(fixedWidth);
|
|
775
|
+
const el = wrapRef.current;
|
|
776
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
777
|
+
const ro = new ResizeObserver(([e]) => e && setMeasured(Math.round(e.contentRect.width)));
|
|
778
|
+
ro.observe(el);
|
|
779
|
+
setMeasured(Math.round(el.getBoundingClientRect().width));
|
|
780
|
+
return () => ro.disconnect();
|
|
781
|
+
}, [fixedWidth]);
|
|
782
|
+
const resolved = useMemo2(
|
|
783
|
+
() => series.map((s, index) => ({
|
|
784
|
+
...s,
|
|
785
|
+
color: s.color ?? seriesColor2(index, palette),
|
|
786
|
+
high: s.data.map((v) => Array.isArray(v) ? Math.max(v[0], v[1]) : v),
|
|
787
|
+
low: s.data.some((v) => Array.isArray(v)) ? s.data.map((v) => Array.isArray(v) ? Math.min(v[0], v[1]) : v) : null
|
|
788
|
+
})),
|
|
789
|
+
[series, palette]
|
|
790
|
+
);
|
|
791
|
+
const rows = Math.max(categories?.length ?? 0, ...resolved.map((s) => s.high.length));
|
|
792
|
+
const h = height ?? rows * 34 + (showValueAxis ? 26 : 8);
|
|
793
|
+
const w = measured;
|
|
794
|
+
const labelW = showCategoryAxis ? Math.min(140, Math.max(0, ...(categories ?? []).map((c) => textWidth(c))) + 12) : 0;
|
|
795
|
+
const left = labelW + 4;
|
|
796
|
+
const right = Math.max(left + 10, w - 12);
|
|
797
|
+
const top = 4;
|
|
798
|
+
const bottom = h - (showValueAxis ? 24 : 4);
|
|
799
|
+
const stackedMap = stacked ? new Map(stack2(resolved.map((s) => ({ id: s.id, data: s.high }))).map((st) => [st.id, st])) : null;
|
|
800
|
+
let [min, max] = stacked ? stackedExtent2([...stackedMap.values()]) : extent2([...resolved.map((s) => ({ id: s.id, data: s.high })), ...resolved.flatMap((s) => s.low ? [{ id: `${s.id}~`, data: s.low }] : [])]) ?? [0, 1];
|
|
801
|
+
min = Math.min(min, 0);
|
|
802
|
+
max = Math.max(max, 0);
|
|
803
|
+
const xs = linearScale2([min, max], [left, right]);
|
|
804
|
+
const band = bandScale2(rows, [top, bottom], bandPadding);
|
|
805
|
+
const zero = xs(0);
|
|
806
|
+
const groups = stacked ? 1 : Math.max(1, resolved.length);
|
|
807
|
+
const barH = Math.max(1, (band.bandwidth - groupGap * (groups - 1)) / groups);
|
|
808
|
+
const tickValues = niceTicks2(min, max, Math.max(2, Math.floor((right - left) / 80)));
|
|
809
|
+
const onMove = (e) => {
|
|
810
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
811
|
+
setHover(band.invert(e.clientY - r.top));
|
|
812
|
+
};
|
|
813
|
+
const tipRows = hover === null ? [] : resolved.flatMap((s) => {
|
|
814
|
+
const v = s.high[hover];
|
|
815
|
+
if (v === null || v === void 0) return [];
|
|
816
|
+
const lo = s.low?.[hover];
|
|
817
|
+
return [{ s, color: getBarColor?.(v, hover, s.id) ?? s.color, text: lo !== null && lo !== void 0 && lo !== v ? `${valueFormatter(lo)} \u2013 ${valueFormatter(v)}` : valueFormatter(v) }];
|
|
818
|
+
});
|
|
819
|
+
const tipW = Math.max(0, ...tipRows.map((r) => textWidth(r.text) + textWidth(r.s.label ?? r.s.id) + 30)) + 8;
|
|
820
|
+
const tipH = tipRows.length * 18 + 14;
|
|
821
|
+
const rowY = hover === null ? 0 : band.center(hover);
|
|
822
|
+
const tipY = Math.min(Math.max(top, rowY - tipH / 2), bottom - tipH);
|
|
823
|
+
return /* @__PURE__ */ jsxs5("div", { ref: wrapRef, className, style: { position: "relative", width: fixedWidth ?? "100%", fontFamily: VAR.font, ...style }, children: [
|
|
824
|
+
legend && /* @__PURE__ */ jsx7(
|
|
825
|
+
Legend,
|
|
826
|
+
{
|
|
827
|
+
...typeof legend === "object" && legend.shape ? { shape: legend.shape } : {},
|
|
828
|
+
items: resolved.map((s) => ({ id: s.id, label: s.label ?? s.id, color: s.color }))
|
|
829
|
+
}
|
|
830
|
+
),
|
|
831
|
+
/* @__PURE__ */ jsx7(
|
|
832
|
+
"svg",
|
|
833
|
+
{
|
|
834
|
+
width: w || "100%",
|
|
835
|
+
height: h,
|
|
836
|
+
role: "img",
|
|
837
|
+
"aria-label": ariaLabel,
|
|
838
|
+
style: { display: "block", overflow: "visible", userSelect: "none" },
|
|
839
|
+
onPointerMove: enableScrubbing ? onMove : void 0,
|
|
840
|
+
onPointerLeave: enableScrubbing ? () => setHover(null) : void 0,
|
|
841
|
+
children: w > 0 && /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
842
|
+
tickValues.map((t) => /* @__PURE__ */ jsx7("line", { x1: xs(t), x2: xs(t), y1: top, y2: bottom, shapeRendering: "crispEdges", style: { stroke: VAR.grid } }, `g${t}`)),
|
|
843
|
+
min < 0 && /* @__PURE__ */ jsx7("line", { x1: zero, x2: zero, y1: top, y2: bottom, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
|
|
844
|
+
hover !== null && /* @__PURE__ */ jsx7("rect", { x: left - 4, y: band.center(hover) - band.step / 2, width: right - left + 8, height: band.step, rx: 6, style: { fill: VAR.grid } }),
|
|
845
|
+
resolved.map((s, k) => {
|
|
846
|
+
const st = stackedMap?.get(s.id);
|
|
847
|
+
return /* @__PURE__ */ jsx7("g", { children: s.high.map((v, i) => {
|
|
848
|
+
if (v === null || v === void 0 || !Number.isFinite(v) || v === 0) return null;
|
|
849
|
+
const yTop = band(i) + (stacked ? 0 : k * (barH + groupGap));
|
|
850
|
+
const lo = s.low?.[i];
|
|
851
|
+
let x0 = st ? xs(st.y0[i]) : lo !== null && lo !== void 0 && s.low ? xs(lo) : zero;
|
|
852
|
+
let x1 = st ? xs(st.y1[i]) : xs(v);
|
|
853
|
+
const isTop = !stacked || !resolved.slice(k + 1).some((o) => (o.high[i] ?? 0) !== 0 && Math.sign(o.high[i]) === Math.sign(v));
|
|
854
|
+
const dir = Math.sign(x1 - x0) || 1;
|
|
855
|
+
if (st && stackGap > 0) {
|
|
856
|
+
if (Math.abs(x0 - zero) > 0.5) x0 += dir * stackGap / 2;
|
|
857
|
+
if (!isTop) x1 -= dir * stackGap / 2;
|
|
858
|
+
}
|
|
859
|
+
const color = getBarColor?.(v, i, s.id) ?? s.color;
|
|
860
|
+
const d = barPath2(yTop, barH, x0, x1, isTop || s.low ? radius : 0, !!s.low);
|
|
861
|
+
return /* @__PURE__ */ jsx7(
|
|
862
|
+
"path",
|
|
863
|
+
{
|
|
864
|
+
d,
|
|
865
|
+
transform: `matrix(0 1 1 0 0 0)`,
|
|
866
|
+
opacity: hover !== null && hover !== i ? DIM_OPACITY * 2 : 1,
|
|
867
|
+
style: { fill: color }
|
|
868
|
+
},
|
|
869
|
+
i
|
|
870
|
+
);
|
|
871
|
+
}) }, s.id);
|
|
872
|
+
}),
|
|
873
|
+
showCategoryAxis && (categories ?? []).map((c, i) => /* @__PURE__ */ jsx7("text", { x: labelW, y: band.center(i), textAnchor: "end", dominantBaseline: "central", fontSize: 12, style: { fill: hover === i ? VAR.textStrong : VAR.text, fontWeight: hover === i ? 700 : 500 }, children: c }, c + i)),
|
|
874
|
+
showValueAxis && tickValues.map((t) => /* @__PURE__ */ jsx7("text", { x: xs(t), y: h - 6, textAnchor: "middle", fontSize: 11, style: { fill: VAR.text }, children: valueFormatter(t) }, t)),
|
|
875
|
+
tipRows.length > 0 && /* @__PURE__ */ jsxs5("g", { transform: `translate(${Math.min(right - tipW, Math.max(left, xs(max) - tipW))},${tipY})`, pointerEvents: "none", children: [
|
|
876
|
+
/* @__PURE__ */ jsx7("rect", { width: tipW, height: tipH, rx: 8, strokeWidth: 1, style: { fill: VAR.bg, stroke: VAR.grid } }),
|
|
877
|
+
tipRows.map((r, k) => /* @__PURE__ */ jsxs5("g", { transform: `translate(10,${7 + 9 + k * 18})`, children: [
|
|
878
|
+
/* @__PURE__ */ jsx7("circle", { cx: 3, cy: 0, r: 3, style: { fill: r.color } }),
|
|
879
|
+
/* @__PURE__ */ jsx7("text", { x: 12, dominantBaseline: "central", fontSize: 12, style: { fill: VAR.text }, children: r.s.label ?? r.s.id }),
|
|
880
|
+
/* @__PURE__ */ jsx7("text", { x: tipW - 20, textAnchor: "end", dominantBaseline: "central", fontSize: 12, fontWeight: 700, style: { fill: VAR.textStrong, fontVariantNumeric: "tabular-nums" }, children: r.text })
|
|
881
|
+
] }, r.s.id))
|
|
882
|
+
] })
|
|
883
|
+
] })
|
|
884
|
+
}
|
|
885
|
+
)
|
|
886
|
+
] });
|
|
887
|
+
}
|
|
888
|
+
|
|
517
889
|
// src/viz/scrubber.tsx
|
|
518
|
-
import { jsx as
|
|
890
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
519
891
|
var TIP_FONT = 12;
|
|
520
892
|
var ROW = 18;
|
|
521
893
|
var TIP_PAD = 7;
|
|
522
|
-
var
|
|
523
|
-
function Scrubber({
|
|
894
|
+
var textWidth2 = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? TIP_FONT : TIP_FONT * 0.6), 0);
|
|
895
|
+
function Scrubber({
|
|
896
|
+
label,
|
|
897
|
+
seriesIds,
|
|
898
|
+
dimFuture = true,
|
|
899
|
+
hideOverlay = false,
|
|
900
|
+
idlePulse = false,
|
|
901
|
+
beaconLabels = false,
|
|
902
|
+
beaconColor,
|
|
903
|
+
showValues = false,
|
|
904
|
+
valueFormatter = formatNumber
|
|
905
|
+
}) {
|
|
524
906
|
const ctx = useCartesianChart();
|
|
525
907
|
const i = ctx.scrubIndex;
|
|
526
|
-
|
|
908
|
+
const pick = seriesIds ? ctx.series.filter((s) => seriesIds.includes(s.id)) : ctx.series;
|
|
909
|
+
const colorOf = (s, v) => beaconColor?.(v, s.id) ?? s.color;
|
|
910
|
+
if (i === null || i >= ctx.length) {
|
|
911
|
+
if (!idlePulse || ctx.band) return null;
|
|
912
|
+
return /* @__PURE__ */ jsx8("g", { pointerEvents: "none", children: pick.map((s) => {
|
|
913
|
+
let last = s.data.length - 1;
|
|
914
|
+
while (last >= 0 && (s.data[last] === null || s.data[last] === void 0)) last--;
|
|
915
|
+
if (last < 0) return null;
|
|
916
|
+
const y = seriesY(ctx, s, last);
|
|
917
|
+
if (y === null) return null;
|
|
918
|
+
const x2 = ctx.x(last);
|
|
919
|
+
const color = colorOf(s, s.data[last]);
|
|
920
|
+
return /* @__PURE__ */ jsxs6("g", { children: [
|
|
921
|
+
/* @__PURE__ */ jsxs6("circle", { cx: x2, cy: y, r: 4, fillOpacity: 0.35, style: { fill: color }, children: [
|
|
922
|
+
/* @__PURE__ */ jsx8("animate", { attributeName: "r", values: "4;13", dur: "1.6s", repeatCount: "indefinite" }),
|
|
923
|
+
/* @__PURE__ */ jsx8("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
|
|
924
|
+
] }),
|
|
925
|
+
/* @__PURE__ */ jsx8("circle", { cx: x2, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } })
|
|
926
|
+
] }, s.id);
|
|
927
|
+
}) });
|
|
928
|
+
}
|
|
527
929
|
const { plot } = ctx;
|
|
528
930
|
const x = ctx.x(i);
|
|
529
931
|
const text = label ? label(i) : ctx.xLabel(i);
|
|
530
|
-
const list =
|
|
932
|
+
const list = pick;
|
|
531
933
|
const textX = Math.min(Math.max(x, plot.left + 30), plot.right - 30);
|
|
532
934
|
const rows = showValues ? list.flatMap((s) => {
|
|
533
935
|
const v = s.data[i];
|
|
534
|
-
|
|
936
|
+
if (v === null || v === void 0 || !Number.isFinite(v)) return [];
|
|
937
|
+
const lo = s.low?.[i];
|
|
938
|
+
const text2 = lo !== null && lo !== void 0 && lo !== v ? `${valueFormatter(lo, s.id)} \u2013 ${valueFormatter(v, s.id)}` : valueFormatter(v, s.id);
|
|
939
|
+
return [{ s, text: text2 }];
|
|
535
940
|
}) : [];
|
|
536
|
-
const tipW = Math.max(0, ...rows.map((r) =>
|
|
941
|
+
const tipW = Math.max(0, ...rows.map((r) => textWidth2(r.text) + (rows.length > 1 || r.s.label ? textWidth2(r.s.label ?? "") + 26 : 18))) + 16;
|
|
537
942
|
const tipH = rows.length * ROW + TIP_PAD * 2;
|
|
538
943
|
const tipRight = x + 12 + tipW <= plot.right;
|
|
539
944
|
const tipX = tipRight ? x + 12 : x - 12 - tipW;
|
|
540
945
|
const tipY = plot.top + 4;
|
|
541
|
-
return /* @__PURE__ */
|
|
542
|
-
dimFuture && !ctx.band && /* @__PURE__ */
|
|
543
|
-
/* @__PURE__ */
|
|
544
|
-
text && /* @__PURE__ */
|
|
946
|
+
return /* @__PURE__ */ jsxs6("g", { pointerEvents: "none", children: [
|
|
947
|
+
dimFuture && !hideOverlay && !ctx.band && /* @__PURE__ */ jsx8("rect", { x, y: plot.top, width: Math.max(0, plot.right - x + 2), height: plot.height, fillOpacity: 0.55, style: { fill: VAR.bg } }),
|
|
948
|
+
/* @__PURE__ */ jsx8("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth: 1, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
|
|
949
|
+
text && /* @__PURE__ */ jsx8("text", { x: textX, y: Math.max(11, plot.top - 2), textAnchor: "middle", fontSize: 11, fontWeight: 600, strokeWidth: 3, strokeLinejoin: "round", paintOrder: "stroke", style: { stroke: VAR.bg, fill: VAR.textStrong }, children: text }),
|
|
545
950
|
!ctx.band && list.map((s) => {
|
|
546
951
|
const y = seriesY(ctx, s, i);
|
|
547
952
|
if (y === null) return null;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
953
|
+
const v = s.data[i];
|
|
954
|
+
const color = colorOf(s, v);
|
|
955
|
+
const text2 = beaconLabels ? typeof beaconLabels === "function" ? beaconLabels(v, s.id) : valueFormatter(v, s.id) : "";
|
|
956
|
+
const right = x + 12 + textWidth2(text2) <= plot.right;
|
|
957
|
+
return /* @__PURE__ */ jsxs6("g", { children: [
|
|
958
|
+
/* @__PURE__ */ jsx8("circle", { cx: x, cy: y, r: 7, fillOpacity: 0.18, style: { fill: color } }),
|
|
959
|
+
/* @__PURE__ */ jsx8("circle", { cx: x, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } }),
|
|
960
|
+
text2 && /* @__PURE__ */ jsx8(
|
|
961
|
+
"text",
|
|
962
|
+
{
|
|
963
|
+
x: right ? x + 10 : x - 10,
|
|
964
|
+
y,
|
|
965
|
+
textAnchor: right ? "start" : "end",
|
|
966
|
+
dominantBaseline: "central",
|
|
967
|
+
fontSize: TIP_FONT,
|
|
968
|
+
fontWeight: 700,
|
|
969
|
+
strokeWidth: 3,
|
|
970
|
+
strokeLinejoin: "round",
|
|
971
|
+
paintOrder: "stroke",
|
|
972
|
+
style: { fill: color, stroke: VAR.bg, fontVariantNumeric: "tabular-nums" },
|
|
973
|
+
children: text2
|
|
974
|
+
}
|
|
975
|
+
)
|
|
551
976
|
] }, s.id);
|
|
552
977
|
}),
|
|
553
|
-
rows.length > 0 && /* @__PURE__ */
|
|
554
|
-
/* @__PURE__ */
|
|
555
|
-
rows.map((r, k) => /* @__PURE__ */
|
|
556
|
-
/* @__PURE__ */
|
|
557
|
-
(rows.length > 1 || r.s.label) && /* @__PURE__ */
|
|
558
|
-
/* @__PURE__ */
|
|
978
|
+
rows.length > 0 && /* @__PURE__ */ jsxs6("g", { transform: `translate(${tipX},${tipY})`, children: [
|
|
979
|
+
/* @__PURE__ */ jsx8("rect", { width: tipW, height: tipH, rx: 8, style: { fill: VAR.bg, stroke: VAR.grid }, strokeWidth: 1 }),
|
|
980
|
+
rows.map((r, k) => /* @__PURE__ */ jsxs6("g", { transform: `translate(10,${TIP_PAD + ROW / 2 + k * ROW})`, children: [
|
|
981
|
+
/* @__PURE__ */ jsx8("circle", { cx: 3, cy: 0, r: 3, style: { fill: r.s.color } }),
|
|
982
|
+
(rows.length > 1 || r.s.label) && /* @__PURE__ */ jsx8("text", { x: 12, dominantBaseline: "central", fontSize: TIP_FONT, style: { fill: VAR.text }, children: r.s.label ?? r.s.id }),
|
|
983
|
+
/* @__PURE__ */ jsx8("text", { x: tipW - 20, textAnchor: "end", dominantBaseline: "central", fontSize: TIP_FONT, fontWeight: 700, style: { fill: VAR.textStrong, fontVariantNumeric: "tabular-nums" }, children: r.text })
|
|
559
984
|
] }, r.s.id))
|
|
560
985
|
] })
|
|
561
986
|
] });
|
|
562
987
|
}
|
|
563
988
|
|
|
564
989
|
// src/viz/annotations.tsx
|
|
565
|
-
import { jsx as
|
|
566
|
-
|
|
990
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
991
|
+
var LABEL_FONT = 11;
|
|
992
|
+
var labelWidth = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? LABEL_FONT : LABEL_FONT * 0.62), 0);
|
|
993
|
+
function LineLabel(props) {
|
|
994
|
+
const { x, y, text, anchor, color, pill } = props;
|
|
995
|
+
const textColor = color === VAR.line ? VAR.text : color;
|
|
996
|
+
if (!pill) {
|
|
997
|
+
return /* @__PURE__ */ jsx9("text", { x, y, textAnchor: anchor, dominantBaseline: "central", fontSize: LABEL_FONT, fontWeight: 600, strokeWidth: 3, strokeLinejoin: "round", paintOrder: "stroke", style: { stroke: VAR.bg, fill: textColor }, children: text });
|
|
998
|
+
}
|
|
999
|
+
const w = labelWidth(text) + 14;
|
|
1000
|
+
const left = anchor === "start" ? x : anchor === "end" ? x - w : x - w / 2;
|
|
1001
|
+
return /* @__PURE__ */ jsxs7("g", { children: [
|
|
1002
|
+
/* @__PURE__ */ jsx9("rect", { x: left, y: y - 10, width: w, height: 20, rx: 10, style: { fill: color === VAR.line ? VAR.textStrong : color } }),
|
|
1003
|
+
/* @__PURE__ */ jsx9("text", { x: left + w / 2, y, textAnchor: "middle", dominantBaseline: "central", fontSize: LABEL_FONT, fontWeight: 700, style: { fill: VAR.bg }, children: text })
|
|
1004
|
+
] });
|
|
1005
|
+
}
|
|
1006
|
+
function ReferenceLine({
|
|
1007
|
+
dataY,
|
|
1008
|
+
dataX,
|
|
1009
|
+
yAxisId,
|
|
1010
|
+
label,
|
|
1011
|
+
labelPosition,
|
|
1012
|
+
labelAlign = "above",
|
|
1013
|
+
labelStyle = "text",
|
|
1014
|
+
labelDx = 0,
|
|
1015
|
+
labelDy = 0,
|
|
1016
|
+
color = VAR.line,
|
|
1017
|
+
dash = "4 4",
|
|
1018
|
+
strokeWidth = 1
|
|
1019
|
+
}) {
|
|
567
1020
|
const ctx = useCartesianChart();
|
|
568
1021
|
const { plot } = ctx;
|
|
1022
|
+
const pill = labelStyle === "pill";
|
|
569
1023
|
if (dataY !== void 0) {
|
|
570
|
-
const y = ctx
|
|
1024
|
+
const y = scaleFor(ctx, yAxisId)(dataY);
|
|
571
1025
|
if (y < plot.top - 0.5 || y > plot.bottom + 0.5) return null;
|
|
572
1026
|
const pos = labelPosition ?? "right";
|
|
573
1027
|
const tx = pos === "left" ? plot.left + 4 : pos === "center" ? (plot.left + plot.right) / 2 : plot.right - 4;
|
|
574
1028
|
const anchor = pos === "left" ? "start" : pos === "center" ? "middle" : "end";
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1029
|
+
const off = pill ? 13 : 9;
|
|
1030
|
+
const ly = labelAlign === "center" ? y : labelAlign === "below" ? y + off : y - off;
|
|
1031
|
+
return /* @__PURE__ */ jsxs7("g", { pointerEvents: "none", children: [
|
|
1032
|
+
/* @__PURE__ */ jsx9("line", { x1: plot.left, x2: plot.right, y1: y, y2: y, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
|
|
1033
|
+
label !== void 0 && /* @__PURE__ */ jsx9(LineLabel, { x: tx + labelDx, y: ly + labelDy, text: label, anchor, color, pill })
|
|
578
1034
|
] });
|
|
579
1035
|
}
|
|
580
1036
|
if (dataX !== void 0) {
|
|
581
1037
|
const x = ctx.x(dataX);
|
|
582
1038
|
const pos = labelPosition ?? "top";
|
|
583
|
-
return /* @__PURE__ */
|
|
584
|
-
/* @__PURE__ */
|
|
585
|
-
label !== void 0 && /* @__PURE__ */
|
|
586
|
-
|
|
1039
|
+
return /* @__PURE__ */ jsxs7("g", { pointerEvents: "none", children: [
|
|
1040
|
+
/* @__PURE__ */ jsx9("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
|
|
1041
|
+
label !== void 0 && /* @__PURE__ */ jsx9(
|
|
1042
|
+
LineLabel,
|
|
587
1043
|
{
|
|
588
|
-
x: x + 4,
|
|
589
|
-
y: pos === "bottom" ? plot.bottom -
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
paintOrder: "stroke",
|
|
595
|
-
style: { stroke: VAR.bg, fill: color === VAR.line ? VAR.text : color },
|
|
596
|
-
children: label
|
|
1044
|
+
x: x + (pill ? 0 : 4) + labelDx,
|
|
1045
|
+
y: (pos === "bottom" ? plot.bottom - 10 : plot.top + 10) + labelDy,
|
|
1046
|
+
text: label,
|
|
1047
|
+
anchor: pill ? "middle" : "start",
|
|
1048
|
+
color,
|
|
1049
|
+
pill
|
|
597
1050
|
}
|
|
598
1051
|
)
|
|
599
1052
|
] });
|
|
600
1053
|
}
|
|
601
1054
|
return null;
|
|
602
1055
|
}
|
|
603
|
-
function Point({ dataX, dataY, seriesId, color, radius = 4, label, labelPosition = "top", pulse = false, onClick }) {
|
|
1056
|
+
function Point({ dataX, dataY, seriesId, yAxisId, color, radius = 4, label, labelPosition = "top", pulse = false, onClick }) {
|
|
604
1057
|
const ctx = useCartesianChart();
|
|
605
1058
|
const s = seriesId ? ctx.getSeries(seriesId) : void 0;
|
|
606
1059
|
const value = dataY ?? (s ? s.data[dataX] : void 0);
|
|
607
1060
|
if (value === null || value === void 0 || !Number.isFinite(value)) return null;
|
|
608
1061
|
const st = s ? ctx.stacked?.get(s.id) : void 0;
|
|
609
1062
|
const x = ctx.x(dataX);
|
|
610
|
-
const y = ctx
|
|
1063
|
+
const y = (s?.scale ?? scaleFor(ctx, yAxisId))(st && dataY === void 0 ? st.y1[dataX] : value);
|
|
611
1064
|
const fill = color ?? s?.color ?? ctx.series[0]?.color ?? "currentColor";
|
|
612
1065
|
const gap = radius + 6;
|
|
613
1066
|
const [lx, ly, anchor] = labelPosition === "bottom" ? [x, y + gap + 8, "middle"] : labelPosition === "left" ? [x - gap, y + 4, "end"] : labelPosition === "right" ? [x + gap, y + 4, "start"] : [x, y - gap, "middle"];
|
|
614
|
-
return /* @__PURE__ */
|
|
615
|
-
pulse && /* @__PURE__ */
|
|
616
|
-
/* @__PURE__ */
|
|
617
|
-
/* @__PURE__ */
|
|
1067
|
+
return /* @__PURE__ */ jsxs7("g", { style: onClick ? { cursor: "pointer" } : void 0, onClick, pointerEvents: onClick ? "auto" : "none", children: [
|
|
1068
|
+
pulse && /* @__PURE__ */ jsxs7("circle", { cx: x, cy: y, r: radius, fillOpacity: 0.35, style: { fill }, children: [
|
|
1069
|
+
/* @__PURE__ */ jsx9("animate", { attributeName: "r", values: `${radius};${radius * 3.2}`, dur: "1.6s", repeatCount: "indefinite" }),
|
|
1070
|
+
/* @__PURE__ */ jsx9("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
|
|
618
1071
|
] }),
|
|
619
|
-
/* @__PURE__ */
|
|
620
|
-
label !== void 0 && /* @__PURE__ */
|
|
1072
|
+
/* @__PURE__ */ jsx9("circle", { cx: x, cy: y, r: radius, strokeWidth: 2, style: { fill, stroke: VAR.bg } }),
|
|
1073
|
+
label !== void 0 && /* @__PURE__ */ jsx9("text", { x: lx, y: ly, textAnchor: anchor, fontSize: 11, fontWeight: 600, strokeWidth: 3, strokeLinejoin: "round", paintOrder: "stroke", style: { stroke: VAR.bg, fill }, children: label })
|
|
621
1074
|
] });
|
|
622
1075
|
}
|
|
623
1076
|
|
|
624
1077
|
// src/viz/period-selector.tsx
|
|
625
|
-
import { useLayoutEffect, useRef as
|
|
626
|
-
import { jsx as
|
|
1078
|
+
import { useLayoutEffect, useRef as useRef4, useState as useState4 } from "react";
|
|
1079
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1080
|
+
function LiveTabLabel({ label, color = "var(--fnv-live, #F04452)" }) {
|
|
1081
|
+
return /* @__PURE__ */ jsxs8("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
|
|
1082
|
+
/* @__PURE__ */ jsxs8("span", { "aria-hidden": "true", style: { position: "relative", width: 7, height: 7 }, children: [
|
|
1083
|
+
/* @__PURE__ */ jsx10("span", { style: { position: "absolute", inset: 0, borderRadius: "50%", background: color } }),
|
|
1084
|
+
/* @__PURE__ */ jsx10("svg", { width: "7", height: "7", viewBox: "0 0 7 7", style: { position: "absolute", inset: 0, overflow: "visible" }, children: /* @__PURE__ */ jsxs8("circle", { cx: "3.5", cy: "3.5", r: "3.5", style: { fill: color }, fillOpacity: 0.45, children: [
|
|
1085
|
+
/* @__PURE__ */ jsx10("animate", { attributeName: "r", values: "3.5;8", dur: "1.6s", repeatCount: "indefinite" }),
|
|
1086
|
+
/* @__PURE__ */ jsx10("animate", { attributeName: "fill-opacity", values: "0.45;0", dur: "1.6s", repeatCount: "indefinite" })
|
|
1087
|
+
] }) })
|
|
1088
|
+
] }),
|
|
1089
|
+
label
|
|
1090
|
+
] });
|
|
1091
|
+
}
|
|
627
1092
|
var DEFAULT_PERIODS = [
|
|
628
1093
|
{ id: "1D", label: "1\uC77C" },
|
|
629
1094
|
{ id: "1W", label: "1\uC8FC" },
|
|
@@ -637,13 +1102,16 @@ function PeriodSelector({
|
|
|
637
1102
|
value,
|
|
638
1103
|
onChange,
|
|
639
1104
|
activeColor = "var(--fnv-accent, #6C4DFF)",
|
|
1105
|
+
liveColor = "var(--fnv-live, #F04452)",
|
|
640
1106
|
fullWidth = false,
|
|
641
1107
|
className,
|
|
642
1108
|
style,
|
|
643
1109
|
"aria-label": ariaLabel = "Period"
|
|
644
1110
|
}) {
|
|
645
|
-
const
|
|
646
|
-
const
|
|
1111
|
+
const activeIsLive = periods.find((p) => p.id === value)?.live ?? false;
|
|
1112
|
+
const pillColor = activeIsLive ? liveColor : activeColor;
|
|
1113
|
+
const listRef = useRef4(null);
|
|
1114
|
+
const [pill, setPill] = useState4(null);
|
|
647
1115
|
useLayoutEffect(() => {
|
|
648
1116
|
const list = listRef.current;
|
|
649
1117
|
const active = list?.querySelector('[aria-selected="true"]');
|
|
@@ -665,7 +1133,7 @@ function PeriodSelector({
|
|
|
665
1133
|
onChange(p.id);
|
|
666
1134
|
listRef.current?.querySelector(`[data-period="${CSS.escape(p.id)}"]`)?.focus();
|
|
667
1135
|
};
|
|
668
|
-
return /* @__PURE__ */
|
|
1136
|
+
return /* @__PURE__ */ jsxs8(
|
|
669
1137
|
"div",
|
|
670
1138
|
{
|
|
671
1139
|
ref: listRef,
|
|
@@ -675,7 +1143,7 @@ function PeriodSelector({
|
|
|
675
1143
|
onKeyDown,
|
|
676
1144
|
style: { position: "relative", display: fullWidth ? "flex" : "inline-flex", gap: 2, fontFamily: VAR.font, ...style },
|
|
677
1145
|
children: [
|
|
678
|
-
pill && /* @__PURE__ */
|
|
1146
|
+
pill && /* @__PURE__ */ jsx10(
|
|
679
1147
|
"span",
|
|
680
1148
|
{
|
|
681
1149
|
"aria-hidden": "true",
|
|
@@ -686,7 +1154,7 @@ function PeriodSelector({
|
|
|
686
1154
|
left: pill.left,
|
|
687
1155
|
width: pill.width,
|
|
688
1156
|
borderRadius: 999,
|
|
689
|
-
background:
|
|
1157
|
+
background: pillColor,
|
|
690
1158
|
opacity: 0.12,
|
|
691
1159
|
transition: "left 0.2s ease, width 0.2s ease"
|
|
692
1160
|
}
|
|
@@ -694,7 +1162,7 @@ function PeriodSelector({
|
|
|
694
1162
|
),
|
|
695
1163
|
periods.map((p) => {
|
|
696
1164
|
const active = p.id === value;
|
|
697
|
-
return /* @__PURE__ */
|
|
1165
|
+
return /* @__PURE__ */ jsx10(
|
|
698
1166
|
"button",
|
|
699
1167
|
{
|
|
700
1168
|
type: "button",
|
|
@@ -712,13 +1180,13 @@ function PeriodSelector({
|
|
|
712
1180
|
border: 0,
|
|
713
1181
|
borderRadius: 999,
|
|
714
1182
|
background: "transparent",
|
|
715
|
-
color: active ? activeColor : VAR.text,
|
|
1183
|
+
color: active ? p.live ? liveColor : activeColor : VAR.text,
|
|
716
1184
|
font: "inherit",
|
|
717
1185
|
fontSize: 13,
|
|
718
1186
|
fontWeight: active ? 700 : 500,
|
|
719
1187
|
cursor: "pointer"
|
|
720
1188
|
},
|
|
721
|
-
children: p.label
|
|
1189
|
+
children: p.live ? /* @__PURE__ */ jsx10(LiveTabLabel, { label: p.label, color: liveColor }) : p.label
|
|
722
1190
|
},
|
|
723
1191
|
p.id
|
|
724
1192
|
);
|
|
@@ -729,103 +1197,253 @@ function PeriodSelector({
|
|
|
729
1197
|
}
|
|
730
1198
|
|
|
731
1199
|
// src/viz/charts.tsx
|
|
732
|
-
import { percentages, seriesColor as
|
|
733
|
-
import { useState as
|
|
734
|
-
import { jsx as
|
|
1200
|
+
import { percentages, seriesColor as seriesColor3 } from "@funnest/charts-viz";
|
|
1201
|
+
import { useState as useState5 } from "react";
|
|
1202
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
735
1203
|
var axisProps = (v) => v ? v === true ? {} : v : null;
|
|
736
1204
|
var axisElements = (x, y) => [
|
|
737
|
-
y && /* @__PURE__ */
|
|
738
|
-
x && /* @__PURE__ */
|
|
1205
|
+
y && /* @__PURE__ */ jsx11(YAxis, { ...y }, "y"),
|
|
1206
|
+
x && /* @__PURE__ */ jsx11(XAxis, { ...x }, "x")
|
|
739
1207
|
];
|
|
740
|
-
|
|
1208
|
+
var defined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
1209
|
+
var KR_SPLIT = { above: "#F04452", below: "#3182F6" };
|
|
1210
|
+
function LineChart({
|
|
1211
|
+
showXAxis,
|
|
1212
|
+
showYAxis,
|
|
1213
|
+
scrubber,
|
|
1214
|
+
curve,
|
|
1215
|
+
strokeWidth,
|
|
1216
|
+
lineStyle,
|
|
1217
|
+
area,
|
|
1218
|
+
connectNulls,
|
|
1219
|
+
points,
|
|
1220
|
+
baseline,
|
|
1221
|
+
splitColors,
|
|
1222
|
+
children,
|
|
1223
|
+
...chart
|
|
1224
|
+
}) {
|
|
741
1225
|
const sc = axisProps(scrubber);
|
|
742
|
-
|
|
1226
|
+
const split = baseline !== void 0 ? splitColors ?? KR_SPLIT : void 0;
|
|
1227
|
+
return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
|
|
743
1228
|
axisElements(axisProps(showXAxis), axisProps(showYAxis)),
|
|
744
|
-
chart.series.map((s) => /* @__PURE__ */
|
|
1229
|
+
chart.series.map((s) => /* @__PURE__ */ jsx11(
|
|
745
1230
|
Line,
|
|
746
1231
|
{
|
|
747
1232
|
seriesId: s.id,
|
|
748
|
-
...curve
|
|
749
|
-
...
|
|
750
|
-
...area ? { area } : {},
|
|
751
|
-
...connectNulls ? { connectNulls } : {}
|
|
1233
|
+
...defined({ curve, strokeWidth, lineStyle, connectNulls, points, baseline, splitColors: split }),
|
|
1234
|
+
...s.areaStyle ?? area ? { area: s.areaStyle ?? area } : {}
|
|
752
1235
|
},
|
|
753
1236
|
s.id
|
|
754
1237
|
)),
|
|
755
1238
|
children,
|
|
756
|
-
sc && /* @__PURE__ */
|
|
1239
|
+
sc && /* @__PURE__ */ jsx11(
|
|
1240
|
+
Scrubber,
|
|
1241
|
+
{
|
|
1242
|
+
...split && baseline !== void 0 ? { beaconColor: (v) => v >= baseline ? split.above : split.below } : {},
|
|
1243
|
+
...sc
|
|
1244
|
+
}
|
|
1245
|
+
)
|
|
757
1246
|
] });
|
|
758
1247
|
}
|
|
759
|
-
function AreaChart({
|
|
1248
|
+
function AreaChart({
|
|
1249
|
+
showXAxis,
|
|
1250
|
+
showYAxis,
|
|
1251
|
+
scrubber,
|
|
1252
|
+
curve = "monotone",
|
|
1253
|
+
fill = "gradient",
|
|
1254
|
+
showLines = true,
|
|
1255
|
+
strokeWidth = 2,
|
|
1256
|
+
lineStyle,
|
|
1257
|
+
baseline,
|
|
1258
|
+
splitColors,
|
|
1259
|
+
gradientStops,
|
|
1260
|
+
fillOpacity,
|
|
1261
|
+
children,
|
|
1262
|
+
...chart
|
|
1263
|
+
}) {
|
|
760
1264
|
const sc = axisProps(scrubber);
|
|
761
|
-
return /* @__PURE__ */
|
|
1265
|
+
return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
|
|
762
1266
|
axisElements(axisProps(showXAxis), axisProps(showYAxis)),
|
|
763
|
-
chart.series.map((s) =>
|
|
764
|
-
|
|
1267
|
+
chart.series.map((s) => {
|
|
1268
|
+
const own = s.areaStyle === "none" ? null : s.areaStyle ?? (chart.stacked && fill === "gradient" ? "solid" : fill);
|
|
1269
|
+
return own ? /* @__PURE__ */ jsx11(
|
|
1270
|
+
Area,
|
|
1271
|
+
{
|
|
1272
|
+
seriesId: s.id,
|
|
1273
|
+
curve,
|
|
1274
|
+
fill: own,
|
|
1275
|
+
...defined({ baseline, splitColors, gradientStops, opacity: fillOpacity ?? (chart.stacked && own === "solid" ? 0.55 : void 0) })
|
|
1276
|
+
},
|
|
1277
|
+
`a${s.id}`
|
|
1278
|
+
) : null;
|
|
1279
|
+
}),
|
|
1280
|
+
showLines && chart.series.map((s) => /* @__PURE__ */ jsx11(Line, { seriesId: s.id, curve, strokeWidth, ...defined({ lineStyle, baseline, splitColors }) }, `l${s.id}`)),
|
|
765
1281
|
children,
|
|
766
|
-
sc && /* @__PURE__ */
|
|
1282
|
+
sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
|
|
767
1283
|
] });
|
|
768
1284
|
}
|
|
769
|
-
function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGap, xAxis, children, ...chart }) {
|
|
1285
|
+
function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGap, stackGap, roundBaseline, barMinSize, layout = "vertical", xAxis, children, ...chart }) {
|
|
770
1286
|
const sc = axisProps(scrubber);
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
/* @__PURE__ */
|
|
774
|
-
|
|
1287
|
+
if (layout === "horizontal") {
|
|
1288
|
+
const yAx = axisProps(showYAxis);
|
|
1289
|
+
return /* @__PURE__ */ jsx11(
|
|
1290
|
+
HorizontalBarChart,
|
|
775
1291
|
{
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1292
|
+
series: chart.series,
|
|
1293
|
+
categories: (xAxis?.data ?? []).map(String),
|
|
1294
|
+
showValueAxis: !!showYAxis,
|
|
1295
|
+
showCategoryAxis: showXAxis !== false,
|
|
1296
|
+
enableScrubbing: !!sc || chart.enableScrubbing === true,
|
|
1297
|
+
legend: !!chart.legend,
|
|
1298
|
+
...defined({
|
|
1299
|
+
width: chart.width,
|
|
1300
|
+
height: chart.height,
|
|
1301
|
+
stacked: chart.stacked,
|
|
1302
|
+
radius,
|
|
1303
|
+
getBarColor,
|
|
1304
|
+
groupGap,
|
|
1305
|
+
stackGap,
|
|
1306
|
+
bandPadding: xAxis?.bandPadding,
|
|
1307
|
+
valueFormatter: yAx?.tickLabelFormatter,
|
|
1308
|
+
palette: chart.palette,
|
|
1309
|
+
className: chart.className,
|
|
1310
|
+
style: chart.style,
|
|
1311
|
+
"aria-label": chart["aria-label"]
|
|
1312
|
+
})
|
|
780
1313
|
}
|
|
781
|
-
)
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, xAxis: { ...xAxis, scaleType: "band" }, ...chart, children: [
|
|
1317
|
+
axisElements(axisProps(showXAxis), axisProps(showYAxis)),
|
|
1318
|
+
/* @__PURE__ */ jsx11(BarPlot, { ...defined({ radius, getBarColor, groupGap, stackGap, roundBaseline, minSize: barMinSize }), highlightScrubbed: !!sc }),
|
|
782
1319
|
children,
|
|
783
|
-
sc && /* @__PURE__ */
|
|
1320
|
+
sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
|
|
784
1321
|
] });
|
|
785
1322
|
}
|
|
1323
|
+
var pct = (_item, f) => `${formatNumber(Math.round(f * 1e3) / 10)}%`;
|
|
786
1324
|
function PercentageBarChart({
|
|
787
1325
|
data,
|
|
788
|
-
|
|
1326
|
+
rows,
|
|
1327
|
+
layout = "horizontal",
|
|
1328
|
+
barHeight,
|
|
1329
|
+
barLength = 160,
|
|
789
1330
|
gap = 2,
|
|
1331
|
+
radius,
|
|
790
1332
|
showLabels = true,
|
|
791
|
-
|
|
1333
|
+
legendPosition,
|
|
1334
|
+
legendShape,
|
|
1335
|
+
formatLabel = pct,
|
|
792
1336
|
palette,
|
|
793
1337
|
className,
|
|
794
1338
|
style,
|
|
795
1339
|
"aria-label": ariaLabel
|
|
796
1340
|
}) {
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1341
|
+
const vertical = layout === "vertical";
|
|
1342
|
+
const thick = barHeight ?? (vertical ? 28 : 10);
|
|
1343
|
+
const end = radius ?? (vertical ? Math.min(6, thick / 2) : thick / 2);
|
|
1344
|
+
const inner = Math.min(2, end);
|
|
1345
|
+
const legendAt = legendPosition ?? (vertical ? "right" : "bottom");
|
|
1346
|
+
const allRows = rows ?? [{ id: "row", data: data ?? [] }];
|
|
1347
|
+
const multi = allRows.length > 1 || !!rows;
|
|
1348
|
+
const colorOf = /* @__PURE__ */ new Map();
|
|
1349
|
+
const legendItems = [];
|
|
1350
|
+
for (const r of allRows)
|
|
1351
|
+
for (const d of r.data)
|
|
1352
|
+
if (!colorOf.has(d.id)) {
|
|
1353
|
+
colorOf.set(d.id, d.color ?? seriesColor3(colorOf.size, palette));
|
|
1354
|
+
legendItems.push(d);
|
|
1355
|
+
}
|
|
1356
|
+
const [hovered, setHovered] = useState5(null);
|
|
1357
|
+
const [hoverRow, setHoverRow] = useState5(null);
|
|
1358
|
+
const rowSegs = allRows.map((r) => {
|
|
1359
|
+
const segs = percentages(r.data);
|
|
1360
|
+
return { row: r, items: r.data.map((d, i) => ({ ...d, color: colorOf.get(d.id), fraction: segs[i].fraction })) };
|
|
1361
|
+
});
|
|
1362
|
+
const renderBar = (items, rowId) => {
|
|
1363
|
+
const visible = items.filter((i) => i.fraction > 0);
|
|
1364
|
+
const ordered = vertical ? [...visible].reverse() : visible;
|
|
1365
|
+
return /* @__PURE__ */ jsx11(
|
|
1366
|
+
"div",
|
|
804
1367
|
{
|
|
805
|
-
title: `${i.label} ${formatLabel(i, i.fraction)}`,
|
|
806
|
-
onPointerEnter: () => setHovered(i.id),
|
|
807
|
-
onPointerLeave: () => setHovered(null),
|
|
808
1368
|
style: {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1369
|
+
display: "flex",
|
|
1370
|
+
flexDirection: vertical ? "column" : "row",
|
|
1371
|
+
gap,
|
|
1372
|
+
...vertical ? { width: thick, height: barLength } : { height: thick, flex: 1, minWidth: 0 }
|
|
1373
|
+
},
|
|
1374
|
+
children: ordered.map((i, k) => {
|
|
1375
|
+
const a = k === 0 ? end : inner;
|
|
1376
|
+
const b = k === ordered.length - 1 ? end : inner;
|
|
1377
|
+
return /* @__PURE__ */ jsx11(
|
|
1378
|
+
"span",
|
|
1379
|
+
{
|
|
1380
|
+
title: `${i.label} ${formatLabel(i, i.fraction)}`,
|
|
1381
|
+
onPointerEnter: () => {
|
|
1382
|
+
setHovered(i.id);
|
|
1383
|
+
setHoverRow(rowId);
|
|
1384
|
+
},
|
|
1385
|
+
onPointerLeave: () => {
|
|
1386
|
+
setHovered(null);
|
|
1387
|
+
setHoverRow(null);
|
|
1388
|
+
},
|
|
1389
|
+
style: {
|
|
1390
|
+
opacity: hovered && hovered !== i.id ? 0.3 : 1,
|
|
1391
|
+
flex: `${i.fraction} 1 0`,
|
|
1392
|
+
[vertical ? "minHeight" : "minWidth"]: 2,
|
|
1393
|
+
background: i.color,
|
|
1394
|
+
borderRadius: vertical ? `${a}px ${a}px ${b}px ${b}px` : `${a}px ${b}px ${b}px ${a}px`,
|
|
1395
|
+
transition: "flex-grow 0.4s ease, opacity 0.15s",
|
|
1396
|
+
cursor: "default"
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
i.id
|
|
1400
|
+
);
|
|
1401
|
+
})
|
|
826
1402
|
}
|
|
827
|
-
)
|
|
828
|
-
|
|
1403
|
+
);
|
|
1404
|
+
};
|
|
1405
|
+
const legendRow = multi ? rowSegs.find((r) => r.row.id === hoverRow) : rowSegs[0];
|
|
1406
|
+
const legend = showLabels && /* @__PURE__ */ jsx11(
|
|
1407
|
+
Legend,
|
|
1408
|
+
{
|
|
1409
|
+
style: legendAt === "top" ? { marginBottom: 10 } : legendAt === "bottom" ? { marginTop: 10 } : {},
|
|
1410
|
+
direction: legendAt === "right" ? "column" : "row",
|
|
1411
|
+
...legendShape ? { shape: legendShape } : {},
|
|
1412
|
+
items: legendItems.map((d) => {
|
|
1413
|
+
const it = legendRow?.items.find((x) => x.id === d.id);
|
|
1414
|
+
return {
|
|
1415
|
+
id: d.id,
|
|
1416
|
+
label: d.label,
|
|
1417
|
+
color: colorOf.get(d.id),
|
|
1418
|
+
...it ? { value: formatLabel(it, it.fraction) } : {},
|
|
1419
|
+
inactive: !!hovered && hovered !== d.id
|
|
1420
|
+
};
|
|
1421
|
+
}),
|
|
1422
|
+
onItemHover: setHovered
|
|
1423
|
+
}
|
|
1424
|
+
);
|
|
1425
|
+
const label = (r) => multi && r.label ? /* @__PURE__ */ jsx11("span", { style: { fontSize: 12, color: hoverRow === r.id ? VAR.textStrong : VAR.text, fontWeight: hoverRow === r.id ? 700 : 500, whiteSpace: "nowrap" }, children: r.label }) : null;
|
|
1426
|
+
const bars = vertical ? /* @__PURE__ */ jsx11("div", { style: { display: "flex", gap: 14, alignItems: "flex-end" }, children: rowSegs.map(({ row, items }) => /* @__PURE__ */ jsxs9("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }, children: [
|
|
1427
|
+
renderBar(items, row.id),
|
|
1428
|
+
label(row)
|
|
1429
|
+
] }, row.id)) }) : multi ? /* @__PURE__ */ jsx11("div", { style: { display: "grid", gridTemplateColumns: "max-content 1fr", columnGap: 12, rowGap: 10, alignItems: "center", flex: legendAt === "right" ? 1 : void 0 }, children: rowSegs.map(({ row, items }) => /* @__PURE__ */ jsxs9("div", { style: { display: "contents" }, children: [
|
|
1430
|
+
label(row) ?? /* @__PURE__ */ jsx11("span", {}),
|
|
1431
|
+
renderBar(items, row.id)
|
|
1432
|
+
] }, row.id)) }) : /* @__PURE__ */ jsx11("div", { style: { display: "flex", flex: legendAt === "right" ? 1 : void 0 }, children: renderBar(rowSegs[0].items, rowSegs[0].row.id) });
|
|
1433
|
+
return /* @__PURE__ */ jsxs9(
|
|
1434
|
+
"div",
|
|
1435
|
+
{
|
|
1436
|
+
className,
|
|
1437
|
+
style: { fontFamily: VAR.font, ...legendAt === "right" ? { display: "flex", alignItems: "center", gap: 16 } : {}, ...style },
|
|
1438
|
+
role: "img",
|
|
1439
|
+
"aria-label": ariaLabel ?? rowSegs.map(({ row, items }) => `${row.label ? `${row.label}: ` : ""}${items.map((i) => `${i.label} ${formatLabel(i, i.fraction)}`).join(", ")}`).join("; "),
|
|
1440
|
+
children: [
|
|
1441
|
+
legendAt === "top" && legend,
|
|
1442
|
+
bars,
|
|
1443
|
+
legendAt !== "top" && legend
|
|
1444
|
+
]
|
|
1445
|
+
}
|
|
1446
|
+
);
|
|
829
1447
|
}
|
|
830
1448
|
export {
|
|
831
1449
|
Area,
|
|
@@ -836,9 +1454,11 @@ export {
|
|
|
836
1454
|
DEFAULT_PERIODS,
|
|
837
1455
|
FnChart,
|
|
838
1456
|
FnChartSlot,
|
|
1457
|
+
HorizontalBarChart,
|
|
839
1458
|
Legend,
|
|
840
1459
|
Line,
|
|
841
1460
|
LineChart,
|
|
1461
|
+
LiveTabLabel,
|
|
842
1462
|
PercentageBarChart,
|
|
843
1463
|
PeriodSelector,
|
|
844
1464
|
Point,
|