Skip to content

Sync development to production (2026-11-06) - #604

Merged
sandesh-sp merged 48 commits into
productionfrom
development
Nov 6, 2026
Merged

sandesh-sp merged 48 commits into
productionfrom
development

Conversation

@BhattaraiSijan

@BhattaraiSijan BhattaraiSijan commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Promotes development to production. Everything merged since the last sync (#572, 2026-10-15): seventeen PRs, 51 files, under src/, tests/, configure/src and docs/, plus a devDependency bump. No deploy, workflow or environment files change.

What changed

PR Change Visible to users
#507 Deck.gl hover highlight on vector tile layers: only the feature's stroke is highlighted, at the layer's pick width, with a new optional style.hoverHighlightWidth Hovered polygons no longer tint their fill
#579 FetchTimeseries: a per-layer series Cadence (hour, day, month, year). The range card's Start and End pickers step at it; a picked day runs 00:00:00 to 23:59:59 UTC, a month its first to last day Pickers match the series' resolution once configured
#580 SeriesChart: the CSV download is named after the card's title, section heading and variable, so one site's file is told from another's Download file names
#581 SeriesChart: the tooltip title and the zoom window's dates print at the series' cadence (Feb 28, 2026 daily, Feb 28, 2026, 14:00 hourly, Feb 2026 monthly) Tooltip and window dates
#587, #611 FeaturePopup: a click that opens a card marks the clicked point with an upright pin in the theme primary, which stays after the card closes until the next click that opens no card. Polygon and line clicks open their card but draw nothing Pin on clicked points on every Feature Popup layer
#617 FeaturePopup: the card and pin anchor to the clicked point's own position (nearest member of a multipoint) instead of the click, re-anchor to the finer tile position as the map zooms in, or anchor to exact lat/lng properties when a layer names them. Adds map:queryRenderedFeatures, map:movePopup and layer:viewportLoaded to the event bus Card and pin stay on the point while zooming
#591 Compare dates: each side draws a layer by its coverage at its own date; a layer hidden on a side is left out of that side Comparison correctness
#592 Compare: choosing side-by-side before a date gets a divider and an Exit Comparison UI
#593 CI: @playwright/test 1.57 to 1.64.1 so Chromium installs on Ubuntu 26.04 runners (devDependency only) None
#603, #619, #620 LayerFilter: a catalogue activation with an egis_url shows a "View GIS Services" link, opened in a new tab, beside its date range; the link keeps the theme primary color after it is clicked Link on the picked activation once configured
#622 LayerFilter: an ongoing event shows a compact "Active" status (pulsing dot) grouped with its dates, in place of the ACTIVE pill that broke mid-word Event theme status
#623 LayerFilter: multi-select menus (e.g. Air4US Pollutant) use the single-select rows: an "All …" row, ticked selected values, and the menu stays open between picks Multi-select filter menus
#606 LayerManager: layer info popover text at the 2xs font size Larger layer descriptions
#610 MapControl: the measure line is solid theme primary over a white casing, with a tighter distance label; deck.gl basemaps are created with antialiasing Measure tool look; smoother deck.gl lines

Config changes required after this is live

The new keys are read by the new build only, and the Configure modal on the current production build drops any key its metaconfig does not list. So make the Configure edits in steps 1 and 3 after this deploys.

  1. Air4US: set Cadence to day on the five AQS timeseries layers: PM₂.₅, O₃, CO, NO₂, SO₂ (Layers → layer → Timeseries → Cadence). Without it the pickers step hourly against daily series and the chart tooltip falls back to date-only. Leave PM₁₀ and the two HMS layers alone; their timeseries blocks have no URL.

  2. Disasters: add egis_url to the seven catalogue activations in the LayerFilter tool's Catalogue (GeoJSON) field, one per entry's properties:

    • 202501_Fire_CA → https://gis.earthdata.nasa.gov/portal/home/group.html?id=c3a8e74d6c374e4db0e4f9185f44d876#content
    • 202409_Hurricane_Helene → https://gis.earthdata.nasa.gov/portal/home/group.html?sortField=added&sortOrder=desc&id=c59e5358c422416cbad77c736524b1fc#content
    • 202410_Hurricane_Milton → https://gis.earthdata.nasa.gov/portal/home/group.html?id=2267e598c75e41629ec137f388d72a37#content
    • 202507_Flood_TX → https://gis.earthdata.nasa.gov/portal/home/group.html?sortField=title&sortOrder=asc&id=f514ef4d04264021b95f899323e19b85#content
    • 202601_WinterWx_US → https://gis.earthdata.nasa.gov/portal/home/group.html?id=461a5be76a2c4e6db6ec470657a6f9d6#content
    • 202604_Typhoon_Sinlaku → https://gis.earthdata.nasa.gov/portal/home/group.html?sortField=added&sortOrder=desc&id=6ed41cdc783d4c68b08b66d923ac1386#content
    • 202606_Earthquake_Venezuela → https://gis.earthdata.nasa.gov/portal/home/group.html?sortField=added&sortOrder=desc&id=58996c58a2714887adf8f5cee0900619#content

    This is a targeted edit of the catalogue JSON, not a whole-config import. The field is free JSON, so the key survives the old build and can be added before or after deploy; the link appears once the new build is live.

  3. Optional:

    • style.hoverHighlightWidth on vector tile layers with Hover Highlight on (Air4US HMS Fire Detection, HMS Smoke Plume). Empty keeps the styled width.
    • Latitude Property / Longitude Property (variables.featurePopup.latitudeProperty / longitudeProperty) on point layers whose features carry their exact coordinates as properties. Empty keeps the geometry anchor with zoom re-anchoring.

No config is needed for #580, #581, #587, #591, #592, #593, #606, #610, #611, #619, #620, #622 or #623.

slesaad and others added 26 commits September 29, 2026 14:12
A hovered polygon keeps its fill: the fill sub-layer gets a zero-alpha
highlight, so only outlines and lines take the highlight colour.

Add a Hover Highlight Stroke Width option. When set, the outline and line
sub-layers are drawn by HighlightWidthPathLayer, a PathLayer whose vertex
shader widens the highlighted path to that many pixels.
…ghlight

geometry.pickingColor holds raw byte values in DECKGL_FILTER_SIZE; the
picking module normalizes it only later, in DECKGL_FILTER_COLOR, so the
highlight test never matched and the hovered stroke kept its width.
Replace HighlightWidthPathLayer with HighlightLineWidthExtension, set on
the MVTLayer itself. Extensions on a composite layer pass down to every
sub-layer alongside the ClipExtension MVTLayer adds per tile; the
extension injects its shader only into PathLayers, so it no longer
depends on GeoJsonLayer's sub-layer ids.
…579)

