Cartogram studio
The same cartogram, solved in your browser instead of in R. Change a parameter and it re-solves: states are rescaled to their seat count, dropped into their slots, pushed apart until nothing overlaps, carved into equal-area cells, and the cells matched back to districts. How each step works →
GeoJSON or TopoJSON, any polygon layer — or just drop a file on the map. Longitude/latitude is detected and projected equal-area; already-projected files are used as they are.
Drives everything downstream — each region's size and its number of cells.
Two columns: a region name or code, and a number. A header is optional.
Matched by code or name, so CA,52 and
California,52 both work.
Any whole number ≥ 1. The total need not be 435 — regions are sized by their share of it.
The area divisor. Lower makes every state bigger, so the map fills more of the frame — and collides sooner.
The gap the relaxation enforces between any two states. Zero skips relaxation and leaves them in their slots.
Every state keeps its exact outline. Each is pinned at its own centroid, the whole arrangement is spread apart until nothing overlaps, and then gravity draws it back together while the states are held apart along their borders. Uses no hand-drawn layout at all — and because the states are only moved, never bent, every shape stays exactly itself.
Both apply only when the hand-drawn slots cannot hold the seat counts. Compaction is gravity toward the centre of mass: it packs the map tighter but does not care who borders whom. Neighbour pull springs each pair of real neighbours toward the spacing they had, so the map keeps its shape — more faithful, less compact. Both are annealed as the solve settles.
More points means a better area estimate and a slower solve. The R build uses 600.
The sampling is the only randomness. Same seed, same cells.
- placement
- —
- carving
- —
- matching
- —
- arrangement
- —
- vs. the R build
- —
Your own seats, and your own map
Seats — pick Custom and type a number for any region. The total need not be 435: each region is sized by its share of whatever the total comes to, so 78 seats across nine regions works exactly as well as 435 across fifty.
Geography — load any GeoJSON or TopoJSON polygon layer.
Longitude/latitude is detected and projected with an Albers equal-area
conic fitted to your data; anything outside ±180 by ±90 is assumed to be
already projected and is only fitted to the frame. Equal-area is not a
preference here but a requirement: the cartogram scales every region so
that drawn area tracks seat count, which is only meaningful if the source
areas tracked ground area, so a Mercator file would quietly inflate the
north. Seat counts are read from a seats property if there is
one; region names from name, NAME,
st, GEOID and similar.
Three things only exist for the built-in map and switch off for a file: colouring by the 2022 winner, the hand-drawn slots and their nudges, and the comparison against the R build. An uploaded region is seeded at its own centre instead of in a hand-drawn slot — which is what those slots are an artist's refinement of. Region scales vary by an order of magnitude on a real map, so seeding at centroids alone leaves the dense corners hopelessly overlapped; the arrangement is spread until the relaxation can separate it, then fitted back to the frame. The padding is enforced before that refit, so the drawn gaps come out smaller — the page reports both numbers. A topology with several layers is decoded from whichever has the most polygons, and the page says which one it used.
What is actually running here
Projection, simplification and dissolve are mapshaper's, done once — those
are the topological operations, and they are baked into the state outlines
this page loads. Everything numerical is redone live by
web/solver.js: the seat-proportional rescaling, the
boundary-disc relaxation, uniform sampling, k-means, the capacity-balanced
power diagram, and the Hungarian assignment. It is a port of the R code,
and it is checked against it — sh/12_test_solver.js confirms
the assignment finds the same optimum R does and that the relaxation leaves
no two states closer than the padding.
Two honest caveats. First, cells here are the raw convex polygons; the
browser clips them to the state outline at paint time with an SVG
clipPath rather than intersecting the geometry as mapshaper
does. Identical on screen, but not true clipped geometry — exact areas and
exported files come from the R build. Second, this will not land on
pixel-identical positions to that build. The relaxation is a Gauss–Seidel
iteration, which is chaotic: a difference of 10⁻¹³ in the first pass grows
by roughly a factor of ten per iteration. Every run is a valid re-solve,
not a reproduction. What holds either way is the invariant — no two states
closer than the padding you asked for.
Updated · solved in your browser by web/solver.js