waffle-charts-cli 0.1.1 → 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 CHANGED
@@ -2,26 +2,57 @@
2
2
 
3
3
  The official CLI for [WaffleCharts](https://mbuchthal.github.io/waffle-charts).
4
4
 
5
- > Beautiful, headless, copy-pasteable charts for React. Built with Visx and Tailwind CSS.
5
+ > **Beautiful, Headless, Copy-Pasteable Charts for React.**
6
+ > Built with [Visx](https://airbnb.io/visx) and [Tailwind CSS](https://tailwindcss.com).
7
+
8
+ [![NPM Version](https://img.shields.io/npm/v/waffle-charts-cli)](https://www.npmjs.com/package/waffle-charts-cli)
9
+ [![License](https://img.shields.io/npm/l/waffle-charts-cli)](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.
6
15
 
7
16
  ## Usage
8
17
 
9
- Run the `add` command to interactively select charts to add to your project:
18
+ Run the `add` command to interactively select charts:
10
19
 
11
20
  ```bash
12
21
  npx waffle-charts-cli add
13
22
  ```
14
23
 
15
- Or add a specific chart directly:
24
+ Or add specific charts directly:
16
25
 
17
26
  ```bash
18
- npx waffle-charts-cli add bar-chart
27
+ npx waffle-charts-cli add bar-chart line-chart
19
28
  ```
20
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
+
21
46
  ## What it does
22
47
 
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.
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.
25
56
 
26
57
  ## License
27
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waffle-charts-cli",
3
- "version": "0.1.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": {
package/src/registry.js CHANGED
@@ -48,5 +48,10 @@ export const registry = {
48
48
  file: "SankeyChart.tsx",
49
49
  label: "Sankey Chart",
50
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"],
51
56
  }
52
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
+ }