9.7 KiB
9.7 KiB
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
- Color: Preset color buttons below "Draw Line" button (Option C)
- Selection: Lines selectable only when "Draw Line" tool is active (Option B)
- Editing: Drag endpoints only - no full line dragging (simplest approach)
- Visual Feedback: Add small circle at mouse cursor during drawing
Implementation Plan
Phase 1: Color Support ✅ DONE
1.1 Database Schema
File: src/lib/db/schema.ts
// 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
selectedColorup 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
handleClickwhen saving line (second click), add color to POST body:
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:
stroke={annotation.color || '#3b82f6'}
1.7 API - Handle Color
File: src/app/api/annotations/route.ts
- In POST handler, extract
colorfrom body - Save to database:
color: body.color || '#3b82f6'
Phase 2: Visual Feedback ✅ DONE
2.1 Cursor Circle During Drawing
File: src/components/SvgOverlay.tsx
- Add new render function:
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 ✅ DONE
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, whenactiveTool === '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)
- If NOT drawing line (drawingLine === null):
3.3 Visual Styling for Selected Line
File: src/components/SvgOverlay.tsx
- In
renderLines(), check if line is selected:
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
selectedLineIdwhenactiveToolchanges - Add to existing Escape key handler: also clear
selectedLineId
Phase 4: Endpoint Dragging ✅ DONE
4.1 Drag State
File: src/components/SvgOverlay.tsx
- Add state:
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:
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:
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
handleMouseMoveto handle drag preview:
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:
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:
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
src/lib/db/schema.ts- Add color fieldsrc/components/Toolbox.tsx- Color picker UIsrc/app/page.tsx- Pass color statesrc/components/CandleChart.tsx- Pass color to overlaysrc/components/SvgOverlay.tsx- Selection, dragging, visual feedbacksrc/app/api/annotations/route.ts- Handle color in POSTsrc/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)