waffle-charts-cli 0.1.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waffle-charts-cli",
3
- "version": "0.1.4",
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": {
@@ -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
+ }