@funnest/charts-react 0.1.0 → 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/NOTICE +5 -0
- package/dist/index.js +845 -223
- 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,114 +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
|
-
var
|
|
521
|
-
|
|
892
|
+
var ROW = 18;
|
|
893
|
+
var TIP_PAD = 7;
|
|
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
|
+
}) {
|
|
522
906
|
const ctx = useCartesianChart();
|
|
523
907
|
const i = ctx.scrubIndex;
|
|
524
|
-
|
|
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
|
+
}
|
|
525
929
|
const { plot } = ctx;
|
|
526
930
|
const x = ctx.x(i);
|
|
527
931
|
const text = label ? label(i) : ctx.xLabel(i);
|
|
528
|
-
const list =
|
|
932
|
+
const list = pick;
|
|
529
933
|
const textX = Math.min(Math.max(x, plot.left + 30), plot.right - 30);
|
|
530
934
|
const rows = showValues ? list.flatMap((s) => {
|
|
531
935
|
const v = s.data[i];
|
|
532
|
-
|
|
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 }];
|
|
533
940
|
}) : [];
|
|
534
|
-
const tipW = Math.max(0, ...rows.map((r) =>
|
|
535
|
-
const tipH = rows.length *
|
|
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;
|
|
942
|
+
const tipH = rows.length * ROW + TIP_PAD * 2;
|
|
536
943
|
const tipRight = x + 12 + tipW <= plot.right;
|
|
537
944
|
const tipX = tipRight ? x + 12 : x - 12 - tipW;
|
|
538
945
|
const tipY = plot.top + 4;
|
|
539
|
-
return /* @__PURE__ */
|
|
540
|
-
dimFuture && !ctx.band && /* @__PURE__ */
|
|
541
|
-
/* @__PURE__ */
|
|
542
|
-
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 }),
|
|
543
950
|
!ctx.band && list.map((s) => {
|
|
544
951
|
const y = seriesY(ctx, s, i);
|
|
545
952
|
if (y === null) return null;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
+
)
|
|
549
976
|
] }, s.id);
|
|
550
977
|
}),
|
|
551
|
-
rows.length > 0 && /* @__PURE__ */
|
|
552
|
-
/* @__PURE__ */
|
|
553
|
-
rows.map((r, k) => /* @__PURE__ */
|
|
554
|
-
/* @__PURE__ */
|
|
555
|
-
(rows.length > 1 || r.s.label) && /* @__PURE__ */
|
|
556
|
-
/* @__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 })
|
|
557
984
|
] }, r.s.id))
|
|
558
985
|
] })
|
|
559
986
|
] });
|
|
560
987
|
}
|
|
561
988
|
|
|
562
989
|
// src/viz/annotations.tsx
|
|
563
|
-
import { jsx as
|
|
564
|
-
|
|
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
|
+
}) {
|
|
565
1020
|
const ctx = useCartesianChart();
|
|
566
1021
|
const { plot } = ctx;
|
|
1022
|
+
const pill = labelStyle === "pill";
|
|
567
1023
|
if (dataY !== void 0) {
|
|
568
|
-
const y = ctx
|
|
1024
|
+
const y = scaleFor(ctx, yAxisId)(dataY);
|
|
569
1025
|
if (y < plot.top - 0.5 || y > plot.bottom + 0.5) return null;
|
|
570
1026
|
const pos = labelPosition ?? "right";
|
|
571
1027
|
const tx = pos === "left" ? plot.left + 4 : pos === "center" ? (plot.left + plot.right) / 2 : plot.right - 4;
|
|
572
1028
|
const anchor = pos === "left" ? "start" : pos === "center" ? "middle" : "end";
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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 })
|
|
576
1034
|
] });
|
|
577
1035
|
}
|
|
578
1036
|
if (dataX !== void 0) {
|
|
579
1037
|
const x = ctx.x(dataX);
|
|
580
1038
|
const pos = labelPosition ?? "top";
|
|
581
|
-
return /* @__PURE__ */
|
|
582
|
-
/* @__PURE__ */
|
|
583
|
-
label !== void 0 && /* @__PURE__ */
|
|
584
|
-
|
|
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,
|
|
585
1043
|
{
|
|
586
|
-
x: x + 4,
|
|
587
|
-
y: pos === "bottom" ? plot.bottom -
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
paintOrder: "stroke",
|
|
593
|
-
style: { stroke: VAR.bg, fill: color === VAR.line ? VAR.text : color },
|
|
594
|
-
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
|
|
595
1050
|
}
|
|
596
1051
|
)
|
|
597
1052
|
] });
|
|
598
1053
|
}
|
|
599
1054
|
return null;
|
|
600
1055
|
}
|
|
601
|
-
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 }) {
|
|
602
1057
|
const ctx = useCartesianChart();
|
|
603
1058
|
const s = seriesId ? ctx.getSeries(seriesId) : void 0;
|
|
604
1059
|
const value = dataY ?? (s ? s.data[dataX] : void 0);
|
|
605
1060
|
if (value === null || value === void 0 || !Number.isFinite(value)) return null;
|
|
606
1061
|
const st = s ? ctx.stacked?.get(s.id) : void 0;
|
|
607
1062
|
const x = ctx.x(dataX);
|
|
608
|
-
const y = ctx
|
|
1063
|
+
const y = (s?.scale ?? scaleFor(ctx, yAxisId))(st && dataY === void 0 ? st.y1[dataX] : value);
|
|
609
1064
|
const fill = color ?? s?.color ?? ctx.series[0]?.color ?? "currentColor";
|
|
610
1065
|
const gap = radius + 6;
|
|
611
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"];
|
|
612
|
-
return /* @__PURE__ */
|
|
613
|
-
pulse && /* @__PURE__ */
|
|
614
|
-
/* @__PURE__ */
|
|
615
|
-
/* @__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" })
|
|
616
1071
|
] }),
|
|
617
|
-
/* @__PURE__ */
|
|
618
|
-
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 })
|
|
619
1074
|
] });
|
|
620
1075
|
}
|
|
621
1076
|
|
|
622
1077
|
// src/viz/period-selector.tsx
|
|
623
|
-
import { useLayoutEffect, useRef as
|
|
624
|
-
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
|
+
}
|
|
625
1092
|
var DEFAULT_PERIODS = [
|
|
626
1093
|
{ id: "1D", label: "1\uC77C" },
|
|
627
1094
|
{ id: "1W", label: "1\uC8FC" },
|
|
@@ -635,13 +1102,16 @@ function PeriodSelector({
|
|
|
635
1102
|
value,
|
|
636
1103
|
onChange,
|
|
637
1104
|
activeColor = "var(--fnv-accent, #6C4DFF)",
|
|
1105
|
+
liveColor = "var(--fnv-live, #F04452)",
|
|
638
1106
|
fullWidth = false,
|
|
639
1107
|
className,
|
|
640
1108
|
style,
|
|
641
1109
|
"aria-label": ariaLabel = "Period"
|
|
642
1110
|
}) {
|
|
643
|
-
const
|
|
644
|
-
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);
|
|
645
1115
|
useLayoutEffect(() => {
|
|
646
1116
|
const list = listRef.current;
|
|
647
1117
|
const active = list?.querySelector('[aria-selected="true"]');
|
|
@@ -663,7 +1133,7 @@ function PeriodSelector({
|
|
|
663
1133
|
onChange(p.id);
|
|
664
1134
|
listRef.current?.querySelector(`[data-period="${CSS.escape(p.id)}"]`)?.focus();
|
|
665
1135
|
};
|
|
666
|
-
return /* @__PURE__ */
|
|
1136
|
+
return /* @__PURE__ */ jsxs8(
|
|
667
1137
|
"div",
|
|
668
1138
|
{
|
|
669
1139
|
ref: listRef,
|
|
@@ -673,7 +1143,7 @@ function PeriodSelector({
|
|
|
673
1143
|
onKeyDown,
|
|
674
1144
|
style: { position: "relative", display: fullWidth ? "flex" : "inline-flex", gap: 2, fontFamily: VAR.font, ...style },
|
|
675
1145
|
children: [
|
|
676
|
-
pill && /* @__PURE__ */
|
|
1146
|
+
pill && /* @__PURE__ */ jsx10(
|
|
677
1147
|
"span",
|
|
678
1148
|
{
|
|
679
1149
|
"aria-hidden": "true",
|
|
@@ -684,7 +1154,7 @@ function PeriodSelector({
|
|
|
684
1154
|
left: pill.left,
|
|
685
1155
|
width: pill.width,
|
|
686
1156
|
borderRadius: 999,
|
|
687
|
-
background:
|
|
1157
|
+
background: pillColor,
|
|
688
1158
|
opacity: 0.12,
|
|
689
1159
|
transition: "left 0.2s ease, width 0.2s ease"
|
|
690
1160
|
}
|
|
@@ -692,7 +1162,7 @@ function PeriodSelector({
|
|
|
692
1162
|
),
|
|
693
1163
|
periods.map((p) => {
|
|
694
1164
|
const active = p.id === value;
|
|
695
|
-
return /* @__PURE__ */
|
|
1165
|
+
return /* @__PURE__ */ jsx10(
|
|
696
1166
|
"button",
|
|
697
1167
|
{
|
|
698
1168
|
type: "button",
|
|
@@ -710,13 +1180,13 @@ function PeriodSelector({
|
|
|
710
1180
|
border: 0,
|
|
711
1181
|
borderRadius: 999,
|
|
712
1182
|
background: "transparent",
|
|
713
|
-
color: active ? activeColor : VAR.text,
|
|
1183
|
+
color: active ? p.live ? liveColor : activeColor : VAR.text,
|
|
714
1184
|
font: "inherit",
|
|
715
1185
|
fontSize: 13,
|
|
716
1186
|
fontWeight: active ? 700 : 500,
|
|
717
1187
|
cursor: "pointer"
|
|
718
1188
|
},
|
|
719
|
-
children: p.label
|
|
1189
|
+
children: p.live ? /* @__PURE__ */ jsx10(LiveTabLabel, { label: p.label, color: liveColor }) : p.label
|
|
720
1190
|
},
|
|
721
1191
|
p.id
|
|
722
1192
|
);
|
|
@@ -727,103 +1197,253 @@ function PeriodSelector({
|
|
|
727
1197
|
}
|
|
728
1198
|
|
|
729
1199
|
// src/viz/charts.tsx
|
|
730
|
-
import { percentages, seriesColor as
|
|
731
|
-
import { useState as
|
|
732
|
-
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";
|
|
733
1203
|
var axisProps = (v) => v ? v === true ? {} : v : null;
|
|
734
1204
|
var axisElements = (x, y) => [
|
|
735
|
-
y && /* @__PURE__ */
|
|
736
|
-
x && /* @__PURE__ */
|
|
1205
|
+
y && /* @__PURE__ */ jsx11(YAxis, { ...y }, "y"),
|
|
1206
|
+
x && /* @__PURE__ */ jsx11(XAxis, { ...x }, "x")
|
|
737
1207
|
];
|
|
738
|
-
|
|
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
|
+
}) {
|
|
739
1225
|
const sc = axisProps(scrubber);
|
|
740
|
-
|
|
1226
|
+
const split = baseline !== void 0 ? splitColors ?? KR_SPLIT : void 0;
|
|
1227
|
+
return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
|
|
741
1228
|
axisElements(axisProps(showXAxis), axisProps(showYAxis)),
|
|
742
|
-
chart.series.map((s) => /* @__PURE__ */
|
|
1229
|
+
chart.series.map((s) => /* @__PURE__ */ jsx11(
|
|
743
1230
|
Line,
|
|
744
1231
|
{
|
|
745
1232
|
seriesId: s.id,
|
|
746
|
-
...curve
|
|
747
|
-
...
|
|
748
|
-
...area ? { area } : {},
|
|
749
|
-
...connectNulls ? { connectNulls } : {}
|
|
1233
|
+
...defined({ curve, strokeWidth, lineStyle, connectNulls, points, baseline, splitColors: split }),
|
|
1234
|
+
...s.areaStyle ?? area ? { area: s.areaStyle ?? area } : {}
|
|
750
1235
|
},
|
|
751
1236
|
s.id
|
|
752
1237
|
)),
|
|
753
1238
|
children,
|
|
754
|
-
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
|
+
)
|
|
755
1246
|
] });
|
|
756
1247
|
}
|
|
757
|
-
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
|
+
}) {
|
|
758
1264
|
const sc = axisProps(scrubber);
|
|
759
|
-
return /* @__PURE__ */
|
|
1265
|
+
return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
|
|
760
1266
|
axisElements(axisProps(showXAxis), axisProps(showYAxis)),
|
|
761
|
-
chart.series.map((s) =>
|
|
762
|
-
|
|
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}`)),
|
|
763
1281
|
children,
|
|
764
|
-
sc && /* @__PURE__ */
|
|
1282
|
+
sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
|
|
765
1283
|
] });
|
|
766
1284
|
}
|
|
767
|
-
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 }) {
|
|
768
1286
|
const sc = axisProps(scrubber);
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
/* @__PURE__ */
|
|
772
|
-
|
|
1287
|
+
if (layout === "horizontal") {
|
|
1288
|
+
const yAx = axisProps(showYAxis);
|
|
1289
|
+
return /* @__PURE__ */ jsx11(
|
|
1290
|
+
HorizontalBarChart,
|
|
773
1291
|
{
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
+
})
|
|
778
1313
|
}
|
|
779
|
-
)
|
|
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 }),
|
|
780
1319
|
children,
|
|
781
|
-
sc && /* @__PURE__ */
|
|
1320
|
+
sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
|
|
782
1321
|
] });
|
|
783
1322
|
}
|
|
1323
|
+
var pct = (_item, f) => `${formatNumber(Math.round(f * 1e3) / 10)}%`;
|
|
784
1324
|
function PercentageBarChart({
|
|
785
1325
|
data,
|
|
786
|
-
|
|
1326
|
+
rows,
|
|
1327
|
+
layout = "horizontal",
|
|
1328
|
+
barHeight,
|
|
1329
|
+
barLength = 160,
|
|
787
1330
|
gap = 2,
|
|
1331
|
+
radius,
|
|
788
1332
|
showLabels = true,
|
|
789
|
-
|
|
1333
|
+
legendPosition,
|
|
1334
|
+
legendShape,
|
|
1335
|
+
formatLabel = pct,
|
|
790
1336
|
palette,
|
|
791
1337
|
className,
|
|
792
1338
|
style,
|
|
793
1339
|
"aria-label": ariaLabel
|
|
794
1340
|
}) {
|
|
795
|
-
const
|
|
796
|
-
const
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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",
|
|
802
1367
|
{
|
|
803
|
-
title: `${i.label} ${formatLabel(i, i.fraction)}`,
|
|
804
|
-
onPointerEnter: () => setHovered(i.id),
|
|
805
|
-
onPointerLeave: () => setHovered(null),
|
|
806
1368
|
style: {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
})
|
|
824
1402
|
}
|
|
825
|
-
)
|
|
826
|
-
|
|
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
|
+
);
|
|
827
1447
|
}
|
|
828
1448
|
export {
|
|
829
1449
|
Area,
|
|
@@ -834,9 +1454,11 @@ export {
|
|
|
834
1454
|
DEFAULT_PERIODS,
|
|
835
1455
|
FnChart,
|
|
836
1456
|
FnChartSlot,
|
|
1457
|
+
HorizontalBarChart,
|
|
837
1458
|
Legend,
|
|
838
1459
|
Line,
|
|
839
1460
|
LineChart,
|
|
1461
|
+
LiveTabLabel,
|
|
840
1462
|
PercentageBarChart,
|
|
841
1463
|
PeriodSelector,
|
|
842
1464
|
Point,
|