feat(webapp): add drag-range drilldown sync and polish chart labels (#32)

<!-- greptile_comment -->

<h3>Greptile Summary</h3>

This PR adds drag-range drilldown to all four chart components (`ChartContainer`, `IPChart`, `ProtocolChart`, `SpectrumStatsChart`), allowing users to click-and-drag across chart x-axes to select a time range and drill into it with adaptive granularity. A new `rangeSelectionStore` synchronizes the selection highlight across charts in real-time. The PR also polishes chart legend labels (shorter metric names, reordered to `router · metric`), adds a skeleton loader for the Spectrum router selector, renames UI headings, and adds new shared utilities (`groupByBucketDurationMs`, `chooseAdaptiveGranularity`, `MIN_DRAG_PIXELS`) in `chart-utils.ts`.

- **Drag-range drilldown**: Mouse handlers on each chart's wrapper div track drag start/end positions, resolve them to label indices, compute an adaptive granularity based on the selected duration, and trigger a drilldown navigation.
- **Cross-chart mirroring**: During a drag, the selected label range is published to `rangeSelectionStore`, and other charts render a translucent overlay highlighting the same time range on their own axes.
- **Bug in SpectrumStatsChart**: The chart update path does not refresh `chart.data.labels` after data reloads, causing the drag-range functions to use stale labels and potentially navigate to incorrect time ranges.
- **Label polish**: Chart legends now show `router · metric` instead of `metric – router`, and metric labels are shortened (e.g., "IPv4" instead of "Unique Protocols (IPv4)").

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

- Mostly safe to merge, but SpectrumStatsChart has a stale-labels bug that will cause incorrect drilldown targets after data reloads.
- The core drag-range mechanism is well-implemented across ChartContainer, IPChart, and ProtocolChart. However, SpectrumStatsChart's chart update path omits the `chart.data.labels` update, which all drag-range functions depend on. This will silently produce wrong navigation after any data reload on that chart. The rest of the changes (label polish, skeleton loader, UI text) are low-risk.
- SpectrumStatsChart.svelte — its chart update path at line 663 does not refresh `chart.data.labels`, causing stale label lookups in the new drag-range code.

<h3>Important Files Changed</h3>




| Filename | Overview |
|----------|----------|
| netflow-webapp/src/lib/components/charts/ChartContainer.svelte | Adds drag-range selection with mirrored highlighting via rangeSelectionStore, adaptive granularity drilldown, and click suppression after drag. Implementation is correct; the chart-container's `position: relative` style ensures overlay positioning works. |
| netflow-webapp/src/lib/components/charts/IPChart.svelte | Adds drag-range selection logic (duplicated from ChartContainer), shorter chart legend labels (`router · metric`), and click suppression. Labels are updated on re-render so range selection stays accurate. |
| netflow-webapp/src/lib/components/charts/ProtocolChart.svelte | Adds drag-range selection logic (duplicated from ChartContainer), shorter metric labels (IPv4/IPv6), and click suppression. Labels are updated on re-render so range selection stays accurate. |
| netflow-webapp/src/lib/components/charts/SpectrumStatsChart.svelte | Adds drag-range selection and skeleton loader for routers. Bug: chart update path does not update `chart.data.labels`, causing stale labels after data reloads—drag-range drilldown will navigate to wrong time ranges. |
| netflow-webapp/src/lib/components/charts/chart-utils.ts | Adds `MIN_DRAG_PIXELS`, `groupByBucketDurationMs()`, and `chooseAdaptiveGranularity()` utility functions. Logic is sound with well-calibrated cutoff thresholds. |
| netflow-webapp/src/lib/stores/rangeSelection.ts | New Svelte writable store for cross-chart drag-range synchronization. Clean and minimal interface. |
| netflow-webapp/src/lib/components/filters/PrimaryFilters.svelte | Renames heading to "Global Controls" and adds drag-range hint to navigation instructions. No logic changes. |
| netflow-webapp/src/lib/components/netflow/NetflowDashboard.svelte | Renames heading from "NetFlow Visualization" to "Traffic Overview". No logic changes. |

</details>



<h3>Sequence Diagram</h3>

```mermaid
sequenceDiagram
    participant User
    participant SourceChart as Source Chart (mousedown/move/up)
    participant Store as rangeSelectionStore
    participant OtherCharts as Other Charts (mirrored overlay)

    User->>SourceChart: mousedown inside chart area
    SourceChart->>SourceChart: isDraggingRange = true, record dragStartX
    SourceChart->>SourceChart: indexFromPixelX(dragStartX) → startIndex
    SourceChart->>Store: set({ sourceChartId, startLabel, endLabel })
    Store->>OtherCharts: subscribe → mirroredRange updated
    OtherCharts->>OtherCharts: getMirroredSelectionStyle() → render overlay

    User->>SourceChart: mousemove (dragging)
    SourceChart->>SourceChart: update dragCurrentX, resolve indices
    SourceChart->>Store: set({ sourceChartId, startLabel, endLabel })
    Store->>OtherCharts: subscribe → update mirrored overlay

    User->>SourceChart: mouseup (finish drag)
    SourceChart->>SourceChart: finishRangeSelection()
    SourceChart->>SourceChart: chooseAdaptiveGranularity(rangeMs)
    SourceChart->>SourceChart: applyRangeDrilldown → emit drilldown event
    SourceChart->>Store: set(null) — clear selection
    Store->>OtherCharts: subscribe → remove mirrored overlay
```

<sub>Last reviewed commit: 02fdc51</sub>

<!-- greptile_other_comments_section -->

<!-- /greptile_comment -->
2d86460fa9oliver committed on 2/18/2026, 9:43:21 AM· committed by GitHubparents4d64ad17cc706f
10 files changedLine totals unavailable