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

@meetreeve/maps-react

v0.6.1

Published

Reeve Maps React components and API clients

Readme

@meetreeve/maps-react

The Reeve Maps frontend SDK. Provides typed API clients (geocode, getMapsConfig, fetchStaticImageBlob) that talk to your app's backend proxy of the reeve-services maps API, a choroplethSpec utility for computing Mapbox paint expressions and legend items, a <StaticMapImage> component for server-rendered or non-interactive map tiles, an interactive <ReeveMap> wrapper over react-map-gl/mapbox-gl with <ChoroplethLayer> and <HeatmapLayer> children (plus an onViewportChange viewport reporter and a useViewportQuery debounced viewport→fetch hook), an unstyled <ChoroplethLegend> for rendering choroplethSpec(...).legend, and a computeBounds helper for deriving fitBounds from a GeoJSON FeatureCollection.


Install

pnpm add @meetreeve/maps-react

Peer dependencies

pnpm add react react-dom mapbox-gl@^3 react-map-gl@^8

You must also import the Mapbox CSS once in your app (e.g. in your root layout or global CSS entry):

import "mapbox-gl/dist/mapbox-gl.css";

Next.js: SSR note

<ReeveMap> renders react-map-gl / mapbox-gl, which touch window at import time and cannot run on the server. Wrap it with next/dynamic:

// components/MapShell.tsx  (client component file)
import dynamic from "next/dynamic";

const ReeveMap = dynamic(
  () => import("@meetreeve/maps-react").then((m) => ({ default: m.ReeveMap })),
  { ssr: false }
);

// — or mark your whole component "use client" and lazy-import at the top:
// "use client";
// const ReeveMap = dynamic(..., { ssr: false });

<StaticMapImage> does not depend on mapbox-gl and can be used freely in server or client components.


API clients

All three clients accept a base string that points at your app's own backend proxy of the reeve-services maps API (e.g. /api/maps/v1). The proxy forwards requests to reeve-services over the host key — credentials never reach the browser.

getMapsConfig(base, init?)

Fetches the Mapbox public token, default style, center, and zoom from the backend.

import { getMapsConfig } from "@meetreeve/maps-react";

const config = await getMapsConfig("/api/maps/v1");
// { style, public_token, default_center, default_zoom, attribution }

geocode(base, body, init?)

Forward-geocodes an address or place name.

import { geocode } from "@meetreeve/maps-react";

const { results } = await geocode("/api/maps/v1", {
  query: "123 Main St, Austin, TX",
  country: "US",
  limit: 5,
});
// results: Array<{ lat, lng, label, relevance? }>

fetchStaticImageBlob(base, params, init?)

Returns a Blob (PNG) for a static map image. Renders server-side via the reeve-services static-image endpoint.

import { fetchStaticImageBlob } from "@meetreeve/maps-react";

const blob = await fetchStaticImageBlob("/api/maps/v1", {
  size: [800, 400],
  center: [-97.7431, 30.2672],
  zoom: 10,
  pins: [{ lng: -97.7431, lat: 30.2672, color: "#E63946", label: "A" }],
  retina: true,
});
const url = URL.createObjectURL(blob);

<StaticMapImage>

Fetches and renders a static map image as an <img>. Manages the object URL lifecycle internally.

import { StaticMapImage } from "@meetreeve/maps-react";

<StaticMapImage
  base="/api/maps/v1"
  size={[640, 320]}
  center={[-97.7431, 30.2672]}
  zoom={10}
  alt="Austin overview"
  className="rounded-lg"
/>

Props extend StaticImageParams (all fetchStaticImageBlob params) plus:

| Prop | Type | Default | Description | |------|------|---------|-------------| | base | string | — | Backend proxy base URL | | alt | string | "Map" | <img> alt text | | className | string | — | CSS class on the container/img | | fetchInit | RequestInit | — | Passed to fetch (headers, signal, etc.) |


<ReeveMap> with <ChoroplethLayer>

Interactive Mapbox map. The token can be passed directly or resolved automatically from configUrl.

