Create a Tile-Count Map¶
A tile-count map gives each region an integer number of equal-sized tiles. The tile count is the data: a region with twice the value gets twice as many tiles.
Two parameters map regions to tiles, and they cannot both be set:
tile_count— a column of integers; each region gets exactly that many tilesgroup_by— a column of labels; each region keeps one tile, and tiles sharing a label are held together in one block
The mosaic layout handles both. It assigns a region's tiles as one connected block,
and pre-morphs the geometries toward their tile counts before assigning, so each
region starts from roughly the area its count asks for. The grid and packing layouts
also accept tile_count; the last section says when each of those fits better.
import warnings
import matplotlib.pyplot as plt
import numpy as np
import carto_flow.symbol_cartogram as sym
from carto_flow.data import load_us_census, load_world
states = load_us_census()
print(f"{len(states)} states")
49 states
Apportion the seats¶
US House seats are apportioned by population with the Hamilton method: give each state its integer floor, then award the remaining seats to the states with the largest fractional remainders. The result is a column of positive integers summing to 435.
population = states["Population"].to_numpy(dtype=float)
exact = population / population.sum() * 435
seats = np.floor(exact).astype(int)
seats[np.argsort(exact - seats)[::-1][: 435 - seats.sum()]] += 1
states = states.copy()
states["Seats"] = seats
print(f"{states['Seats'].sum()} seats, smallest delegation {states['Seats'].min()}")
states[["State Abbreviation", "Population", "Seats"]].sort_values("Seats", ascending=False).head(10)
435 seats, smallest delegation 1
| State Abbreviation | Population | Seats | |
|---|---|---|---|
| 1 | CA | 39346023 | 53 |
| 24 | TX | 28635442 | 38 |
| 7 | FL | 21216924 | 29 |
| 4 | NY | 19514849 | 26 |
| 0 | PA | 12794885 | 17 |
| 22 | IL | 12716164 | 17 |
| 27 | OH | 11675275 | 16 |
| 33 | NC | 10386227 | 14 |
| 11 | GA | 10516579 | 14 |
| 41 | MI | 9973907 | 13 |
Build the tilegram¶
create_layout with tile_count="Seats" and layout="mosaic" returns a
LayoutResult holding the tile assignment. style() turns each assigned lattice
cell into a hexagon, and plot() maps any column of the source table to fill color.
layout = sym.create_layout(states, tile_count="Seats", layout="mosaic", show_progress=False)
tilegram = layout.style()
fig, ax = plt.subplots(figsize=(12, 7))
tilegram.plot(
ax=ax,
source_gdf=states,
facecolor="Region",
edgecolor="white",
linewidth=0.4,
legend=True,
)
ax.set_title("US House seats — one hexagon per seat, colored by census region")
ax.axis("off")
plt.tight_layout()
Check what each region received¶
layout.regions_gdf has one row per region: target_count is what tile_count
asked for, tile_count is what the assignment delivered, and the geometry is the
union of that region's tiles.
regions = layout.regions_gdf.join(states[["State Abbreviation", "Seats"]])
print(f"{(regions['tile_count'] == regions['target_count']).sum()} of {len(regions)} regions got their exact count")
regions[["State Abbreviation", "target_count", "tile_count"]].sort_values("tile_count", ascending=False).head(10)
49 of 49 regions got their exact count
| State Abbreviation | target_count | tile_count | |
|---|---|---|---|
| 1 | CA | 53 | 53 |
| 24 | TX | 38 | 38 |
| 7 | FL | 29 | 29 |
| 4 | NY | 26 | 26 |
| 0 | PA | 17 | 17 |
| 22 | IL | 17 | 17 |
| 27 | OH | 16 | 16 |
| 33 | NC | 14 | 14 |
| 11 | GA | 14 | 14 |
| 41 | MI | 13 | 13 |
tilegram.symbols holds the same assignment one row per tile. original_index is the
row of the source table the tile belongs to, so a 53-seat state appears 53 times.
print(f"{len(tilegram.symbols)} tiles")
largest = int(states["Seats"].idxmax())
repeats = int((tilegram.symbols["original_index"] == largest).sum())
print(f"row {largest} ({states.loc[largest, 'State Abbreviation']}) appears {repeats} times")
tilegram.symbols[["geometry", "original_index", "_symbol_size"]].head(5)
435 tiles row 1 (CA) appears 53 times
| geometry | original_index | _symbol_size | |
|---|---|---|---|
| 0 | POLYGON ((1443492.733 -1777468.194, 1370731.14... | 6 | 84017.848588 |
| 1 | POLYGON ((1596292.075 -1777468.194, 1523530.48... | 6 | 84017.848588 |
| 2 | POLYGON ((1214293.721 -1645140.083, 1141532.13... | 6 | 84017.848588 |
| 3 | POLYGON ((1367093.062 -1645140.083, 1294331.47... | 6 | 84017.848588 |
| 4 | POLYGON ((1519892.404 -1645140.083, 1447130.81... | 6 | 84017.848588 |
Regions too small for a tile¶
A region draws its tiles from its own land mass. Mosaic splits the input into
geographically connected components and gives each component a pool of lattice
cells — those the component overlaps by at least min_overlap_frac, 0.1 by
default. A component no cell overlaps that much gets an empty pool, and every
region sitting on it is left out of the mosaic entirely.
A 200-tile world population tilegram hits that case. The layout names the regions it dropped rather than letting them disappear silently.
world = load_world()
world = world[world["pop_est"] > 0].copy()
world_pop = world["pop_est"].to_numpy(dtype=float)
world["tiles"] = np.maximum(np.round(world_pop / world_pop.sum() * 200), 1).astype(int)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
dropped = sym.create_layout(world, tile_count="tiles", layout="mosaic", show_progress=False)
print(next(str(w.message) for w in caught if "received no tile" in str(w.message)))
4 region(s) received no tile and are missing from the mosaic: Jamaica, Vanuatu, N. Cyprus, Cyprus. Each sits on a land mass no tile overlaps by min_overlap_frac, so its geographic component has no tiles to assign. To place them, set min_one_tile_per_region=True to place them on the nearest free cells, raise the tile budget so each of them covers more of a tile, or lower min_overlap_frac.
min_one_tile_per_region=True gives a component with an empty pool the unclaimed lattice cells
that overlap it most, enough of them to cover its requested count. The cells come
from outside every component's pool, so no other region gives one up.
The option is off by default: a region with less than one tile's worth of area is drawn at a full tile either way, which overstates it against every other region on the map.
seeded = sym.create_layout(
world,
tile_count="tiles",
layout=sym.MosaicLayout(min_one_tile_per_region=True),
show_progress=False,
)
before = dropped.regions_gdf["tile_count"].to_numpy()
after = seeded.regions_gdf["tile_count"].to_numpy()
rescued = np.flatnonzero((before == 0) & (after > 0))
print("dropped by default:", world["name"].to_numpy()[before == 0].tolist())
print("dropped with min_one_tile_per_region:", world["name"].to_numpy()[after == 0].tolist())
print(f"regions at their exact count: {int((after == world['tiles']).sum())} of {len(world)}")
dropped by default: ['Jamaica', 'Vanuatu', 'N. Cyprus', 'Cyprus'] dropped with min_one_tile_per_region: [] regions at their exact count: 177 of 177
rescued_set = set(rescued.tolist())
styled = seeded.style()
source = styled.symbols["original_index"].to_numpy()
fig, ax = plt.subplots(figsize=(13, 6))
world.boundary.plot(ax=ax, color="0.85", linewidth=0.4)
styled.plot(
ax=ax,
facecolor=["#d62728" if i in rescued_set else "#4e79a7" for i in source],
edgecolor="white",
linewidth=0.3,
)
ax.set_title("World population, 200 tiles — cells seeded by min_one_tile_per_region in red")
ax.axis("off")
plt.tight_layout()
Group regions instead of counting tiles¶
group_by keeps one tile per region and labels each region with a group. Mosaic
constrains a group's tiles to a single connected block and repairs any split it
finds. Here every congressional district is one tile, grouped by its state.
districts = load_us_census(level="congressional_district")
grouped = sym.create_layout(districts, group_by="STATE", layout="mosaic", show_progress=False)
grouped_tilegram = grouped.style()
palette = plt.get_cmap("tab20").colors
group_of_tile = grouped_tilegram.symbols["group_index"].to_numpy()
fig, ax = plt.subplots(figsize=(12, 7))
districts.dissolve("STATE").boundary.plot(ax=ax, color="0.85", linewidth=0.5)
grouped_tilegram.plot(
ax=ax,
facecolor=[palette[g % len(palette)] for g in group_of_tile],
edgecolor="white",
linewidth=0.4,
)
grouped.regions_gdf.boundary.plot(ax=ax, color="0.25", linewidth=0.9)
ax.set_title(f"{len(districts)} congressional districts, one tile each, grouped by state")
ax.axis("off")
plt.tight_layout()
to_geodataframe(level="group") merges each group's tiles into one polygon and
returns one row per group, with the attributes of the group's first source row.
by_state = grouped_tilegram.to_geodataframe(source_gdf=districts, level="group")
print(f"{len(by_state)} rows, one per state")
by_state[["State Name", "tile_count"]].sort_values("tile_count", ascending=False).head(8)
48 rows, one per state
| State Name | tile_count | |
|---|---|---|
| 3 | California | 53 |
| 40 | Texas | 36 |
| 29 | New York | 27 |
| 7 | Florida | 27 |
| 10 | Illinois | 18 |
| 35 | Pennsylvania | 18 |
| 32 | Ohio | 16 |
| 8 | Georgia | 14 |
Setting tile_count and group_by together raises rather than dropping one of them.
try:
sym.create_layout(states, tile_count="Seats", group_by="Region", layout="mosaic", show_progress=False)
except ValueError as exc:
print(exc)
Cannot set both tile_count and group_by.
When another layout fits better¶
"grid" also accepts tile_count, but it places every requested tile as a separate
item with nothing tying a region's tiles together, so a region needing many tiles
can end up with strays away from its main block. Grid refuses group_by outright.
It does win on per-region fidelity at one symbol per region, which is what the
demers_cartogram and tile_map_cartogram presets produce.
Choose Between the Grid and Mosaic Layouts
measures both on this kind of input and ends with a decision table.
CirclePackingLayout accepts tile_count as well and packs circles instead of
filling a lattice, so the result has gaps and no fixed tile geometry. Use it when
the symbols should keep a circular form rather than tile the plane.
See Also¶
- Choose Between the Grid and Mosaic Layouts
- Style Symbols by Category — per-group colors, shapes and labels
- Explanation: Mosaic Layout Algorithm
- Reference: Layout