@sjvair/monitor-map
v3.6.3
Published
SJVAir's interactive monitor map
Readme
@sjvair/monitor-map
SJVAir's interactive air quality monitor map, built with Svelte 5, Vite, and MapTiler SDK. It ships two ways:
- A code-split widget (
npm run build), embedded via a<div id="SJVAirMonitorMap">in a Django template on sjvair.com. - A component library (
npm run build:lib, published to npm as@sjvair/monitor-map), imported by thev3-mobileCapacitor app and by other SJVAir projects (e.g.data-dashboard) that want to embed the map or reuse its map-display logic against their own data.
Installation
npm install @sjvair/monitor-mapUsage
Full-page map, via sv-router
The default integration path mounts a complete map (all integrations: monitors,
collocation sites, wind, HMS smoke/fire, EV stations) as a route in a host app that
uses sv-router:
// router.ts
import { createRouter } from "sv-router";
import { monitorMapRoutes } from "@sjvair/monitor-map";
export const { route, navigate, p, isActive, Router } = createRouter({
"/my-map": monitorMapRoutes
// ...the host app's own routes
});<!-- app root -->
<script>
import { provideMonitorMapRouter } from "@sjvair/monitor-map";
import { route, navigate, p, isActive, Router } from "./router";
provideMonitorMapRouter({ route, navigate, p, isActive, basePath: "/my-map" });
</script>
<Router />This is what the sjvair.com widget and v3-mobile use today, via MonitorMapLayout
(the thin wrapper monitorMapRoutes points to).
Embedding MapShell directly, with a reduced integration set
MonitorMapLayout is an opinionated wrapper around the lower-level MapShell
component: it wires up every integration, every manager, and the full display-options
menu. A host that only wants some of that — say, just the monitors integration, with no
EV stations/wind/HMS — should compose MapShell directly instead:
<script>
import { MapShell, monitorsMapIntegration, monitorsManager, Menu } from "@sjvair/monitor-map";
monitorsManager.init();
</script>
<MapShell
integrations={[monitorsMapIntegration]}
ready={monitorsManager.initialized}
panelOpen={false}
routerEscapeHatch={false}
>
{#snippet menu()}
<Menu>
<!-- display options for the integrations you included -->
</Menu>
{/snippet}
</MapShell>See src/lib/MonitorMapLayout.svelte for the reference example of wiring a manager, an
integration list, and menu/overlay content into MapShell.
routerEscapeHatch: MapShell defaults to routerEscapeHatch={true}, which
intercepts clicks and forces full-page navigation for links outside the map's own
routes — correct for a full-page map, wrong for a map embedded alongside a host app's
own navigation. A host with its own routing (like data-dashboard) must pass
routerEscapeHatch={false}, which also skips useMonitorMapRouter() entirely, so no
provideMonitorMapRouter(...) ancestor is required. Pass basePath if you need the
escape-hatch's "known route" prefix without going through provideMonitorMapRouter.
Mount at most one MapShell per app instance — its underlying map/load-screen/
integration-registration state is module-level singleton state shared across the whole
page.
Extending
Adding a new map feature
Map features are a plugin system built on a few abstract base classes, exported from the package root:
MapIntegration— base apply/remove lifecycleMapLayerIntegration— adds MapTiler layer managementMapGeoJSONIntegration— adds GeoJSON data-source management, plus aMapIconManagerfor sprite icons (this issrc/lib/map/integrations/ map-geojson-integration.svelte.ts'sMapIconLayerIntegration, exported under this name)
Extend one of these rather than manipulating the MapTiler map instance directly. Look
at src/lib/monitors/monitors-map-integration.svelte.ts or src/lib/wind/wind.svelte.ts
as reference implementations.
Reusing the monitors map display with different data (MonitorsDataSource)
MonitorsMapIntegration (and the icon-generation base class behind
MonitorsIconManager/CollocationIconManager) don't hardcode the live monitorsManager
singleton — they accept a MonitorsDataSource via their constructor:
export interface MonitorsDataSource {
readonly meta: MonitorsMeta | null;
readonly pollutant: "pm25" | "o3" | null;
readonly latest: XMap<string, MonitorLatestType<"pm25" | "o3">> | null;
readonly levels: Array<SJVAirEntryLevel> | null;
}monitorsManager (auto-polling, always-current readings) is the default when no data
source is given, so existing consumers are unaffected. A host app that wants the same
clustering/icon-rendering/filtering/tooltip logic driven by different data — for
example, data-dashboard's Monitors tab, which colors monitors by the average of
historical summary data over a selected date range rather than a live reading —
implements its own MonitorsDataSource (no polling, populated from
getMonitorSummariesDaily/etc.) and constructs its own instance:
import { MonitorsMapIntegration, type MonitorsDataSource } from "@sjvair/monitor-map";
const myDataSource: MonitorsDataSource = /* ... */;
const monitorsMapIntegration = new MonitorsMapIntegration(myDataSource);This reuses all of the existing map-rendering work instead of forking it — see
CLAUDE.md's "Monitor Data Flow" section for more detail.
Development
npm run dev # Start development server
npm run build # Widget build (vite build, code-split ES modules)
npm run build:lib # Library build (svelte-package), what gets published to npm
npm run preview # Preview widget build
npm run check # Type-check Svelte components
npm run check:watch # Type-check in watch mode
npm run lint # Prettier check + ESLint
npm run format # Auto-format with PrettierNo test framework is currently configured.
Publishing
Publishing to npm (@sjvair/monitor-map) happens via GitHub Actions
(.github/workflows/release-package.yml) whenever a GitHub Release is published (or the
workflow is triggered manually). Bump version in package.json before cutting the
release.
