@funnest/charts-react 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +637 -95
- 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,534 @@ function HorizontalBarChart({
|
|
|
892
913
|
] })
|
|
893
914
|
] })
|
|
894
915
|
}
|
|
895
|
-
)
|
|
916
|
+
),
|
|
917
|
+
enableScrubbing && /* @__PURE__ */ jsx7("span", { "aria-live": "polite", style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: focused && hover !== null ? [categories?.[hover] ?? String(hover), ...tipRows.map((r) => `${r.s.label ?? r.s.id} ${r.text}`)].join(", ") : "" })
|
|
896
918
|
] });
|
|
897
919
|
}
|
|
898
920
|
|
|
899
|
-
// src/viz/
|
|
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
|
+
function CandlePlot({
|
|
1278
|
+
data,
|
|
1279
|
+
yAxisId,
|
|
1280
|
+
upColor = "#F04452",
|
|
1281
|
+
downColor = "#3182F6",
|
|
1282
|
+
bodyWidth = 0.7,
|
|
1283
|
+
hollowUp = false,
|
|
1284
|
+
tooltip = true,
|
|
1285
|
+
labels = KO,
|
|
1286
|
+
valueFormatter = formatNumber
|
|
1287
|
+
}) {
|
|
1288
|
+
const ctx = useCartesianChart();
|
|
1289
|
+
const band = ctx.band;
|
|
1290
|
+
if (!band) return null;
|
|
1291
|
+
const y = scaleFor(ctx, yAxisId);
|
|
1292
|
+
const bw = Math.max(1, Math.min(band.step * bodyWidth, band.step - 1));
|
|
1293
|
+
const i = ctx.scrubIndex;
|
|
1294
|
+
const scrubbed = i !== null ? data[i] : null;
|
|
1295
|
+
let tip = null;
|
|
1296
|
+
if (tooltip && i !== null && scrubbed) {
|
|
1297
|
+
const prev = i > 0 ? data[i - 1] : null;
|
|
1298
|
+
const base = prev?.close ?? scrubbed.open;
|
|
1299
|
+
const chg = base ? (scrubbed.close - base) / base * 100 : 0;
|
|
1300
|
+
const rows = [
|
|
1301
|
+
[labels.open, valueFormatter(scrubbed.open)],
|
|
1302
|
+
[labels.high, valueFormatter(scrubbed.high), upColor],
|
|
1303
|
+
[labels.low, valueFormatter(scrubbed.low), downColor],
|
|
1304
|
+
[labels.close, `${valueFormatter(scrubbed.close)} (${chg > 0 ? "+" : ""}${chg.toFixed(2)}%)`, chg > 0 ? upColor : chg < 0 ? downColor : void 0]
|
|
1305
|
+
];
|
|
1306
|
+
if (scrubbed.volume !== void 0) rows.push([labels.volume ?? KO.volume, formatNumber(scrubbed.volume)]);
|
|
1307
|
+
const width = (s) => [...s].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? 12 : 7.2), 0);
|
|
1308
|
+
const tw = Math.max(...rows.map(([a, b]) => width(a) + width(b) + 24)) + 8;
|
|
1309
|
+
const th = rows.length * 18 + 14;
|
|
1310
|
+
const x = band.center(i);
|
|
1311
|
+
const right = x + 12 + tw <= ctx.plot.right;
|
|
1312
|
+
tip = /* @__PURE__ */ jsxs8("g", { pointerEvents: "none", children: [
|
|
1313
|
+
/* @__PURE__ */ jsx10("line", { x1: x, x2: x, y1: ctx.plot.top, y2: ctx.plot.bottom, strokeWidth: 1, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
|
|
1314
|
+
/* @__PURE__ */ jsxs8("g", { transform: `translate(${right ? x + 12 : x - 12 - tw},${ctx.plot.top + 4})`, children: [
|
|
1315
|
+
/* @__PURE__ */ jsx10("rect", { width: tw, height: th, rx: 8, strokeWidth: 1, style: { fill: VAR.bg, stroke: VAR.grid } }),
|
|
1316
|
+
rows.map(([a, b, color], k) => /* @__PURE__ */ jsxs8("g", { transform: `translate(10,${16 + k * 18})`, children: [
|
|
1317
|
+
/* @__PURE__ */ jsx10("text", { dominantBaseline: "central", fontSize: 12, style: { fill: VAR.text }, children: a }),
|
|
1318
|
+
/* @__PURE__ */ jsx10("text", { x: tw - 20, textAnchor: "end", dominantBaseline: "central", fontSize: 12, fontWeight: 700, style: { fill: color ?? VAR.textStrong, fontVariantNumeric: "tabular-nums" }, children: b })
|
|
1319
|
+
] }, a))
|
|
1320
|
+
] })
|
|
1321
|
+
] });
|
|
1322
|
+
}
|
|
1323
|
+
return /* @__PURE__ */ jsxs8("g", { children: [
|
|
1324
|
+
data.map((d, k) => {
|
|
1325
|
+
if (!d) return null;
|
|
1326
|
+
const up = d.close >= d.open;
|
|
1327
|
+
const color = up ? upColor : downColor;
|
|
1328
|
+
const cx = Math.round(band.center(k)) + 0.5;
|
|
1329
|
+
const top = y(Math.max(d.open, d.close));
|
|
1330
|
+
const bottom = y(Math.min(d.open, d.close));
|
|
1331
|
+
const h = Math.max(1, bottom - top);
|
|
1332
|
+
const hollow = hollowUp && up && bw >= 4;
|
|
1333
|
+
return /* @__PURE__ */ jsxs8("g", { opacity: i !== null && i !== k ? 0.55 : 1, children: [
|
|
1334
|
+
/* @__PURE__ */ jsx10("line", { x1: cx, x2: cx, y1: y(d.high), y2: y(d.low), strokeWidth: 1, style: { stroke: color } }),
|
|
1335
|
+
/* @__PURE__ */ jsx10(
|
|
1336
|
+
"rect",
|
|
1337
|
+
{
|
|
1338
|
+
x: cx - bw / 2,
|
|
1339
|
+
y: top,
|
|
1340
|
+
width: bw,
|
|
1341
|
+
height: h,
|
|
1342
|
+
rx: Math.min(1.5, bw / 4),
|
|
1343
|
+
strokeWidth: hollow ? 1 : 0,
|
|
1344
|
+
style: hollow ? { fill: VAR.bg, stroke: color } : { fill: color }
|
|
1345
|
+
}
|
|
1346
|
+
)
|
|
1347
|
+
] }, k);
|
|
1348
|
+
}),
|
|
1349
|
+
tip
|
|
1350
|
+
] });
|
|
1351
|
+
}
|
|
1352
|
+
var axisProps = (v) => v ? v === true ? {} : v : null;
|
|
1353
|
+
function CandlestickChart({
|
|
1354
|
+
data,
|
|
1355
|
+
showVolume,
|
|
1356
|
+
volumeHeight = 0.22,
|
|
1357
|
+
showYAxis = true,
|
|
1358
|
+
showXAxis,
|
|
1359
|
+
tooltip = true,
|
|
1360
|
+
upColor = "#F04452",
|
|
1361
|
+
downColor = "#3182F6",
|
|
1362
|
+
bodyWidth,
|
|
1363
|
+
hollowUp,
|
|
1364
|
+
labels,
|
|
1365
|
+
valueFormatter = formatNumber,
|
|
1366
|
+
xAxis,
|
|
1367
|
+
children,
|
|
1368
|
+
getScrubberAccessibilityLabel,
|
|
1369
|
+
...chart
|
|
1370
|
+
}) {
|
|
1371
|
+
const hasVolume = showVolume ?? data.some((d) => d?.volume !== void 0);
|
|
1372
|
+
const maxVol = Math.max(0, ...data.map((d) => d?.volume ?? 0));
|
|
1373
|
+
const lows = data.flatMap((d) => d ? [d.low] : []);
|
|
1374
|
+
const highs = data.flatMap((d) => d ? [d.high] : []);
|
|
1375
|
+
const lo = lows.length ? Math.min(...lows) : 0;
|
|
1376
|
+
const hi = highs.length ? Math.max(...highs) : 1;
|
|
1377
|
+
const pad = (hi - lo || Math.abs(hi) || 1) * 0.04;
|
|
1378
|
+
const share = hasVolume ? Math.min(0.6, Math.max(0.05, volumeHeight)) : 0;
|
|
1379
|
+
const priceMin = lo - pad - (hi - lo + 2 * pad) * share / (1 - share);
|
|
1380
|
+
const yAx = axisProps(showYAxis);
|
|
1381
|
+
const xAx = axisProps(showXAxis);
|
|
1382
|
+
const l = labels ?? KO;
|
|
1383
|
+
return /* @__PURE__ */ jsxs8(
|
|
1384
|
+
CartesianChart,
|
|
1385
|
+
{
|
|
1386
|
+
enableScrubbing: tooltip,
|
|
1387
|
+
...chart,
|
|
1388
|
+
xAxis: { bandPadding: 0.2, ...xAxis, scaleType: "band" },
|
|
1389
|
+
series: [
|
|
1390
|
+
{ id: "price", data: data.map((d) => d ? [d.low, d.high] : null), yAxisId: "price", color: upColor },
|
|
1391
|
+
...hasVolume ? [{ id: "volume", data: data.map((d) => d?.volume ?? null), yAxisId: "volume", color: "color-mix(in srgb, CanvasText 18%, transparent)" }] : []
|
|
1392
|
+
],
|
|
1393
|
+
yAxis: [
|
|
1394
|
+
{ id: "price", includeZero: false, domain: { min: priceMin, max: hi + pad } },
|
|
1395
|
+
// Volume keeps to the bottom band: its axis is stretched so the tallest bar reaches volumeHeight.
|
|
1396
|
+
{ id: "volume", domain: { min: 0, max: maxVol > 0 ? maxVol / Math.max(0.05, volumeHeight) : 1 } }
|
|
1397
|
+
],
|
|
1398
|
+
getScrubberAccessibilityLabel: getScrubberAccessibilityLabel ?? ((i) => {
|
|
1399
|
+
const d = data[i];
|
|
1400
|
+
return d ? `${l.open} ${valueFormatter(d.open)}, ${l.high} ${valueFormatter(d.high)}, ${l.low} ${valueFormatter(d.low)}, ${l.close} ${valueFormatter(d.close)}` : "";
|
|
1401
|
+
}),
|
|
1402
|
+
children: [
|
|
1403
|
+
yAx && /* @__PURE__ */ jsx10(YAxis, { tickLabelFormatter: valueFormatter, ...yAx, axisId: "price" }, "y"),
|
|
1404
|
+
xAx && /* @__PURE__ */ jsx10(XAxis, { ...xAx }, "x"),
|
|
1405
|
+
hasVolume && /* @__PURE__ */ jsx10(
|
|
1406
|
+
BarPlot,
|
|
1407
|
+
{
|
|
1408
|
+
seriesIds: ["volume"],
|
|
1409
|
+
radius: 1,
|
|
1410
|
+
highlightScrubbed: false,
|
|
1411
|
+
getBarColor: (_v, i) => {
|
|
1412
|
+
const d = data[i];
|
|
1413
|
+
return d ? `color-mix(in srgb, ${d.close >= d.open ? upColor : downColor} 30%, transparent)` : void 0;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
),
|
|
1417
|
+
/* @__PURE__ */ jsx10(
|
|
1418
|
+
CandlePlot,
|
|
1419
|
+
{
|
|
1420
|
+
data,
|
|
1421
|
+
yAxisId: "price",
|
|
1422
|
+
upColor,
|
|
1423
|
+
downColor,
|
|
1424
|
+
tooltip,
|
|
1425
|
+
labels: l,
|
|
1426
|
+
valueFormatter,
|
|
1427
|
+
...bodyWidth !== void 0 ? { bodyWidth } : {},
|
|
1428
|
+
...hollowUp !== void 0 ? { hollowUp } : {}
|
|
1429
|
+
}
|
|
1430
|
+
),
|
|
1431
|
+
children
|
|
1432
|
+
]
|
|
1433
|
+
}
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// src/viz/scrubber.tsx
|
|
1438
|
+
import { useEffect as useEffect5 } from "react";
|
|
1439
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
902
1440
|
var TIP_FONT = 12;
|
|
903
1441
|
var ROW = 18;
|
|
904
1442
|
var TIP_PAD = 7;
|
|
905
|
-
var
|
|
1443
|
+
var textWidth3 = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? TIP_FONT : TIP_FONT * 0.6), 0);
|
|
906
1444
|
function Scrubber({
|
|
907
1445
|
label,
|
|
908
1446
|
seriesIds,
|
|
@@ -918,7 +1456,7 @@ function Scrubber({
|
|
|
918
1456
|
const i = ctx.scrubIndex;
|
|
919
1457
|
const { setDimFuture } = ctx;
|
|
920
1458
|
const fade = dimFuture && !hideOverlay;
|
|
921
|
-
|
|
1459
|
+
useEffect5(() => {
|
|
922
1460
|
setDimFuture(fade);
|
|
923
1461
|
return () => setDimFuture(false);
|
|
924
1462
|
}, [fade, setDimFuture]);
|
|
@@ -926,7 +1464,7 @@ function Scrubber({
|
|
|
926
1464
|
const colorOf = (s, v) => beaconColor?.(v, s.id) ?? s.color;
|
|
927
1465
|
if (i === null || i >= ctx.length) {
|
|
928
1466
|
if (!idlePulse || ctx.band) return null;
|
|
929
|
-
return /* @__PURE__ */
|
|
1467
|
+
return /* @__PURE__ */ jsx11("g", { pointerEvents: "none", children: pick.map((s) => {
|
|
930
1468
|
let last = s.data.length - 1;
|
|
931
1469
|
while (last >= 0 && (s.data[last] === null || s.data[last] === void 0)) last--;
|
|
932
1470
|
if (last < 0) return null;
|
|
@@ -934,12 +1472,12 @@ function Scrubber({
|
|
|
934
1472
|
if (y === null) return null;
|
|
935
1473
|
const x2 = ctx.x(last);
|
|
936
1474
|
const color = colorOf(s, s.data[last]);
|
|
937
|
-
return /* @__PURE__ */
|
|
938
|
-
/* @__PURE__ */
|
|
939
|
-
/* @__PURE__ */
|
|
940
|
-
/* @__PURE__ */
|
|
1475
|
+
return /* @__PURE__ */ jsxs9("g", { children: [
|
|
1476
|
+
/* @__PURE__ */ jsxs9("circle", { cx: x2, cy: y, r: 4, fillOpacity: 0.35, style: { fill: color }, children: [
|
|
1477
|
+
/* @__PURE__ */ jsx11("animate", { attributeName: "r", values: "4;13", dur: "1.6s", repeatCount: "indefinite" }),
|
|
1478
|
+
/* @__PURE__ */ jsx11("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
|
|
941
1479
|
] }),
|
|
942
|
-
/* @__PURE__ */
|
|
1480
|
+
/* @__PURE__ */ jsx11("circle", { cx: x2, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } })
|
|
943
1481
|
] }, s.id);
|
|
944
1482
|
}) });
|
|
945
1483
|
}
|
|
@@ -955,25 +1493,25 @@ function Scrubber({
|
|
|
955
1493
|
const text2 = lo !== null && lo !== void 0 && lo !== v ? `${valueFormatter(lo, s.id)} \u2013 ${valueFormatter(v, s.id)}` : valueFormatter(v, s.id);
|
|
956
1494
|
return [{ s, text: text2 }];
|
|
957
1495
|
}) : [];
|
|
958
|
-
const tipW = Math.max(0, ...rows.map((r) =>
|
|
1496
|
+
const tipW = Math.max(0, ...rows.map((r) => textWidth3(r.text) + (rows.length > 1 || r.s.label ? textWidth3(r.s.label ?? "") + 26 : 18))) + 16;
|
|
959
1497
|
const tipH = rows.length * ROW + TIP_PAD * 2;
|
|
960
1498
|
const tipRight = x + 12 + tipW <= plot.right;
|
|
961
1499
|
const tipX = tipRight ? x + 12 : x - 12 - tipW;
|
|
962
1500
|
const tipY = plot.top + 4;
|
|
963
|
-
return /* @__PURE__ */
|
|
964
|
-
/* @__PURE__ */
|
|
965
|
-
text && /* @__PURE__ */
|
|
1501
|
+
return /* @__PURE__ */ jsxs9("g", { pointerEvents: "none", children: [
|
|
1502
|
+
/* @__PURE__ */ jsx11("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth: 1, shapeRendering: "crispEdges", style: { stroke: VAR.line } }),
|
|
1503
|
+
text && /* @__PURE__ */ jsx11("text", { x: textX, y: Math.max(11, plot.top - 2), textAnchor: "middle", fontSize: 11, fontWeight: 600, strokeWidth: 3, strokeLinejoin: "round", paintOrder: "stroke", style: { stroke: VAR.bg, fill: VAR.textStrong }, children: text }),
|
|
966
1504
|
!ctx.band && list.map((s) => {
|
|
967
1505
|
const y = seriesY(ctx, s, i);
|
|
968
1506
|
if (y === null) return null;
|
|
969
1507
|
const v = s.data[i];
|
|
970
1508
|
const color = colorOf(s, v);
|
|
971
1509
|
const text2 = beaconLabels ? typeof beaconLabels === "function" ? beaconLabels(v, s.id) : valueFormatter(v, s.id) : "";
|
|
972
|
-
const right = x + 12 +
|
|
973
|
-
return /* @__PURE__ */
|
|
974
|
-
/* @__PURE__ */
|
|
975
|
-
/* @__PURE__ */
|
|
976
|
-
text2 && /* @__PURE__ */
|
|
1510
|
+
const right = x + 12 + textWidth3(text2) <= plot.right;
|
|
1511
|
+
return /* @__PURE__ */ jsxs9("g", { children: [
|
|
1512
|
+
/* @__PURE__ */ jsx11("circle", { cx: x, cy: y, r: 7, fillOpacity: 0.18, style: { fill: color } }),
|
|
1513
|
+
/* @__PURE__ */ jsx11("circle", { cx: x, cy: y, r: 4, strokeWidth: 2, style: { fill: color, stroke: VAR.bg } }),
|
|
1514
|
+
text2 && /* @__PURE__ */ jsx11(
|
|
977
1515
|
"text",
|
|
978
1516
|
{
|
|
979
1517
|
x: right ? x + 10 : x - 10,
|
|
@@ -991,32 +1529,32 @@ function Scrubber({
|
|
|
991
1529
|
)
|
|
992
1530
|
] }, s.id);
|
|
993
1531
|
}),
|
|
994
|
-
rows.length > 0 && /* @__PURE__ */
|
|
995
|
-
/* @__PURE__ */
|
|
996
|
-
rows.map((r, k) => /* @__PURE__ */
|
|
997
|
-
/* @__PURE__ */
|
|
998
|
-
(rows.length > 1 || r.s.label) && /* @__PURE__ */
|
|
999
|
-
/* @__PURE__ */
|
|
1532
|
+
rows.length > 0 && /* @__PURE__ */ jsxs9("g", { transform: `translate(${tipX},${tipY})`, children: [
|
|
1533
|
+
/* @__PURE__ */ jsx11("rect", { width: tipW, height: tipH, rx: 8, style: { fill: VAR.bg, stroke: VAR.grid }, strokeWidth: 1 }),
|
|
1534
|
+
rows.map((r, k) => /* @__PURE__ */ jsxs9("g", { transform: `translate(10,${TIP_PAD + ROW / 2 + k * ROW})`, children: [
|
|
1535
|
+
/* @__PURE__ */ jsx11("circle", { cx: 3, cy: 0, r: 3, style: { fill: r.s.color } }),
|
|
1536
|
+
(rows.length > 1 || r.s.label) && /* @__PURE__ */ jsx11("text", { x: 12, dominantBaseline: "central", fontSize: TIP_FONT, style: { fill: VAR.text }, children: r.s.label ?? r.s.id }),
|
|
1537
|
+
/* @__PURE__ */ jsx11("text", { x: tipW - 20, textAnchor: "end", dominantBaseline: "central", fontSize: TIP_FONT, fontWeight: 700, style: { fill: VAR.textStrong, fontVariantNumeric: "tabular-nums" }, children: r.text })
|
|
1000
1538
|
] }, r.s.id))
|
|
1001
1539
|
] })
|
|
1002
1540
|
] });
|
|
1003
1541
|
}
|
|
1004
1542
|
|
|
1005
1543
|
// src/viz/annotations.tsx
|
|
1006
|
-
import { jsx as
|
|
1544
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1007
1545
|
var LABEL_FONT = 11;
|
|
1008
1546
|
var labelWidth = (t) => [...t].reduce((w, ch) => w + (ch.charCodeAt(0) > 11904 ? LABEL_FONT : LABEL_FONT * 0.62), 0);
|
|
1009
1547
|
function LineLabel(props) {
|
|
1010
1548
|
const { x, y, text, anchor, color, pill } = props;
|
|
1011
1549
|
const textColor = color === VAR.line ? VAR.text : color;
|
|
1012
1550
|
if (!pill) {
|
|
1013
|
-
return /* @__PURE__ */
|
|
1551
|
+
return /* @__PURE__ */ jsx12("text", { x, y, textAnchor: anchor, dominantBaseline: "central", fontSize: LABEL_FONT, fontWeight: 600, strokeWidth: 3, strokeLinejoin: "round", paintOrder: "stroke", style: { stroke: VAR.bg, fill: textColor }, children: text });
|
|
1014
1552
|
}
|
|
1015
1553
|
const w = labelWidth(text) + 14;
|
|
1016
1554
|
const left = anchor === "start" ? x : anchor === "end" ? x - w : x - w / 2;
|
|
1017
|
-
return /* @__PURE__ */
|
|
1018
|
-
/* @__PURE__ */
|
|
1019
|
-
/* @__PURE__ */
|
|
1555
|
+
return /* @__PURE__ */ jsxs10("g", { children: [
|
|
1556
|
+
/* @__PURE__ */ jsx12("rect", { x: left, y: y - 10, width: w, height: 20, rx: 10, style: { fill: color === VAR.line ? VAR.textStrong : color } }),
|
|
1557
|
+
/* @__PURE__ */ jsx12("text", { x: left + w / 2, y, textAnchor: "middle", dominantBaseline: "central", fontSize: LABEL_FONT, fontWeight: 700, style: { fill: VAR.bg }, children: text })
|
|
1020
1558
|
] });
|
|
1021
1559
|
}
|
|
1022
1560
|
function ReferenceLine({
|
|
@@ -1044,17 +1582,17 @@ function ReferenceLine({
|
|
|
1044
1582
|
const anchor = pos === "left" ? "start" : pos === "center" ? "middle" : "end";
|
|
1045
1583
|
const off = pill ? 13 : 9;
|
|
1046
1584
|
const ly = labelAlign === "center" ? y : labelAlign === "below" ? y + off : y - off;
|
|
1047
|
-
return /* @__PURE__ */
|
|
1048
|
-
/* @__PURE__ */
|
|
1049
|
-
label !== void 0 && /* @__PURE__ */
|
|
1585
|
+
return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
|
|
1586
|
+
/* @__PURE__ */ jsx12("line", { x1: plot.left, x2: plot.right, y1: y, y2: y, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
|
|
1587
|
+
label !== void 0 && /* @__PURE__ */ jsx12(LineLabel, { x: tx + labelDx, y: ly + labelDy, text: label, anchor, color, pill })
|
|
1050
1588
|
] });
|
|
1051
1589
|
}
|
|
1052
1590
|
if (dataX !== void 0) {
|
|
1053
1591
|
const x = ctx.x(dataX);
|
|
1054
1592
|
const pos = labelPosition ?? "top";
|
|
1055
|
-
return /* @__PURE__ */
|
|
1056
|
-
/* @__PURE__ */
|
|
1057
|
-
label !== void 0 && /* @__PURE__ */
|
|
1593
|
+
return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
|
|
1594
|
+
/* @__PURE__ */ jsx12("line", { x1: x, x2: x, y1: plot.top, y2: plot.bottom, strokeWidth, strokeDasharray: dash || void 0, shapeRendering: "crispEdges", style: { stroke: color } }),
|
|
1595
|
+
label !== void 0 && /* @__PURE__ */ jsx12(
|
|
1058
1596
|
LineLabel,
|
|
1059
1597
|
{
|
|
1060
1598
|
x: x + (pill ? 0 : 4) + labelDx,
|
|
@@ -1080,26 +1618,26 @@ function Point({ dataX, dataY, seriesId, yAxisId, color, radius = 4, label, labe
|
|
|
1080
1618
|
const fill = color ?? s?.color ?? ctx.series[0]?.color ?? "currentColor";
|
|
1081
1619
|
const gap = radius + 6;
|
|
1082
1620
|
const [lx, ly, anchor] = labelPosition === "bottom" ? [x, y + gap + 8, "middle"] : labelPosition === "left" ? [x - gap, y + 4, "end"] : labelPosition === "right" ? [x + gap, y + 4, "start"] : [x, y - gap, "middle"];
|
|
1083
|
-
return /* @__PURE__ */
|
|
1084
|
-
pulse && /* @__PURE__ */
|
|
1085
|
-
/* @__PURE__ */
|
|
1086
|
-
/* @__PURE__ */
|
|
1621
|
+
return /* @__PURE__ */ jsxs10("g", { style: onClick ? { cursor: "pointer" } : void 0, onClick, pointerEvents: onClick ? "auto" : "none", children: [
|
|
1622
|
+
pulse && /* @__PURE__ */ jsxs10("circle", { cx: x, cy: y, r: radius, fillOpacity: 0.35, style: { fill }, children: [
|
|
1623
|
+
/* @__PURE__ */ jsx12("animate", { attributeName: "r", values: `${radius};${radius * 3.2}`, dur: "1.6s", repeatCount: "indefinite" }),
|
|
1624
|
+
/* @__PURE__ */ jsx12("animate", { attributeName: "fill-opacity", values: "0.35;0", dur: "1.6s", repeatCount: "indefinite" })
|
|
1087
1625
|
] }),
|
|
1088
|
-
/* @__PURE__ */
|
|
1089
|
-
label !== void 0 && /* @__PURE__ */
|
|
1626
|
+
/* @__PURE__ */ jsx12("circle", { cx: x, cy: y, r: radius, strokeWidth: 2, style: { fill, stroke: VAR.bg } }),
|
|
1627
|
+
label !== void 0 && /* @__PURE__ */ jsx12("text", { opacity: nearScrub(ctx, x) ? 0 : 1, x: lx, y: ly, textAnchor: anchor, fontSize: 11, fontWeight: 600, strokeWidth: 3, strokeLinejoin: "round", paintOrder: "stroke", style: { stroke: VAR.bg, fill }, children: label })
|
|
1090
1628
|
] });
|
|
1091
1629
|
}
|
|
1092
1630
|
|
|
1093
1631
|
// src/viz/period-selector.tsx
|
|
1094
|
-
import { useLayoutEffect, useRef as
|
|
1095
|
-
import { jsx as
|
|
1632
|
+
import { useLayoutEffect, useRef as useRef5, useState as useState6 } from "react";
|
|
1633
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1096
1634
|
function LiveTabLabel({ label, color = "var(--fnv-live, #F04452)" }) {
|
|
1097
|
-
return /* @__PURE__ */
|
|
1098
|
-
/* @__PURE__ */
|
|
1099
|
-
/* @__PURE__ */
|
|
1100
|
-
/* @__PURE__ */
|
|
1101
|
-
/* @__PURE__ */
|
|
1102
|
-
/* @__PURE__ */
|
|
1635
|
+
return /* @__PURE__ */ jsxs11("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
|
|
1636
|
+
/* @__PURE__ */ jsxs11("span", { "aria-hidden": "true", style: { position: "relative", width: 7, height: 7 }, children: [
|
|
1637
|
+
/* @__PURE__ */ jsx13("span", { style: { position: "absolute", inset: 0, borderRadius: "50%", background: color } }),
|
|
1638
|
+
/* @__PURE__ */ jsx13("svg", { width: "7", height: "7", viewBox: "0 0 7 7", style: { position: "absolute", inset: 0, overflow: "visible" }, children: /* @__PURE__ */ jsxs11("circle", { cx: "3.5", cy: "3.5", r: "3.5", style: { fill: color }, fillOpacity: 0.45, children: [
|
|
1639
|
+
/* @__PURE__ */ jsx13("animate", { attributeName: "r", values: "3.5;8", dur: "1.6s", repeatCount: "indefinite" }),
|
|
1640
|
+
/* @__PURE__ */ jsx13("animate", { attributeName: "fill-opacity", values: "0.45;0", dur: "1.6s", repeatCount: "indefinite" })
|
|
1103
1641
|
] }) })
|
|
1104
1642
|
] }),
|
|
1105
1643
|
label
|
|
@@ -1126,8 +1664,8 @@ function PeriodSelector({
|
|
|
1126
1664
|
}) {
|
|
1127
1665
|
const activeIsLive = periods.find((p) => p.id === value)?.live ?? false;
|
|
1128
1666
|
const pillColor = activeIsLive ? liveColor : activeColor;
|
|
1129
|
-
const listRef =
|
|
1130
|
-
const [pill, setPill] =
|
|
1667
|
+
const listRef = useRef5(null);
|
|
1668
|
+
const [pill, setPill] = useState6(null);
|
|
1131
1669
|
useLayoutEffect(() => {
|
|
1132
1670
|
const list = listRef.current;
|
|
1133
1671
|
const active = list?.querySelector('[aria-selected="true"]');
|
|
@@ -1149,7 +1687,7 @@ function PeriodSelector({
|
|
|
1149
1687
|
onChange(p.id);
|
|
1150
1688
|
listRef.current?.querySelector(`[data-period="${CSS.escape(p.id)}"]`)?.focus();
|
|
1151
1689
|
};
|
|
1152
|
-
return /* @__PURE__ */
|
|
1690
|
+
return /* @__PURE__ */ jsxs11(
|
|
1153
1691
|
"div",
|
|
1154
1692
|
{
|
|
1155
1693
|
ref: listRef,
|
|
@@ -1159,7 +1697,7 @@ function PeriodSelector({
|
|
|
1159
1697
|
onKeyDown,
|
|
1160
1698
|
style: { position: "relative", display: fullWidth ? "flex" : "inline-flex", gap: 2, fontFamily: VAR.font, ...style },
|
|
1161
1699
|
children: [
|
|
1162
|
-
pill && /* @__PURE__ */
|
|
1700
|
+
pill && /* @__PURE__ */ jsx13(
|
|
1163
1701
|
"span",
|
|
1164
1702
|
{
|
|
1165
1703
|
"aria-hidden": "true",
|
|
@@ -1178,7 +1716,7 @@ function PeriodSelector({
|
|
|
1178
1716
|
),
|
|
1179
1717
|
periods.map((p) => {
|
|
1180
1718
|
const active = p.id === value;
|
|
1181
|
-
return /* @__PURE__ */
|
|
1719
|
+
return /* @__PURE__ */ jsx13(
|
|
1182
1720
|
"button",
|
|
1183
1721
|
{
|
|
1184
1722
|
type: "button",
|
|
@@ -1202,7 +1740,7 @@ function PeriodSelector({
|
|
|
1202
1740
|
fontWeight: active ? 700 : 500,
|
|
1203
1741
|
cursor: "pointer"
|
|
1204
1742
|
},
|
|
1205
|
-
children: p.live ? /* @__PURE__ */
|
|
1743
|
+
children: p.live ? /* @__PURE__ */ jsx13(LiveTabLabel, { label: p.label, color: liveColor }) : p.label
|
|
1206
1744
|
},
|
|
1207
1745
|
p.id
|
|
1208
1746
|
);
|
|
@@ -1213,13 +1751,13 @@ function PeriodSelector({
|
|
|
1213
1751
|
}
|
|
1214
1752
|
|
|
1215
1753
|
// src/viz/charts.tsx
|
|
1216
|
-
import { percentages, seriesColor as
|
|
1217
|
-
import { useState as
|
|
1218
|
-
import { jsx as
|
|
1219
|
-
var
|
|
1754
|
+
import { percentages, seriesColor as seriesColor5 } from "@funnest/charts-viz";
|
|
1755
|
+
import { useState as useState7 } from "react";
|
|
1756
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1757
|
+
var axisProps2 = (v) => v ? v === true ? {} : v : null;
|
|
1220
1758
|
var axisElements = (x, y) => [
|
|
1221
|
-
y && /* @__PURE__ */
|
|
1222
|
-
x && /* @__PURE__ */
|
|
1759
|
+
y && /* @__PURE__ */ jsx14(YAxis, { ...y }, "y"),
|
|
1760
|
+
x && /* @__PURE__ */ jsx14(XAxis, { ...x }, "x")
|
|
1223
1761
|
];
|
|
1224
1762
|
var defined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
1225
1763
|
var KR_SPLIT = { above: "#F04452", below: "#3182F6" };
|
|
@@ -1238,11 +1776,11 @@ function LineChart({
|
|
|
1238
1776
|
children,
|
|
1239
1777
|
...chart
|
|
1240
1778
|
}) {
|
|
1241
|
-
const sc =
|
|
1779
|
+
const sc = axisProps2(scrubber);
|
|
1242
1780
|
const split = baseline !== void 0 ? splitColors ?? KR_SPLIT : void 0;
|
|
1243
|
-
return /* @__PURE__ */
|
|
1244
|
-
axisElements(
|
|
1245
|
-
chart.series.map((s) => /* @__PURE__ */
|
|
1781
|
+
return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
|
|
1782
|
+
axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
|
|
1783
|
+
chart.series.map((s) => /* @__PURE__ */ jsx14(
|
|
1246
1784
|
Line,
|
|
1247
1785
|
{
|
|
1248
1786
|
seriesId: s.id,
|
|
@@ -1252,7 +1790,7 @@ function LineChart({
|
|
|
1252
1790
|
s.id
|
|
1253
1791
|
)),
|
|
1254
1792
|
children,
|
|
1255
|
-
sc && /* @__PURE__ */
|
|
1793
|
+
sc && /* @__PURE__ */ jsx14(
|
|
1256
1794
|
Scrubber,
|
|
1257
1795
|
{
|
|
1258
1796
|
...split && baseline !== void 0 ? { beaconColor: (v) => v >= baseline ? split.above : split.below } : {},
|
|
@@ -1277,12 +1815,12 @@ function AreaChart({
|
|
|
1277
1815
|
children,
|
|
1278
1816
|
...chart
|
|
1279
1817
|
}) {
|
|
1280
|
-
const sc =
|
|
1281
|
-
return /* @__PURE__ */
|
|
1282
|
-
axisElements(
|
|
1818
|
+
const sc = axisProps2(scrubber);
|
|
1819
|
+
return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, ...chart, children: [
|
|
1820
|
+
axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
|
|
1283
1821
|
chart.series.map((s) => {
|
|
1284
1822
|
const own = s.areaStyle === "none" ? null : s.areaStyle ?? (chart.stacked && fill === "gradient" ? "solid" : fill);
|
|
1285
|
-
return own ? /* @__PURE__ */
|
|
1823
|
+
return own ? /* @__PURE__ */ jsx14(
|
|
1286
1824
|
Area,
|
|
1287
1825
|
{
|
|
1288
1826
|
seriesId: s.id,
|
|
@@ -1293,16 +1831,16 @@ function AreaChart({
|
|
|
1293
1831
|
`a${s.id}`
|
|
1294
1832
|
) : null;
|
|
1295
1833
|
}),
|
|
1296
|
-
showLines && chart.series.map((s) => /* @__PURE__ */
|
|
1834
|
+
showLines && chart.series.map((s) => /* @__PURE__ */ jsx14(Line, { seriesId: s.id, curve, strokeWidth, ...defined({ lineStyle, baseline, splitColors }) }, `l${s.id}`)),
|
|
1297
1835
|
children,
|
|
1298
|
-
sc && /* @__PURE__ */
|
|
1836
|
+
sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
|
|
1299
1837
|
] });
|
|
1300
1838
|
}
|
|
1301
1839
|
function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGap, stackGap, roundBaseline, barMinSize, layout = "vertical", xAxis, children, ...chart }) {
|
|
1302
|
-
const sc =
|
|
1840
|
+
const sc = axisProps2(scrubber);
|
|
1303
1841
|
if (layout === "horizontal") {
|
|
1304
|
-
const yAx =
|
|
1305
|
-
return /* @__PURE__ */
|
|
1842
|
+
const yAx = axisProps2(showYAxis);
|
|
1843
|
+
return /* @__PURE__ */ jsx14(
|
|
1306
1844
|
HorizontalBarChart,
|
|
1307
1845
|
{
|
|
1308
1846
|
series: chart.series,
|
|
@@ -1329,11 +1867,11 @@ function BarChart({ showXAxis, showYAxis, scrubber, radius, getBarColor, groupGa
|
|
|
1329
1867
|
}
|
|
1330
1868
|
);
|
|
1331
1869
|
}
|
|
1332
|
-
return /* @__PURE__ */
|
|
1333
|
-
axisElements(
|
|
1334
|
-
/* @__PURE__ */
|
|
1870
|
+
return /* @__PURE__ */ jsxs12(CartesianChart, { enableScrubbing: !!sc, xAxis: { ...xAxis, scaleType: "band" }, ...chart, children: [
|
|
1871
|
+
axisElements(axisProps2(showXAxis), axisProps2(showYAxis)),
|
|
1872
|
+
/* @__PURE__ */ jsx14(BarPlot, { ...defined({ radius, getBarColor, groupGap, stackGap, roundBaseline, minSize: barMinSize }), highlightScrubbed: !!sc }),
|
|
1335
1873
|
children,
|
|
1336
|
-
sc && /* @__PURE__ */
|
|
1874
|
+
sc && /* @__PURE__ */ jsx14(Scrubber, { ...sc })
|
|
1337
1875
|
] });
|
|
1338
1876
|
}
|
|
1339
1877
|
var pct = (_item, f) => `${formatNumber(Math.round(f * 1e3) / 10)}%`;
|
|
@@ -1366,11 +1904,11 @@ function PercentageBarChart({
|
|
|
1366
1904
|
for (const r of allRows)
|
|
1367
1905
|
for (const d of r.data)
|
|
1368
1906
|
if (!colorOf.has(d.id)) {
|
|
1369
|
-
colorOf.set(d.id, d.color ??
|
|
1907
|
+
colorOf.set(d.id, d.color ?? seriesColor5(colorOf.size, palette));
|
|
1370
1908
|
legendItems.push(d);
|
|
1371
1909
|
}
|
|
1372
|
-
const [hovered, setHovered] =
|
|
1373
|
-
const [hoverRow, setHoverRow] =
|
|
1910
|
+
const [hovered, setHovered] = useState7(null);
|
|
1911
|
+
const [hoverRow, setHoverRow] = useState7(null);
|
|
1374
1912
|
const rowSegs = allRows.map((r) => {
|
|
1375
1913
|
const segs = percentages(r.data);
|
|
1376
1914
|
return { row: r, items: r.data.map((d, i) => ({ ...d, color: colorOf.get(d.id), fraction: segs[i].fraction })) };
|
|
@@ -1378,7 +1916,7 @@ function PercentageBarChart({
|
|
|
1378
1916
|
const renderBar = (items, rowId) => {
|
|
1379
1917
|
const visible = items.filter((i) => i.fraction > 0);
|
|
1380
1918
|
const ordered = vertical ? [...visible].reverse() : visible;
|
|
1381
|
-
return /* @__PURE__ */
|
|
1919
|
+
return /* @__PURE__ */ jsx14(
|
|
1382
1920
|
"div",
|
|
1383
1921
|
{
|
|
1384
1922
|
style: {
|
|
@@ -1390,7 +1928,7 @@ function PercentageBarChart({
|
|
|
1390
1928
|
children: ordered.map((i, k) => {
|
|
1391
1929
|
const a = k === 0 ? end : inner;
|
|
1392
1930
|
const b = k === ordered.length - 1 ? end : inner;
|
|
1393
|
-
return /* @__PURE__ */
|
|
1931
|
+
return /* @__PURE__ */ jsx14(
|
|
1394
1932
|
"span",
|
|
1395
1933
|
{
|
|
1396
1934
|
title: `${i.label} ${formatLabel(i, i.fraction)}`,
|
|
@@ -1419,7 +1957,7 @@ function PercentageBarChart({
|
|
|
1419
1957
|
);
|
|
1420
1958
|
};
|
|
1421
1959
|
const legendRow = multi ? rowSegs.find((r) => r.row.id === hoverRow) : rowSegs[0];
|
|
1422
|
-
const legend = showLabels && /* @__PURE__ */
|
|
1960
|
+
const legend = showLabels && /* @__PURE__ */ jsx14(
|
|
1423
1961
|
Legend,
|
|
1424
1962
|
{
|
|
1425
1963
|
style: legendAt === "top" ? { marginBottom: 10 } : legendAt === "bottom" ? { marginTop: 10 } : {},
|
|
@@ -1438,15 +1976,15 @@ function PercentageBarChart({
|
|
|
1438
1976
|
onItemHover: setHovered
|
|
1439
1977
|
}
|
|
1440
1978
|
);
|
|
1441
|
-
const label = (r) => multi && r.label ? /* @__PURE__ */
|
|
1442
|
-
const bars = vertical ? /* @__PURE__ */
|
|
1979
|
+
const label = (r) => multi && r.label ? /* @__PURE__ */ jsx14("span", { style: { fontSize: 12, color: hoverRow === r.id ? VAR.textStrong : VAR.text, fontWeight: hoverRow === r.id ? 700 : 500, whiteSpace: "nowrap" }, children: r.label }) : null;
|
|
1980
|
+
const bars = vertical ? /* @__PURE__ */ jsx14("div", { style: { display: "flex", gap: 14, alignItems: "flex-end" }, children: rowSegs.map(({ row, items }) => /* @__PURE__ */ jsxs12("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }, children: [
|
|
1443
1981
|
renderBar(items, row.id),
|
|
1444
1982
|
label(row)
|
|
1445
|
-
] }, row.id)) }) : multi ? /* @__PURE__ */
|
|
1446
|
-
label(row) ?? /* @__PURE__ */
|
|
1983
|
+
] }, row.id)) }) : multi ? /* @__PURE__ */ jsx14("div", { style: { display: "grid", gridTemplateColumns: "max-content 1fr", columnGap: 12, rowGap: 10, alignItems: "center", flex: legendAt === "right" ? 1 : void 0 }, children: rowSegs.map(({ row, items }) => /* @__PURE__ */ jsxs12("div", { style: { display: "contents" }, children: [
|
|
1984
|
+
label(row) ?? /* @__PURE__ */ jsx14("span", {}),
|
|
1447
1985
|
renderBar(items, row.id)
|
|
1448
|
-
] }, row.id)) }) : /* @__PURE__ */
|
|
1449
|
-
return /* @__PURE__ */
|
|
1986
|
+
] }, row.id)) }) : /* @__PURE__ */ jsx14("div", { style: { display: "flex", flex: legendAt === "right" ? 1 : void 0 }, children: renderBar(rowSegs[0].items, rowSegs[0].row.id) });
|
|
1987
|
+
return /* @__PURE__ */ jsxs12(
|
|
1450
1988
|
"div",
|
|
1451
1989
|
{
|
|
1452
1990
|
className,
|
|
@@ -1466,8 +2004,11 @@ export {
|
|
|
1466
2004
|
AreaChart,
|
|
1467
2005
|
BarChart,
|
|
1468
2006
|
BarPlot,
|
|
2007
|
+
CandlePlot,
|
|
2008
|
+
CandlestickChart,
|
|
1469
2009
|
CartesianChart,
|
|
1470
2010
|
DEFAULT_PERIODS,
|
|
2011
|
+
DonutChart,
|
|
1471
2012
|
FnChart,
|
|
1472
2013
|
FnChartSlot,
|
|
1473
2014
|
HorizontalBarChart,
|
|
@@ -1480,6 +2021,7 @@ export {
|
|
|
1480
2021
|
Point,
|
|
1481
2022
|
ReferenceLine,
|
|
1482
2023
|
Scrubber,
|
|
2024
|
+
TreemapChart,
|
|
1483
2025
|
XAxis,
|
|
1484
2026
|
YAxis,
|
|
1485
2027
|
formatNumber,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type XAxisProps, type YAxisProps } from './axes';
|
|
3
|
+
import { type CartesianChartProps } from './cartesian-chart';
|
|
4
|
+
export interface Candle {
|
|
5
|
+
open: number;
|
|
6
|
+
high: number;
|
|
7
|
+
low: number;
|
|
8
|
+
close: number;
|
|
9
|
+
volume?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface CandlePlotProps {
|
|
12
|
+
data: readonly (Candle | null)[];
|
|
13
|
+
/** Axis the prices are on (when the chart has several). */
|
|
14
|
+
yAxisId?: string;
|
|
15
|
+
/** Default '#F04452' (Korean red-up). */
|
|
16
|
+
upColor?: string;
|
|
17
|
+
/** Default '#3182F6'. */
|
|
18
|
+
downColor?: string;
|
|
19
|
+
/** Body width as a share of the slot. Default 0.7. */
|
|
20
|
+
bodyWidth?: number;
|
|
21
|
+
/** Hollow bodies for up candles. Default false. */
|
|
22
|
+
hollowUp?: boolean;
|
|
23
|
+
/** Pointer line + OHLC tooltip while scrubbing. Default true. */
|
|
24
|
+
tooltip?: boolean;
|
|
25
|
+
/** Tooltip row names. Default 시가/고가/저가/종가 (Korean) — pass English if needed. */
|
|
26
|
+
labels?: {
|
|
27
|
+
open: string;
|
|
28
|
+
high: string;
|
|
29
|
+
low: string;
|
|
30
|
+
close: string;
|
|
31
|
+
volume?: string;
|
|
32
|
+
};
|
|
33
|
+
valueFormatter?: (value: number) => string;
|
|
34
|
+
}
|
|
35
|
+
/** Candles (wick + body) for each x slot. Needs a band x axis and a y axis that covers low–high. */
|
|
36
|
+
export declare function CandlePlot({ data, yAxisId, upColor, downColor, bodyWidth, hollowUp, tooltip, labels, valueFormatter, }: CandlePlotProps): import("react").JSX.Element | null;
|
|
37
|
+
type AxisToggle<P> = boolean | P;
|
|
38
|
+
export interface CandlestickChartProps extends Omit<CartesianChartProps, 'series' | 'yAxis' | 'stacked' | 'children' | 'legend'>, Pick<CandlePlotProps, 'upColor' | 'downColor' | 'bodyWidth' | 'hollowUp' | 'labels' | 'valueFormatter'> {
|
|
39
|
+
data: readonly (Candle | null)[];
|
|
40
|
+
/** Volume bars along the bottom (from `data[i].volume`). Default true when volumes exist. */
|
|
41
|
+
showVolume?: boolean;
|
|
42
|
+
/** Share of the height the volume bars may use. Default 0.22. */
|
|
43
|
+
volumeHeight?: number;
|
|
44
|
+
/** Default true (right side). */
|
|
45
|
+
showYAxis?: AxisToggle<YAxisProps>;
|
|
46
|
+
showXAxis?: AxisToggle<XAxisProps>;
|
|
47
|
+
/** Pointer line + OHLC tooltip. Default true. */
|
|
48
|
+
tooltip?: boolean;
|
|
49
|
+
/** ReferenceLine (use `yAxisId="price"`), Point, … */
|
|
50
|
+
children?: ReactNode;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A small candlestick chart in SVG for cards and dashboards (the full trading chart is `FnChart`).
|
|
54
|
+
* Prices sit on axis "price", volume on axis "volume".
|
|
55
|
+
*/
|
|
56
|
+
export declare function CandlestickChart({ data, showVolume, volumeHeight, showYAxis, showXAxis, tooltip, upColor, downColor, bodyWidth, hollowUp, labels, valueFormatter, xAxis, children, getScrubberAccessibilityLabel, ...chart }: CandlestickChartProps): import("react").JSX.Element;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
import { type LegendShape } from './legend';
|
|
3
|
+
export interface DonutItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
value: number;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DonutCenter {
|
|
10
|
+
/** Big text in the middle. */
|
|
11
|
+
value: string;
|
|
12
|
+
/** Small text under it. */
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DonutChartProps {
|
|
16
|
+
data: readonly DonutItem[];
|
|
17
|
+
/** Diameter, px. Default 180. */
|
|
18
|
+
size?: number;
|
|
19
|
+
/** Hole size as a share of the radius, 0–0.95. 0 draws a pie. Default 0.68. */
|
|
20
|
+
innerRadius?: number;
|
|
21
|
+
/** Gap between segments, degrees. Default 1.5. */
|
|
22
|
+
padAngle?: number;
|
|
23
|
+
/** Rounded segment corners, px. Default 3. */
|
|
24
|
+
cornerRadius?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Text in the hole. Default: the hovered item's share and label, otherwise the total.
|
|
27
|
+
* Return null to leave the hole empty.
|
|
28
|
+
*/
|
|
29
|
+
center?: (active: {
|
|
30
|
+
item: DonutItem;
|
|
31
|
+
fraction: number;
|
|
32
|
+
} | null, total: number) => DonutCenter | ReactNode | null;
|
|
33
|
+
/** Label under the total when nothing is hovered (e.g. "총 자산"). */
|
|
34
|
+
totalLabel?: string;
|
|
35
|
+
valueFormatter?: (value: number) => string;
|
|
36
|
+
/** Default true. */
|
|
37
|
+
showLegend?: boolean;
|
|
38
|
+
/** Default 'right'. */
|
|
39
|
+
legendPosition?: 'right' | 'bottom';
|
|
40
|
+
legendShape?: LegendShape;
|
|
41
|
+
/** Legend values. Default: the share ("42.0%"). */
|
|
42
|
+
formatLabel?: (item: DonutItem, fraction: number) => string;
|
|
43
|
+
/** Controlled highlight (id) — e.g. synced with a table row. */
|
|
44
|
+
activeId?: string | null;
|
|
45
|
+
onActiveChange?: (id: string | null) => void;
|
|
46
|
+
palette?: readonly string[];
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: CSSProperties;
|
|
49
|
+
'aria-label'?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Ring (or pie) of shares: portfolio by asset, sector weights, account mix.
|
|
53
|
+
* Hover or arrow keys highlight a segment; the hole shows its share.
|
|
54
|
+
*/
|
|
55
|
+
export declare function DonutChart({ data, size, innerRadius, padAngle, cornerRadius, center, totalLabel, valueFormatter, showLegend, legendPosition, legendShape, formatLabel, activeId, onActiveChange, palette, className, style, 'aria-label': ariaLabel, }: DonutChartProps): import("react").JSX.Element;
|
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.3.0",
|
|
4
4
|
"description": "React bindings for Funnest Charts: the trading widget plus SVG line, area and bar charts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"NOTICE"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@funnest/charts": "0.
|
|
50
|
-
"@funnest/charts-
|
|
51
|
-
"@funnest/charts-
|
|
49
|
+
"@funnest/charts": "0.3.0",
|
|
50
|
+
"@funnest/charts-ui": "0.3.0",
|
|
51
|
+
"@funnest/charts-viz": "0.3.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"react": ">=18",
|