"use client";
import dynamic from "next/dynamic";
import { computeBounds, choroplethSpec } from "@meetreeve/maps-react";
import type { FeatureCollection } from "geojson";

const ReeveMap = dynamic(
  () => import("@meetreeve/maps-react").then((m) => ({ default: m.ReeveMap })),
  { ssr: false }
);
const ChoroplethLayer = dynamic(
  () => import("@meetreeve/maps-react").then((m) => ({ default: m.ChoroplethLayer })),
  { ssr: false }
);

// --- in your component ---
const geojson: FeatureCollection = /* your data, each feature has properties.value */;
const values = geojson.features.map((f) => f.properties?.value as number);

// Compute legend (same math as reeve-services BE — FE and BE classifications agree)
const { legend } = choroplethSpec(values, { scheme: "YlOrRd", classes: 5 });

// Fit map to data bounds
const bounds = computeBounds(geojson); // BBox | null

return (
  <div style={{ height: 500 }}>
    <ReeveMap
      configUrl="/api/maps/v1"
      fitBounds={bounds}
      interactiveLayerIds={["districts-fill"]}
      onFeatureClick={(feature) => console.log(feature)}
    >
      <ChoroplethLayer
        id="districts"
        data={geojson}
        property="value"
        values={values}
        scheme="YlOrRd"
        classes={5}
      />
    </ReeveMap>

    {/* Legend */}
    <ul>
      {legend.map((item) => (
        <li key={item.label} style={{ color: item.color }}>{item.label}</li>
      ))}
    </ul>
  </div>
);

ReeveMapProps

| Prop | Type | Default | Description | |------|------|---------|-------------| | token | string | — | Mapbox public token (pk.*). If omitted, fetched via configUrl. | | configUrl | string | — | Backend proxy base; calls GET {configUrl}/config for token + style. | | style | string | "mapbox://styles/mapbox/dark-v11" | Mapbox style URL | | style_ | CSSProperties | — | Inline style on the container div (named style_ to avoid clash with map style) | | initialViewState | { longitude, latitude, zoom } | — | Starting viewport | | fitBounds | BBox \| null | — | [[minLng,minLat],[maxLng,maxLat]] fit on mount | | fitPadding | number | 40 | Padding (px) around fitBounds | | interactiveLayerIds | string[] | — | Layer ids to enable hover/click events on | | onFeatureClick | (feature: unknown) => void | — | Called with the topmost feature on click | | onFeatureHover | (feature: unknown \| null) => void | — | Called with hovered feature (or null on leave) | | onViewportChange | (v: ViewportBounds) => void | — | Called on load and on every moveend/resize with {west, south, east, north, zoom} | | className | string | — | CSS class on the container div | | children | ReactNode | — | <Source>/<Layer> or <ChoroplethLayer>/<HeatmapLayer> | | centerOn | { lat: number; lon: number } \| null | — | Recenter the map when this changes to a new finite pair (zoom untouched) | | centerOnOptions | { animate?: boolean; duration?: number } | — | Passed through to the flyTo call centerOn triggers | | reuseMaps | boolean | — | Passthrough to react-map-gl's <Map reuseMaps> (DEV-3199 hardening) | | id | string | "reeve-map" | Identifies this instance for the dev-only duplicate-mount warning | | selectedFeatureId | string \| number \| null | — | Currently-selected feature id, exposed to children via useReeveMapSelection() | | selectionIdProperty | string | "id" | GeoJSON feature property compared against selectedFeatureId | | onFeatureSelect | (id: string \| number, feature: unknown) => void | — | Called on click when properties[selectionIdProperty] is a string or number (requires interactiveLayerIds) |


Selection + recenter

<ReeveMap> owns selected-feature state and controlled recentering, so a consumer doesn't need to reimplement the DEV-3463 recenter-on-navigation fix or the "subject" GeoJSON-property hack for a selected-parcel ring.

Recenter on navigation — pass centerOn with the subject's current coordinates; ReeveMap flies the camera whenever the lat/lon values change (current zoom is preserved, and a new {lat, lon} object with the same values does not re-fly):

