Skip to content

feat: Interactive Map-Panel Intelligence Linking with Choropleth Heatmap & URL State Sharing #23

@alohays

Description

@alohays

Problem

The map and sidebar panels exist in total isolation. There is zero communication between them.

  • Clicking "Ukraine" in the Entity Tracker panel → nothing happens on the map
  • Clicking a country on the map → nothing happens in the panels
  • The Instability Index panel shows risk scores as a flat text list, but the map shows no corresponding choropleth coloring
  • There is no URL state sharing — if you zoom to the Middle East and enable specific layers, sharing the URL shows the recipient the default view

The App.ts wiring (lines 66-68) sends source data to panels via sourceManager.onItems(), but the map receives data only through its own layer system — no bridge connects the two. The SourceItem interface already has entities?: string[] and metadata?: Record<string, unknown> — fields designed for exactly this kind of cross-referencing but never used for map-panel linking.

This isolation misses the core value of having a map AND panels side-by-side: cross-referencing. The whole point of a "mission control" dashboard is that selecting something in one view highlights it everywhere else. This is what creates the "wow" experience that makes users share the project.

Visual Mockup

Choropleth Heatmap for Instability Index

When the instability-index panel has data, the map automatically colors every country polygon based on its risk score. A continuous color scale maps scores 0-10:

  • 0-3 (Low): Cool blue #1a5276
  • 4-6 (Moderate): Amber #f39c12
  • 7-10 (High): Hot red #e74c3c

Countries without data remain at the default dark-matter style. Country boundaries visible with 1px strokes. A small color scale legend in the bottom-right map corner shows the gradient with labels "Low Risk" / "Moderate" / "High Risk".

Panel → Map Linking

Each country name in InstabilityIndexPanel becomes clickable. Clicking "Ukraine" triggers a flyTo animation centering the map on Ukraine at zoom 5. The country polygon highlights with a bright accent border (2px var(--accent) stroke) for 5 seconds, then fades back.

Each entity name in EntityTrackerPanel is also clickable. If the entity has known coordinates (via a bundled geocoding table of ~200 major countries/cities), clicking pans the map there.

Map → Panel Linking

Clicking a country on the map does two things:

  1. Highlights the country's entry in the Instability Index panel (bright left border + subtle background glow)
  2. Filters the News Feed to show only items whose entities array contains that country name, with a "Showing: Ukraine" indicator and an "✕ clear filter" button to restore the full feed

AI-Annotated News Items

  • Each news item gains a small inline sentiment tag using the existing item.sentiment field (green/neutral/red pill badges)
  • Items flagged by AI focal point detection get a "FOCAL" badge — a small red pill that draws the eye to the most important stories

URL State Sharing

The current view state is encoded in the URL hash:

#center=30.5,31.2&zoom=5&layers=instability,conflicts&filter=Ukraine&panel=instability-index
  • Map center, zoom, pitch, bearing
  • Enabled layer names
  • Active panel filters and highlighted entity
  • Uses history.replaceState (not pushState) to avoid polluting browser history
  • On page load, URL hash is parsed and the entire view is restored
  • Copy URL → share → recipient sees your exact view

Implementation Approach

Phase 1: Event bus for cross-component communication

  • Create src/core/events/EventBus.ts — typed pub/sub
  • Events: entity:select, entity:deselect, map:click, map:flyTo, panel:filter, panel:highlight, state:change
  • Wire into App.ts, pass to MapEngine, PanelManager, and all panels

Phase 2: Country geocoding table

  • Create data/geo/country-centroids.json — ~200 country/territory names → [lng, lat] centroids + ISO codes
  • Static ~15KB file from public domain Natural Earth data

Phase 3: World countries GeoJSON for choropleth

  • Add data/geo/countries-110m.geojson — Natural Earth 110m simplified world polygons (~500KB) with ISO codes
  • Required for choropleth to have polygons to color

Phase 4: Choropleth layer type

  • Create src/core/map/layers/ChoroplethLayer.ts implementing LayerPlugin
  • Dynamic polygon coloring via MapLibre's data-driven styling with match/interpolate expressions
  • Colors based on feature.properties.iso_a3 matched against instability scores
  • Register as 'choropleth' in layer registry
  • Add 'choropleth' to LayerSchema.type enum in forge/src/config/schema.ts

Phase 5: MapEngine flyTo and highlight methods

Add to MapEngine:

  • flyTo(center: [number, number], zoom?: number): void
  • highlightFeature(featureId: string, duration?: number): void
  • setCountryHighlight(countryCode: string): void — temporary polygon style change
  • onClick(handler: (features, lngLat) => void): void — expose map clicks

Phase 6: Make panel items interactive

  • InstabilityIndexPanel: clickable country rows → emit entity:select with name + coordinates (from centroid lookup)
  • EntityTrackerPanel: clickable entity rows → emit entity:select
  • NewsFeedPanel: add filter(entityName) method that shows only matching items; add sentiment/focal badges; add clear-filter button

Phase 7: App.ts wiring

  • Listen entity:select → call mapEngine.flyTo() + mapEngine.setCountryHighlight()
  • Listen map:click → call panelManager.highlightEntity() + newsFeedPanel.filter()
  • On instability score updates → pass scores to choropleth layer

Phase 8: URL state manager

  • Create src/core/state/URLStateManager.ts
  • Parse window.location.hash on init: center, zoom, pitch, bearing, layers, filter, highlight
  • setState(partial) → merge + history.replaceState + update hash
  • Subscribe to map moveend and layer toggle events to keep URL in sync

Acceptance Criteria

  • Instability Index scores render as a choropleth heatmap on the map (countries colored by risk)
  • Color scale legend visible in the map corner
  • Clicking a country name in Instability Index panel → map flies to that country with highlight
  • Clicking a country on the map → highlights entry in sidebar panels
  • Clicking a country on the map → filters news feed to related items
  • "Clear filter" control restores the full news feed
  • News items show inline sentiment tags and "FOCAL" badges for AI-flagged items
  • Map state (center, zoom, layers) encoded in URL hash
  • Opening URL with hash parameters restores the encoded view state
  • choropleth added as valid layer type in config schema
  • Country centroid data covers at least 193 UN member states
  • Event bus architecture allows future panels/plugins to subscribe to cross-component events

WorldMonitor Reference

Directly inspired by WorldMonitor's Country Instability Index choropleth heatmap and URL state sharing (encoding map center, zoom, and active layers in URL parameters). This goes further by adding bidirectional map-panel linking — clicking something in a panel highlights it on the map AND clicking on the map filters panels. WorldMonitor does not fully implement this bidirectional cross-referencing, making this a differentiator for monitor-forge rather than just catch-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestfrontendFrontend engine changeshigh-impactHigh-impact feature for project growthmapMap engine and visualization

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions