@funnest/charts-react 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  * </FnChart>
8
8
  * ```
9
9
  *
10
- * Plus SVG chart components (LineChart, AreaChart, BarChart, PercentageBarChart, CartesianChart
10
+ * Plus SVG chart components (LineChart, AreaChart, BarChart, CandlestickChart, DonutChart, TreemapChart,
11
+ * PercentageBarChart, CartesianChart
11
12
  * and its parts) built on the platform-neutral math in @funnest/charts-viz.
12
13
  */
13
14
  export { FnChart, FnChartSlot, useChartEvent, useFnChart } from './fn-chart';
package/dist/index.js CHANGED
@@ -93,6 +93,7 @@ import {
93
93
 
94
94
  // src/viz/context.ts
95
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;
96
97
  var CartesianContext = createContext2(null);
97
98
  function useCartesianChart() {
98
99
  const ctx = useContext2(CartesianContext);
@@ -244,6 +245,7 @@ function CartesianChart({
244
245
  const [motion, setMotion] = useState2(false);
245
246
  const [hidden, setHidden] = useState2([]);
246
247
  const [emphasis, setEmphasis] = useState2(null);
248
+ const [dimFuture, setDimFuture] = useState2(false);
247
249
  useEffect2(() => setMotion(animate && !prefersReducedMotion()), [animate]);
248
250
  const legendCfg = normalizeLegend(legend);
249
251
  const axes = useMemo(() => {
@@ -325,9 +327,11 @@ function CartesianChart({
325
327
  xData,
326
328
  scrubIndex,
327
329
  focused,
328
- animate: motion
330
+ animate: motion,
331
+ futureMask: dimFuture && !band && scrubIndex !== null && scrubIndex < length ? `url(#${id}-future)` : void 0,
332
+ setDimFuture
329
333
  };
330
- }, [id, width, height, inset, axisSpace, all, visible, axes, firstAxis, xAxis, stacked, scrubIndex, focused, motion]);
334
+ }, [id, width, height, inset, axisSpace, all, visible, axes, firstAxis, xAxis, stacked, scrubIndex, focused, motion, dimFuture]);
331
335
  const xsRef = useRef2([]);
332
336
  xsRef.current = useMemo(() => Array.from({ length: ctx.length }, (_, i) => ctx.x(i)), [ctx.length, ctx.x]);
333
337
  const updateScrub = useCallback(
@@ -415,6 +419,11 @@ function CartesianChart({
415
419
  updateScrub(null);
416
420
  } : void 0,
417
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
+ ] }),
418
427
  width > 0 && hasData && /* @__PURE__ */ jsx3(CartesianContext.Provider, { value: ctx, children }),
419
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 })
420
429
  ]
@@ -572,13 +581,13 @@ function Area({ seriesId, curve = "monotone", fill = "gradient", opacity, connec
572
581
  ] }, key);
573
582
  };