<ReeveMap token={token} centerOn={{ lat: subject.lat, lon: subject.lon }} centerOnOptions={{ duration: 800 }}>
  {/* ... */}
</ReeveMap>

Selection — pass selectedFeatureId (+ optionally selectionIdProperty, default "id") and read it back from any child via useReeveMapSelection(), which also hands you a ready-to-use Mapbox isSelectedExpression:

import { Layer } from "react-map-gl/mapbox";
import { ReeveMap, useReeveMapSelection } from "@meetreeve/maps-react";

function SelectedRingLayer() {
  const { isSelectedExpression } = useReeveMapSelection();
  return (
    <Layer
      id="parcels-outline"
      type="circle"
      source="parcels"
      paint={{
        "circle-stroke-color": ["case", isSelectedExpression, "#34d399", "#888888"],
        "circle-stroke-width": ["case", isSelectedExpression, 2, 0.5],
      }}
    />
  );
}

<ReeveMap
  token={token}
  interactiveLayerIds={["parcels-fill"]}
  selectedFeatureId={selectedBbl}
  selectionIdProperty="bbl"
  onFeatureSelect={(id) => setSelectedBbl(id)}
>
  <SelectedRingLayer />
</ReeveMap>;

isSelectedExpression/selectedCaseExpression are also exported as pure functions (from both the main entry and @meetreeve/maps-react/math) for building the same expression outside of a component:

import { selectedCaseExpression } from "@meetreeve/maps-react/math";

const strokeColor = selectedCaseExpression("bbl", selectedBbl, "#34d399", "#888888");

Single-GL-context hardeningreuseMaps passes through to react-map-gl's pooled-map-instance support, and in development ReeveMap warns (once, via console.warn) if a second instance sharing the same id mounts at the same time — the DEV-3199 shape (two simultaneous WebGL contexts). Give each concurrently-mountable map slot its own id, and gate hidden/inactive slots so only one is ever actually mounted.


ChoroplethLayerProps

| Prop | Type | Default | Description | |------|------|---------|-------------| | id | string | — | ID prefix; fill layer gets id {id}-fill | | data | FeatureCollection | — | GeoJSON source data | | property | string | "value" | Feature property to classify | | values | number[] | — | All numeric values (used to compute breaks) | | method | "quantile" \| "equal_interval" | "quantile" | Break classification method | | classes | number | 5 | Number of classes | | scheme | string | "YlOrRd" | Color scheme: "YlOrRd", "Blues", or "Gold" | | breaks | number[] | — | Explicit interior break values (overrides method/classes) | | opacity | number | 0.7 | Fill opacity |

HeatmapLayerProps

| Prop | Type | Default | Description | |------|------|---------|-------------| | id | string | — | ID prefix; heatmap layer gets id {id}-heatmap | | data | FeatureCollection | — | GeoJSON source data | | weightProperty | string | "weight" | Feature property used as heatmap weight | | intensity | number | 1 | Heatmap intensity multiplier | | radius | number | 15 | Heatmap radius (px) |


choroplethSpec and classBreaks

Use these to compute legend items or Mapbox paint expressions outside of <ChoroplethLayer>.

import { choroplethSpec, classBreaks, SCHEMES } from "@meetreeve/maps-react";

// Compute breaks only
const breaks = classBreaks(values, { method: "quantile", classes: 5 });

// Compute paint expression + legend
const { paintExpression, legend } = choroplethSpec(values, {
  method: "quantile",
  classes: 5,
  scheme: "Blues",
  property: "price", // feature property key
});

The break math (quantile and equal-interval) mirrors the reeve-services backend (choropleth.py) exactly — FE and BE classifications agree, so legend items rendered here match any server-side choropleth outputs.

Available schemes: "YlOrRd" (yellow-orange-red), "Blues", "Gold" (price ramp).

The return value also carries the breaks: number[] and colors: string[] actually used (explicit opts.breaks/opts.colors, or the computed/sampled values), so a consumer that paints per-feature in JS instead of handing paintExpression to Mapbox can reuse them directly with colorForValue.

Fixed-break layers (no live data)

values may be empty when breaks is passed explicitly — a fixed threshold layout (e.g. a distress/air layer with no per-viewport data) doesn't need dummy values just to build its legend:

const { legend } = choroplethSpec([], {
  breaks: [25, 50, 75],
  colors: ["#10b981", "#f59e0b", "#f97316", "#ef4444"],
});

An explicitly empty breaks: [] combined with empty values produces a single-class legend labeled "—".

Custom labels and formatting

labels (length must equal the class count, same as colors) replaces the generated legend labels verbatim; formatValue replaces the default fmtVal number formatting used to build generated labels:

const { legend } = choroplethSpec(values, {
  breaks: [25],
  colors: ["#10b981", "#f43f5e"],
  labels: ["Clean (<25)", "Distressed (>=25)"],
});

// or, keep generated labels but customize the number formatting:
const { legend: priceLegend } = choroplethSpec(prices, {
  classes: 5,
  scheme: "Gold",
  formatValue: (v) => `$${v.toFixed(0)}`,
});

scheme also accepts an inline array of hex colors instead of a named SCHEMES key — sampled to the class count the same way, so a fixed sequential palette (e.g. a brand ramp) doesn't need sampleColors hand-rolled again: choroplethSpec(values, { classes: 4, scheme: ["#f5e7c6", "#c9a962", "#8a5a00"] }).

colorForValue

The JS mirror of the Mapbox step expression choroplethSpec builds — for a consumer that paints per-feature in JS rather than handing paintExpression to Mapbox:

import { colorForValue } from "@meetreeve/maps-react";

const { breaks, colors } = choroplethSpec(values, { classes: 5, scheme: "YlOrRd" });
const color = colorForValue(featureValue, breaks, colors);

<ChoroplethLegend>

