From 5767669b2c622f2d4d20092a52cc8a022da11d53 Mon Sep 17 00:00:00 2001 From: Marko Djordjevic Date: Thu, 12 Feb 2026 14:00:28 +0100 Subject: [PATCH] docs: add detailed implementation plan for line drawing improvements --- LINE_DRAWING_IMPROVEMENTS.md | 324 +++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 LINE_DRAWING_IMPROVEMENTS.md diff --git a/LINE_DRAWING_IMPROVEMENTS.md b/LINE_DRAWING_IMPROVEMENTS.md new file mode 100644 index 0000000..3e76fd7 --- /dev/null +++ b/LINE_DRAWING_IMPROVEMENTS.md @@ -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 ( + + ); +}; +``` +- Call in SVG return: `{renderCursorCircle()}` + +--- + +### Phase 3: Line Selection + +#### 3.1 Selection State +**File**: `src/components/SvgOverlay.tsx` +- Add state: `const [selectedLineId, setSelectedLineId] = useState(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(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 ( + <> + handleHandleMouseDown(e, line.id, 'start')} + /> + 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)