Voronoi Cartogram Algorithm¶
Overview¶
A Voronoi cartogram represents each region as a Voronoi cell whose area is proportional to a data variable. Rather than deforming polygon boundaries (as in flow cartograms), the algorithm moves a set of generator points — initially placed at geometry centroids — until each point's Voronoi cell has the correct target area. The result is a Centroidal Voronoi Tessellation (CVT): a tessellation where every generator is the weighted centroid of its own cell.
The implementation lives in
backends.py
and the fields/ package.
The entry point is create_voronoi_cartogram()
(api.py).
Mathematical Foundation¶
CVT Condition¶
Given \(G\) regions with weights \(w_1, \ldots, w_G\) and a convex outer boundary \(\Omega\), a CVT satisfies:
where \(V_i\) is the Voronoi cell of generator \(\mathbf{p}_i\) and \(\rho\) is a density field proportional to the target weights. For uniform density within each cell this reduces to: each generator equals the area-weighted centroid of its cell.
Error Metric¶
Convergence is tracked by the area coefficient of variation (area CV):
where \(a_i\) is the current cell area and \(a_i^{\text{target}} \propto w_i\). Lower is better; zero means perfect proportionality.
Lloyd Relaxation Update¶
Each iteration applies one step of Lloyd relaxation with successive over-relaxation (SOR):
where \(\mathbf{c}_i\) is the weighted centroid of cell \(V_i\) and \(\alpha > 1\) is the over-relaxation factor (SOR accelerates convergence compared to \(\alpha = 1\)).
Computational Pipeline¶
---
displayMode: compact
config:
theme: neutral
---
flowchart LR
subgraph Init[INITIALIZATION]
direction TB
I1[Compute target areas<br/>from weights]
I1 --> I2[Optional: prescale<br/>disconnected components]
I2 --> I3[Initialize generators<br/>at geometry centroids]
I3 --> I4[Build outer boundary<br/>from unary union]
end
subgraph Loop[ITERATION LOOP]
direction TB
L1[Label pixels / compute cells<br/>euclidean or geodesic BFS]
L1 --> L2[Compute weighted centroid<br/>of each cell]
L2 --> L3[Over-relaxed update<br/>p ← p + α·c − α·p]
L3 --> L4[Constrain points<br/>inside boundary]
L4 --> L5[Optional: topology repair]
L5 --> L6[Compute area CV]
L6 --> L7{Converged<br/>or max_iter?}
L7 -->|No| L1
L7 -->|Yes| L8[Return VoronoiCartogram]
end
Init --> Loop
classDef hidden display: none;
Initialization¶
- Compute target areas: \(a_i^{\text{target}} = w_i \cdot A_{\text{total}} / \sum w_j\), where \(A_{\text{total}}\) is the area of the outer union boundary.
- Optionally prescale disconnected components (see Prescaling).
- Place one generator at each geometry's centroid (or representative point if the centroid falls outside the geometry).
- Build the outer boundary polygon as
unary_union(geometries), optionally simplified byoptions.simplify_tol.
Per-Iteration Steps¶
Pixel labeling / cell computation differs by backend (see Backends).
Weighted centroid: For the raster backend, the centroid of cell \(i\) is:
where the sum is over pixels \(k\) inside the cell, and weight \(w_i\) is the target area normalized weight of generator \(i\).
Over-relaxed update: \(\mathbf{p}_i \leftarrow \mathbf{p}_i + \alpha(\mathbf{c}_i - \mathbf{p}_i)\)
Boundary constraint: any generator that drifts outside the outer boundary is hard-snapped back to the nearest boundary edge point.
The Two Backends¶
RasterBackend (default)¶
Labels a raster grid of resolution × resolution pixels by the nearest
generator. The weighted centroid of each label region is then computed as
a pixel average.
Speed: 10–50× faster than the exact backend because centroid computation reduces to array indexing (no geometric intersection).
Key parameters:
| Parameter | Default | Description |
|---|---|---|
resolution |
300 | Pixel grid size (longer axis) |
relaxation |
"overrelax" |
SOR factor schedule |
distance_mode |
"euclidean" |
"euclidean" or "geodesic" (see Geodesic Labeling) |
area_equalizer_rate |
0.1 | Power-diagram bias learning rate |
boundary |
None |
AdhesiveBoundary or ElasticBoundary |
adjacency_spring |
0.0 | Spring strength preserving adjacency |
Pure FFT-flow mode: pass relaxation=0.0 together with ElasticBoundary to skip Lloyd relaxation entirely and drive movement solely from area-pressure via the FFT velocity field.
ExactBackend¶
Computes scipy.spatial.Voronoi on the generator points augmented with
mirror points at the boundary, then clips each infinite/bounded cell to the
outer boundary using shapely intersection.
Accuracy: geometrically exact cells. Useful for small datasets or when precise cell shapes matter for downstream analysis.
Limitation: does not support ElasticBoundary (only AdhesiveBoundary).
Relaxation Schedule¶
The over-relaxation factor \(\alpha\) controls step size. A value \(\alpha > 1\) (SOR) converges faster than plain Lloyd (\(\alpha = 1\)) but can overshoot if too large.
RelaxationSchedule(start, decay, minimum) decays the factor geometrically:
The shorthand "overrelax" resolves to RelaxationSchedule(start=1.9, decay=0.98, minimum=1.0). Passing a plain float (e.g. relaxation=1.5) uses a constant factor. A callable f(iteration) -> float allows arbitrary schedules.
Convergence Criteria¶
The algorithm stops at the first satisfied condition:
| Criterion | Parameter | Description |
|---|---|---|
| Area CV tolerance | area_cv_tol |
Stop when area_cv < area_cv_tol |
| Displacement tolerance | tol |
Stop when max centroid displacement per iter < tol (in CRS units) |
| Iteration limit | n_iter |
Hard stop after n_iter iterations (default 30) |
Prescaling¶
When VoronoiOptions(prescale_components=True), each group of geometrically
connected polygons is uniformly scaled to its collective target area before
the Lloyd iteration starts. This is the same routine used by the flow cartogram
(prescale_connected_components() from
geo_utils/prescale.py).
Prescaling reduces initial area CV, allowing faster convergence with fewer iterations. It is particularly effective when some regions are far from their target areas at the start.
Boundary Behavior¶
By default the outer boundary is fixed: generators are constrained inside it and the boundary polygon does not change.
AdhesiveBoundary(strength): generators whose geometry touches the outer
boundary are attracted toward the boundary edge (snapped by strength ∈ [0,1]
toward the nearest boundary point). Available on both backends.
ElasticBoundary(strength, step_scale, density_smooth, min_boundary_points, adhesion_strength): the boundary
vertices themselves are advected by an FFT-derived velocity field proportional
to the area pressure at each cell. The outer hull flexes to accommodate large
area changes at the periphery. Available on RasterBackend only.
min_boundary_points densifies simple shapes (e.g. "bbox") for smoother
deformation; adhesion_strength combines elastic deformation with centroid
adhesion in a single pass (equivalent to AdhesiveBoundary but with the snap
target tracking the evolving boundary shape).
Limitations¶
Fixed topology order. Voronoi cells are assigned to generators by position;
if two generators cross, their cell assignments may swap unexpectedly. Use
TopologyRepair or repair_topology() to detect and fix this (see
Contiguity Repair).
Boundary artifacts. The outer boundary is treated as hard walls. Peripheral
generators may cluster near the boundary if their target area is large compared
to available boundary-adjacent space. ElasticBoundary mitigates this.
Resolution trade-off (RasterBackend). Higher resolution gives smoother
cell boundaries and more accurate centroids but increases memory and compute
time quadratically. Values of 300–512 are typical.