feat: complete prediction UI feedback tasks (11.2, 11.4, 11.5)

- Implement disagreement visual highlighting with distinct colors
  - Yellow highlight for 'missed_by_human' predictions
  - Orange for 'label_mismatch' disagreements
  - Warning icon on disagreement markers
- Add click-to-convert prediction feedback
  - Click disagreement predictions to create span annotations
  - Auto-fill with predicted label and times
  - Set source as 'model_confirmed' or 'model_corrected'
- Add dismiss action for false positive predictions
  - Alt+Click or Ctrl+Click to dismiss predictions
  - Saves negative annotation with label 'O'
  - Records original prediction in model_prediction field
- Filter predictions when 'Show only disagreements' is enabled
This commit is contained in:
Marko Djordjevic 2026-02-16 11:40:55 +01:00
parent a18c6d110a
commit 65f00e6ce7
13 changed files with 905 additions and 11 deletions

1
AI-grep Submodule

@ -0,0 +1 @@
Subproject commit b512548cf3ae90983631d0d2ca359a183b25d87c

134
CLAUDE.md
View file

@ -13,3 +13,137 @@ pause after every section.
CLAUDE_DESCRIPTION.md is used for LLMs to understand the project faster. Keep it up to date with the latest changes and updates.
<!-- rtk-instructions v2 -->
# RTK (Rust Token Killer) - Token-Optimized Commands
## Golden Rule
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
**Important**: Even in command chains with `&&`, use `rtk`:
```bash
# ❌ Wrong
git add . && git commit -m "msg" && git push
# ✅ Correct
rtk git add . && rtk git commit -m "msg" && rtk git push
```
## RTK Commands by Workflow
### Build & Compile (80-90% savings)
```bash
rtk cargo build # Cargo build output
rtk cargo check # Cargo check output
rtk cargo clippy # Clippy warnings grouped by file (80%)
rtk tsc # TypeScript errors grouped by file/code (83%)
rtk lint # ESLint/Biome violations grouped (84%)
rtk prettier --check # Files needing format only (70%)
rtk next build # Next.js build with route metrics (87%)
```
### Test (90-99% savings)
```bash
rtk cargo test # Cargo test failures only (90%)
rtk vitest run # Vitest failures only (99.5%)
rtk playwright test # Playwright failures only (94%)
rtk test <cmd> # Generic test wrapper - failures only
```
### Git (59-80% savings)
```bash
rtk git status # Compact status
rtk git log # Compact log (works with all git flags)
rtk git diff # Compact diff (80%)
rtk git show # Compact show (80%)
rtk git add # Ultra-compact confirmations (59%)
rtk git commit # Ultra-compact confirmations (59%)
rtk git push # Ultra-compact confirmations
rtk git pull # Ultra-compact confirmations
rtk git branch # Compact branch list
rtk git fetch # Compact fetch
rtk git stash # Compact stash
rtk git worktree # Compact worktree
```
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
### GitHub (26-87% savings)
```bash
rtk gh pr view <num> # Compact PR view (87%)
rtk gh pr checks # Compact PR checks (79%)
rtk gh run list # Compact workflow runs (82%)
rtk gh issue list # Compact issue list (80%)
rtk gh api # Compact API responses (26%)
```
### JavaScript/TypeScript Tooling (70-90% savings)
```bash
rtk pnpm list # Compact dependency tree (70%)
rtk pnpm outdated # Compact outdated packages (80%)
rtk pnpm install # Compact install output (90%)
rtk npm run <script> # Compact npm script output
rtk npx <cmd> # Compact npx command output
rtk prisma # Prisma without ASCII art (88%)
```
### Files & Search (60-75% savings)
```bash
rtk ls <path> # Tree format, compact (65%)
rtk read <file> # Code reading with filtering (60%)
rtk grep <pattern> # Search grouped by file (75%)
rtk find <pattern> # Find grouped by directory (70%)
```
### Analysis & Debug (70-90% savings)
```bash
rtk err <cmd> # Filter errors only from any command
rtk log <file> # Deduplicated logs with counts
rtk json <file> # JSON structure without values
rtk deps # Dependency overview
rtk env # Environment variables compact
rtk summary <cmd> # Smart summary of command output
rtk diff # Ultra-compact diffs
```
### Infrastructure (85% savings)
```bash
rtk docker ps # Compact container list
rtk docker images # Compact image list
rtk docker logs <c> # Deduplicated logs
rtk kubectl get # Compact resource list
rtk kubectl logs # Deduplicated pod logs
```
### Network (65-70% savings)
```bash
rtk curl <url> # Compact HTTP responses (70%)
rtk wget <url> # Compact download output (65%)
```
### Meta Commands
```bash
rtk gain # View token savings statistics
rtk gain --history # View command history with savings
rtk discover # Analyze Claude Code sessions for missed RTK usage
rtk proxy <cmd> # Run command without filtering (for debugging)
rtk init # Add RTK instructions to CLAUDE.md
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
```
## Token Savings Overview
| Category | Commands | Typical Savings |
|----------|----------|-----------------|
| Tests | vitest, playwright, cargo test | 90-99% |
| Build | next, tsc, lint, prettier | 70-87% |
| Git | status, log, diff, add, commit | 59-80% |
| GitHub | gh pr, gh run, gh issue | 26-87% |
| Package Managers | pnpm, npm, npx | 70-90% |
| Files | ls, read, grep, find | 60-75% |
| Infrastructure | docker, kubectl | 85% |
| Network | curl, wget | 65-70% |
Overall average: **60-90% token reduction** on common development operations.
<!-- /rtk-instructions -->

