Skip to content

feat(viz): point-in-polygon choropleth from user GeoJSON + richer hover + theme-aware geo maps#4088

Merged
jqnatividad merged 6 commits into
masterfrom
viz-choropleth-pip
Jun 28, 2026
Merged

feat(viz): point-in-polygon choropleth from user GeoJSON + richer hover + theme-aware geo maps#4088
jqnatividad merged 6 commits into
masterfrom
viz-choropleth-pip

Conversation

@jqnatividad

Copy link
Copy Markdown
Collaborator

Builds on #4087. Six commits, all part of #302.

What's new

  1. Point-in-polygon choropleth from a user GeoJSON (viz choropleth + viz smart). Each row's --lat/--lon is binned directly into the GeoJSON region whose polygon contains it (even-odd ray casting; handles holes & MultiPolygon), and the matched feature id becomes the location — exact, works at any admin level, no geocoding/GeoNames needed. Points outside every region snap to the nearest feature by default (--no-snap drops them); either way a stderr coverage note reports how many missed every polygon. Wired into viz smart as the "Regions" panel when --geojson is supplied. Zero new Cargo.lock crates (hand-rolled on geojson 0.24, default-features off).

    Why PIP and not a code/name join: GeoNames admin1 code is alphabetical (JP.01=Aichi), not ISO 3166-2 (JP46=Kagoshima), and no dump carries ISO 3166-2 — so the only sub-national key is the locale-fragile UTF-8 name. PIP on lat/lon sidesteps the join entirely.

  2. Richer choropleth hover. Instead of a bare feature id + value, each region shows a human-readable name + id (Kagoshima (JP46)), the labeled value (count: 65), the share of total (15.6% of total, count/sum aggs only), and the rank (rank 1 of 47). Names come from a new --feature-name-key flag or are auto-detected from common name properties. Applies to all paths (PIP, literal --locations, geocoded) and both the geo and MapLibre (--map) basemaps.

  3. Input disambiguation + error propagation. Supplying --lat/--lon + --geojson (PIP) and --locations (pre-keyed) without --geocode now errors instead of silently ignoring one. viz smart --geojson now propagates GeoJSON load/parse/--feature-id-key errors instead of silently dropping the panel.

  4. Theme-aware geo maps. geo-subplot charts (choropleth, geo, viz smart map/region panels) previously always rendered light-gray land regardless of --theme. They now follow the theme — dark land + dark geo.bgcolor under a dark theme so the whole map (sea + area outside the projection) matches the page. The viz smart light/dark toggle also recolors the geo land/sea live.

Cross-repo dependency

⚠️ This bumps the plotly fork pin (Cargo.lock) to dathere/plotly@c933b51 on feat/choropleth-maps, which adds geo.bgcolor to the typed LayoutGeo builder (the canonical plotly.js attribute, mirroring the existing land/ocean color fields). Cargo.toml is unchanged (pin is by branch). CI fetches the fork commit automatically.

Tests & artifacts

  • 100 viz unit + 150 integration tests pass; clippy clean.
  • Browser-verified: PIP prefecture choropleth + enriched hover; dark standalone & global choropleths render uniformly dark; smart light/dark toggle flips geo land/sea both ways.
  • Gallery + smart dashboards regenerated (the 3 --dictionary infer LLM dashboards refreshed via a local LM Studio endpoint).
  • docs/help/viz.md regenerated for the new --feature-name-key/--no-snap flags.

🤖 Generated with Claude Code

jqnatividad and others added 6 commits June 28, 2026 07:33
…ndalone)

Bin each row's lat/lon directly into a user-supplied GeoJSON's polygons and
use the matched feature id as the location — exact, works at any admin level,
and needs no geocoding or GeoNames lookup. Even-odd ray casting handles holes
and MultiPolygon. Points outside every region snap to the nearest feature by
default; --no-snap drops them. Either way a stderr coverage note reports how
many points missed every polygon (pip_assign distinguishes Inside/Snapped/
Outside so snapped points are visible, not silently absorbed).

Wired into `viz smart` as the "Regions" panel when --geojson is supplied, and
into standalone `viz choropleth`. Zero new Cargo.lock crates (hand-rolled on
geojson 0.24, default-features off). Gallery: seismic dashboard now leads with
a Japan-prefecture choropleth (examples/viz/japan_prefectures.geojson).

Why PIP and not a code/name join: GeoNames admin1 `code` is alphabetical
(JP.01=Aichi), not ISO 3166-2 (JP46=Kagoshima), and no dump carries ISO
3166-2, so the only sub-national key is the locale-fragile UTF-8 name. PIP on
lat/lon sidesteps the join entirely.

Closes part of #302.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rank)

Choropleth hover previously showed only a cryptic feature id and a bare
number. Each region now shows a human-readable name + id (Kagoshima (JP46)),
the value labeled with its measure (count: 65), the share of total
(15.6% of total, for count/sum aggregations only), and the rank
(rank 1 of 47).