Unstyled color-swatch legend list — pass it choroplethSpec(...).legend (or any plain {label,color}[], e.g. map-legend.tsx's current stops). Renders null when items is empty. It carries no colors, fonts, borders, positioning, or layout of its own — each <li> is a bare list item, so itemClassName fully controls its display (e.g. flex for the default swatch-then-label row, or grid for a different arrangement). Style it entirely via className/titleClassName/listClassName/itemClassName/swatchClassName/labelClassName (or wrap it), and use children for trailing content like a data-source note.

import { choroplethSpec, ChoroplethLegend } from "@meetreeve/maps-react";

const { legend } = choroplethSpec(values, { scheme: "YlOrRd", classes: 5 });

<ChoroplethLegend
  items={legend}
  title="Distress score"
  className="absolute bottom-3 left-3 z-[500] rounded-lg bg-card/85 p-3 backdrop-blur"
  listClassName="mt-2 space-y-1"
  itemClassName="flex items-center gap-2 text-sm"
  swatchClassName="h-3 w-3 rounded-full"
>
  <p className="mt-2 text-xs text-muted-foreground">
    Illustrative sample · not live (no warehouse key)
  </p>
</ChoroplethLegend>;

ChoroplethLegendProps

| Prop | Type | Default | Description | |------|------|---------|-------------| | items | LegendItem[] \| { label, color }[] | — | Legend rows; min/max are ignored if present | | title | string | — | Optional heading rendered above the list | | children | ReactNode | — | Rendered after the list (e.g. a source-note footer) | | className | string | — | Root <div role="region"> | | titleClassName | string | — | Title <p> | | listClassName | string | — | <ul> | | itemClassName | string | — | Each <li> | | swatchClassName | string | — | Each swatch <span> | | labelClassName | string | — | Each label <span> | | style | CSSProperties | — | Inline style on the root | | aria-label | string | title, then "Legend" if both are absent/blank | Accessible name for the region |

Layer registry + chips

createLayerRegistry and <LayerChips> are the generic substrate for a layer-toggle UI (e.g. parcel / distress / air rights / price-per-sf / zoning) — the registry itself carries no product-specific data; you supply your own LayerDef<YourFeatureType>[].

import { createLayerRegistry } from "@meetreeve/maps-react";
import type { LayerDef } from "@meetreeve/maps-react";

interface MyParcel {
  distress: number | null;
  zoning: string | null;
}

const LAYERS: LayerDef<MyParcel, "distress" | "zoning">[] = [
  { id: "distress", label: "Distress", value: (p) => p.distress, has: (p) => p.distress !== null },
  { id: "zoning", label: "Zoning", title: "Zoning District", value: () => null, has: (p) => p.zoning !== null },
];

const registry = createLayerRegistry(LAYERS);
// registry.layers   — the input, order preserved
// registry.ids      — ["distress", "zoning"]
// registry.byId.zoning.title  — "Zoning District"

createLayerRegistry throws if two layers share an id.

import { useState } from "react";
import { LayerChips } from "@meetreeve/maps-react";

function LayerToggle() {
  const [active, setActive] = useState<"distress" | "zoning">("distress");

  return (
    <LayerChips
      layers={registry.layers}
      active={active}
      aria-label="Map layers"
      chipClassName={({ id, active }) => (active ? `chip chip--${id} chip--on` : `chip chip--${id}`)}
      onBeforeChange={() => {
        // synchronous — runs BEFORE onChange, so you can clear stale
        // parcels/source/loading state before the new layer's data lands
        // (avoids a one-frame flicker of the old layer under the new one).
      }}
      onChange={(next) => setActive(next)}
    />
  );
}

<LayerChips> is unstyled by design (package convention — no Tailwind/clsx, no inline colours): style chips via className/chipClassName (string or a ({ id, active }) => string function). Each chip is a real <button type="button" role="button" aria-pressed> whose accessible name is the layer's label, so getByRole("button", { name: "..." }) and fireEvent.click work directly in tests. Clicking the already-active chip is a no-op — neither onBeforeChange nor onChange fires.


computeBounds

Derives a BBox from any FeatureCollection (supports Point, LineString, Polygon, Multi*, GeometryCollection). Returns null for an empty collection.

import { computeBounds } from "@meetreeve/maps-react";
import type { BBox } from "@meetreeve/maps-react";

const bounds: BBox | null = computeBounds(geojson);
// bounds: [[minLng, minLat], [maxLng, maxLat]]

Pass the result directly to <ReeveMap fitBounds={bounds}>.


Viewport queries

onViewportChange (on <ReeveMap>) plus useViewportQuery generalize the Cadasense map-panel pattern: a debounced viewport → bbox → refetch, with a monotonic request guard so a slow response never overwrites a newer one, and a zoom-aware result cap. The facet schema and fetcher stay app-specific — the hook only owns debounce, sequencing, and loading/error/capped state.

import { ReeveMap, useViewportQuery } from "@meetreeve/maps-react";
import type { ViewportBounds } from "@meetreeve/maps-react";

function ParcelsMap() {
  const { onViewportChange, items, capped, loading, error } = useViewportQuery({
    fetcher: async ({ viewport, limit, signal }) => {
      const res = await fetch(`/api/map-layer?bbox=${JSON.stringify(viewport)}&limit=${limit}`, {
        signal,
      });
      return res.json(); // { items, capped, meta? }
    },
    // debounceMs and limitForZoom both default sensibly — override if needed
  });

  return (
    <>
      <ReeveMap token="pk.test" onViewportChange={onViewportChange} />
      {capped && <p>Zoom in to see all results.</p>}
      {loading && <p>Loading…</p>}
      {error != null && <p>Failed to load parcels.</p>}
    </>
  );
}

Pass deps: [layerId] (or any array) to reset items/capped and refetch the current viewport whenever a value changes — e.g. switching the active map layer. Pass enabled: false to suspend fetching entirely (no calls to fetcher) while still tracking onViewportChange.

limitForZoom, DEFAULT_MAX_FEATURES, and DEFAULT_VIEWPORT_DEBOUNCE_MS are also exported from the side-effect-free @meetreeve/maps-react/math subpath, so a server route can import the same cap without dragging in React or mapbox-gl:

import { limitForZoom, DEFAULT_MAX_FEATURES } from "@meetreeve/maps-react/math";