candle-annotator/LINE_DRAWING_IMPROVEMENTS.md

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

  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 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 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:
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 color from 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, 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:
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 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 handleMouseMove to 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

  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)