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