GeoMap

A world map with a bubble per place, its area proportional to its count, over coastlines and country borders precomputed at build time. Optionally zoomable, merging crowded places into one bubble that splits as you zoom in.
The land silhouette and the shared country borders are two paths precomputed from Natural Earth 1:50m (no runtime fetch, no map dependency, no tiles), and each point is a circle at its projected coordinate whose AREA is proportional to its count, so doubling the count doubles the disc rather than its width.
Add zoomable and the wheel zooms about the pointer, two fingers pinch, a drag pans, and places too close together to draw separately merge into one bubble carrying their summed count, splitting into their members as you zoom in.
Single-identity encoding, so there is no tone axis and no legend. Press a bubble to flag its label and count (the others dim); the accessible name carries the biggest places with their values, because a screen reader user cannot see bubbles.
Active installs
iOS
Active installs
Android
Active installs
Web
tsx
<GeoMap
title="Active installs"
points={[
{ label: "New York", lat: 40.7128, lng: -74.006, count: 6310 },
{ label: "London", lat: 51.5072, lng: -0.1276, count: 5170 },
{ label: "Bengaluru", lat: 12.9716, lng: 77.5946, count: 3890 },
{ label: "Sydney", lat: -33.8688, lng: 151.2093, count: 1260 }
]}
/>

Props

Prop
Description
points
required
GeoMapPoint[]
The places to plot; every bubble takes the primary token.
title
string
Optional heading shown above the map.
compact
boolean
Density (omit for the default map size).
formatValue
(v: number) => string
Formats the flagged and accessible counts (data formatting, not styling).
selected
number | null
Press-to-inspect: the selected point index (controlled). Pass null for none.
defaultSelected
number
Press-to-inspect: the initially selected point (uncontrolled).
onSelect
(index: number | null) => void
Fired when a press selects a point (or clears it with null).
zoomable
boolean
Zoom and pan the map, and aggregate crowded places into one bubble that splits into its members as the map is driven in. The wheel zooms about the pointer, two fingers pinch, a drag pans once zoomed, and the zoom controls and arrow keys give the same reach without a pointer.
zoom
number
The zoom factor, 1 (the whole world) upward (controlled). Needs `zoomable`.
defaultZoom
number
The zoom factor the map opens at (uncontrolled). Needs `zoomable`.
onZoomChange
(zoom: number) => void
Fired whenever the zoom factor changes, by gesture, control or key.
onSelectPlaces
(indices: number[]) => void
Fired alongside `onSelect` with EVERY point index inside the pressed bubble. A single place reports one index and an aggregated bubble reports all of its members, so `onSelect`'s own meaning never silently changes.
testID
string
E2E hook forwarded to the root element.
style
LayoutStyle
Composition within a parent only, never a restyle hook and never a width: the parent layout container provides the bounds.

Do & Don’t

GeoMap

Don’t
Places that all carry the same count draw identical bubbles, so the map costs a lot of space to say nothing; rank them in a BarList instead.
Do
Signups by city
Plot a modest number of places whose counts really differ, so the area encoding has something to say.

Zooming

Don’t
Regional totals
Turn it on for places that are already far apart. Nothing will ever merge, so the controls and the gesture are cost with no benefit, and a reader is invited to explore a map that has nothing further to show.
Do
Sessions by city
Turn on zoomable when places crowd each other, so a cluster can be opened up rather than drawn as overlapping discs.