docs: add detailed implementation plan for line drawing improvements

This commit is contained in:
Marko Djordjevic 2026-02-12 14:00:28 +01:00
parent daec116aab
commit 5767669b2c

View file

@ -0,0 +1,324 @@
# Line Drawing Improvements Plan
## Context
The candle annotator app has a working line drawing feature, but it needs enhancements:
- Currently only draws blue lines
- No way to select or edit lines after creation
- Limited visual feedback during drawing
- Delete tool works by clicking near line
## Current State
- Line drawing is working: click to start, move mouse (dashed preview), click to finish
- Lines are stored in database with geometry (startTime, startPrice, endTime, endPrice)
- Lines maintain relative position to candles during zoom/pan (coordinate conversion working)
- SVG overlay is properly layered (z-index) and sized (CSS 100%)
## Approved Design Decisions
1. **Color**: Preset color buttons below "Draw Line" button (Option C)
2. **Selection**: Lines selectable only when "Draw Line" tool is active (Option B)
3. **Editing**: Drag endpoints only - no full line dragging (simplest approach)
4. **Visual Feedback**: Add small circle at mouse cursor during drawing
## Implementation Plan
### Phase 1: Color Support
#### 1.1 Database Schema
**File**: `src/lib/db/schema.ts`
```typescript
// Add to annotations table:
color: text('color').default('#3b82f6'), // hex color code
```
- Need to run migration or recreate database
- Default color: `#3b82f6` (blue)
#### 1.2 Toolbox UI
**File**: `src/components/Toolbox.tsx`
- Add state: `const [selectedColor, setSelectedColor] = useState('#3b82f6')`
- Add preset color buttons below "Draw Line" button
- Colors: Red (#ef4444), Green (#22c55e), Blue (#3b82f6), Yellow (#eab308), White (#ffffff)
- Active color button should be highlighted (variant='default', others 'outline')
- Pass `selectedColor` up to parent via new prop: `onColorChange`
#### 1.3 Page Component
**File**: `src/app/page.tsx`
- Add state: `const [selectedColor, setSelectedColor] = useState('#3b82f6')`
- Pass to Toolbox: `onColorChange={setSelectedColor}`
- Pass to CandleChart: `selectedColor={selectedColor}`
#### 1.4 Chart Component
**File**: `src/components/CandleChart.tsx`
- Accept new prop: `selectedColor: string`
- Pass to SvgOverlay: `selectedColor={selectedColor}`
#### 1.5 SVG Overlay - Create with Color
**File**: `src/components/SvgOverlay.tsx`
- Accept new prop: `selectedColor: string`
- In `handleClick` when saving line (second click), add color to POST body:
```typescript
body: JSON.stringify({
timestamp: drawingLine.start.time,
label_type: 'line',
color: selectedColor, // NEW
geometry: { ... }
})
```
#### 1.6 SVG Overlay - Render with Color
**File**: `src/components/SvgOverlay.tsx`
- In `renderLines()`, read color from annotation:
```typescript
stroke={annotation.color || '#3b82f6'}
```
#### 1.7 API - Handle Color
**File**: `src/app/api/annotations/route.ts`
- In POST handler, extract `color` from body
- Save to database: `color: body.color || '#3b82f6'`
---
### Phase 2: Visual Feedback
#### 2.1 Cursor Circle During Drawing
**File**: `src/components/SvgOverlay.tsx`
- Add new render function:
```typescript
const renderCursorCircle = () => {
if (!drawingLine || !mousePosition) return null;
return (
<circle
cx={mousePosition.x}
cy={mousePosition.y}
r={5}
fill="transparent"
stroke={selectedColor}
strokeWidth={2}
/>
);
};
```
- Call in SVG return: `{renderCursorCircle()}`
---
### Phase 3: Line Selection
#### 3.1 Selection State
**File**: `src/components/SvgOverlay.tsx`
- Add state: `const [selectedLineId, setSelectedLineId] = useState<number | null>(null)`
#### 3.2 Click Logic Update
**File**: `src/components/SvgOverlay.tsx`
- In `handleClick`, when `activeTool === 'line'`:
- If NOT drawing line (drawingLine === null):
- First check if clicking near existing line
- If yes → select it (setSelectedLineId)
- If no → start new line
- If drawing line:
- Finish and save line (existing behavior)
#### 3.3 Visual Styling for Selected Line
**File**: `src/components/SvgOverlay.tsx`
- In `renderLines()`, check if line is selected:
```typescript
const isSelected = annotation.id === selectedLineId;
strokeWidth={isSelected ? 3 : 2}
opacity={isSelected ? 1 : 0.85}
```
#### 3.4 Deselect on Tool Change or Escape
**File**: `src/components/SvgOverlay.tsx`
- Reset `selectedLineId` when `activeTool` changes
- Add to existing Escape key handler: also clear `selectedLineId`
---
### Phase 4: Endpoint Dragging
#### 4.1 Drag State
**File**: `src/components/SvgOverlay.tsx`
- Add state:
```typescript
interface DragState {
lineId: number;
endpoint: 'start' | 'end';
originalPoint: Point;
}
const [dragState, setDragState] = useState<DragState | null>(null);
```
#### 4.2 Render Endpoint Handles
**File**: `src/components/SvgOverlay.tsx`
- Add new render function:
```typescript
const renderHandles = () => {
if (!selectedLineId) return null;
const line = annotations.find(a => a.id === selectedLineId);
if (!line || !line.geometry) return null;
const start = dataToPixel(line.geometry.startTime, line.geometry.startPrice);
const end = dataToPixel(line.geometry.endTime, line.geometry.endPrice);
if (!start || !end) return null;
return (
<>
<circle
cx={start.x} cy={start.y} r={6}
fill="white"
stroke={line.color || '#3b82f6'}
strokeWidth={2}
style={{ cursor: 'move' }}
onMouseDown={(e) => handleHandleMouseDown(e, line.id, 'start')}
/>
<circle
cx={end.x} cy={end.y} r={6}
fill="white"
stroke={line.color || '#3b82f6'}
strokeWidth={2}
style={{ cursor: 'move' }}
onMouseDown={(e) => handleHandleMouseDown(e, line.id, 'end')}
/>
</>
);
};
```
#### 4.3 Handle Drag Interaction
**File**: `src/components/SvgOverlay.tsx`
- Implement handlers:
```typescript
const handleHandleMouseDown = (e: React.MouseEvent, lineId: number, endpoint: 'start' | 'end') => {
e.stopPropagation(); // Prevent line click
const line = annotations.find(a => a.id === lineId);
if (!line?.geometry) return;
const point = endpoint === 'start'
? { time: line.geometry.startTime, price: line.geometry.startPrice }
: { time: line.geometry.endTime, price: line.geometry.endPrice };
setDragState({ lineId, endpoint, originalPoint: point });
};
```
#### 4.4 Mouse Move During Drag
**File**: `src/components/SvgOverlay.tsx`
- Update `handleMouseMove` to handle drag preview:
```typescript
if (dragState && mousePosition) {
const dataPoint = pixelToData(mousePosition.x, mousePosition.y);
if (dataPoint) {
// Update local preview of line position
// (optimistically update annotations array temporarily)
}
}
```
#### 4.5 Mouse Up - Save to Database
**File**: `src/components/SvgOverlay.tsx`
- Add handler:
```typescript
const handleMouseUp = async () => {
if (!dragState || !mousePosition) return;
const dataPoint = pixelToData(mousePosition.x, mousePosition.y);
if (!dataPoint) return;
const line = annotations.find(a => a.id === dragState.lineId);
if (!line?.geometry) return;
const updatedGeometry = {
...line.geometry,
...(dragState.endpoint === 'start'
? { startTime: dataPoint.time, startPrice: dataPoint.price }
: { endTime: dataPoint.time, endPrice: dataPoint.price })
};
await fetch(`/api/annotations/${dragState.lineId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ geometry: updatedGeometry })
});
await fetchAnnotations();
setDragState(null);
onAnnotationChange?.();
};
```
- Add to SVG: `onMouseUp={handleMouseUp}`
#### 4.6 API - PATCH Endpoint
**File**: `src/app/api/annotations/[id]/route.ts`
- Add PATCH handler:
```typescript
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json();
const { geometry } = body;
const result = await db
.update(annotations)
.set({ geometry: JSON.stringify(geometry) })
.where(eq(annotations.id, parseInt(params.id)))
.returning();
const updated = result[0];
return NextResponse.json({
...updated,
geometry: updated.geometry ? JSON.parse(updated.geometry) : null
});
} catch (error: any) {
return NextResponse.json(
{ error: error.message || 'Failed to update annotation' },
{ status: 500 }
);
}
}
```
---
## Testing Checklist
After implementation, verify:
- [ ] Can select different colors before drawing line
- [ ] Lines are drawn in selected color
- [ ] Existing lines render with their stored color
- [ ] Small circle appears at cursor during line drawing
- [ ] Can click existing line (in line tool mode) to select it
- [ ] Selected line has visual highlight (thicker stroke)
- [ ] Selected line shows endpoint handles (white circles)
- [ ] Can drag endpoint handles to reposition line
- [ ] Line updates in database after dragging
- [ ] Lines maintain position during zoom/pan
- [ ] Delete tool still works (click near line)
- [ ] Can deselect by clicking empty space or pressing Escape
- [ ] Switching tools deselects line
---
## Files Modified Summary
1. `src/lib/db/schema.ts` - Add color field
2. `src/components/Toolbox.tsx` - Color picker UI
3. `src/app/page.tsx` - Pass color state
4. `src/components/CandleChart.tsx` - Pass color to overlay
5. `src/components/SvgOverlay.tsx` - Selection, dragging, visual feedback
6. `src/app/api/annotations/route.ts` - Handle color in POST
7. `src/app/api/annotations/[id]/route.ts` - NEW: PATCH handler
---
## Notes
- Lines use data coordinates (time/price) so they survive zoom/pan automatically
- SVG overlay has z-index and proper sizing - this is working
- Current delete functionality works by clicking near line - keep this behavior
- Color state is managed at page level, flows down to components
- Database migration needed for color field (or recreate dev database)