import { DistanceCartogram } from "semiotic/geo"
DistanceCartogram distorts a geographic map so that distances reflect a cost metric (travel time, shipping cost, network latency) rather than physical distance. Inspired by Stanford'sORBISproject, it repositions points in polar coordinates around a center node, with the strength prop controlling the blend between geographic truth and cost-based distortion.
Quick Start
Provide points with coordinates and a cost field, acenter node ID, and a costAccessor. Points closer in cost cluster near the center. This example shows flight hours from London Heathrow to 20 major world cities.
import { DistanceCartogram } from "semiotic" const frameProps = { /* --- Data --- */ points: [ { id: "London", lon: -0.1, lat: 51.5, flightHours: 0 }, { id: "Paris", lon: 2.35, lat: 48.86, flightHours: 1.2 }, { id: "New York", lon: -74.0, lat: 40.71, flightHours: 7.5 }, { id: "Tokyo", lon: 139.69, lat: 35.69, flightHours: 11.5 }, { id: "Sydney", lon: 151.21, lat: -33.87, flightHours: 22.0 }, // ...20 major world cities with flight hours from London ], lines: [ { source: "London", target: "Paris" }, { source: "London", target: "New York" }, { source: "London", target: "Tokyo" }, // ...direct and connecting flight routes ], /* --- Size --- */ width: 600, height: 400, /* --- Layout --- */ projection: "mercator", center: "London", /* --- Process --- */ costAccessor: "flightHours", /* --- Customize --- */ costLabel: "hrs", /* --- Interact --- */ tooltip: true } export default () => { return <DistanceCartogram {...frameProps} /> }
Examples
Strength Control
strength=0 shows pure geographic positions.strength=1 shows full cartogram distortion. Values between blend the two. Notice how Paris (1.2 hrs) stays close to London while Sydney (22 hrs) moves far away in the distorted view.
import { DistanceCartogram } from "semiotic" const frameProps = { /* --- Data --- */ points: worldCities, lines: flightRoutes, /* --- Size --- */ width: 600, height: 400, /* --- Layout --- */ projection: "mercator", center: "London", strength: 0, /* --- Process --- */ costAccessor: "flightHours", /* --- Customize --- */ title: "strength=0 (geographic)", costLabel: "hrs", /* --- Interact --- */ tooltip: true } export default () => { return <DistanceCartogram {...frameProps} /> }
European Rail Network
High-speed rail distorts European geography differently than air travel. Paris to Brussels takes just 1.4 hours by Thalys, while Paris to Rome takes 11 hours despite the shorter flight time. This cartogram reveals the rail accessibility landscape from Paris Gare du Nord.
import { DistanceCartogram } from "semiotic" const frameProps = { /* --- Data --- */ points: [ { id: "Paris", lon: 2.35, lat: 48.86, railHours: 0 }, { id: "Brussels", lon: 4.35, lat: 50.85, railHours: 1.4 }, { id: "London", lon: -0.1, lat: 51.5, railHours: 2.3 }, { id: "Berlin", lon: 13.41, lat: 52.52, railHours: 8.0 }, { id: "Rome", lon: 12.5, lat: 41.9, railHours: 11.0 }, // ...15 European cities with rail hours from Paris ], lines: [{ source: "Paris", target: "Brussels" }, { source: "Paris", target: "London" }, ... ], /* --- Size --- */ width: 600, height: 400, /* --- Layout --- */ projection: "mercator", center: "Paris", /* --- Process --- */ costAccessor: "railHours", /* --- Customize --- */ pointRadius: 6, costLabel: "hrs", /* --- Interact --- */ tooltip: true } export default () => { return <DistanceCartogram {...frameProps} /> }
Colored by Reachability
Combine cartogram distortion with color encoding to double-encode the cost dimension. Cities are grouped by flight time: under 5 hours (short-haul), 5 to 10 hours (medium-haul), and over 10 hours (long-haul).
import { DistanceCartogram } from "semiotic" const frameProps = { /* --- Data --- */ points: worldCities, lines: flightRoutes, /* --- Size --- */ width: 600, height: 400, /* --- Layout --- */ projection: "mercator", center: "London", /* --- Process --- */ costAccessor: "flightHours", /* --- Customize --- */ colorBy: (d) => d.flightHours === 0 ? "Hub" : d.flightHours <= 5 ? "Short-haul" : d.flightHours <= 10 ? "Medium-haul" : "Long-haul", showLegend: true, costLabel: "hrs", /* --- Interact --- */ tooltip: true } export default () => { return <DistanceCartogram {...frameProps} /> }
How It Works
The distance cartogram applies a post-projection transform:
- Project all points to screen coordinates using the base projection (e.g., Mercator).
- Identify the center point's screen position.
- For each other point, compute the angle from center (preserved) and a new radius proportional to its cost value.
- Interpolate between the geographic position and the cost-based position using
strength (0-1). - Optionally reposition connecting lines to follow the distorted points.
This means geographic direction is preserved but distance is distorted — Paris remains northwest of London but at strength=1 it appears much closer than New York, even though New York is farther geographically. Set zoomable to enable pan and zoom for exploring dense clusters.
Props
| Prop | Type | Required | Default | Description |
|---|
points | array | Yes | — | Array of data objects with geographic coordinates and a cost/distance field. |
center | string | Yes | — | ID of the center point. All distances are measured from this node. |
costAccessor | string | function | Yes | — | Field name or function to extract the cost/distance value from each point. |
strength | number | — | 1 | Interpolation between geographic positions (0) and cartogram positions (1). Animate this for smooth transitions. |
lineMode | "straight" | "fractional" | — | "straight" | How connecting lines are drawn. Straight snaps to endpoints; fractional interpolates intermediate points. |
nodeIdAccessor | string | — | "id" | Field name to identify points. |
xAccessor | string | function | — | "lon" | Longitude accessor. |
yAccessor | string | function | — | "lat" | Latitude accessor. |
lines | array | — | — | Array of { source, target } objects for connecting lines between points. |
projection | string | object | function | — | "mercator" | Base geographic projection before cartogram distortion. |
zoomable | boolean | — | true with tileURL, false otherwise | Enable mouse wheel zoom and pan on the cartogram. Defaults to true when tileURL is set. |
transition | number | — | — | Transition duration in ms when center or strength changes. |
colorBy | string | function | — | — | Field name or function for point color. |
colorScheme | string | array | — | "category10" | Color scheme. |
pointRadius | number | — | 5 | Base circle radius. |
showRings | boolean | number | number[] | — | true | Concentric distance rings around center. true for auto, number for ring count, or number[] for explicit cost values. |
ringStyle | object | — | — | Ring style: { stroke, strokeWidth, strokeDasharray, labelColor, labelSize }. |
showNorth | boolean | — | true | Show a north-pointing compass indicator. |
costLabel | string | — | — | Unit label for ring values (e.g. "hrs", "km"). |
tooltip | boolean | function | object | — | — | Tooltip configuration. |
showLegend | boolean | — | false | Show a legend. |
title | string | — | — | Chart title. |
width | number | — | 600 | Chart width. |
height | number | — | 400 | Chart height. |
selection | object | — | — | Selection config for LinkedCharts. |
linkedHover | object | — | — | Linked hover config. |
frameProps | object | — | — | Additional StreamGeoFrame props. |
Graduating to the Frame
UseStreamGeoFrame withprojectionTransform for full control over the cartogram pipeline.
Chart (simple)
JSX
import { DistanceCartogram } from "semiotic/geo" <DistanceCartogram points={cities} center="London" costAccessor="flightHours" costLabel="hrs" strength={0.8} lines={routes} tooltip />
Frame (full control)
JSX
import { StreamGeoFrame } from "semiotic/geo" <StreamGeoFrame projection="mercator" points={cities} lines={routesWithCoords} lineDataAccessor="coordinates" xAccessor="lon" yAccessor="lat" pointIdAccessor="id" projectionTransform={{ center: "London", centerAccessor: "id", costAccessor: "flightHours", strength: 0.8, }} pointStyle={(d) => ({ fill: "#6366f1", r: 5, stroke: "#fff", })} size={[600, 400]} />