@funnest/charts-react 0.2.1 → 0.4.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,540 @@ 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
+ var fade = (color, a) => {
1278
+ const m = /^#([0-9a-f]{6})$/i.exec(color);
1279
+ if (!m) return `color-mix(in srgb, ${color} ${Math.round(a * 100)}%, transparent)`;
1280
+ const n = parseInt(m[1], 16);
1281
+ return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
1282
+ };
1283
+ function CandlePlot({
1284
+ data,
1285
+ yAxisId,
1286
+ upColor = "#F04452",
1287
+ downColor = "#3182F6",
1288
+ bodyWidth = 0.7,
1289
+ hollowUp = false,
1290
+ tooltip = true,
1291
+ labels = KO,
1292
+ valueFormatter = formatNumber
1293
+ }) {
1294
+ const ctx = useCartesianChart();
1295
+ const band = ctx.band;
1296
+ if (!band) return null;
1297
+ const y = scaleFor(ctx, yAxisId);
1298
+ const bw = Math.max(1, Math.min(band.step * bodyWidth, band.step - 1));
1299
+ const i = ctx.scrubIndex;
1300
+ const scrubbed = i !== null ? data[i] : null;
1301
+ let tip = null;
1302
+ if (tooltip && i !== null && scrubbed) {
1303
+ const prev = i > 0 ? data[i - 1] : null;
1304
+ const base = prev?.close ?? scrubbed.open;
1305
+ const chg = base ? (scrubbed.close - base) / base * 100 : 0;
1306
+ const rows = [
1307
+ [labels.open, valueFormatter(scrubbed.open)],
1308
+ [labels.high, valueFormatter(scrubbed.high), upColor],
1309
+ [labels.low, valueFormatter(scrubbed.low), downColor],
1310
+ [labels.close, `${valueFormatter(scrubbed.close)} (${chg > 0 ? "+" : ""}${chg.toFixed(2)}%)`, chg > 0 ? upColor : chg < 0 ? downColor : void 0]
1311
+ ];
1312
+ if (scrubbed.volume !== void 0) rows.push([labels.volume ?? KO.volume, formatNumber(scrubbed.volume)]);
1313
+ const width = (s) => [...s].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? 12 : 7.2), 0);
1314
+ const tw = Math.max(...rows.map(([a, b]) => width(a) + width(b) + 24)) + 8;
1315
+ const th = rows.length * 18 + 14;
1316
+ const x = band.center(i);
1317
+ const right = x + 12 + tw <= ctx.plot.right;
1318
+ tip = /* @__PURE__ */ jsxs8("g", { pointerEvents: "none", children: [
1319
+ /* @__PURE__ */ jsx10("line", { x1: x, x2: x, y1: ctx.plot.top, y2: ctx.plot.bottom, strokeWidth: 1, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
1320
+ /* @__PURE__ */ jsxs8("g", { transform: `translate(${right ? x + 12 : x - 12 - tw},${ctx.plot.top + 4})`, children: [
1321
+ /* @__PURE__ */ jsx10("rect", { width: tw, height: th, rx: 8, strokeWidth: 1, style: { fill: VAR.bg, stroke: VAR.grid } }),
1322
+ rows.map(([a, b, color], k) => /* @__PURE__ */ jsxs8("g", { transform: `translate(10,${16 + k * 18})`, children: [
1323
+ /* @__PURE__ */ jsx10("text", { dominantBaseline: "central", fontSize: 12, style: { fill: VAR.text }, children: a }),
1324
+ /* @__PURE__ */ jsx10("text", { x: tw - 20, textAnchor: "end", dominantBaseline: "central", fontSize: 12, fontWeight: 700, style: { fill: color ?? VAR.textStrong, fontVariantNumeric: "tabular-nums" }, children: b })
1325
+ ] }, a))
1326
+ ] })
1327
+ ] });
1328
+ }
1329
+ return /* @__PURE__ */ jsxs8("g", { children: [
1330
+ data.map((d, k) => {
1331
+ if (!d) return null;
1332
+ const up = d.close >= d.open;
1333
+ const color = up ? upColor : downColor;
1334
+ const cx = Math.round(band.center(k)) + 0.5;
1335
+ const top = y(Math.max(d.open, d.close));
1336
+ const bottom = y(Math.min(d.open, d.close));
1337
+ const h = Math.max(1, bottom - top);
1338
+ const hollow = hollowUp && up && bw >= 4;
1339
+ return /* @__PURE__ */ jsxs8("g", { opacity: i !== null && i !== k ? 0.55 : 1, children: [
1340
+ /* @__PURE__ */ jsx10("line", { x1: cx, x2: cx, y1: y(d.high), y2: y(d.low), strokeWidth: 1, style: { stroke: color } }),
1341
+ /* @__PURE__ */ jsx10(
1342
+ "rect",
1343
+ {
1344
+ x: cx - bw / 2,
1345
+ y: top,
1346
+ width: bw,
1347
+ height: h,
1348
+ rx: Math.min(1.5, bw / 4),
1349
+ strokeWidth: hollow ? 1 : 0,
1350
+ style: hollow ? { fill: VAR.bg, stroke: color } : { fill: color }
1351
+ }
1352
+ )
1353
+ ] }, k);
1354
+ }),
1355
+ tip
1356
+ ] });
1357
+ }
1358
+ var axisProps = (v) => v ? v === true ? {} : v : null;
1359
+ function CandlestickChart({
1360
+ data,
1361
+ showVolume,
1362
+ volumeHeight = 0.22,
1363
+ showYAxis = true,
1364
+ showXAxis,
1365
+ tooltip = true,
1366
+ upColor = "#F04452",
1367
+ downColor = "#3182F6",
1368
+ bodyWidth,
1369
+ hollowUp,
1370
+ labels,
1371
+ valueFormatter = formatNumber,
1372
+ xAxis,
1373
+ children,
1374
+ getScrubberAccessibilityLabel,
1375
+ ...chart
1376
+ }) {
1377
+ const hasVolume = showVolume ?? data.some((d) => d?.volume !== void 0);
1378
+ const maxVol = Math.max(0, ...data.map((d) => d?.volume ?? 0));
1379
+ const lows = data.flatMap((d) => d ? [d.low] : []);
1380
+ const highs = data.flatMap((d) => d ? [d.high] : []);
1381
+ const lo = lows.length ? Math.min(...lows) : 0;
1382
+ const hi = highs.length ? Math.max(...highs) : 1;
1383
+ const pad = (hi - lo || Math.abs(hi) || 1) * 0.04;
1384
+ const share = hasVolume ? Math.min(0.6, Math.max(0.05, volumeHeight)) : 0;
1385
+ const priceMin = lo - pad - (hi - lo + 2 * pad) * share / (1 - share);
1386
+ const yAx = axisProps(showYAxis);
1387
+ const xAx = axisProps(showXAxis);
1388
+ const l = labels ?? KO;
1389
+ return /* @__PURE__ */ jsxs8(
1390
+ CartesianChart,
1391
+ {
1392
+ enableScrubbing: tooltip,
1393
+ ...chart,
1394
+ xAxis: { bandPadding: 0.2, ...xAxis, scaleType: "band" },
1395
+ series: [
1396
+ { id: "price", data: data.map((d) => d ? [d.low, d.high] : null), yAxisId: "price", color: upColor },
1397
+ ...hasVolume ? [{ id: "volume", data: data.map((d) => d?.volume ?? null), yAxisId: "volume", color: "color-mix(in srgb, CanvasText 18%, transparent)" }] : []
1398
+ ],
1399
+ yAxis: [
1400
+ { id: "price", includeZero: false, domain: { min: priceMin, max: hi + pad } },
1401
+ // Volume keeps to the bottom band: its axis is stretched so the tallest bar reaches volumeHeight.
1402
+ { id: "volume", domain: { min: 0, max: maxVol > 0 ? maxVol / Math.max(0.05, volumeHeight) : 1 } }
1403
+ ],
1404
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel ?? ((i) => {
1405
+ const d = data[i];
1406
+ return d ? `${l.open} ${valueFormatter(d.open)}, ${l.high} ${valueFormatter(d.high)}, ${l.low} ${valueFormatter(d.low)}, ${l.close} ${valueFormatter(d.close)}` : "";
1407
+ }),
1408
+ children: [
1409
+ yAx && /* @__PURE__ */ jsx10(YAxis, { tickLabelFormatter: valueFormatter, ...yAx, axisId: "price" }, "y"),
1410
+ xAx && /* @__PURE__ */ jsx10(XAxis, { ...xAx }, "x"),
1411
+ hasVolume && /* @__PURE__ */ jsx10(
1412
+ BarPlot,
1413
+ {
1414
+ seriesIds: ["volume"],
1415
+ radius: 1,
1416
+ highlightScrubbed: false,
1417
+ getBarColor: (_v, i) => {
1418
+ const d = data[i];
1419
+ return d ? fade(d.close >= d.open ? upColor : downColor, 0.3) : void 0;
1420
+ }
1421
+ }
1422
+ ),
1423
+ /* @__PURE__ */ jsx10(
1424
+ CandlePlot,
1425
+ {
1426
+ data,
1427
+ yAxisId: "price",
1428
+ upColor,
1429
+ downColor,
1430
+ tooltip,
1431
+ labels: l,
1432
+ valueFormatter,
1433
+ ...bodyWidth !== void 0 ? { bodyWidth } : {},
1434
+ ...hollowUp !== void 0 ? { hollowUp } : {}
1435
+ }
1436
+ ),
1437
+ children
1438
+ ]
1439
+ }
1440
+ );
1441
+ }
1442
+
1443
+ // src/viz/scrubber.tsx
1444
+ import { useEffect as useEffect5 } from "react";
1445
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
902
1446
  var TIP_FONT = 12;
