An animation of a True-Scale Map of Sri Lanka's Administrative districts, morphing into a Continous Area Cartograms representing population
A cartogram is a map in which some thematic mapping variable, such as population or Gross Domestic Product (GDP), is substituted for land area or distance. The geometry or space of the map is distorted in order to convey the information of this alternate variable. Cartograms help to visualize the relative sizes of the variables in a way that a traditional map does not, making them useful tools in geographical statistics.
A Continuous Area Cartogram (CAC) is a type of cartogram where the map is distorted gradually and smoothly to reflect a specific variable while maintaining the contiguous nature of regions. Unlike other cartograms that might use non-contiguous shapes or overlapping regions, CACs ensure that all areas are mapped to contiguous regions, preserving neighboring relationships. This makes them more recognizable and easier to understand in terms of geographical layout.
Cartograms are especially useful for providing a visual representation of statistical data, allowing for easy comparisons and understanding of geographic distributions of data. They highlight disparities and patterns that might be overlooked in standard map representations. For example, a cartogram can make it visually apparent how much larger the population of one city is compared to another, despite the actual geographic size being smaller.
This repository implements Dougenik, Chrisman, and Niemeyer's algorithm, which they described in the 1985 paper, An Algorithm to Construct Continuous Area Cartograms.
First, construct a DNC object, providing the regions (as shapely Polygons) and their corresponding values.
Second, call run which returns a new polygon, appropriately modified.
import os
from shapely import Polygon
from cac import DNC
polygons = [
Polygon(
[
(0, 0),
(0, 1),
(1, 1),
(1, 0),
(0, 0),
]
),
Polygon(
[
(1, 0),
(1, 1),
(2, 1),
(2, 0),
(1, 0),
]
),
Polygon(
[
(0, 1),
(0, 2),
(1, 2),
(1, 1),
(0, 1),
]
),
Polygon(
[
(1, 1),
(1, 2),
(2, 2),
(2, 1),
(1, 1),
]
),
]
dnc = DNC(
polygons,
[1, 4, 1, 1],
)
new_polygon = dnc.run(
dir_output=os.path.join(
os.path.dirname(__file__),
'output',
)
)
print(new_polygon)
Output:
[<POLYGON ((0.032 0.232, 0.142 0.979, 0.68 1.32, 0.587 0.078, 0.032 0.232))>, <POLYGON ((0.587 0.078, 0.68 1.32, 1.922 1.413, 2.122 -0.122, 0.587 0.078))>, <POLYGON ((0.142 0.979, 0.112 1.888, 1.021 1.858, 0.68 1.32, 0.142 0.979))>, <POLYGON ((0.68 1.32, 1.021 1.858, 1.768 1.968, 1.922 1.413, 0.68 1.32))>]
The output directory stores the following content:
- geojson: GeoJSON files for each step of the algorithm
- images: Rendered images for each step of the algorithm
- animated.gif: Images combined into an animated GIF
Alternatively, DNCs objects can be constructed from geopandas.GeoDataFrame objects, TopoJSON, GeoJSON or gig.Ent objects.
@classmethod
def from_gdf(cls, gdf: gpd.GeoDataFrame, values: list[float], **kwargs):
...
@classmethod
def from_geojson(cls, geojson_path: str, values: list[float], **kwargs):
...
@classmethod
def from_topojson(cls, topojson_path: str, values: list[float], **kwargs):
...
@classmethod
def from_ents(cls, ents: list[Ent], values: list[float], **kwargs):
...
For more details and source code, see examples/README.md.
We plan to deploy this as a PyPI package soon.
For now you may clone/fork the repository, install the dependencies, and run the code directly.
- Simplify and improve the API, which is still a little disorganized.
- Generate some more complex examples.
The Algorithm
For each polygon
Read and store PolygonValue (negative value illegal)
Sum PolygonValue into TotalValue
For each iteration (user controls when done)
For each polygon
Calculate area and centroid (using current boundaries)
Sum areas into TotalArea
For each polygon
Desired = (TotalArea * (PolygonValue / TotalValue))
Radius = √ (Area / 𝜋)
Mass = √ (Desired / 𝜋) - √ (Area / 𝜋)
SizeError = Max(Area, Desired) / Min(Area, Desired)
ForceReductionFactor = 1 / (1 + Mean (SizeError))
For each boundary line; Read coordinate chain
For each coordinate pair
For each polygon centroid
Find angle, Distance from centroid to coordinate
If (Distance > Radius of polygon)
Fij = Mas * (Radius / Distance)
Else
Fij = Mass * (Distance² / Radius²) * (4 - 3 * (Distance / Radius))
Using Fij and angles, calculate vector sum
Multiply by ForceReductionFactor
Move coordinate accordingly
Write distorted line to output and plot result
In code, we will refer to this as the DNC Algorithm, after its authors.
The intuition behind DNC, is as follows:
Our map is a collection of polygons, each representing a region. Some polygons need to be expanded, while others need to be contracted.
Every polygon influences how every point in every polygon is moved to achieve the expansion/contraction. Hence, the algorithm is O(nm), where n = number of polygons and m = number of points in each polygon.