## Summary

A layer's time series does not always run at the layer's own cadence. On
Air4US every AQS layer declares `time.interval: PT1H` for its hourly map
tables, while its time series is drawn from the daily tables. The range
card's pickers were hardcoded to the hour for everyone, so a daily
series offered a clock that meant nothing (the Air4US review asked for
the hour to go).

The timeseries block now declares its own **Cadence**: `hour` (the
default, today's behavior), `day`, `month` or `year`, authored in
Configure's Timeseries row next to Default Range. The card's Start and
End pickers step at it, using the Timeline DateSelector's matching mode.
A picked Start opens its step and a picked End closes it, so a day runs
`T00:00:00` to `T23:59:59`, a month its first to its last day, a year
Jan 1 to Dec 31; End never passes its ceiling (today). Hour mode is
unchanged: picks to the minute, `…:00` and `…:59`. An unreadable value
warns and keeps the hour picker, as an unreadable Default Range does.

This is the first of two slices. The second carries the cadence in the
chart-series payload so the chart's tooltip and zoom-window labels print
at the same precision, replacing the date-only tooltip from #569.

## What's in the diff

- `FetchTimeseries/lib/timeseries.ts`: `cadence?` on `TimeseriesConfig`;
`parseCadence`, `cadenceStart`, `cadenceEnd`, `DEFAULT_TIME_MODE`.
- `FetchTimeseries/FetchTimeseriesTool.tsx`: the mode is read where the
range is seeded for a layer and handed to the card.
- `FetchTimeseries/lib/components/RangeCard.tsx`: `timeMode` prop,
passed to both pickers; picks open and close their step.
- `configure/src/metaconfigs/layer-vector-config.json`,
`layer-vectortile-config.json`: a "Cadence" dropdown after "Default
Range".
- `FetchTimeseries/README.md`.
- Tests: `fetchTimeseries.spec.js` (`parseCadence`, step open and close
for the four modes, year end), `fetchTimeseriesTool.spec.js` (day
cadence shows days and runs picks over whole days; month cadence runs
1st to last and caps at today; unreadable cadence warns and keeps the
hour picker).

## Testing

- `npm run test:unit` green (210 files, 3600 tests) and `tsc --noEmit`
clean.
- Manual, on the Air4US mission: in Configure, open the PM2.5 layer's
Time tab and set Cadence to "day" in the Timeseries row; save. On the
map, click a site and press the time series action. Expect Start and End
as days only (`Sep 25, 2025`, no clock). Pick a Start day: the request
filter reads `datetime >= '<day>T00:00:00'`. Pick an End day: `datetime
<= '<day>T23:59:59'`. Set Cadence to "month": the pickers show
`September 2026`, and End picked as the current month stops at today.

## Not in this PR

- Slice 2: the payload carries the cadence and the chart follows it.
- The seeded range is not aligned to the cadence: a month cadence seeds
Start at `2025-09-25`, shown as `Sep 2025`, until the first pick.
- Hour mode still picks to the minute; whether an hourly series should
snap to the hour is open.
- The mission config: the five AQS layers need `cadence: day`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
)

