import { ProportionalSymbolMap, resolveReferenceGeography } from "semiotic/geo"
ProportionalSymbolMap places sized circles at geographic coordinates, encoding a numeric value as circle area. Optionally color circles by category and overlay them on a real-world basemap. Think of it as a geographic BubbleChart.
Quick Start
Load a world basemap with resolveReferenceGeography, then pass your city data as points with a sizeByaccessor. Circles scale automatically by population.
JSX
import { useState, useEffect } from "react" import { ProportionalSymbolMap, resolveReferenceGeography } from "semiotic/geo" const cities = [ { name: "Tokyo", lon: 139.7, lat: 35.7, population: 37400000, region: "Asia" }, { name: "Delhi", lon: 77.1, lat: 28.7, population: 30291000, region: "Asia" }, { name: "São Paulo", lon: -46.6, lat: -23.5, population: 22043000, region: "South America" }, // ...more cities with lon, lat, population ] function WorldSymbolMap() { const [worldAreas, setWorldAreas] = useState(null) useEffect(() => { resolveReferenceGeography("world-110m").then(features => { setWorldAreas(features) }).catch(err => console.error("Failed to load world map:", err)) }, []) if (!worldAreas) return <div>Loading world map...</div> return ( <ProportionalSymbolMap points={cities} sizeBy="population" areas={worldAreas} areaStyle={{ fill: "#f0f0f0", stroke: "#ccc", strokeWidth: 0.5 }} tooltip /> ) }
Examples
Color by Region
Add colorBy to color circles by a categorical field like continent or region. Enable showLegend to display the color key.
Loading world map...
JSX
<ProportionalSymbolMap points={cities} sizeBy="population" colorBy="region" showLegend areas={worldAreas} areaStyle={{ fill: "#f0f0f0", stroke: "#ccc", strokeWidth: 0.5 }} tooltip />
Custom Size Range
Adjust sizeRange to control the minimum and maximum circle radii. Larger ranges make population differences more dramatic.
Loading world map...
JSX
<ProportionalSymbolMap points={cities} sizeBy="population" sizeRange={[5, 45]} colorBy="region" areas={worldAreas} areaStyle={{ fill: "#f0f0f0", stroke: "#ccc", strokeWidth: 0.5 }} tooltip />
Pass a function to tooltip for fully custom hover content.
Loading world map...
JSX
<ProportionalSymbolMap points={cities} sizeBy="population" colorBy="region" tooltip={(d) => ( <div> <strong>{d.name}</strong><br /> Pop: {(d.population / 1e6).toFixed(1)}M </div> )} areas={worldAreas} areaStyle={{ fill: "#f0f0f0", stroke: "#ccc", strokeWidth: 0.5 }} />
Custom Symbol (Ring + Fill)
Use frameProps.pointStyle to style the inner filled circle and frameProps.svgAnnotationRules with per-point annotations to draw a ring around each symbol. This creates a radar-blip effect with a solid inner dot and a stroke-only outer ring with a gap.
Loading world map...
JSX
<ProportionalSymbolMap points={cities} sizeBy="population" sizeRange={[4, 20]} colorBy="region" areas={worldAreas} frameProps={{ // Inner filled circle (canvas layer) pointStyle: (d) => ({ fill: d.fill || "#6366f1", fillOpacity: 0.85, stroke: "none", r: d.r || 6, }), // Outer ring (SVG overlay layer) svgAnnotationRules: (annotation, _index, context) => { if (annotation.type !== "ring") return null const node = context.sceneNodes.find( n => n.type === "point" && n.datum?.id === annotation.id ) if (!node) return null const ringR = (node.r || 6) + 3 // 3px gap return ( <circle cx={node.x} cy={node.y} r={ringR} fill="none" stroke={node.style?.fill || "#6366f1"} strokeWidth={1.5} strokeOpacity={0.6} /> ) }, // One ring annotation per data point annotations: cities.map(c => ({ type: "ring", id: c.id, })), }} />
Props
| Prop | Type | Required | Default | Description |
|---|
points | array | Yes | — | Array of data objects with geographic coordinates. |
xAccessor | string | function | — | "lon" | Longitude accessor. |
yAccessor | string | function | — | "lat" | Latitude accessor. |
sizeBy | string | function | Yes | — | Field name or function to determine circle size. |
sizeRange | [number, number] | — | [3, 30] | Min and max circle radius. |
colorBy | string | function | — | — | Field name or function to determine circle color. |
colorScheme | string | array | — | "category10" | Color scheme name or custom colors array. |
projection | string | object | function | — | "equalEarth" | Map projection. |
areas | GeoJSON.Feature[] | — | — | Optional background geography (country/state boundaries). |
areaStyle | object | — | { fill: "#f0f0f0", stroke: "#ccc" } | Style for background areas. |
zoomable | boolean | — | true with tileURL, false otherwise | Enable pan and zoom on the map. Defaults to true when tileURL is set. |
tooltip | boolean | function | object | — | — | Tooltip configuration. |
showLegend | boolean | — | false | Show a color legend. |
legendInteraction | "none" | "highlight" | "isolate" | — | "none" | Legend interaction mode. |
title | string | — | — | Chart title. |
width | number | — | 600 | Chart width in pixels. |
height | number | — | 400 | Chart height in pixels. |
selection | object | — | — | Selection config for LinkedCharts. |
linkedHover | object | — | — | Linked hover config for cross-chart highlighting. |
frameProps | object | — | — | Additional StreamGeoFrame props. |
Graduating to the Frame
Graduate toStreamGeoFrame when you need mixed areas + points, streaming data, or custom renderers.
Chart (simple)
JSX
import { ProportionalSymbolMap, resolveReferenceGeography } from "semiotic/geo" // Load world basemap first const worldAreas = await resolveReferenceGeography("world-110m") <ProportionalSymbolMap points={cities} sizeBy="population" colorBy="region" areas={worldAreas} areaStyle={{ fill: "#f0f0f0", stroke: "#ccc", strokeWidth: 0.5 }} showLegend tooltip />
Frame (full control)
JSX
import { StreamGeoFrame, resolveReferenceGeography } from "semiotic/geo" const worldAreas = await resolveReferenceGeography("world-110m") <StreamGeoFrame projection="equalEarth" areas={worldAreas} points={cities} xAccessor="lon" yAccessor="lat" areaStyle={() => ({ fill: "#f0f0f0", stroke: "#ccc", strokeWidth: 0.5, })} pointStyle={(d) => ({ fill: colorScale(d.region), r: sizeScale(d.population), stroke: "#fff", })} enableHover size={[700, 450]} />
- ChoroplethMap — color regions by value instead of placing symbols
- FlowMap — show connections between geographic points
- BubbleChart — similar sized circles in abstract (non-geographic) space
- StreamGeoFrame — the underlying Frame with full control