feat(charts): mirror crosshair (#19)

<!-- greptile_comment -->

<h3>Greptile Summary</h3>


Implements synchronized crosshair functionality across multiple Chart.js visualizations using a centralized store pattern.

**Key Changes:**
- Created `CrosshairStore` singleton (`crosshair.ts`) to coordinate hover state across charts
- Extended crosshair plugin with `sync` callbacks (`onHover`, `getExternalLabel`) for multi-chart coordination
- Integrated crosshair synchronization in all chart components (`ChartContainer`, `IPChart`, `ProtocolChart`, `SpectrumStatsChart`)
- Each chart registers/unregisters with the store on mount/destroy
- Local hover takes precedence over external hover for better UX
- Tooltips only display on the actively hovered chart, while crosshairs mirror across all registered charts

**Architecture:**
The implementation uses a pub-sub pattern where the hovered chart notifies the store, which triggers redraws on all other charts. Charts query the store during their draw cycle to determine if they should display an external crosshair.

<h3>Confidence Score: 5/5</h3>


- This PR is safe to merge with minimal risk
- Implementation is clean, well-architected, and follows established patterns. Proper lifecycle management with register/unregister prevents memory leaks. The singleton store pattern is appropriate for coordinating global UI state. No logic errors or security issues identified.
- No files require special attention

<h3>Important Files Changed</h3>




| Filename | Overview |
|----------|----------|
| netflow-webapp/src/lib/stores/crosshair.ts | New singleton store for synchronized crosshair state management across charts |
| netflow-webapp/src/lib/components/charts/crosshair-plugin.ts | Added sync callbacks and external label support for cross-chart synchronization |
| netflow-webapp/src/lib/components/charts/ChartContainer.svelte | Integrated crosshair store with registration/unregistration and sync callbacks |
| netflow-webapp/src/lib/components/charts/IPChart.svelte | Added crosshair plugin with sync support and proper chart lifecycle management |
| netflow-webapp/src/lib/components/charts/ProtocolChart.svelte | Added crosshair plugin with sync support and proper chart lifecycle management |
| netflow-webapp/src/lib/components/charts/SpectrumStatsChart.svelte | Added crosshair plugin with sync support for scatter chart type |

</details>



<h3>Sequence Diagram</h3>

```mermaid
sequenceDiagram
    participant User
    participant ChartA as Chart A (e.g., IPChart)
    participant Plugin as Crosshair Plugin
    participant Store as Crosshair Store
    participant ChartB as Chart B (e.g., ProtocolChart)
    
    Note over ChartA,ChartB: Initialization Phase
    ChartA->>Store: register('ip', chartInstance)
    ChartB->>Store: register('protocol', chartInstance)
    
    Note over ChartA,ChartB: User Hovers Over Chart A
    User->>ChartA: mousemove event
    ChartA->>Plugin: afterEvent (mousemove)
    Plugin->>Plugin: calculateHoveredDate(mouseX)
    Plugin->>Store: setHover(label, 'ip')
    Store->>Store: _hoveredLabel = label<br/>_sourceChartId = 'ip'
    Store->>ChartB: chart.draw()
    
    Note over ChartB: Chart B Redraw
    ChartB->>Plugin: afterDraw
    Plugin->>Store: getExternalLabel('protocol')
    Store-->>Plugin: return label (sourceId != 'protocol')
    Plugin->>Plugin: Find label index<br/>Convert to pixel position
    Plugin->>ChartB: Draw crosshair at synchronized position
    
    Note over ChartA: Chart A Continues
    Plugin->>ChartA: afterDraw
    Plugin->>Store: getExternalLabel('ip')
    Store-->>Plugin: return null (sourceId == 'ip')
    Plugin->>ChartA: Draw crosshair at local mouse position<br/>Show tooltip after delay
    
    Note over ChartA,ChartB: User Leaves Chart A
    User->>ChartA: mouseout event
    ChartA->>Plugin: afterEvent (mouseout)
    Plugin->>Store: setHover(null, 'ip')
    Store->>Store: _hoveredLabel = null<br/>_sourceChartId = null
    Store->>ChartB: chart.draw()
    ChartB->>Plugin: afterDraw
    Plugin->>Store: getExternalLabel('protocol')
    Store-->>Plugin: return null
    Plugin->>ChartB: No crosshair drawn
    
    Note over ChartA,ChartB: Cleanup Phase
    ChartA->>Store: unregister('ip')
    ChartB->>Store: unregister('protocol')
```

<!-- greptile_other_comments_section -->

<!-- /greptile_comment -->
47b81aae7foliver committed on 1/21/2026, 7:31:23 PM· committed by GitHubparents1ae89b4d2bd6a1
6 files changedLine totals unavailable