Metadata-Version: 2.4
Name: mapcraft
Version: 0.1.0
Summary: Choropleth maps that refuse to mislead — correct normalisation, classification, and projection, disclosed on the map.
Author: Divyansh Rai
License: MIT
Project-URL: Homepage, https://github.com/Divyansh2202/mapcraft
Project-URL: Repository, https://github.com/Divyansh2202/mapcraft
Project-URL: Issues, https://github.com/Divyansh2202/mapcraft/issues
Keywords: choropleth,map,cartography,svg,visualization,geospatial
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pillow>=10; extra == "dev"
Dynamic: license-file

# mapcraft

**Your map is probably a population map.**

<p align="center">
  <img src="gallery/01-normalisation-before.png" width="49%" alt="Births by state, raw counts">
  <img src="gallery/01-normalisation-after.png" width="49%" alt="Birth rate by state, per 1,000 residents">
</p>

Same US Census file. Same states. Same projection. The only difference is that
the right-hand map divides by population.

On the left, California, Texas, Florida, New York and Ohio are darkest — because
that is where people live. On the right, Utah, the Dakotas, Nebraska and Alaska
lead. **Only one of the top six states survives the change.**

The numbers, from public-domain Census data you can re-derive yourself:

| | |
|---|---|
| correlation(raw births, population) | **0.9921** — the left map is 99.2% a population map |
| correlation(birth rate, population) | **0.0616** — the right map is independent of population |
| top-6 states in common | **1 of 6** |

Mapping raw counts across regions of different size is the most common error in
thematic mapping, and it is invisible: the map renders cleanly, looks
authoritative, and answers a question nobody asked.

`mapcraft` makes that decision, and the several others a choropleth needs,
correctly — and prints every one of them on the map.

```bash
pip install mapcraft
```

```python
import mapcraft

svg = mapcraft.choropleth(
    {"01001": 12.4, "01003": 11.8, ...},   # or "CA", "California", 1001, "IND"
    pack="us-counties-2021",
    title="Birth rate by county, 2021",
    unit="births per 1,000 residents",
    normalize=True,
)
```

---

## What it refuses to draw

Most libraries draw whatever you give them. This one raises instead, because
each of these produces a map that misleads while looking finished.

```python
>>> mapcraft.choropleth(births_by_county, pack="us-counties-2021")
UnnormalizedCounts: These values look like raw counts: values are whole numbers
and correlate 0.99 with population; a map of these would largely be a map of
where people live. Pass normalize=True to map a rate, or normalize=False if the
absolute magnitude is genuinely what you mean to show.
```

| Refusal | Why |
|---|---|
| `UnnormalizedCounts` | Counts across unequal populations draw a population map — or the sample is too small to tell counts from rates, in which case it asks rather than guesses |
| `DuplicateRegion` | `"CA"` and `"California"` are one state — keeping the last row silently discards data |
| `AmbiguousKey` | `"Orange"` names counties in eight states; guessing attributes data to the wrong place |
| `CoverageTooLow` | Under 95% coverage, blank regions read as zero rather than as missing |

Every refusal has an explicit override. None of them is the default.

---

## The decisions it makes, and shows

Every map carries a footer stating exactly what was done:

```
normalisation: per 1,000 residents  ·  classification: jenks (5 classes)
projection: Albers USA (composite equal-area)  ·  coverage: 3141/3231 regions (97%)
data: us-counties-2021 (2021)
```

**Normalisation** — counts are detected by evidence, not by guessing at column
names: whole numbers, plus either a high correlation with population or a large
drop in spread once population is divided out. On real data those signals
separate cleanly — county births reduce spread **14×**, while sex ratio and
population density stay below 1×.

**Where this is honest about its limits:** below about 20 regions, a
correlation estimate is too wide to conclude anything. Eight hand-picked
Indian states of genuine count data score 0.64 correlation and 0.37× reduction
— both of which read as "rate". Rather than guess, `mapcraft` says it cannot
tell and asks you to state which it is. It never quietly assumes.

**Classification** — `jenks` by default, because natural breaks follow the
grouping the data actually has. `quantile` and `equal_interval` are available;
the choice is always stated, because it changes the conclusion.

<p align="center">
  <img src="gallery/02-classification-quantile.png" width="49%" alt="Quantile classification">
  <img src="gallery/02-classification-jenks.png" width="49%" alt="Jenks classification">
</p>

**Projection** — always equal-area, because a choropleth asks the reader to
compare coloured regions. Mercator inflates a region at 49°N to **1.92×** the
size of an equal one at 25°N; Albers and Mollweide measure **1.0000**.

<p align="center">
  <img src="gallery/03-projection-mercator.png" width="49%" alt="Mercator">
  <img src="gallery/03-projection-mollweide.png" width="49%" alt="Mollweide equal-area">
</p>

**Colour** — single-hue sequential ramps that stay readable with any colour
vision deficiency, in greyscale, and in dark mode. There is no rainbow option,
because rainbow ramps invent boundaries the data does not contain.

---

## Keys just work

```python
"06"  "CA"  "California"  6          # all resolve to California
"01001"  1001  "Autauga County, AL"  # all resolve to Autauga County
"IN"  "IND"  356  "India"            # all resolve to India
```

A key that lost its leading zero — a spreadsheet reading `01001` as `1001` —
is repaired and reported. Left unrepaired, that single quirk silently drops
every county in states 01–09 while the map still renders as a complete
United States.

---

## Beyond the US

<p align="center">
  <img src="gallery/06-india-sexratio.png" width="49%" alt="Sex ratio by Indian state">
  <img src="gallery/05-india-density.png" width="49%" alt="Population density by Indian state">
</p>

`india-states-2011` covers all 36 states and union territories, keyed by ISO
3166-2 (`IN-MH`), short code (`MH`), or name — including the misspelling
"Telengana" that the population source uses. Boundaries follow the post-2019
reorganisation, with Ladakh separate and Dadra & Nagar Haveli merged with Daman
& Diu, and the population table matches that vintage, so every one of the 36
regions has a denominator.

> Boundaries come from Natural Earth, whose depiction of disputed areas
> including Jammu & Kashmir and Arunachal Pradesh does not necessarily match
> the official position of the Government of India. This pack is not suitable
> for any use where the exact boundary is the subject.

---

## Every county in the US

<p align="center">
  <img src="gallery/04-counties-birthrate.png" width="100%" alt="Birth rate by US county">
</p>

3,141 counties, normalised automatically, Alaska/Hawaii/Puerto Rico placed,
90 regions without population data drawn as explicit no-data and counted in
the legend.

---

## Command line

```bash
mapcraft births.csv --pack us-states-2024 --normalize yes \
  --title "Birth rate by state" --unit "per 1,000 residents" -o map.svg

mapcraft --list-packs
```

## Use with a coding agent

`SKILL.md` in this repo is a ready-made agent skill. Point Claude Code, Codex,
or any agent at it and ask for a map; it will call the library rather than
writing its own drawing code.

---

## Data

Bundled packs are built only from public-domain sources — US Census TIGER and
Population Estimates, Natural Earth, World Bank. See [DATA_SOURCES.md](DATA_SOURCES.md).

`tools/build_pack.py` rebuilds them **byte-identically**, so you can verify the
shipped packs came from the sources named:

```bash
SOURCE_DATE_EPOCH=1700000000 python tools/build_pack.py
md5sum mapcraft/packs/*.gz
```

Boundary and population vintages are pinned **together**. `us-counties-2021`
uses 2021 population with 2021 boundaries deliberately, not the newest
available: Connecticut replaced its counties with planning regions and Alaska
split Valdez–Cordova, so newer population with older boundaries would leave
those regions blank and divide some rates by the wrong denominator — silently.

GADM is excluded: its licence forbids redistribution and commercial use.

## Install and test

```bash
pip install mapcraft            # no dependencies
python -m pytest                # 47 tests, numeric assertions only
```

Tests assert on break values, projected coordinates of known landmarks,
coverage counts, and the equal-area invariant — not on rendered pixels.

## Licence

MIT.
