candle-annotator/openspec/specs/chart-canvas/spec.md
Marko Djordjevic 925e7284e3 Archive code-review-fix change and sync specs to main
- Synced 14 capability delta specs to main specs
- Created 6 new main specs: api-authentication, error-boundary, input-validation, security-headers, shared-types
- Updated 8 existing specs with security, validation, and performance requirements
- Archived change to openspec/changes/archive/2026-02-20-code-review-fix/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 08:54:59 +01:00

115 lines
7 KiB
Markdown

## ADDED Requirements
### Requirement: Candlestick chart rendering
The system SHALL render candle data as a candlestick chart using the `lightweight-charts` library (v4). The chart MUST display OHLC data with candlestick visuals using a black and white color scheme. Bullish candles SHALL have a white interior with black outline and black wicks. Bearish candles SHALL be completely black (black fill and black wicks). The chart SHALL be a client-side React component. The chart SHALL fetch and display candles scoped to the active chart by passing `chartId` to the `GET /api/candles` endpoint.
#### Scenario: Chart renders candle data for active chart
- **WHEN** an active chart is selected and candle data exists for that chart
- **THEN** the chart fetches candles via `GET /api/candles?chartId=<activeChartId>` and renders only that chart's candles
#### Scenario: Empty state
- **WHEN** no charts exist or the active chart has no candle data
- **THEN** the chart area displays an empty chart with a prompt to upload CSV data
#### Scenario: Bullish candle appearance
- **WHEN** a candle's close price is higher than its open price (bullish)
- **THEN** the candle displays with white interior, black border, and black wick
#### Scenario: Bearish candle appearance
- **WHEN** a candle's close price is lower than its open price (bearish)
- **THEN** the candle displays as completely black (black fill and black wick)
#### Scenario: Chart switches when active chart changes
- **WHEN** user selects a different chart from the chart selector
- **THEN** the chart clears existing data, fetches candles for the newly selected chart, and renders the new dataset
### Requirement: Chart interactivity
The chart SHALL support zooming (mouse wheel), panning (click and drag on time axis), and crosshair display (showing price/time on hover). These are built-in lightweight-charts behaviors that MUST be enabled.
#### Scenario: Zoom and pan
- **WHEN** user scrolls the mouse wheel over the chart
- **THEN** the chart zooms in or out on the time axis
#### Scenario: Crosshair display
- **WHEN** user hovers the mouse over the chart
- **THEN** a crosshair displays with the current price and time at the cursor position
### Requirement: Responsive chart layout
The chart MUST fill the available width of the main content area (excluding the sidebar). The chart height SHALL be responsive, using the full viewport height minus header/toolbar areas. The chart MUST resize when the browser window is resized.
#### Scenario: Window resize
- **WHEN** user resizes the browser window
- **THEN** the chart resizes to fill the available space without requiring a page reload
### Requirement: Dark theme chart
The chart SHALL use a dark color scheme consistent with the application's Slate-900 dark theme. Background MUST be dark, grid lines subtle, and text/crosshair in light colors.
#### Scenario: Dark theme applied
- **WHEN** the chart renders
- **THEN** the chart background, grid, text, and crosshair colors match the dark theme (dark background, light text)
### Requirement: Annotation markers on chart
The chart SHALL display visual markers for existing annotations using the `series.setMarkers()` API. Break Up annotations MUST appear as green upward arrows above the bar. Break Down annotations MUST appear as red downward arrows below the bar. Markers MUST update when annotations are added or deleted. Markers SHALL be scoped to the active chart by fetching annotations via `GET /api/annotations?chartId=<activeChartId>`.
#### Scenario: Break Up marker display
- **WHEN** a Break Up annotation exists for a candle timestamp in the active chart
- **THEN** a green upward arrow marker appears above that candle on the chart
#### Scenario: Break Down marker display
- **WHEN** a Break Down annotation exists for a candle timestamp in the active chart
- **THEN** a red downward arrow marker appears below that candle on the chart
#### Scenario: Marker updates on annotation change
- **WHEN** user adds or deletes an annotation on the active chart
- **THEN** chart markers update immediately without requiring a page reload
#### Scenario: Markers refresh on chart switch
- **WHEN** user switches to a different chart
- **THEN** markers from the previous chart are cleared and markers for the new chart's annotations are loaded
### Requirement: Refs for event handler closure values
The CandleChart component SHALL use `useRef` for state values that are read inside event handlers (`drawingState`, `selectedLineId`, `dragState`, `annotations`). The ref SHALL be updated alongside every `setState` call. Event handlers SHALL read from the ref instead of the closure variable.
#### Scenario: Click handler reads current state
- **WHEN** the user clicks on the chart after state has changed
- **THEN** the click handler reads the current value from the ref (not a stale closure value)
#### Scenario: Reduced re-subscription frequency
- **WHEN** refs are used for mutable state in event handlers
- **THEN** the `useEffect` dependency array for chart event subscriptions is smaller, reducing re-subscription frequency
### Requirement: Theme change without chart re-creation
The CandleChart component SHALL apply theme changes using `chart.applyOptions()` instead of destroying and re-creating the entire chart instance. This preserves scroll position, zoom level, and attached primitives.
#### Scenario: Theme toggle preserves state
- **WHEN** the user toggles between light and dark theme
- **THEN** the chart colors update without losing scroll position, zoom level, or annotation primitives
### Requirement: Dynamic candle interval detection
The CandleChart component SHALL determine the actual candle interval from the data (by examining the time difference between consecutive candles) instead of hardcoding 60 seconds. The detected interval SHALL be used for span annotation iteration loops.
#### Scenario: 1-minute candles
- **WHEN** candle data has 60-second intervals
- **THEN** the span iteration step is 60 seconds
#### Scenario: 1-hour candles
- **WHEN** candle data has 3600-second intervals
- **THEN** the span iteration step is 3600 seconds (not 60)
#### Scenario: No performance degradation on high timeframes
- **WHEN** iterating over a span on daily candles
- **THEN** the loop iterates over actual candle count (not 1440x more iterations)
### Requirement: Named constants for magic numbers
The CandleChart component SHALL extract hardcoded magic numbers (8px padding, 60s interval, color values) into named constants at the module level.
#### Scenario: Constants used instead of magic numbers
- **WHEN** the CandleChart source is inspected
- **THEN** magic numbers like `8`, `60`, hardcoded colors are replaced with descriptive constant names
### Requirement: Module-level empty Set default
The `new Set<string>()` default prop value SHALL be defined as a module-level constant instead of being recreated on every render.
#### Scenario: Stable default reference
- **WHEN** CandleChart renders without `hiddenLabels` prop
- **THEN** it uses a module-level `EMPTY_SET` constant (same reference across renders)