waffle-charts-cli 0.1.0 → 0.1.1
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 +28 -0
- package/package.json +4 -3
- package/src/registry.js +5 -0
- package/templates/SankeyChart.tsx +188 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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. Built with Visx and Tailwind CSS.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Run the `add` command to interactively select charts to add to your project:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx waffle-charts-cli add
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add a specific chart directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx waffle-charts-cli add bar-chart
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it does
|
|
22
|
+
|
|
23
|
+
1. **Checks dependencies**: Detects if you have the necessary `@visx` packages and installs them if missing.
|
|
24
|
+
2. **Copies code**: Downloads (or copies) the component source code directly into your project's component directory.
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
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.1",
|
|
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,10 @@ 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"],
|
|
46
51
|
}
|
|
47
52
|
};
|
|
@@ -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
|
+
}
|