v4.3.1 · on npm
SF MapSVG

Documentation · The guide

How to draw San Francisco.

Everything you need to render the city as SVG, from a single server-side string to an animated, keyboard-accessible explorer. For every option and method, see the API reference.

This reference describes @kahwee/sf-map-svg 4.3.1, the version used throughout this site. Examples and options below match this version.

Introduction

This small TypeScript library, SF Map SVG, draws San Francisco as plain SVG. It knows the city’s precise coastline, the supervisorial districts of 2002, 2012 and 2022, three independent neighborhood definitions, park boundaries, highways, key roads and BART stations. The renderer has no runtime dependencies and never loads map tiles. You supply the geography explicitly, through bundled imports or your own data loading.

It offers two ways to work, built on the same geography:

  • renderMap returns an SVG string and a projection. It needs no DOM, so it runs in Node, at the edge, or in a build step.
  • createMap returns a controller for an interactive map in the browser, with selection, a camera, typed events and optional motion.

Geography is always passed in explicitly. The root entry point bundles no data, so a page that needs only a few layers ships only those layers.

Install

Shell
pnpm add @kahwee/sf-map-svg

Server rendering needs Node 24 or newer. Browser maps need a DOM and a bundler that can import JSON, such as Vite, Rollup, esbuild or webpack.

A first static map

Import renderMap and a geography preset, then choose the layers you want. Every layer is independent and optional.

Static SVG
import { renderMap } from '@kahwee/sf-map-svg/static';
import { staticMapData } from '@kahwee/sf-map-svg/data/static';

const { svg, project, viewBox } = renderMap(staticMapData, {
  year: 2022,
  neighborhoodLines: true,
  landmarks: true,
  bartStations: true,
  title: 'San Francisco',
});

// Place your own points with the same projection.
const [x, y] = project([-122.3937, 37.7955]); // Ferry Building
The 2022 supervisorial districts rendered as SVG
Fig. 1. The 2022 districts, fitted to the city’s coastline.

The result is a single self-contained <svg> with a title and description. Insert it into HTML, save it as a file, or embed it in an <img>. User-supplied text and attributes are escaped. Each map gets unique ids; pass idPrefix for stable ids across builds.

Choosing geography

Pick the smallest dataset that has what you need. Each is a frozen, typed import.

ImportContainsUse with
/data/staticCoast, all district years, SFAR neighborhoods, parks, highways, key roads, BARTrenderMap
/data/fullEverything above, plus all three neighborhood collections and district feature mapscreateMap
/guide/dataA lightweight, simplified guide datasetBoth
/data/coast, /data/districts, …One dataset each, to compose your ownBoth

Canonical geometry keeps its full source precision. If you render at build time, only the SVG ships to the browser, whatever the dataset’s size. See Performance.

Layers

Layers draw in a fixed, predictable order. Static maps turn them on with options; interactive maps switch them at runtime with map.configure({ layers }).

Coast. Always drawn. Land, water and the coastline.
Districts. districtFills, districtLines, districtLabels.
Neighborhoods. neighborhoodLines; interactively also neighborhoodLabels.
Parks. landmarks: park property boundaries.
Roads. highways, keyRoads, roadLabels.
BART. bartStations: the eight stations in the city.

Set labels: false to hide every visible label while keeping accessible titles. In interactive maps, district layers default on in 'districts' mode and neighborhood layers in 'neighborhoods' mode; explicit layers values always win.

Divisions

Supervisorial districts

Three dated snapshots are included: 2002, 2012 and 2022. Each keeps its own published geometry under a shared coastline mask. Choose one with year, or change it on an interactive map with map.setDistrictYear(year).

Neighborhoods

There is no single neighborhood map of San Francisco. The library ships three definitions and keeps them separate, each with its own names, aliases and sources:

Source keyAreasDefinition
'realtor'92San Francisco Association of Realtors market areas, August 2010. The default.
'sf-find'117General locations from the Mayor’s Office of Neighborhood Services, 2006. Approximate by design.
'analysis'41Reporting areas grouped from census tracts; they can combine several named places.
Switching definitions
const map = createMap(fullMapData, { mode: 'neighborhoods', source: 'realtor' });
map.setSource('sf-find');

// Look a place up in one definition, without a map.
import { getNeighborhood } from '@kahwee/sf-map-svg/data';
getNeighborhood('Inner Mission', { source: 'realtor' });

Compare them side by side in the layers studio, or pick one point and see how each names it in One spot, three San Franciscos.

Interactive maps

createMap returns a controller. Mount map.element, then drive the map through the controller.

Interactive
import { createMap } from '@kahwee/sf-map-svg';
import { fullMapData } from '@kahwee/sf-map-svg/data/full';

