waffle-charts-cli 0.1.3 → 0.1.5
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 +3 -3
- package/package.json +4 -4
- package/src/registry.js +5 -0
- package/templates/CandlestickChart.tsx +267 -0
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# WaffleCharts CLI 🧇
|
|
2
2
|
|
|
3
|
-
The official CLI for [WaffleCharts](https://
|
|
3
|
+
The official CLI for [WaffleCharts](https://surprisewaffles-io.github.io/waffle-charts).
|
|
4
4
|
|
|
5
5
|
> **Beautiful, Headless, Copy-Pasteable Charts for React.**
|
|
6
6
|
> Built with [Visx](https://airbnb.io/visx) and [Tailwind CSS](https://tailwindcss.com).
|
|
7
7
|
|
|
8
8
|
[](https://www.npmjs.com/package/waffle-charts-cli)
|
|
9
|
-
[](https://github.com/
|
|
9
|
+
[](https://github.com/surprisewaffles-io/waffle-charts/blob/main/LICENSE)
|
|
10
10
|
|
|
11
11
|
## Why WaffleCharts?
|
|
12
12
|
|
|
@@ -56,4 +56,4 @@ npx waffle-charts-cli add bar-chart line-chart
|
|
|
56
56
|
|
|
57
57
|
## License
|
|
58
58
|
|
|
59
|
-
MIT © [WaffleCharts](https://github.com/
|
|
59
|
+
MIT © [WaffleCharts](https://github.com/surprisewaffles-io/waffle-charts)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waffle-charts-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "CLI to add WaffleCharts components to your project",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"visx"
|
|
17
17
|
],
|
|
18
18
|
"repository": {
|
|
19
|
-
"url": "git+https://github.com/
|
|
19
|
+
"url": "git+https://github.com/surprisewaffles-io/waffle-charts.git"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://
|
|
21
|
+
"homepage": "https://surprisewaffles-io.github.io/waffle-charts",
|
|
22
22
|
"author": "WaffleCharts Team",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"files": [
|
|
@@ -38,4 +38,4 @@
|
|
|
38
38
|
"prompts": "^2.4.2"
|
|
39
39
|
},
|
|
40
40
|
"type": "module"
|
|
41
|
-
}
|
|
41
|
+
}
|
package/src/registry.js
CHANGED
|
@@ -58,5 +58,10 @@ export const registry = {
|
|
|
58
58
|
file: "ChordChart.tsx",
|
|
59
59
|
label: "Chord Diagram",
|
|
60
60
|
dependencies: ["@visx/chord", "@visx/scale", "@visx/tooltip", "@visx/shape", "@visx/responsive", "@visx/group", "clsx", "tailwind-merge"],
|
|
61
|
+
},
|
|
62
|
+
"candlestick-chart": {
|
|
63
|
+
file: "CandlestickChart.tsx",
|
|
64
|
+
label: "Candlestick Chart",
|
|
65
|
+
dependencies: ["@visx/group", "@visx/scale", "@visx/shape", "@visx/axis", "@visx/grid", "@visx/responsive", "@visx/tooltip", "@visx/event", "d3-array", "clsx", "tailwind-merge"],
|
|
61
66
|
}
|
|
62
67
|
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Group } from '@visx/group';
|
|
3
|
+
import { scaleTime, scaleLinear } from '@visx/scale';
|
|
4
|
+
import { Bar, Line } from '@visx/shape';
|
|
5
|
+
import { AxisBottom, AxisLeft } from '@visx/axis';
|
|
6
|
+
import { GridRows, GridColumns } from '@visx/grid';
|
|
7
|
+
import { ParentSize } from '@visx/responsive';
|
|
8
|
+
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
|
|
9
|
+
import { localPoint } from '@visx/event';
|
|
10
|
+
import { bisector, extent } from 'd3-array';
|
|
11
|
+
import { cn } from '../../lib/utils';
|
|
12
|
+
|
|
13
|
+
export type CandlestickChartProps<T> = {
|
|
14
|
+
data: T[];
|
|
15
|
+
xKey: keyof T;
|
|
16
|
+
openKey: keyof T;
|
|
17
|
+
highKey: keyof T;
|
|
18
|
+
lowKey: keyof T;
|
|
19
|
+
closeKey: keyof T;
|
|
20
|
+
className?: string;
|
|
21
|
+
upColor?: string;
|
|
22
|
+
downColor?: string;
|
|
23
|
+
xAxisLabel?: string;
|
|
24
|
+
yAxisLabel?: string;
|
|
25
|
+
showXAxis?: boolean;
|
|
26
|
+
showYAxis?: boolean;
|
|
27
|
+
showGrid?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function CandlestickChartContent<T>({
|
|
31
|
+
data,
|
|
32
|
+
width,
|
|
33
|
+
height,
|
|
34
|
+
xKey,
|
|
35
|
+
openKey,
|
|
36
|
+
highKey,
|
|
37
|
+
lowKey,
|
|
38
|
+
closeKey,
|
|
39
|
+
className,
|
|
40
|
+
upColor = "#22c55e", // green-500
|
|
41
|
+
downColor = "#ef4444", // red-500
|
|
42
|
+
xAxisLabel,
|
|
43
|
+
yAxisLabel,
|
|
44
|
+
showXAxis = true,
|
|
45
|
+
showYAxis = true,
|
|
46
|
+
showGrid = true,
|
|
47
|
+
}: CandlestickChartProps<T> & { width: number; height: number }) {
|
|
48
|
+
const margin = { top: 20, right: 30, bottom: 50, left: 50 };
|
|
49
|
+
const innerWidth = width - margin.left - margin.right;
|
|
50
|
+
const innerHeight = height - margin.top - margin.bottom;
|
|
51
|
+
|
|
52
|
+
// Accessors
|
|
53
|
+
const getX = (d: T) => d[xKey] as Date;
|
|
54
|
+
const getOpen = (d: T) => Number(d[openKey]);
|
|
55
|
+
const getHigh = (d: T) => Number(d[highKey]);
|
|
56
|
+
const getLow = (d: T) => Number(d[lowKey]);
|
|
57
|
+
const getClose = (d: T) => Number(d[closeKey]);
|
|
58
|
+
|
|
59
|
+
// Scales
|
|
60
|
+
const xScale = useMemo(
|
|
61
|
+
() =>
|
|
62
|
+
scaleTime({
|
|
63
|
+
range: [0, innerWidth],
|
|
64
|
+
domain: extent(data, getX) as [Date, Date],
|
|
65
|
+
}),
|
|
66
|
+
[innerWidth, data, getX]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const yScale = useMemo(
|
|
70
|
+
() =>
|
|
71
|
+
scaleLinear({
|
|
72
|
+
range: [innerHeight, 0],
|
|
73
|
+
domain: [
|
|
74
|
+
Math.min(...data.map(getLow)),
|
|
75
|
+
Math.max(...data.map(getHigh)),
|
|
76
|
+
],
|
|
77
|
+
nice: true,
|
|
78
|
+
}),
|
|
79
|
+
[innerHeight, data, getHigh, getLow]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Tooltip
|
|
83
|
+
const {
|
|
84
|
+
tooltipOpen,
|
|
85
|
+
tooltipLeft,
|
|
86
|
+
tooltipTop,
|
|
87
|
+
tooltipData,
|
|
88
|
+
hideTooltip,
|
|
89
|
+
showTooltip,
|
|
90
|
+
} = useTooltip<T>();
|
|
91
|
+
|
|
92
|
+
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
|
93
|
+
scroll: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const bisectDate = bisector<T, Date>((d) => new Date(d[xKey] as any)).left;
|
|
97
|
+
|
|
98
|
+
const handlePointerMove = (event: React.PointerEvent<SVGRectElement>) => {
|
|
99
|
+
const { x } = localPoint(event) || { x: 0 };
|
|
100
|
+
const x0 = xScale.invert(x);
|
|
101
|
+
const index = bisectDate(data, x0, 1);
|
|
102
|
+
const d0 = data[index - 1];
|
|
103
|
+
const d1 = data[index];
|
|
104
|
+
let d = d0;
|
|
105
|
+
if (d1 && getX(d1)) {
|
|
106
|
+
d = x0.valueOf() - getX(d0).valueOf() > getX(d1).valueOf() - x0.valueOf() ? d1 : d0;
|
|
107
|
+
}
|
|
108
|
+
showTooltip({
|
|
109
|
+
tooltipData: d,
|
|
110
|
+
tooltipLeft: xScale(getX(d)),
|
|
111
|
+
tooltipTop: yScale(Math.max(getOpen(d), getClose(d))),
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Calculate candle width dynamically based on data length
|
|
116
|
+
// Use 80% of the available space per data point, capped at a max width
|
|
117
|
+
const candleWidth = Math.min((innerWidth / data.length) * 0.8, 20);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className={cn("relative", className)}>
|
|
121
|
+
<svg ref={containerRef} width={width} height={height}>
|
|
122
|
+
<Group left={margin.left} top={margin.top}>
|
|
123
|
+
{showGrid && (
|
|
124
|
+
<>
|
|
125
|
+
<GridRows
|
|
126
|
+
scale={yScale}
|
|
127
|
+
width={innerWidth}
|
|
128
|
+
strokeDasharray="3,3"
|
|
129
|
+
stroke="hsl(var(--border, 214.3 31.8% 91.4%))"
|
|
130
|
+
strokeOpacity={0.5}
|
|
131
|
+
/>
|
|
132
|
+
<GridColumns
|
|
133
|
+
scale={xScale}
|
|
134
|
+
height={innerHeight}
|
|
135
|
+
strokeDasharray="3,3"
|
|
136
|
+
stroke="hsl(var(--border, 214.3 31.8% 91.4%))"
|
|
137
|
+
strokeOpacity={0.5}
|
|
138
|
+
/>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{data.map((d, i) => {
|
|
143
|
+
const open = getOpen(d);
|
|
144
|
+
const close = getClose(d);
|
|
145
|
+
const high = getHigh(d);
|
|
146
|
+
const low = getLow(d);
|
|
147
|
+
const x = xScale(getX(d));
|
|
148
|
+
const isUp = close > open;
|
|
149
|
+
const color = isUp ? upColor : downColor;
|
|
150
|
+
const barHeight = Math.abs(yScale(open) - yScale(close));
|
|
151
|
+
const barY = yScale(Math.max(open, close));
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Group key={i} left={x}>
|
|
155
|
+
{/* Wick */}
|
|
156
|
+
<Line
|
|
157
|
+
from={{ x: 0, y: yScale(low) }}
|
|
158
|
+
to={{ x: 0, y: yScale(high) }}
|
|
159
|
+
stroke={color}
|
|
160
|
+
strokeWidth={1.5}
|
|
161
|
+
/>
|
|
162
|
+
{/* Body */}
|
|
163
|
+
<Bar
|
|
164
|
+
x={-candleWidth / 2}
|
|
165
|
+
y={barY}
|
|
166
|
+
width={candleWidth}
|
|
167
|
+
height={Math.max(barHeight, 1)} // Ensure at least 1px height
|
|
168
|
+
fill={color}
|
|
169
|
+
className="cursor-pointer"
|
|
170
|
+
onFocus={() => { }}
|
|
171
|
+
onMouseOver={() => showTooltip({
|
|
172
|
+
tooltipData: d,
|
|
173
|
+
tooltipLeft: xScale(getX(d)) + margin.left,
|
|
174
|
+
tooltipTop: barY + margin.top
|
|
175
|
+
})}
|
|
176
|
+
onMouseOut={hideTooltip}
|
|
177
|
+
/>
|
|
178
|
+
</Group>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
|
|
182
|
+
{showXAxis && (
|
|
183
|
+
<AxisBottom
|
|
184
|
+
top={innerHeight}
|
|
185
|
+
scale={xScale}
|
|
186
|
+
stroke="hsl(var(--border, 214.3 31.8% 91.4%))"
|
|
187
|
+
tickStroke="hsl(var(--border, 214.3 31.8% 91.4%))"
|
|
188
|
+
label={xAxisLabel}
|
|
189
|
+
numTicks={width > 500 ? 10 : 5}
|
|
190
|
+
labelProps={{
|
|
191
|
+
fill: "hsl(var(--muted-foreground, 215.4 16.3% 46.9%))",
|
|
192
|
+
fontSize: 12,
|
|
193
|
+
textAnchor: 'middle',
|
|
194
|
+
dy: 0
|
|
195
|
+
}}
|
|
196
|
+
tickLabelProps={{
|
|
197
|
+
fill: "hsl(var(--muted-foreground, 215.4 16.3% 46.9%))",
|
|
198
|
+
fontSize: 11,
|
|
199
|
+
textAnchor: "middle",
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{showYAxis && (
|
|
205
|
+
<AxisLeft
|
|
206
|
+
scale={yScale}
|
|
207
|
+
stroke="hsl(var(--border, 214.3 31.8% 91.4%))"
|
|
208
|
+
tickStroke="hsl(var(--border, 214.3 31.8% 91.4%))"
|
|
209
|
+
label={yAxisLabel}
|
|
210
|
+
labelProps={{
|
|
211
|
+
fill: "hsl(var(--muted-foreground, 215.4 16.3% 46.9%))",
|
|
212
|
+
fontSize: 12,
|
|
213
|
+
textAnchor: 'middle',
|
|
214
|
+
dx: -10
|
|
215
|
+
}}
|
|
216
|
+
tickLabelProps={{
|
|
217
|
+
fill: "hsl(var(--muted-foreground, 215.4 16.3% 46.9%))",
|
|
218
|
+
fontSize: 11,
|
|
219
|
+
textAnchor: "end",
|
|
220
|
+
dx: -4,
|
|
221
|
+
dy: 4 // Center vertically
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
</Group>
|
|
226
|
+
</svg>
|
|
227
|
+
{tooltipOpen && tooltipData && (
|
|
228
|
+
<TooltipInPortal
|
|
229
|
+
top={tooltipTop}
|
|
230
|
+
left={tooltipLeft}
|
|
231
|
+
style={{
|
|
232
|
+
...defaultStyles,
|
|
233
|
+
backgroundColor: "hsl(var(--background))",
|
|
234
|
+
color: "hsl(var(--foreground))",
|
|
235
|
+
border: "1px solid hsl(var(--border))",
|
|
236
|
+
borderRadius: "0.5rem",
|
|
237
|
+
padding: "0.5rem",
|
|
238
|
+
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
|
|
239
|
+
zIndex: 50,
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
<div className="text-xs font-semibold mb-1">
|
|
243
|
+
{getX(tooltipData).toLocaleDateString()}
|
|
244
|
+
</div>
|
|
245
|
+
<div className="grid grid-cols-2 gap-x-3 text-xs">
|
|
246
|
+
<span className="text-muted-foreground">Open:</span>
|
|
247
|
+
<span className="font-mono">{getOpen(tooltipData).toFixed(2)}</span>
|
|
248
|
+
<span className="text-muted-foreground">High:</span>
|
|
249
|
+
<span className="font-mono">{getHigh(tooltipData).toFixed(2)}</span>
|
|
250
|
+
<span className="text-muted-foreground">Low:</span>
|
|
251
|
+
<span className="font-mono">{getLow(tooltipData).toFixed(2)}</span>
|
|
252
|
+
<span className="text-muted-foreground">Close:</span>
|
|
253
|
+
<span className="font-mono">{getClose(tooltipData).toFixed(2)}</span>
|
|
254
|
+
</div>
|
|
255
|
+
</TooltipInPortal>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function CandlestickChart<T>(props: CandlestickChartProps<T>) {
|
|
262
|
+
return (
|
|
263
|
+
<ParentSize>
|
|
264
|
+
{({ width, height }) => <CandlestickChartContent {...props} width={width} height={height} />}
|
|
265
|
+
</ParentSize>
|
|
266
|
+
);
|
|
267
|
+
}
|