waffle-charts-cli 0.1.0 → 0.1.2
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/README.md +59 -0
- package/package.json +4 -3
- package/src/registry.js +10 -0
- package/templates/CompositeChart.tsx +247 -0
- package/templates/SankeyChart.tsx +188 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# WaffleCharts CLI 🧇
|
|
2
|
+
|
|
3
|
+
The official CLI for [WaffleCharts](https://mbuchthal.github.io/waffle-charts).
|
|
4
|
+
|
|
5
|
+
> **Beautiful, Headless, Copy-Pasteable Charts for React.**
|
|
6
|
+
> Built with [Visx](https://airbnb.io/visx) and [Tailwind CSS](https://tailwindcss.com).
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/waffle-charts-cli)
|
|
9
|
+
[](https://github.com/mbuchthal/waffle-charts/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
## Why WaffleCharts?
|
|
12
|
+
|
|
13
|
+
WaffleCharts is not a component library you install and get stuck with. It's a collection of **templates**.
|
|
14
|
+
The CLI copies the full source code of the chart directly into your project. You own the code. You can customize the axis, the colors, the tooltip logic—everything.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Run the `add` command to interactively select charts:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx waffle-charts-cli add
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or add specific charts directly:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx waffle-charts-cli add bar-chart line-chart
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Available Charts
|
|
31
|
+
|
|
32
|
+
| Chart | Command | Description |
|
|
33
|
+
| :--- | :--- | :--- |
|
|
34
|
+
| **Bar Chart** | `bar-chart` | Standard vertical bar chart for categorical data. |
|
|
35
|
+
| **Line Chart** | `line-chart` | Smooth curves for trends over time. |
|
|
36
|
+
| **Area Chart** | `area-chart` | Filled area charts for volume visualization. |
|
|
37
|
+
| **Pie Chart** | `pie-chart` | Donut or Pie charts for proportions. |
|
|
38
|
+
| **Radar Chart** | `radar-chart` | Spider/Web chart for multi-variable comparison. |
|
|
39
|
+
| **Scatter Plot** | `scatter-chart` | X/Y plotting for correlation. |
|
|
40
|
+
| **Bubble Chart** | `bubble-chart` | 3D data visualization (X, Y, Radius). |
|
|
41
|
+
| **Heatmap** | `heatmap-chart` | Grid-based density visualization. |
|
|
42
|
+
| **Treemap** | `treemap-chart` | Hierarchical data visualization. |
|
|
43
|
+
| **Sankey** | `sankey-chart` | Flow and process visualization. |
|
|
44
|
+
| **Composite** | `composite-chart` | **New!** Dual-axis Bar + Line combination. |
|
|
45
|
+
|
|
46
|
+
## What it does
|
|
47
|
+
|
|
48
|
+
1. **Checks dependencies**: Detects if you have the necessary `@visx` packages (`shape`, `scale`, `axis`, etc.) and installs them if missing.
|
|
49
|
+
2. **Copies code**: Downloads the component source code to `src/components/waffle/` (or your configured directory).
|
|
50
|
+
3. **Zero Lock-in**: Once added, the code is yours.
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- React 16+
|
|
55
|
+
- Tailwind CSS configured in your project.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT © [WaffleCharts](https://github.com/mbuchthal/waffle-charts)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waffle-charts-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "CLI to add WaffleCharts components to your project",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"bin",
|
|
26
26
|
"src",
|
|
27
|
-
"templates"
|
|
27
|
+
"templates",
|
|
28
|
+
"README.md"
|
|
28
29
|
],
|
|
29
30
|
"publishConfig": {
|
|
30
31
|
"access": "public"
|
|
@@ -37,4 +38,4 @@
|
|
|
37
38
|
"prompts": "^2.4.2"
|
|
38
39
|
},
|
|
39
40
|
"type": "module"
|
|
40
|
-
}
|
|
41
|
+
}
|
package/src/registry.js
CHANGED
|
@@ -43,5 +43,15 @@ export const registry = {
|
|
|
43
43
|
file: "ScatterChart.tsx",
|
|
44
44
|
label: "Scatter Chart",
|
|
45
45
|
dependencies: ["@visx/shape", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/scale", "@visx/axis", "@visx/grid", "@visx/glyph", "clsx", "tailwind-merge"],
|
|
46
|
+
},
|
|
47
|
+
"sankey-chart": {
|
|
48
|
+
file: "SankeyChart.tsx",
|
|
49
|
+
label: "Sankey Chart",
|
|
50
|
+
dependencies: ["@visx/sankey", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/scale", "clsx", "tailwind-merge"],
|
|
51
|
+
},
|
|
52
|
+
"composite-chart": {
|
|
53
|
+
file: "CompositeChart.tsx",
|
|
54
|
+
label: "Composite Chart",
|
|
55
|
+
dependencies: ["@visx/shape", "@visx/scale", "@visx/axis", "@visx/grid", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/curve", "@visx/event", "clsx", "tailwind-merge"],
|
|
46
56
|
}
|
|
47
57
|
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Group } from '@visx/group';
|
|
2
|
+
import { Bar, LinePath } from '@visx/shape';
|
|
3
|
+
import { scaleBand, scaleLinear } from '@visx/scale';
|
|
4
|
+
import { AxisLeft, AxisRight, AxisBottom } from '@visx/axis';
|
|
5
|
+
import { GridRows } from '@visx/grid';
|
|
6
|
+
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
|
|
7
|
+
import { ParentSize } from '@visx/responsive';
|
|
8
|
+
import { curveMonotoneX } from '@visx/curve';
|
|
9
|
+
import { localPoint } from '@visx/event';
|
|
10
|
+
import { cn } from '../../lib/utils';
|
|
11
|
+
import React, { useMemo } from 'react';
|
|
12
|
+
|
|
13
|
+
// Types
|
|
14
|
+
export type CompositeChartProps<T> = {
|
|
15
|
+
data: T[];
|
|
16
|
+
xKey: keyof T;
|
|
17
|
+
barKey: keyof T;
|
|
18
|
+
lineKey: keyof T;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
className?: string;
|
|
22
|
+
barColor?: string;
|
|
23
|
+
lineColor?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CompositeChartContentProps<T> = CompositeChartProps<T> & {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function CompositeChartContent<T>({
|
|
32
|
+
data,
|
|
33
|
+
xKey,
|
|
34
|
+
barKey,
|
|
35
|
+
lineKey,
|
|
36
|
+
width,
|
|
37
|
+
height,
|
|
38
|
+
className,
|
|
39
|
+
barColor = '#3b82f6', // blue-500
|
|
40
|
+
lineColor = '#ef4444', // red-500
|
|
41
|
+
}: CompositeChartContentProps<T>) {
|
|
42
|
+
const margin = { top: 40, right: 50, bottom: 50, left: 50 };
|
|
43
|
+
const innerWidth = width - margin.left - margin.right;
|
|
44
|
+
const innerHeight = height - margin.top - margin.bottom;
|
|
45
|
+
|
|
46
|
+
// Accessors
|
|
47
|
+
const getX = (d: T) => d[xKey] as unknown as string;
|
|
48
|
+
const getBarValue = (d: T) => d[barKey] as unknown as number;
|
|
49
|
+
const getLineValue = (d: T) => d[lineKey] as unknown as number;
|
|
50
|
+
|
|
51
|
+
// Scales
|
|
52
|
+
const xScale = useMemo(
|
|
53
|
+
() =>
|
|
54
|
+
scaleBand({
|
|
55
|
+
range: [0, innerWidth],
|
|
56
|
+
domain: data.map(getX),
|
|
57
|
+
padding: 0.4,
|
|
58
|
+
}),
|
|
59
|
+
[innerWidth, data, getX]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const y1Scale = useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
scaleLinear({
|
|
65
|
+
range: [innerHeight, 0],
|
|
66
|
+
domain: [0, Math.max(...data.map(getBarValue)) * 1.1], // 10% headroom
|
|
67
|
+
}),
|
|
68
|
+
[innerHeight, data, getBarValue]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const y2Scale = useMemo(
|
|
72
|
+
() =>
|
|
73
|
+
scaleLinear({
|
|
74
|
+
range: [innerHeight, 0],
|
|
75
|
+
domain: [0, Math.max(...data.map(getLineValue)) * 1.1],
|
|
76
|
+
}),
|
|
77
|
+
[innerHeight, data, getLineValue]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Tooltip
|
|
81
|
+
const {
|
|
82
|
+
tooltipOpen,
|
|
83
|
+
tooltipLeft,
|
|
84
|
+
tooltipTop,
|
|
85
|
+
tooltipData,
|
|
86
|
+
hideTooltip,
|
|
87
|
+
showTooltip,
|
|
88
|
+
} = useTooltip<{ x: string; barVal: number; lineVal: number }>();
|
|
89
|
+
|
|
90
|
+
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
|
91
|
+
scroll: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const handleTooltip = (event: React.MouseEvent | React.TouchEvent) => {
|
|
95
|
+
const { x } = localPoint(event) || { x: 0 };
|
|
96
|
+
const x0 = x - margin.left;
|
|
97
|
+
const bandWidth = xScale.step();
|
|
98
|
+
const index = Math.floor(x0 / bandWidth);
|
|
99
|
+
|
|
100
|
+
if (index >= 0 && index < data.length) {
|
|
101
|
+
const d = data[index];
|
|
102
|
+
const barVal = getBarValue(d);
|
|
103
|
+
const lineVal = getLineValue(d);
|
|
104
|
+
const xVal = getX(d);
|
|
105
|
+
|
|
106
|
+
const barX = xScale(xVal) ?? 0;
|
|
107
|
+
|
|
108
|
+
showTooltip({
|
|
109
|
+
tooltipData: { x: xVal, barVal, lineVal },
|
|
110
|
+
tooltipLeft: barX + xScale.bandwidth() / 2 + margin.left,
|
|
111
|
+
tooltipTop: y1Scale(barVal) + margin.top, // Snap to bar top or mouse Y
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (width < 50) return null;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className={cn("relative font-sans", className)}>
|
|
120
|
+
<svg
|
|
121
|
+
ref={containerRef}
|
|
122
|
+
width={width}
|
|
123
|
+
height={height}
|
|
124
|
+
className="overflow-visible"
|
|
125
|
+
onMouseMove={handleTooltip}
|
|
126
|
+
onMouseLeave={() => hideTooltip()}
|
|
127
|
+
onTouchMove={handleTooltip}
|
|
128
|
+
>
|
|
129
|
+
<Group left={margin.left} top={margin.top}>
|
|
130
|
+
<GridRows scale={y1Scale} width={innerWidth} strokeDasharray="3,3" strokeOpacity={0.2} />
|
|
131
|
+
|
|
132
|
+
{/* Bars (Primary Axis) */}
|
|
133
|
+
{data.map((d) => {
|
|
134
|
+
const xVal = getX(d);
|
|
135
|
+
const barWidth = xScale.bandwidth();
|
|
136
|
+
const barHeight = innerHeight - (y1Scale(getBarValue(d)) ?? 0);
|
|
137
|
+
const barX = xScale(xVal);
|
|
138
|
+
const barY = innerHeight - barHeight;
|
|
139
|
+
return (
|
|
140
|
+
<Bar
|
|
141
|
+
key={`bar-${xVal}`}
|
|
142
|
+
x={barX}
|
|
143
|
+
y={barY}
|
|
144
|
+
width={barWidth}
|
|
145
|
+
height={barHeight}
|
|
146
|
+
fill={barColor}
|
|
147
|
+
rx={4}
|
|
148
|
+
opacity={0.8}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
|
|
153
|
+
{/* Line (Secondary Axis) */}
|
|
154
|
+
<LinePath
|
|
155
|
+
data={data}
|
|
156
|
+
x={(d) => (xScale(getX(d)) ?? 0) + xScale.bandwidth() / 2}
|
|
157
|
+
y={(d) => y2Scale(getLineValue(d)) ?? 0}
|
|
158
|
+
stroke={lineColor}
|
|
159
|
+
strokeWidth={3}
|
|
160
|
+
curve={curveMonotoneX}
|
|
161
|
+
/>
|
|
162
|
+
{/* Line Points */}
|
|
163
|
+
{data.map((d, i) => (
|
|
164
|
+
<circle
|
|
165
|
+
key={i}
|
|
166
|
+
cx={(xScale(getX(d)) ?? 0) + xScale.bandwidth() / 2}
|
|
167
|
+
cy={y2Scale(getLineValue(d)) ?? 0}
|
|
168
|
+
r={4}
|
|
169
|
+
fill={lineColor}
|
|
170
|
+
stroke="white"
|
|
171
|
+
strokeWidth={2}
|
|
172
|
+
/>
|
|
173
|
+
))}
|
|
174
|
+
|
|
175
|
+
{/* Axes */}
|
|
176
|
+
<AxisBottom
|
|
177
|
+
scale={xScale}
|
|
178
|
+
top={innerHeight}
|
|
179
|
+
stroke="hsl(var(--muted-foreground))"
|
|
180
|
+
tickStroke="hsl(var(--muted-foreground))"
|
|
181
|
+
tickLabelProps={{
|
|
182
|
+
fill: "hsl(var(--muted-foreground))",
|
|
183
|
+
fontSize: 11,
|
|
184
|
+
textAnchor: 'middle',
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
<AxisLeft
|
|
188
|
+
scale={y1Scale}
|
|
189
|
+
stroke="hsl(var(--muted-foreground))"
|
|
190
|
+
tickStroke="hsl(var(--muted-foreground))"
|
|
191
|
+
tickLabelProps={{
|
|
192
|
+
fill: barColor, // Color match axis to data
|
|
193
|
+
fontSize: 11,
|
|
194
|
+
textAnchor: 'end',
|
|
195
|
+
dx: -4,
|
|
196
|
+
dy: 2,
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
<AxisRight
|
|
200
|
+
scale={y2Scale}
|
|
201
|
+
left={innerWidth}
|
|
202
|
+
stroke="hsl(var(--muted-foreground))"
|
|
203
|
+
tickStroke="hsl(var(--muted-foreground))"
|
|
204
|
+
tickLabelProps={{
|
|
205
|
+
fill: lineColor, // Color match axis to data
|
|
206
|
+
fontSize: 11,
|
|
207
|
+
textAnchor: 'start',
|
|
208
|
+
dx: 4,
|
|
209
|
+
dy: 2,
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
</Group>
|
|
213
|
+
</svg>
|
|
214
|
+
|
|
215
|
+
{/* Tooltip */}
|
|
216
|
+
{tooltipOpen && tooltipData && (
|
|
217
|
+
<TooltipInPortal
|
|
218
|
+
top={tooltipTop}
|
|
219
|
+
left={tooltipLeft}
|
|
220
|
+
style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
|
|
221
|
+
>
|
|
222
|
+
<div className="rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95">
|
|
223
|
+
<span className="font-semibold block mb-1">{tooltipData.x}</span>
|
|
224
|
+
<div className="flex items-center gap-2 text-xs">
|
|
225
|
+
<span className="w-2 h-2 rounded-full" style={{ background: barColor }}></span>
|
|
226
|
+
<span>Bar: {tooltipData.barVal}</span>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="flex items-center gap-2 text-xs">
|
|
229
|
+
<span className="w-2 h-2 rounded-full" style={{ background: lineColor }}></span>
|
|
230
|
+
<span>Line: {tooltipData.lineVal}</span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</TooltipInPortal>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function CompositeChart<T>(props: CompositeChartProps<T>) {
|
|
240
|
+
return (
|
|
241
|
+
<div className="w-full h-[400px]">
|
|
242
|
+
<ParentSize>
|
|
243
|
+
{({ width, height }) => <CompositeChartContent {...props} width={width} height={height} />}
|
|
244
|
+
</ParentSize>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Group } from '@visx/group';
|
|
2
|
+
import { Sankey } from '@visx/sankey';
|
|
3
|
+
import { scaleOrdinal } from '@visx/scale';
|
|
4
|
+
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
|
|
5
|
+
import { ParentSize } from '@visx/responsive';
|
|
6
|
+
import { cn } from '../../lib/utils'; // Adjust path if needed
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
// Types for your Sankey data
|
|
10
|
+
export type SankeyNode = {
|
|
11
|
+
name: string;
|
|
12
|
+
index?: number; // Added by Visx
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SankeyLink = {
|
|
16
|
+
source: number;
|
|
17
|
+
target: number;
|
|
18
|
+
value: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SankeyData = {
|
|
22
|
+
nodes: SankeyNode[];
|
|
23
|
+
links: SankeyLink[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SankeyChartProps = {
|
|
27
|
+
data: SankeyData;
|
|
28
|
+
width?: number;
|
|
29
|
+
height?: number;
|
|
30
|
+
className?: string;
|
|
31
|
+
colorScheme?: string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type SankeyChartContentProps = SankeyChartProps & {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function SankeyChartContent({
|
|
40
|
+
data,
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
className,
|
|
44
|
+
colorScheme = ['#a855f7', '#ec4899', '#3b82f6', '#14b8a6', '#f59e0b', '#ef4444'], // Default diverse palette
|
|
45
|
+
}: SankeyChartContentProps) {
|
|
46
|
+
const margin = { top: 20, right: 20, bottom: 20, left: 20 };
|
|
47
|
+
const innerWidth = width - margin.left - margin.right;
|
|
48
|
+
const innerHeight = height - margin.top - margin.bottom;
|
|
49
|
+
|
|
50
|
+
// Color Scale
|
|
51
|
+
const colorScale = useMemo(
|
|
52
|
+
() =>
|
|
53
|
+
scaleOrdinal({
|
|
54
|
+
domain: data.nodes.map((node) => node.name),
|
|
55
|
+
range: colorScheme,
|
|
56
|
+
}),
|
|
57
|
+
[data, colorScheme]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Tooltip
|
|
61
|
+
const {
|
|
62
|
+
tooltipOpen,
|
|
63
|
+
tooltipLeft,
|
|
64
|
+
tooltipTop,
|
|
65
|
+
tooltipData,
|
|
66
|
+
hideTooltip,
|
|
67
|
+
showTooltip,
|
|
68
|
+
} = useTooltip<{ name: string; value: number }>();
|
|
69
|
+
|
|
70
|
+
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
|
71
|
+
scroll: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/* Use a separate ref for measuring the SVG position if needed for relative tooltip calculations */
|
|
75
|
+
const svgRef = React.useRef<SVGSVGElement>(null);
|
|
76
|
+
|
|
77
|
+
// Merge refs (containerRef from visx needs to be called with the element)
|
|
78
|
+
const setRefs = (node: SVGSVGElement | null) => {
|
|
79
|
+
containerRef(node);
|
|
80
|
+
svgRef.current = node;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (width < 50) return null;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className={cn("relative font-sans", className)}>
|
|
87
|
+
<svg ref={setRefs} width={width} height={height} className="overflow-visible">
|
|
88
|
+
<Sankey
|
|
89
|
+
root={data}
|
|
90
|
+
size={[innerWidth, innerHeight]}
|
|
91
|
+
nodeWidth={15}
|
|
92
|
+
nodePadding={10}
|
|
93
|
+
extent={[[margin.left, margin.top], [innerWidth + margin.left, innerHeight + margin.top]]}
|
|
94
|
+
>
|
|
95
|
+
{({ graph }) => (
|
|
96
|
+
<Group>
|
|
97
|
+
{/* Links */}
|
|
98
|
+
{graph.links.map((link: any, i: number) => (
|
|
99
|
+
<path
|
|
100
|
+
key={`link-${i}`}
|
|
101
|
+
d={link.path || ''}
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
strokeOpacity={0.2}
|
|
104
|
+
fill="none"
|
|
105
|
+
strokeWidth={Math.max(1, link.width || 0)}
|
|
106
|
+
className="text-foreground transition-opacity duration-200 hover:stroke-opacity-50"
|
|
107
|
+
onMouseEnter={(event) => {
|
|
108
|
+
const containerRect = svgRef.current?.getBoundingClientRect();
|
|
109
|
+
const containerLeft = containerRect?.left || 0;
|
|
110
|
+
const containerTop = containerRect?.top || 0;
|
|
111
|
+
|
|
112
|
+
showTooltip({
|
|
113
|
+
tooltipData: {
|
|
114
|
+
name: `${link.source.name} → ${link.target.name}`,
|
|
115
|
+
value: link.value
|
|
116
|
+
},
|
|
117
|
+
tooltipLeft: event.clientX - containerLeft,
|
|
118
|
+
tooltipTop: event.clientY - containerTop,
|
|
119
|
+
});
|
|
120
|
+
}}
|
|
121
|
+
onMouseLeave={() => hideTooltip()}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
|
|
125
|
+
{/* Nodes */}
|
|
126
|
+
{graph.nodes.map((node: any, i: number) => (
|
|
127
|
+
<Group key={`node-${i}`} top={node.y0} left={node.x0}>
|
|
128
|
+
<rect
|
|
129
|
+
width={Math.max(0, node.x1 - node.x0)}
|
|
130
|
+
height={Math.max(0, node.y1 - node.y0)}
|
|
131
|
+
fill={colorScale(node.name)}
|
|
132
|
+
opacity={0.8}
|
|
133
|
+
rx={2}
|
|
134
|
+
className="transition-all duration-200 hover:opacity-100 cursor-pointer stroke-background"
|
|
135
|
+
strokeWidth={0}
|
|
136
|
+
onMouseEnter={() => {
|
|
137
|
+
showTooltip({
|
|
138
|
+
tooltipData: { name: node.name, value: node.value },
|
|
139
|
+
tooltipLeft: node.x1 + margin.left,
|
|
140
|
+
tooltipTop: node.y0 + margin.top + (node.y1 - node.y0) / 2,
|
|
141
|
+
});
|
|
142
|
+
}}
|
|
143
|
+
onMouseLeave={() => hideTooltip()}
|
|
144
|
+
/>
|
|
145
|
+
{/* Node Label (only if tall enough) */}
|
|
146
|
+
{(node.y1 - node.y0) > 12 && (
|
|
147
|
+
<text
|
|
148
|
+
x={node.x0 < width / 2 ? (node.x1 - node.x0) + 6 : -6}
|
|
149
|
+
y={(node.y1 - node.y0) / 2}
|
|
150
|
+
dy=".35em"
|
|
151
|
+
fontSize={10} // Reduced base font size
|
|
152
|
+
textAnchor={node.x0 < width / 2 ? 'start' : 'end'}
|
|
153
|
+
className="fill-foreground font-medium pointer-events-none select-none"
|
|
154
|
+
>
|
|
155
|
+
{node.name}
|
|
156
|
+
</text>
|
|
157
|
+
)}
|
|
158
|
+
</Group>
|
|
159
|
+
))}
|
|
160
|
+
</Group>
|
|
161
|
+
)}
|
|
162
|
+
</Sankey>
|
|
163
|
+
</svg>
|
|
164
|
+
{tooltipOpen && tooltipData && (
|
|
165
|
+
<TooltipInPortal
|
|
166
|
+
top={tooltipTop}
|
|
167
|
+
left={tooltipLeft}
|
|
168
|
+
style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
|
|
169
|
+
>
|
|
170
|
+
<div className="rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95">
|
|
171
|
+
<span className="font-semibold block">{tooltipData.name}</span>
|
|
172
|
+
<span className="text-xs text-muted-foreground">Value: {tooltipData.value}</span>
|
|
173
|
+
</div>
|
|
174
|
+
</TooltipInPortal>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const SankeyChart = (props: SankeyChartProps) => {
|
|
181
|
+
return (
|
|
182
|
+
<div className="w-full h-[400px]">
|
|
183
|
+
<ParentSize>
|
|
184
|
+
{({ width, height }) => <SankeyChartContent {...props} width={width} height={height} />}
|
|
185
|
+
</ParentSize>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|