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.
@@ -0,0 +1,177 @@
1
+ import { Pie } from '@visx/shape';
2
+ import { Group } from '@visx/group';
3
+ import { scaleOrdinal } from '@visx/scale';
4
+ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
5
+ import { useState } from 'react';
6
+ import { ParentSize } from '@visx/responsive';
7
+ import { cn } from '../../lib/utils';
8
+ import { arc as d3arc } from 'd3-shape'; // Direct import for custom arc generation
9
+
10
+ // Types
11
+ export type PieChartProps<T> = {
12
+ data: T[];
13
+ valueKey: keyof T;
14
+ labelKey: keyof T; // Used for tooltip or legend
15
+ width?: number;
16
+ height?: number;
17
+ className?: string; // Wrapper class
18
+ innerRadius?: number; // 0 for Pie, >0 for Donut
19
+ colors?: string[]; // CSS variable strings or hex
20
+ centerText?: {
21
+ title: string;
22
+ subtitle?: string;
23
+ };
24
+ };
25
+
26
+ type PieChartContentProps<T> = PieChartProps<T> & {
27
+ width: number;
28
+ height: number;
29
+ };
30
+
31
+ function PieChartContent<T>({
32
+ data,
33
+ width,
34
+ height,
35
+ valueKey,
36
+ labelKey,
37
+ className,
38
+ innerRadius = 0, // Default to full pie
39
+ colors,
40
+ centerText,
41
+ }: PieChartContentProps<T>) {
42
+ const margin = { top: 20, right: 20, bottom: 20, left: 20 };
43
+ const innerWidth = width - margin.left - margin.right;
44
+ const innerHeight = height - margin.top - margin.bottom;
45
+ const radius = Math.min(innerWidth, innerHeight) / 2;
46
+ const centerY = innerHeight / 2;
47
+ const centerX = innerWidth / 2;
48
+
49
+ // Accessors
50
+ const getValue = (d: T) => Number(d[valueKey]);
51
+
52
+ // Scales (Color)
53
+ // We prefer using CSS classes/variables, but Visx Pie returns arcs.
54
+ // We will map index to a Tailwind color class if provided, or default ordinals.
55
+ const defaultColors = ['text-primary', 'text-blue-500', 'text-indigo-500', 'text-sky-500', 'text-cyan-500']
56
+ const colorScale = scaleOrdinal({
57
+ domain: data.map((_, i) => i),
58
+ range: colors || defaultColors,
59
+ });
60
+
61
+ // Tooltip
62
+ const {
63
+ tooltipOpen,
64
+ tooltipLeft,
65
+ tooltipTop,
66
+ tooltipData,
67
+ hideTooltip,
68
+ showTooltip,
69
+ } = useTooltip<T>();
70
+
71
+ const { containerRef, TooltipInPortal } = useTooltipInPortal({
72
+ scroll: true,
73
+ detectBounds: true,
74
+ });
75
+
76
+ // Interaction State
77
+ const [activeShape, setActiveShape] = useState<number | null>(null);
78
+
79
+ if (width < 10) return null;
80
+
81
+ return (
82
+ <div className={cn("relative flex items-center justify-center", className)}>
83
+ <svg ref={containerRef} width={width} height={height} className="overflow-visible">
84
+ <Group top={centerY + margin.top} left={centerX + margin.left}>
85
+ <Pie
86
+ data={data}
87
+ pieValue={getValue}
88
+ outerRadius={radius}
89
+ innerRadius={innerRadius}
90
+ padAngle={0.02}
91
+ cornerRadius={3}
92
+ >
93
+ {(pie) => {
94
+ return pie.arcs.map((arc, index) => {
95
+ const [centroidX, centroidY] = pie.path.centroid(arc);
96
+ const isHovered = activeShape === index;
97
+ const currentOuterRadius = isHovered ? radius + 5 : radius;
98
+
99
+ // Create custom arc generator for hover effect
100
+ // Create custom arc generator for hover effect
101
+ const arcGenerator = d3arc().cornerRadius(3);
102
+ // @ts-ignore - d3 types might still complain about explicit config matching
103
+ const arcPath = arcGenerator({
104
+ innerRadius,
105
+ outerRadius: currentOuterRadius,
106
+ startAngle: arc.startAngle,
107
+ endAngle: arc.endAngle,
108
+ padAngle: arc.padAngle,
109
+ });
110
+
111
+ return (
112
+ <g key={`arc-${index}`}>
113
+ <path
114
+ d={arcPath || ''}
115
+ className={cn("fill-current transition-all duration-300 cursor-pointer hover:opacity-80", colorScale(index))}
116
+ // If colors are passed as specific colors (not classes), you might use fill={...} instead.
117
+ // This implementation assumes 'colors' prop contains Tailwind TEXT color classes (e.g. 'text-blue-500'),
118
+ // which fill-current will inherit.
119
+ onMouseEnter={() => {
120
+ setActiveShape(index);
121
+ showTooltip({
122
+ tooltipData: arc.data,
123
+ tooltipLeft: centroidX + centerX + margin.left,
124
+ tooltipTop: centroidY + centerY + margin.top,
125
+ })
126
+ }}
127
+ onMouseLeave={() => {
128
+ setActiveShape(null);
129
+ hideTooltip();
130
+ }}
131
+ />
132
+ </g>
133
+ )
134
+ })
135
+ }}
136
+ </Pie>
137
+
138
+ {/* Center Text (Donut only) */}
139
+ {innerRadius > 0 && centerText && (
140
+ <text
141
+ textAnchor="middle"
142
+ pointerEvents="none"
143
+ >
144
+ <tspan x="0" dy="-0.5em" className="fill-foreground text-2xl font-bold">{centerText.title}</tspan>
145
+ {centerText.subtitle && (
146
+ <tspan x="0" dy="1.5em" className="fill-muted-foreground text-sm uppercase tracking-wider">{centerText.subtitle}</tspan>
147
+ )}
148
+ </text>
149
+ )}
150
+
151
+ </Group>
152
+ </svg>
153
+ {tooltipOpen && tooltipData && (
154
+ <TooltipInPortal
155
+ top={tooltipTop}
156
+ left={tooltipLeft}
157
+ style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
158
+ >
159
+ <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">
160
+ <p className="font-semibold">{String(getValue(tooltipData))}</p>
161
+ <p className="text-xs text-muted-foreground">{String(tooltipData[labelKey])}</p>
162
+ </div>
163
+ </TooltipInPortal>
164
+ )}
165
+ </div>
166
+ );
167
+ }
168
+
169
+ export const PieChart = <T,>(props: PieChartProps<T>) => {
170
+ return (
171
+ <div className="w-full h-[300px]">
172
+ <ParentSize>
173
+ {({ width, height }) => <PieChartContent {...props} width={width} height={height} />}
174
+ </ParentSize>
175
+ </div>
176
+ )
177
+ }
@@ -0,0 +1,163 @@
1
+ import { Group } from '@visx/group';
2
+ import { scaleLinear } from '@visx/scale';
3
+ import { Point } from '@visx/point';
4
+ import { ParentSize } from '@visx/responsive';
5
+ import { cn } from '../../lib/utils';
6
+ import { useMemo } from 'react';
7
+
8
+ export type RadarChartProps<T> = {
9
+ data: T[];
10
+ radiusKey: keyof T;
11
+ angleKey: keyof T;
12
+ className?: string;
13
+ gridColor?: string;
14
+ polygonColor?: string;
15
+ width?: number;
16
+ height?: number;
17
+ };
18
+
19
+ type RadarChartContentProps<T> = RadarChartProps<T> & {
20
+ width: number;
21
+ height: number;
22
+ };
23
+
24
+ function RadarChartContent<T>({
25
+ data,
26
+ width,
27
+ height,
28
+ radiusKey,
29
+ angleKey,
30
+ className,
31
+ gridColor = "stroke-border",
32
+ polygonColor = "fill-primary",
33
+ }: RadarChartContentProps<T>) {
34
+ const margin = { top: 40, right: 40, bottom: 40, left: 40 };
35
+ const xMax = width - margin.left - margin.right;
36
+ const yMax = height - margin.top - margin.bottom;
37
+ const radius = Math.min(xMax, yMax) / 2;
38
+
39
+ const getRadius = (d: T) => Number(d[radiusKey]);
40
+ const getAngle = (d: T) => d[angleKey] as string;
41
+
42
+ const yScale = useMemo(
43
+ () =>
44
+ scaleLinear<number>({
45
+ range: [0, radius],
46
+ domain: [0, Math.max(...data.map(getRadius)) * 1.1],
47
+ }),
48
+ [radius, data, radiusKey]
49
+ );
50
+
51
+ const angleStep = (Math.PI * 2) / data.length;
52
+
53
+ // Generate grid points
54
+ // 5 concentric circles
55
+ const gridLevels = [1, 2, 3, 4, 5];
56
+ const gridPoints = gridLevels.map((level) => {
57
+ const r = (radius / 5) * level;
58
+ return data.map((_, i) => {
59
+ const angle = i * angleStep - Math.PI / 2;
60
+ return new Point({
61
+ x: r * Math.cos(angle),
62
+ y: r * Math.sin(angle),
63
+ });
64
+ });
65
+ });
66
+
67
+ // Calculate polygon points
68
+ const points = data.map((d, i) => {
69
+ const angle = i * angleStep - Math.PI / 2;
70
+ const r = yScale(getRadius(d));
71
+ return new Point({
72
+ x: r * Math.cos(angle),
73
+ y: r * Math.sin(angle),
74
+ });
75
+ });
76
+
77
+ if (width < 10) return null;
78
+
79
+ return (
80
+ <div className={cn("relative flex items-center justify-center", className)}>
81
+ <svg width={width} height={height} className="overflow-visible">
82
+ <Group top={height / 2} left={width / 2}>
83
+ {/* Grid Rings (Polygonal) */}
84
+ {gridPoints.map((levelPoints, i) => (
85
+ <polygon
86
+ key={`grid-level-${i}`}
87
+ points={levelPoints.map(p => `${p.x},${p.y}`).join(' ')}
88
+ fill="transparent"
89
+ stroke="currentColor"
90
+ className={cn("stroke-1 opacity-20", gridColor)}
91
+ />
92
+ ))}
93
+
94
+ {/* Axes */}
95
+ {data.map((_, i) => {
96
+ const angle = i * angleStep - Math.PI / 2;
97
+ const r = radius;
98
+ const x = r * Math.cos(angle);
99
+ const y = r * Math.sin(angle);
100
+ return (
101
+ <line
102
+ key={`axis-${i}`}
103
+ x1={0}
104
+ y1={0}
105
+ x2={x}
106
+ y2={y}
107
+ className={cn("stroke-1 opacity-20", gridColor)}
108
+ />
109
+ );
110
+ })}
111
+
112
+ {/* Labels */}
113
+ {data.map((d, i) => {
114
+ const angle = i * angleStep - Math.PI / 2;
115
+ const r = radius + 20; // Offset label
116
+ const x = r * Math.cos(angle);
117
+ const y = r * Math.sin(angle);
118
+ return (
119
+ <text
120
+ key={`label-${i}`}
121
+ x={x}
122
+ y={y}
123
+ dy="0.35em"
124
+ textAnchor={x > 0 ? 'start' : x < 0 ? 'end' : 'middle'}
125
+ className="text-xs fill-muted-foreground capit"
126
+ >
127
+ {getAngle(d)}
128
+ </text>
129
+ );
130
+ })}
131
+
132
+ {/* The Radar Polygon */}
133
+ <polygon
134
+ points={points.map(p => `${p.x},${p.y}`).join(' ')}
135
+ className={cn("stroke-primary stroke-2 fill-primary/20 hover:opacity-80 transition-opacity", polygonColor)}
136
+ />
137
+
138
+ {/* Dots on corners */}
139
+ {points.map((p, i) => (
140
+ <circle
141
+ key={`point-${i}`}
142
+ cx={p.x}
143
+ cy={p.y}
144
+ r={4}
145
+ className="fill-background stroke-primary stroke-2"
146
+ />
147
+ ))}
148
+
149
+ </Group>
150
+ </svg>
151
+ </div>
152
+ );
153
+ }
154
+
155
+ export const RadarChart = <T,>(props: RadarChartProps<T>) => {
156
+ return (
157
+ <div className="w-full h-[300px]">
158
+ <ParentSize>
159
+ {({ width, height }) => <RadarChartContent {...props} width={width} height={height} />}
160
+ </ParentSize>
161
+ </div>
162
+ )
163
+ }
@@ -0,0 +1,167 @@
1
+ import { useMemo } from 'react';
2
+ import { Group } from '@visx/group';
3
+ import { Circle } from '@visx/shape';
4
+ import { scaleLinear } from '@visx/scale';
5
+ import { AxisBottom, AxisLeft } from '@visx/axis';
6
+ import { Grid } from '@visx/grid';
7
+ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
8
+ import { ParentSize } from '@visx/responsive';
9
+ import { cn } from '../../lib/utils';
10
+
11
+ // Types
12
+ export type ScatterChartProps<T> = {
13
+ data: T[];
14
+ xKey: keyof T;
15
+ yKey: keyof T;
16
+ className?: string; // Wrapper class
17
+ pointClassName?: string; // Point color/style
18
+ width?: number;
19
+ height?: number;
20
+ };
21
+
22
+ // Internal component
23
+ type ScatterChartContentProps<T> = ScatterChartProps<T> & {
24
+ width: number;
25
+ height: number;
26
+ };
27
+
28
+ function ScatterChartContent<T>({
29
+ data,
30
+ width,
31
+ height,
32
+ xKey,
33
+ yKey,
34
+ className,
35
+ pointClassName = "fill-primary",
36
+ }: ScatterChartContentProps<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) => Number(d[xKey]);
44
+ const getY = (d: T) => Number(d[yKey]);
45
+
46
+ // Scales
47
+ const xScale = useMemo(
48
+ () =>
49
+ scaleLinear<number>({
50
+ range: [0, xMax],
51
+ round: true,
52
+ domain: [0, Math.max(...data.map(getX)) * 1.1], // Add padding
53
+ }),
54
+ [xMax, data, xKey],
55
+ );
56
+
57
+ const yScale = useMemo(
58
+ () =>
59
+ scaleLinear<number>({
60
+ range: [yMax, 0],
61
+ round: true,
62
+ domain: [0, Math.max(...data.map(getY)) * 1.1],
63
+ }),
64
+ [yMax, data, yKey],
65
+ );
66
+
67
+ // Tooltip
68
+ const {
69
+ tooltipOpen,
70
+ tooltipLeft,
71
+ tooltipTop,
72
+ tooltipData,
73
+ hideTooltip,
74
+ showTooltip,
75
+ } = useTooltip<T>();
76
+
77
+ const { containerRef, TooltipInPortal } = useTooltipInPortal({
78
+ scroll: true,
79
+ });
80
+
81
+ if (width < 10) return null;
82
+
83
+ return (
84
+ <div className={cn("relative", className)}>
85
+ <svg ref={containerRef} width={width} height={height} className="overflow-visible">
86
+ <Group left={margin.left} top={margin.top}>
87
+ <Grid
88
+ xScale={xScale}
89
+ yScale={yScale}
90
+ width={xMax}
91
+ height={yMax}
92
+ stroke="hsl(var(--border))"
93
+ strokeOpacity={0.4}
94
+ />
95
+ <AxisBottom
96
+ top={yMax}
97
+ scale={xScale}
98
+ stroke="hsl(var(--border))"
99
+ tickStroke="hsl(var(--border))"
100
+ tickLabelProps={{
101
+ fill: "hsl(var(--muted-foreground))",
102
+ fontSize: 11,
103
+ textAnchor: "middle",
104
+ }}
105
+ />
106
+ <AxisLeft
107
+ scale={yScale}
108
+ stroke="transparent"
109
+ tickStroke="hsl(var(--border))"
110
+ tickLabelProps={{
111
+ fill: "hsl(var(--muted-foreground))",
112
+ fontSize: 11,
113
+ textAnchor: "end",
114
+ dx: -4,
115
+ dy: 4,
116
+ }}
117
+ />
118
+ {data.map((d, i) => {
119
+ const cx = xScale(getX(d));
120
+ const cy = yScale(getY(d));
121
+ return (
122
+ <Circle
123
+ key={`point-${i}`}
124
+ cx={cx}
125
+ cy={cy}
126
+ r={6}
127
+ className={cn("transition-all duration-300 hover:r-8 hover:opacity-80 cursor-pointer", pointClassName)}
128
+ onMouseEnter={() => {
129
+ showTooltip({
130
+ tooltipData: d,
131
+ tooltipLeft: cx + margin.left, // absolute relative to container
132
+ tooltipTop: cy + margin.top, // absolute relative to container
133
+ });
134
+ }}
135
+ onMouseLeave={() => hideTooltip()}
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
+ <div className="flex flex-col gap-1">
149
+ <span className="font-semibold">{String(getY(tooltipData))}</span>
150
+ <span className="text-xs text-muted-foreground">{String(getX(tooltipData))}</span>
151
+ </div>
152
+ </div>
153
+ </TooltipInPortal>
154
+ )}
155
+ </div>
156
+ );
157
+ }
158
+
159
+ export const ScatterChart = <T,>(props: ScatterChartProps<T>) => {
160
+ return (
161
+ <div className="w-full h-[300px]">
162
+ <ParentSize>
163
+ {({ width, height }) => <ScatterChartContent {...props} width={width} height={height} />}
164
+ </ParentSize>
165
+ </div>
166
+ )
167
+ }
@@ -0,0 +1,135 @@
1
+ import { Group } from '@visx/group';
2
+ import {
3
+ Treemap,
4
+ hierarchy,
5
+ treemapSquarify,
6
+ treemapBinary,
7
+ treemapDice,
8
+ treemapSlice,
9
+ treemapSliceDice,
10
+ treemapResquarify
11
+ } from '@visx/hierarchy';
12
+ import { ParentSize } from '@visx/responsive';
13
+ import { cn } from '../../lib/utils';
14
+ import { useMemo } from 'react';
15
+
16
+ // Types
17
+ export type TreemapData = {
18
+ name: string;
19
+ size?: number; // Leaf nodes have size
20
+ parent?: string; // For explicit parent-child (optional if using nested structure)
21
+ children?: TreemapData[];
22
+ };
23
+
24
+ const tileMethods = {
25
+ binary: treemapBinary,
26
+ squarify: treemapSquarify,
27
+ resquarify: treemapResquarify,
28
+ slice: treemapSlice,
29
+ dice: treemapDice,
30
+ sliceDice: treemapSliceDice,
31
+ };
32
+
33
+ export type TreemapChartProps = {
34
+ data: TreemapData; // Root node
35
+ width?: number;
36
+ height?: number;
37
+ className?: string;
38
+ tileMethod?: keyof typeof tileMethods;
39
+ background?: string;
40
+ };
41
+
42
+ type TreemapChartContentProps = TreemapChartProps & {
43
+ width: number;
44
+ height: number;
45
+ };
46
+
47
+ function TreemapChartContent({
48
+ data,
49
+ width,
50
+ height,
51
+ className,
52
+ background = "fill-background",
53
+ tileMethod = "squarify"
54
+ }: TreemapChartContentProps) {
55
+
56
+ const root = useMemo(() => {
57
+ // If data is already a hierarchy tree
58
+ const rootHierarchy = hierarchy(data)
59
+ .sort((a, b) => (b.value || 0) - (a.value || 0));
60
+
61
+ // Sum values for layout
62
+ return rootHierarchy.sum((d) => d.size ?? 0);
63
+ }, [data]);
64
+
65
+ const tile = tileMethods[tileMethod] || treemapSquarify;
66
+
67
+ if (width < 10) return null;
68
+
69
+ return (
70
+ <div className={cn("relative", className)}>
71
+ <svg width={width} height={height} className="overflow-visible">
72
+ <rect width={width} height={height} rx={14} className={background} />
73
+ <Treemap<TreemapData>
74
+ top={0}
75
+ left={0}
76
+ root={root}
77
+ size={[width, height]}
78
+ tile={tile}
79
+ round
80
+ >
81
+ {treemap => {
82
+ // @visx/hierarchy Treemap passes the root node of the layout as the argument
83
+ const nodes = treemap.descendants();
84
+
85
+ return (
86
+ <Group>
87
+ {nodes.map((node, i) => {
88
+ const width = node.x1 - node.x0;
89
+ const height = node.y1 - node.y0;
90
+ // Skip root if we want, or render it as background. usually we skip root rect or render it transparent
91
+ if (node.depth === 0) return null;
92
+
93
+ return (
94
+ <Group key={`node-${i}`} top={node.y0} left={node.x0}>
95
+ <rect
96
+ width={width}
97
+ height={height}
98
+ className={cn("stroke-background stroke-[2px] transition-all hover:opacity-80",
99
+ node.depth === 1 ? "fill-primary" : "fill-accent"
100
+ )}
101
+ />
102
+ {width > 30 && height > 20 && (
103
+ <text
104
+ x={width / 2}
105
+ y={height / 2}
106
+ dy=".33em"
107
+ fontSize={10}
108
+ textAnchor="middle"
109
+ fill="white"
110
+ className="pointer-events-none font-medium truncate"
111
+ >
112
+ {node.data.name}
113
+ </text>
114
+ )}
115
+ </Group>
116
+ )
117
+ })}
118
+ </Group>
119
+ )
120
+ }}
121
+ </Treemap>
122
+ </svg>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ export const TreemapChart = (props: TreemapChartProps) => {
128
+ return (
129
+ <div className="w-full h-[300px]">
130
+ <ParentSize>
131
+ {({ width, height }) => <TreemapChartContent {...props} width={width} height={height} />}
132
+ </ParentSize>
133
+ </div>
134
+ )
135
+ }