@epoch32/visage 0.0.0-stage → 0.0.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/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/bundle.dev.js +4875 -0
- package/dist/index.js +9633 -0
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/dist/vite/index.js +707 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Child } from "../dom";
|
|
2
|
+
import { useChartContext } from "./context";
|
|
3
|
+
import { pathArc } from "./path";
|
|
4
|
+
|
|
5
|
+
export interface PieProps {
|
|
6
|
+
dataKey: string;
|
|
7
|
+
nameKey?: string;
|
|
8
|
+
innerRadius?: number;
|
|
9
|
+
outerRadius?: number;
|
|
10
|
+
colors?: string[];
|
|
11
|
+
cx?: number;
|
|
12
|
+
cy?: number;
|
|
13
|
+
padAngle?: number; // radians
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_PALETTE = [
|
|
17
|
+
"#8b5cf6", // Purple
|
|
18
|
+
"#ec4899", // Pink
|
|
19
|
+
"#3b82f6", // Blue
|
|
20
|
+
"#10b981", // Emerald
|
|
21
|
+
"#f59e0b", // Amber
|
|
22
|
+
"#6366f1", // Indigo
|
|
23
|
+
"#14b8a6", // Teal
|
|
24
|
+
"#ef4444" // Red
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function Pie(props: PieProps): Child {
|
|
28
|
+
return () => {
|
|
29
|
+
const ctx = useChartContext();
|
|
30
|
+
|
|
31
|
+
const innerRadius = props.innerRadius ?? 0;
|
|
32
|
+
const outerRadius = props.outerRadius ?? 80;
|
|
33
|
+
const colors = props.colors ?? DEFAULT_PALETTE;
|
|
34
|
+
const padAngle = props.padAngle ?? 0.02;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<g class="visage-pie">
|
|
38
|
+
{() => {
|
|
39
|
+
const dataArr = ctx.data();
|
|
40
|
+
if (dataArr.length === 0) return null;
|
|
41
|
+
|
|
42
|
+
const w = ctx.innerWidth();
|
|
43
|
+
const h = ctx.innerHeight();
|
|
44
|
+
const centerX = props.cx ?? w / 2;
|
|
45
|
+
const centerY = props.cy ?? h / 2;
|
|
46
|
+
|
|
47
|
+
let total = 0;
|
|
48
|
+
for (const item of dataArr) {
|
|
49
|
+
const val = Number(item[props.dataKey]) || 0;
|
|
50
|
+
total += val;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (total === 0) return null;
|
|
54
|
+
|
|
55
|
+
const slices: Child[] = [];
|
|
56
|
+
let currentAngle = -Math.PI / 2; // start from 12 o'clock
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < dataArr.length; i++) {
|
|
59
|
+
const item = dataArr[i];
|
|
60
|
+
const val = Number(item[props.dataKey]) || 0;
|
|
61
|
+
const sliceAngle = (val / total) * (2 * Math.PI);
|
|
62
|
+
const startAngle = currentAngle + (val > 0 ? padAngle / 2 : 0);
|
|
63
|
+
const endAngle = currentAngle + sliceAngle - (val > 0 ? padAngle / 2 : 0);
|
|
64
|
+
|
|
65
|
+
currentAngle += sliceAngle;
|
|
66
|
+
|
|
67
|
+
if (val <= 0 || endAngle <= startAngle) continue;
|
|
68
|
+
|
|
69
|
+
const d = pathArc({
|
|
70
|
+
cx: centerX,
|
|
71
|
+
cy: centerY,
|
|
72
|
+
innerRadius,
|
|
73
|
+
outerRadius,
|
|
74
|
+
startAngle,
|
|
75
|
+
endAngle
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const fill = colors[i % colors.length];
|
|
79
|
+
|
|
80
|
+
slices.push(<path d={d} fill={fill} stroke="#ffffff" stroke-width={1.5} />);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return slices;
|
|
84
|
+
}}
|
|
85
|
+
</g>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Child } from "../dom";
|
|
2
|
+
import type { ClassProp } from "../jsx-runtime/jsx";
|
|
3
|
+
import { useChartContext } from "./context";
|
|
4
|
+
import type { SeriesMeta } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface TooltipProps {
|
|
7
|
+
format?: (val: any, name: string) => string;
|
|
8
|
+
labelFormat?: (label: any) => string;
|
|
9
|
+
content?: (datum: any, series: SeriesMeta[]) => Child;
|
|
10
|
+
class?: ClassProp;
|
|
11
|
+
className?: ClassProp;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ChartTooltip(props: TooltipProps): Child {
|
|
15
|
+
const render = () => {
|
|
16
|
+
const ctx = useChartContext();
|
|
17
|
+
const hover = ctx.hoverState();
|
|
18
|
+
if (!hover.active || !hover.datum) return null;
|
|
19
|
+
|
|
20
|
+
const w = ctx.width();
|
|
21
|
+
const datum = hover.datum;
|
|
22
|
+
const series = ctx.series();
|
|
23
|
+
const xKey = ctx.xDataKey();
|
|
24
|
+
|
|
25
|
+
// Avoid tooltip escaping the right edge
|
|
26
|
+
const cardWidth = 160;
|
|
27
|
+
let left = hover.x + 14;
|
|
28
|
+
if (left + cardWidth > w) {
|
|
29
|
+
left = Math.max(10, hover.x - cardWidth - 14);
|
|
30
|
+
}
|
|
31
|
+
const top = Math.max(10, hover.y - 30);
|
|
32
|
+
|
|
33
|
+
if (props.content) {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
style={{
|
|
37
|
+
position: "absolute",
|
|
38
|
+
left: `${left}px`,
|
|
39
|
+
top: `${top}px`,
|
|
40
|
+
pointerEvents: "none",
|
|
41
|
+
zIndex: 50
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{props.content(datum, series)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const label = xKey && datum[xKey] !== undefined ? datum[xKey] : `#${hover.index + 1}`;
|
|
50
|
+
const formattedLabel = props.labelFormat ? props.labelFormat(label) : String(label);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
class={props.class || props.className}
|
|
55
|
+
style={{
|
|
56
|
+
position: "absolute",
|
|
57
|
+
left: `${left}px`,
|
|
58
|
+
top: `${top}px`,
|
|
59
|
+
pointerEvents: "none",
|
|
60
|
+
zIndex: 50,
|
|
61
|
+
minWidth: `${cardWidth}px`,
|
|
62
|
+
padding: "8px 12px",
|
|
63
|
+
borderRadius: "10px",
|
|
64
|
+
background: "rgba(255, 255, 255, 0.92)",
|
|
65
|
+
backdropFilter: "blur(12px)",
|
|
66
|
+
WebkitBackdropFilter: "blur(12px)",
|
|
67
|
+
border: "1px solid rgba(226, 232, 240, 0.8)",
|
|
68
|
+
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.05)",
|
|
69
|
+
fontSize: "12px",
|
|
70
|
+
color: "#1e293b",
|
|
71
|
+
lineHeight: "1.4"
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<div
|
|
75
|
+
style={{
|
|
76
|
+
fontWeight: "600",
|
|
77
|
+
color: "#475569",
|
|
78
|
+
marginBottom: "6px",
|
|
79
|
+
borderBottom: "1px solid #f1f5f9",
|
|
80
|
+
paddingBottom: "4px"
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{formattedLabel}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
87
|
+
{series.map((s) => {
|
|
88
|
+
const rawVal = datum[s.dataKey];
|
|
89
|
+
if (rawVal === undefined || rawVal === null) return null;
|
|
90
|
+
|
|
91
|
+
const valStr = props.format
|
|
92
|
+
? props.format(rawVal, s.name ?? s.dataKey)
|
|
93
|
+
: String(rawVal);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
style={{
|
|
98
|
+
display: "flex",
|
|
99
|
+
alignItems: "center",
|
|
100
|
+
justifyContent: "space-between",
|
|
101
|
+
gap: "8px"
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
105
|
+
<span
|
|
106
|
+
style={{
|
|
107
|
+
width: "8px",
|
|
108
|
+
height: "8px",
|
|
109
|
+
borderRadius: "50%",
|
|
110
|
+
background: s.color ?? "#8b5cf6"
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
<span style={{ color: "#64748b" }}>{s.name ?? s.dataKey}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<span style={{ fontWeight: "600", color: "#0f172a" }}>{valStr}</span>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
(render as any).__visage_overlay = true;
|
|
124
|
+
return render;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const Tooltip = ChartTooltip;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createContext, useContext } from "../signals";
|
|
2
|
+
import type { AnyScale, HoverState, LinearScale, Margin, SeriesMeta } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface ChartContextValue {
|
|
5
|
+
data: () => any[];
|
|
6
|
+
width: () => number;
|
|
7
|
+
height: () => number;
|
|
8
|
+
margin: Margin;
|
|
9
|
+
innerWidth: () => number;
|
|
10
|
+
innerHeight: () => number;
|
|
11
|
+
xScale: () => AnyScale;
|
|
12
|
+
yScale: () => LinearScale;
|
|
13
|
+
hoverState: () => HoverState;
|
|
14
|
+
setHoverState: (state: HoverState) => void;
|
|
15
|
+
xDataKey: () => string | undefined;
|
|
16
|
+
setXDataKey: (key: string) => void;
|
|
17
|
+
series: () => SeriesMeta[];
|
|
18
|
+
registerSeries: (meta: SeriesMeta) => void;
|
|
19
|
+
unregisterSeries: (id: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ChartContext = createContext<ChartContextValue | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
export function useChartContext(): ChartContextValue {
|
|
25
|
+
const ctx = useContext(ChartContext);
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
throw new Error("Visage Chart child component must be rendered within a <Chart> container.");
|
|
28
|
+
}
|
|
29
|
+
return ctx;
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./Area";
|
|
2
|
+
export * from "./Axis";
|
|
3
|
+
export * from "./Bar";
|
|
4
|
+
export * from "./CartesianGrid";
|
|
5
|
+
export * from "./Chart";
|
|
6
|
+
export * from "./Cursor";
|
|
7
|
+
export * from "./context";
|
|
8
|
+
export * from "./Legend";
|
|
9
|
+
export * from "./Line";
|
|
10
|
+
export * from "./Pie";
|
|
11
|
+
export * from "./path";
|
|
12
|
+
export * from "./scale";
|
|
13
|
+
export * from "./Tooltip";
|
|
14
|
+
export * from "./types";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { CurveType, Point } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface PathLineOptions {
|
|
4
|
+
curve?: CurveType;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates an SVG path `d` string representing a polyline or smooth spline curve.
|
|
9
|
+
*/
|
|
10
|
+
export function pathLine(points: Point[], options: PathLineOptions = {}): string {
|
|
11
|
+
if (points.length === 0) return "";
|
|
12
|
+
if (points.length === 1) {
|
|
13
|
+
const p = points[0]!;
|
|
14
|
+
return `M ${p.x.toFixed(2)},${p.y.toFixed(2)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const curve = options.curve ?? "linear";
|
|
18
|
+
|
|
19
|
+
if (curve === "smooth" && points.length >= 2) {
|
|
20
|
+
if (points.length === 2) {
|
|
21
|
+
const p0 = points[0]!;
|
|
22
|
+
const p1 = points[1]!;
|
|
23
|
+
return `M ${p0.x.toFixed(2)},${p0.y.toFixed(2)} L ${p1.x.toFixed(2)},${p1.y.toFixed(2)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const n = points.length;
|
|
27
|
+
const dx: number[] = [];
|
|
28
|
+
const dy: number[] = [];
|
|
29
|
+
const slopes: number[] = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < n - 1; i++) {
|
|
32
|
+
const pA = points[i]!;
|
|
33
|
+
const pB = points[i + 1]!;
|
|
34
|
+
const deltaX = pB.x - pA.x;
|
|
35
|
+
const deltaY = pB.y - pA.y;
|
|
36
|
+
dx.push(deltaX);
|
|
37
|
+
dy.push(deltaY);
|
|
38
|
+
slopes.push(deltaX === 0 ? 0 : deltaY / deltaX);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const m: number[] = new Array(n).fill(0);
|
|
42
|
+
m[0] = slopes[0]!;
|
|
43
|
+
m[n - 1] = slopes[n - 2]!;
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i < n - 1; i++) {
|
|
46
|
+
const sPrev = slopes[i - 1]!;
|
|
47
|
+
const sNext = slopes[i]!;
|
|
48
|
+
if (sPrev * sNext <= 0) {
|
|
49
|
+
m[i] = 0;
|
|
50
|
+
} else {
|
|
51
|
+
const dxPrev = dx[i - 1]!;
|
|
52
|
+
const dxNext = dx[i]!;
|
|
53
|
+
const common = dxPrev + dxNext;
|
|
54
|
+
m[i] =
|
|
55
|
+
common === 0 ? 0 : (3 * common) / ((common + dxNext) / sPrev + (common + dxPrev) / sNext);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let d = `M ${points[0]!.x.toFixed(2)},${points[0]!.y.toFixed(2)}`;
|
|
60
|
+
for (let i = 0; i < n - 1; i++) {
|
|
61
|
+
const p1 = points[i]!;
|
|
62
|
+
const p2 = points[i + 1]!;
|
|
63
|
+
const deltaX = dx[i]!;
|
|
64
|
+
const cp1x = p1.x + deltaX / 3;
|
|
65
|
+
const cp1y = p1.y + (m[i]! * deltaX) / 3;
|
|
66
|
+
const cp2x = p2.x - deltaX / 3;
|
|
67
|
+
const cp2y = p2.y - (m[i + 1]! * deltaX) / 3;
|
|
68
|
+
|
|
69
|
+
d += ` C ${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(2)},${cp2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
|
|
70
|
+
}
|
|
71
|
+
return d;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (curve === "step") {
|
|
75
|
+
let d = `M ${points[0]!.x.toFixed(2)},${points[0]!.y.toFixed(2)}`;
|
|
76
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
77
|
+
const p1 = points[i]!;
|
|
78
|
+
const p2 = points[i + 1]!;
|
|
79
|
+
const midX = (p1.x + p2.x) / 2;
|
|
80
|
+
d += ` L ${midX.toFixed(2)},${p1.y.toFixed(2)} L ${midX.toFixed(2)},${p2.y.toFixed(2)} L ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
|
|
81
|
+
}
|
|
82
|
+
return d;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Linear
|
|
86
|
+
let d = `M ${points[0]!.x.toFixed(2)},${points[0]!.y.toFixed(2)}`;
|
|
87
|
+
for (let i = 1; i < points.length; i++) {
|
|
88
|
+
d += ` L ${points[i]!.x.toFixed(2)},${points[i]!.y.toFixed(2)}`;
|
|
89
|
+
}
|
|
90
|
+
return d;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface PathAreaOptions {
|
|
94
|
+
curve?: CurveType;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generates an SVG path `d` string for a closed area polygon, anchored to a baseline Y coordinate.
|
|
99
|
+
*/
|
|
100
|
+
export function pathArea(
|
|
101
|
+
points: Point[],
|
|
102
|
+
baselineY: number,
|
|
103
|
+
options: PathAreaOptions = {}
|
|
104
|
+
): string {
|
|
105
|
+
if (points.length === 0) return "";
|
|
106
|
+
|
|
107
|
+
const lineD = pathLine(points, options);
|
|
108
|
+
if (!lineD) return "";
|
|
109
|
+
|
|
110
|
+
const first = points[0]!;
|
|
111
|
+
const last = points[points.length - 1]!;
|
|
112
|
+
|
|
113
|
+
return `${lineD} L ${last.x.toFixed(2)},${baselineY.toFixed(2)} L ${first.x.toFixed(2)},${baselineY.toFixed(2)} Z`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ArcOptions {
|
|
117
|
+
cx: number;
|
|
118
|
+
cy: number;
|
|
119
|
+
innerRadius: number;
|
|
120
|
+
outerRadius: number;
|
|
121
|
+
startAngle: number; // in radians
|
|
122
|
+
endAngle: number; // in radians
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generates an SVG arc path string for a pie or donut slice.
|
|
127
|
+
*/
|
|
128
|
+
export function pathArc(options: ArcOptions): string {
|
|
129
|
+
const { cx, cy, innerRadius, outerRadius, startAngle, endAngle } = options;
|
|
130
|
+
|
|
131
|
+
const cosStart = Math.cos(startAngle);
|
|
132
|
+
const sinStart = Math.sin(startAngle);
|
|
133
|
+
const cosEnd = Math.cos(endAngle);
|
|
134
|
+
const sinEnd = Math.sin(endAngle);
|
|
135
|
+
|
|
136
|
+
const x0 = cx + outerRadius * cosStart;
|
|
137
|
+
const y0 = cy + outerRadius * sinStart;
|
|
138
|
+
const x1 = cx + outerRadius * cosEnd;
|
|
139
|
+
const y1 = cy + outerRadius * sinEnd;
|
|
140
|
+
|
|
141
|
+
const largeArcFlag = endAngle - startAngle <= Math.PI ? 0 : 1;
|
|
142
|
+
|
|
143
|
+
if (innerRadius <= 0) {
|
|
144
|
+
// Pie slice (wedge down to center)
|
|
145
|
+
return `M ${cx.toFixed(2)},${cy.toFixed(2)} L ${x0.toFixed(2)},${y0.toFixed(2)} A ${outerRadius.toFixed(2)},${outerRadius.toFixed(2)} 0 ${largeArcFlag} 1 ${x1.toFixed(2)},${y1.toFixed(2)} Z`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Donut slice (arc from outer down to inner arc and back)
|
|
149
|
+
const x2 = cx + innerRadius * cosEnd;
|
|
150
|
+
const y2 = cy + innerRadius * sinEnd;
|
|
151
|
+
const x3 = cx + innerRadius * cosStart;
|
|
152
|
+
const y3 = cy + innerRadius * sinStart;
|
|
153
|
+
|
|
154
|
+
return `M ${x0.toFixed(2)},${y0.toFixed(2)} A ${outerRadius.toFixed(2)},${outerRadius.toFixed(2)} 0 ${largeArcFlag} 1 ${x1.toFixed(2)},${y1.toFixed(2)} L ${x2.toFixed(2)},${y2.toFixed(2)} A ${innerRadius.toFixed(2)},${innerRadius.toFixed(2)} 0 ${largeArcFlag} 0 ${x3.toFixed(2)},${y3.toFixed(2)} Z`;
|
|
155
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { BandScale, LinearScale } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculates human-friendly rounded tick marks across a numerical interval.
|
|
5
|
+
* Uses the standard Wilkinson-style step rounding algorithm.
|
|
6
|
+
*/
|
|
7
|
+
export function generateNiceTicks(min: number, max: number, count = 5): number[] {
|
|
8
|
+
if (min === max) return [min];
|
|
9
|
+
if (count <= 0) return [];
|
|
10
|
+
|
|
11
|
+
const span = max - min;
|
|
12
|
+
const rawStep = span / count;
|
|
13
|
+
const magnitude = 10 ** Math.floor(Math.log10(rawStep));
|
|
14
|
+
const err = rawStep / magnitude;
|
|
15
|
+
|
|
16
|
+
let step: number;
|
|
17
|
+
if (err >= 7.5) {
|
|
18
|
+
step = 10 * magnitude;
|
|
19
|
+
} else if (err >= 3.5) {
|
|
20
|
+
step = 5 * magnitude;
|
|
21
|
+
} else if (err >= 1.5) {
|
|
22
|
+
step = 2 * magnitude;
|
|
23
|
+
} else {
|
|
24
|
+
step = 1 * magnitude;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const start = Math.floor(min / step) * step;
|
|
28
|
+
const end = Math.ceil(max / step) * step;
|
|
29
|
+
const ticks: number[] = [];
|
|
30
|
+
|
|
31
|
+
for (let current = start; current <= end + step * 0.5; current += step) {
|
|
32
|
+
// Avoid IEEE 754 precision noise (e.g. 0.30000000000000004)
|
|
33
|
+
const rounded = Number(current.toPrecision(12));
|
|
34
|
+
ticks.push(rounded);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return ticks;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LinearScaleOptions {
|
|
41
|
+
clamp?: boolean;
|
|
42
|
+
nice?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a continuous linear mathematical scale mapping input domain to SVG pixel range.
|
|
47
|
+
*/
|
|
48
|
+
export function createLinearScale(
|
|
49
|
+
domain: [number, number],
|
|
50
|
+
range: [number, number],
|
|
51
|
+
options: LinearScaleOptions = {}
|
|
52
|
+
): LinearScale {
|
|
53
|
+
let [d0, d1] = domain;
|
|
54
|
+
const [r0, r1] = range;
|
|
55
|
+
|
|
56
|
+
if (options.nice && d0 !== d1) {
|
|
57
|
+
const ticks = generateNiceTicks(d0, d1, 5);
|
|
58
|
+
if (ticks.length > 0) {
|
|
59
|
+
d0 = Math.min(d0, ticks[0]!);
|
|
60
|
+
d1 = Math.max(d1, ticks[ticks.length - 1]!);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const fn = ((val: number): number => {
|
|
65
|
+
let v = val;
|
|
66
|
+
if (options.clamp) {
|
|
67
|
+
const minD = Math.min(d0, d1);
|
|
68
|
+
const maxD = Math.max(d0, d1);
|
|
69
|
+
v = Math.max(minD, Math.min(maxD, v));
|
|
70
|
+
}
|
|
71
|
+
const ratio = d1 === d0 ? 0 : (v - d0) / (d1 - d0);
|
|
72
|
+
return r0 + ratio * (r1 - r0);
|
|
73
|
+
}) as LinearScale;
|
|
74
|
+
|
|
75
|
+
fn.type = "linear";
|
|
76
|
+
fn.domain = [d0, d1];
|
|
77
|
+
fn.range = [r0, r1];
|
|
78
|
+
|
|
79
|
+
fn.invert = (pixel: number): number => {
|
|
80
|
+
const ratio = r1 === r0 ? 0 : (pixel - r0) / (r1 - r0);
|
|
81
|
+
return d0 + ratio * (d1 - d0);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
fn.ticks = (count = 5): number[] => {
|
|
85
|
+
return generateNiceTicks(d0, d1, count);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return fn;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BandScaleOptions {
|
|
92
|
+
paddingInner?: number;
|
|
93
|
+
paddingOuter?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates an ordinal band scale mapping categorical strings (e.g. months, labels)
|
|
98
|
+
* to partitioned SVG pixel bands (used by Bar and column charts).
|
|
99
|
+
*/
|
|
100
|
+
export function createBandScale(
|
|
101
|
+
domain: any[],
|
|
102
|
+
range: [number, number],
|
|
103
|
+
options: BandScaleOptions = {}
|
|
104
|
+
): BandScale {
|
|
105
|
+
const paddingInner = options.paddingInner ?? 0.2;
|
|
106
|
+
const paddingOuter = options.paddingOuter ?? 0.1;
|
|
107
|
+
|
|
108
|
+
const n = domain.length;
|
|
109
|
+
const totalSteps = Math.max(1, n - paddingInner + 2 * paddingOuter);
|
|
110
|
+
const step = (range[1] - range[0]) / totalSteps;
|
|
111
|
+
const bandwidth = step * (1 - paddingInner);
|
|
112
|
+
|
|
113
|
+
const fn = ((val: any): number => {
|
|
114
|
+
let idx = -1;
|
|
115
|
+
if (typeof val === "number" && Number.isInteger(val) && val >= 0 && val < domain.length) {
|
|
116
|
+
idx = val;
|
|
117
|
+
} else {
|
|
118
|
+
idx = domain.indexOf(val);
|
|
119
|
+
}
|
|
120
|
+
if (idx === -1) return range[0];
|
|
121
|
+
return range[0] + (idx + paddingOuter) * step;
|
|
122
|
+
}) as BandScale;
|
|
123
|
+
|
|
124
|
+
fn.type = "band";
|
|
125
|
+
fn.domain = [...domain];
|
|
126
|
+
fn.range = [range[0], range[1]];
|
|
127
|
+
|
|
128
|
+
fn.bandwidth = () => bandwidth;
|
|
129
|
+
fn.step = () => step;
|
|
130
|
+
fn.ticks = () => [...domain];
|
|
131
|
+
|
|
132
|
+
return fn;
|
|
133
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface Margin {
|
|
2
|
+
top: number;
|
|
3
|
+
right: number;
|
|
4
|
+
bottom: number;
|
|
5
|
+
left: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CurveType = "linear" | "smooth" | "step";
|
|
9
|
+
|
|
10
|
+
export interface Point {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LinearScale {
|
|
16
|
+
type: "linear";
|
|
17
|
+
(val: number): number;
|
|
18
|
+
domain: [number, number];
|
|
19
|
+
range: [number, number];
|
|
20
|
+
invert: (pixel: number) => number;
|
|
21
|
+
ticks: (count?: number) => number[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BandScale {
|
|
25
|
+
type: "band";
|
|
26
|
+
(val: any): number;
|
|
27
|
+
domain: any[];
|
|
28
|
+
range: [number, number];
|
|
29
|
+
bandwidth: () => number;
|
|
30
|
+
step: () => number;
|
|
31
|
+
ticks: () => any[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AnyScale = LinearScale | BandScale;
|
|
35
|
+
|
|
36
|
+
export interface HoverState {
|
|
37
|
+
active: boolean;
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
index: number;
|
|
41
|
+
datum?: any;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SeriesMeta {
|
|
45
|
+
id: string;
|
|
46
|
+
dataKey: string;
|
|
47
|
+
type: "line" | "area" | "bar";
|
|
48
|
+
color?: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
}
|