Pin Map
ProPoints of interest on a projected map, over an optional base layer of shapes. Markers that would overlap merge into one pin that states its own count, so the map never shows fewer things than it has. Shares the choropleth's projections and fitting. Pure SVG.
import { SvPinMapComponent } from '@styloviz-pro/charts';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Planar coordinates (a floor plan, a fictional city)
ProPrice labels and a selected pin
ProDots instead of labels on a dense map
ProTheme the pins (SVG fill cannot read a CSS token)
ProWhen to use
- WHAT and WHERE — the listings, stores, incidents or sites themselves.
- A companion map beside a result list, with hover and selection synced both ways.
- For one number per administrative region, use the Choropleth Map: colour per shape answers "how much", not "which one".
Async states
- loading
- empty
- error
- retry
Item identity & owned state
- Restorable state
- highlightIds
Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their
Changeevents to store what the user does.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| loading | boolean | false | True while the markers or the base layer are being fetched. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`. |
| emptyMessage | string | 'Nothing to show on the map' | Copy for the no-data state. |
| filtered | boolean | false | True when the emptiness is caused by a filter rather than an empty set. |
| markers | readonly SvMapMarker[] | [] | The points. |
| features | readonly SvMapFeature[] | [] | Optional shapes drawn behind the pins for context — districts, a coastline, a floor outline. Purely decorative: not focusable, not clickable. |
| projection | SvMapProjection | 'equal-earth' | How lon/lat becomes a plane. |
| width | number | 640 | Map width in pixels. |
| height | number | 360 | Map height in pixels. |
| clusterRadius | number | 34 | Minimum distance in pixels between two drawn pins. Markers closer than this merge into one counted pin. `0` disables merging. |
| showLabels | boolean | true | Draw each marker's `label` inside the pin. Turn off for a dense map where dots read better than fifty price tags. |
| selectedId | string | '' | The marker drawn as selected. `''` for none. |
| highlightIds | readonly string[] | [] | Markers to draw ringed — a hover in a companion list, say. Cheap to pass one id; the map does not care how many. |
| baseFill | string | 'color-mix(in oklab, var(--color-gray-4… | Fill for the base shapes. |
| baseStroke | string | 'color-mix(in oklab, var(--color-gray-5… | Outline for the base shapes. |
| showBaseLabels | boolean | false | Draw the base shapes' names at their centre. |
| title | string | '' | Title rendered above the map. |
| ariaLabel | string | '' | Accessible name override. Falls back to a generated summary. |
| customClass | string | '' | Extra CSS classes applied to the root wrapper. |
| palette | Partial<Record<SvPinTone, string>> | {} | Override the colour for any tone. SVG `fill` CANNOT READ A TAILWIND THEME TOKEN, which is why this input exists rather than a set of `fill-*` classes. The pins are painted with literal hexes (see `fillFor`) because a class name assembled at runtime never reaches the stylesheet — so a template that has remapped `--color-brand-*` to its own palette gets indigo pins on an olive page with no way to fix it from CSS. Pass the hexes here instead: ```ts readonly pinPalette = { accent: '#718126', default: '#3c441f' }; ``` Only the tones you name are replaced; the rest keep the defaults, which are the pairs `check:contrast` already accepts for white text on a solid fill. A colour you supply is yours to check. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retry | void | The host should re-run the failed load. |
| markerHover | SvPinMapMarkerEvent | A single marker was hovered or keyboard-focused. Not emitted for clusters. |
| markerSelect | SvPinMapMarkerEvent | A single marker was activated. Not emitted for clusters. |
| clusterSelect | SvPinMapClusterEvent | A merged pin was activated. Separate from `markerSelect` on purpose. A host that opened `markers[0]` on every click would open one listing and quietly discard the other four — the exact bug clustering exists to prevent. Two outputs make "which one did you mean?" the host's explicit decision. |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.
Template
ProBound to the names this component actually exposes.
Host class
ProDrive the three async inputs from your own fetch.
- This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `markers`, `loading` and `error`.
- Set `filtered` when a search or filter is active. It is the difference between "you have no orders" and "no orders match this filter" — telling a customer with 4,000 orders the first one reads as a bug.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Four projections, including identity for pre-projected or invented coordinates.
- Labelled price bubbles or plain dots, with a per-tone colour palette.
- Optional base shapes with haloed labels, drawn behind the pins and out of the tab order.
- Cluster radius, or 0 to disable merging entirely.
Best practices
- Do: Leave clustering on. Two markers a few pixels apart hide each other, and an overlapped pin makes the map show one thing where there are five.
- Do: Handle clusterSelect separately from markerSelect — opening markers[0] on a merged pin discards the rest.
- Do: Feed the map the same set the list shows; a map with more pins than results is a second, contradictory answer.
- Don't: Do not pass lat/lng as [lat, lon]. It is [longitude, latitude], the GeoJSON order.
- Tip: clusterRadius is in SCREEN pixels, not degrees or metres: the thing being avoided is a visual collision.
Accessibility
- The SVG is a `role="group"` whose generated summary states the point count and says outright when pins were merged because they overlap.
- A pin's position IS the datum, and position is the one thing that does not survive into text — so `sv-data-summary-table` publishes one row per MARKER, not per pin, with its label, detail, status and which merged pin it sits inside. A merged pin draws one thing where there are five; the table is where the other four are.
- One roving tab stop for the whole map: Tab enters and leaves, arrow keys and Home/End move between pins in reading order, Enter/Space activates. Each pin carries an aria-label; a merged pin names its count and every member it stands for.
- The selected pin exposes `aria-pressed`.
Feedback on Pin Map
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.