903
1447
  var ROW = 18;
904
1448
  var TIP_PAD = 7;
905
- var textWidth2 = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? TIP_FONT : TIP_FONT * 0.6), 0);
1449
+ var textWidth3 = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? TIP_FONT : TIP_FONT * 0.6), 0);
906
1450
  function Scrubber({
907
1451
  label,
908
1452
  seriesIds,
@@ -917,16 +1461,16 @@ function Scrubber({
917
1461
  const ctx = useCartesianChart();
918
1462
  const i = ctx.scrubIndex;
919
1463
  const { setDimFuture } = ctx;
920
- const fade = dimFuture && !hideOverlay;
921
- useEffect4(() => {
922
- setDimFuture(fade);
1464
+ const fade2 = dimFuture && !hideOverlay;
1465
+ useEffect5(() => {
1466
+ setDimFuture(fade2);
923
1467
  return () => setDimFuture(false);
924
- }, [fade, setDimFuture]);
1468
+ }, [fade2, setDimFuture]);
925
1469
  const pick = seriesIds ? ctx.series.filter((s) => seriesIds.includes(s.id)) : ctx.series;
926
1470
  const colorOf = (s, v) => beaconColor?.(v, s.id) ?? s.color;
927
1471
  if (i === null || i >= ctx.length) {
928
1472
  if (!idlePulse || ctx.band) return null;
929
- return /* @__PURE__ */ jsx8("g", { pointerEvents: "none", children: pick.map((s) => {
1473
+ return /* @__PURE__ */ jsx11("g", { pointerEvents: "none", children: pick.map((s) => {
930
1474
  let last = s.data.length - 1;
931
1475
  while (last >= 0 && (s.data[last] === null || s.data[last] === void 0)) last--;
932
1476
  if (last < 0) return null;
@@ -934,12 +1478,12 @@ function Scrubber({
934
1478
  if (y === null) return null;
935
1479
  const x2 = ctx.x(last);
936
1480
  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" })
1481
+ return /* @__PURE__ */ jsxs9("g", { children: [
1482
+ /* @__PURE__ */ jsxs9("circle", { cx: x2, cy: y, r: 4, fillOpacity: 0.35, style: { fill: color }, children: [
1483
+ /* @__PURE__ */ jsx11("animate", { attributeName: "r", values: "4;13", dur: "1.6s", repeatCount: "indefinite" }),
1484
+ /* @__PURE__ */ jsx11("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
941
1485
  ] }),
942
- /* @__PURE__ */ jsx8("circle", { cx: x2, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } })
1486
+ /* @__PURE__ */ jsx11("circle", { cx: x2, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } })
943
1487
  ] }, s.id);
944
1488
  }) });
945
1489
  }
@@ -955,25 +1499,25 @@ function Scrubber({
955
1499
  const text2 = lo !== null && lo !== void 0 && lo !== v ? `${valueFormatter(lo, s.id)} \u2013 ${valueFormatter(v, s.id)}` : valueFormatter(v, s.id);
956
1500
  return [{ s, text: text2 }];
957
1501
  }) : [];
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;
1502
+ 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
1503
  const tipH = rows.length * ROW + TIP_PAD * 2;
960
1504
  const tipRight = x + 12 + tipW <= plot.right;
961
1505
  const tipX = tipRight ? x + 12 : x - 12 - tipW;
962
1506
  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 }),
1507
+ return /* @__PURE__ */ jsxs9("g", { pointerEvents: "none", children: [
1508
+ /* @__PURE__ */ jsx11("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth: 1, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
1509
+ 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
1510
  !ctx.band && list.map((s) => {
967
1511
  const y = seriesY(ctx, s, i);
968
1512
  if (y === null) return null;
969
1513
  const v = s.data[i];
970
1514
  const color = colorOf(s, v);
971
1515
  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(
1516
+ const right = x + 12 + textWidth3(text2) <= plot.right;
1517
+ return /* @__PURE__ */ jsxs9("g", { children: [
1518
+ /* @__PURE__ */ jsx11("circle", { cx: x, cy: y, r: 7, fillOpacity: 0.18, style: { fill: color } }),
1519
+ /* @__PURE__ */ jsx11("circle", { cx: x, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } }),
1520
+ text2 && /* @__PURE__ */ jsx11(
977
1521
  "text",
978
1522
  {
979
1523
  x: right ? x + 10 : x - 10,
@@ -991,32 +1535,32 @@ function Scrubber({
991
1535
  )
992
1536
  ] }, s.id);
993
1537
  }),
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 })
1538
+ rows.length > 0 && /* @__PURE__ */ jsxs9("g", { transform: `translate(${tipX},${tipY})`, children: [
1539
+ /* @__PURE__ */ jsx11("rect", { width: tipW, height: tipH, rx: 8, style: { fill: VAR.bg, stroke: VAR.grid }, strokeWidth: 1 }),
1540
+ rows.map((r, k) => /* @__PURE__ */ jsxs9("g", { transform: `translate(10,${TIP_PAD + ROW / 2 + k * ROW})`, children: [
1541
+ /* @__PURE__ */ jsx11("circle", { cx: 3, cy: 0, r: 3, style: { fill: r.s.color } }),
1542
+ (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 }),
1543
+ /* @__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
1544
  ] }, r.s.id))
1001
1545
  ] })
1002
1546
  ] });
1003
1547
  }
1004
1548
 
1005
1549
  // src/viz/annotations.tsx
1006
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1550
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1007
1551
  var LABEL_FONT = 11;
1008
1552
  var labelWidth = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? LABEL_FONT : LABEL_FONT * 0.62), 0);
1009
1553
  function LineLabel(props) {
1010
1554
  const { x, y, text, anchor, color, pill } = props;
1011
1555
  const textColor = color === VAR.line ? VAR.text : color;
1012
1556
  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 });
1557
+ 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
1558
  }
1015
1559
  const w = labelWidth(text) + 14;
1016
1560
  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 })
1561
+ return /* @__PURE__ */ jsxs10("g", { children: [
1562
+ /* @__PURE__ */ jsx12("rect", { x: left, y: y - 10, width: w, height: 20, rx: 10, style: { fill: color === VAR.line ? VAR.textStrong : color } }),
1563
+ /* @__PURE__ */ jsx12("text", { x: left + w / 2, y, textAnchor: "middle", dominantBaseline: "central", fontSize: LABEL_FONT, fontWeight: 700, style: { fill: VAR.bg }, children: text })
1020
1564
  ] });
1021
1565
  }
1022
1566
  function ReferenceLine({
@@ -1044,17 +1588,17 @@ function ReferenceLine({
1044
1588
  const anchor = pos === "left" ? "start" : pos === "center" ? "middle" : "end";
1045
1589
  const off = pill ? 13 : 9;
1046
1590
  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 })
1591
+ return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
1592
+ /* @__PURE__ */ jsx12("line", { x1: plot.left, x2: plot.right, y1: y, y2: y, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
1593
+ label !== void 0 && /* @__PURE__ */ jsx12(LineLabel, { x: tx + labelDx, y: ly + labelDy, text: label, anchor, color, pill })
1050
1594
  ] });
1051
1595
  }
1052
1596
  if (dataX !== void 0) {
1053
1597
  const x = ctx.x(dataX);
1054
1598
  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(
1599
+ return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
1600
+ /* @__PURE__ */ jsx12("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
1601
+ label !== void 0 && /* @__PURE__ */ jsx12(
1058
1602
  LineLabel,
1059
1603
  {
1060
1604
  x: x + (pill ? 0 : 4) + labelDx,
@@ -1080,26 +1624,26 @@ function Point({ dataX, dataY, seriesId, yAxisId, color, radius = 4, label, labe
1080
1624
  const fill = color ?? s?.color ?? ctx.series[0]?.color ?? "currentColor";
1081
1625
  const gap = radius + 6;
1082
1626
  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" })
1627
+ return /* @__PURE__ */ jsxs10("g", { style: onClick ? { cursor: "pointer" } : void 0, onClick, pointerEvents: onClick ? "auto" : "none", children: [
1628
+ pulse && /* @__PURE__ */ jsxs10("circle", { cx: x, cy: y, r: radius, fillOpacity: 0.35, style: { fill }, children: [
1629
+ /* @__PURE__ */ jsx12("animate", { attributeName: "r", values: `${radius};${radius * 3.2}`, dur: "1.6s", repeatCount: "indefinite" }),
1630
+ /* @__PURE__ */ jsx12("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
1087
1631
  ] }),
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 })
1632
+ /* @__PURE__ */ jsx12("circle", { cx: x, cy: y, r: radius, strokeWidth: 2, style: { fill, stroke: VAR.bg } }),
1633
+ 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
1634
  ] });
1091
1635
  }
1092
1636
 
1093
1637
  // 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";
1638
+ import { useLayoutEffect, useRef as useRef5, useState as useState6 } from "react";
1639
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1096
1640
  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" })
1641
+ return /* @__PURE__ */ jsxs11("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
1642
+ /* @__PURE__ */ jsxs11("span", { "aria-hidden": "true", style: { position: "relative", width: 7, height: 7 }, children: [
1643
+ /* @__PURE__ */ jsx13("span", { style: { position: "absolute", inset: 0, borderRadius: "50%", background: color } }),
1644
+ /* @__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: [
1645
+ /* @__PURE__ */ jsx13("animate", { attributeName: "r", values: "3.5;8", dur: "1.6s", repeatCount: "indefinite" }),
1646
+ /* @__PURE__ */ jsx13("animate", { attributeName: "fill-opacity", values: "0.45;0", dur: "1.6s", repeatCount: "indefinite" })
1103
1647
  ] }) })
1104
1648
  ] }),
1105
1649
  label
@@ -1126,8 +1670,8 @@ function PeriodSelector({
1126
1670
  }) {
1127
1671
  const activeIsLive = periods.find((p) => p.id === value)?.live ?? false;
1128
1672
  const pillColor = activeIsLive ? liveColor : activeColor;
1129
- const listRef = useRef4(null);
1130
- const [pill, setPill] = useState4(null);
1673
+ const listRef = useRef5(null);
1674
+ const [pill, setPill] = useState6(null);
1131
1675
  useLayoutEffect(() => {
1132
1676
  const list = listRef.current;
1133
1677
  const active = list?.querySelector('[aria-selected="true"]');
@@ -1149,7 +1693,7 @@ function PeriodSelector({
1149
1693
  onChange(p.id);
1150
1694
  listRef.current?.querySelector(`[data-period="${CSS.escape(p.id)}"]`)?.focus();
1151
1695
  };
1152
- return /* @__PURE__ */ jsxs8(
1696
+ return /* @__PURE__ */ jsxs11(
1153
1697
  "div",
1154
1698
  {
1155
1699
  ref: listRef,
@@ -1159,7 +1703,7 @@ function PeriodSelector({
1159
1703
  onKeyDown,
1160
1704
  style: { position: "relative", display: fullWidth ? "flex" : "inline-flex", gap: 2, fontFamily: VAR.font, ...style },
1161
1705
  children: [
1162
- pill && /* @__PURE__ */ jsx10(
1706
+ pill && /* @__PURE__ */ jsx13(
1163
1707
  "span",
1164
1708
  {
1165
1709
  "aria-hidden": "true",
@@ -1178,7 +1722,7 @@ function PeriodSelector({
1178
1722
  ),
1179
1723
  periods.map((p) => {
1180
1724
  const active = p.id === value;
1181
- return /* @__PURE__ */ jsx10(
1725
+ return /* @__PURE__ */ jsx13(
1182
1726
  "button",
1183
1727
  {
1184
1728
  type: "button",
@@ -1202,7 +1746,7 @@ function PeriodSelector({
1202
1746
  fontWeight: active ? 700 : 500,
1203
1747
  cursor: "pointer"
1204
1748
  },
1205
- children: p.live ? /* @__PURE__ */ jsx10(LiveTabLabel, { label: p.label, color: liveColor }) : p.label
1749
+ children: p.live ? /* @__PURE__ */ jsx13(LiveTabLabel, { label: p.label, color: liveColor }) : p.label
1206
1750
  },
1207
1751
  p.id
1208
1752
  );
@@ -1213,13 +1757,13 @@ function PeriodSelector({
1213
1757
  }
1214
1758
 
1215
1759
  // 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;
1760
+ import { percentages, seriesColor as seriesColor5 } from "@funnest/charts-viz";
1761
+ import { useState as useState7 } from "react";
1762
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1763
+ var axisProps2 = (v) => v ? v === true ? {} : v : null;
1220
1764
  var axisElements = (x, y) => [
1221
- y && /* @__PURE__ */ jsx11(YAxis, { ...y }, "y"),
1222
- x && /* @__PURE__ */ jsx11(XAxis, { ...x }, "x")
1765
+ y && /* @__PURE__ */ jsx14(YAxis, { ...y }, "y"),
1766
+ x && /* @__PURE__ */ jsx14(XAxis, { ...x }, "x")
1223
1767
  ];
1224
1768
  var defined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
1225
1769
  var KR_SPLIT = { above: "#F04452", below: "#3182F6" };
@@ -1238,11 +1782,11 @@ function LineChart({
1238
1782
  children,
1239
1783
  ...chart
1240
1784
  }) {
1241
- const sc = axisProps(scrubber);
1785
+ const sc = axisProps2(scrubber);
1242
1786
  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(
1787
+ return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1788
+ axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
1789
+ chart.series.map((s) => /* @__PURE__ */ jsx14(
1246
1790
  Line,
1247
1791
  {
1248
1792
  seriesId: s.id,
@@ -1252,7 +1796,7 @@ function LineChart({
1252
1796
  s.id
1253
1797
  )),
1254
1798
  children,
1255
- sc && /* @__PURE__ */ jsx11(
1799
+ sc && /* @__PURE__ */ jsx14(
1256
1800
  Scrubber,
1257
1801
  {
1258
1802
  ...split && baseline !== void 0 ? { beaconColor: (v) => v >= baseline ? split.above : split.below } : {},
@@ -1277,12 +1821,12 @@ function AreaChart({
1277
1821
  children,
1278
1822
  ...chart
1279
1823
  }) {
1280
- const sc = axisProps(scrubber);
1281
- return /* @__PURE__ */ jsxs9(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1282
- axisElements(axisProps(showXAxis), axisProps(showYAxis)),
1824
+ const sc = axisProps2(scrubber);
1825
+ return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
1826
+ axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
1283
1827
  chart.series.map((s) => {
1284
1828
  const own = s.areaStyle === "none" ? null : s.areaStyle ?? (chart.stacked && fill === "gradient" ? "solid" : fill);
1285
- return own ? /* @__PURE__ */ jsx11(
1829
+ return own ? /* @__PURE__ */ jsx14(
1286
1830
  Area,
1287
1831
  {
1288
1832
  seriesId: s.id,
@@ -1293,16 +1837,16 @@ function AreaChart({
1293
1837
  `a${s.id}`
1294
1838
  ) : null;
1295
1839
  }),
1296
- showLines && chart.series.map((s) => /* @__PURE__ */ jsx11(Line, { seriesId: s.id, curve, strokeWidth, ...defined({ lineStyle, baseline, splitColors }) }, `l${s.id}`)),
1840
+ showLines && chart.series.map((s) => /* @__PURE__ */ jsx14(Line, { seriesId: s.id, curve, strokeWidth, ...defined({ lineStyle, baseline, splitColors }) }, `l${s.id}`)),
1297
1841
  children,
1298
- sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
1842
+ sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
1299
1843
  ] });
1300
1844
  }
1301
1845
  function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGap, stackGap, roundBaseline, barMinSize, layout = "vertical", xAxis, children, ...chart }) {
1302
- const sc = axisProps(scrubber);
1846
+ const sc = axisProps2(scrubber);
1303
1847
  if (layout === "horizontal") {
1304
- const yAx = axisProps(showYAxis);
1305
- return /* @__PURE__ */ jsx11(
1848
+ const yAx = axisProps2(showYAxis);
1849
+ return /* @__PURE__ */ jsx14(
1306
1850
  HorizontalBarChart,
1307
1851
  {
1308
1852
  series: chart.series,
@@ -1329,11 +1873,11 @@ function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGa
1329
1873
  }
1330
1874
  );
1331
1875
  }
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 }),
1876
+ return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, xAxis: { ...xAxis, scaleType: "band" }, ...chart, children: [
1877
+ axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
1878
+ /* @__PURE__ */ jsx14(BarPlot, { ...defined({ radius, getBarColor, groupGap, stackGap, roundBaseline, minSize: barMinSize }), highlightScrubbed: !!sc }),
1335
1879
  children,
1336
- sc && /* @__PURE__ */ jsx11(Scrubber, { ...sc })
1880
+ sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
1337
1881
  ] });
1338
1882
  }
1339
1883
  var pct = (_item, f) => `${formatNumber(Math.round(f * 1e3) / 10)}%`;
@@ -1366,11 +1910,11 @@ function PercentageBarChart({
1366
1910
  for (const r of allRows)
1367
1911
  for (const d of r.data)
1368
1912
  if (!colorOf.has(d.id)) {
1369
- colorOf.set(d.id, d.color ?? seriesColor3(colorOf.size, palette));
1913
+ colorOf.set(d.id, d.color ?? seriesColor5(colorOf.size, palette));
1370
1914
  legendItems.push(d);
1371
1915
  }
1372
- const [hovered, setHovered] = useState5(null);
1373
- const [hoverRow, setHoverRow] = useState5(null);
1916
+ const [hovered, setHovered] = useState7(null);
1917
+ const [hoverRow, setHoverRow] = useState7(null);
1374
1918
  const rowSegs = allRows.map((r) => {
1375
1919
  const segs = percentages(r.data);
1376
1920
  return { row: r, items: r.data.map((d, i) => ({ ...d, color: colorOf.get(d.id), fraction: segs[i].fraction })) };
@@ -1378,7 +1922,7 @@ function PercentageBarChart({
1378
1922
  const renderBar = (items, rowId) => {
1379
1923
  const visible = items.filter((i) => i.fraction > 0);
1380
1924
  const ordered = vertical ? [...visible].reverse() : visible;
1381
- return /* @__PURE__ */ jsx11(
1925
+ return /* @__PURE__ */ jsx14(
1382
1926
  "div",
1383
1927
  {
1384
1928
  style: {
@@ -1390,7 +1934,7 @@ function PercentageBarChart({
1390
1934
  children: ordered.map((i, k) => {
1391
1935
  const a = k === 0 ? end : inner;
1392
1936
  const b = k === ordered.length - 1 ? end : inner;
1393
- return /* @__PURE__ */ jsx11(
1937
+ return /* @__PURE__ */ jsx14(
1394
1938
  "span",
1395
1939
  {
1396
1940
  title: `${i.label} ${formatLabel(i, i.fraction)}`,
@@ -1419,7 +1963,7 @@ function PercentageBarChart({
1419
1963
  );
1420
1964
  };
1421
1965
  const legendRow = multi ? rowSegs.find((r) => r.row.id === hoverRow) : rowSegs[0];
1422
- const legend = showLabels && /* @__PURE__ */ jsx11(
1966
+ const legend = showLabels && /* @__PURE__ */ jsx14(
1423
1967
  Legend,
1424
1968
  {
1425
1969
  style: legendAt === "top" ? { marginBottom: 10 } : legendAt === "bottom" ? { marginTop: 10 } : {},
@@ -1438,15 +1982,15 @@ function PercentageBarChart({
1438
1982
  onItemHover: setHovered
1439
1983
  }
1440
1984
  );
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: [
1985
+ 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;
1986
+ 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
1987
  renderBar(items, row.id),
1444
1988
  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", {}),
1989
+ ] }, 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: [
1990
+ label(row) ?? /* @__PURE__ */ jsx14("span", {}),
1447
1991
  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(
1992
+ ] }, row.id)) }) : /* @__PURE__ */ jsx14("div", { style: { display: "flex", flex: legendAt === "right" ? 1 : void 0 }, children: renderBar(rowSegs[0].items, rowSegs[0].row.id) });
1993
+ return /* @__PURE__ */ jsxs12(
1450
1994
  "div",
1451
1995
  {
1452
1996
  className,
@@ -1466,8 +2010,11 @@ export {
1466
2010
  AreaChart,
1467
2011
  BarChart,
1468
2012
  BarPlot,
2013
+ CandlePlot,
2014
+ CandlestickChart,
1469
2015
  CartesianChart,
1470
2016
  DEFAULT_PERIODS,
2017
+ DonutChart,
1471
2018
  FnChart,
1472
2019
  FnChartSlot,
1473
2020
  HorizontalBarChart,
@@ -1480,6 +2027,7 @@ export {
1480
2027
  Point,
1481
2028
  ReferenceLine,
1482
2029
  Scrubber,
2030
+ TreemapChart,
1483
2031
  XAxis,
1484
2032
  YAxis,
1485
2033
  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.4.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-ui": "0.4.0",
50
+ "@funnest/charts": "0.4.0",
51
+ "@funnest/charts-viz": "0.4.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "react": ">=18",