From ce3684314709ba7ada080f9179e1d3eb470fedea Mon Sep 17 00:00:00 2001 From: Thomas Teisberg Date: Fri, 15 May 2026 16:36:49 -0700 Subject: [PATCH] Add polygon region KDE histogram to viewer Draw a polygon on the map (custom lightweight tool) to get a seaborn-kdeplot-style panel: one unit-area Gaussian-KDE curve per enabled season plus a bold white "All" curve, rendered with uPlot. Point-in-polygon is tested in the map's projected CRS and the in-region trace indices are cached so variable/season changes only re-pull values. A fixed custom legend sits to the right (uPlot's live legend reflowed and shifted the canvas under the cursor) and highlights the entry matching the hovered line. Co-Authored-By: Claude Opus 4.7 (1M context) --- claude_plans/20260515-region-histogram.md | 43 ++++++ web/index.html | 114 ++++++++++++++ web/package-lock.json | 7 + web/package.json | 1 + web/src/histogram.ts | 180 ++++++++++++++++++++++ web/src/main.ts | 92 +++++++++++ web/src/map.ts | 141 +++++++++++++++++ 7 files changed, 578 insertions(+) create mode 100644 claude_plans/20260515-region-histogram.md create mode 100644 web/src/histogram.ts diff --git a/claude_plans/20260515-region-histogram.md b/claude_plans/20260515-region-histogram.md new file mode 100644 index 0000000..9703bb6 --- /dev/null +++ b/claude_plans/20260515-region-histogram.md @@ -0,0 +1,43 @@ +# Region polygon → per-season KDE histogram (viewer) + +Status: done (2026-05-15) — implemented; custom right-side legend with +hover focus-highlight (uPlot's live legend reflowed and was dropped). + +## Goal +Let the user draw a polygon on the map; show a seaborn-`kdeplot`-style panel of +the selected variable's distribution — one unit-area Gaussian-KDE curve per +enabled season plus a bold "All" curve. Polygon persists across variable +changes (only the curves recompute); honors the season on/off checkboxes. + +## Decisions +- Draw tool: custom lightweight (no plugin). +- Chart: `uplot` (new dep) + its CSS. +- Curves: Gaussian KDE, Silverman bandwidth, normalized to unit area. +- Season filter: chart respects the existing season checkboxes. + +## Pieces +1. **map.ts** — polygon draw + point-in-polygon + - State: drawing flag, vertex latlngs, provisional polyline, final polygon. + - `startPolygonDraw()`, `clearPolygon()`, `hasPolygon()`, `onPolygonChange(cb)`. + - Click adds vertex; dblclick / Enter finishes; Esc cancels. Suppress + doubleClickZoom + hover tooltip while drawing. + - `tracesInPolygon(data)` → indices: project vertices + each trace via + `map.options.crs.project` (zoom-independent, pole-correct), ray-cast. + - Clear polygon on hemisphere rebuild / destroyMap. +2. **histogram.ts** — KDE + uPlot + - `kde(values, gridX)` Gaussian, bw = 1.06·σ·n^(-1/5) (skip n<2 / σ=0), + trapezoidal renorm to area 1. + - Categorical season→color map (stable, chroma brewer), "All" = bold light. + - uPlot line chart in a floating panel; title = variable label/unit. +3. **main.ts** — wiring + - Sidebar "Draw region" / "Clear region" buttons + `#region-panel`. + - onPolygonChange → cache in-polygon indices → updateRegionHistogram(). + - updateRegionHistogram(): group cached∩qcPass∩!NaN indices by + frameCollection, keep enabled seasons; x-range = 2–98 pct of union; + KDE per season + All; render; show panel. + - Recompute on variable change (after ensureVariable) and season toggle; + polygon/index cache untouched. Clear on dataset switch. + +## Notes +- Panel: bottom-left, above Leaflet scale control, dark-theme styled. +- Seasons with <2 in-polygon points: no own curve but counted in "All". diff --git a/web/index.html b/web/index.html index edc2599..b4f7fce 100644 --- a/web/index.html +++ b/web/index.html @@ -240,6 +240,107 @@ .leaflet-container { background: #1a1a2e; } + + .region-buttons { + display: flex; + gap: 6px; + } + + .region-buttons button { + flex: 1; + background: #0f3460; + color: #e6e6e6; + border: 1px solid #16527a; + border-radius: 4px; + padding: 6px 8px; + font-size: 12px; + cursor: pointer; + } + + .region-buttons button:hover { + background: #16527a; + } + + .region-buttons button:disabled { + opacity: 0.45; + cursor: default; + } + + #region-panel { + position: absolute; + bottom: 30px; + left: 10px; + z-index: 999; + background: rgba(22, 33, 62, 0.94); + border: 1px solid #0f3460; + border-radius: 4px; + padding: 8px; + } + + #region-panel[hidden] { + display: none; + } + + #region-body { + display: flex; + align-items: flex-start; + } + + #region-chart { + width: 460px; + } + + #region-legend { + width: 150px; + margin-left: 8px; + padding-top: 22px; + font-size: 11px; + color: #ccc; + } + + #region-legend .leg-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + line-height: 1.2; + padding: 1px 4px; + border-radius: 3px; + } + + #region-legend .leg-swatch { + flex: 0 0 auto; + width: 16px; + height: 0; + border-top-style: solid; + } + + #region-legend .leg-row.leg-all { + font-weight: 700; + color: #fff; + } + + #region-legend .leg-row.leg-hi { + background: rgba(255, 255, 255, 0.16); + color: #fff; + } + + #region-chart .region-empty { + color: #aaa; + font-size: 12px; + padding: 24px 12px; + text-align: center; + } + + .u-title { + font-size: 12px; + color: #ccc; + } + + .u-legend { + font-size: 11px; + color: #ccc; + } @@ -265,6 +366,13 @@

Variable

+