npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 the v3-mobile Capacitor 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-map

Usage

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 lifecycle
  • MapLayerIntegration — adds MapTiler layer management
  • MapGeoJSONIntegration — adds GeoJSON data-source management, plus a MapIconManager for sprite icons (this is src/lib/map/integrations/ map-geojson-integration.svelte.ts's MapIconLayerIntegration, 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 Prettier

No 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.