openspec: span annotation
This commit is contained in:
parent
88e7347918
commit
8a7eb1fb08
5 changed files with 497 additions and 0 deletions
2
openspec/changes/span-annotation/.openspec.yaml
Normal file
2
openspec/changes/span-annotation/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-02-13
|
||||
185
openspec/changes/span-annotation/design.md
Normal file
185
openspec/changes/span-annotation/design.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
## Context
|
||||
|
||||
The candle annotator currently supports two annotation types: point markers (Break Up/Down displayed as arrow markers via `series.setMarkers()`) and trend lines (drawn on an SVG overlay via `SvgOverlay.tsx`). All annotations are stored in a single `annotations` table with `timestamp`, `label_type`, `geometry` (JSON), and `color` fields.
|
||||
|
||||
The application uses Next.js 16 + React 19, lightweight-charts v4.2.3, SQLite + Drizzle ORM, and Tailwind CSS + shadcn/ui. State management is in `page.tsx` with props passed down to `CandleChart`, `SvgOverlay`, and `Toolbox`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Add span/range annotation: select start and end candles, assign a pattern label, render as colored rectangle
|
||||
- Rectangles must snap precisely to candle time coordinates (not free-floating like SVG lines)
|
||||
- Support optional metadata: confidence (1-5), outcome, notes
|
||||
- Sub-span support within a parent span
|
||||
- Sidebar list for span annotations with search/filter/select/delete
|
||||
- Export in 3 ML formats: Windowed CSV, BIO-tagged CSV, Raw JSON
|
||||
- User-configurable pattern label categories with colors and hotkeys
|
||||
|
||||
**Non-Goals:**
|
||||
- Overlapping span visual stacking (v1 will render overlapping spans at the same level; vertical offset is a future enhancement)
|
||||
- Real-time collaboration or multi-user support
|
||||
- Undo/redo system
|
||||
- Image crop export format (mentioned as optional in spec, deferred)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Rendering: lightweight-charts `ISeriesPrimitive` API vs SVG overlay
|
||||
|
||||
**Decision:** Use the `ISeriesPrimitive` plugin API (available since lw-charts v4.1) to render span rectangles as canvas-native primitives.
|
||||
|
||||
**Why not SVG overlay (current line approach):**
|
||||
- SVG overlay positions are in pixel space and must be manually recalculated on every zoom/pan
|
||||
- No automatic coordinate snapping to candle positions
|
||||
- Separate DOM layer causes z-index issues and performance overhead
|
||||
- No built-in hit-testing
|
||||
|
||||
**Why `ISeriesPrimitive`:**
|
||||
- Renders directly on the chart canvas — pixel-perfect alignment with candles
|
||||
- Coordinates are defined in data space `{time, price}` — automatic repositioning on zoom/pan
|
||||
- Built-in `hitTest()` for click detection
|
||||
- `zOrder: 'bottom'` renders behind candles (the desired look for span backgrounds)
|
||||
- `autoscaleInfo()` ensures spans are visible in the price range
|
||||
- Follows the official lightweight-charts plugin pattern (rectangle-drawing-tool example exists)
|
||||
|
||||
**Implementation:** Create a `SpanRectanglePrimitive` class implementing `ISeriesPrimitive`. Each span annotation gets one primitive instance attached via `series.attachPrimitive(rect)`. The primitive converts `{time, price}` corners to pixels in `updateAllViews()` and draws a filled/stroked rectangle in the renderer's `draw()` method using `useBitmapCoordinateSpace` for HiDPI support.
|
||||
|
||||
**Rectangle corners:** For a span from candle A to candle B, the rectangle corners are:
|
||||
- Top-left: `{time: A.time, price: max(high) of candles in range}`
|
||||
- Bottom-right: `{time: B.time, price: min(low) of candles in range}`
|
||||
|
||||
This makes the rectangle hug the actual price range of the pattern, not some arbitrary height.
|
||||
|
||||
### 2. Data model: New table vs extending `annotations`
|
||||
|
||||
**Decision:** Create a separate `span_annotations` table rather than extending the existing `annotations` table.
|
||||
|
||||
**Rationale:**
|
||||
- Span annotations have fundamentally different fields (start_time, end_time, label, confidence, outcome, notes, sub_spans) vs point annotations (single timestamp, label_type)
|
||||
- Mixing them in one table would require many nullable columns and type-checking everywhere
|
||||
- Separate table keeps queries clean and allows independent evolution
|
||||
- The existing `annotations` table and API routes remain untouched — zero risk of breaking existing functionality
|
||||
|
||||
**Schema:**
|
||||
```sql
|
||||
span_annotations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chart_id INTEGER NOT NULL REFERENCES charts(id),
|
||||
start_time INTEGER NOT NULL, -- Unix timestamp of first candle
|
||||
end_time INTEGER NOT NULL, -- Unix timestamp of last candle
|
||||
label TEXT NOT NULL, -- pattern name (e.g., 'bull_flag')
|
||||
confidence INTEGER, -- 1-5 scale, nullable
|
||||
outcome TEXT, -- 'win'|'loss'|'breakeven'|null
|
||||
notes TEXT, -- free-text, nullable
|
||||
sub_spans TEXT, -- JSON array of sub-span objects, nullable
|
||||
color TEXT NOT NULL DEFAULT '#2196F3',
|
||||
created_at INTEGER NOT NULL
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Label configuration: Reuse `annotationTypes` vs new table
|
||||
|
||||
**Decision:** Create a new `span_label_types` table separate from `annotationTypes`.
|
||||
|
||||
**Rationale:**
|
||||
- Existing `annotationTypes` has fields specific to point annotations (`icon`, `category` with 'marker'/'line')
|
||||
- Span labels need different fields: `color`, `hotkey`, no icon, no category
|
||||
- Keeping them separate avoids overloading the existing type system
|
||||
- The annotation-types management page (`/annotation-types`) stays focused on its current scope
|
||||
|
||||
**Schema:**
|
||||
```sql
|
||||
span_label_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE, -- internal name (e.g., 'bull_flag')
|
||||
display_name TEXT NOT NULL, -- UI label (e.g., 'Bull Flag')
|
||||
color TEXT NOT NULL, -- hex color for rectangle fill
|
||||
hotkey TEXT, -- keyboard shortcut (e.g., '1')
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
)
|
||||
```
|
||||
|
||||
Seed with default labels: bull_flag, bear_flag, head_and_shoulders, double_bottom, wedge_up, wedge_down, custom.
|
||||
|
||||
### 4. Span tool interaction flow
|
||||
|
||||
**Decision:** Two-click interaction with live preview rectangle, followed by an inline popover for label/metadata.
|
||||
|
||||
**Flow:**
|
||||
1. User activates "Span" tool (button in Toolbox or hotkey)
|
||||
2. User clicks candle A → start candle is highlighted with a vertical marker
|
||||
3. As mouse moves, a preview rectangle stretches from candle A to the candle under cursor (snapped to nearest candle time). The rectangle height covers the high/low range of the candles in the preview span.
|
||||
4. User clicks candle B → span is defined
|
||||
5. A popover/dialog appears near the chart with:
|
||||
- Label dropdown (from `span_label_types`)
|
||||
- Confidence slider (1-5, optional)
|
||||
- Outcome select (win/loss/breakeven/none, optional)
|
||||
- Notes textarea (optional)
|
||||
- Save / Cancel buttons
|
||||
6. On Save → POST to API, attach primitive to chart, update sidebar list
|
||||
7. On Cancel or Escape → discard, clear preview
|
||||
|
||||
**Candle snapping:** On click, convert pixel x to time via `coordinateToTime()`, then find the nearest candle from the loaded candle data array. This ensures the span always starts/ends exactly on a candle boundary.
|
||||
|
||||
### 5. Component architecture
|
||||
|
||||
**Decision:** Create focused new components rather than extending existing ones.
|
||||
|
||||
- `SpanRectanglePrimitive.ts` — lightweight-charts `ISeriesPrimitive` implementation (pure TypeScript, no React)
|
||||
- `SpanAnnotationManager.tsx` — React component that manages span state, handles click interactions, creates/removes primitives. Rendered inside `CandleChart.tsx` alongside `SvgOverlay`.
|
||||
- `SpanPopover.tsx` — Label/metadata assignment popover (shadcn Popover or Dialog)
|
||||
- `SpanAnnotationList.tsx` — Sidebar section for span annotations (similar to label list in `Toolbox.tsx`)
|
||||
|
||||
**State flow:** `page.tsx` holds `spanAnnotations[]`, `selectedSpanId`, and passes them to child components. The span tool mode is added to the existing `activeTool` state (type union extended).
|
||||
|
||||
### 6. API design
|
||||
|
||||
**Decision:** Separate API routes for span annotations, mirroring the existing annotation API pattern.
|
||||
|
||||
- `GET /api/span-annotations?chartId=X` — list spans for chart
|
||||
- `POST /api/span-annotations` — create span
|
||||
- `PATCH /api/span-annotations/[id]` — update span (label, metadata, sub-spans)
|
||||
- `DELETE /api/span-annotations/[id]` — delete span
|
||||
- `GET /api/span-label-types` — list configured labels
|
||||
- `POST /api/span-label-types` — create label type
|
||||
- `PATCH /api/span-label-types/[id]` — update label type
|
||||
- `DELETE /api/span-label-types/[id]` — delete label type
|
||||
- `GET /api/export/spans?chartId=X&format=windowed|bio|json` — export spans
|
||||
|
||||
### 7. Export formats
|
||||
|
||||
**Decision:** Single export endpoint with `format` query parameter. The endpoint generates the requested format server-side.
|
||||
|
||||
- **Windowed CSV (`format=windowed`)**: One row per span with flattened OHLCV columns. Configurable `context_padding` parameter (default 10 candles before/after).
|
||||
- **BIO-tagged CSV (`format=bio`)**: One row per candle across the entire dataset. BIO tagging: B-{label} for first candle, I-{label} for continuation, O for outside. Multi-label columns for overlapping spans.
|
||||
- **Raw JSON (`format=json`)**: Full annotation objects with metadata summary.
|
||||
|
||||
### 8. Sub-spans
|
||||
|
||||
**Decision:** Store sub-spans as a JSON array in the `sub_spans` column of `span_annotations`. Sub-spans reference time ranges within the parent span.
|
||||
|
||||
**Interaction:** After creating a span, user can edit it and add sub-spans via the edit popover. Each sub-span has: `label` (text), `start_time`, `end_time`. Sub-spans render as slightly different-shaded rectangles within the parent or with thin divider lines.
|
||||
|
||||
**Deferred to later phase:** Sub-span visual rendering and editing UI are complex. Phase 1 stores the data model; Phase 2 adds the UI.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk] `ISeriesPrimitive` API complexity** — The primitive API requires implementing several interfaces (pane views, renderers, HiDPI bitmap scaling). This is more code than an SVG rect.
|
||||
- Mitigation: Follow the official rectangle-drawing-tool plugin example closely. The pattern is well-documented and proven.
|
||||
|
||||
**[Risk] Performance with many span annotations** — Each span creates a separate primitive instance with its own renderer.
|
||||
- Mitigation: For v1, this is acceptable (users won't have hundreds of overlapping spans). If needed later, batch multiple rectangles into a single primitive.
|
||||
|
||||
**[Risk] Popover positioning** — The label assignment popover needs to appear near the chart click point without being clipped by viewport edges.
|
||||
- Mitigation: Use shadcn Popover with `side="top"` and `avoidCollisions={true}`. Anchor to the end-click pixel position.
|
||||
|
||||
**[Risk] Click interaction conflict** — When span tool is active, clicks must go to the canvas for span creation but the SVG overlay currently captures pointer events for line mode.
|
||||
- Mitigation: The SVG overlay already conditionally enables pointer-events only for `line` and `delete` tools. Span mode will bypass the SVG overlay and use the chart's native `subscribeClick`.
|
||||
|
||||
**[Trade-off] Separate tables vs unified annotations** — Separate tables mean two different annotation systems to query/manage. But this avoids schema migration complexity and keeps the existing system stable.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should span labels be manageable from the existing `/annotation-types` page or a new dedicated page? (Recommendation: new section on the same page, since users manage all annotation config in one place)
|
||||
- Should hotkeys for span labels work globally or only when span tool is active? (Recommendation: only when span tool is active, to avoid conflicts with existing hotkeys)
|
||||
35
openspec/changes/span-annotation/proposal.md
Normal file
35
openspec/changes/span-annotation/proposal.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
## Why
|
||||
|
||||
The tool currently only supports point annotations (single-candle markers like Break Up/Down) and line drawings. Many candlestick patterns span multiple consecutive candles (bull flags, head and shoulders, double bottoms, wedges). To label these patterns for ML training, users need the ability to select a range of candles and assign a pattern label. This is the standard approach for labeling multi-candle patterns in time series data.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a new "span" annotation type that selects a range of consecutive candles via two clicks (start candle, end candle)
|
||||
- Add a label assignment UI (dropdown/popover) with user-configurable pattern categories (e.g., bull_flag, bear_flag, head_and_shoulders)
|
||||
- Add optional metadata fields per span: confidence (1-5), outcome (win/loss/breakeven), free-text notes
|
||||
- Render span annotations as semi-transparent colored rectangles behind the candle range with label tags
|
||||
- Add span annotation list in the sidebar (search, filter, select, delete)
|
||||
- Support editing and deleting existing span annotations
|
||||
- Add sub-span support within a parent span (e.g., marking "pole" and "flag" within a bull flag)
|
||||
- Add export in three formats: Windowed Classification CSV, BIO-tagged CSV, Raw Annotations JSON
|
||||
- Add user-configurable label categories with colors and hotkeys
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `span-annotation`: Core span/range annotation interaction - two-click candle selection, label assignment popover, visual rendering as colored rectangles on the chart, span selection/editing/deletion, sidebar list management
|
||||
- `span-labels-config`: User-configurable pattern label categories with names, colors, and hotkeys (stored in DB via annotation_types or a new config)
|
||||
- `span-export`: Export span annotations in multiple ML-friendly formats - Windowed Classification CSV, BIO-tagged sequence labels CSV, and Raw Annotations JSON with configurable context padding
|
||||
|
||||
### Modified Capabilities
|
||||
- `annotation-tools`: Add "Span" tool mode to the active tool state. The tool toggle system needs a new mode alongside existing break_up, break_down, line, and delete modes.
|
||||
- `backend-api`: New API endpoints for span annotations CRUD, span label config CRUD, and span export endpoints. Existing annotation endpoints may need to coexist or be extended.
|
||||
- `chart-canvas`: Chart needs to render span annotation rectangles (via SVG overlay or chart primitives) in addition to existing markers and lines. Click handling needs span mode support.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Database**: New `span_annotations` table (or extend `annotations` with span-specific fields). New `span_labels` config table for pattern categories.
|
||||
- **Frontend components**: New SpanOverlay component (or extend SvgOverlay), new SpanPopover/Dialog for label assignment, new SpanAnnotationList sidebar section.
|
||||
- **API routes**: New `/api/span-annotations` CRUD endpoints, new `/api/span-labels` config endpoints, new `/api/export/spans` endpoints for the three export formats.
|
||||
- **State management**: New state in page.tsx for span mode, active span selection, span annotations list.
|
||||
- **Dependencies**: No new external dependencies expected - lightweight-charts SVG overlay approach already established for lines.
|
||||
182
openspec/changes/span-annotation/specs/span-annotation/spec.md
Normal file
182
openspec/changes/span-annotation/specs/span-annotation/spec.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Span annotation data model
|
||||
The system SHALL store span annotations in a `span_annotations` database table with columns: `id` (integer primary key, auto-increment), `chart_id` (integer, FK to charts.id), `start_time` (integer, Unix timestamp of first candle), `end_time` (integer, Unix timestamp of last candle), `label` (text, pattern name referencing span_label_types.name), `confidence` (integer, nullable, 1-5 scale), `outcome` (text, nullable, one of 'win'|'loss'|'breakeven'), `notes` (text, nullable), `sub_spans` (text, nullable, JSON array), `color` (text, default '#2196F3'), `created_at` (integer, Unix timestamp).
|
||||
|
||||
#### Scenario: Schema structure
|
||||
- **WHEN** the database is initialized or migrated
|
||||
- **THEN** the `span_annotations` table exists with all required columns and the foreign key constraint on `chart_id`
|
||||
|
||||
#### Scenario: start_time before end_time
|
||||
- **WHEN** a span annotation is created where the user clicked the later candle first
|
||||
- **THEN** the system SHALL swap the values so `start_time` is always less than or equal to `end_time`
|
||||
|
||||
### Requirement: Two-click span creation
|
||||
When the "span" tool is active, the system SHALL implement a two-click interaction to define a span. The first click sets the start candle. The second click sets the end candle. Both clicks SHALL snap to the nearest candle timestamp from the loaded candle data using `chart.timeScale().coordinateToTime()` followed by finding the nearest candle in the data array.
|
||||
|
||||
#### Scenario: First click sets start candle
|
||||
- **WHEN** "span" tool is active and user clicks on the chart
|
||||
- **THEN** the system identifies the nearest candle to the click position and highlights it as the span start point
|
||||
|
||||
#### Scenario: Second click completes span selection
|
||||
- **WHEN** "span" tool is active and user has already clicked a start candle, then clicks a second position
|
||||
- **THEN** the system identifies the nearest candle to the second click and defines the span range from start to end candle
|
||||
|
||||
#### Scenario: Clicking same candle twice
|
||||
- **WHEN** user clicks the same candle for both start and end
|
||||
- **THEN** the system creates a single-candle span (start_time equals end_time)
|
||||
|
||||
#### Scenario: Clicking end before start
|
||||
- **WHEN** user clicks a candle that is earlier in time than the start candle
|
||||
- **THEN** the system SHALL swap start and end so the span is always in chronological order
|
||||
|
||||
### Requirement: Live preview rectangle during span selection
|
||||
After the first click, the system SHALL display a preview rectangle that stretches from the start candle to the candle currently under the cursor. The preview rectangle height SHALL cover the price range (min low to max high) of all candles within the preview span. The preview SHALL update as the mouse moves and SHALL snap to candle positions.
|
||||
|
||||
#### Scenario: Preview follows cursor
|
||||
- **WHEN** user has clicked the first candle and moves the mouse
|
||||
- **THEN** a semi-transparent preview rectangle renders from the start candle to the candle nearest to the cursor, with height spanning the high/low range of candles in that range
|
||||
|
||||
#### Scenario: Preview updates on mouse move
|
||||
- **WHEN** cursor moves to a different candle position
|
||||
- **THEN** the preview rectangle resizes to include the new candle range with updated price bounds
|
||||
|
||||
#### Scenario: Preview disappears on cancel
|
||||
- **WHEN** user presses Escape during span selection (after first click)
|
||||
- **THEN** the preview rectangle disappears and the span selection is cancelled without saving
|
||||
|
||||
### Requirement: Label assignment popover
|
||||
After the second click completes the span selection, the system SHALL display a popover near the click position containing: a label dropdown populated from `span_label_types`, an optional confidence slider (1-5), an optional outcome select (win/loss/breakeven/none), an optional notes textarea, and Save/Cancel buttons.
|
||||
|
||||
#### Scenario: Popover appears after span selection
|
||||
- **WHEN** user completes the two-click span selection
|
||||
- **THEN** a popover appears near the end-click position with label dropdown, confidence, outcome, notes fields, and Save/Cancel buttons
|
||||
|
||||
#### Scenario: Save span annotation
|
||||
- **WHEN** user selects a label and clicks Save in the popover
|
||||
- **THEN** the system sends a POST request to create the span annotation, renders the span rectangle on the chart, updates the sidebar list, and closes the popover
|
||||
|
||||
#### Scenario: Cancel span annotation
|
||||
- **WHEN** user clicks Cancel in the popover or presses Escape
|
||||
- **THEN** the popover closes, the preview rectangle disappears, and no annotation is saved
|
||||
|
||||
#### Scenario: Label is required
|
||||
- **WHEN** user clicks Save without selecting a label
|
||||
- **THEN** the Save button SHALL be disabled or the system SHALL show a validation message requiring a label
|
||||
|
||||
### Requirement: Span rectangle rendering via ISeriesPrimitive
|
||||
The system SHALL render saved span annotations as semi-transparent colored rectangles on the chart using the lightweight-charts `ISeriesPrimitive` API. Each span annotation SHALL have one `SpanRectanglePrimitive` instance attached to the candlestick series via `series.attachPrimitive()`. The rectangle SHALL be defined by data coordinates: top-left `{time: start_time, price: max_high}` and bottom-right `{time: end_time, price: min_low}` where max_high and min_low are the highest high and lowest low of candles within the span range.
|
||||
|
||||
#### Scenario: Rectangle renders for saved span
|
||||
- **WHEN** a span annotation exists for the active chart
|
||||
- **THEN** a semi-transparent colored rectangle renders behind the candles in the span range, with color from the span's label type configuration
|
||||
|
||||
#### Scenario: Rectangle z-order
|
||||
- **WHEN** span rectangles render on the chart
|
||||
- **THEN** they SHALL render behind (below) the candlestick series so candles remain fully visible on top
|
||||
|
||||
#### Scenario: Rectangle updates on zoom/pan
|
||||
- **WHEN** user zooms or pans the chart
|
||||
- **THEN** span rectangles automatically reposition and resize to stay aligned with their candle range (handled natively by the ISeriesPrimitive lifecycle)
|
||||
|
||||
#### Scenario: Label tag display
|
||||
- **WHEN** a span rectangle renders on the chart
|
||||
- **THEN** a small label tag showing the pattern name SHALL be displayed above the rectangle
|
||||
|
||||
#### Scenario: Rectangle color from label config
|
||||
- **WHEN** a span annotation has label "bull_flag" and the bull_flag label type has color "#4CAF50"
|
||||
- **THEN** the rectangle renders with fill color "#4CAF50" at reduced opacity (semi-transparent)
|
||||
|
||||
### Requirement: Span selection on chart
|
||||
The system SHALL allow users to select an existing span annotation by clicking within its rectangle on the chart.
|
||||
|
||||
#### Scenario: Click to select span
|
||||
- **WHEN** user clicks within a span rectangle on the chart (detected via `hitTest()`)
|
||||
- **THEN** the system sets the selectedSpanId state, highlights the span rectangle (e.g., thicker border or increased opacity), and scrolls to it in the sidebar list
|
||||
|
||||
#### Scenario: Click to deselect span
|
||||
- **WHEN** user clicks on an already-selected span rectangle
|
||||
- **THEN** the system deselects it by clearing selectedSpanId and removing the highlight
|
||||
|
||||
#### Scenario: Click outside any span
|
||||
- **WHEN** user clicks on the chart outside any span rectangle while a span is selected
|
||||
- **THEN** the system deselects the currently selected span
|
||||
|
||||
### Requirement: Span editing
|
||||
The system SHALL allow users to edit an existing span annotation's metadata (label, confidence, outcome, notes).
|
||||
|
||||
#### Scenario: Open edit popover
|
||||
- **WHEN** user double-clicks on an existing span rectangle or selects it and presses Enter
|
||||
- **THEN** a popover appears pre-populated with the span's current label, confidence, outcome, and notes
|
||||
|
||||
#### Scenario: Save edited span
|
||||
- **WHEN** user modifies fields in the edit popover and clicks Save
|
||||
- **THEN** the system sends a PATCH request to update the span annotation, re-renders the rectangle with any color changes, and updates the sidebar list
|
||||
|
||||
#### Scenario: Cancel edit
|
||||
- **WHEN** user clicks Cancel or presses Escape in the edit popover
|
||||
- **THEN** changes are discarded and the popover closes
|
||||
|
||||
### Requirement: Span deletion
|
||||
The system SHALL allow users to delete span annotations via keyboard shortcut, sidebar button, or the delete tool.
|
||||
|
||||
#### Scenario: Delete selected span with keyboard
|
||||
- **WHEN** a span is selected (selectedSpanId is set) and user presses Delete or Backspace
|
||||
- **THEN** the system sends a DELETE request, removes the rectangle primitive from the chart, clears the selection, and updates the sidebar list
|
||||
|
||||
#### Scenario: Delete span from sidebar
|
||||
- **WHEN** user clicks the trash icon on a span entry in the sidebar list
|
||||
- **THEN** the system sends a DELETE request, removes the rectangle from the chart, and updates the sidebar list
|
||||
|
||||
#### Scenario: Delete span with delete tool
|
||||
- **WHEN** the "delete" tool is active and user clicks within a span rectangle
|
||||
- **THEN** the system deletes that span annotation via API, removes the rectangle, and updates the sidebar
|
||||
|
||||
### Requirement: Span annotation sidebar list
|
||||
The system SHALL display a "Span Annotations" section in the sidebar showing all span annotations for the active chart.
|
||||
|
||||
#### Scenario: Display span list
|
||||
- **WHEN** span annotations exist for the active chart
|
||||
- **THEN** the sidebar displays a scrollable list of span annotations sorted by start_time (newest first), with each entry showing: start/end time range, label name with colored badge, and delete button
|
||||
|
||||
#### Scenario: Empty span list
|
||||
- **WHEN** no span annotations exist for the active chart
|
||||
- **THEN** the section displays "No span annotations yet. Use the Span tool to select candle ranges."
|
||||
|
||||
#### Scenario: Click span in list to select
|
||||
- **WHEN** user clicks a span entry in the sidebar list
|
||||
- **THEN** the system sets selectedSpanId, highlights the corresponding rectangle on the chart, and scrolls the chart to center on the span's time range
|
||||
|
||||
#### Scenario: Selected span highlight in list
|
||||
- **WHEN** a span is selected
|
||||
- **THEN** the corresponding entry in the sidebar list has a highlighted background and border
|
||||
|
||||
#### Scenario: Span list updates on chart switch
|
||||
- **WHEN** user switches to a different chart
|
||||
- **THEN** the span list clears and reloads with span annotations belonging to the newly selected chart
|
||||
|
||||
### Requirement: Span count display
|
||||
The sidebar SHALL display a count summary of span annotations grouped by label type.
|
||||
|
||||
#### Scenario: Display span counts
|
||||
- **WHEN** span annotations exist for the active chart
|
||||
- **THEN** the sidebar displays counts per label type (e.g., "Bull Flag: 3 | Bear Flag: 2")
|
||||
|
||||
#### Scenario: Counts update after changes
|
||||
- **WHEN** user adds or deletes a span annotation
|
||||
- **THEN** the count summary updates immediately
|
||||
|
||||
### Requirement: Hotkey label assignment
|
||||
When the span tool is active and the user has completed a two-click selection, pressing a configured hotkey SHALL assign the corresponding label and save the span immediately without showing the popover.
|
||||
|
||||
#### Scenario: Hotkey assigns label after span selection
|
||||
- **WHEN** span tool is active, user has selected start and end candles, and presses hotkey "1" (mapped to "bull_flag")
|
||||
- **THEN** the system saves the span with label "bull_flag", default confidence/outcome/notes, renders the rectangle, and updates the sidebar
|
||||
|
||||
#### Scenario: Hotkey ignored when no span selected
|
||||
- **WHEN** span tool is active but no span range has been selected yet, and user presses a label hotkey
|
||||
- **THEN** the system takes no action
|
||||
|
||||
#### Scenario: Hotkey ignored when span tool is not active
|
||||
- **WHEN** a different tool (e.g., break_up, line) is active and user presses a span label hotkey
|
||||
- **THEN** the system takes no action (hotkeys only work when span tool is active)
|
||||
93
openspec/changes/span-annotation/tasks.md
Normal file
93
openspec/changes/span-annotation/tasks.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
## 1. Database Schema & Migrations
|
||||
|
||||
- [ ] 1.1 Add `span_label_types` table to Drizzle schema (id, name, display_name, color, hotkey, is_active, sort_order, created_at)
|
||||
- [ ] 1.2 Add `span_annotations` table to Drizzle schema (id, chart_id FK, start_time, end_time, label, confidence, outcome, notes, sub_spans JSON, color, created_at)
|
||||
- [ ] 1.3 Run migration to create both tables
|
||||
- [ ] 1.4 Seed `span_label_types` with default labels: bull_flag, bear_flag, head_and_shoulders, double_bottom, wedge_up, wedge_down, custom (with distinct colors and hotkeys 1-7)
|
||||
|
||||
## 2. Span Label Types API
|
||||
|
||||
- [ ] 2.1 Create `GET /api/span-label-types` endpoint — return all active label types sorted by sort_order
|
||||
- [ ] 2.2 Create `POST /api/span-label-types` endpoint — create a new label type
|
||||
- [ ] 2.3 Create `PATCH /api/span-label-types/[id]` endpoint — update label type fields
|
||||
- [ ] 2.4 Create `DELETE /api/span-label-types/[id]` endpoint — delete a label type
|
||||
|
||||
## 3. Span Annotations CRUD API
|
||||
|
||||
- [ ] 3.1 Create `GET /api/span-annotations?chartId=X` endpoint — return all span annotations for a chart, sorted by start_time desc
|
||||
- [ ] 3.2 Create `POST /api/span-annotations` endpoint — create span annotation with start/end time swap validation (start <= end)
|
||||
- [ ] 3.3 Create `PATCH /api/span-annotations/[id]` endpoint — update label, confidence, outcome, notes, sub_spans
|
||||
- [ ] 3.4 Create `DELETE /api/span-annotations/[id]` endpoint — delete a span annotation
|
||||
|
||||
## 4. SpanRectanglePrimitive (Chart Rendering)
|
||||
|
||||
- [ ] 4.1 Create `SpanRectanglePrimitive.ts` implementing `ISeriesPrimitive` with data-space rectangle coordinates (start_time, end_time, max_high, min_low)
|
||||
- [ ] 4.2 Implement `updateAllViews()` with a pane renderer that converts time/price to pixel coordinates and draws a filled semi-transparent rectangle using `useBitmapCoordinateSpace`
|
||||
- [ ] 4.3 Implement label tag text rendering above the rectangle (pattern name)
|
||||
- [ ] 4.4 Implement `hitTest()` to detect clicks within the rectangle bounds
|
||||
- [ ] 4.5 Add highlight state (thicker border / increased opacity) for selected spans
|
||||
- [ ] 4.6 Set `zOrder: 'bottom'` so rectangles render behind candlesticks
|
||||
|
||||
## 5. Span Tool State & Integration
|
||||
|
||||
- [ ] 5.1 Extend `activeTool` type in `page.tsx` to include `'span'` tool mode
|
||||
- [ ] 5.2 Add span-related state to `page.tsx`: `spanAnnotations[]`, `selectedSpanId`, `spanLabelTypes[]`
|
||||
- [ ] 5.3 Add `fetchSpanAnnotations(chartId)` and `fetchSpanLabelTypes()` data fetching functions
|
||||
- [ ] 5.4 Load span annotations and label types when chart changes (alongside existing annotation loading)
|
||||
- [ ] 5.5 Add "Span" tool button to Toolbox component alongside existing tools
|
||||
|
||||
## 6. Two-Click Span Selection & Preview
|
||||
|
||||
- [ ] 6.1 Create `SpanAnnotationManager.tsx` component that manages span interaction state (idle / first-click-done / popover-open)
|
||||
- [ ] 6.2 Implement first-click handler: snap to nearest candle, store start candle, render start marker via a preview primitive
|
||||
- [ ] 6.3 Implement mouse-move preview: stretch preview rectangle from start candle to cursor candle, computing price range (min low to max high) of candles in range
|
||||
- [ ] 6.4 Implement second-click handler: snap to nearest candle, finalize span range, swap if end < start, trigger popover
|
||||
- [ ] 6.5 Implement Escape key to cancel span selection and clear preview
|
||||
|
||||
## 7. Label Assignment Popover
|
||||
|
||||
- [ ] 7.1 Create `SpanPopover.tsx` with shadcn Popover/Dialog: label dropdown (from span_label_types), confidence slider (1-5), outcome select (win/loss/breakeven/none), notes textarea, Save/Cancel buttons
|
||||
- [ ] 7.2 Position popover near the end-click position with collision avoidance
|
||||
- [ ] 7.3 Wire Save button: POST to API, attach SpanRectanglePrimitive to chart series, update spanAnnotations state, close popover
|
||||
- [ ] 7.4 Wire Cancel button / Escape: discard span, clear preview, close popover
|
||||
- [ ] 7.5 Disable Save when no label is selected (validation)
|
||||
|
||||
## 8. Span Selection, Editing & Deletion
|
||||
|
||||
- [ ] 8.1 Implement span click-to-select using hitTest: set selectedSpanId, highlight rectangle, scroll sidebar list to selected item
|
||||
- [ ] 8.2 Implement click-to-deselect (click selected span again or click outside any span)
|
||||
- [ ] 8.3 Implement double-click / Enter to open edit popover pre-populated with current span data
|
||||
- [ ] 8.4 Wire edit Save: PATCH to API, update primitive color/label, update state
|
||||
- [ ] 8.5 Implement Delete/Backspace keyboard shortcut for selected span: DELETE API call, remove primitive, clear selection, update state
|
||||
- [ ] 8.6 Implement delete-tool click on span rectangle: same DELETE flow as keyboard shortcut
|
||||
|
||||
## 9. Span Annotation Sidebar List
|
||||
|
||||
- [ ] 9.1 Create `SpanAnnotationList.tsx` component: scrollable list of span annotations sorted by start_time desc, showing time range, label with colored badge, delete button
|
||||
- [ ] 9.2 Add count summary grouped by label type (e.g., "Bull Flag: 3 | Bear Flag: 2")
|
||||
- [ ] 9.3 Implement click-to-select in list: set selectedSpanId, highlight chart rectangle, scroll chart to center on span's time range
|
||||
- [ ] 9.4 Highlight selected span entry in list (background/border)
|
||||
- [ ] 9.5 Wire delete button per list item: DELETE API call, remove primitive, update state
|
||||
- [ ] 9.6 Show empty state message when no span annotations exist
|
||||
- [ ] 9.7 Clear and reload span list when active chart changes
|
||||
|
||||
## 10. Hotkey Label Assignment
|
||||
|
||||
- [ ] 10.1 Add keydown listener for span label hotkeys (active only when span tool is active and span range is selected)
|
||||
- [ ] 10.2 On hotkey press: save span with mapped label (default confidence/outcome/notes), render rectangle, update sidebar — skip popover
|
||||
- [ ] 10.3 Ignore hotkeys when span tool is inactive or no span range selected
|
||||
|
||||
## 11. Export Endpoints
|
||||
|
||||
- [ ] 11.1 Create `GET /api/export/spans?chartId=X&format=json` — Raw Annotations JSON export with full metadata
|
||||
- [ ] 11.2 Create `GET /api/export/spans?chartId=X&format=windowed` — Windowed Classification CSV with flattened OHLCV columns and configurable `context_padding` (default 10)
|
||||
- [ ] 11.3 Create `GET /api/export/spans?chartId=X&format=bio` — BIO-tagged CSV with one row per candle, B-{label}/I-{label}/O tagging, multi-label columns for overlapping spans
|
||||
|
||||
## 12. Integration Testing & Polish
|
||||
|
||||
- [ ] 12.1 Verify full create flow: activate span tool → two clicks → popover → save → rectangle renders → sidebar updates
|
||||
- [ ] 12.2 Verify edit flow: select span → double-click → edit popover → save → updates reflect
|
||||
- [ ] 12.3 Verify delete flow: select span → Delete key / trash icon → removed from chart and sidebar
|
||||
- [ ] 12.4 Verify chart switch: span annotations and sidebar list reload for new chart
|
||||
- [ ] 12.5 Verify all three export formats produce correct output
|
||||
- [ ] 12.6 Verify hotkey label assignment works end-to-end
|
||||
Loading…
Add table
Add a link
Reference in a new issue