@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,86 @@
|
|
|
1
|
+
import type { Child } from "../dom";
|
|
2
|
+
import { onCleanup } from "../signals";
|
|
3
|
+
import { useChartContext } from "./context";
|
|
4
|
+
|
|
5
|
+
export interface BarProps {
|
|
6
|
+
dataKey: string;
|
|
7
|
+
fill?: string;
|
|
8
|
+
radius?: number;
|
|
9
|
+
maxWidth?: number;
|
|
10
|
+
name?: string;
|
|
11
|
+
opacity?: number | string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Bar(props: BarProps): Child {
|
|
15
|
+
return () => {
|
|
16
|
+
const ctx = useChartContext();
|
|
17
|
+
|
|
18
|
+
const fill = props.fill ?? "#6366f1";
|
|
19
|
+
const radius = props.radius ?? 4;
|
|
20
|
+
const id = `bar-${props.dataKey}-${Math.random().toString(36).slice(2, 7)}`;
|
|
21
|
+
|
|
22
|
+
ctx.registerSeries({
|
|
23
|
+
id,
|
|
24
|
+
dataKey: props.dataKey,
|
|
25
|
+
type: "bar",
|
|
26
|
+
color: fill,
|
|
27
|
+
name: props.name ?? props.dataKey
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
onCleanup(() => {
|
|
31
|
+
ctx.unregisterSeries(id);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<g class={`visage-bar-${props.dataKey}`} opacity={props.opacity ?? 1}>
|
|
36
|
+
{() => {
|
|
37
|
+
const dataArr = ctx.data();
|
|
38
|
+
if (dataArr.length === 0) return null;
|
|
39
|
+
|
|
40
|
+
const xScale = ctx.xScale();
|
|
41
|
+
const yScale = ctx.yScale();
|
|
42
|
+
const baselineY = ctx.innerHeight();
|
|
43
|
+
|
|
44
|
+
// Check if there are other Bar series to calculate grouped offsets
|
|
45
|
+
const allBars = ctx.series().filter((s) => s.type === "bar");
|
|
46
|
+
const barIndex = allBars.findIndex((s) => s.id === id);
|
|
47
|
+
const totalBars = Math.max(1, allBars.length);
|
|
48
|
+
|
|
49
|
+
const bars: Child[] = [];
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < dataArr.length; i++) {
|
|
52
|
+
const item = dataArr[i];
|
|
53
|
+
const val = item[props.dataKey];
|
|
54
|
+
if (typeof val !== "number" || Number.isNaN(val)) continue;
|
|
55
|
+
|
|
56
|
+
let slotX = 0;
|
|
57
|
+
let slotW = 20;
|
|
58
|
+
|
|
59
|
+
if (xScale.type === "band") {
|
|
60
|
+
slotX = xScale(i);
|
|
61
|
+
slotW = xScale.bandwidth();
|
|
62
|
+
} else {
|
|
63
|
+
const stepW = ctx.innerWidth() / Math.max(1, dataArr.length);
|
|
64
|
+
slotX = xScale(i) - stepW / 2;
|
|
65
|
+
slotW = stepW * 0.8;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Grouped bar width
|
|
69
|
+
const subBarW = slotW / totalBars;
|
|
70
|
+
const barW = props.maxWidth ? Math.min(props.maxWidth, subBarW) : subBarW;
|
|
71
|
+
const barX = slotX + (barIndex >= 0 ? barIndex * subBarW : 0) + (subBarW - barW) / 2;
|
|
72
|
+
|
|
73
|
+
const y = yScale(val);
|
|
74
|
+
const h = Math.max(0, baselineY - y);
|
|
75
|
+
|
|
76
|
+
bars.push(
|
|
77
|
+
<rect x={barX} y={y} width={barW} height={h} rx={radius} ry={radius} fill={fill} />
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return bars;
|
|
82
|
+
}}
|
|
83
|
+
</g>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Child } from "../dom";
|
|
2
|
+
import { useChartContext } from "./context";
|
|
3
|
+
|
|
4
|
+
export interface CartesianGridProps {
|
|
5
|
+
horizontal?: boolean;
|
|
6
|
+
vertical?: boolean;
|
|
7
|
+
stroke?: string;
|
|
8
|
+
strokeWidth?: number | string;
|
|
9
|
+
strokeDasharray?: string;
|
|
10
|
+
opacity?: number | string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CartesianGrid(props: CartesianGridProps): Child {
|
|
14
|
+
return () => {
|
|
15
|
+
const ctx = useChartContext();
|
|
16
|
+
|
|
17
|
+
const horizontal = props.horizontal ?? true;
|
|
18
|
+
const vertical = props.vertical ?? false;
|
|
19
|
+
const stroke = props.stroke ?? "#e2e8f0";
|
|
20
|
+
const strokeWidth = props.strokeWidth ?? 1;
|
|
21
|
+
const strokeDasharray = props.strokeDasharray;
|
|
22
|
+
const opacity = props.opacity ?? 0.8;
|
|
23
|
+
|
|
24
|
+
const lines: Child[] = [];
|
|
25
|
+
const w = ctx.innerWidth();
|
|
26
|
+
const h = ctx.innerHeight();
|
|
27
|
+
const yScale = ctx.yScale();
|
|
28
|
+
const xScale = ctx.xScale();
|
|
29
|
+
|
|
30
|
+
if (horizontal) {
|
|
31
|
+
const ticks = yScale.ticks(5);
|
|
32
|
+
for (const t of ticks) {
|
|
33
|
+
const y = yScale(t);
|
|
34
|
+
lines.push(
|
|
35
|
+
<line
|
|
36
|
+
x1={0}
|
|
37
|
+
y1={y}
|
|
38
|
+
x2={w}
|
|
39
|
+
y2={y}
|
|
40
|
+
stroke={stroke}
|
|
41
|
+
stroke-width={strokeWidth}
|
|
42
|
+
stroke-dasharray={strokeDasharray}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (vertical) {
|
|
49
|
+
const ticks = xScale.ticks(5);
|
|
50
|
+
for (const t of ticks) {
|
|
51
|
+
const x = xScale(t);
|
|
52
|
+
lines.push(
|
|
53
|
+
<line
|
|
54
|
+
x1={x}
|
|
55
|
+
y1={0}
|
|
56
|
+
x2={x}
|
|
57
|
+
y2={h}
|
|
58
|
+
stroke={stroke}
|
|
59
|
+
stroke-width={strokeWidth}
|
|
60
|
+
stroke-dasharray={strokeDasharray}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<g class="visage-cartesian-grid" opacity={opacity}>
|
|
68
|
+
{lines}
|
|
69
|
+
</g>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { type Child, resolveValue } from "../dom";
|
|
2
|
+
import type { ClassProp, ReactiveProp } from "../jsx-runtime/jsx";
|
|
3
|
+
import { computed, onCleanup, signal, untrack } from "../signals";
|
|
4
|
+
import { ChartContext, type ChartContextValue } from "./context";
|
|
5
|
+
import { createBandScale, createLinearScale } from "./scale";
|
|
6
|
+
import type { AnyScale, HoverState, Margin, SeriesMeta } from "./types";
|
|
7
|
+
|
|
8
|
+
export interface ChartProps {
|
|
9
|
+
data: ReactiveProp<any[]>;
|
|
10
|
+
width?: number | string;
|
|
11
|
+
height?: number | string;
|
|
12
|
+
margin?: Partial<Margin>;
|
|
13
|
+
yDomain?: [number, number];
|
|
14
|
+
class?: ClassProp;
|
|
15
|
+
className?: ClassProp;
|
|
16
|
+
style?: any;
|
|
17
|
+
children?: Child;
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_MARGIN: Margin = {
|
|
22
|
+
top: 16,
|
|
23
|
+
right: 16,
|
|
24
|
+
bottom: 24,
|
|
25
|
+
left: 36
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function Chart(props: ChartProps): Child {
|
|
29
|
+
const margin: Margin = {
|
|
30
|
+
...DEFAULT_MARGIN,
|
|
31
|
+
...props.margin
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const initialW = typeof props.width === "number" ? props.width : 500;
|
|
35
|
+
const initialH = typeof props.height === "number" ? props.height : 300;
|
|
36
|
+
|
|
37
|
+
const measuredWidth = signal<number>(initialW);
|
|
38
|
+
const measuredHeight = signal<number>(initialH);
|
|
39
|
+
|
|
40
|
+
let containerEl: HTMLElement | null = null;
|
|
41
|
+
|
|
42
|
+
const handleRef = (el: HTMLElement) => {
|
|
43
|
+
containerEl = el;
|
|
44
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
45
|
+
const ro = new ResizeObserver((entries) => {
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const cr = entry.contentRect;
|
|
48
|
+
if (cr.width > 0) measuredWidth.set(cr.width);
|
|
49
|
+
if (cr.height > 0) measuredHeight.set(cr.height);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
ro.observe(el);
|
|
53
|
+
onCleanup(() => ro.disconnect());
|
|
54
|
+
} else {
|
|
55
|
+
if (el.clientWidth > 0) measuredWidth.set(el.clientWidth);
|
|
56
|
+
if (el.clientHeight > 0) measuredHeight.set(el.clientHeight);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const innerWidth = computed(() => Math.max(10, measuredWidth() - margin.left - margin.right));
|
|
61
|
+
const innerHeight = computed(() => Math.max(10, measuredHeight() - margin.top - margin.bottom));
|
|
62
|
+
|
|
63
|
+
const rawData = () => resolveValue(props.data) || [];
|
|
64
|
+
|
|
65
|
+
const seriesList = signal<SeriesMeta[]>([]);
|
|
66
|
+
const xDataKey = signal<string | undefined>(undefined);
|
|
67
|
+
|
|
68
|
+
const registerSeries = (meta: SeriesMeta) => {
|
|
69
|
+
untrack(() => {
|
|
70
|
+
seriesList.update((prev) => {
|
|
71
|
+
if (prev.some((s) => s.id === meta.id)) return prev;
|
|
72
|
+
return [...prev, meta];
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const unregisterSeries = (id: string) => {
|
|
78
|
+
untrack(() => {
|
|
79
|
+
seriesList.update((prev) => prev.filter((s) => s.id !== id));
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const hoverState = signal<HoverState>({
|
|
84
|
+
active: false,
|
|
85
|
+
x: 0,
|
|
86
|
+
y: 0,
|
|
87
|
+
index: -1
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const xScale = computed<AnyScale>(() => {
|
|
91
|
+
const dataArr = rawData();
|
|
92
|
+
const w = innerWidth();
|
|
93
|
+
const key = xDataKey();
|
|
94
|
+
|
|
95
|
+
const hasBarSeries = untrack(() => seriesList().some((s) => s.type === "bar"));
|
|
96
|
+
|
|
97
|
+
if (hasBarSeries && key && dataArr.length > 0) {
|
|
98
|
+
const categories = dataArr.map((d: any) => String(d[key]));
|
|
99
|
+
return createBandScale(categories, [0, w], {
|
|
100
|
+
paddingInner: 0.1,
|
|
101
|
+
paddingOuter: 0.05
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const maxIdx = Math.max(1, dataArr.length - 1);
|
|
106
|
+
return createLinearScale([0, maxIdx], [0, w]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const yScale = computed(() => {
|
|
110
|
+
const h = innerHeight();
|
|
111
|
+
const dataArr = rawData();
|
|
112
|
+
const currentSeries = seriesList();
|
|
113
|
+
|
|
114
|
+
let minY = 0;
|
|
115
|
+
let maxY = 10;
|
|
116
|
+
|
|
117
|
+
if (props.yDomain) {
|
|
118
|
+
minY = props.yDomain[0];
|
|
119
|
+
maxY = props.yDomain[1];
|
|
120
|
+
} else if (dataArr.length > 0 && currentSeries.length > 0) {
|
|
121
|
+
let foundMin = Infinity;
|
|
122
|
+
let foundMax = -Infinity;
|
|
123
|
+
|
|
124
|
+
for (const item of dataArr) {
|
|
125
|
+
for (const s of currentSeries) {
|
|
126
|
+
const val = item[s.dataKey];
|
|
127
|
+
if (typeof val === "number" && !Number.isNaN(val)) {
|
|
128
|
+
if (val < foundMin) foundMin = val;
|
|
129
|
+
if (val > foundMax) foundMax = val;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (foundMax > -Infinity) {
|
|
135
|
+
minY = Math.min(0, foundMin);
|
|
136
|
+
maxY = foundMax === 0 ? 1 : foundMax * 1.1; // 10% headroom
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// SVG coordinate space: 0 is top, innerHeight is bottom
|
|
141
|
+
return createLinearScale([minY, maxY], [h, 0], { nice: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const handlePointer = (clientX: number, clientY: number) => {
|
|
145
|
+
if (!containerEl) return;
|
|
146
|
+
const rect = containerEl.getBoundingClientRect();
|
|
147
|
+
const relX = clientX - rect.left - margin.left;
|
|
148
|
+
const relY = clientY - rect.top - margin.top;
|
|
149
|
+
|
|
150
|
+
const w = innerWidth();
|
|
151
|
+
const h = innerHeight();
|
|
152
|
+
|
|
153
|
+
if (relX < 0 || relX > w || relY < 0 || relY > h) {
|
|
154
|
+
hoverState.set({ active: false, x: 0, y: 0, index: -1 });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const dataArr = rawData();
|
|
159
|
+
if (dataArr.length === 0) return;
|
|
160
|
+
|
|
161
|
+
const count = dataArr.length;
|
|
162
|
+
const stepW = w / Math.max(1, count - 1);
|
|
163
|
+
const rawIdx = Math.round(relX / stepW);
|
|
164
|
+
const clampedIdx = Math.max(0, Math.min(count - 1, rawIdx));
|
|
165
|
+
|
|
166
|
+
hoverState.set({
|
|
167
|
+
active: true,
|
|
168
|
+
x: relX + margin.left,
|
|
169
|
+
y: relY + margin.top,
|
|
170
|
+
index: clampedIdx,
|
|
171
|
+
datum: dataArr[clampedIdx]
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
176
|
+
handlePointer(e.clientX, e.clientY);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const handleMouseLeave = () => {
|
|
180
|
+
hoverState.set({ active: false, x: 0, y: 0, index: -1 });
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleTouchMove = (e: TouchEvent) => {
|
|
184
|
+
const touch = e.touches[0];
|
|
185
|
+
if (touch) handlePointer(touch.clientX, touch.clientY);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleTouchEnd = () => {
|
|
189
|
+
hoverState.set({ active: false, x: 0, y: 0, index: -1 });
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const ctxValue: ChartContextValue = {
|
|
193
|
+
data: rawData,
|
|
194
|
+
width: () => measuredWidth(),
|
|
195
|
+
height: () => measuredHeight(),
|
|
196
|
+
margin,
|
|
197
|
+
innerWidth: () => innerWidth(),
|
|
198
|
+
innerHeight: () => innerHeight(),
|
|
199
|
+
xScale: () => xScale(),
|
|
200
|
+
yScale: () => yScale(),
|
|
201
|
+
hoverState: () => hoverState(),
|
|
202
|
+
setHoverState: (s) => hoverState.set(s),
|
|
203
|
+
xDataKey: () => xDataKey(),
|
|
204
|
+
setXDataKey: (k) => xDataKey.set(k),
|
|
205
|
+
series: () => seriesList(),
|
|
206
|
+
registerSeries,
|
|
207
|
+
unregisterSeries
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const containerStyle = {
|
|
211
|
+
position: "relative",
|
|
212
|
+
width: typeof props.width === "number" ? `${props.width}px` : (props.width ?? "100%"),
|
|
213
|
+
height: typeof props.height === "number" ? `${props.height}px` : (props.height ?? "300px"),
|
|
214
|
+
touchAction: "none",
|
|
215
|
+
userSelect: "none",
|
|
216
|
+
...props.style
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const rawChildren = Array.isArray(props.children) ? props.children : [props.children];
|
|
220
|
+
const childrenList: any[] = [];
|
|
221
|
+
const stack = [...rawChildren];
|
|
222
|
+
while (stack.length > 0) {
|
|
223
|
+
const item = stack.shift();
|
|
224
|
+
if (Array.isArray(item)) {
|
|
225
|
+
stack.unshift(...item);
|
|
226
|
+
} else if (item !== null && item !== undefined && item !== false) {
|
|
227
|
+
childrenList.push(item);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const svgChildren: Child[] = [];
|
|
232
|
+
const overlayChildren: Child[] = [];
|
|
233
|
+
|
|
234
|
+
for (const child of childrenList) {
|
|
235
|
+
if (typeof child === "function" && (child as any).__visage_overlay) {
|
|
236
|
+
overlayChildren.push(child);
|
|
237
|
+
} else {
|
|
238
|
+
svgChildren.push(child);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return ChartContext.Provider({
|
|
243
|
+
value: ctxValue,
|
|
244
|
+
children: () => (
|
|
245
|
+
<div
|
|
246
|
+
ref={handleRef}
|
|
247
|
+
class={props.class || props.className}
|
|
248
|
+
style={containerStyle}
|
|
249
|
+
onMouseMove={handleMouseMove}
|
|
250
|
+
onMouseLeave={handleMouseLeave}
|
|
251
|
+
onTouchMove={handleTouchMove}
|
|
252
|
+
onTouchEnd={handleTouchEnd}
|
|
253
|
+
>
|
|
254
|
+
<svg
|
|
255
|
+
role="img"
|
|
256
|
+
aria-label={props["aria-label"] ?? "Data visualization chart"}
|
|
257
|
+
viewBox={() => `0 0 ${measuredWidth()} ${measuredHeight()}`}
|
|
258
|
+
width="100%"
|
|
259
|
+
height="100%"
|
|
260
|
+
style={{ overflow: "visible", display: "block" }}
|
|
261
|
+
>
|
|
262
|
+
<title>{props.title ?? "Data Visualization Chart"}</title>
|
|
263
|
+
<g transform={`translate(${margin.left}, ${margin.top})`}>{svgChildren}</g>
|
|
264
|
+
</svg>
|
|
265
|
+
{overlayChildren}
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Child } from "../dom";
|
|
2
|
+
import { useChartContext } from "./context";
|
|
3
|
+
|
|
4
|
+
export interface ChartCursorProps {
|
|
5
|
+
stroke?: string;
|
|
6
|
+
strokeWidth?: number | string;
|
|
7
|
+
strokeDasharray?: string;
|
|
8
|
+
opacity?: number | string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ChartCursor(props: ChartCursorProps): Child {
|
|
12
|
+
return () => {
|
|
13
|
+
const ctx = useChartContext();
|
|
14
|
+
|
|
15
|
+
const stroke = props.stroke ?? "#94a3b8";
|
|
16
|
+
const strokeWidth = props.strokeWidth ?? 1;
|
|
17
|
+
const strokeDasharray = props.strokeDasharray ?? "4 4";
|
|
18
|
+
const opacity = props.opacity ?? 0.8;
|
|
19
|
+
|
|
20
|
+
const hover = ctx.hoverState();
|
|
21
|
+
if (!hover.active) return null;
|
|
22
|
+
|
|
23
|
+
const h = ctx.innerHeight();
|
|
24
|
+
// hover.x is relative to container, subtract margin.left for group coordinate space
|
|
25
|
+
const x = hover.x - ctx.margin.left;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<g class="visage-chart-cursor">
|
|
29
|
+
<line
|
|
30
|
+
x1={x}
|
|
31
|
+
y1={0}
|
|
32
|
+
x2={x}
|
|
33
|
+
y2={h}
|
|
34
|
+
stroke={stroke}
|
|
35
|
+
stroke-width={strokeWidth}
|
|
36
|
+
stroke-dasharray={strokeDasharray}
|
|
37
|
+
opacity={opacity}
|
|
38
|
+
/>
|
|
39
|
+
</g>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Child } from "../dom";
|
|
2
|
+
import type { ClassProp } from "../jsx-runtime/jsx";
|
|
3
|
+
import { useChartContext } from "./context";
|
|
4
|
+
|
|
5
|
+
export interface LegendProps {
|
|
6
|
+
align?: "left" | "center" | "right";
|
|
7
|
+
class?: ClassProp;
|
|
8
|
+
className?: ClassProp;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ChartLegend(props: LegendProps): Child {
|
|
12
|
+
const render = () => {
|
|
13
|
+
const ctx = useChartContext();
|
|
14
|
+
const align = props.align ?? "center";
|
|
15
|
+
|
|
16
|
+
const justifyMap = {
|
|
17
|
+
left: "flex-start",
|
|
18
|
+
center: "center",
|
|
19
|
+
right: "flex-end"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const series = ctx.series();
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
class={props.class || props.className}
|
|
26
|
+
style={{
|
|
27
|
+
display: "flex",
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
justifyContent: justifyMap[align],
|
|
30
|
+
gap: "16px",
|
|
31
|
+
paddingTop: "8px",
|
|
32
|
+
fontSize: "12px",
|
|
33
|
+
color: "#475569"
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{series.map((s) => (
|
|
37
|
+
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
38
|
+
<span
|
|
39
|
+
style={{
|
|
40
|
+
width: "10px",
|
|
41
|
+
height: "10px",
|
|
42
|
+
borderRadius: "3px",
|
|
43
|
+
background: s.color ?? "#8b5cf6"
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
<span>{s.name ?? s.dataKey}</span>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
(render as any).__visage_overlay = true;
|
|
53
|
+
return render;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Legend = ChartLegend;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Child } from "../dom";
|
|
2
|
+
import { onCleanup } from "../signals";
|
|
3
|
+
import { useChartContext } from "./context";
|
|
4
|
+
import { pathLine } from "./path";
|
|
5
|
+
import type { CurveType, Point } from "./types";
|
|
6
|
+
|
|
7
|
+
export interface LineProps {
|
|
8
|
+
dataKey: string;
|
|
9
|
+
stroke?: string;
|
|
10
|
+
strokeWidth?: number | string;
|
|
11
|
+
strokeDasharray?: string;
|
|
12
|
+
curve?: CurveType;
|
|
13
|
+
dot?: boolean | { r?: number; fill?: string; stroke?: string; strokeWidth?: number };
|
|
14
|
+
name?: string;
|
|
15
|
+
opacity?: number | string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Line(props: LineProps): Child {
|
|
19
|
+
return () => {
|
|
20
|
+
const ctx = useChartContext();
|
|
21
|
+
|
|
22
|
+
const stroke = props.stroke ?? "#8b5cf6";
|
|
23
|
+
const strokeWidth = props.strokeWidth ?? 2;
|
|
24
|
+
const curve = props.curve ?? "linear";
|
|
25
|
+
const id = `line-${props.dataKey}-${Math.random().toString(36).slice(2, 7)}`;
|
|
26
|
+
|
|
27
|
+
ctx.registerSeries({
|
|
28
|
+
id,
|
|
29
|
+
dataKey: props.dataKey,
|
|
30
|
+
type: "line",
|
|
31
|
+
color: stroke,
|
|
32
|
+
name: props.name ?? props.dataKey
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
onCleanup(() => {
|
|
36
|
+
ctx.unregisterSeries(id);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<g class={`visage-line-${props.dataKey}`} opacity={props.opacity ?? 1}>
|
|
41
|
+
{() => {
|
|
42
|
+
const dataArr = ctx.data();
|
|
43
|
+
if (dataArr.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const xScale = ctx.xScale();
|
|
46
|
+
const yScale = ctx.yScale();
|
|
47
|
+
|
|
48
|
+
const points: Point[] = [];
|
|
49
|
+
for (let i = 0; i < dataArr.length; i++) {
|
|
50
|
+
const item = dataArr[i];
|
|
51
|
+
const val = item[props.dataKey];
|
|
52
|
+
if (typeof val !== "number" || Number.isNaN(val)) continue;
|
|
53
|
+
|
|
54
|
+
let x = 0;
|
|
55
|
+
if (xScale.type === "band") {
|
|
56
|
+
x = xScale(i) + xScale.bandwidth() / 2;
|
|
57
|
+
} else {
|
|
58
|
+
x = xScale(i);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const y = yScale(val);
|
|
62
|
+
points.push({ x, y });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const d = pathLine(points, { curve });
|
|
66
|
+
|
|
67
|
+
const dots: Child[] = [];
|
|
68
|
+
if (props.dot && points.length > 0) {
|
|
69
|
+
const dotCfg = typeof props.dot === "object" ? props.dot : {};
|
|
70
|
+
const r = dotCfg.r ?? 3.5;
|
|
71
|
+
const fill = dotCfg.fill ?? "#ffffff";
|
|
72
|
+
const dotStroke = dotCfg.stroke ?? stroke;
|
|
73
|
+
const dotSw = dotCfg.strokeWidth ?? 2;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < points.length; i++) {
|
|
76
|
+
const p = points[i]!;
|
|
77
|
+
dots.push(
|
|
78
|
+
<circle
|
|
79
|
+
cx={p.x}
|
|
80
|
+
cy={p.y}
|
|
81
|
+
r={r}
|
|
82
|
+
fill={fill}
|
|
83
|
+
stroke={dotStroke}
|
|
84
|
+
stroke-width={dotSw}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
<path
|
|
92
|
+
d={d}
|
|
93
|
+
fill="none"
|
|
94
|
+
stroke={stroke}
|
|
95
|
+
stroke-width={strokeWidth}
|
|
96
|
+
stroke-dasharray={props.strokeDasharray}
|
|
97
|
+
stroke-linejoin="round"
|
|
98
|
+
stroke-linecap="round"
|
|
99
|
+
/>,
|
|
100
|
+
...dots
|
|
101
|
+
];
|
|
102
|
+
}}
|
|
103
|
+
</g>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
}
|