waffle-charts-cli 0.1.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/bin/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { add } from '../src/commands/add.js';
9
+
10
+ const program = new Command();
11
+
12
+ const packageJson = JSON.parse(
13
+ fs.readFileSync(
14
+ path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json'),
15
+ 'utf-8'
16
+ )
17
+ );
18
+
19
+ program
20
+ .name('waffle-charts')
21
+ .description('CLI to easily add WaffleCharts components to your React project')
22
+ .version(packageJson.version);
23
+
24
+ program
25
+ .command('add')
26
+ .description('Add a chart component to your project')
27
+ .argument('[component]', 'The component to add (e.g. bar-chart)')
28
+ .option('-p, --path <path>', 'Path to add the component to', 'src/components/ui/waffle')
29
+ .action(add);
30
+
31
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "waffle-charts-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI to add WaffleCharts components to your project",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "waffle-charts": "./bin/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "react",
14
+ "charts",
15
+ "cli",
16
+ "visx"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mbuchthal/waffle-charts.git"
21
+ },
22
+ "author": "WaffleCharts Team",
23
+ "license": "MIT",
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "templates"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "chalk": "^5.3.0",
34
+ "commander": "^12.1.0",
35
+ "fs-extra": "^11.2.0",
36
+ "ora": "^8.0.1",
37
+ "prompts": "^2.4.2"
38
+ },
39
+ "type": "module"
40
+ }
@@ -0,0 +1,91 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import ora from 'ora';
6
+ import { fileURLToPath } from 'url';
7
+ import { registry } from '../registry.js';
8
+ import { execSync } from 'child_process';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ export async function add(component, options) {
14
+ let selectedComponents = component ? [component] : [];
15
+
16
+ // 1. If no component provided, ask user
17
+ if (!component) {
18
+ const response = await prompts({
19
+ type: 'multiselect',
20
+ name: 'components',
21
+ message: 'Which charts would you like to add?',
22
+ choices: Object.entries(registry).map(([key, value]) => ({
23
+ title: value.label,
24
+ value: key
25
+ })),
26
+ min: 1
27
+ });
28
+
29
+ if (!response.components || response.components.length === 0) {
30
+ console.log(chalk.yellow('No components selected. Exiting.'));
31
+ process.exit(0);
32
+ }
33
+ selectedComponents = response.components;
34
+ }
35
+
36
+ // 2. Validate selections
37
+ for (const comp of selectedComponents) {
38
+ if (!registry[comp]) {
39
+ console.error(chalk.red(`Error: Component "${comp}" not found in registry.`));
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ const targetDir = path.resolve(process.cwd(), options.path);
45
+
46
+ // 3. Ensure target directory exists
47
+ if (!fs.existsSync(targetDir)) {
48
+ const spinner = ora(`Creating directory ${options.path}...`).start();
49
+ fs.mkdirpSync(targetDir);
50
+ spinner.succeed(`Created directory ${options.path}`);
51
+ }
52
+
53
+ // 4. Install Dependencies
54
+ const dependencies = new Set();
55
+ selectedComponents.forEach(comp => {
56
+ registry[comp].dependencies.forEach(dep => dependencies.add(dep));
57
+ });
58
+
59
+ const dependencySpinner = ora('Installing dependencies...').start();
60
+ try {
61
+ // Detect package manager (simple check)
62
+ const packageManager = fs.existsSync('yarn.lock') ? 'yarn add' : 'npm install';
63
+ const installCmd = `${packageManager} ${Array.from(dependencies).join(' ')}`;
64
+ execSync(installCmd, { stdio: 'ignore' });
65
+ dependencySpinner.succeed('Dependencies installed');
66
+ } catch (error) {
67
+ dependencySpinner.fail('Failed to install dependencies');
68
+ console.error(error);
69
+ }
70
+
71
+ // 5. Copy Files
72
+ for (const comp of selectedComponents) {
73
+ const spinner = ora(`Adding ${registry[comp].label}...`).start();
74
+ const templatePath = path.join(__dirname, '../../templates', registry[comp].file);
75
+ const destPath = path.join(targetDir, registry[comp].file);
76
+
77
+ try {
78
+ if (fs.existsSync(templatePath)) {
79
+ fs.copyFileSync(templatePath, destPath);
80
+ spinner.succeed(`Added ${registry[comp].label} to ${options.path}/${registry[comp].file}`);
81
+ } else {
82
+ spinner.fail(`Template for ${comp} not found at ${templatePath}`);
83
+ }
84
+ } catch (error) {
85
+ spinner.fail(`Failed to copy ${comp}`);
86
+ console.error(error);
87
+ }
88
+ }
89
+
90
+ console.log(chalk.green('\nDone! Happy charting with WaffleCharts. 🧇'));
91
+ }
@@ -0,0 +1,47 @@
1
+ export const registry = {
2
+ "bar-chart": {
3
+ file: "BarChart.tsx",
4
+ label: "Bar Chart",
5
+ dependencies: ["@visx/shape", "@visx/scale", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/axis", "@visx/grid", "clsx", "tailwind-merge"],
6
+ },
7
+ "line-chart": {
8
+ file: "LineChart.tsx",
9
+ label: "Line Chart",
10
+ dependencies: ["@visx/shape", "@visx/curve", "@visx/scale", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/axis", "@visx/grid", "@visx/glyph", "clsx", "tailwind-merge"],
11
+ },
12
+ "area-chart": {
13
+ file: "AreaChart.tsx",
14
+ label: "Area Chart",
15
+ dependencies: ["@visx/shape", "@visx/curve", "@visx/scale", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/axis", "@visx/grid", "clsx", "tailwind-merge"],
16
+ },
17
+ "pie-chart": {
18
+ file: "PieChart.tsx",
19
+ label: "Pie Chart",
20
+ dependencies: ["@visx/shape", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/text", "clsx", "tailwind-merge"],
21
+ },
22
+ "radar-chart": {
23
+ file: "RadarChart.tsx",
24
+ label: "Radar Chart",
25
+ dependencies: ["@visx/shape", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/scale", "@visx/point", "@visx/grid", "@visx/curve", "clsx", "tailwind-merge"],
26
+ },
27
+ "bubble-chart": {
28
+ file: "BubbleChart.tsx",
29
+ label: "Bubble Chart",
30
+ dependencies: ["@visx/shape", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/scale", "@visx/axis", "@visx/grid", "clsx", "tailwind-merge"],
31
+ },
32
+ "heatmap-chart": {
33
+ file: "HeatmapChart.tsx",
34
+ label: "Heatmap Chart",
35
+ dependencies: ["@visx/heatmap", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/scale", "@visx/axis", "clsx", "tailwind-merge"],
36
+ },
37
+ "treemap-chart": {
38
+ file: "TreemapChart.tsx",
39
+ label: "Treemap Chart",
40
+ dependencies: ["@visx/hierarchy", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/scale", "clsx", "tailwind-merge"],
41
+ },
42
+ "scatter-chart": {
43
+ file: "ScatterChart.tsx",
44
+ label: "Scatter Chart",
45
+ dependencies: ["@visx/shape", "@visx/group", "@visx/responsive", "@visx/tooltip", "@visx/scale", "@visx/axis", "@visx/grid", "@visx/glyph", "clsx", "tailwind-merge"],
46
+ }
47
+ };
@@ -0,0 +1,205 @@
1
+ import { useMemo } from 'react';
2
+ import { AreaStack } from '@visx/shape';
3
+ import { Group } from '@visx/group';
4
+ import { scaleTime, scaleLinear } from '@visx/scale';
5
+ import { AxisBottom, AxisLeft } from '@visx/axis';
6
+ import { GridRows, GridColumns } from '@visx/grid';
7
+ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
8
+ import { localPoint } from '@visx/event';
9
+ import { ParentSize } from '@visx/responsive';
10
+ import { cn } from '../../lib/utils';
11
+ import { bisector } from 'd3-array';
12
+
13
+ export type AreaChartProps<T> = {
14
+ data: T[];
15
+ xKey: keyof T;
16
+ keys: (keyof T)[]; // Keys to stack
17
+ colors?: string[]; // CSS text-color classes
18
+ className?: string;
19
+ width?: number;
20
+ height?: number;
21
+ };
22
+
23
+ type AreaChartContentProps<T> = AreaChartProps<T> & {
24
+ width: number;
25
+ height: number;
26
+ };
27
+
28
+ function AreaChartContent<T>({
29
+ data,
30
+ width,
31
+ height,
32
+ xKey,
33
+ keys,
34
+ colors = ['text-blue-500', 'text-indigo-500', 'text-purple-500'],
35
+ className,
36
+ }: AreaChartContentProps<T>) {
37
+ // Config
38
+ const margin = { top: 40, right: 30, bottom: 50, left: 50 };
39
+ const xMax = width - margin.left - margin.right;
40
+ const yMax = height - margin.top - margin.bottom;
41
+
42
+ // Accessors
43
+ const getX = (d: T) => new Date(d[xKey] as string | number | Date);
44
+ const getY0 = (d: unknown) => (d as { [key: string]: number })[0];
45
+ const getY1 = (d: unknown) => (d as { [key: string]: number })[1];
46
+
47
+
48
+ // Scales
49
+ const xScale = useMemo(
50
+ () =>
51
+ scaleTime({
52
+ range: [0, xMax],
53
+ domain: [Math.min(...data.map(d => getX(d).getTime())), Math.max(...data.map(d => getX(d).getTime()))],
54
+ }),
55
+ [xMax, data, xKey],
56
+ );
57
+
58
+ const yScale = useMemo(
59
+ () =>
60
+ scaleLinear<number>({
61
+ range: [yMax, 0],
62
+ round: true,
63
+ // Calculate max value from stacked data could be complex, assuming simplistic sum or finding max from data
64
+ // For accurate stacking, we should ideally calculate the max stack value.
65
+ // For this demo, let's just make sure we cover the data range generously.
66
+ domain: [0, Math.max(...data.map(d => keys.reduce((acc, k) => acc + Number(d[k]), 0))) * 1.1],
67
+ nice: true,
68
+ }),
69
+ [yMax, data, keys],
70
+ );
71
+
72
+ // Tooltip - Simplified for AreaStack (just showing nearest X for now)
73
+ const {
74
+ tooltipOpen,
75
+ tooltipLeft,
76
+ tooltipTop,
77
+ tooltipData,
78
+ hideTooltip,
79
+ showTooltip,
80
+ } = useTooltip<T>();
81
+
82
+ const { containerRef, TooltipInPortal } = useTooltipInPortal({
83
+ scroll: true,
84
+ });
85
+
86
+ const bisectDate = bisector<T, Date>(d => getX(d)).left;
87
+
88
+ const handleTooltip = (event: React.MouseEvent<SVGRectElement> | React.TouchEvent<SVGRectElement>) => {
89
+ const { x } = localPoint(event) || { x: 0 };
90
+ const x0 = xScale.invert(x - margin.left);
91
+ const index = bisectDate(data, x0, 1);
92
+ const d0 = data[index - 1];
93
+ const d1 = data[index];
94
+ let d = d0;
95
+ if (d1 && getX(d1)) {
96
+ d = x0.valueOf() - getX(d0).valueOf() > getX(d1).valueOf() - x0.valueOf() ? d1 : d0;
97
+ }
98
+
99
+ if (d) {
100
+ showTooltip({
101
+ tooltipData: d,
102
+ tooltipLeft: xScale(getX(d)) + margin.left,
103
+ tooltipTop: yScale(0) + margin.top, // Snap to bottom or follow mouse
104
+ });
105
+ }
106
+ };
107
+
108
+ if (width < 10) return null;
109
+
110
+ return (
111
+ <div className={cn("relative", className)}>
112
+ <svg ref={containerRef} width={width} height={height} className="overflow-visible">
113
+ <Group left={margin.left} top={margin.top}>
114
+ <GridRows scale={yScale} width={xMax} height={yMax} strokeDasharray="3,3" stroke="hsl(var(--border))" />
115
+ <GridColumns scale={xScale} width={xMax} height={yMax} strokeDasharray="3,3" stroke="hsl(var(--border))" />
116
+
117
+ <AxisBottom
118
+ top={yMax}
119
+ scale={xScale}
120
+ stroke="hsl(var(--border))"
121
+ tickStroke="hsl(var(--border))"
122
+ tickLabelProps={{
123
+ fill: "hsl(var(--muted-foreground))",
124
+ fontSize: 11,
125
+ textAnchor: "middle",
126
+ }}
127
+ />
128
+ <AxisLeft
129
+ scale={yScale}
130
+ stroke="transparent"
131
+ tickStroke="hsl(var(--border))"
132
+ tickLabelProps={{
133
+ fill: "hsl(var(--muted-foreground))",
134
+ fontSize: 11,
135
+ textAnchor: "end",
136
+ dx: -4,
137
+ dy: 4,
138
+ }}
139
+ />
140
+
141
+ <AreaStack
142
+ data={data}
143
+ keys={keys as string[]}
144
+ x={d => xScale(getX(d.data)) ?? 0}
145
+ y0={d => yScale(getY0(d)) ?? 0}
146
+ y1={d => yScale(getY1(d)) ?? 0}
147
+ >
148
+ {({ stacks, path }) =>
149
+ stacks.map((stack, i) => (
150
+ <path
151
+ key={`stack-${stack.key}`}
152
+ d={path(stack) || ''}
153
+ stroke="transparent"
154
+ // Use currentColor to inherit color from text-class
155
+ className={cn("fill-current opacity-80 hover:opacity-100 transition-opacity", colors[i % colors.length])}
156
+ />
157
+ ))
158
+ }
159
+ </AreaStack>
160
+
161
+ {/* Invisible Overlay for Tooltip */}
162
+ <rect
163
+ x={0}
164
+ y={0}
165
+ width={xMax}
166
+ height={yMax}
167
+ fill="transparent"
168
+ onTouchStart={handleTooltip}
169
+ onTouchMove={handleTooltip}
170
+ onMouseMove={handleTooltip}
171
+ onMouseLeave={() => hideTooltip()}
172
+ />
173
+ </Group>
174
+ </svg>
175
+ {tooltipOpen && tooltipData && (
176
+ <TooltipInPortal
177
+ top={tooltipTop}
178
+ left={tooltipLeft}
179
+ style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
180
+ >
181
+ <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">
182
+ <p className="font-semibold text-xs text-muted-foreground mb-1">{getX(tooltipData).toLocaleDateString()}</p>
183
+ {keys.map((key, i) => (
184
+ <div key={key as string} className="flex items-center gap-2">
185
+ <div className={cn("w-2 h-2 rounded-full bg-current", colors[i % colors.length])} />
186
+ <span className="text-xs capitalize">{key as string}:</span>
187
+ <span className="font-mono text-xs font-bold">{String(tooltipData[key])}</span>
188
+ </div>
189
+ ))}
190
+ </div>
191
+ </TooltipInPortal>
192
+ )}
193
+ </div>
194
+ );
195
+ }
196
+
197
+ export const AreaChart = <T,>(props: AreaChartProps<T>) => {
198
+ return (
199
+ <div className="w-full h-[300px]">
200
+ <ParentSize>
201
+ {({ width, height }) => <AreaChartContent {...props} width={width} height={height} />}
202
+ </ParentSize>
203
+ </div>
204
+ )
205
+ }
@@ -0,0 +1,165 @@
1
+ import { useMemo } from 'react';
2
+ import { Bar } from '@visx/shape';
3
+ import { Group } from '@visx/group';
4
+ import { scaleBand, scaleLinear } from '@visx/scale';
5
+ import { AxisBottom, AxisLeft } from '@visx/axis';
6
+ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
7
+ import { localPoint } from '@visx/event';
8
+ import { ParentSize } from '@visx/responsive';
9
+ import { cn } from '../../lib/utils';
10
+
11
+ // Types
12
+ export type BarChartProps<T> = {
13
+ data: T[];
14
+ xKey: keyof T;
15
+ yKey: keyof T;
16
+ className?: string;
17
+ barColor?: string;
18
+ width?: number;
19
+ height?: number;
20
+ };
21
+
22
+ // Internal component with required dimensions
23
+ type BarChartContentProps<T> = BarChartProps<T> & {
24
+ width: number;
25
+ height: number;
26
+ };
27
+
28
+ function BarChartContent<T>({
29
+ data,
30
+ width,
31
+ height,
32
+ xKey,
33
+ yKey,
34
+ className,
35
+ barColor = "bg-primary"
36
+ }: BarChartContentProps<T>) {
37
+ // Config
38
+ const margin = { top: 40, right: 30, bottom: 50, left: 50 };
39
+ const xMax = width - margin.left - margin.right;
40
+ const yMax = height - margin.top - margin.bottom;
41
+
42
+ // Accessors
43
+ const getX = (d: T) => d[xKey] as string;
44
+ const getY = (d: T) => Number(d[yKey]);
45
+
46
+ // Scales
47
+ const xScale = useMemo(
48
+ () =>
49
+ scaleBand<string>({
50
+ range: [0, xMax],
51
+ round: true,
52
+ domain: data.map(getX),
53
+ padding: 0.4,
54
+ }),
55
+ [xMax, data, xKey],
56
+ );
57
+
58
+ const yScale = useMemo(
59
+ () =>
60
+ scaleLinear<number>({
61
+ range: [yMax, 0],
62
+ round: true,
63
+ domain: [0, Math.max(...data.map(getY))],
64
+ }),
65
+ [yMax, data, yKey],
66
+ );
67
+
68
+ // Tooltip
69
+ const {
70
+ tooltipOpen,
71
+ tooltipLeft,
72
+ tooltipTop,
73
+ tooltipData,
74
+ hideTooltip,
75
+ showTooltip,
76
+ } = useTooltip<T>();
77
+
78
+ const { containerRef, TooltipInPortal } = useTooltipInPortal({
79
+ scroll: true,
80
+ });
81
+
82
+ if (width < 10) return null;
83
+
84
+ return (
85
+ <div className={cn("relative", className)}>
86
+ <svg ref={containerRef} width={width} height={height} className="overflow-visible">
87
+ <Group left={margin.left} top={margin.top}>
88
+ <AxisBottom
89
+ top={yMax}
90
+ scale={xScale}
91
+ stroke="hsl(var(--border))"
92
+ tickStroke="hsl(var(--border))"
93
+ tickLabelProps={{
94
+ fill: "hsl(var(--muted-foreground))",
95
+ fontSize: 11,
96
+ textAnchor: "middle",
97
+ }}
98
+ />
99
+ <AxisLeft
100
+ scale={yScale}
101
+ stroke="transparent"
102
+ tickStroke="hsl(var(--border))"
103
+ tickLabelProps={{
104
+ fill: "hsl(var(--muted-foreground))",
105
+ fontSize: 11,
106
+ textAnchor: "end",
107
+ dx: -4,
108
+ dy: 4,
109
+ }}
110
+ numTicks={5}
111
+ />
112
+ {data.map((d) => {
113
+ const letter = getX(d);
114
+ const barWidth = xScale.bandwidth();
115
+ const barHeight = yMax - (yScale(getY(d)) ?? 0);
116
+ const barX = xScale(letter);
117
+ const barY = yMax - barHeight;
118
+ return (
119
+ <Bar
120
+ key={`bar-${letter}`}
121
+ x={barX}
122
+ y={barY}
123
+ width={barWidth}
124
+ height={barHeight}
125
+ className={cn("fill-primary transition-all duration-300 hover:opacity-80 cursor-pointer", barColor)}
126
+ onMouseLeave={() => hideTooltip()}
127
+ onMouseMove={(event) => {
128
+ const eventSvgCoords = localPoint(event);
129
+ const left = barX! + barWidth / 2;
130
+ showTooltip({
131
+ tooltipData: d,
132
+ tooltipTop: eventSvgCoords?.y,
133
+ tooltipLeft: left,
134
+ });
135
+ }}
136
+ />
137
+ );
138
+ })}
139
+ </Group>
140
+ </svg>
141
+ {tooltipOpen && tooltipData && (
142
+ <TooltipInPortal
143
+ top={tooltipTop}
144
+ left={tooltipLeft}
145
+ style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
146
+ >
147
+ <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">
148
+ <p className="font-semibold">{String(getY(tooltipData))}</p>
149
+ <p className="text-xs text-muted-foreground">{getX(tooltipData)}</p>
150
+ </div>
151
+ </TooltipInPortal>
152
+ )}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ export const BarChart = <T,>(props: BarChartProps<T>) => {
158
+ return (
159
+ <div className="w-full h-[300px]">
160
+ <ParentSize>
161
+ {({ width, height }) => <BarChartContent {...props} width={width} height={height} />}
162
+ </ParentSize>
163
+ </div>
164
+ )
165
+ }