## Summary

From the Air4US review of the station time series: every site's CSV
download was named after the variable alone (`24-HR_BLK_AVG.csv`), so
two sites' files could not be told apart and the pollutant was nowhere
in the name. The file is now named after the card: title, section
heading and variable,
`Washington_Georgia_Site_1_AQS_Surface_PM2.5_Concentration_24-HR_BLK_AVG.csv`.
Long, but every part is what the card shows, and on the AQS layers the
pollutant only exists in the heading (the variable is the sample
duration).

Each part is reduced to word characters, dots and dashes. Names are
NFKD-normalized first, so `PM₂.₅` survives as `PM2.5` rather than
`PM_._`. A missing part is skipped, and so is a part that repeats the
one before it: an ungrouped series is labelled with the layer name,
which is also the heading, and the file should not say it twice.

## What's in the diff

- `SeriesChart/lib/chartData.ts`: `csvFileName(parts)`.
- `SeriesChart/lib/components/SeriesChartPanel.tsx`: the card builds the
name from `payload.title`, `payload.subtitle` and the picked series'
label and hands it to the footer; `downloadCsv` takes the name.
- `SeriesChart/README.md`.
- Tests: `seriesChartData.spec.js` (the three parts with subscripts,
missing and repeated parts, accents, fallback).

## Testing

- `npm run test:unit` green (210 files, 3600 tests) and `tsc --noEmit`
clean.
- Manual, on the Air4US mission with
`Air4US_v182_timeseries-title.config.json`: click a PM2.5 site in
Washington County, GA, press the time series action, Download CSV.
Expect
`Washington_Georgia_Site_1_AQS_Surface_PM2.5_Concentration_24-HR_BLK_AVG.csv`
(or `…_1_HOUR.csv` for that variable).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
… date

A side pinned to a date clones the live deck layer and overrides only its
source, so it inherited the primary map's visibility. A layer with no data
at the global instant is hidden on the primary map, and its clone stayed
hidden on a side pinned to a date it does have data for: Landsat at
Jan 11, 2025 against Jan 6.

The pinned patch now carries the layer's visibility at the pinned date, by
the rule TimeControl applies at the global instant: on, and not out of
coverage. Layers the coverage gate does not reach keep the live layer's
visibility.
deck.gl stops drawing a layer marked visible: false but still runs its
lifecycle, so a hidden tile layer handed to a comparison side fetched a
viewport of tiles it never drew. Each side now leaves hidden clones out,
as the primary map's sync already does.
A side-by-side pane's map is built with its style still loading, and
mapbox-gl throws from getStyle() until it lands. Entering side-by-side
straight away threw out of MapComparison.enable before the divider,
captions and enabled flag went up, so Exit returned early and the map
stayed split until reload.

- DeckGLAdapter: _anchorBelowLabels sends a still-loading map's layers
  unanchored; the pane's style.load re-sends them under the labels.
- MapComparison: enable raises the divider, captions and enabled flag
  even when the engine throws; disable also tears down a split the
  engine reports as on; setLayout re-marks the divider either way.

Closes #589
…unners

