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 +31 -0
- package/package.json +40 -0
- package/src/commands/add.js +91 -0
- package/src/registry.js +47 -0
- package/templates/AreaChart.tsx +205 -0
- package/templates/BarChart.tsx +165 -0
- package/templates/BubbleChart.tsx +193 -0
- package/templates/HeatmapChart.tsx +155 -0
- package/templates/LineChart.tsx +230 -0
- package/templates/PieChart.tsx +177 -0
- package/templates/RadarChart.tsx +163 -0
- package/templates/ScatterChart.tsx +167 -0
- package/templates/TreemapChart.tsx +135 -0
|
@@ -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
|
+
}
|