@funnest/charts-react 0.2.1 → 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
@@ -780,6 +780,7 @@ function HorizontalBarChart({
780
780
  const wrapRef = useRef3(null);
781
781
  const [measured, setMeasured] = useState3(fixedWidth ?? 0);
782
782
  const [hover, setHover] = useState3(null);
783
+ const [focused, setFocused] = useState3(false);
783
784
  useEffect3(() => {
784
785
  if (fixedWidth !== void 0) return setMeasured(fixedWidth);
785
786
  const el = wrapRef.current;
@@ -820,6 +821,19 @@ function HorizontalBarChart({
820
821
  const r = e.currentTarget.getBoundingClientRect();
821
822
  setHover(band.invert(e.clientY - r.top));
822
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
+ };
823
837
  const tipRows = hover === null ? [] : resolved.flatMap((s) => {
824
838
  const v = s.high[hover];
825
839
  if (v === null || v === void 0) return [];
@@ -845,9 +859,16 @@ function HorizontalBarChart({
845
859
  height: h,
846
860
  role: "img",
847
861
  "aria-label": ariaLabel,
848
- 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 },
849
864
  onPointerMove: enableScrubbing ? onMove : void 0,
850
- 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,
851
872
  children: w > 0 && /* @__PURE__ */ jsxs5(Fragment3, { children: [
852
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}`)),
853
874
  min < 0 && /* @__PURE__ */ jsx7("line", { x1: zero, x2: zero, y1: top, y2: bottom, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
@@ -892,17 +913,534 @@ function HorizontalBarChart({
892
913
  ] })
893
914
  ] })
894
915
  }
895
- )
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(", ") : "" })
896
918
  ] });
897
919
  }
898
920
 
899
- // src/viz/scrubber.tsx
900
- import { useEffect as useEffect4 } from "react";
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";
901
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";
902
1440
  var TIP_FONT = 12;
903
1441
  var ROW = 18;
904
1442
  var TIP_PAD = 7;
905
- 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);
906
1444
  function Scrubber({
907
1445
  label,
908
1446
  seriesIds,
@@ -918,7 +1456,7 @@ function Scrubber({
918
1456
  const i = ctx.scrubIndex;
919
1457
  const { setDimFuture } = ctx;
920
1458
  const fade = dimFuture && !hideOverlay;
921
- useEffect4(() => {
1459
+ useEffect5(() => {
922
1460
  setDimFuture(fade);
923
1461
  return () => setDimFuture(false);
924
1462
  }, [fade, setDimFuture]);
@@ -926,7 +1464,7 @@ function Scrubber({
926
1464
  const colorOf = (s, v) => beaconColor?.(v, s.id) ?? s.color;
927
1465
  if (i === null || i >= ctx.length) {
928
1466
  if (!idlePulse || ctx.band) return null;
929
- return /* @__PURE__ */ jsx8("g", { pointerEvents: "none", children: pick.map((s) => {
1467
+ return /* @__PURE__ */ jsx11("g", { pointerEvents: "none", children: pick.map((s) => {
930
1468
  let last = s.data.length - 1;
931
1469
  while (last >= 0 && (s.data[last] === null || s.data[last] === void 0)) last--;
932
1470
  if (last < 0) return null;
@@ -934,12 +1472,12 @@ function Scrubber({
934
1472
  if (y === null) return null;
935
1473
  const x2 = ctx.x(last);
936
1474
  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" })
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" })
941
1479
  ] }),
942
- /* @__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 } })
943
1481
  ] }, s.id);
944
1482
  }) });
945
1483
  }
@@ -955,25 +1493,25 @@ function Scrubber({
955
1493
  const text2 = lo !== null && lo !== void 0 && lo !== v ? `${valueFormatter(lo, s.id)} \u2013 ${valueFormatter(v, s.id)}` : valueFormatter(v, s.id);
956
1494
  return [{ s, text: text2 }];
957
1495
  }) : [];
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;
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;
959
1497
  const tipH = rows.length * ROW + TIP_PAD * 2;
960
1498
  const tipRight = x + 12 + tipW <= plot.right;
961
1499
  const tipX = tipRight ? x + 12 : x - 12 - tipW;
962
1500
  const tipY = plot.top + 4;
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 }),
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 }),
966
1504
  !ctx.band && list.map((s) => {
967
1505
  const y = seriesY(ctx, s, i);
968
1506
  if (y === null) return null;
969
1507
  const v = s.data[i];
970
1508
  const color = colorOf(s, v);
971
1509
  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(
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(
977
1515
  "text",
978
1516
  {
979
1517
  x: right ? x + 10 : x - 10,
@@ -991,32 +1529,32 @@ function Scrubber({
991
1529
  )
992
1530
  ] }, s.id);
993
1531
  }),
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 })
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 })
1000
1538
  ] }, r.s.id))
1001
1539
  ] })
1002
1540
  ] });
1003
1541
  }
1004
1542
 
1005
1543
  // src/viz/annotations.tsx
1006
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1544
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1007
1545
  var LABEL_FONT = 11;
1008
1546
  var labelWidth = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? LABEL_FONT : LABEL_FONT * 0.62), 0);
1009
1547
  function LineLabel(props) {
1010
1548
  const { x, y, text, anchor, color, pill } = props;
1011
1549
  const textColor = color === VAR.line ? VAR.text : color;
1012
1550
  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 });
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 });
1014
1552
  }
1015
1553
  const w = labelWidth(text) + 14;
1016
1554
  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 })
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 })
1020
1558
  ] });
1021
1559
  }
1022
1560
  function ReferenceLine({
@@ -1044,17 +1582,17 @@ function ReferenceLine({
1044
1582
  const anchor = pos === "left" ? "start" : pos === "center" ? "middle" : "end";
1045
1583
  const off = pill ? 13 : 9;
1046
1584
  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 })
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 })
1050
1588
  ] });
1051
1589
  }
1052
1590
  if (dataX !== void 0) {
1053
1591
  const x = ctx.x(dataX);
1054
1592
  const pos = labelPosition ?? "top";
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(
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(
1058
1596
  LineLabel,
1059
1597
  {
1060
1598
  x: x + (pill ? 0 : 4) + labelDx,
@@ -1080,26 +1618,26 @@ function Point({ dataX, dataY, seriesId, yAxisId, color, radius = 4, label, labe
1080
1618
  const fill = color ?? s?.color ?? ctx.series[0]?.color ?? "currentColor";
1081
1619
  const gap = radius + 6;
1082
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"];
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" })
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" })
1087
1625
  ] }),
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 })
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 })
1090
1628
  ] });
1091
1629
  }
1092
1630
 
1093
1631
  // src/viz/period-selector.tsx
1094
- import { useLayoutEffect, useRef as useRef4, useState as useState4 } from "react";
1095
- 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";
1096
1634
  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" })
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" })
1103
1641
  ] }) })
1104
1642
  ] }),
1105
1643
  label
@@ -1126,8 +1664,8 @@ function PeriodSelector({
1126
1664
  }) {
1127
1665
  const activeIsLive = periods.find((p) => p.id === value)?.live ?? false;
1128
1666
  const pillColor = activeIsLive ? liveColor : activeColor;
1129
- const listRef = useRef4(null);
1130
- const [pill, setPill] = useState4(null);
1667
+ const listRef = useRef5(null);
1668
+ const [pill, setPill] = useState6(null);
1131
1669
  useLayoutEffect(() => {
1132
1670
  const list = listRef.current;
1133
1671
  const active = list?.querySelector('[aria-selected="true"]');
@@ -1149,7 +1687,7 @@ function PeriodSelector({
1149
1687
  onChange(p.id);
1150
1688
  listRef.current?.querySelector(`[data-period="${CSS.escape(p.id)}"]`)?.focus();
1151
1689
  };
1152
- return /* @__PURE__ */ jsxs8(
1690
+ return /* @__PURE__ */ jsxs11(
1153
1691
  "div",
1154
1692
  {
1155
1693
  ref: listRef,
@@ -1159,7 +1697,7 @@ function PeriodSelector({
1159
1697
  onKeyDown,
1160
1698
  style: { position: "relative", display: fullWidth ? "flex" : "inline-flex", gap: 2, fontFamily: VAR.font, ...style },
1161
1699
  children: [
1162
- pill && /* @__PURE__ */ jsx10(
1700
+ pill && /* @__PURE__ */ jsx13(
1163
1701
  "span",
1164
1702
  {
1165
1703
  "aria-hidden": "true",
@@ -1178,7 +1716,7 @@ function PeriodSelector({
1178
1716
  ),
1179
1717
  periods.map((p) => {
1180
1718
  const active = p.id === value;
1181
- return /* @__PURE__ */ jsx10(
1719
+ return /* @__PURE__ */ jsx13(
1182
1720
  "button",
1183
1721
  {
1184
1722
  type: "button",
@@ -1202,7 +1740,7 @@ function PeriodSelector({
1202
1740
  fontWeight: active ? 700 : 500,
1203
1741
  cursor: "pointer"
1204
1742
  },
1205
- 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
1206
1744
  },
1207
1745
  p.id
1208
1746
  );
@@ -1213,13 +1751,13 @@ function PeriodSelector({
1213
1751
  }
1214
1752
 
1215
1753
  // src/viz/charts.tsx
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";
1219
- 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;
1220
1758
  var axisElements = (x, y) => [
1221
- y && /* @__PURE__ */ jsx11(YAxis, { ...y }, "y"),
1222
- x && /* @__PURE__ */ jsx11(XAxis, { ...x }, "x")
1759
+ y && /* @__PURE__ */ jsx14(YAxis, { ...y }, "y"),
1760
+ x && /* @__PURE__ */ jsx14(XAxis, { ...x }, "x")
1223
1761
  ];
1224
1762
  var defined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
1225
1763
  var KR_SPLIT = { above: "#F04452", below: "#3182F6" };
@@ -1238,11 +1776,11 @@ function LineChart({
1238
1776
  children,
1239
1777
  ...chart
1240
1778
  }) {
1241
- const sc = axisProps(scrubber);
1779
+ const sc = axisProps2(scrubber);
1242
1780
  const split = baseline !== void 0 ? splitColors ?? KR_SPLIT : void 0;
1243
- return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1244
- axisElements(axisProps(showXAxis), axisProps(showYAxis)),
1245
- 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(
1246
1784
  Line,
1247
1785
  {
1248
1786
  seriesId: s.id,
@@ -1252,7 +1790,7 @@ function LineChart({
1252
1790
  s.id
1253
1791
  )),
1254
1792
  children,
1255
- sc && /* @__PURE__ */ jsx11(
1793
+ sc && /* @__PURE__ */ jsx14(
1256
1794
  Scrubber,
1257
1795
  {
1258
1796
  ...split && baseline !== void 0 ? { beaconColor: (v) => v >= baseline ? split.above : split.below } : {},
@@ -1277,12 +1815,12 @@ function AreaChart({
1277
1815
  children,
1278
1816
  ...chart
1279
1817
  }) {
1280
- const sc = axisProps(scrubber);
1281
- return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1282
- 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)),
1283
1821
  chart.series.map((s) => {
1284
1822
  const own = s.areaStyle === "none" ? null : s.areaStyle ?? (chart.stacked && fill === "gradient" ? "solid" : fill);
1285
- return own ? /* @__PURE__ */ jsx11(
1823
+ return own ? /* @__PURE__ */ jsx14(
1286
1824
  Area,
1287
1825
  {
1288
1826
  seriesId: s.id,
@@ -1293,16 +1831,16 @@ function AreaChart({
1293
1831
  `a${s.id}`
1294
1832
  ) : null;
1295
1833
  }),
1296
- 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}`)),
1297
1835
  children,
1298
- sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
1836
+ sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
1299
1837
  ] });
1300
1838
  }
1301
1839
  function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGap, stackGap, roundBaseline, barMinSize, layout = "vertical", xAxis, children, ...chart }) {
1302
- const sc = axisProps(scrubber);
1840
+ const sc = axisProps2(scrubber);
1303
1841
  if (layout === "horizontal") {
1304
- const yAx = axisProps(showYAxis);
1305
- return /* @__PURE__ */ jsx11(
1842
+ const yAx = axisProps2(showYAxis);
1843
+ return /* @__PURE__ */ jsx14(
1306
1844
  HorizontalBarChart,
1307
1845
  {
1308
1846
  series: chart.series,
@@ -1329,11 +1867,11 @@ function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGa
1329
1867
  }
1330
1868
  );
1331
1869
  }
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 }),
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 }),
1335
1873
  children,
