Skip to content

Commit 3df5b42

Browse files
committed
Address React Doctor findings and simplify plotting hooks
1 parent 2f6fff4 commit 3df5b42

10 files changed

Lines changed: 211 additions & 168 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function App() {
5959
<div className="flex min-h-screen flex-col bg-background">
6060
<header className="border-b border-border bg-card py-4">
6161
<div className="container mx-auto px-4">
62-
<h1 className="text-2xl font-bold text-foreground">
62+
<h1 className="text-2xl font-semibold text-foreground">
6363
High-Performance CSV Plotter
6464
</h1>
6565
</div>

src/components/DataPlot.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ const DataPlot: React.FC<DataPlotProps> = React.memo(({ data }) => {
9393
}, [containerWidth]);
9494

9595
return (
96-
<div className="h-[440px] rounded border bg-white p-2" ref={containerRef}>
96+
<div className="h-[440px] rounded border border-border bg-card p-2" ref={containerRef}>
9797
{!data || data.length === 0 ? (
9898
<div className="flex h-full items-center justify-center">
99-
<p className="text-center text-gray-500">
99+
<p className="text-center text-muted-foreground">
100100
Select a CSV file to display the plot
101101
</p>
102102
</div>

src/components/FileSelector.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useRef } from 'react';
22
import { Button } from './ui/button';
33

44
interface FileSelectorProps {
@@ -9,6 +9,8 @@ interface FileSelectorProps {
99

1010
const FileSelector: React.FC<FileSelectorProps> = React.memo(
1111
({ onFileSelect, isLoading, progress }) => {
12+
const inputRef = useRef<HTMLInputElement>(null);
13+
1214
const handleFileChange = useCallback(
1315
(e: React.ChangeEvent<HTMLInputElement>) => {
1416
const file = e.target.files?.[0];
@@ -19,27 +21,30 @@ const FileSelector: React.FC<FileSelectorProps> = React.memo(
1921
[onFileSelect],
2022
);
2123

24+
const handleTriggerFileSelect = useCallback(() => {
25+
inputRef.current?.click();
26+
}, []);
27+
2228
return (
2329
<div className="mb-4">
2430
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-4">
2531
<input
32+
ref={inputRef}
2633
type="file"
2734
accept=".csv"
2835
onChange={handleFileChange}
2936
className="hidden"
3037
id="file-input"
3138
disabled={isLoading}
3239
/>
33-
<label htmlFor="file-input">
34-
<Button
35-
variant="outline"
36-
disabled={isLoading}
37-
asChild
38-
className="cursor-pointer"
39-
>
40-
<span>Select CSV File</span>
41-
</Button>
42-
</label>
40+
<Button
41+
variant="outline"
42+
disabled={isLoading}
43+
type="button"
44+
onClick={handleTriggerFileSelect}
45+
>
46+
Select CSV File
47+
</Button>
4348
<a
4449
href="https://onedrive.live.com/?redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvYy8zNWYwYjA3ZTAxZmNhYmQxL0lRQjQxOUFKWm1fMlNvcUpycThSdlBiYkFmWno1V1EzcmxoQnhLN0JLQzM5TDZzP2U9cU54TGdy&cid=35F0B07E01FCABD1&id=35F0B07E01FCABD1%21s09d0d7786f664af68a89aeaf11bcf6db&parId=35F0B07E01FCABD1%21112&o=OneUp"
4550
target="_blank"

src/components/PlotControls.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ const PlotControls: React.FC<PlotControlsProps> = React.memo(
314314
</div>
315315
{showTooltip && totalPoints === 0 && (
316316
<div
317-
className="pointer-events-none fixed z-50 rounded bg-black px-2 py-1 text-sm text-white"
317+
className="pointer-events-none fixed z-50 rounded bg-zinc-950 px-2 py-1 text-sm text-white"
318318
style={{
319319
left: `${mousePosition.x + 10}px`,
320320
top: `${mousePosition.y + 10}px`,

src/components/ui/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ function Button({
5555
);
5656
}
5757

58-
export { Button, buttonVariants };
58+
export { Button };

src/components/ui/card.tsx

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,6 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
3535
);
3636
}
3737

38-
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
39-
return (
40-
<div
41-
data-slot="card-description"
42-
className={cn('text-sm text-muted-foreground', className)}
43-
{...props}
44-
/>
45-
);
46-
}
47-
4838
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
4939
return (
5040
<div
@@ -55,21 +45,4 @@ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
5545
);
5646
}
5747

58-
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
59-
return (
60-
<div
61-
data-slot="card-footer"
62-
className={cn('flex items-center px-6', className)}
63-
{...props}
64-
/>
65-
);
66-
}
67-
68-
export {
69-
Card,
70-
CardHeader,
71-
CardFooter,
72-
CardTitle,
73-
CardDescription,
74-
CardContent,
75-
};
48+
export { Card, CardHeader, CardTitle, CardContent };

src/components/ui/slider.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ function Slider({
1111
max = 100,
1212
...props
1313
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
14+
const thumbIdPrefix = React.useId();
1415
const _values = React.useMemo(
1516
() =>
1617
Array.isArray(value)
@@ -47,10 +48,10 @@ function Slider({
4748
)}
4849
/>
4950
</SliderPrimitive.Track>
50-
{Array.from({ length: _values.length }, (_, index) => (
51+
{_values.map((_, index) => (
5152
<SliderPrimitive.Thumb
5253
data-slot="slider-thumb"
53-
key={index}
54+
key={`${thumbIdPrefix}-${index}`}
5455
className="block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
5556
/>
5657
))}

src/hooks/useDataStream.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { useState, useCallback, useEffect, useRef } from 'react';
22
import { DataPoint } from '@/types';
33

4+
type DataStreamStatus = 'idle' | 'processing';
5+
46
export function useDataStream() {
57
const [data, setData] = useState<DataPoint[]>([]);
6-
const [isLoading, setIsLoading] = useState(false);
8+
const [status, setStatus] = useState<DataStreamStatus>('idle');
79
const [progress, setProgress] = useState(0);
810
const [error, setError] = useState<string | null>(null);
911

@@ -20,7 +22,7 @@ export function useDataStream() {
2022
}, []);
2123

2224
const processFile = useCallback(async (file: File) => {
23-
setIsLoading(true);
25+
setStatus('processing');
2426
setError(null);
2527
setProgress(0);
2628
setData([]);
@@ -59,13 +61,13 @@ export function useDataStream() {
5961

6062
pointsCollectionRef.current.sort((a, b) => a.x - b.x);
6163
setData([...pointsCollectionRef.current]);
62-
setIsLoading(false);
64+
setStatus('idle');
6365
}
6466
};
6567

6668
workerRef.current.onerror = (e) => {
6769
setError(`Worker error: ${e.message}`);
68-
setIsLoading(false);
70+
setStatus('idle');
6971
};
7072

7173
const arrayBuffer = await file.arrayBuffer();
@@ -76,9 +78,10 @@ export function useDataStream() {
7678
} catch (err) {
7779
const message = err instanceof Error ? err.message : String(err);
7880
setError(`Error processing file: ${message}`);
79-
setIsLoading(false);
81+
setStatus('idle');
8082
}
8183
}, []);
8284

85+
const isLoading = status === 'processing';
8386
return { data, isLoading, progress, error, processFile };
8487
}

src/hooks/usePlotWindow.ts

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ interface UsePlotWindowProps {
1515
defaultDownsamplingThreshold?: number;
1616
}
1717

18+
interface PlaybackState {
19+
start: number;
20+
windowSize: number;
21+
isPlaying: boolean;
22+
}
23+
1824
export function usePlotWindow({
1925
data,
2026
defaultWindowSize = DEFAULT_WINDOW_SIZE,
@@ -23,47 +29,53 @@ export function usePlotWindow({
2329
defaultDownsamplingThreshold = DEFAULT_DOWNSAMPLING_THRESHOLD,
2430
}: UsePlotWindowProps) {
2531
const dataLength = data.length;
26-
const [start, setStart] = useState(0);
27-
const [windowSize, setWindowSize] = useState(defaultWindowSize);
28-
const [interval, setInterval] = useState(defaultInterval);
29-
const [increment, setIncrement] = useState(defaultIncrement);
30-
const [downsamplingThreshold, setDownsamplingThreshold] = useState(
32+
const [playbackState, setPlaybackState] = useState<PlaybackState>({
33+
start: 0,
34+
windowSize: defaultWindowSize,
35+
isPlaying: false,
36+
});
37+
const [interval, setIntervalState] = useState(defaultInterval);
38+
const [increment, setIncrementState] = useState(defaultIncrement);
39+
const [downsamplingThreshold, setDownsamplingThresholdState] = useState(
3140
defaultDownsamplingThreshold,
3241
);
33-
const [isPlaying, setIsPlaying] = useState(false);
3442

3543
const animationRef = useRef<number | null>(null);
3644
const lastUpdateTimeRef = useRef<number>(0);
37-
const maxStart = Math.max(dataLength - windowSize, 0);
45+
const maxStart = Math.max(dataLength - playbackState.windowSize, 0);
3846

3947
const togglePlay = useCallback(() => {
40-
setIsPlaying((prev) => !prev);
48+
setPlaybackState((prev) => ({
49+
...prev,
50+
isPlaying: !prev.isPlaying,
51+
}));
4152
}, []);
4253

4354
const animateFrame = useCallback(
4455
(timestamp: number) => {
4556
if (timestamp - lastUpdateTimeRef.current >= interval) {
46-
setStart((prev) => {
47-
const newStart = prev + increment;
48-
if (newStart >= maxStart) {
49-
setIsPlaying(false);
50-
return maxStart;
51-
}
52-
return newStart;
57+
setPlaybackState((prev) => {
58+
const nextStart = Math.min(prev.start + increment, maxStart);
59+
60+
return {
61+
...prev,
62+
start: nextStart,
63+
isPlaying: nextStart < maxStart,
64+
};
5365
});
5466

5567
lastUpdateTimeRef.current = timestamp;
5668
}
5769

58-
if (isPlaying) {
70+
if (playbackState.isPlaying) {
5971
animationRef.current = requestAnimationFrame(animateFrame);
6072
}
6173
},
62-
[interval, increment, isPlaying, maxStart],
74+
[increment, interval, maxStart, playbackState.isPlaying],
6375
);
6476

6577
useEffect(() => {
66-
if (isPlaying) {
78+
if (playbackState.isPlaying) {
6779
lastUpdateTimeRef.current = performance.now();
6880
animationRef.current = requestAnimationFrame(animateFrame);
6981
} else if (animationRef.current) {
@@ -77,60 +89,75 @@ export function usePlotWindow({
7789
animationRef.current = null;
7890
}
7991
};
80-
}, [isPlaying, animateFrame]);
92+
}, [playbackState.isPlaying, animateFrame]);
8193

8294
useEffect(() => {
83-
setIsPlaying(false);
84-
setStart(0);
85-
86-
if (dataLength > 0) {
87-
setWindowSize(Math.min(defaultWindowSize, dataLength));
88-
} else {
89-
setWindowSize(defaultWindowSize);
90-
}
95+
setPlaybackState((prev) => ({
96+
...prev,
97+
isPlaying: false,
98+
start: 0,
99+
windowSize:
100+
dataLength > 0
101+
? Math.min(defaultWindowSize, dataLength)
102+
: defaultWindowSize,
103+
}));
91104
}, [dataLength, defaultWindowSize]);
92105

93106
const handleStartChange = useCallback(
94107
(value: number) => {
95-
if (dataLength === 0) {
96-
setStart(0);
97-
return;
98-
}
99-
100-
setStart(Math.max(0, Math.min(value, maxStart)));
108+
setPlaybackState((prev) => ({
109+
...prev,
110+
start: dataLength === 0 ? 0 : Math.max(0, Math.min(value, maxStart)),
111+
}));
101112
},
102113
[dataLength, maxStart],
103114
);
104115

105116
const handleWindowSizeChange = useCallback(
106117
(value: number) => {
107118
if (dataLength === 0) {
108-
setWindowSize(defaultWindowSize);
109-
setStart(0);
119+
setPlaybackState((prev) => ({
120+
...prev,
121+
start: 0,
122+
windowSize: defaultWindowSize,
123+
}));
110124
return;
111125
}
112126

113127
const nextWindowSize = Math.max(1, Math.min(value, dataLength));
114-
setWindowSize(nextWindowSize);
115-
setStart((currentStart) =>
116-
Math.min(currentStart, Math.max(dataLength - nextWindowSize, 0)),
117-
);
128+
setPlaybackState((prev) => ({
129+
...prev,
130+
windowSize: nextWindowSize,
131+
start: Math.min(prev.start, Math.max(dataLength - nextWindowSize, 0)),
132+
}));
118133
},
119134
[dataLength, defaultWindowSize],
120135
);
121136

137+
const handleIntervalChange = useCallback((value: number) => {
138+
setIntervalState(value);
139+
}, []);
140+
141+
const handleIncrementChange = useCallback((value: number) => {
142+
setIncrementState(value);
143+
}, []);
144+
145+
const handleDownsamplingThresholdChange = useCallback((value: number) => {
146+
setDownsamplingThresholdState(value);
147+
}, []);
148+
122149
return {
123-
start,
150+
start: playbackState.start,
124151
setStart: handleStartChange,
125-
windowSize,
152+
windowSize: playbackState.windowSize,
126153
setWindowSize: handleWindowSizeChange,
127154
interval,
128-
setInterval,
155+
setInterval: handleIntervalChange,
129156
increment,
130-
setIncrement,
157+
setIncrement: handleIncrementChange,
131158
downsamplingThreshold,
132-
setDownsamplingThreshold,
133-
isPlaying,
159+
setDownsamplingThreshold: handleDownsamplingThresholdChange,
160+
isPlaying: playbackState.isPlaying,
134161
togglePlay,
135162
};
136163
}

0 commit comments

Comments
 (0)