const map = createMap(fullMapData, {
  mode: 'districts',            // 'basemap' | 'neighborhoods' | 'districts'
  year: 2022,
  layers: { bartStations: true },
  appearance: { theme: 'districts' },
});
const host = document.querySelector('#map');
if (!host) throw new Error('Missing #map container');
host.append(map.element);

const stop = map.on('districtchange', ({ id, year }) => console.log(id, year));
map.selectDistrict(5, { fit: true });
map.camera.zoom(1.5, { animate: true });

// Call when your application removes this view.
function disposeMap() {
  stop();
  map.destroy();
  map.element.remove();
}

Update layers, controls, features, appearance, mode, source, year and labels with map.configure(). Live appearance retains camera, selection and focus. Try it in the map design playground. The camera works in a fixed 800 × 800 projected space, so viewports serialize cleanly into URLs.

Choropleths

Color districts by any value with districtStyle, a function from a district row to { fill, stroke, opacity }. It runs once per district when the map is built or restyled, not on every hover.

Coloring by data
const yesShare = (row) => row.yes / (row.yes + row.no);
const style = (district) => ({
  fill: color(yesShare(measure.districts[district.id - 1])),
});

// Static
renderMap(staticMapData, { year: 2022, districtStyle: style });

// Interactive: restyle when the data changes.
// With features.layerTransitions, the fills crossfade.
map.setDistrictStyle(style);

The measures, propositions and supervisorial votes examples are complete choropleth pages.

Animation

Every animation is opt-in, and every one yields to the reader’s reduced-motion setting.

Static maps that draw themselves

Pass animation: true to renderMap. The SVG carries its own scoped CSS: land fades in, the coast and lines draw, districts grow in order, then labels and points appear. It plays when the SVG is inserted into a page or loaded as an image, with no JavaScript. Re-insert the markup to replay it.

Draw-in
const { svg } = renderMap(staticMapData, {
  neighborhoodLines: true,
  highways: true,
  bartStations: true,
  animation: { duration: 3000, delay: 200 },
});

Interactive motion

FeatureWhat moves
motionThe camera eases between views, selections and fits.
markerEntranceNew markers enter with a stagger; retained markers keep their state.
selectedMarkerRingA ring marks the selected marker.
layerTransitionsLayers fade as they switch; neighborhood definitions crossfade on setSource, and district fills on setDistrictStyle.
districtMorphDistrict outlines travel between map years on setDistrictYear, then settle on the exact geometry.
Motion features
const map = createMap(fullMapData, {
  features: {
    motion: { duration: 600 },
    layerTransitions: true,
    districtMorph: { duration: 1400 },
  },
});
map.setDistrictYear(2012);                    // morphs
map.setDistrictYear(2022, { animate: false }); // instant

Markers and overlays

Markers are points with stable ids; overlays are lines or polygons you supply as GeoJSON geometry. Both work in static and interactive maps.

Markers
map.setMarkers([
  { id: 'ferry', label: 'Ferry Building', lng: -122.3937, lat: 37.7955 },
  { id: 'dolores', label: 'Dolores Park', lng: -122.4269, lat: 37.7596, color: '#b4432a' },
]);
map.selectMarker('ferry', { fit: true });
map.on('markerchange', ({ id }) => console.log(id));

setMarkers reconciles by id: markers that stay keep their DOM nodes, focus and animations. Turn on clustering for dense data; a full, accessible chooser remains available.

Accessibility

  • Static maps have a <title> and a description of the layers they show.
  • Interactive districts, neighborhoods and markers are reachable by keyboard and announce their state.
  • A visually hidden status region reports selections and mode changes.
  • Touch gestures are opt-in, so a map never traps page scrolling on phones.
  • All motion stops under prefers-reduced-motion.

Performance

The fastest map is one rendered before the page loads. Call renderMap in a build step or on the server and ship only the SVG. This site does exactly that: its example maps and plates are rendered at build time, and its interactive maps load a copy of fullMapData simplified for display.

  • Import individual datasets rather than presets when a page needs only a few layers.
  • Use /guide/data for a lightweight interactive guide.
  • Load interactive maps when they scroll into view with a dynamic import().

Documentation for coding assistants

Start with the developer guide in Markdown for defaults, configuration updates, coordinates and cleanup. The llms.txt index points to task-specific docs; download the complete text documentation for guides and exact TypeScript contracts in one file.

These files follow the package selected for this site. A local preview can include unreleased APIs; check its changelog before copying them into an npm integration.

Guides, transit and sources

/guide provides ready-made neighborhood guide layouts, including a server-rendered shell you can enhance later. /transit provides the schematic BART journey animation. Every geographic source, license and download date is recorded in SOURCES.md.