Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function App() {
<div className="flex min-h-screen flex-col bg-background">
<header className="border-b border-border bg-card py-4">
<div className="container mx-auto px-4">
<h1 className="text-2xl font-bold text-foreground">
<h1 className="text-2xl font-semibold text-foreground">
High-Performance CSV Plotter
</h1>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/DataPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ const DataPlot: React.FC<DataPlotProps> = React.memo(({ data }) => {
}, [containerWidth]);

return (
<div className="h-[440px] rounded border bg-white p-2" ref={containerRef}>
<div className="h-[440px] rounded border border-border bg-card p-2" ref={containerRef}>
{!data || data.length === 0 ? (
<div className="flex h-full items-center justify-center">
<p className="text-center text-gray-500">
<p className="text-center text-muted-foreground">
Select a CSV file to display the plot
</p>
</div>
Expand Down
27 changes: 16 additions & 11 deletions src/components/FileSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useRef } from 'react';
import { Button } from './ui/button';

interface FileSelectorProps {
Expand All @@ -9,6 +9,8 @@ interface FileSelectorProps {

const FileSelector: React.FC<FileSelectorProps> = React.memo(
({ onFileSelect, isLoading, progress }) => {
const inputRef = useRef<HTMLInputElement>(null);

const handleFileChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
Expand All @@ -19,27 +21,30 @@ const FileSelector: React.FC<FileSelectorProps> = React.memo(
[onFileSelect],
);

const handleTriggerFileSelect = useCallback(() => {
inputRef.current?.click();
Comment thread
abdusabri marked this conversation as resolved.
Outdated
}, []);

return (
<div className="mb-4">
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-4">
<input
ref={inputRef}
type="file"
accept=".csv"
onChange={handleFileChange}
className="hidden"
id="file-input"
disabled={isLoading}
/>
<label htmlFor="file-input">
<Button
variant="outline"
disabled={isLoading}
asChild
className="cursor-pointer"
>
<span>Select CSV File</span>
</Button>
</label>
<Button
variant="outline"
disabled={isLoading}
type="button"
onClick={handleTriggerFileSelect}
>
Select CSV File
</Button>
<a
href="https://onedrive.live.com/?redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvYy8zNWYwYjA3ZTAxZmNhYmQxL0lRQjQxOUFKWm1fMlNvcUpycThSdlBiYkFmWno1V1EzcmxoQnhLN0JLQzM5TDZzP2U9cU54TGdy&cid=35F0B07E01FCABD1&id=35F0B07E01FCABD1%21s09d0d7786f664af68a89aeaf11bcf6db&parId=35F0B07E01FCABD1%21112&o=OneUp"
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion src/components/PlotControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ const PlotControls: React.FC<PlotControlsProps> = React.memo(
</div>
{showTooltip && totalPoints === 0 && (
<div
className="pointer-events-none fixed z-50 rounded bg-black px-2 py-1 text-sm text-white"
className="pointer-events-none fixed z-50 rounded bg-zinc-950 px-2 py-1 text-sm text-white"
style={{
left: `${mousePosition.x + 10}px`,
top: `${mousePosition.y + 10}px`,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ function Button({
);
}

export { Button, buttonVariants };
export { Button };
29 changes: 1 addition & 28 deletions src/components/ui/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,6 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
);
}

function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
);
}

function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
Expand All @@ -55,21 +45,4 @@ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
);
}

function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6', className)}
{...props}
/>
);
}

export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
export { Card, CardHeader, CardTitle, CardContent };
5 changes: 3 additions & 2 deletions src/components/ui/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function Slider({
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const thumbIdPrefix = React.useId();
const _values = React.useMemo(
() =>
Array.isArray(value)
Expand Down Expand Up @@ -47,10 +48,10 @@ function Slider({
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
{_values.map((_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
key={`${thumbIdPrefix}-${index}`}
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"
/>
))}
Expand Down
13 changes: 8 additions & 5 deletions src/hooks/useDataStream.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { DataPoint } from '@/types';

type DataStreamStatus = 'idle' | 'processing';

export function useDataStream() {
const [data, setData] = useState<DataPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [status, setStatus] = useState<DataStreamStatus>('idle');
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);

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

const processFile = useCallback(async (file: File) => {
setIsLoading(true);
setStatus('processing');
setError(null);
setProgress(0);
setData([]);
Expand Down Expand Up @@ -59,13 +61,13 @@ export function useDataStream() {

pointsCollectionRef.current.sort((a, b) => a.x - b.x);
setData([...pointsCollectionRef.current]);
setIsLoading(false);
setStatus('idle');
}
};

workerRef.current.onerror = (e) => {
setError(`Worker error: ${e.message}`);
setIsLoading(false);
setStatus('idle');
};

const arrayBuffer = await file.arrayBuffer();
Expand All @@ -76,9 +78,10 @@ export function useDataStream() {
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setError(`Error processing file: ${message}`);
setIsLoading(false);
setStatus('idle');
}
}, []);

const isLoading = status === 'processing';
return { data, isLoading, progress, error, processFile };
}
117 changes: 72 additions & 45 deletions src/hooks/usePlotWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ interface UsePlotWindowProps {
defaultDownsamplingThreshold?: number;
}

interface PlaybackState {
start: number;
windowSize: number;
isPlaying: boolean;
}

export function usePlotWindow({
data,
defaultWindowSize = DEFAULT_WINDOW_SIZE,
Expand All @@ -23,47 +29,53 @@ export function usePlotWindow({
defaultDownsamplingThreshold = DEFAULT_DOWNSAMPLING_THRESHOLD,
}: UsePlotWindowProps) {
const dataLength = data.length;
const [start, setStart] = useState(0);
const [windowSize, setWindowSize] = useState(defaultWindowSize);
const [interval, setInterval] = useState(defaultInterval);
const [increment, setIncrement] = useState(defaultIncrement);
const [downsamplingThreshold, setDownsamplingThreshold] = useState(
const [playbackState, setPlaybackState] = useState<PlaybackState>({
start: 0,
windowSize: defaultWindowSize,
isPlaying: false,
});
const [interval, setIntervalState] = useState(defaultInterval);
const [increment, setIncrementState] = useState(defaultIncrement);
const [downsamplingThreshold, setDownsamplingThresholdState] = useState(
defaultDownsamplingThreshold,
);
const [isPlaying, setIsPlaying] = useState(false);

const animationRef = useRef<number | null>(null);
const lastUpdateTimeRef = useRef<number>(0);
const maxStart = Math.max(dataLength - windowSize, 0);
const maxStart = Math.max(dataLength - playbackState.windowSize, 0);

const togglePlay = useCallback(() => {
setIsPlaying((prev) => !prev);
setPlaybackState((prev) => ({
...prev,
isPlaying: !prev.isPlaying,
}));
}, []);

const animateFrame = useCallback(
(timestamp: number) => {
if (timestamp - lastUpdateTimeRef.current >= interval) {
setStart((prev) => {
const newStart = prev + increment;
if (newStart >= maxStart) {
setIsPlaying(false);
return maxStart;
}
return newStart;
setPlaybackState((prev) => {
const nextStart = Math.min(prev.start + increment, maxStart);

return {
...prev,
start: nextStart,
isPlaying: nextStart < maxStart,
};
});

lastUpdateTimeRef.current = timestamp;
}

if (isPlaying) {
if (playbackState.isPlaying) {
animationRef.current = requestAnimationFrame(animateFrame);
}
},
[interval, increment, isPlaying, maxStart],
[increment, interval, maxStart, playbackState.isPlaying],
);

useEffect(() => {
if (isPlaying) {
if (playbackState.isPlaying) {
lastUpdateTimeRef.current = performance.now();
animationRef.current = requestAnimationFrame(animateFrame);
} else if (animationRef.current) {
Expand All @@ -77,60 +89,75 @@ export function usePlotWindow({
animationRef.current = null;
}
};
}, [isPlaying, animateFrame]);
}, [playbackState.isPlaying, animateFrame]);

useEffect(() => {
setIsPlaying(false);
setStart(0);

if (dataLength > 0) {
setWindowSize(Math.min(defaultWindowSize, dataLength));
} else {
setWindowSize(defaultWindowSize);
}
setPlaybackState((prev) => ({
...prev,
isPlaying: false,
start: 0,
windowSize:
dataLength > 0
? Math.min(defaultWindowSize, dataLength)
: defaultWindowSize,
}));
}, [dataLength, defaultWindowSize]);

const handleStartChange = useCallback(
(value: number) => {
if (dataLength === 0) {
setStart(0);
return;
}

setStart(Math.max(0, Math.min(value, maxStart)));
setPlaybackState((prev) => ({
...prev,
start: dataLength === 0 ? 0 : Math.max(0, Math.min(value, maxStart)),
}));
},
[dataLength, maxStart],
);

const handleWindowSizeChange = useCallback(
(value: number) => {
if (dataLength === 0) {
setWindowSize(defaultWindowSize);
setStart(0);
setPlaybackState((prev) => ({
...prev,
start: 0,
windowSize: defaultWindowSize,
}));
return;
}

const nextWindowSize = Math.max(1, Math.min(value, dataLength));
setWindowSize(nextWindowSize);
setStart((currentStart) =>
Math.min(currentStart, Math.max(dataLength - nextWindowSize, 0)),
);
setPlaybackState((prev) => ({
...prev,
windowSize: nextWindowSize,
start: Math.min(prev.start, Math.max(dataLength - nextWindowSize, 0)),
}));
},
[dataLength, defaultWindowSize],
);

const handleIntervalChange = useCallback((value: number) => {
setIntervalState(value);
}, []);

const handleIncrementChange = useCallback((value: number) => {
setIncrementState(value);
}, []);

const handleDownsamplingThresholdChange = useCallback((value: number) => {
setDownsamplingThresholdState(value);
}, []);

return {
start,
start: playbackState.start,
setStart: handleStartChange,
windowSize,
windowSize: playbackState.windowSize,
setWindowSize: handleWindowSizeChange,
interval,
setInterval,
setInterval: handleIntervalChange,
increment,
setIncrement,
setIncrement: handleIncrementChange,
downsamplingThreshold,
setDownsamplingThreshold,
isPlaying,
setDownsamplingThreshold: handleDownsamplingThresholdChange,
isPlaying: playbackState.isPlaying,
togglePlay,
};
}
Loading
Loading