ubuntu-latest moved from Ubuntu 24.04 to 26.04 on 2026-10-26. Playwright
1.57.0 has no Chromium build for 26.04, so every e2e job has failed at
"Install Playwright Browsers" since. Support arrived in Playwright 1.61.
…nners (#593)

## Line breakdown

| Category | Lines added | % |
|---|---|---|
| Generated (`package-lock.json`) | 26 | 96.3% |
| Config/build (`package.json`) | 1 | 3.7% |

This PR upgrades Playwright from 1.57.0 to 1.64.1, because GitHub moved
`ubuntu-latest` to Ubuntu 26.04 on 2026-10-26 and 1.57.0 has no Chromium
build for it, so every e2e job has failed at "Install Playwright
Browsers" since then.

## What this does

- **E2e jobs run again:** on `ubuntu-latest`, `npx playwright install
--with-deps chromium` succeeds and the smoke, eventbus-integration and
deployment-mode specs run against Chromium.
- **Playwright is 1.64.1 everywhere:** `@playwright/test`, `playwright`
and `playwright-core` are all pinned to 1.64.1 in the lockfile, so `npx
playwright --version` reports 1.64.1 after `npm ci`.
- **Node 20 is now the floor:** Playwright's engine requirement moves
from >=18 to >=20, which CI, the Dockerfile (`node:20-slim`) and
`.nvmrc` (20.11.1) already meet.
- **Nothing else changes:** the workflow file, `playwright.config.js`
and all test code are untouched.

## Decisions to review

- **Bump Playwright instead of pinning the runner to `ubuntu-24.04`:**
this ships a new Chromium build and seven minor releases of Playwright
changes, so an e2e failure after merge could come from the upgrade
rather than the app. Leaving the job on `ubuntu-latest` also means the
next runner image change can break CI the same way.
#591)

## Summary

Fixes #588. Comparing Jan 11, 2025 against Jan 6, 2025 on the Disasters
mission, Landsat Natural Color was blank on the Jan 6 side even though
Jan 6 has data. Its Jan 6 tiles were fetched successfully but never
drawn. Swapping the dates did the same to AVIRIS-3.

A comparison side pinned to a date clones the live deck layer and
overrides only its source. It therefore inherited the primary map's
visibility, and the primary map hides a time-enabled layer at an instant
it has no data for. The pinned side now decides visibility from the
layer's coverage at its own date, by the same rule TimeControl applies
to the primary map: on, and not out of coverage. Layers the coverage
gate never hides (`dynamicExtent`, controlled) keep the live layer's
visibility. Untimed layers and layers whose source cannot be pinned are
untouched.

The second commit stops each side from handing deck.gl the layers it
hides. deck.gl does not draw a `visible: false` layer, but it still runs
the layer's lifecycle. Before this commit a hidden tile layer on a side
fetched a viewport of tiles it never drew: about 20 per hidden layer
every time a comparison was set up. The primary map's sync already
leaves hidden layers out, and the sides now do the same.

## What's in the diff

- `Map_/comparisonTimePins.ts`: each pinned patch carries `visible`,
from `L_.layers.on` and `evaluateLayerDataCoverage` on the pinned
config, gated by `isCoverageGated`.
- `MapEngines/Adapters/DeckGLAdapter.ts`: `_comparisonClonesFor` drops
clones that are `visible: false` on their side (swipe canvases and
side-by-side panes).
- Tests:
- `comparisonTimePins.spec.js`: in coverage at the pin, out of coverage
at the pin, no declared coverage, switched off, not reached by the gate.
- `deckGLComparisonProps.spec.js`: a hidden layer is left out of its
swipe side and its side-by-side pane.

## Testing

- `npm run test:unit` green (210 files, 3606 tests) and `tsc --noEmit`
clean for the changed files.
- Manual, on the Disasters mission over Los Angeles with Landsat Natural
Color and AVIRIS-3 False Color Early Look RGB on:
1. Set the timeline to Jan 11, 2025, open Compare date and pick Jan 6,
2025.
2. Expect Landsat imagery on the Jan 6 side and AVIRIS-3 on the Jan 11
side.
  3. Swap the sides: each layer follows its date.
4. Since the comparison started, the only tile requests are AVIRIS for
Jan 11 and Landsat for Jan 6. There are none for AVIRIS on Jan 6 or
Landsat on Jan 11.

## Not in this PR

- #589: choosing side-by-side before a date leaves the map split with no
divider, and Exit can't clear it.
- #590: the panel keeps showing a comparison after something else ends
it.
- 503s from the tile server under comparison load.
- A time-enabled layer that has never been drawn on the main map may be
missing from both sides (noted in #588).
…#592)

Closes #589

Choosing side-by-side before a date left the map split with no divider,
and Exit couldn't clear it. The new panes' styles were still loading, so
`getStyle()` threw and `MapComparison.enable` stopped before the divider
went up.

- `DeckGLAdapter`: a map whose style is still loading gets its layers
unanchored; they're placed under the labels once the style loads.
- `MapComparison`: the divider and the enabled state go up even if the
engine throws, and Exit also clears a split the engine still has on.

Unit tests cover both. To check by hand: Compare date → side-by-side →
pick Date 2, then Exit.
…fills

- Skip the hover line width in the picking pass so the hover target
  matches the styled stroke and neighbouring features stay hoverable.
- Give the polygon fill a one-unit-alpha highlight instead of zero, which
  kept luma's highlight blend from dividing 0 by 0 on transparent fills.
- Keep the fill tint for polygons whose outline would not show on hover
  (weight or opacity 0), so they still give hover feedback.
…507)

## Purpose

With hover highlight on, a hovered vector tile polygon tinted fill and
outline together, so a highlight strong enough to see on the outline
also washed over the whole shape. This keeps the fill as styled and
highlights only the stroke, and adds an option for the hovered stroke's
width.

## Proposed Changes

- [CHANGE] The highlight colour applies to outlines and lines only. Each
tile's GeoJsonLayer draws a polygon's fill and outline as separate
sub-layers; the `polygons-fill` sub-layer gets a zero-alpha
`highlightColor` through `_subLayerProps`, which deck.gl blends in as
nothing.
- [ADD] **Hover Highlight Stroke Width** (`style.hoverHighlightWidth`),
a deck.gl-only field on vector tile layers. When set, the MVTLayer gets
`HighlightLineWidthExtension` and a `highlightLineWidth` prop.
- [ADD] `HighlightLineWidthExtension`, a deck.gl `LayerExtension` that
adds a `DECKGL_FILTER_SIZE` shader hook to PathLayers: a vertex whose
picking colour marks it as highlighted (`isVertexHighlighted`, the same
test the colour highlight uses) is drawn at the configured pixel width.
It follows the hover without rebuilding the layer or regenerating width
attributes.
- [ADD] Tests for the untinted fill, the extension and width being set,
and widths that should be ignored (empty, text, 0, negative).

## Issues

- Closes #508

## Testing

- `npx vitest run`: 187 files, 3113 tests pass.
- Live, on this branch's dev build, AirQuality mission, HMS Smoke Plume
Polygon on Aug 20, 2026, deck.gl engine: every tile's sub-layers carry
`HighlightLineWidthExtension` alongside MVTLayer's `ClipExtension`.
Hovering a plume with a 12px opaque highlight draws a thick outline in
the highlight colour, and the plume's fill stays as styled.

## Notes for review

- **Set on the MVTLayer, not through `_subLayerProps`.** Extensions on a
composite layer pass down to every sub-layer, and MVTLayer appends its
per-tile `ClipExtension` to them. Setting `extensions` through
`_subLayerProps` instead would replace the ones the parent hands down,
dropping that clipping. The extension returns no shader for non-path
layers, so fills and points compile unchanged.
- **The picking colour is normalized in the hook.**
`geometry.pickingColor` holds raw byte values at `DECKGL_FILTER_SIZE`;
the picking module normalizes it only later, in `DECKGL_FILTER_COLOR`.
Without normalizing, the highlight test never matches.
- **Points are unchanged.** `ScatterplotLayer` draws a point's fill and
stroke in one sub-layer, so a hovered point still tints whole and keeps
its styled stroke width.
- **The default highlight is faint on strokes.** With no colour
configured, the highlight is black at 10% opacity, which was chosen for
fills. On a 1–2px outline it barely shows. Missions using hover
highlight will likely want to set a colour.
## Summary

Second half of the series cadence (#579). That PR declares the cadence
in the layer's timeseries block and makes the range pickers step at it.
The chart still printed every time as a date only, hardcoded in #569 for
the daily Air4US series, so an hourly series lost its clock and a
monthly one showed a day that meant nothing.

The cadence now travels on the chart-series payload, and the chart
prints the tooltip title and the zoom window's end dates at that
precision: `Feb 28, 2026, 14:00` for hour, `Feb 28, 2026` for day, `Feb
2026` for month, `2026` for year. A payload without one prints the date,
today's behavior. Axis ticks are untouched.

FetchTimeseries sends `cadence` only when the layer declares a readable
one. An unset layer keeps both of today's defaults, hourly pickers and a
date-only chart, rather than switching the chart to clocks.

## What's in the diff

- `_shared/types/chartSeries.ts`: `ChartCadence` and `cadence?` on the
payload; the guard rejects a value outside the four words, like any
other malformed payload.
- `FetchTimeseries/lib/timeseries.ts`: `buildPayload` copies the parsed
config cadence onto the payload, lowercased, when there is one.
- `SeriesChart/lib/chartData.ts`: `formatTooltipTime(ms, cadence)` over
one formatter per cadence; `buildChart` and `buildChartOption` take the
cadence and hand it to the tooltip and the window labels.
- `SeriesChart/lib/components/SeriesChartPanel.tsx`: the canvas receives
`payload.cadence`.
- READMEs for both plugins.
- Tests: `chartSeries.spec.js` (accepted word, rejected word),
`seriesChartData.spec.js` (each cadence's format, the hourly tooltip
title, window dates by month and by hour), `fetchTimeseries.spec.js`
(declared cadence rides, unreadable or absent is not sent),
`fetchTimeseriesTool.spec.js` (the emitted payload carries it, and
nothing when unset).

## Merge order

Stacked on #579 and targets its branch; retarget to development once
#579 merges. Independent of the CSV name PR, though both touch
`chartData.ts` and the panel on nearby lines, so whichever lands second
gets a small rebase.

## Testing

- `npm run test:unit` green (210 files, 3602 tests) and `tsc --noEmit`
clean.
- Manual, on the Air4US mission: with the PM2.5 layer's Cadence set to
"day" (from #579), click a site and press the time series action. Expect
the tooltip titled `Feb 28, 2026` and date-only window labels, as today.
Set Cadence to "hour", save, reload, press again: the tooltip reads `Feb
28, 2026, 00:00` and the window labels carry the clock. Set it to
"month": `Feb 2026`.

## Not in this PR

- Axis ticks still pick their own format from the visible span.
- Other fetchers (the AOI statistics chart) send no cadence and keep the
date-only chart until they set one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What

In the Event theme of the LayerFilter panel, a picked activation now
shows an **EGIS services** link beside its date range. It opens the
activation's EGIS portal group in a new tab. Activations without a URL
render exactly as before.

## How

- Activations declare one optional flat property, `properties.egis_url`.
Presence alone decides display: no link array, no show/hide flag.
- `buildEntryDisplays` passes it through as `egisUrl`, kept only when it
is an `http(s)` string. Anything else (`javascript:`, numbers, empty) is
treated as absent.
- `FilterPanel` renders the anchor in the existing entry meta row after
the date range and the ACTIVE pill, `target="_blank" rel="noopener
noreferrer"`, with a masked open-in-new glyph. The row now also appears
for an entry that has a URL but no dates.
- Styles: dates take `margin-right: auto` and the row is
`justify-content: flex-end`, so ACTIVE and the link sit right-aligned
together. Theme tokens only.
- No parser or engine change: `parseCatalog` already carries unknown
properties through.
- Catalogue field description in `config.json` and SCHEMA.md §5 document
the property.

## Config

Six Disasters activations get an `egis_url` (CA Fire, Helene, Milton,
Winter Wx, Sinlaku, Venezuela). Texas Flood has no EGIS group. Hurricane
Isaias is not in the catalogue yet and needs its own entry before it can
carry its URL.

## Tests

- `__tests__/entryDisplay.spec.ts`: http(s) passthrough, absent → null,
rejected values.
- `__tests__/FilterPanel.spec.tsx`: anchor href/target/rel/text, no
anchor without a URL, meta row for URL without dates.
- Full unit suite: 3630 passed. `npm run typecheck` clean for
LayerFilter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What

On the deck.gl engine, a click that opens the FeaturePopup card now also
outlines the clicked feature on the map: a white casing under a
primary-color ring, no fill, click-transparent, drawn through the
existing `map:createLayer` handler the way AOI draws its selection.
Points get a ring around the marker.

The outline outlives the card. Pressing an action (Fetch timeseries,
Analyze) or the close button leaves it in place, so the feature the
chart or analysis is about stays marked. It moves on the next click that
opens a card and clears on a click that opens none (empty space, or a
layer without the popup opt-in). Teardown removes it.

Nothing in core changed. Leaflet is untouched (the plugin is inactive
there and the per-feature click path already restyles the clicked
shape).

## Why

With the card closed and the chart open there was no way to tell which
station the chart described.

## Known limit

Vector-tile features come back from the pick as a tile-grid copy, so the
outline drifts off the dot after zooming in and a polygon crossing a
tile edge is only outlined on the clicked tile's piece. Tracked
separately: the engine-side highlight that renders from the tiles on
screen.

## Tests

- New
`src/essence/Tools/FeaturePopup/__tests__/FeaturePopupTool.spec.js`:
casing-then-ring creation, replacement under the same ids, the three
removal paths, survival across dismiss and action, removal on destroy.
- Full unit suite green (211 files, 3605 tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
sandesh-sp and others added 2 commits November 4, 2026 10:16
## Summary
- Layer info popover body text (`.blocks-layer-legend__info-popover`)
uses `--theme-font-size-2xs` (0.88rem fallback) instead of
`--theme-font-size-3xs` (0.75rem), so layer descriptions are easier to
read.

## Notes
- The popover title and markdown headings are also 2xs, so the title is
set apart only by its semibold weight and ink color.

## Test plan
- [ ] Open the Layer Manager, click a layer's info button, and confirm
the description text renders at the larger size in each brand.
sandesh-sp and others added 4 commits November 5, 2026 14:17
The card was anchored to the click location, which sits anywhere inside
the rendered marker and drifts off the point as the map zooms in. A
point feature now anchors to its own geometry, a multipoint to the
member nearest the click; lines and polygons keep the click.

Vector tiles snap positions to a grid that coarsens at lower zooms, so
a point read zoomed out is still off once zoomed in. After each move,
and once the clicked layer has loaded every tile in view, the point is
looked up again in the tiles drawn now and the card and pin take its
finer position. A layer can instead name the properties holding each
feature's exact latitude and longitude (Latitude Property / Longitude
Property in Configure), which the card and pin anchor to directly.

Core additions on the event bus:
- map:queryRenderedFeatures: features drawn near a lat/lng, as plain
  GeoJSON
- map:movePopup: re-anchor the open card without reopening it
- layer:viewportLoaded: a deck.gl vector tile layer has every tile the
  view needs

The deck.gl adapter's queryRenderedFeatures now picks through the
overlay on a basemap, where it previously always returned nothing, and
takes a radius for point queries.
deck.gl fires onViewportLoad each time the drawn tile set changes and has
fully loaded, cached views included, so it alone covers every view worth
re-reading. Drop the map:moveend look, the settled flag and zoomRead: a
look now runs when the map is zoomed in past the point's zoom, and a
match sets that zoom.

Trim the comments, Configure tooltips and plugin description, drop a
try/catch in MapPopup_.move that cannot fire, and merge overlapping
re-anchor tests.
Follow-up to #603. The link beside a picked activation's dates read
"EGIS services"; it now reads "View GIS Services" in the panel, the
config description and the schema doc, and the test asserts the new
text.
The ACTIVE pill sat between the date range and the View GIS Services
link, and broke mid-word when all three shared the row. It is now a
dot-and-word status grouped with the dates, in the theme primary color,
that drops under the dates as one unit when space runs short.
Multi-select filters drop the checkbox rows for the same rows single select
uses: counts beside the label, every held value highlighted and ticked, and
an All row that clears the selection. Picking a value toggles it and keeps
the menu open.
#620)

Pins the View GIS Services link to the theme primary color in its
visited, active, hover and focus states so it no longer changes color
after being clicked.
…es (#622)

Closes #621

## What
Picking an ongoing event in the Event theme showed an ACTIVE pill wedged
between the date range and the View GIS Services link. When all three
shared the row, the pill broke mid-word ("ACTI / VE").

The pill is replaced with a compact status (a pulsing dot + "Active")
grouped with the dates on the left:
- Dates and status wrap together as one unit; the status never breaks
and drops under the dates when the row is short of room.
- The GIS link stays on the right, aligned with the dates line.
- Colors come from the theme primary tokens; the pulse is disabled under
`prefers-reduced-motion`.

## Testing
- `npx vitest run src/essence/Tools/LayerFilter` — 13 passed.
- Checked in the DisastersTool mission with an entry set to
`end_datetime: null` (active + EGIS link): status sits under the dates,
link level with the dates. An entry with a link only looks unchanged.
- Not checked at the panel's minimum width.

## Notes
- To reproduce, the picked entry needs both an `egis_url` and a
null/future `end_datetime`.
## What

Multi-select filter menus (e.g. Air4US Pollutant) now render like
single-select ones (e.g. Disasters Hazard) instead of native checkbox
rows.

- **Same rows.** Each value uses the single-select row: count badge
beside the label rather than pushed hard right.
- **Selection marking.** Every held value gets the selected background
and the tick; `aria-pressed` carries the state for assistive tech now
that the checkbox is gone.
- **All row.** An "All …" row heads the menu, ticked when nothing is
held; clicking it clears the selection.
- **Toggle in place.** Picking a value adds or removes it and keeps the
menu open for the next pick. Single select still closes on pick.

The unused `&__option` checkbox-row styles are removed.

## Testing

- Checked in the running app: Air4US Pollutant menu matches the
Disasters Hazard menu; picking CO then NO₂ kept the menu open, ticked
and highlighted both, and the trigger read "CO, NO₂ (2)".
- LayerFilter specs green (3 files, 13 tests).
## What

On the deck.gl engine, the FeaturePopup card and pin point at the
clicked feature's own position rather than wherever the click landed
inside the marker.

- **Point anchor.** A point anchors to its geometry, a multipoint to the
member nearest the click. Lines and polygons keep the click.
- **Re-anchoring on zoom.** Vector tiles snap positions to a grid that
coarsens at lower zooms. When the clicked layer has loaded the tiles for
a view zoomed in past the anchor's zoom, the point is looked up again
and the card and pin take its finer position.
- **Exact position from properties.** A layer can name the properties
holding each feature's exact latitude and longitude (Latitude Property /
Longitude Property in Configure). The card and pin anchor there, with no
re-anchoring.

Core additions on the event bus:
- `map:queryRenderedFeatures`: features drawn near a lat/lng, as plain
GeoJSON
- `map:movePopup`: re-anchor the open card without reopening it
- `layer:viewportLoaded`: a deck.gl vector tile layer's drawn tiles have
all loaded

The deck.gl adapter's `queryRenderedFeatures` picks through the overlay
on a basemap, where it used to return nothing, and takes a radius for
point queries.

## Why

The card was anchored to the click location, which sits anywhere inside
the rendered marker and drifts off the point as the map zooms in. A
point clicked zoomed out on a vector tile layer was also off by the
tile's snapping error.

## Notes

- `layer:viewportLoaded` fires each time the drawn tile set changes and
has fully loaded, cached views included, so it is the only trigger for
re-anchoring.
- `map:queryRenderedFeatures` reads `geometry.coordinates` out
explicitly: on a vector tile pick a JSON round-trip of the geometry
drops them.
- Latitude/Longitude Property are offered on plain vector layers too,
for parity, though GeoJSON geometry is already exact.

## Tests

- `FeaturePopupTool.spec.js`: point and multipoint anchors,
exact-property anchors, re-anchoring on `layer:viewportLoaded` (other
layers ignored, no look without a zoom gain, the radius measured from
the last found zoom), actions carrying the re-anchored position, the pin
moving alone once the card closes.
- `MapPopup_.spec.ts`, `deckGLAdapter.spec.js`,
`LeafletAdapter.spec.js`: `movePopup` and radius queries. 267 tests
across the four suites pass.
- Manual, Air4US AQS PM₂.₅ layer: clicked a station at zoom 5, zoomed to
12; the card and pin moved from (-109.56116, 40.46367) to (-109.56148,
40.46471), the station's recorded position.
@sandesh-sp sandesh-sp changed the title Sync development to production (2026-11-03) Sync development to production (2026-11-06) Nov 6, 2026
A time series split by groupBy can give each series its own display name and chart title. Buttons keep the raw value; legend, hover text, footer chip and CSV header read the label, and the card title follows the picked button. Code half of Air-Quality-Portal/aq-arch#212.
…ies (#624)

## What

A time series split by `groupBy` can now give each series its own
display name and its own chart title. The duration buttons keep their
raw values, the legend, hover text, footer chip and CSV header read the
configured label, and the card title switches with the picked button.

Code half of Air-Quality-Portal/aq-arch#212. The per-layer config (URL
filters, `yKey`, the `groups` rows) is applied separately in Configure.

## How

New optional `groups` on `variables.timeseries`, one row per `groupBy`
value:

```json
"groups": [
  { "value": "1 HOUR", "label": "First Daily Maximum", "title": "First Daily Maximum AQS Surface CO Concentration" }
]
```

- **Contract** (`_shared/types/chartSeries.ts`): `ChartSeries` gains
optional `displayLabel` and `title`. `label` is unchanged and still the
uniqueness key, so the guard is untouched.
- **FetchTimeseries** (`lib/timeseries.ts`): `buildPayload` matches each
series' key to a `groups` row (exact, then case-insensitive; malformed
rows ignored) and stamps `displayLabel`/`title`. The truncation note
rides on the series title too. When any row carries a title, the payload
`subtitle` becomes the feature title (site) instead of the layer name,
since the group title already names the layer.
- **SeriesChart**: header uses `picked.title ?? payload.title`; footer
chip, tooltip series name and CSV header use `displayLabel ?? label`;
buttons and dropdown keep `label`. CSV file name follows the same
fallbacks.
- **Configure**: `Series Labels` objectarray (`value`, `label`, `title`)
under the Timeseries row in both layer-vector and layer-vectortile
metaconfigs, so the key survives a save.
- Docs: FetchTimeseries README table + config example, SeriesChart
README contract note.

Title priority: 1) picked series' group title, 2) the layer's Title
template filled from the feature, 3) Title Property → name → title → id
→ layer name, as before. Layers without `groups` render exactly as today
(no new keys on the payload).