574
583
  if (splitColors && baseline !== void 0 && !run0IsStacked(ctx, s)) {
575
- return /* @__PURE__ */ jsxs4("g", { children: [
584
+ return /* @__PURE__ */ jsxs4("g", { mask: ctx.futureMask, children: [
576
585
  /* @__PURE__ */ jsx5(SplitClips, { id: gid, y: base, ctx }),
577
586
  paint("above", splitColors.above, ctx.plot.top, `${gid}-above`),
578
587
  paint("below", splitColors.below, ctx.plot.bottom, `${gid}-below`)
579
588
  ] });
580
589
  }
581
- return /* @__PURE__ */ jsx5("g", { children: paint("fill", s.color, ctx.plot.top) });
590
+ return /* @__PURE__ */ jsx5("g", { mask: ctx.futureMask, children: paint("fill", s.color, ctx.plot.top) });
582
591
  }
583
592
  var run0IsStacked = (ctx, s) => !!ctx.stacked?.get(s.id);
584
593
  function Line({
@@ -606,7 +615,7 @@ function Line({
606
615
  const gid = `${ctx.id}-line-${s.index}`;
607
616
  const baseY = baselineY(ctx, s.scale, baseline);
608
617
  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(
618
+ const strokes = (color, clip) => /* @__PURE__ */ jsx5("g", { clipPath: clip ? `url(#${clip})` : void 0, mask: ctx.futureMask, children: runs.map((run, k) => /* @__PURE__ */ jsx5(
610
619
  "path",
611
620
  {
612
621
  d: linePath(run.upper, curve),
@@ -640,6 +649,7 @@ function Line({
640
649
  p.label && /* @__PURE__ */ jsx5(
641
650
  "text",
642
651
  {
652
+ opacity: nearScrub(ctx, x) ? 0 : 1,
643
653
  x,
644
654
  y: p.labelPosition === "bottom" ? y + r + 13 : y - r - 6,
645
655
  textAnchor: "middle",
@@ -770,6 +780,7 @@ function HorizontalBarChart({
770
780
  const wrapRef = useRef3(null);
771
781
  const [measured, setMeasured] = useState3(fixedWidth ?? 0);
772
782
  const [hover, setHover] = useState3(null);
783
+ const [focused, setFocused] = useState3(false);
773
784
  useEffect3(() => {
774
785
  if (fixedWidth !== void 0) return setMeasured(fixedWidth);
775
786
  const el = wrapRef.current;
@@ -810,6 +821,19 @@ function HorizontalBarChart({
810
821
  const r = e.currentTarget.getBoundingClientRect();
811
822
  setHover(band.invert(e.clientY - r.top));
812
823
  };
824
+ const onKeyDown = (e) => {
825
+ const last = rows - 1;
826
+ if (last < 0) return;
827
+ let next;
828
+ if (e.key === "ArrowDown" || e.key === "ArrowRight") next = hover === null ? 0 : Math.min(last, hover + 1);
829
+ else if (e.key === "ArrowUp" || e.key === "ArrowLeft") next = hover === null ? last : Math.max(0, hover - 1);
830
+ else if (e.key === "Home") next = 0;
831
+ else if (e.key === "End") next = last;
832
+ else if (e.key === "Escape") next = null;
833
+ if (next === void 0) return;
834
+ e.preventDefault();
835
+ setHover(next);
836
+ };
813
837
  const tipRows = hover === null ? [] : resolved.flatMap((s) => {
814
838
  const v = s.high[hover];
815
839
  if (v === null || v === void 0) return [];
@@ -835,9 +859,16 @@ function HorizontalBarChart({
835
859
  height: h,
836
860
  role: "img",
837
861
  "aria-label": ariaLabel,
838
- style: { display: "block", overflow: "visible", userSelect: "none" },
862
+ tabIndex: enableScrubbing ? 0 : void 0,
863
+ style: { display: "block", overflow: "visible", userSelect: "none", outline: "none", borderRadius: 6, boxShadow: focused ? `0 0 0 2px ${VAR.accentRing}` : void 0 },
839
864
  onPointerMove: enableScrubbing ? onMove : void 0,
840
- onPointerLeave: enableScrubbing ? () => setHover(null) : void 0,
865
+ onPointerLeave: enableScrubbing ? () => !focused && setHover(null) : void 0,
866
+ onKeyDown: enableScrubbing ? onKeyDown : void 0,
867
+ onFocus: enableScrubbing ? () => setFocused(true) : void 0,
868
+ onBlur: enableScrubbing ? () => {
869
+ setFocused(false);
870
+ setHover(null);
871
+ } : void 0,
841
872
  children: w > 0 && /* @__PURE__ */ jsxs5(Fragment3, { children: [
842
873
  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
874
  min < 0 && /* @__PURE__ */ jsx7("line", { x1: zero, x2: zero, y1: top, y2: bottom, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
@@ -882,16 +913,534 @@ function HorizontalBarChart({
882
913
  ] })
883
914
  ] })
884
915
  }
885
- )
916
+ ),
917
+ enableScrubbing && /* @__PURE__ */ jsx7("span", { "aria-live": "polite", style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: focused && hover !== null ? [categories?.[hover] ?? String(hover), ...tipRows.map((r) => `${r.s.label ?? r.s.id} ${r.text}`)].join(", ") : "" })
886
918
  ] });
887
919
  }
888
920
 
889
- // src/viz/scrubber.tsx
921
+ // src/viz/donut.tsx
922
+ import { arcPath, arcs, seriesColor as seriesColor3 } from "@funnest/charts-viz";
923
+ import { useId as useId2, useState as useState4 } from "react";
890
924
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
925
+ var pctText = (f) => `${formatNumber(Math.round(f * 1e3) / 10)}%`;
926
+ var isCenter = (v) => !!v && typeof v === "object" && "value" in v && typeof v.value === "string";
927
+ function DonutChart({
928
+ data,
929
+ size = 180,
930
+ innerRadius = 0.68,
931
+ padAngle = 1.5,
932
+ cornerRadius = 3,
933
+ center,
934
+ totalLabel,
935
+ valueFormatter = formatNumber,
936
+ showLegend = true,
937
+ legendPosition = "right",
938
+ legendShape,
939
+ formatLabel = (_i, f) => pctText(f),
940
+ activeId,
941
+ onActiveChange,
942
+ palette,
943
+ className,
944
+ style,
945
+ "aria-label": ariaLabel
946
+ }) {
947
+ const uid = useId2();
948
+ const [own, setOwn] = useState4(null);
949
+ const [focused, setFocused] = useState4(false);
950
+ const active = activeId !== void 0 ? activeId : own;
951
+ const setActive = (id) => {
952
+ if (activeId === void 0) setOwn(id);
953
+ onActiveChange?.(id);
954
+ };
955
+ const segs = arcs(data, padAngle * Math.PI / 180);
956
+ const items = data.map((d, i) => ({ ...d, color: d.color ?? seriesColor3(i, palette), seg: segs[i] }));
957
+ const visible = items.filter((i) => i.seg.fraction > 0);
958
+ const total = data.reduce((t, d) => t + Math.max(0, d.value), 0);
959
+ const hit = items.find((i) => i.id === active) ?? null;
960
+ const pop = 4;
961
+ const r = size / 2 - pop;
962
+ const ir = Math.max(0, Math.min(0.95, innerRadius)) * r;
963
+ const c = size / 2;
964
+ const centerContent = (() => {
965
+ const a = hit ? { item: hit, fraction: hit.seg.fraction } : null;
966
+ if (center) return center(a, total);
967
+ return a ? { value: pctText(a.fraction), label: a.item.label } : { value: valueFormatter(total), ...totalLabel ? { label: totalLabel } : {} };
968
+ })();
969
+ const onKeyDown = (e) => {
970
+ if (!visible.length) return;
971
+ const k = visible.findIndex((i) => i.id === active);
972
+ let next;
973
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") next = visible[(k + 1) % visible.length].id;
974
+ else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = visible[(k <= 0 ? visible.length : k) - 1].id;
975
+ else if (e.key === "Home") next = visible[0].id;
976
+ else if (e.key === "End") next = visible[visible.length - 1].id;
977
+ else if (e.key === "Escape") next = null;
978
+ if (next === void 0) return;
979
+ e.preventDefault();
980
+ setActive(next);
981
+ };
982
+ const describe = (i) => `${i.label} ${formatLabel(i, i.seg.fraction)} (${valueFormatter(i.value)})`;
983
+ const side = legendPosition === "right";
984
+ const svg = /* @__PURE__ */ jsxs6(
985
+ "svg",
986
+ {
987
+ width: size,
988
+ height: size,
989
+ viewBox: `0 0 ${size} ${size}`,
990
+ role: "img",
991
+ "aria-label": ariaLabel ?? items.map(describe).join(", "),
992
+ tabIndex: 0,
993
+ style: { display: "block", flex: "none", overflow: "visible", outline: "none", borderRadius: "50%", boxShadow: focused ? `0 0 0 2px ${VAR.accentRing}` : void 0 },
994
+ onKeyDown,
995
+ onFocus: () => setFocused(true),
996
+ onBlur: () => {
997
+ setFocused(false);
998
+ setActive(null);
999
+ },
1000
+ onPointerLeave: () => setActive(null),
1001
+ children: [
1002
+ total <= 0 && /* @__PURE__ */ jsx8("circle", { cx: c, cy: c, r: (r + ir) / 2, fill: "none", strokeWidth: Math.max(1, r - ir), style: { stroke: VAR.grid } }),
1003
+ visible.map((i) => {
1004
+ const on = i.id === active;
1005
+ const mid = (i.seg.startAngle + i.seg.endAngle) / 2;
1006
+ const dx = on ? Math.sin(mid) * pop : 0;
1007
+ const dy = on ? -Math.cos(mid) * pop : 0;
1008
+ return /* @__PURE__ */ jsx8(
1009
+ "path",
1010
+ {
1011
+ d: arcPath(c, c, ir, r, i.seg.startAngle, i.seg.endAngle, cornerRadius),
1012
+ transform: `translate(${dx.toFixed(2)},${dy.toFixed(2)})`,
1013
+ opacity: active && !on ? 0.35 : 1,
1014
+ onPointerEnter: () => setActive(i.id),
1015
+ style: { fill: i.color, transition: "transform 0.15s ease, opacity 0.15s", cursor: "default" },
1016
+ children: /* @__PURE__ */ jsx8("title", { children: describe(i) })
1017
+ },
1018
+ i.id
1019
+ );
1020
+ }),
1021
+ ir > 24 && centerContent !== null && (isCenter(centerContent) ? /* @__PURE__ */ jsxs6("g", { pointerEvents: "none", style: { fontFamily: VAR.font }, children: [
1022
+ /* @__PURE__ */ jsx8(
1023
+ "text",
1024
+ {
1025
+ x: c,
1026
+ y: centerContent.label ? c - 8 : c,
1027
+ textAnchor: "middle",
1028
+ dominantBaseline: "central",
1029
+ fontSize: Math.max(12, Math.min(22, ir * 0.36)),
1030
+ fontWeight: 700,
1031
+ style: { fill: VAR.textStrong, fontVariantNumeric: "tabular-nums" },
1032
+ children: centerContent.value
1033
+ }
1034
+ ),
1035
+ centerContent.label && /* @__PURE__ */ jsx8("text", { x: c, y: c + 13, textAnchor: "middle", dominantBaseline: "central", fontSize: 12, style: { fill: VAR.text }, children: centerContent.label })
1036
+ ] }) : /* @__PURE__ */ jsx8("foreignObject", { x: c - ir * 0.72, y: c - ir * 0.72, width: ir * 1.44, height: ir * 1.44, pointerEvents: "none", children: /* @__PURE__ */ jsx8("div", { style: { width: "100%", height: "100%", display: "grid", placeItems: "center", textAlign: "center" }, children: centerContent }) }))
1037
+ ]
1038
+ }
1039
+ );
1040
+ return /* @__PURE__ */ jsxs6(
1041
+ "div",
1042
+ {
1043
+ className,
1044
+ style: {
1045
+ fontFamily: VAR.font,
1046
+ position: "relative",
1047
+ display: "flex",
1048
+ flexDirection: side ? "row" : "column",
1049
+ alignItems: "center",
1050
+ gap: side ? 24 : 14,
1051
+ ...style
1052
+ },
1053
+ children: [
1054
+ svg,
1055
+ showLegend && /* @__PURE__ */ jsx8(
1056
+ Legend,
1057
+ {
1058
+ direction: side ? "column" : "row",
1059
+ justify: side ? "flex-start" : "center",
1060
+ ...legendShape ? { shape: legendShape } : {},
1061
+ items: items.map((i) => ({ id: i.id, label: i.label, color: i.color, value: formatLabel(i, i.seg.fraction), inactive: !!active && active !== i.id })),
1062
+ onItemHover: setActive
1063
+ }
1064
+ ),
1065
+ /* @__PURE__ */ jsx8("span", { id: `${uid}-live`, "aria-live": "polite", style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: focused && hit ? describe(hit) : "" })
1066
+ ]
1067
+ }
1068
+ );
1069
+ }
1070
+
1071
+ // src/viz/treemap.tsx
1072
+ import { seriesColor as seriesColor4, treemap } from "@funnest/charts-viz";
1073
+ import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
1074
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1075
+ var hex = (c) => {
1076
+ const m = /^#([0-9a-f]{6})$/i.exec(c);
1077
+ if (!m) return null;
1078
+ const n = parseInt(m[1], 16);
1079
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1080
+ };
1081
+ var mix = (a, b, t) => {
1082
+ const x = hex(a);
1083
+ const y = hex(b);
1084
+ if (!x || !y) return t >= 0.5 ? b : a;
1085
+ const c = x.map((v, i) => Math.round(v + (y[i] - v) * t));
1086
+ return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
1087
+ };
1088
+ var defaultChange = (c) => `${c > 0 ? "+" : ""}${c.toFixed(2)}%`;
1089
+ var textWidth2 = (t, size) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? size : size * 0.6), 0);
1090
+ function TreemapChart({
1091
+ data,
1092
+ width: fixedWidth,
1093
+ height = 280,
1094
+ gap = 2,
1095
+ radius = 4,
1096
+ upColor = "#F04452",
1097
+ downColor = "#3182F6",
1098
+ flatColor = "#8B95A1",
1099
+ maxChange = 5,
1100
+ getColor,
1101
+ changeFormatter = defaultChange,
1102
+ valueFormatter = formatNumber,
1103
+ enableTooltip = true,
1104
+ onItemClick,
1105
+ palette,
1106
+ className,
1107
+ style,
1108
+ "aria-label": ariaLabel
1109
+ }) {
1110
+ const wrapRef = useRef4(null);
1111
+ const [measured, setMeasured] = useState5(fixedWidth ?? 0);
1112
+ const [active, setActive] = useState5(null);
1113
+ const [focused, setFocused] = useState5(false);
1114
+ useEffect4(() => {
1115
+ if (fixedWidth !== void 0) return setMeasured(fixedWidth);
1116
+ const el = wrapRef.current;
1117
+ if (!el || typeof ResizeObserver === "undefined") return;
1118
+ const ro = new ResizeObserver(([e]) => e && setMeasured(Math.round(e.contentRect.width)));
1119
+ ro.observe(el);
1120
+ setMeasured(Math.round(el.getBoundingClientRect().width));
1121
+ return () => ro.disconnect();
1122
+ }, [fixedWidth]);
1123
+ const w = measured;
1124
+ const byId = useMemo3(() => new Map(data.map((d, i) => [d.id, { d, i }])), [data]);
1125
+ const rects = useMemo3(() => treemap(data, w, height, gap), [data, w, height, gap]);
1126
+ const colorOf = (d, i) => {
1127
+ const custom = getColor?.(d, i) ?? d.color;
1128
+ if (custom) return custom;
1129
+ if (d.change === void 0) return seriesColor4(i, palette);
1130
+ if (Math.abs(d.change) < 1e-9) return flatColor;
1131
+ const t = 0.45 + 0.55 * Math.min(1, Math.abs(d.change) / maxChange);
1132
+ return mix(flatColor, d.change > 0 ? upColor : downColor, t);
1133
+ };
1134
+ const onKeyDown = (e) => {
1135
+ if (!rects.length) return;
1136
+ const k = rects.findIndex((r) => r.id === active);
1137
+ let next;
1138
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") next = rects[(k + 1) % rects.length].id;
1139
+ else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = rects[(k <= 0 ? rects.length : k) - 1].id;
1140
+ else if (e.key === "Home") next = rects[0].id;
1141
+ else if (e.key === "End") next = rects[rects.length - 1].id;
1142
+ else if (e.key === "Escape") next = null;
1143
+ else if ((e.key === "Enter" || e.key === " ") && active && onItemClick) {
1144
+ e.preventDefault();
1145
+ onItemClick(byId.get(active).d);
1146
+ return;
1147
+ }
1148
+ if (next === void 0) return;
1149
+ e.preventDefault();
1150
+ setActive(next);
1151
+ };
1152
+ const describe = (d) => [d.label, d.sublabel, d.change !== void 0 ? changeFormatter(d.change) : void 0, valueFormatter(d.value)].filter(Boolean).join(" ");
1153
+ const hit = active ? rects.find((r) => r.id === active) : void 0;
1154
+ const hitItem = hit ? byId.get(hit.id).d : void 0;
1155
+ let tip = null;
1156
+ if (enableTooltip && hit && hitItem) {
1157
+ const rows = [[hitItem.label, valueFormatter(hitItem.value)]];
1158
+ if (hitItem.change !== void 0) rows.push(["", changeFormatter(hitItem.change)]);
1159
+ const tw = Math.max(...rows.map(([a, b]) => textWidth2(a, 12) + textWidth2(b, 12) + 28)) + 8;
1160
+ const th = rows.length * 18 + 14;
1161
+ const cx = hit.x + hit.width / 2;
1162
+ let tx = cx - tw / 2;
1163
+ tx = Math.max(0, Math.min(w - tw, tx));
1164
+ const ty = hit.y - th - 6 >= 0 ? hit.y - th - 6 : Math.min(height - th, hit.y + hit.height + 6 <= height - th ? hit.y + hit.height + 6 : hit.y + 6);
1165
+ tip = { x: tx, y: ty, w: tw, h: th, rows };
1166
+ }
1167
+ return /* @__PURE__ */ jsxs7("div", { ref: wrapRef, className, style: { position: "relative", width: fixedWidth ?? "100%", fontFamily: VAR.font, ...style }, children: [
1168
+ /* @__PURE__ */ jsxs7(
1169
+ "svg",
1170
+ {
1171
+ width: w || "100%",
1172
+ height,
1173
+ role: "img",
1174
+ "aria-label": ariaLabel ?? data.map(describe).join(", "),
1175
+ tabIndex: 0,
1176
+ style: { display: "block", overflow: "visible", userSelect: "none", outline: "none", borderRadius: radius + 2, boxShadow: focused ? `0 0 0 2px ${VAR.accentRing}` : void 0 },
1177
+ onKeyDown,
1178
+ onFocus: () => setFocused(true),
1179
+ onBlur: () => {
1180
+ setFocused(false);
1181
+ setActive(null);
1182
+ },
1183
+ onPointerLeave: () => setActive(null),
1184
+ children: [
1185
+ rects.map((r) => {
1186
+ const { d, i } = byId.get(r.id);
1187
+ const on = active === r.id;
1188
+ const fs = Math.max(9, Math.min(22, Math.sqrt(r.width * r.height) / 6, (r.width - 10) / Math.max(1, textWidth2(d.label, 1))));
1189
+ const sub = d.change !== void 0 ? changeFormatter(d.change) : d.sublabel;
1190
+ const fitsLabel = r.width >= textWidth2(d.label, fs) + 10 && r.height >= fs + 8;
1191
+ const fitsSub = !!sub && fitsLabel && r.height >= fs * 2 + 14 && r.width >= textWidth2(sub, fs * 0.8) + 10;
1192
+ const cy = r.y + r.height / 2;
1193
+ return /* @__PURE__ */ jsxs7("g", { onPointerEnter: () => setActive(r.id), onClick: onItemClick ? () => onItemClick(d) : void 0, style: { cursor: onItemClick ? "pointer" : "default" }, children: [
1194
+ /* @__PURE__ */ jsx9(
1195
+ "rect",
1196
+ {
1197
+ x: r.x,
1198
+ y: r.y,
1199
+ width: Math.max(0, r.width),
1200
+ height: Math.max(0, r.height),
1201
+ rx: Math.min(radius, r.width / 2, r.height / 2),
1202
+ style: { fill: colorOf(d, i), transition: "opacity 0.15s" },
1203
+ opacity: active && !on ? 0.55 : 1
1204
+ }
1205
+ ),
1206
+ on && /* @__PURE__ */ jsx9(
1207
+ "rect",
1208
+ {
1209
+ x: r.x + 1,
1210
+ y: r.y + 1,
1211
+ width: Math.max(0, r.width - 2),
1212
+ height: Math.max(0, r.height - 2),
1213
+ rx: Math.min(radius, r.width / 2, r.height / 2),
1214
+ fill: "none",
1215
+ strokeWidth: 2,
1216
+ style: { stroke: "#fff" }
1217
+ }
1218
+ ),
1219
+ fitsLabel && /* @__PURE__ */ jsx9(
1220
+ "text",
1221
+ {
1222
+ x: r.x + r.width / 2,
1223
+ y: fitsSub ? cy - fs * 0.45 : cy,
1224
+ textAnchor: "middle",
1225
+ dominantBaseline: "central",
1226
+ fontSize: fs,
1227
+ fontWeight: 700,
1228
+ pointerEvents: "none",
1229
+ style: { fill: "#fff" },
1230
+ children: d.label
1231
+ }
1232
+ ),
1233
+ fitsSub && /* @__PURE__ */ jsx9(
1234
+ "text",
1235
+ {
1236
+ x: r.x + r.width / 2,
1237
+ y: cy + fs * 0.65,
1238
+ textAnchor: "middle",
1239
+ dominantBaseline: "central",
1240
+ fontSize: fs * 0.78,
1241
+ fontWeight: 600,
1242
+ pointerEvents: "none",
1243
+ style: { fill: "rgba(255,255,255,0.88)", fontVariantNumeric: "tabular-nums" },
1244
+ children: sub
1245
+ }
1246
+ )
1247
+ ] }, r.id);
1248
+ }),
1249
+ tip && /* @__PURE__ */ jsxs7("g", { transform: `translate(${tip.x},${tip.y})`, pointerEvents: "none", children: [
1250
+ /* @__PURE__ */ jsx9("rect", { width: tip.w, height: tip.h, rx: 8, strokeWidth: 1, style: { fill: VAR.bg, stroke: VAR.grid } }),
1251
+ tip.rows.map(([a, b], k) => /* @__PURE__ */ jsxs7("g", { transform: `translate(10,${7 + 9 + k * 18})`, children: [
1252
+ /* @__PURE__ */ jsx9("text", { dominantBaseline: "central", fontSize: 12, style: { fill: VAR.text }, children: a }),
1253
+ /* @__PURE__ */ jsx9(
1254
+ "text",
1255
+ {
1256
+ x: tip.w - 20,
1257
+ textAnchor: "end",
1258
+ dominantBaseline: "central",
1259
+ fontSize: 12,
1260
+ fontWeight: 700,
1261
+ style: { fill: k === 1 && hitItem?.change ? hitItem.change > 0 ? upColor : downColor : VAR.textStrong, fontVariantNumeric: "tabular-nums" },
1262
+ children: b
1263
+ }
1264
+ )
1265
+ ] }, k))
1266
+ ] })
1267
+ ]
1268
+ }
1269
+ ),
1270
+ /* @__PURE__ */ jsx9("span", { "aria-live": "polite", style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: focused && hitItem ? describe(hitItem) : "" })
1271
+ ] });
1272
+ }
1273
+
1274
+ // src/viz/candles.tsx
1275
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1276
+ var KO = { open: "\uC2DC\uAC00", high: "\uACE0\uAC00", low: "\uC800\uAC00", close: "\uC885\uAC00", volume: "\uAC70\uB798\uB7C9" };
1277
+ function CandlePlot({
1278
+ data,
1279
+ yAxisId,
1280
+ upColor = "#F04452",
1281
+ downColor = "#3182F6",
1282
+ bodyWidth = 0.7,
1283
+ hollowUp = false,
1284
+ tooltip = true,
1285
+ labels = KO,
1286
+ valueFormatter = formatNumber
1287
+ }) {
1288
+ const ctx = useCartesianChart();
1289
+ const band = ctx.band;
1290
+ if (!band) return null;
1291
+ const y = scaleFor(ctx, yAxisId);
1292
+ const bw = Math.max(1, Math.min(band.step * bodyWidth, band.step - 1));
1293
+ const i = ctx.scrubIndex;
1294
+ const scrubbed = i !== null ? data[i] : null;
1295
+ let tip = null;
1296
+ if (tooltip && i !== null && scrubbed) {
1297
+ const prev = i > 0 ? data[i - 1] : null;
1298
+ const base = prev?.close ?? scrubbed.open;
1299
+ const chg = base ? (scrubbed.close - base) / base * 100 : 0;
1300
+ const rows = [
1301
+ [labels.open, valueFormatter(scrubbed.open)],
1302
+ [labels.high, valueFormatter(scrubbed.high), upColor],
1303
+ [labels.low, valueFormatter(scrubbed.low), downColor],
1304
+ [labels.close, `${valueFormatter(scrubbed.close)} (${chg > 0 ? "+" : ""}${chg.toFixed(2)}%)`, chg > 0 ? upColor : chg < 0 ? downColor : void 0]
1305
+ ];
1306
+ if (scrubbed.volume !== void 0) rows.push([labels.volume ?? KO.volume, formatNumber(scrubbed.volume)]);
1307
+ const width = (s) => [...s].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? 12 : 7.2), 0);
1308
+ const tw = Math.max(...rows.map(([a, b]) => width(a) + width(b) + 24)) + 8;
1309
+ const th = rows.length * 18 + 14;
1310
+ const x = band.center(i);
1311
+ const right = x + 12 + tw <= ctx.plot.right;
1312
+ tip = /* @__PURE__ */ jsxs8("g", { pointerEvents: "none", children: [
1313
+ /* @__PURE__ */ jsx10("line", { x1: x, x2: x, y1: ctx.plot.top, y2: ctx.plot.bottom, strokeWidth: 1, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
1314
+ /* @__PURE__ */ jsxs8("g", { transform: `translate(${right ? x + 12 : x - 12 - tw},${ctx.plot.top + 4})`, children: [
1315
+ /* @__PURE__ */ jsx10("rect", { width: tw, height: th, rx: 8, strokeWidth: 1, style: { fill: VAR.bg, stroke: VAR.grid } }),
1316
+ rows.map(([a, b, color], k) => /* @__PURE__ */ jsxs8("g", { transform: `translate(10,${16 + k * 18})`, children: [
1317
+ /* @__PURE__ */ jsx10("text", { dominantBaseline: "central", fontSize: 12, style: { fill: VAR.text }, children: a }),
1318
+ /* @__PURE__ */ jsx10("text", { x: tw - 20, textAnchor: "end", dominantBaseline: "central", fontSize: 12, fontWeight: 700, style: { fill: color ?? VAR.textStrong, fontVariantNumeric: "tabular-nums" }, children: b })
1319
+ ] }, a))
1320
+ ] })
1321
+ ] });
1322
+ }
1323
+ return /* @__PURE__ */ jsxs8("g", { children: [
1324
+ data.map((d, k) => {
1325
+ if (!d) return null;
1326
+ const up = d.close >= d.open;
1327
+ const color = up ? upColor : downColor;
1328
+ const cx = Math.round(band.center(k)) + 0.5;
1329
+ const top = y(Math.max(d.open, d.close));
1330
+ const bottom = y(Math.min(d.open, d.close));
1331
+ const h = Math.max(1, bottom - top);
1332
+ const hollow = hollowUp && up && bw >= 4;
1333
+ return /* @__PURE__ */ jsxs8("g", { opacity: i !== null && i !== k ? 0.55 : 1, children: [
1334
+ /* @__PURE__ */ jsx10("line", { x1: cx, x2: cx, y1: y(d.high), y2: y(d.low), strokeWidth: 1, style: { stroke: color } }),
1335
+ /* @__PURE__ */ jsx10(
1336
+ "rect",
1337
+ {
1338
+ x: cx - bw / 2,
1339
+ y: top,
1340
+ width: bw,
1341
+ height: h,
1342
+ rx: Math.min(1.5, bw / 4),
1343
+ strokeWidth: hollow ? 1 : 0,
1344
+ style: hollow ? { fill: VAR.bg, stroke: color } : { fill: color }
1345
+ }
1346
+ )
1347
+ ] }, k);
1348
+ }),
1349
+ tip
1350
+ ] });
1351
+ }
1352
+ var axisProps = (v) => v ? v === true ? {} : v : null;
1353
+ function CandlestickChart({
1354
+ data,
1355
+ showVolume,
1356
+ volumeHeight = 0.22,
1357
+ showYAxis = true,
1358
+ showXAxis,
1359
+ tooltip = true,
1360
+ upColor = "#F04452",
1361
+ downColor = "#3182F6",
1362
+ bodyWidth,
1363
+ hollowUp,
1364
+ labels,
1365
+ valueFormatter = formatNumber,
1366
+ xAxis,
1367
+ children,
1368
+ getScrubberAccessibilityLabel,
1369
+ ...chart
1370
+ }) {
1371
+ const hasVolume = showVolume ?? data.some((d) => d?.volume !== void 0);
1372
+ const maxVol = Math.max(0, ...data.map((d) => d?.volume ?? 0));
1373
+ const lows = data.flatMap((d) => d ? [d.low] : []);
1374
+ const highs = data.flatMap((d) => d ? [d.high] : []);
1375
+ const lo = lows.length ? Math.min(...lows) : 0;
1376
+ const hi = highs.length ? Math.max(...highs) : 1;
1377
+ const pad = (hi - lo || Math.abs(hi) || 1) * 0.04;
1378
+ const share = hasVolume ? Math.min(0.6, Math.max(0.05, volumeHeight)) : 0;
1379
+ const priceMin = lo - pad - (hi - lo + 2 * pad) * share / (1 - share);
1380
+ const yAx = axisProps(showYAxis);
1381
+ const xAx = axisProps(showXAxis);
1382
+ const l = labels ?? KO;
1383
+ return /* @__PURE__ */ jsxs8(
1384
+ CartesianChart,
1385
+ {
1386
+ enableScrubbing: tooltip,
1387
+ ...chart,
1388
+ xAxis: { bandPadding: 0.2, ...xAxis, scaleType: "band" },
1389
+ series: [
1390
+ { id: "price", data: data.map((d) => d ? [d.low, d.high] : null), yAxisId: "price", color: upColor },
1391
+ ...hasVolume ? [{ id: "volume", data: data.map((d) => d?.volume ?? null), yAxisId: "volume", color: "color-mix(in srgb, CanvasText 18%, transparent)" }] : []
1392
+ ],
1393
+ yAxis: [
1394
+ { id: "price", includeZero: false, domain: { min: priceMin, max: hi + pad } },
1395
+ // Volume keeps to the bottom band: its axis is stretched so the tallest bar reaches volumeHeight.
1396
+ { id: "volume", domain: { min: 0, max: maxVol > 0 ? maxVol / Math.max(0.05, volumeHeight) : 1 } }
1397
+ ],
1398
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel ?? ((i) => {
1399
+ const d = data[i];
1400
+ return d ? `${l.open} ${valueFormatter(d.open)}, ${l.high} ${valueFormatter(d.high)}, ${l.low} ${valueFormatter(d.low)}, ${l.close} ${valueFormatter(d.close)}` : "";
1401
+ }),
1402
+ children: [
1403
+ yAx && /* @__PURE__ */ jsx10(YAxis, { tickLabelFormatter: valueFormatter, ...yAx, axisId: "price" }, "y"),
1404
+ xAx && /* @__PURE__ */ jsx10(XAxis, { ...xAx }, "x"),
1405
+ hasVolume && /* @__PURE__ */ jsx10(
1406
+ BarPlot,
1407
+ {
1408
+ seriesIds: ["volume"],
1409
+ radius: 1,
1410
+ highlightScrubbed: false,
1411
+ getBarColor: (_v, i) => {
1412
+ const d = data[i];
1413
+ return d ? `color-mix(in srgb, ${d.close >= d.open ? upColor : downColor} 30%, transparent)` : void 0;
1414
+ }
1415
+ }
1416
+ ),
1417
+ /* @__PURE__ */ jsx10(
1418
+ CandlePlot,
1419
+ {
1420
+ data,
1421
+ yAxisId: "price",
1422
+ upColor,
1423
+ downColor,
1424
+ tooltip,
1425
+ labels: l,
1426
+ valueFormatter,
1427
+ ...bodyWidth !== void 0 ? { bodyWidth } : {},
1428
+ ...hollowUp !== void 0 ? { hollowUp } : {}
1429
+ }
1430
+ ),
1431
+ children
1432
+ ]
1433
+ }
1434
+ );
1435
+ }
1436
+
1437
+ // src/viz/scrubber.tsx
1438
+ import { useEffect as useEffect5 } from "react";
1439
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
891
1440
  var TIP_FONT = 12;
892
1441
  var ROW = 18;
893
1442
  var TIP_PAD = 7;
894
- var textWidth2 = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? TIP_FONT : TIP_FONT * 0.6), 0);
1443
+ var textWidth3 = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? TIP_FONT : TIP_FONT * 0.6), 0);
895
1444
  function Scrubber({
896
1445
  label,
897
1446
  seriesIds,
@@ -905,11 +1454,17 @@ function Scrubber({
905
1454
  }) {
906
1455
  const ctx = useCartesianChart();
907
1456
  const i = ctx.scrubIndex;
1457
+ const { setDimFuture } = ctx;
1458
+ const fade = dimFuture && !hideOverlay;
1459
+ useEffect5(() => {
1460
+ setDimFuture(fade);
1461
+ return () => setDimFuture(false);
1462
+ }, [fade, setDimFuture]);
908
1463
  const pick = seriesIds ? ctx.series.filter((s) => seriesIds.includes(s.id)) : ctx.series;
909
1464
  const colorOf = (s, v) => beaconColor?.(v, s.id) ?? s.color;
910
1465
  if (i === null || i >= ctx.length) {
911
1466
  if (!idlePulse || ctx.band) return null;
912
- return /* @__PURE__ */ jsx8("g", { pointerEvents: "none", children: pick.map((s) => {
1467
+ return /* @__PURE__ */ jsx11("g", { pointerEvents: "none", children: pick.map((s) => {
913
1468
  let last = s.data.length - 1;
914
1469
  while (last >= 0 && (s.data[last] === null || s.data[last] === void 0)) last--;
915
1470
  if (last < 0) return null;
@@ -917,12 +1472,12 @@ function Scrubber({
917
1472
  if (y === null) return null;
918
1473
  const x2 = ctx.x(last);
919
1474
  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" })
1475
+ return /* @__PURE__ */ jsxs9("g", { children: [
1476
+ /* @__PURE__ */ jsxs9("circle", { cx: x2, cy: y, r: 4, fillOpacity: 0.35, style: { fill: color }, children: [
1477
+ /* @__PURE__ */ jsx11("animate", { attributeName: "r", values: "4;13", dur: "1.6s", repeatCount: "indefinite" }),
1478
+ /* @__PURE__ */ jsx11("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
924
1479
  ] }),
925
- /* @__PURE__ */ jsx8("circle", { cx: x2, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } })
1480
+ /* @__PURE__ */ jsx11("circle", { cx: x2, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } })
926
1481
  ] }, s.id);
927
1482
  }) });
928
1483
  }
@@ -938,26 +1493,25 @@ function Scrubber({
938
1493
  const text2 = lo !== null && lo !== void 0 && lo !== v ? `${valueFormatter(lo, s.id)} \u2013 ${valueFormatter(v, s.id)}` : valueFormatter(v, s.id);
939
1494
  return [{ s, text: text2 }];
940
1495
  }) : [];
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;
1496
+ const tipW = Math.max(0, ...rows.map((r) => textWidth3(r.text) + (rows.length > 1 || r.s.label ? textWidth3(r.s.label ?? "") + 26 : 18))) + 16;
942
1497
  const tipH = rows.length * ROW + TIP_PAD * 2;
943
1498
  const tipRight = x + 12 + tipW <= plot.right;
944
1499
  const tipX = tipRight ? x + 12 : x - 12 - tipW;
945
1500
  const tipY = plot.top + 4;
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 }),
1501
+ return /* @__PURE__ */ jsxs9("g", { pointerEvents: "none", children: [
1502
+ /* @__PURE__ */ jsx11("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth: 1, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
1503
+ text && /* @__PURE__ */ jsx11("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 }),
950
1504
  !ctx.band && list.map((s) => {
951
1505
  const y = seriesY(ctx, s, i);
952
1506
  if (y === null) return null;
953
1507
  const v = s.data[i];
954
1508
  const color = colorOf(s, v);
955
1509
  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(
1510
+ const right = x + 12 + textWidth3(text2) <= plot.right;
1511
+ return /* @__PURE__ */ jsxs9("g", { children: [
1512
+ /* @__PURE__ */ jsx11("circle", { cx: x, cy: y, r: 7, fillOpacity: 0.18, style: { fill: color } }),
1513
+ /* @__PURE__ */ jsx11("circle", { cx: x, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } }),
1514
+ text2 && /* @__PURE__ */ jsx11(
961
1515
  "text",
962
1516
  {
963
1517
  x: right ? x + 10 : x - 10,
@@ -975,32 +1529,32 @@ function Scrubber({
975
1529
  )
976
1530
  ] }, s.id);
977
1531
  }),
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 })
1532
+ rows.length > 0 && /* @__PURE__ */ jsxs9("g", { transform: `translate(${tipX},${tipY})`, children: [
1533
+ /* @__PURE__ */ jsx11("rect", { width: tipW, height: tipH, rx: 8, style: { fill: VAR.bg, stroke: VAR.grid }, strokeWidth: 1 }),
1534
+ rows.map((r, k) => /* @__PURE__ */ jsxs9("g", { transform: `translate(10,${TIP_PAD + ROW / 2 + k * ROW})`, children: [
1535
+ /* @__PURE__ */ jsx11("circle", { cx: 3, cy: 0, r: 3, style: { fill: r.s.color } }),
1536
+ (rows.length > 1 || r.s.label) && /* @__PURE__ */ jsx11("text", { x: 12, dominantBaseline: "central", fontSize: TIP_FONT, style: { fill: VAR.text }, children: r.s.label ?? r.s.id }),
1537
+ /* @__PURE__ */ jsx11("text", { x: tipW - 20, textAnchor: "end", dominantBaseline: "central", fontSize: TIP_FONT, fontWeight: 700, style: { fill: VAR.textStrong, fontVariantNumeric: "tabular-nums" }, children: r.text })
984
1538
  ] }, r.s.id))
985
1539
  ] })
986
1540
  ] });
987
1541
  }
988
1542
 
989
1543
  // src/viz/annotations.tsx
990
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1544
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
991
1545
  var LABEL_FONT = 11;
992
1546
  var labelWidth = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? LABEL_FONT : LABEL_FONT * 0.62), 0);
993
1547
  function LineLabel(props) {
994
1548
  const { x, y, text, anchor, color, pill } = props;
995
1549
  const textColor = color === VAR.line ? VAR.text : color;
996
1550
  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 });
1551
+ return /* @__PURE__ */ jsx12("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
1552
  }
999
1553
  const w = labelWidth(text) + 14;
1000
1554
  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 })
1555
+ return /* @__PURE__ */ jsxs10("g", { children: [
1556
+ /* @__PURE__ */ jsx12("rect", { x: left, y: y - 10, width: w, height: 20, rx: 10, style: { fill: color === VAR.line ? VAR.textStrong : color } }),
1557
+ /* @__PURE__ */ jsx12("text", { x: left + w / 2, y, textAnchor: "middle", dominantBaseline: "central", fontSize: LABEL_FONT, fontWeight: 700, style: { fill: VAR.bg }, children: text })
1004
1558
  ] });
1005
1559
  }
1006
1560
  function ReferenceLine({
@@ -1028,17 +1582,17 @@ function ReferenceLine({
1028
1582
  const anchor = pos === "left" ? "start" : pos === "center" ? "middle" : "end";
1029
1583
  const off = pill ? 13 : 9;
1030
1584
  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 })
1585
+ return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
1586
+ /* @__PURE__ */ jsx12("line", { x1: plot.left, x2: plot.right, y1: y, y2: y, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
1587
+ label !== void 0 && /* @__PURE__ */ jsx12(LineLabel, { x: tx + labelDx, y: ly + labelDy, text: label, anchor, color, pill })
1034
1588
  ] });
1035
1589
  }
1036
1590
  if (dataX !== void 0) {
1037
1591
  const x = ctx.x(dataX);
1038
1592
  const pos = labelPosition ?? "top";
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(
1593
+ return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
1594
+ /* @__PURE__ */ jsx12("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
1595
+ label !== void 0 && /* @__PURE__ */ jsx12(
1042
1596
  LineLabel,
1043
1597
  {
1044
1598
  x: x + (pill ? 0 : 4) + labelDx,
@@ -1064,26 +1618,26 @@ function Point({ dataX, dataY, seriesId, yAxisId, color, radius = 4, label, labe
1064
1618
  const fill = color ?? s?.color ?? ctx.series[0]?.color ?? "currentColor";
1065
1619
  const gap = radius + 6;
1066
1620
  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"];
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" })
1621
+ return /* @__PURE__ */ jsxs10("g", { style: onClick ? { cursor: "pointer" } : void 0, onClick, pointerEvents: onClick ? "auto" : "none", children: [
1622
+ pulse && /* @__PURE__ */ jsxs10("circle", { cx: x, cy: y, r: radius, fillOpacity: 0.35, style: { fill }, children: [
1623
+ /* @__PURE__ */ jsx12("animate", { attributeName: "r", values: `${radius};${radius * 3.2}`, dur: "1.6s", repeatCount: "indefinite" }),
1624
+ /* @__PURE__ */ jsx12("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
1071
1625
  ] }),
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 })
1626
+ /* @__PURE__ */ jsx12("circle", { cx: x, cy: y, r: radius, strokeWidth: 2, style: { fill, stroke: VAR.bg } }),
1627
+ label !== void 0 && /* @__PURE__ */ jsx12("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 })
1074
1628
  ] });
1075
1629
  }
1076
1630
 
1077
1631
  // src/viz/period-selector.tsx
1078
- import { useLayoutEffect, useRef as useRef4, useState as useState4 } from "react";
1079
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1632
+ import { useLayoutEffect, useRef as useRef5, useState as useState6 } from "react";
1633
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1080
1634
  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" })
1635
+ return /* @__PURE__ */ jsxs11("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
1636
+ /* @__PURE__ */ jsxs11("span", { "aria-hidden": "true", style: { position: "relative", width: 7, height: 7 }, children: [
1637
+ /* @__PURE__ */ jsx13("span", { style: { position: "absolute", inset: 0, borderRadius: "50%", background: color } }),
1638
+ /* @__PURE__ */ jsx13("svg", { width: "7", height: "7", viewBox: "0 0 7 7", style: { position: "absolute", inset: 0, overflow: "visible" }, children: /* @__PURE__ */ jsxs11("circle", { cx: "3.5", cy: "3.5", r: "3.5", style: { fill: color }, fillOpacity: 0.45, children: [
1639
+ /* @__PURE__ */ jsx13("animate", { attributeName: "r", values: "3.5;8", dur: "1.6s", repeatCount: "indefinite" }),
1640
+ /* @__PURE__ */ jsx13("animate", { attributeName: "fill-opacity", values: "0.45;0", dur: "1.6s", repeatCount: "indefinite" })
1087
1641
  ] }) })
1088
1642
  ] }),
1089
1643
  label
@@ -1110,8 +1664,8 @@ function PeriodSelector({
1110
1664
  }) {
1111
1665
  const activeIsLive = periods.find((p) => p.id === value)?.live ?? false;
1112
1666
  const pillColor = activeIsLive ? liveColor : activeColor;
1113
- const listRef = useRef4(null);
1114
- const [pill, setPill] = useState4(null);
1667
+ const listRef = useRef5(null);
1668
+ const [pill, setPill] = useState6(null);
1115
1669
  useLayoutEffect(() => {
1116
1670
  const list = listRef.current;
1117
1671
  const active = list?.querySelector('[aria-selected="true"]');
@@ -1133,7 +1687,7 @@ function PeriodSelector({
1133
1687
  onChange(p.id);
1134
1688
  listRef.current?.querySelector(`[data-period="${CSS.escape(p.id)}"]`)?.focus();
1135
1689
  };
1136
- return /* @__PURE__ */ jsxs8(
1690
+ return /* @__PURE__ */ jsxs11(
1137
1691
  "div",
1138
1692
  {
1139
1693
  ref: listRef,
@@ -1143,7 +1697,7 @@ function PeriodSelector({
1143
1697
  onKeyDown,
1144
1698
  style: { position: "relative", display: fullWidth ? "flex" : "inline-flex", gap: 2, fontFamily: VAR.font, ...style },
1145
1699
  children: [
1146
- pill && /* @__PURE__ */ jsx10(
1700
+ pill && /* @__PURE__ */ jsx13(
1147
1701
  "span",
1148
1702
  {
1149
1703
  "aria-hidden": "true",
@@ -1162,7 +1716,7 @@ function PeriodSelector({
1162
1716
  ),
1163
1717
  periods.map((p) => {
1164
1718
  const active = p.id === value;
1165
- return /* @__PURE__ */ jsx10(
1719
+ return /* @__PURE__ */ jsx13(
1166
1720
  "button",
1167
1721
  {
1168
1722
  type: "button",
@@ -1186,7 +1740,7 @@ function PeriodSelector({
1186
1740
  fontWeight: active ? 700 : 500,
1187
1741
  cursor: "pointer"
1188
1742
  },
1189
- children: p.live ? /* @__PURE__ */ jsx10(LiveTabLabel, { label: p.label, color: liveColor }) : p.label
1743
+ children: p.live ? /* @__PURE__ */ jsx13(LiveTabLabel, { label: p.label, color: liveColor }) : p.label
1190
1744
  },
1191
1745
  p.id
1192
1746
  );
@@ -1197,13 +1751,13 @@ function PeriodSelector({
1197
1751
  }
1198
1752
 
1199
1753
  // src/viz/charts.tsx
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";
1203
- var axisProps = (v) => v ? v === true ? {} : v : null;
1754
+ import { percentages, seriesColor as seriesColor5 } from "@funnest/charts-viz";
1755
+ import { useState as useState7 } from "react";
1756
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1757
+ var axisProps2 = (v) => v ? v === true ? {} : v : null;
1204
1758
  var axisElements = (x, y) => [
1205
- y && /* @__PURE__ */ jsx11(YAxis, { ...y }, "y"),
1206
- x && /* @__PURE__ */ jsx11(XAxis, { ...x }, "x")
1759
+ y && /* @__PURE__ */ jsx14(YAxis, { ...y }, "y"),
1760
+ x && /* @__PURE__ */ jsx14(XAxis, { ...x }, "x")
1207
1761
  ];
1208
1762
  var defined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
1209
1763
  var KR_SPLIT = { above: "#F04452", below: "#3182F6" };
@@ -1222,11 +1776,11 @@ function LineChart({
1222
1776
  children,
1223
1777
  ...chart
1224
1778
  }) {
1225
- const sc = axisProps(scrubber);
1779
+ const sc = axisProps2(scrubber);
1226
1780
  const split = baseline !== void 0 ? splitColors ?? KR_SPLIT : void 0;
1227
- return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1228
- axisElements(axisProps(showXAxis), axisProps(showYAxis)),
1229
- chart.series.map((s) => /* @__PURE__ */ jsx11(
1781
+ return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1782
+ axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
1783
+ chart.series.map((s) => /* @__PURE__ */ jsx14(
1230
1784
  Line,
1231
1785
  {
1232
1786
  seriesId: s.id,
@@ -1236,7 +1790,7 @@ function LineChart({
1236
1790
  s.id
1237
1791
  )),
1238
1792
  children,
1239
- sc && /* @__PURE__ */ jsx11(
1793
+ sc && /* @__PURE__ */ jsx14(
1240
1794
  Scrubber,
1241
1795
  {
1242
1796
  ...split && baseline !== void 0 ? { beaconColor: (v) => v >= baseline ? split.above : split.below } : {},
@@ -1261,32 +1815,32 @@ function AreaChart({
1261
1815
  children,
1262
1816
  ...chart
1263
1817
  }) {
1264
- const sc = axisProps(scrubber);
1265
- return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1266
- axisElements(axisProps(showXAxis), axisProps(showYAxis)),
1818
+ const sc = axisProps2(scrubber);
1819
+ return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1820
+ axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
1267
1821
  chart.series.map((s) => {
1268
1822
  const own = s.areaStyle === "none" ? null : s.areaStyle ?? (chart.stacked && fill === "gradient" ? "solid" : fill);
1269
- return own ? /* @__PURE__ */ jsx11(
1823
+ return own ? /* @__PURE__ */ jsx14(
1270
1824
  Area,
1271
1825
  {
1272
1826
  seriesId: s.id,
1273
1827
  curve,
1274
1828
  fill: own,
1275
- ...defined({ baseline, splitColors, gradientStops, opacity: fillOpacity ?? (chart.stacked && own === "solid" ? 0.55 : void 0) })
1829
+ ...defined({ baseline, splitColors, gradientStops, opacity: fillOpacity ?? (chart.stacked && own === "solid" ? 0.3 : void 0) })
1276
1830
  },
1277
1831
  `a${s.id}`
1278
1832
  ) : null;
1279
1833
  }),
1280
- showLines && chart.series.map((s) => /* @__PURE__ */ jsx11(Line, { seriesId: s.id, curve, strokeWidth, ...defined({ lineStyle, baseline, splitColors }) }, `l${s.id}`)),
1834
+ showLines && chart.series.map((s) => /* @__PURE__ */ jsx14(Line, { seriesId: s.id, curve, strokeWidth, ...defined({ lineStyle, baseline, splitColors }) }, `l${s.id}`)),
1281
1835
  children,
1282
- sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
1836
+ sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
1283
1837
  ] });
1284
1838
  }
1285
1839
  function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGap, stackGap, roundBaseline, barMinSize, layout = "vertical", xAxis, children, ...chart }) {
1286
- const sc = axisProps(scrubber);
1840
+ const sc = axisProps2(scrubber);
1287
1841
  if (layout === "horizontal") {
1288
- const yAx = axisProps(showYAxis);
1289
- return /* @__PURE__ */ jsx11(
1842
+ const yAx = axisProps2(showYAxis);
1843
+ return /* @__PURE__ */ jsx14(
1290
1844
  HorizontalBarChart,
1291
1845
  {
1292
1846
  series: chart.series,
@@ -1313,11 +1867,11 @@ function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGa
1313
1867
  }
1314
1868
  );
1315
1869
  }
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 }),
1870
+ return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, xAxis: { ...xAxis, scaleType: "band" }, ...chart, children: [
1871
+ axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
1872
+ /* @__PURE__ */ jsx14(BarPlot, { ...defined({ radius, getBarColor, groupGap, stackGap, roundBaseline, minSize: barMinSize }), highlightScrubbed: !!sc }),
1319
1873
  children,
1320
- sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
1874
+ sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
1321
1875
  ] });
1322
1876
  }
1323
1877
  var pct = (_item, f) => `${formatNumber(Math.round(f * 1e3) / 10)}%`;
@@ -1350,11 +1904,11 @@ function PercentageBarChart({
1350
1904
  for (const r of allRows)
1351
1905
  for (const d of r.data)
1352
1906
  if (!colorOf.has(d.id)) {
1353
- colorOf.set(d.id, d.color ?? seriesColor3(colorOf.size, palette));
1907
+ colorOf.set(d.id, d.color ?? seriesColor5(colorOf.size, palette));
1354
1908
  legendItems.push(d);
1355
1909
  }
1356
- const [hovered, setHovered] = useState5(null);
1357
- const [hoverRow, setHoverRow] = useState5(null);
1910
+ const [hovered, setHovered] = useState7(null);
1911
+ const [hoverRow, setHoverRow] = useState7(null);
1358
1912
  const rowSegs = allRows.map((r) => {
1359
1913
  const segs = percentages(r.data);
1360
1914
  return { row: r, items: r.data.map((d, i) => ({ ...d, color: colorOf.get(d.id), fraction: segs[i].fraction })) };
@@ -1362,7 +1916,7 @@ function PercentageBarChart({
1362
1916
  const renderBar = (items, rowId) => {
1363
1917
  const visible = items.filter((i) => i.fraction > 0);
1364
1918
  const ordered = vertical ? [...visible].reverse() : visible;
1365
- return /* @__PURE__ */ jsx11(
1919
+ return /* @__PURE__ */ jsx14(
1366
1920
  "div",
1367
1921
  {
1368
1922
  style: {
@@ -1374,7 +1928,7 @@ function PercentageBarChart({
1374
1928
  children: ordered.map((i, k) => {
1375
1929
  const a = k === 0 ? end : inner;
1376
1930
  const b = k === ordered.length - 1 ? end : inner;
1377
- return /* @__PURE__ */ jsx11(
1931
+ return /* @__PURE__ */ jsx14(
1378
1932
  "span",
1379
1933
  {
1380
1934
  title: `${i.label} ${formatLabel(i, i.fraction)}`,
@@ -1403,7 +1957,7 @@ function PercentageBarChart({
1403
1957
  );
1404
1958
  };
1405
1959
  const legendRow = multi ? rowSegs.find((r) => r.row.id === hoverRow) : rowSegs[0];
1406
- const legend = showLabels && /* @__PURE__ */ jsx11(
1960
+ const legend = showLabels && /* @__PURE__ */ jsx14(
1407
1961
  Legend,
1408
1962
  {
1409
1963
  style: legendAt === "top" ? { marginBottom: 10 } : legendAt === "bottom" ? { marginTop: 10 } : {},
@@ -1422,15 +1976,15 @@ function PercentageBarChart({
1422
1976
  onItemHover: setHovered
1423
1977
  }
1424
1978
  );
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: [
1979
+ const label = (r) => multi && r.label ? /* @__PURE__ */ jsx14("span", { style: { fontSize: 12, color: hoverRow === r.id ? VAR.textStrong : VAR.text, fontWeight: hoverRow === r.id ? 700 : 500, whiteSpace: "nowrap" }, children: r.label }) : null;
1980
+ const bars = vertical ? /* @__PURE__ */ jsx14("div", { style: { display: "flex", gap: 14, alignItems: "flex-end" }, children: rowSegs.map(({ row, items }) => /* @__PURE__ */ jsxs12("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }, children: [
1427
1981
  renderBar(items, row.id),
1428
1982
  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", {}),
1983
+ ] }, row.id)) }) : multi ? /* @__PURE__ */ jsx14("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__ */ jsxs12("div", { style: { display: "contents" }, children: [
1984
+ label(row) ?? /* @__PURE__ */ jsx14("span", {}),
1431
1985
  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(
1986
+ ] }, row.id)) }) : /* @__PURE__ */ jsx14("div", { style: { display: "flex", flex: legendAt === "right" ? 1 : void 0 }, children: renderBar(rowSegs[0].items, rowSegs[0].row.id) });
1987
+ return /* @__PURE__ */ jsxs12(
1434
1988
  "div",
1435
1989
  {
1436
1990
  className,
@@ -1450,8 +2004,11 @@ export {
1450
2004
  AreaChart,
1451
2005
  BarChart,
1452
2006
  BarPlot,
2007
+ CandlePlot,
2008
+ CandlestickChart,
1453
2009
  CartesianChart,
1454
2010
  DEFAULT_PERIODS,
2011
+ DonutChart,
1455
2012
  FnChart,
1456
2013
  FnChartSlot,
1457
2014
  HorizontalBarChart,
@@ -1464,6 +2021,7 @@ export {
1464
2021
  Point,
1465
2022
  ReferenceLine,
1466
2023
  Scrubber,
2024
+ TreemapChart,
1467
2025
  XAxis,
1468
2026
  YAxis,
1469
2027
  formatNumber,
@@ -0,0 +1,57 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type XAxisProps, type YAxisProps } from './axes';
3
+ import { type CartesianChartProps } from './cartesian-chart';
4
+ export interface Candle {
5
+ open: number;
6
+ high: number;
7
+ low: number;
8
+ close: number;
9
+ volume?: number;
10
+ }
11
+ export interface CandlePlotProps {
12
+ data: readonly (Candle | null)[];
13
+ /** Axis the prices are on (when the chart has several). */
14
+ yAxisId?: string;
15
+ /** Default '#F04452' (Korean red-up). */
16
+ upColor?: string;
17
+ /** Default '#3182F6'. */
18
+ downColor?: string;
19
+ /** Body width as a share of the slot. Default 0.7. */
20
+ bodyWidth?: number;
21
+ /** Hollow bodies for up candles. Default false. */
22
+ hollowUp?: boolean;
23
+ /** Pointer line + OHLC tooltip while scrubbing. Default true. */
24
+ tooltip?: boolean;
25
+ /** Tooltip row names. Default 시가/고가/저가/종가 (Korean) — pass English if needed. */
26
+ labels?: {
27
+ open: string;
28
+ high: string;
29
+ low: string;
30
+ close: string;
31
+ volume?: string;
32
+ };
33
+ valueFormatter?: (value: number) => string;
34
+ }
35
+ /** Candles (wick + body) for each x slot. Needs a band x axis and a y axis that covers low–high. */
36
+ export declare function CandlePlot({ data, yAxisId, upColor, downColor, bodyWidth, hollowUp, tooltip, labels, valueFormatter, }: CandlePlotProps): import("react").JSX.Element | null;
37
+ type AxisToggle<P> = boolean | P;
38
+ export interface CandlestickChartProps extends Omit<CartesianChartProps, 'series' | 'yAxis' | 'stacked' | 'children' | 'legend'>, Pick<CandlePlotProps, 'upColor' | 'downColor' | 'bodyWidth' | 'hollowUp' | 'labels' | 'valueFormatter'> {
39
+ data: readonly (Candle | null)[];
40
+ /** Volume bars along the bottom (from `data[i].volume`). Default true when volumes exist. */
41
+ showVolume?: boolean;
42
+ /** Share of the height the volume bars may use. Default 0.22. */
43
+ volumeHeight?: number;
44
+ /** Default true (right side). */
45
+ showYAxis?: AxisToggle<YAxisProps>;
46
+ showXAxis?: AxisToggle<XAxisProps>;
47
+ /** Pointer line + OHLC tooltip. Default true. */
48
+ tooltip?: boolean;
49
+ /** ReferenceLine (use `yAxisId="price"`), Point, … */
50
+ children?: ReactNode;
51
+ }
52
+ /**
53
+ * A small candlestick chart in SVG for cards and dashboards (the full trading chart is `FnChart`).
54
+ * Prices sit on axis "price", volume on axis "volume".
55
+ */
56
+ export declare function CandlestickChart({ data, showVolume, volumeHeight, showYAxis, showXAxis, tooltip, upColor, downColor, bodyWidth, hollowUp, labels, valueFormatter, xAxis, children, getScrubberAccessibilityLabel, ...chart }: CandlestickChartProps): import("react").JSX.Element;
57
+ export {};
@@ -65,7 +65,16 @@ export interface CartesianContextValue {
65
65
  /** The chart has keyboard focus (scrubbing with arrow keys). */
66
66
  focused: boolean;
67
67
  animate: boolean;
68
+ /**
69
+ * `mask` value for series marks while a Scrubber dims the future (right of the pointer);
70
+ * undefined otherwise. Lines and areas apply it, so the fade works on any background.
71
+ */
72
+ futureMask: string | undefined;
73
+ /** Scrubber turns future dimming on/off. */
74
+ setDimFuture(on: boolean): void;
68
75
  }
76
+ /** Hide labels the scrubber line would run through. */
77
+ export declare const nearScrub: (ctx: CartesianContextValue, x: number, within?: number) => boolean;
69
78
  export declare const CartesianContext: import("react").Context<CartesianContextValue | null>;
70
79
  export declare function useCartesianChart(): CartesianContextValue;
71
80
  /**
@@ -0,0 +1,55 @@
1
+ import { type CSSProperties, type ReactNode } from 'react';
2
+ import { type LegendShape } from './legend';
3
+ export interface DonutItem {
4
+ id: string;
5
+ label: string;
6
+ value: number;
7
+ color?: string;
8
+ }
9
+ export interface DonutCenter {
10
+ /** Big text in the middle. */
11
+ value: string;
12
+ /** Small text under it. */
13
+ label?: string;
14
+ }
15
+ export interface DonutChartProps {
16
+ data: readonly DonutItem[];
17
+ /** Diameter, px. Default 180. */
18
+ size?: number;
19
+ /** Hole size as a share of the radius, 0–0.95. 0 draws a pie. Default 0.68. */
20
+ innerRadius?: number;
21
+ /** Gap between segments, degrees. Default 1.5. */
22
+ padAngle?: number;
23
+ /** Rounded segment corners, px. Default 3. */
24
+ cornerRadius?: number;
25
+ /**
26
+ * Text in the hole. Default: the hovered item's share and label, otherwise the total.
27
+ * Return null to leave the hole empty.
28
+ */
29
+ center?: (active: {
30
+ item: DonutItem;
31
+ fraction: number;
32
+ } | null, total: number) => DonutCenter | ReactNode | null;
33
+ /** Label under the total when nothing is hovered (e.g. "총 자산"). */
34
+ totalLabel?: string;
35
+ valueFormatter?: (value: number) => string;
36
+ /** Default true. */
37
+ showLegend?: boolean;
38
+ /** Default 'right'. */
39
+ legendPosition?: 'right' | 'bottom';
40
+ legendShape?: LegendShape;
41
+ /** Legend values. Default: the share ("42.0%"). */
42
+ formatLabel?: (item: DonutItem, fraction: number) => string;
43
+ /** Controlled highlight (id) — e.g. synced with a table row. */
44
+ activeId?: string | null;
45
+ onActiveChange?: (id: string | null) => void;
46
+ palette?: readonly string[];
47
+ className?: string;
48
+ style?: CSSProperties;
49
+ 'aria-label'?: string;
50
+ }
51
+ /**
52
+ * Ring (or pie) of shares: portfolio by asset, sector weights, account mix.
53
+ * Hover or arrow keys highlight a segment; the hole shows its share.
54
+ */
55
+ export declare function DonutChart({ data, size, innerRadius, padAngle, cornerRadius, center, totalLabel, valueFormatter, showLegend, legendPosition, legendShape, formatLabel, activeId, onActiveChange, palette, className, style, 'aria-label': ariaLabel, }: DonutChartProps): import("react").JSX.Element;
@@ -3,6 +3,9 @@ export { XAxis, YAxis, type XAxisProps, type YAxisProps } from './axes';
3
3
  export { Line, Area, type LineProps, type AreaProps, type AreaFill, type LinePoint, type SplitColors, type GradientStop } from './line';
4
4
  export { BarPlot, type BarPlotProps } from './bar';
5
5
  export { HorizontalBarChart, type HorizontalBarChartProps } from './horizontal-bar';
6
+ export { DonutChart, type DonutChartProps, type DonutItem, type DonutCenter } from './donut';
7
+ export { TreemapChart, type TreemapChartProps, type TreemapItem } from './treemap';
8
+ export { CandlestickChart, CandlePlot, type CandlestickChartProps, type CandlePlotProps, type Candle } from './candles';
6
9
  export { Scrubber, type ScrubberProps } from './scrubber';
7
10
  export { ReferenceLine, Point, type ReferenceLineProps, type PointProps } from './annotations';
8
11
  export { Legend, type LegendProps, type LegendItem, type LegendPosition, type LegendShape } from './legend';
@@ -0,0 +1,48 @@
1
+ import { type CSSProperties } from 'react';
2
+ export interface TreemapItem {
3
+ id: string;
4
+ label: string;
5
+ /** Tile size (market cap, holding value). */
6
+ value: number;
7
+ /** Colors the tile: up / down intensity (e.g. today's change, %). */
8
+ change?: number;
9
+ /** Fixed tile color (wins over `change`). */
10
+ color?: string;
11
+ /** Second line under the label (e.g. price). */
12
+ sublabel?: string;
13
+ }
14
+ export interface TreemapChartProps {
15
+ data: readonly TreemapItem[];
16
+ width?: number;
17
+ /** Default 280. */
18
+ height?: number;
19
+ /** Gap between tiles, px. Default 2. */
20
+ gap?: number;
21
+ /** Tile corner radius. Default 4. */
22
+ radius?: number;
23
+ /** Color for rising tiles. Default '#F04452' (Korean red-up). */
24
+ upColor?: string;
25
+ /** Color for falling tiles. Default '#3182F6'. */
26
+ downColor?: string;
27
+ /** Color for flat (or near-zero) tiles. Default '#8B95A1'. */
28
+ flatColor?: string;
29
+ /** |change| at or above this gets the full up/down color. Default 5 (%). */
30
+ maxChange?: number;
31
+ /** Custom tile color; return undefined to fall back. */
32
+ getColor?: (item: TreemapItem, index: number) => string | undefined;
33
+ /** Change text on tiles and in the tooltip. Default "+2.31%". */
34
+ changeFormatter?: (change: number) => string;
35
+ valueFormatter?: (value: number) => string;
36
+ /** Hover / arrow keys show a tooltip with the value. Default true. */
37
+ enableTooltip?: boolean;
38
+ onItemClick?: (item: TreemapItem) => void;
39
+ palette?: readonly string[];
40
+ className?: string;
41
+ style?: CSSProperties;
42
+ 'aria-label'?: string;
43
+ }
44
+ /**
45
+ * Tiles sized by value and colored by change: market maps, sector heatmaps, holdings by weight.
46
+ * Labels shrink with the tile and hide when they don't fit.
47
+ */
48
+ export declare function TreemapChart({ data, width: fixedWidth, height, gap, radius, upColor, downColor, flatColor, maxChange, getColor, changeFormatter, valueFormatter, enableTooltip, onItemClick, palette, className, style, 'aria-label': ariaLabel, }: TreemapChartProps): import("react").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts-react",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "React bindings for Funnest Charts: the trading widget plus SVG line, area and bar charts",
5
5
  "keywords": [
6
6
  "react",
@@ -46,9 +46,9 @@
46
46
  "NOTICE"
47
47
  ],
48
48
  "dependencies": {
49
- "@funnest/charts": "0.2.0",
50
- "@funnest/charts-ui": "0.2.0",
51
- "@funnest/charts-viz": "0.2.0"
49
+ "@funnest/charts": "0.3.0",
50
+ "@funnest/charts-ui": "0.3.0",
51
+ "@funnest/charts-viz": "0.3.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "react": ">=18",