Spatial charts encode values in a coordinate system whose geometry already has meaning. A geographic map uses a projection and feature boundaries. A vector field uses position, direction, and magnitude. Neither should be treated as an ordinary categorical chart with decorative shapes.
| Reader question | Start with |
|---|---|
| How does an aggregate differ across named regions? | Choropleth |
| Where did individual events occur? | Projected point layer |
| How do direction and magnitude vary over a sampled plane? | Vector field |
| How does a path move through space? | Projected line with directional context |
| Must small regions be compared precisely by value? | Sorted bars or a table beside the map |
The application chooses the projection and spatial sampling policy. The opt-in @tanstack/charts/geo entry uses d3-geo to turn GeoJSON into shared scene paths and interaction points. See Scales and D3.
A choropleth joins one value to each named geographic feature and maps that value through a color scale.
The join is part of data preparation. Match features through stable IDs, report unmatched records, and distinguish missing values from zero. Keep the color domain and legend explicit so one filtered region cannot silently rescale the meaning of every other fill.
Area draws attention. A large region can appear important even when its value is ordinary. Pair the map with a sorted table or bars when precise ranking is part of the task.
Legends and Color covers sequential, diverging, and threshold choices.
TanStack accepts GeoJSON and does not bundle a political boundary dataset. Convert an application-owned TopoJSON atlas once, validate its feature count and IDs, then keep that geometry stable while values change.
import { feature } from 'topojson-client'
import worldAtlas from 'world-atlas/countries-110m.json'
import type {
GeometryCollection,
Objects,
Topology,
} from 'topojson-specification'
interface AtlasProperties {
name: string
}
type WorldObjects = Objects<AtlasProperties> & {
countries: GeometryCollection<AtlasProperties>
}
const topology = worldAtlas as unknown as Topology<WorldObjects>
const countries = feature<AtlasProperties>(topology, topology.objects.countries)world-atlas redistributes Natural Earth boundaries; us-atlas provides Census-derived state geometry. topojson-client.feature performs the standard conversion to GeoJSON consumed by geoShape. These remain application dependencies, so neither their data nor the converter enters ordinary Cartesian or geo-only consumer bundles.
Create the D3 projection inside projection. Its context contains the final plot bounds, so fitExtent reruns correctly when the chart resizes.
import { defineChart } from '@tanstack/charts'
import { geoShape } from '@tanstack/charts/geo'
import { geoEqualEarth } from 'd3-geo'
const regions = [
{
type: 'Feature' as const,
properties: { id: 'west', errorRate: 3.2 },
geometry: {
type: 'Polygon' as const,
coordinates: [
[
[-120, 30],
[-100, 30],
[-100, 46],
[-120, 46],
[-120, 30],
],
],
},
},
{
type: 'Feature' as const,
properties: { id: 'east', errorRate: 8.7 },
geometry: {
type: 'Polygon' as const,
coordinates: [
[
[-98, 30],
[-72, 30],
[-72, 46],
[-98, 46],
[-98, 30],
],
],
},
},
]
const collection = {
type: 'FeatureCollection' as const,
features: regions,
}
const map = defineChart({
marks: [
geoShape(regions, {
key: (region) => region.properties.id,
projection: ({ chart }) =>
geoEqualEarth().fitExtent(
[
[chart.x, chart.y],
[chart.x + chart.width, chart.y + chart.height],
],
collection,
),
fill: (region) =>
region.properties.errorRate >= 5 ? '#ef4444' : '#fbbf24',
stroke: '#ffffff',
strokeWidth: 0.75,
}),
],
guides: false,
x: null,
y: null,
})geoShape uses geoPath for each feature and D3 centroids for focus. Supply anchor only when a feature needs a different semantic longitude/latitude than its spherical centroid. The complete option contract is in Geo Shape Mark.
Point and MultiPoint features use D3 geoPath().pointRadius(). The r channel can carry pixels directly; rScale maps a quantitative value first.
The same GeoJSON can use any D3 projection factory. Sphere and graticule geometry are ordinary geoShape layers.
Polygon, LineString, and Point features can share one responsive projection.
A vector field places an arrow at each sampled position. Direction uses angle; magnitude can use length, color, or both.
Choose a sampling density that remains legible at the smallest container. More arrows can obscure the flow instead of adding evidence. When pixel length encodes magnitude, define a bounded scale and disclose whether vectors were normalized.
Vector channel and anchor options are listed in Rules, Links, Arrows, Vectors, and Ticks.
A projection turns geographic coordinates into planar geometry. Fit it to the resolved plot bounds, not the viewport, and recompute when the chart container changes. Preserve source longitude and latitude alongside projected coordinates for tooltips and selection.
When a custom spatial shape must render beside geoShape:
See Custom Marks and Renderers and Responsive Charts.
A legend is required whenever fill or vector color carries quantitative meaning.
A map should not be the only route to critical regional values. Provide a table or list with the same feature names, values, units, and current selection.
For interactive maps:
Interactions and Selections defines the controlled gesture loop.