@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 +2 -1
- package/dist/index.js +646 -98
- package/dist/viz/candles.d.ts +57 -0
- package/dist/viz/donut.d.ts +55 -0
- package/dist/viz/index.d.ts +3 -0
- package/dist/viz/treemap.d.ts +48 -0
- package/package.json +4 -4
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,
|
|
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
|
-
|
|
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/
|
|
900
|
-
import {
|
|
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
|
|
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
|
|
921
|
-
|
|
922
|
-
setDimFuture(
|
|
1464
|
+
const fade2 = dimFuture && !hideOverlay;
|
|
1465
|
+
useEffect5(() => {
|
|
1466
|
+
setDimFuture(fade2);
|
|
923
1467
|
return () => setDimFuture(false);
|
|
924
|
-
}, [
|
|
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__ */
|
|
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__ */
|
|
938
|
-
/* @__PURE__ */
|
|
939
|
-
/* @__PURE__ */
|
|
940
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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) =>
|
|
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__ */
|
|
964
|
-
/* @__PURE__ */
|
|
965
|
-
text && /* @__PURE__ */
|
|
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 +
|
|
973
|
-
return /* @__PURE__ */
|
|
974
|
-
/* @__PURE__ */
|
|
975
|
-
/* @__PURE__ */
|
|
976
|
-
text2 && /* @__PURE__ */
|
|
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__ */
|
|
995
|
-
/* @__PURE__ */
|
|
996
|
-
rows.map((r, k) => /* @__PURE__ */
|
|
997
|
-
/* @__PURE__ */
|
|
998
|
-
(rows.length > 1 || r.s.label) && /* @__PURE__ */
|
|
999
|
-
/* @__PURE__ */
|
|
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
|
|
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__ */
|
|
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__ */
|
|
1018
|
-
/* @__PURE__ */
|
|
1019
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1048
|
-
/* @__PURE__ */
|
|
1049
|
-
label !== void 0 && /* @__PURE__ */
|
|
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__ */
|
|
1056
|
-
/* @__PURE__ */
|
|
1057
|
-
label !== void 0 && /* @__PURE__ */
|
|
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__ */
|
|
1084
|
-
pulse && /* @__PURE__ */
|
|
1085
|
-
/* @__PURE__ */
|
|
1086
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1089
|
-
label !== void 0 && /* @__PURE__ */
|
|
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
|
|
1095
|
-
import { jsx as
|
|
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__ */
|
|
1098
|
-
/* @__PURE__ */
|
|
1099
|
-
/* @__PURE__ */
|
|
1100
|
-
/* @__PURE__ */
|
|
1101
|
-
/* @__PURE__ */
|
|
1102
|
-
/* @__PURE__ */
|
|
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 =
|
|
1130
|
-
const [pill, setPill] =
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
|
1217
|
-
import { useState as
|
|
1218
|
-
import { jsx as
|
|
1219
|
-
var
|
|
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__ */
|
|
1222
|
-
x && /* @__PURE__ */
|
|
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 =
|
|
1785
|
+
const sc = axisProps2(scrubber);
|
|
1242
1786
|
const split = baseline !== void 0 ? splitColors ?? KR_SPLIT : void 0;
|
|
1243
|
-
return /* @__PURE__ */
|
|
1244
|
-
axisElements(
|
|
1245
|
-
chart.series.map((s) => /* @__PURE__ */
|
|
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__ */
|
|
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 =
|
|
1281
|
-
return /* @__PURE__ */
|
|
1282
|
-
axisElements(
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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 =
|
|
1846
|
+
const sc = axisProps2(scrubber);
|
|
1303
1847
|
if (layout === "horizontal") {
|
|
1304
|
-
const yAx =
|
|
1305
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
1333
|
-
axisElements(
|
|
1334
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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 ??
|
|
1913
|
+
colorOf.set(d.id, d.color ?? seriesColor5(colorOf.size, palette));
|
|
1370
1914
|
legendItems.push(d);
|
|
1371
1915
|
}
|
|
1372
|
-
const [hovered, setHovered] =
|
|
1373
|
-
const [hoverRow, setHoverRow] =
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
1442
|
-
const bars = vertical ? /* @__PURE__ */
|
|
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__ */
|
|
1446
|
-
label(row) ?? /* @__PURE__ */
|
|
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__ */
|
|
1449
|
-
return /* @__PURE__ */
|
|
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;
|
package/dist/viz/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
50
|
-
"@funnest/charts
|
|
51
|
-
"@funnest/charts-
|
|
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",
|