@meetreeve/maps-react
v0.1.0
Published
Reeve Maps React components and API clients
Downloads
194
Keywords
Readme
@reeve/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, and a computeBounds helper for deriving fitBounds from a GeoJSON FeatureCollection.
Install
pnpm add @reeve/maps-reactPeer dependencies
pnpm add react react-dom mapbox-gl@^3 react-map-gl@^8You 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("@reeve/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 "@reeve/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 "@reeve/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 "@reeve/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 "@reeve/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 "@reeve/maps-react";
import type { FeatureCollection } from "geojson";
const ReeveMap = dynamic(
() => import("@reeve/maps-react").then((m) => ({ default: m.ReeveMap })),
{ ssr: false }
);
const ChoroplethLayer = dynamic(
() => import("@reeve/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) |
| className | string | — | CSS class on the container div |
| children | ReactNode | — | <Source>/<Layer> or <ChoroplethLayer>/<HeatmapLayer> |
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 "@reeve/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).
computeBounds
Derives a BBox from any FeatureCollection (supports Point, LineString, Polygon, Multi*, GeometryCollection). Returns null for an empty collection.
import { computeBounds } from "@reeve/maps-react";
import type { BBox } from "@reeve/maps-react";
const bounds: BBox | null = computeBounds(geojson);
// bounds: [[minLng, minLat], [maxLng, maxLat]]Pass the result directly to <ReeveMap fitBounds={bounds}>.