## Tests

- `tests/unit/fetchTimeseries.spec.js` (+5): matched row stamps both
fields and moves the site to the subtitle; no groups → unchanged shape;
label-only row; case-insensitive match + malformed rows ignored;
truncation note on the series title.
- `tests/unit/seriesChartAdapter.spec.js` (+2): buttons keep raw labels
while header and footer follow the pick; a series without a title falls
back to the payload title and its label.
- Full unit suite: 3650 passed. `tsc --noEmit` clean for the touched
files.

## Live check (after merge to a dev instance)

Add a `Series Labels` row to one AQS layer in Configure, click a site,
open Time Series: title and footer switch per button, tooltip name
matches the footer, CSV header uses the label, buttons unchanged, export
retains `groups`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
… map

Anchored HTML overlays such as the measure label reposition on the
engine's move event, which the deck.gl adapter only raised once the
camera settled. The basemap and side-by-side pane camera handlers now
emit move during a gesture and moveend at its end.

Closes #625
… map (#626)

## What

The measure distance label, and any other HTML overlay pinned to a map
point, now moves with the deck.gl map on every frame of a pan, zoom or
fly-to. Before this change it froze at its old screen position and only
jumped into place once the map settled.

Closes #625

## How

`DeckGLAdapter.addOverlay` repositions its node on the engine's `move`
and `moveend` events, but the deck.gl adapter only ever raised
`moveend`:

- `_onBasemapMove` kept `_viewState` in sync on every frame but emitted
nothing. It now emits `move` after updating `_viewState`, so the overlay
projects against the current camera.
- `_onPaneCameraChange` (side-by-side panes) emitted only when settled.
It now emits `move` for frames during a gesture and `moveend` at the
end.

`moveend` is unchanged, so listeners for camera-settled fire exactly as
before. The bus event `map:move`, which `Map_` forwards from the
engine's `move`, fires on deck.gl too now; it had no subscribers before.

## Tests

- Full unit suite: 3673 passed. There are no unit tests for the adapter.
- Live check (DisastersTool, deck.gl): pinned a probe overlay and ran an
animated `map:fitBounds`. The map raised 21 `map:move` events alongside
3 `map:moveend`, and the overlay's `left` updated mid-animation instead
of only at the end.
- Not yet checked by hand: dragging with the Measure tool, and the
side-by-side comparison view.
@sandesh-sp
sandesh-sp merged commit 5392574 into production Nov 6, 2026
8 of 9 checks passed

This branch was previously deployed

1 inactive deployment
development — 7a509a0b Deployed Nov 6, 2026 by sandesh-sp via app / App (development) #146
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants