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