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,193 @@
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 BubbleChartProps<T> = {
13
+ data: T[];
14
+ xKey: keyof T;
15
+ yKey: keyof T;
16
+ zKey: keyof T; // Radius
17
+ className?: string; // Wrapper class
18
+ pointClassName?: string; // Point color/style
19
+ width?: number;
20
+ height?: number;
21
+ minRadius?: number;
22
+ maxRadius?: number;
23
+ };
24
+
25
+ // Internal component
26
+ type BubbleChartContentProps<T> = BubbleChartProps<T> & {
27
+ width: number;
28
+ height: number;
29
+ };
30
+
31
+ function BubbleChartContent<T>({
32
+ data,
33
+ width,
34
+ height,
35
+ xKey,
36
+ yKey,
37
+ zKey,
38
+ className,
39
+ pointClassName = "fill-primary/50", // Use opacity for bubbles
40
+ minRadius = 4,
41
+ maxRadius = 30,
42
+ }: BubbleChartContentProps<T>) {
43
+ // Config
44
+ const margin = { top: 40, right: 30, bottom: 50, left: 50 };
45
+ const xMax = width - margin.left - margin.right;
46
+ const yMax = height - margin.top - margin.bottom;
47
+
48
+ // Accessors
49
+ const getX = (d: T) => Number(d[xKey]);
50
+ const getY = (d: T) => Number(d[yKey]);
51
+ const getZ = (d: T) => Number(d[zKey]);
52
+
53
+ // Scales
54
+ const xScale = useMemo(
55
+ () =>
56
+ scaleLinear<number>({
57
+ range: [0, xMax],
58
+ round: true,
59
+ domain: [0, Math.max(...data.map(getX)) * 1.1], // Add padding
60
+ }),
61
+ [xMax, data, xKey],
62
+ );
63
+
64
+ const yScale = useMemo(
65
+ () =>
66
+ scaleLinear<number>({
67
+ range: [yMax, 0],
68
+ round: true,
69
+ domain: [0, Math.max(...data.map(getY)) * 1.1],
70
+ }),
71
+ [yMax, data, yKey],
72
+ );
73
+
74
+ const zScale = useMemo(
75
+ () =>
76
+ scaleLinear<number>({
77
+ range: [minRadius, maxRadius],
78
+ round: true,
79
+ domain: [Math.min(...data.map(getZ)), Math.max(...data.map(getZ))],
80
+ }),
81
+ [minRadius, maxRadius, data, zKey],
82
+ );
83
+
84
+ // Tooltip
85
+ const {
86
+ tooltipOpen,
87
+ tooltipLeft,
88
+ tooltipTop,
89
+ tooltipData,
90
+ hideTooltip,
91
+ showTooltip,
92
+ } = useTooltip<T>();
93
+
94
+ const { containerRef, TooltipInPortal } = useTooltipInPortal({
95
+ scroll: true,
96
+ });
97
+
98
+ if (width < 10) return null;
99
+
100
+ return (
101
+ <div className={cn("relative", className)}>
102
+ <svg ref={containerRef} width={width} height={height} className="overflow-visible">
103
+ <Group left={margin.left} top={margin.top}>
104
+ <Grid
105
+ xScale={xScale}
106
+ yScale={yScale}
107
+ width={xMax}
108
+ height={yMax}
109
+ stroke="hsl(var(--border))"
110
+ strokeOpacity={0.4}
111
+ />
112
+ <AxisLeft
113
+ scale={yScale}
114
+ stroke="transparent"
115
+ tickStroke="hsl(var(--border))"
116
+ tickLabelProps={{
117
+ fill: "hsl(var(--muted-foreground))",
118
+ fontSize: 11,
119
+ textAnchor: "end",
120
+ dx: -4,
121
+ dy: 4,
122
+ }}
123
+ />
124
+ <AxisBottom
125
+ top={yMax}
126
+ scale={xScale}
127
+ stroke="hsl(var(--border))"
128
+ tickStroke="hsl(var(--border))"
129
+ tickLabelProps={{
130
+ fill: "hsl(var(--muted-foreground))",
131
+ fontSize: 11,
132
+ textAnchor: "middle",
133
+ }}
134
+ />
135
+ {data.map((d, i) => {
136
+ const cx = xScale(getX(d));
137
+ const cy = yScale(getY(d));
138
+ const r = zScale(getZ(d));
139
+
140
+ return (
141
+ <Circle
142
+ key={`point-${i}`}
143
+ cx={cx}
144
+ cy={cy}
145
+ r={r}
146
+ className={cn("transition-all duration-300 hover:opacity-80 cursor-pointer stroke-background stroke-1", pointClassName)}
147
+ onMouseEnter={() => {
148
+ showTooltip({
149
+ tooltipData: d,
150
+ tooltipLeft: cx + margin.left, // absolute relative to container
151
+ tooltipTop: cy + margin.top, // absolute relative to container
152
+ });
153
+ }}
154
+ onMouseLeave={() => hideTooltip()}
155
+ />
156
+ );
157
+ })}
158
+ </Group>
159
+ </svg>
160
+ {tooltipOpen && tooltipData && (
161
+ <TooltipInPortal
162
+ top={tooltipTop}
163
+ left={tooltipLeft}
164
+ style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
165
+ >
166
+ <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">
167
+ <div className="flex flex-col gap-1">
168
+ <span className="font-semibold text-xs text-muted-foreground">Values</span>
169
+ <div className="grid grid-cols-2 gap-x-2 gap-y-0.5 mt-1">
170
+ <span>X:</span>
171
+ <span className="font-mono">{String(getX(tooltipData))}</span>
172
+ <span>Y:</span>
173
+ <span className="font-mono">{String(getY(tooltipData))}</span>
174
+ <span>Z:</span>
175
+ <span className="font-mono">{String(getZ(tooltipData))}</span>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </TooltipInPortal>
180
+ )}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ export const BubbleChart = <T,>(props: BubbleChartProps<T>) => {
186
+ return (
187
+ <div className="w-full h-[300px]">
188
+ <ParentSize>
189
+ {({ width, height }) => <BubbleChartContent {...props} width={width} height={height} />}
190
+ </ParentSize>
191
+ </div>
192
+ )
193
+ }
@@ -0,0 +1,155 @@
1
+ import { Group } from '@visx/group';
2
+ import { scaleLinear } from '@visx/scale';
3
+ import { HeatmapRect } from '@visx/heatmap';
4
+ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
5
+ import { ParentSize } from '@visx/responsive';
6
+ import { cn } from '../../lib/utils';
7
+ import { useMemo } from 'react';
8
+
9
+ // Types
10
+ export type HeatmapData = {
11
+ bin: number;
12
+ bins: {
13
+ bin: number;
14
+ count: number;
15
+ }[];
16
+ };
17
+
18
+ export type HeatmapChartProps = {
19
+ data: HeatmapData[];
20
+ width?: number;
21
+ height?: number;
22
+ className?: string;
23
+ colorRange?: [string, string]; // Hex colors for min/max
24
+ gap?: number;
25
+ };
26
+
27
+ type HeatmapChartContentProps = HeatmapChartProps & {
28
+ width: number;
29
+ height: number;
30
+ };
31
+
32
+ function HeatmapChartContent({
33
+ data,
34
+ width,
35
+ height,
36
+ className,
37
+ colorRange = ['#e2e8f0', '#0f172a'], // Default slate-200 to slate-900 (using hex for interpolation usually better, but Visx scale accepts colors)
38
+ gap = 2,
39
+ }: HeatmapChartContentProps) {
40
+ const margin = { top: 10, right: 10, bottom: 20, left: 20 };
41
+ const xMax = width - margin.left - margin.right;
42
+ const yMax = height - margin.top - margin.bottom;
43
+
44
+ // Helpers
45
+ const binWidth = xMax / data.length;
46
+
47
+ // Scales
48
+ const xScale = useMemo(
49
+ () =>
50
+ scaleLinear<number>({
51
+ domain: [0, data.length],
52
+ range: [0, xMax],
53
+ }),
54
+ [xMax, data],
55
+ );
56
+
57
+ const yScale = useMemo(
58
+ () =>
59
+ scaleLinear<number>({
60
+ domain: [0, data[0].bins.length],
61
+ range: [yMax, 0],
62
+ }),
63
+ [yMax, data],
64
+ );
65
+
66
+ const maxCount = Math.max(...data.flatMap((d) => d.bins.map((b) => b.count)));
67
+ const colorScale = useMemo(
68
+ () =>
69
+ scaleLinear<string>({
70
+ domain: [0, maxCount],
71
+ range: colorRange,
72
+ }),
73
+ [maxCount, colorRange],
74
+ );
75
+
76
+ // Tooltip
77
+ const {
78
+ tooltipOpen,
79
+ tooltipLeft,
80
+ tooltipTop,
81
+ tooltipData,
82
+ hideTooltip,
83
+ showTooltip,
84
+ } = useTooltip<number>(); // Tooltip data is just the bin count (number)
85
+
86
+ const { containerRef, TooltipInPortal } = useTooltipInPortal({
87
+ scroll: true,
88
+ });
89
+
90
+ if (width < 10) return null;
91
+
92
+ return (
93
+ <div className={cn("relative", className)}>
94
+ <svg ref={containerRef} width={width} height={height} className="overflow-visible">
95
+ <Group left={margin.left} top={margin.top}>
96
+ <HeatmapRect<HeatmapData, { bin: number; count: number }>
97
+ data={data}
98
+ xScale={xScale}
99
+ yScale={yScale}
100
+ colorScale={colorScale}
101
+ binWidth={binWidth}
102
+ binHeight={binWidth}
103
+ gap={gap}
104
+ >
105
+ {(heatmap) =>
106
+ heatmap.map((heatmapBins) =>
107
+ heatmapBins.map((bin) => (
108
+ <rect
109
+ key={`heatmap-rect-${bin.row}-${bin.column}`}
110
+ className="transition-all duration-300 hover:opacity-80 cursor-pointer"
111
+ width={bin.width}
112
+ height={bin.height}
113
+ x={bin.x}
114
+ y={bin.y}
115
+ fill={bin.color}
116
+ rx={2}
117
+ onMouseEnter={() => {
118
+ showTooltip({
119
+ tooltipData: bin.count ?? 0, // Ensure strictly number
120
+ tooltipLeft: bin.x + margin.left + bin.width / 2,
121
+ tooltipTop: bin.y + margin.top,
122
+ });
123
+ }}
124
+ onMouseLeave={() => hideTooltip()}
125
+ />
126
+ ))
127
+ )
128
+ }
129
+ </HeatmapRect>
130
+ </Group>
131
+ </svg>
132
+ {tooltipOpen && tooltipData !== undefined && (
133
+ <TooltipInPortal
134
+ top={tooltipTop}
135
+ left={tooltipLeft}
136
+ style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
137
+ >
138
+ <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">
139
+ <p className="font-semibold">Value: {tooltipData}</p>
140
+ </div>
141
+ </TooltipInPortal>
142
+ )}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ export const HeatmapChart = (props: HeatmapChartProps) => {
148
+ return (
149
+ <div className="w-full h-[300px]">
150
+ <ParentSize>
151
+ {({ width, height }) => <HeatmapChartContent {...props} width={width} height={height} />}
152
+ </ParentSize>
153
+ </div>
154
+ )
155
+ }
@@ -0,0 +1,230 @@
1
+ import { useMemo } from 'react';
2
+ import { AreaClosed, LinePath, Bar } from '@visx/shape';
3
+ import { curveMonotoneX } from '@visx/curve';
4
+ import { Group } from '@visx/group';
5
+ import { scaleTime, scaleLinear } from '@visx/scale';
6
+ import { AxisBottom, AxisLeft } from '@visx/axis';
7
+ import { GridRows, GridColumns } from '@visx/grid';
8
+ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
9
+ import { localPoint } from '@visx/event';
10
+ import { ParentSize } from '@visx/responsive';
11
+ import { cn } from '../../lib/utils';
12
+ import { GlyphCircle } from '@visx/glyph';
13
+ import { bisector } from 'd3-array';
14
+
15
+ // Types
16
+ export type LineChartProps<T> = {
17
+ data: T[];
18
+ xKey: keyof T;
19
+ yKey: keyof T;
20
+ className?: string;
21
+ lineColor?: string;
22
+ areaColor?: string; // CSS class for area fill
23
+ width?: number;
24
+ height?: number;
25
+ };
26
+
27
+ type LineChartContentProps<T> = LineChartProps<T> & {
28
+ width: number;
29
+ height: number;
30
+ };
31
+
32
+ function LineChartContent<T>({
33
+ data,
34
+ width,
35
+ height,
36
+ xKey,
37
+ yKey,
38
+ className,
39
+ lineColor = "stroke-primary",
40
+ areaColor = "text-primary", // using text color to set fill via currentColor/opacity
41
+ }: LineChartContentProps<T>) {
42
+ // Config
43
+ const margin = { top: 40, right: 30, bottom: 50, left: 50 };
44
+ const xMax = width - margin.left - margin.right;
45
+ const yMax = height - margin.top - margin.bottom;
46
+
47
+ // Accessors
48
+ const getX = (d: T) => new Date(d[xKey] as string | number | Date);
49
+ const getY = (d: T) => Number(d[yKey]);
50
+
51
+ // Bisector for tooltip
52
+ const bisectDate = bisector<T, Date>(d => getX(d)).left;
53
+
54
+ // Scales
55
+ const xScale = useMemo(
56
+ () =>
57
+ scaleTime({
58
+ range: [0, xMax],
59
+ domain: [Math.min(...data.map(d => getX(d).getTime())), Math.max(...data.map(d => getX(d).getTime()))],
60
+ }),
61
+ [xMax, data, xKey],
62
+ );
63
+
64
+ const yScale = useMemo(
65
+ () =>
66
+ scaleLinear<number>({
67
+ range: [yMax, 0],
68
+ round: true,
69
+ domain: [0, Math.max(...data.map(getY)) * 1.1], // Add some padding
70
+ }),
71
+ [yMax, data, yKey],
72
+ );
73
+
74
+ // Tooltip
75
+ const {
76
+ tooltipOpen,
77
+ tooltipLeft,
78
+ tooltipTop,
79
+ tooltipData,
80
+ hideTooltip,
81
+ showTooltip,
82
+ } = useTooltip<T>();
83
+
84
+ const { containerRef, TooltipInPortal } = useTooltipInPortal({
85
+ scroll: true,
86
+ });
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(getY(d)) + margin.top,
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
+ {/* Defs for gradient */}
114
+ <defs>
115
+ <linearGradient id="area-gradient" x1="0" y1="0" x2="0" y2="1">
116
+ <stop offset="0%" stopColor="currentColor" stopOpacity={0.2} className={areaColor} />
117
+ <stop offset="100%" stopColor="currentColor" stopOpacity={0} className={areaColor} />
118
+ </linearGradient>
119
+ </defs>
120
+
121
+ <Group left={margin.left} top={margin.top}>
122
+ <GridRows scale={yScale} width={xMax} height={yMax} strokeDasharray="3,3" stroke="hsl(var(--border))" tickValues={[...yScale.ticks(5)]} />
123
+ <GridColumns scale={xScale} width={xMax} height={yMax} strokeDasharray="3,3" stroke="hsl(var(--border))" />
124
+
125
+ <AxisBottom
126
+ top={yMax}
127
+ scale={xScale}
128
+ stroke="hsl(var(--border))"
129
+ tickStroke="hsl(var(--border))"
130
+ tickLabelProps={{
131
+ fill: "hsl(var(--muted-foreground))",
132
+ fontSize: 11,
133
+ textAnchor: "middle",
134
+ }}
135
+ numTicks={width > 520 ? 10 : 5}
136
+ />
137
+ <AxisLeft
138
+ scale={yScale}
139
+ stroke="transparent"
140
+ tickStroke="hsl(var(--border))"
141
+ tickLabelProps={{
142
+ fill: "hsl(var(--muted-foreground))",
143
+ fontSize: 11,
144
+ textAnchor: "end",
145
+ dx: -4,
146
+ dy: 4,
147
+ }}
148
+ numTicks={5}
149
+ />
150
+
151
+ <AreaClosed
152
+ data={data}
153
+ x={d => xScale(getX(d)) ?? 0}
154
+ y={d => yScale(getY(d)) ?? 0}
155
+ yScale={yScale}
156
+ curve={curveMonotoneX}
157
+ fill="url(#area-gradient)"
158
+ className={areaColor} // Pass helper class to set currentColor if needed
159
+ />
160
+
161
+ <LinePath
162
+ data={data}
163
+ x={d => xScale(getX(d)) ?? 0}
164
+ y={d => yScale(getY(d)) ?? 0}
165
+ curve={curveMonotoneX}
166
+ strokeWidth={2}
167
+ className={cn("fill-transparent transition-all", lineColor)}
168
+ />
169
+
170
+ {/* Tooltip Overlay */}
171
+ <Bar
172
+ x={0}
173
+ y={0}
174
+ width={xMax}
175
+ height={yMax}
176
+ fill="transparent"
177
+ rx={14}
178
+ onTouchStart={handleTooltip}
179
+ onTouchMove={handleTooltip}
180
+ onMouseMove={handleTooltip}
181
+ onMouseLeave={() => hideTooltip()}
182
+ />
183
+
184
+ {/* Tooltip Dot */}
185
+ {tooltipOpen && tooltipData && (
186
+ <g>
187
+ <GlyphCircle
188
+ left={xScale(getX(tooltipData)) ?? 0}
189
+ top={yScale(getY(tooltipData)) ?? 0}
190
+ size={50}
191
+ className={cn("fill-background", lineColor)}
192
+ strokeWidth={2}
193
+ stroke="currentColor" // Inherits from parent or setting explicitly
194
+ />
195
+ <circle
196
+ cx={xScale(getX(tooltipData)) ?? 0}
197
+ cy={yScale(getY(tooltipData)) ?? 0}
198
+ r={4}
199
+ className={cn("fill-background stroke-2", lineColor)}
200
+ />
201
+ </g>
202
+ )}
203
+
204
+ </Group>
205
+ </svg>
206
+ {tooltipOpen && tooltipData && (
207
+ <TooltipInPortal
208
+ top={tooltipTop}
209
+ left={tooltipLeft}
210
+ style={{ ...defaultStyles, padding: 0, borderRadius: 0, boxShadow: 'none', background: 'transparent' }}
211
+ >
212
+ <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">
213
+ <p className="font-semibold">{String(getY(tooltipData))}</p>
214
+ <p className="text-xs text-muted-foreground">{getX(tooltipData).toLocaleDateString()}</p>
215
+ </div>
216
+ </TooltipInPortal>
217
+ )}
218
+ </div>
219
+ );
220
+ }
221
+
222
+ export const LineChart = <T,>(props: LineChartProps<T>) => {
223
+ return (
224
+ <div className="w-full h-[300px]">
225
+ <ParentSize>
226
+ {({ width, height }) => <LineChartContent {...props} width={width} height={height} />}
227
+ </ParentSize>
228
+ </div>
229
+ )
230
+ }