The whole label is pre-rendered in Rust and attached via hover_text_array +
HoverInfo::Text (the proven ScatterGeo pattern), so it sidesteps plotly
hovertemplate token-binding and handles the Rust-computed pct/rank uniformly.
Region names come from the new --feature-name-key flag, or are auto-detected
from common name properties (properties.name, etc.). PipFeature now retains an
HTML-escaped name; the resolvers realign names to aggregate's output order and
return the hover array, which PanelKind::Choropleth carries to the smart render
arm. Applies to all paths (point-in-polygon, literal --locations, geocoded) and
both the geo Choropleth and MapLibre ChoroplethMap (--map) basemaps.

Browser-verified the rendered tooltip; regenerated the gallery so the showcase
dashboards reflect the new hover. 6 new unit + 4 new integration tests.

Part of #302.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rors

Address roborev job #3243 (two Medium findings on the PIP choropleth commit):

1. build_choropleth_plot selected point-in-polygon mode whenever --geojson +
   --lat/--lon were present, silently ignoring an also-supplied --locations
   column. lat/lon binning and a pre-keyed --locations column are two different
   ways to identify regions; supplying both without --geocode now errors as
   ambiguous instead of quietly dropping --locations.

2. build_smart_pip_choropleth_panel used `.ok()?`, swallowing GeoJSON
   load/parse errors and an unmatched --feature-id-key so `viz smart --geojson`
   succeeded with no Regions panel and no diagnostic. It now returns
   CliResult<Option<Panel>> and propagates those errors via `?`; Ok(None) is
   reserved for valid input that yields fewer than 2 regions.

Added 2 integration tests (ambiguous pip+locations; smart bad --feature-id-key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leths

Address roborev job #3244 (Medium): the enriched hover's human-readable region
names were only wired through the point-in-polygon paths. A literal --locations
choropleth backed by a custom --geojson (the geojson-id and --map paths) never
resolved feature names, so --feature-name-key and auto-detected properties.name
were silently ignored and hover fell back to the bare id — despite the docs and
CHANGELOG saying names apply there.

choropleth_literal_locations now, when --geojson is present, loads/builds the
features with feature_id_key + feature_name_key, aligns names to the aggregated
locations, and passes them into choropleth_hover_text. The name-alignment logic
(previously duplicated in the PIP resolver and the smart PIP panel) is extracted
into a shared aligned_region_names() helper.

Added a literal+geojson hover-names integration test; regenerated the gallery so
figure 28 (--map western_states) shows state names (Colorado (CO), ...).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nels)

The geo-subplot charts always rendered light-gray land (and light-blue ocean
for scatter-geo) regardless of --theme, so a dark theme showed light land on a
dark page. Now the geo land/ocean fills follow the theme:

- A shared geo_palette(theme) returns light (qsv's built-in look, unchanged) or
  dark land/water, applied at all five LayoutGeo sites (choropleth standalone +
  smart render arm, geo subcommand, smart scatter-geo grid + inline panels).
- Choropleth (no oceancolor by default) paints a dark ocean under a dark theme,
  since the geo.bgcolor "sea" fallback isn't settable via the pinned plotly
  fork's typed LayoutGeo builder.
- The viz smart light/dark toggle now relayouts geo landcolor/oceancolor/
  lakecolor too (previously only geo.bgcolor), so switching modes live recolors
  the map's land and sea, not just its background.

Light/default renders are visually unchanged (land stays the lightgray hex,
choropleths keep their white sea). Added viz_choropleth_geo_theme_aware test;
browser-verified dark standalone choropleth + smart toggle both directions.
Regenerated the gallery and all smart dashboards (incl. the three
--dictionary infer LLM dashboards, refreshed via a local LM Studio endpoint).

Part of #302.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follows up the theme-aware-geo change now that the plotly fork exposes
`geo.bgcolor` on the typed `LayoutGeo` builder (dathere/plotly
feat/choropleth-maps @ c933b51). bgcolor is the canonical plotly.js attribute
for the geo subplot background — the toggle JS already relayouted it — so this
replaces the prior workaround:

- geo_palette now returns (land, water, background); all five LayoutGeo sites
  set .bgcolor(geo_bg) directly.
- The two choropleth sites drop the dark-only showocean/oceancolor branch: a
  choropleth paints no ocean, so geo.bgcolor IS the sea. Light/dark is now
  symmetric (white vs #111111), and the background also covers the area outside
  the projection — fixing the white corners a global/oval dark map showed under
  the oceancolor-only workaround.
- Scatter-geo sites keep their light-blue ocean and now also set bgcolor.

Bumps the plotly pin in Cargo.lock (branch unchanged in Cargo.toml). Updated
viz_choropleth_geo_theme_aware to assert geo bgcolor; browser-verified a global
dark choropleth (uniformly dark, no white corners). Regenerated the gallery and
the geo smart dashboards (incl. the --dictionary infer world choropleth).

Part of #302.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@jqnatividad jqnatividad merged commit 26968d6 into master Jun 28, 2026
18 checks passed
@jqnatividad jqnatividad deleted the viz-choropleth-pip branch June 28, 2026 11:38
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.

1 participant