2
next-env.d.ts vendored
View file

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -95,10 +95,10 @@
## 11. Prediction UI — Disagreements & Feedback
- [x] 11.1 Implement disagreement detection — compare human spans vs prediction spans with >50% overlap, classify as missed_by_model, missed_by_human, label_mismatch
- [ ] 11.2 Render disagreement highlights — red dashed border (missed_by_model), yellow highlight (missed_by_human), orange border (label_mismatch)
- [x] 11.2 Render disagreement highlights — red dashed border (missed_by_model), yellow highlight (missed_by_human), orange border (label_mismatch)
- [x] 11.3 Add "Show only disagreements" filter toggle in PredictionPanel
- [ ] 11.4 Implement prediction-to-annotation feedback — click missed_by_human prediction opens span annotation dialog pre-filled with predicted label/times
- [ ] 11.5 Add "Not a pattern" dismiss action — saves negative annotation with label "O" and model_prediction metadata
- [x] 11.4 Implement prediction-to-annotation feedback — click missed_by_human prediction opens span annotation dialog pre-filled with predicted label/times
- [x] 11.5 Add "Not a pattern" dismiss action — saves negative annotation with label "O" and model_prediction metadata
- [x] 11.6 Display prediction summary in PredictionPanel — prediction count, agreement count, disagreement count
## 12. Inference API Connection & Error Handling

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-15

View file

@ -0,0 +1,92 @@
## Context
The candle annotator uses lightweight-charts for rendering candlestick data and currently has two rendering layers for annotations:
1. **Canvas-based (ISeriesPrimitive)**: Used by `SpanRectanglePrimitive.ts` for span annotations. These are attached to the series via `series.attachPrimitive()` and render natively within the chart canvas. They support `hitTest()`, autoscaling, and z-ordering.
2. **SVG overlay**: Used by `SvgOverlay.tsx` for line annotations. An absolutely-positioned SVG element sits on top of the chart with `zIndex: 1111`, intercepting pointer events when the line or delete tool is active. It duplicates coordinate conversion logic and manages its own interaction state.
The existing `src/plugins/trend-line.ts` already implements a `TrendLine` class using `ISeriesPrimitive<Time>` but is not wired into the interactive drawing flow. The Toolbox component manages tool modes — line drawing is triggered by annotation types with `category: 'line'`.
## Goals / Non-Goals
**Goals:**
- Replace SVG overlay line rendering with the existing `TrendLine` plugin (canvas-based)
- Add interactive drawing for lines using chart's native click/crosshair events instead of SVG pointer events
- Add a rectangle annotation tool using a new `RectangleDrawingPrimitive` plugin
- Maintain all existing line behaviors: two-click drawing, preview, selection, endpoint dragging, deletion
- Use the same database schema and API endpoints — rectangles stored as `label_type: "rectangle"` with geometry JSON
- Keep rendering approach consistent with `SpanRectanglePrimitive` patterns
**Non-Goals:**
- Changing span annotation implementation (already uses ISeriesPrimitive)
- Adding axis labels or price/time markers to lines or rectangles
- Multi-select or group operations on annotations
- Undo/redo functionality
- Toolbar UI for the rectangle drawing tool from the upstream example (we use our own Toolbox)
## Decisions
### 1. Remove SVG overlay entirely
**Decision**: Delete `SvgOverlay.tsx` and move all line rendering to the `TrendLine` plugin.
**Rationale**: The SVG overlay creates a layering problem — it must intercept pointer events by sitting above the chart, which blocks chart interactions (zoom, pan, crosshair) when the line tool is active. The plugin system renders within the chart canvas and can coexist with native chart interactions. `SpanRectanglePrimitive` already demonstrates this approach works well.
**Alternative considered**: Keep SVG for interactive drawing (preview), use plugin only for saved lines. Rejected because it still requires maintaining two rendering systems and the coordinate conversion duplication.
### 2. Manage drawing interaction in CandleChart via chart events
**Decision**: Use `chart.subscribeClick()` and `chart.subscribeCrosshairMove()` in `CandleChart.tsx` to handle the two-click drawing flow for both lines and rectangles.
**Rationale**: The chart API provides native click and crosshair events that include time/price data coordinates directly, eliminating the need for manual pixel-to-data conversion. This matches how the upstream `RectangleDrawingTool` example works. `CandleChart.tsx` already owns the chart and series references.
**Alternative considered**: Create a separate `DrawingManager` component. Rejected for this scope — CandleChart already manages span primitives, so adding line/rectangle primitive management follows the existing pattern. Extraction can happen later if complexity warrants it.
### 3. Enhance TrendLine plugin with hit testing and selection
**Decision**: Add `hitTest()` and `setSelected()` methods to the existing `TrendLine` class, following the `SpanRectanglePrimitive` pattern.
**Rationale**: `hitTest()` enables the chart's built-in click handling to detect which primitive was clicked, eliminating the need for manual distance-to-line-segment calculation in the overlay. Selection state changes line appearance (thicker stroke, handles).
**Implementation**: Hit testing for lines uses perpendicular distance to the line segment (same algorithm currently in `SvgOverlay`, moved into the renderer's coordinate space).
### 4. Create RectangleDrawingPrimitive following SpanRectanglePrimitive patterns
**Decision**: Create a new `src/plugins/rectangle-drawing.ts` that implements `ISeriesPrimitive<Time>` directly (not extending a base class), storing two corner points `{time, price}` and rendering a filled rectangle.
**Rationale**: The upstream `RectangleDrawingTool` example is complex (multiple axis views, toolbar UI, PluginBase class) — most of that complexity is unnecessary since we have our own Toolbox and don't need axis highlighting. Our `SpanRectanglePrimitive` already demonstrates a simpler rectangle rendering approach. The new plugin adapts that pattern but uses two arbitrary corner points instead of candle-range-derived bounds.
**Differences from SpanRectanglePrimitive**: Span rectangles are defined by candle ranges (start_time/end_time → compute min_low/max_high from candle data). Rectangle annotations are freeform — two arbitrary {time, price} corners, no dependency on candle data.
### 5. Preview primitives for drawing feedback
**Decision**: Create temporary "preview" instances of `TrendLine` and `RectangleDrawingPrimitive` during the two-click drawing flow. Update endpoint on crosshair move. Remove and replace with permanent instance on second click.
**Rationale**: This is the same approach used in the upstream `RectangleDrawingTool` example (`PreviewRectangle` class). A preview primitive with dashed/semi-transparent styling gives visual feedback without requiring a separate rendering layer. Attach on first click, update on move, detach and replace on second click.
### 6. Endpoint dragging via primitive update
**Decision**: When a line is selected and the user drags a handle, call `trendLine.updatePoints(p1, p2)` and `requestUpdate()` to re-render at new coordinates. On mouse up, persist via PATCH API.
**Rationale**: The `TrendLine` class already has `updatePoints()`. Drag detection uses crosshair move events. This replaces the SVG-based drag handling that manipulated DOM elements directly.
**Handle rendering**: Draw small circles at endpoints when selected, as part of the `TrendLinePaneRenderer.draw()` method (no separate SVG elements needed).
### 7. Database and API — no changes
**Decision**: Rectangles use the existing `annotations` table with `label_type: "rectangle"` and `geometry: {"startTime", "startPrice", "endTime", "endPrice"}` — identical schema to lines.
**Rationale**: The geometry format is the same (two points). No migration needed. The API endpoints (`POST/PATCH/DELETE /api/annotations`) already handle arbitrary `label_type` and `geometry` values.
## Risks / Trade-offs
**[Risk] Line hit testing accuracy in canvas coordinates** → The distance-to-line-segment algorithm needs to work in bitmap coordinate space. Mitigation: Port the existing algorithm from SvgOverlay, test with various zoom levels. Use a tolerance of ~10 CSS pixels (scaled by pixel ratio).
**[Risk] Breaking existing saved line annotations** → Existing lines in the database use the same geometry format, so they should render identically with the plugin. Mitigation: No data migration needed; verify rendering parity before removing SVG overlay.
**[Risk] Pointer event handling conflicts** → Chart's native click events fire for both annotation tools and regular chart interaction. Mitigation: Only handle annotation clicks when `activeTool` is set to a drawing/delete tool. When no tool is active, clicks pass through to normal chart behavior (zoom, crosshair, etc.).
**[Risk] Performance with many primitives** → Each line/rectangle is a separate `ISeriesPrimitive` instance. Mitigation: This is the same approach used for spans, and the number of annotations per chart is typically small (tens, not thousands).
**[Trade-off] No PluginBase class** → We implement `ISeriesPrimitive` directly rather than using the upstream `PluginBase` abstraction. This means slightly more boilerplate per plugin but avoids adding a dependency/abstraction for just two plugin types.

View file

@ -0,0 +1,29 @@
## Why
The current line drawing implementation uses an SVG overlay (`SvgOverlay.tsx`) positioned on top of the lightweight-charts canvas. This creates a disconnect — lines don't participate in chart autoscaling, coordinate conversion is duplicated outside the chart library, and the rendering layer is split between SVG and canvas. Lightweight-charts natively supports drawing primitives via the `ISeriesPrimitive` API, and the project already has a `TrendLine` plugin in `src/plugins/trend-line.ts` (unused) and uses `SpanRectanglePrimitive` for span annotations. Migrating line drawing to the native plugin system and adding rectangle annotations will unify the rendering approach, improve performance, and enable consistent interaction patterns.
## What Changes
- **BREAKING**: Remove SVG overlay line drawing (`SvgOverlay.tsx`) and replace with lightweight-charts `TrendLine` plugin-based rendering
- Implement interactive two-click line drawing using chart's native `subscribeClick()` and `subscribeCrosshairMove()` events instead of SVG pointer events
- Add a new rectangle annotation tool based on the lightweight-charts `RectangleDrawingTool` plugin pattern (two-click to define opposite corners)
- Add "rectangle" tool mode to the Toolbox alongside existing line/span/label tools
- Store rectangle annotations in the database with `label_type: "rectangle"` and geometry containing `{startTime, startPrice, endTime, endPrice}`
- Update line editing (drag endpoints) and deletion to work through the plugin system instead of SVG hit detection
- Add preview rendering during drawing for both lines and rectangles (using crosshair move events)
## Capabilities
### New Capabilities
- `rectangle-annotation`: Two-click rectangle drawing tool using lightweight-charts ISeriesPrimitive, with preview during drawing, persistence, editing, and deletion
### Modified Capabilities
- `annotation-tools`: Line drawing changes from SVG overlay to lightweight-charts TrendLine plugin; line tool mode interaction moves from SVG events to chart subscribeClick/subscribeCrosshairMove; line editing and deletion use plugin-based hit detection instead of SVG proximity calculation
## Impact
- **Frontend components**: `SvgOverlay.tsx` removed or gutted; `CandleChart.tsx` updated to manage TrendLine primitives; `Toolbox.tsx` gains rectangle tool button
- **Plugins**: `src/plugins/trend-line.ts` enhanced with interactive drawing support (click handlers, preview); new `src/plugins/rectangle-drawing.ts` added
- **API**: Existing annotation endpoints used as-is; rectangle annotations use the same `annotations` table with `label_type: "rectangle"` and `geometry` JSON
- **Database**: No schema changes — rectangles use the existing `geometry` column pattern (same as lines)
- **Dependencies**: No new dependencies — uses lightweight-charts APIs already in use

View file

@ -0,0 +1,83 @@
## ADDED Requirements
### Requirement: Active tool includes rectangle mode
The system SHALL add "rectangle" to the available tool modes alongside "select", "break_up", "break_down", "line", "span", and "delete". Only one tool SHALL be active at a time.
#### Scenario: Rectangle tool in tool list
- **WHEN** the Toolbox renders
- **THEN** a rectangle tool button is available alongside the existing tool buttons
### Requirement: Line rendering via TrendLine plugin
The system SHALL render saved line annotations using the `TrendLine` class (implementing `ISeriesPrimitive<Time>`) instead of SVG `<line>` elements. Each line annotation SHALL have one `TrendLine` primitive instance attached to the candlestick series via `series.attachPrimitive()`. The `SvgOverlay` component SHALL be removed.
#### Scenario: Saved lines render as canvas primitives
- **WHEN** line annotations exist for the active chart
- **THEN** each line renders via a TrendLine primitive on the chart canvas (not SVG overlay)
#### Scenario: Lines participate in autoscaling
- **WHEN** a line annotation's price range extends beyond visible candle data
- **THEN** the chart autoscale includes the line's price range via `autoscaleInfo()`
#### Scenario: Lines update on zoom/pan
- **WHEN** user zooms or pans the chart
- **THEN** line primitives automatically reposition via the ISeriesPrimitive lifecycle
### Requirement: Line hit testing
The `TrendLine` class SHALL implement `hitTest(x, y)` to detect clicks near the line. Hit testing SHALL calculate the perpendicular distance from the click point to the line segment and return a hit if within 10 CSS pixels (scaled by device pixel ratio).
#### Scenario: Click near line detected
- **WHEN** user clicks within 10 CSS pixels of a line segment
- **THEN** `hitTest()` returns a `PrimitiveHoveredItem` with the annotation ID as `externalId`
#### Scenario: Click far from line not detected
- **WHEN** user clicks more than 10 CSS pixels from any line segment
- **THEN** `hitTest()` returns null
### Requirement: Line selection handles via plugin
When a line is selected, the `TrendLine` renderer SHALL draw circular endpoint handles (radius 6px) at both endpoints of the line. The handles SHALL be rendered as part of the canvas draw call, not as separate SVG elements.
#### Scenario: Handles appear on selection
- **WHEN** a line is selected (via click with line tool active)
- **THEN** circular handles render at both endpoints of the line on the canvas
#### Scenario: Handles disappear on deselection
- **WHEN** the selected line is deselected (Escape key or clicking elsewhere)
- **THEN** the endpoint handles no longer render
## MODIFIED Requirements
### Requirement: Two-click line drawing
When the "line" tool is active, the system SHALL implement a two-click drawing interaction using `chart.subscribeClick()` for click detection and `chart.subscribeCrosshairMove()` for preview updates. The first click sets the start point (time, price). The second click sets the end point (time, price). After the second click, the system SHALL save an annotation with `label_type: "line"` and `geometry` containing JSON: `{"startTime": <unix>, "startPrice": <float>, "endTime": <unix>, "endPrice": <float>}`. The line SHALL render immediately as a TrendLine primitive attached to the candlestick series.
#### Scenario: Draw a trend line
- **WHEN** "line" tool is active and user clicks two points on the chart
- **THEN** system saves a line annotation with start/end coordinates and renders the line as a TrendLine primitive
#### Scenario: Visual feedback during line drawing
- **WHEN** "line" tool is active and user has clicked the first point but not the second
- **THEN** system displays a preview TrendLine primitive (dashed or semi-transparent) from the first point to the current crosshair position, updating via `subscribeCrosshairMove()`
#### Scenario: Cancel line drawing
- **WHEN** user presses Escape during a two-click line drawing (after first click)
- **THEN** system cancels the line drawing, detaches the preview primitive, and clears the drawing state without saving
### Requirement: Delete annotation
When the "delete" tool is active and the user clicks on or near an existing annotation (marker, line, or rectangle), the system SHALL remove that annotation from the database and update the chart display immediately. Line and rectangle hit detection SHALL use the primitive's `hitTest()` method instead of SVG proximity calculation.
#### Scenario: Delete a marker annotation
- **WHEN** "delete" tool is active and user clicks on a candle that has a marker annotation
- **THEN** system removes the annotation from the database and the marker disappears from the chart
#### Scenario: Delete a line annotation
- **WHEN** "delete" tool is active and user clicks near an existing line
- **THEN** system detects the hit via `TrendLine.hitTest()`, sends DELETE /api/annotations/{id}, detaches the primitive from the series, and updates the annotation list
#### Scenario: Delete a rectangle annotation
- **WHEN** "delete" tool is active and user clicks within a rectangle
- **THEN** system detects the hit via `RectangleDrawingPrimitive.hitTest()`, sends DELETE /api/annotations/{id}, detaches the primitive, and updates the annotation list
## REMOVED Requirements
### Requirement: SVG overlay rendering
**Reason**: Replaced by TrendLine plugin rendering. Line annotations now render via `ISeriesPrimitive` on the chart canvas instead of SVG `<line>` elements in an overlay. All coordinate conversion, hit detection, and interaction handling moves to the plugin system and chart native events.
**Migration**: Remove `SvgOverlay.tsx` component. Remove its usage from `CandleChart.tsx`. Line annotation data in the database is unchanged — the same geometry format is used by the TrendLine plugin.

View file

@ -0,0 +1,105 @@
## ADDED Requirements
### Requirement: Rectangle tool mode
The Toolbox SHALL include a "rectangle" tool button. When activated, the chart enters rectangle drawing mode. Only one tool SHALL be active at a time — activating the rectangle tool deactivates any other active tool.
#### Scenario: Activate rectangle tool
- **WHEN** user clicks the rectangle tool button in the Toolbox
- **THEN** the rectangle tool becomes active, the button appears visually selected, and the chart cursor changes to crosshair
#### Scenario: Deactivate rectangle tool
- **WHEN** user clicks the already-active rectangle tool button
- **THEN** the tool deactivates, the mode returns to "select", and the cursor returns to default
#### Scenario: Rectangle tool deactivates other tools
- **WHEN** the "line" tool is active and user clicks the rectangle tool button
- **THEN** the line tool deactivates and the rectangle tool becomes active
### Requirement: Two-click rectangle drawing
When the "rectangle" tool is active, the system SHALL implement a two-click interaction to define a rectangle. The first click sets one corner point (time, price). The second click sets the opposite corner point (time, price). The rectangle is defined by these two arbitrary data-coordinate corners — it is NOT constrained to candle boundaries.
#### Scenario: Draw a rectangle
- **WHEN** "rectangle" tool is active and user clicks two points on the chart
- **THEN** the system saves a rectangle annotation with the two corner coordinates and renders a filled semi-transparent rectangle on the chart
#### Scenario: First click registers corner
- **WHEN** "rectangle" tool is active and user clicks on the chart
- **THEN** the system records the click position as {time, price} for the first corner
#### Scenario: Second click completes rectangle
- **WHEN** user has set the first corner and clicks a second position
- **THEN** the system saves a rectangle annotation via POST /api/annotations with `label_type: "rectangle"` and `geometry: {"startTime", "startPrice", "endTime", "endPrice"}`
### Requirement: Rectangle preview during drawing
After the first click, the system SHALL display a preview rectangle that stretches from the first corner to the current cursor position. The preview SHALL have a dashed border and reduced opacity to distinguish it from saved rectangles.
#### Scenario: Preview follows cursor
- **WHEN** user has clicked the first corner and moves the mouse
- **THEN** a semi-transparent preview rectangle renders from the first corner to the cursor position, updating in real-time via crosshair move events
#### Scenario: Preview disappears on cancel
- **WHEN** user presses Escape during rectangle drawing (after first click)
- **THEN** the preview rectangle disappears and the drawing is cancelled without saving
### Requirement: Rectangle rendering via ISeriesPrimitive
The system SHALL render saved rectangle annotations using a `RectangleDrawingPrimitive` class that implements `ISeriesPrimitive<Time>`. Each rectangle annotation SHALL have one primitive instance attached to the candlestick series via `series.attachPrimitive()`.
#### Scenario: Rectangle renders for saved annotation
- **WHEN** a rectangle annotation exists for the active chart
- **THEN** a semi-transparent filled rectangle renders on the chart canvas at the stored corner coordinates
#### Scenario: Rectangle uses annotation color
- **WHEN** a rectangle annotation has a color value
- **THEN** the rectangle renders with that color as fill (at reduced opacity) and border
#### Scenario: Rectangle updates on zoom/pan
- **WHEN** user zooms or pans the chart
- **THEN** rectangle primitives automatically reposition via the ISeriesPrimitive lifecycle (coordinate conversion in paneView.update())
#### Scenario: Rectangle z-order
- **WHEN** rectangle annotations render on the chart
- **THEN** they SHALL render at "bottom" z-order (behind candlesticks), consistent with span rectangles
### Requirement: Rectangle hit testing
The `RectangleDrawingPrimitive` SHALL implement `hitTest(x, y)` to detect clicks within the rectangle bounds. Hit testing SHALL convert pixel coordinates to data coordinates and check if the point falls within the rectangle's time/price range.
#### Scenario: Click inside rectangle detected
- **WHEN** user clicks at a position inside a rectangle's bounds
- **THEN** `hitTest()` returns a `PrimitiveHoveredItem` with the annotation ID as `externalId`
#### Scenario: Click outside rectangle not detected
- **WHEN** user clicks at a position outside all rectangle bounds
- **THEN** `hitTest()` returns null
### Requirement: Rectangle selection
The system SHALL allow users to select a rectangle annotation by clicking within its bounds when the "rectangle" or "select" tool is active.
#### Scenario: Click to select rectangle
- **WHEN** user clicks within a rectangle annotation
- **THEN** the rectangle appears selected (thicker border or increased opacity)
#### Scenario: Click to deselect rectangle
- **WHEN** user clicks outside all rectangle annotations while one is selected
- **THEN** the selection is cleared
### Requirement: Rectangle deletion
The system SHALL allow users to delete rectangle annotations via the delete tool.
#### Scenario: Delete rectangle with delete tool
- **WHEN** the "delete" tool is active and user clicks within a rectangle
- **THEN** the system sends DELETE /api/annotations/{id}, removes the primitive from the chart, and updates the annotation list
#### Scenario: Delete selected rectangle with keyboard
- **WHEN** a rectangle is selected and user presses Delete or Backspace
- **THEN** the system deletes the rectangle annotation and removes it from the chart
### Requirement: Rectangle database storage
Rectangle annotations SHALL be stored in the existing `annotations` table with `label_type: "rectangle"` and `geometry` containing JSON: `{"startTime": <unix>, "startPrice": <float>, "endTime": <float>, "endPrice": <float>}`. The `startTime/startPrice` represents one corner, `endTime/endPrice` the opposite corner.
#### Scenario: Rectangle annotation persisted
- **WHEN** user completes a two-click rectangle drawing
- **THEN** the system sends POST /api/annotations with label_type "rectangle", the active chart_id, selected color, and geometry JSON with the two corner coordinates
#### Scenario: Rectangle annotation loaded on chart switch
- **WHEN** user switches to a chart that has rectangle annotations
- **THEN** the system fetches annotations, creates RectangleDrawingPrimitive instances for each rectangle annotation, and attaches them to the series

View file

@ -0,0 +1,57 @@
## 1. Enhance TrendLine Plugin
- [ ] 1.1 Add `hitTest(x, y)` method to `TrendLine` class — calculate perpendicular distance from click point to line segment in pixel coordinates, return `PrimitiveHoveredItem` with annotation ID if within 10px tolerance
- [ ] 1.2 Add `setSelected(isSelected)` method and selection state to `TrendLine` — when selected, renderer draws thicker line and circular endpoint handles (radius 6px, white fill, colored stroke)
- [ ] 1.3 Add `attached()/detached()` lifecycle methods to `TrendLine` to store `SeriesAttachedParameter` reference (needed for hitTest coordinate conversion)
- [ ] 1.4 Add preview line support — dashed stroke style and reduced opacity when an `isPreview` option is set
## 2. Create RectangleDrawingPrimitive Plugin
- [ ] 2.1 Create `src/plugins/rectangle-drawing.ts` implementing `ISeriesPrimitive<Time>` with renderer, pane view, and data model for two corner points `{time, price}`
- [ ] 2.2 Implement `RectangleDrawingPaneRenderer.draw()` — filled semi-transparent rectangle with border, using `useBitmapCoordinateSpace` for HiDPI scaling
- [ ] 2.3 Implement `hitTest(x, y)` — convert pixel to data coordinates, check if within rectangle time/price bounds
- [ ] 2.4 Add `setSelected(isSelected)` and selection visual feedback (thicker border, increased opacity)
- [ ] 2.5 Add preview mode support — dashed border and reduced opacity for preview rectangles during drawing
- [ ] 2.6 Add `autoscaleInfo()` to include rectangle price range in chart scaling
- [ ] 2.7 Set z-order to "bottom" so rectangles render behind candlesticks
## 3. Wire Up Drawing Interaction in CandleChart
- [ ] 3.1 Add state for drawing mode: `drawingState: {tool: 'line'|'rectangle', firstPoint: {time, price}} | null` and `previewPrimitive: TrendLine | RectangleDrawingPrimitive | null`
- [ ] 3.2 Subscribe to `chart.subscribeClick()` — on first click when line/rectangle tool active, record first point and attach preview primitive; on second click, save annotation via API, detach preview, attach permanent primitive
- [ ] 3.3 Subscribe to `chart.subscribeCrosshairMove()` — when drawing in progress, update preview primitive's endpoint via `updatePoints()` or equivalent
- [ ] 3.4 Handle Escape key — detach preview primitive and clear drawing state
- [ ] 3.5 Manage TrendLine primitives for saved line annotations — create/attach on load, detach on delete, update on edit
## 4. Wire Up Rectangle Primitives in CandleChart
- [ ] 4.1 On annotation fetch, create `RectangleDrawingPrimitive` instances for `label_type: "rectangle"` annotations and attach to series
- [ ] 4.2 On chart switch, detach old rectangle primitives and create new ones for the new chart's annotations
- [ ] 4.3 Handle rectangle selection — on click hit detected via primitive `hitTest()`, call `setSelected()` and track selected annotation ID
- [ ] 4.4 Handle rectangle deletion — when delete tool active and hit detected, send DELETE API call, detach primitive, refresh annotations
## 5. Update Toolbox
- [ ] 5.1 Add "rectangle" tool button to Toolbox (using existing `RectangleHorizontal` lucide icon import, which is already present)
- [ ] 5.2 Wire rectangle button to `onToolChange('rectangle')` with same toggle behavior as other tools
## 6. Remove SVG Overlay
- [ ] 6.1 Remove `SvgOverlay` import and JSX from `CandleChart.tsx`
- [ ] 6.2 Delete `src/components/SvgOverlay.tsx`
- [ ] 6.3 Move line annotation primitive management into CandleChart (replace what SvgOverlay was doing — loading saved lines, managing line primitives on annotation fetch/delete)
## 7. Line Endpoint Dragging
- [ ] 7.1 Implement drag detection — when a selected line's endpoint handle is clicked (via hitTest near endpoint), enter drag mode
- [ ] 7.2 On crosshair move during drag, call `trendLine.updatePoints()` to reposition the dragged endpoint in real-time
- [ ] 7.3 On click to release drag, persist updated geometry via PATCH /api/annotations/{id}
## 8. Verification
- [ ] 8.1 Verify existing saved line annotations render correctly with TrendLine plugin (visual parity with old SVG rendering)
- [ ] 8.2 Verify line drawing, preview, selection, dragging, and deletion all work end-to-end
- [ ] 8.3 Verify rectangle drawing, preview, selection, and deletion work end-to-end
- [ ] 8.4 Verify chart zoom/pan correctly repositions both line and rectangle primitives
- [ ] 8.5 Verify tool switching between line, rectangle, span, label, and delete modes works correctly
- [ ] 8.6 Verify annotations persist and reload correctly on chart switch

View file

@ -306,6 +306,83 @@ export default function Home() {
setSelectedSpanId(spanId);
};
// Handle prediction click to convert to annotation
const handlePredictionClick = useCallback(async (span: PredictionSpan, disagreementType: string | null) => {
if (!activeChartId) return;
// Find the span label type that matches the prediction label
const matchingLabelType = spanLabelTypes.find((lt) => lt.name === span.label);
if (!matchingLabelType) {
console.warn(`No span label type found for prediction label: ${span.label}`);
return;
}
// Create span annotation from prediction
try {
const response = await fetch('/api/span-annotations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chart_id: activeChartId,
start_time: span.start_time,
end_time: span.end_time,
label: span.label,
confidence: 3, // Default confidence for model-confirmed annotations
source: disagreementType === 'label_mismatch' ? 'model_corrected' : 'model_confirmed',
model_prediction: {
label: span.label,
confidence: span.avg_confidence,
},
}),
});
if (response.ok) {
await fetchSpanAnnotations(activeChartId);
// Show a brief notification (you could add a toast notification here)
console.log(`Created span annotation from prediction: ${span.label}`);
} else {
console.error('Failed to create span annotation from prediction');
}
} catch (error) {
console.error('Error creating span annotation from prediction:', error);
}
}, [activeChartId, spanLabelTypes, fetchSpanAnnotations]);
// Handle prediction dismiss (save as negative annotation with label "O")
const handlePredictionDismiss = useCallback(async (span: PredictionSpan, disagreementType: string | null) => {
if (!activeChartId) return;
// Create negative annotation with label "O"
try {
const response = await fetch('/api/span-annotations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chart_id: activeChartId,
start_time: span.start_time,
end_time: span.end_time,
label: 'O', // "O" means "not a pattern"
confidence: 5, // High confidence for explicit user correction
source: 'human_correction',
model_prediction: {
label: span.label,
confidence: span.avg_confidence,
},
}),
});
if (response.ok) {
await fetchSpanAnnotations(activeChartId);
console.log(`Dismissed prediction as "not a pattern": ${span.label}`);
} else {
console.error('Failed to save negative annotation');
}
} catch (error) {
console.error('Error saving negative annotation:', error);
}
}, [activeChartId, fetchSpanAnnotations]);
const handleDeleteSpan = async (spanId: number) => {
try {
const response = await fetch(`/api/span-annotations/${spanId}`, {
@ -715,6 +792,8 @@ export default function Home() {
modelInfo={predictionState.modelInfo}
predictionSummary={predictionSummary}
showOnlyDisagreements={showOnlyDisagreements}
onPredictionClick={handlePredictionClick}
onPredictionDismiss={handlePredictionDismiss}
/>
</main>
</div>

View file

@ -84,6 +84,8 @@ interface CandleChartProps {
modelInfo?: ModelInfoResponse | null;
predictionSummary?: PredictionSummary | null;
showOnlyDisagreements?: boolean;
onPredictionClick?: (span: PredictionSpan, disagreementType: string | null) => void;
onPredictionDismiss?: (span: PredictionSpan, disagreementType: string | null) => void;
}
export interface CandleChartHandle {
@ -112,6 +114,8 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
modelInfo = null,
predictionSummary = null,
showOnlyDisagreements = false,
onPredictionClick,
onPredictionDismiss,
}, ref) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
@ -392,6 +396,20 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
});
}
// Build disagreement lookup map for highlighting
// Map: predictionSpan start_time -> disagreement type
const disagreementMap = new Map<number, string>();
const disagreementSpanSet = new Set<string>(); // Set of "start-end" keys for filtering
if (predictionSummary?.disagreements) {
predictionSummary.disagreements.forEach((d) => {
if (d.predictionSpan) {
disagreementMap.set(d.predictionSpan.start_time, d.type);
disagreementSpanSet.add(`${d.predictionSpan.start_time}-${d.predictionSpan.end_time}`);
}
});
}
// Helper to convert hex to rgba
const hexToRgba = (hex: string, alpha: number): string => {
hex = hex.replace('#', '');
@ -404,7 +422,16 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
// Build candle price lookup for histogram values
const candleMap = new Map(candles.map((c) => [c.time, c]));
// Filter and map predictions to histogram data
// Build a map from prediction time to its span for disagreement lookup
const predictionTimeToSpan = new Map<number, PredictionSpan>();
predictionSpans.forEach((span) => {
// Associate all times in the span with the span object
for (let t = span.start_time; t <= span.end_time; t += 60) { // Assuming 1-minute candles, adjust if needed
predictionTimeToSpan.set(t, span);
}
});
// Filter and map predictions to histogram data with disagreement highlighting
const histogramData: (HistogramData & { color?: string })[] = perCandlePredictions
.filter((p) => {
// Filter by confidence threshold
@ -413,17 +440,44 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
if (!selectedLabels.has(p.label)) return false;
// Skip "O" (no-pattern) labels
if (p.label === 'O') return false;
// If showOnlyDisagreements is enabled, only show predictions that are part of disagreements
if (showOnlyDisagreements) {
const span = predictionTimeToSpan.get(p.time);
if (!span) return false;
const spanKey = `${span.start_time}-${span.end_time}`;
if (!disagreementSpanSet.has(spanKey)) return false;
}
return true;
})
.map((p) => {
const candle = candleMap.get(p.time);
// Use candle high as the histogram value so it overlays correctly
const value = candle ? candle.high : 0;
const baseColor = labelColorMap[p.label] || '#888888';
// Check if this prediction is part of a disagreement
const span = predictionTimeToSpan.get(p.time);
const disagreementType = span ? disagreementMap.get(span.start_time) : null;
let baseColor = labelColorMap[p.label] || '#888888';
let alpha = 0.15;
// Apply disagreement-specific colors and styling
if (disagreementType === 'missed_by_human') {
// Yellow highlight for predictions missed by humans
baseColor = '#eab308'; // yellow
alpha = 0.25;
} else if (disagreementType === 'label_mismatch') {
// Orange for label mismatches
baseColor = '#f97316'; // orange
alpha = 0.25;
}
return {
time: p.time as Time,
value,
color: hexToRgba(baseColor, 0.15),
color: hexToRgba(baseColor, alpha),
};
})
.sort((a, b) => (a.time as number) - (b.time as number));
@ -436,24 +490,43 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
if (span.avg_confidence < confidenceThreshold) return false;
if (!selectedLabels.has(span.label)) return false;
if (span.label === 'O') return false;
// If showOnlyDisagreements is enabled, only show spans that are disagreements
if (showOnlyDisagreements) {
const spanKey = `${span.start_time}-${span.end_time}`;
if (!disagreementSpanSet.has(spanKey)) return false;
}
return true;
})
.map((span) => {
const baseColor = labelColorMap[span.label] || '#888888';
const disagreementType = disagreementMap.get(span.start_time);
let baseColor = labelColorMap[span.label] || '#888888';
let labelText = span.label;
// Apply disagreement-specific styling to markers
if (disagreementType === 'missed_by_human') {
baseColor = '#eab308'; // yellow
labelText = `${span.label}`;
} else if (disagreementType === 'label_mismatch') {
baseColor = '#f97316'; // orange
labelText = `${span.label}`;
}
const confidencePct = Math.round(span.avg_confidence * 100);
return {
time: span.start_time as Time,
position: 'belowBar' as const,
color: baseColor,
shape: 'square' as const,
text: `${span.label} (${confidencePct}%)`,
text: `${labelText} (${confidencePct}%)`,
size: 1,
};
})
.sort((a, b) => (a.time as number) - (b.time as number));
histogramSeriesRef.current.setMarkers(spanMarkers);
}, [predictionVisible, perCandlePredictions, predictionSpans, confidenceThreshold, selectedLabels, modelInfo, candles]);
}, [predictionVisible, perCandlePredictions, predictionSpans, confidenceThreshold, selectedLabels, modelInfo, candles, predictionSummary, showOnlyDisagreements]);
// Handle chart clicks for annotation
useEffect(() => {
@ -537,6 +610,41 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
}
}
// Handle clicks on prediction spans (for converting to annotations or dismissing)
if (!activeTool && predictionVisible && predictionSpans.length > 0) {
const timestamp = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
// Find if click is within any prediction span
const clickedSpan = predictionSpans.find(
(span) => timestamp >= span.start_time && timestamp <= span.end_time
);
if (clickedSpan) {
// Check if this span is a disagreement
const disagreementMap = new Map<number, string>();
if (predictionSummary?.disagreements) {
predictionSummary.disagreements.forEach((d) => {
if (d.predictionSpan) {
disagreementMap.set(d.predictionSpan.start_time, d.type);
}
});
}
const disagreementType = disagreementMap.get(clickedSpan.start_time) || null;
// Check if Alt or Ctrl key is pressed for dismiss action
if ((param.sourceEvent?.altKey || param.sourceEvent?.ctrlKey) && onPredictionDismiss) {
// Alt+Click or Ctrl+Click: Dismiss as "not a pattern"
onPredictionDismiss(clickedSpan, disagreementType);
} else if (onPredictionClick) {
// Normal click: Convert to annotation (only for disagreements)
if (disagreementType === 'missed_by_human' || disagreementType === 'label_mismatch') {
onPredictionClick(clickedSpan, disagreementType);
}
}
}
}
// Select/deselect label markers by clicking them
const isMarkerTool = annotationTypes.find(
(t) => t.category === 'marker' && t.name === activeTool
@ -567,7 +675,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
return () => {
chartRef.current?.unsubscribeClick(handleClick);
};
}, [activeTool, candles, annotations, annotationTypes, onAnnotationChange]);
}, [activeTool, candles, annotations, annotationTypes, onAnnotationChange, predictionVisible, predictionSpans, predictionSummary, onPredictionClick, onPredictionDismiss]);
// Fetch data on mount
useEffect(() => {

204
src/plugins/trend-line.ts Normal file
View file

@ -0,0 +1,204 @@
import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas';
import {
AutoscaleInfo,
Coordinate,
IChartApi,
ISeriesApi,
ISeriesPrimitive,
IPrimitivePaneRenderer,
IPrimitivePaneView,
Logical,
SeriesOptionsMap,
SeriesType,
Time,
} from 'lightweight-charts';
class TrendLinePaneRenderer implements IPrimitivePaneRenderer {
_p1: ViewPoint;
_p2: ViewPoint;
_text1: string;
_text2: string;
_options: TrendLineOptions;
constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: TrendLineOptions) {
this._p1 = p1;
this._p2 = p2;
this._text1 = text1;
this._text2 = text2;
this._options = options;
}
draw(target: CanvasRenderingTarget2D) {
target.useBitmapCoordinateSpace(scope => {
if (
this._p1.x === null ||
this._p1.y === null ||
this._p2.x === null ||
this._p2.y === null
)
return;
const ctx = scope.context;
const x1Scaled = Math.round(this._p1.x * scope.horizontalPixelRatio);
const y1Scaled = Math.round(this._p1.y * scope.verticalPixelRatio);
const x2Scaled = Math.round(this._p2.x * scope.horizontalPixelRatio);
const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio);
ctx.lineWidth = this._options.width;
ctx.strokeStyle = this._options.lineColor;
ctx.beginPath();
ctx.moveTo(x1Scaled, y1Scaled);
ctx.lineTo(x2Scaled, y2Scaled);
ctx.stroke();
if (this._options.showLabels) {
this._drawTextLabel(scope, this._text1, x1Scaled, y1Scaled, true);
this._drawTextLabel(scope, this._text2, x2Scaled, y2Scaled, false);
}
});
}
_drawTextLabel(scope: BitmapCoordinatesRenderingScope, text: string, x: number, y: number, left: boolean) {
scope.context.font = '24px Arial';
scope.context.beginPath();
const offset = 5 * scope.horizontalPixelRatio;
const textWidth = scope.context.measureText(text);
const leftAdjustment = left ? textWidth.width + offset * 4 : 0;
scope.context.fillStyle = this._options.labelBackgroundColor;
scope.context.roundRect(x + offset - leftAdjustment, y - 24, textWidth.width + offset * 2, 24 + offset, 5);
scope.context.fill();
scope.context.beginPath();
scope.context.fillStyle = this._options.labelTextColor;
scope.context.fillText(text, x + offset * 2 - leftAdjustment, y);
}
}
interface ViewPoint {
x: Coordinate | null;
y: Coordinate | null;
}
class TrendLinePaneView implements IPrimitivePaneView {
_source: TrendLine;
_p1: ViewPoint = { x: null, y: null };
_p2: ViewPoint = { x: null, y: null };
constructor(source: TrendLine) {
this._source = source;
}
update() {
const series = this._source._series;
const y1 = series.priceToCoordinate(this._source._p1.price);
const y2 = series.priceToCoordinate(this._source._p2.price);
const timeScale = this._source._chart.timeScale();
const x1 = timeScale.timeToCoordinate(this._source._p1.time);
const x2 = timeScale.timeToCoordinate(this._source._p2.time);
this._p1 = { x: x1, y: y1 };
this._p2 = { x: x2, y: y2 };
}
renderer() {
return new TrendLinePaneRenderer(
this._p1,
this._p2,
'' + this._source._p1.price.toFixed(1),
'' + this._source._p2.price.toFixed(1),
this._source._options
);
}
}
interface Point {
time: Time;
price: number;
}
export interface TrendLineOptions {
lineColor: string;
width: number;
showLabels: boolean;
labelBackgroundColor: string;
labelTextColor: string;
}
const defaultOptions: TrendLineOptions = {
lineColor: 'rgb(0, 0, 0)',
width: 2,
showLabels: false,
labelBackgroundColor: 'rgba(255, 255, 255, 0.85)',
labelTextColor: 'rgb(0, 0, 0)',
};
export class TrendLine implements ISeriesPrimitive<Time> {
_chart: IChartApi;
_series: ISeriesApi<keyof SeriesOptionsMap>;
_p1: Point;
_p2: Point;
_paneViews: TrendLinePaneView[];
_options: TrendLineOptions;
_minPrice: number;
_maxPrice: number;
constructor(
chart: IChartApi,
series: ISeriesApi<SeriesType>,
p1: Point,
p2: Point,
options?: Partial<TrendLineOptions>
) {
this._chart = chart;
this._series = series;
this._p1 = p1;
this._p2 = p2;
this._minPrice = Math.min(this._p1.price, this._p2.price);
this._maxPrice = Math.max(this._p1.price, this._p2.price);
this._options = {
...defaultOptions,
...options,
};
this._paneViews = [new TrendLinePaneView(this)];
}
updatePoints(p1: Point, p2: Point) {
this._p1 = p1;
this._p2 = p2;
this._minPrice = Math.min(this._p1.price, this._p2.price);
this._maxPrice = Math.max(this._p1.price, this._p2.price);
}
getP1(): Point {
return this._p1;
}
getP2(): Point {
return this._p2;
}
autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical): AutoscaleInfo | null {
const p1Index = this._pointIndex(this._p1);
const p2Index = this._pointIndex(this._p2);
if (p1Index === null || p2Index === null) return null;
if (endTimePoint < p1Index || startTimePoint > p2Index) return null;
return {
priceRange: {
minValue: this._minPrice,
maxValue: this._maxPrice,
},
};
}
updateAllViews() {
this._paneViews.forEach(pw => pw.update());
}
paneViews() {
return this._paneViews;
}
_pointIndex(p: Point): number | null {
const coordinate = this._chart
.timeScale()
.timeToCoordinate(p.time);
if (coordinate === null) return null;
const index = this._chart.timeScale().coordinateToLogical(coordinate);
return index;
}
}