1336
- sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
1874
+ sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
1337
1875
  ] });
1338
1876
  }
1339
1877
  var pct = (_item, f) => `${formatNumber(Math.round(f * 1e3) / 10)}%`;
@@ -1366,11 +1904,11 @@ function PercentageBarChart({
1366
1904
  for (const r of allRows)
1367
1905
  for (const d of r.data)
1368
1906
  if (!colorOf.has(d.id)) {
1369
- colorOf.set(d.id, d.color ?? seriesColor3(colorOf.size, palette));
1907
+ colorOf.set(d.id, d.color ?? seriesColor5(colorOf.size, palette));
1370
1908
  legendItems.push(d);
1371
1909
  }
1372
- const [hovered, setHovered] = useState5(null);
1373
- const [hoverRow, setHoverRow] = useState5(null);
1910
+ const [hovered, setHovered] = useState7(null);
1911
+ const [hoverRow, setHoverRow] = useState7(null);
1374
1912
  const rowSegs = allRows.map((r) => {
1375
1913
  const segs = percentages(r.data);
1376
1914
  return { row: r, items: r.data.map((d, i) => ({ ...d, color: colorOf.get(d.id), fraction: segs[i].fraction })) };
@@ -1378,7 +1916,7 @@ function PercentageBarChart({
1378
1916
  const renderBar = (items, rowId) => {
1379
1917
  const visible = items.filter((i) => i.fraction > 0);
1380
1918
  const ordered = vertical ? [...visible].reverse() : visible;
1381
- return /* @__PURE__ */ jsx11(
1919
+ return /* @__PURE__ */ jsx14(
1382
1920
  "div",
1383
1921
  {
1384
1922
  style: {
@@ -1390,7 +1928,7 @@ function PercentageBarChart({
1390
1928
  children: ordered.map((i, k) => {
1391
1929
  const a = k === 0 ? end : inner;
1392
1930
  const b = k === ordered.length - 1 ? end : inner;
1393
- return /* @__PURE__ */ jsx11(
1931
+ return /* @__PURE__ */ jsx14(
1394
1932
  "span",
1395
1933
  {
1396
1934
  title: `${i.label} ${formatLabel(i, i.fraction)}`,
@@ -1419,7 +1957,7 @@ function PercentageBarChart({
1419
1957
  );
1420
1958
  };
1421
1959
  const legendRow = multi ? rowSegs.find((r) => r.row.id === hoverRow) : rowSegs[0];
1422
- const legend = showLabels && /* @__PURE__ */ jsx11(
1960
+ const legend = showLabels && /* @__PURE__ */ jsx14(
1423
1961
  Legend,
1424
1962
  {
1425
1963
  style: legendAt === "top" ? { marginBottom: 10 } : legendAt === "bottom" ? { marginTop: 10 } : {},
@@ -1438,15 +1976,15 @@ function PercentageBarChart({
1438
1976
  onItemHover: setHovered
1439
1977
  }
1440
1978
  );
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: [
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: [
1443
1981
  renderBar(items, row.id),
1444
1982
  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", {}),
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", {}),
1447
1985
  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(
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(
1450
1988
  "div",
1451
1989
  {
1452
1990
  className,
@@ -1466,8 +2004,11 @@ export {
1466
2004
  AreaChart,
1467
2005
  BarChart,
1468
2006
  BarPlot,
2007
+ CandlePlot,
2008
+ CandlestickChart,
1469
2009
  CartesianChart,
1470
2010
  DEFAULT_PERIODS,
2011
+ DonutChart,
1471
2012
  FnChart,
1472
2013
  FnChartSlot,
1473
2014
  HorizontalBarChart,
@@ -1480,6 +2021,7 @@ export {
1480
2021
  Point,
1481
2022
  ReferenceLine,
1482
2023
  Scrubber,
2024
+ TreemapChart,
1483
2025
  XAxis,
1484
2026
  YAxis,
1485
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 {};
@@ -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.1",
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.1",
50
- "@funnest/charts-viz": "0.2.1",
51
- "@funnest/charts-ui": "0.2.1"
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",