Skip to main content
Heatfall logo

Heatfall

Turn coordinate lists into clear maps colored by count.

Geohash rectangles or H3 cells · Legends built in · Pillow images and SVG maps

Quick start · Choose a grid · Color guide · Full documentation

PyPI version Test status Documentation status Python 3.8 to 3.13 MIT license

Heatfall groups geographic observations into geohash rectangles or H3 cells, counts the points in each cell, and draws the occupied cells over a basemap. Save the result as a Pillow image, or combine a heat layer with points, routes, and service areas through Landfall.

A clustered H3 heatmap of synthetic activity across downtown Tampa, rendered by Heatfall

A seeded set of 1,040 synthetic observations, clustered around five Tampa neighborhoods with scattered background activity. The default legend labels each cell color with the point-count range it represents. Regenerate it with examples/generate_doc_maps.py.

Two grid systems Counts stay explicit Compose and export
Geohash rectangles or mostly-hexagonal H3 cells. Raw counts named in built-in legends. Add points and routes; export images or SVG.

Install

Python 3.8–3.13 is supported. Install the latest published release with:

python -m pip install heatfall

New in 1.2.0: Translucent count legends are enabled by default. Place and style them precisely, use a sequential palette, or share explicit count colors across maps. See the release notes.

To install this release exactly:

python -m pip install "heatfall==1.2.0"

The examples below use Heatfall 1.2.0 or newer. Check your version with python -m pip show heatfall.

The default OpenStreetMap basemap needs network access when tiles are not cached. No API key is required for that default provider.

Quick start

Create an H3 map from 15 synthetic observations. Repeated coordinates deliberately produce cells with different counts.

import heatfall

lats = [27.9470] * 8 + [27.9515] * 4 + [27.9430] * 2 + [27.9475]
lons = [-82.4580] * 8 + [-82.4500] * 4 + [-82.4475] * 2 + [-82.4400]

image = heatfall.plot_heat_h3s(
    lats,
    lons,
    precision=8,
    color_scheme="wheel",
    size=(800, 500),
)
image.save("h3-heatmap.png")

Both plotting functions return a PIL.Image.Image. Heatfall fits the map to the added cells automatically. size is the output width and height in pixels.

For dataframe columns, pass lists such as df["latitude"].tolist() and df["longitude"].tolist(). See the point data guide for CSV input and coordinate conversion.

Choose a grid

Geohash H3
Function plot_heat_hashes() plot_heat_h3s()
Cell shape Latitude/longitude rectangles Mostly hexagons, with pentagons in the global grid
precision range 1–12 0–15; H3 calls this resolution
Choose it when Your data or downstream tools already use geohashes You want hexagonal aggregation or already use H3
City-scale starting point Try precision=6 Try precision=8

Higher precision means smaller cells. If most occupied cells contain only one point, reduce precision to aggregate more observations. Start with a coarser grid for data spread over a large region. The two precision scales are independent: geohash precision 8 and H3 resolution 8 do not imply the same cell size.

Geohash at precision 6: the 15 quick-start observations in rectangular cells with a count legend H3 at resolution 8: the same observations in hexagonal cells with a count legend

Geohash at precision 6, then H3 at resolution 8. The same 15 observations from the quick start appear in both grids. Each map fits its own cell boundaries, so the basemap extent can differ.

Create the same map using geohash cells:

import heatfall

lats = [27.9470] * 8 + [27.9515] * 4 + [27.9430] * 2 + [27.9475]
lons = [-82.4580] * 8 + [-82.4500] * 4 + [-82.4475] * 2 + [-82.4400]

image = heatfall.plot_heat_hashes(
    lats, lons, precision=6, color_scheme="wheel", size=(800, 500)
)
image.save("geohash-heatmap.png")

Understand the colors

Each observation contributes one count to its cell. Only occupied cells are drawn. Within a heat layer, cells with the same count share a color; different count levels receive different palette colors.

Heat fills default to 60% opacity (40% transparent), keeping streets and labels visible beneath the cells. Set opacity on either plotting function or heat layer method to control the fill: 0.4 is lighter, 1.0 is solid, and 0.0 is invisible. Opacity applies uniformly to the layer; counts still determine the palette colors.

color_scheme Behavior
"heatmap" — default Maps low-to-high counts to five steps from blue, green, yellow, orange, to red
"distinct" Generates visually distinct colors for the count levels
"wheel" Selects colors from an HSV color wheel
"random" Generates random colors for the count levels
"sequential" Maps lower counts to light blue and higher counts to dark blue

The "heatmap" and "sequential" palettes order colors by numeric count; the heatmap uses five color steps from blue at the low end through green, yellow, and orange to red at the high end. Colors are scaled separately for each layer. "distinct", "wheel", and "random" distinguish count levels without implying an order, and "distinct" and "random" may change between calls. Use count_colors to assign fixed colors to count values when comparing maps.

The example below uses a sequential palette. For direct geohash/H3 comparisons, the shared color example fixes colors by count.

Legends are enabled by default. Their softly translucent panel lets heat cells remain visible beneath the labels. The default heatmap legend has at most five swatches, one for each palette color, labeled with the count range that color represents. Other color schemes show each distinct count. Entries are discrete, not a continuous gradient. Set legend=False to hide the legend. Use heatfall.LegendOptions to style the title, layer headings, labels, swatches, panel, and shadow independently, and to control columns, count order, and exact placement. It lists high-to-low by default; set count_order="ascending" for low-to-high. A map context also exposes immutable context.heat_layers metadata and context.set_legend() for composed maps.

Use LegendOptions(background_color="white", background_opacity=0.65) for a more transparent card. Background opacity ranges from 0 to 1 and multiplies the color's existing alpha; text and swatches keep their own opacity. Set shadow=False to remove the shadow as well. Titles wrap by default to keep the card compact. Use title_max_width to set the wrapping width in pixels, title_line_spacing to adjust wrapped line spacing, or title_wrap=False to disable automatic wrapping. See the placement and sizing gallery and background opacity preview.

image = heatfall.plot_heat_h3s(
    lats, lons, precision=8,
    color_scheme="sequential",
    legend=heatfall.LegendOptions(
        position=(0.96, 0.08),
        units="fraction",
        anchor="top-right",
        offset=(-8, 8),
        title="Observations per cell",
    ),
)

Positions can use nine named anchors or any (x, y) coordinate in pixels or fractions of the output dimensions. Placement is checked against the output canvas; use allow_clipping=True only when intentional. The result shows raw counts per cell, not counts normalized by cell area. It does not apply smoothing or accept observation weights.

The 15-observation example with a sequential blue palette and a translucent legend

A sequential palette makes increasing counts read from light to dark blue. The legend's background is translucent; text and swatches retain their own opacity.

Add other layers

heatfall.Context extends landfall.Context, so heat cells can share a map with ordinary geographic objects. Add the heat layer first, then add the overlays.

Landfall's Context API documents the inherited methods; its shapes and styling guide explains point sizes, line widths, circle radii, and overlay colors.

import heatfall

lats = [27.9470] * 8 + [27.9515] * 4 + [27.9430] * 2 + [27.9475]
lons = [-82.4580] * 8 + [-82.4500] * 4 + [-82.4475] * 2 + [-82.4400]

context = heatfall.Context()
context.add_heat_h3s(lats, lons, precision=8, color_scheme="wheel")

# A reference location.
context.add_points([27.9470], [-82.4580], colors=["black"], point_size=10)

# A route in (latitude, longitude) order.
context.add_line(
    [(27.9430, -82.4475), (27.9475, -82.4400)], color="black", width=3
)

# One circle per center, with radii in meters.
context.add_circles(
    [27.9515], [-82.4500], [300],
    color="blue", fill_color="transparent", width=2,
)

context.render_pillow(800, 500).save("layered-heatmap.png")

A Heatfall H3 layer with a point, route, and circle

Landfall also supplies polygons, GeoJSON support, styling, and SVG rendering. See its documentation for the inherited methods. For Heatfall's own layer methods, see the API below. The image shows the 15-observation example above, including its default count legend.

API

The full API reference documents signatures, defaults, return values, and context heat methods.

The public package exports two plotting functions, Context, and the LegendOptions and HeatLayerInfo types for configuring and inspecting legends:

Entry point Result
heatfall.plot_heat_hashes() A Pillow image containing a geohash heat layer
heatfall.plot_heat_h3s() A Pillow image containing an H3 heat layer
heatfall.Context() A map context for composing layers

Both plotting functions accept the same arguments:

Argument Default Meaning
lats Required List of latitudes in decimal degrees, from −90 to 90
lons Required Matching list of longitudes in decimal degrees, from −180 to 180
precision Required Geohash length 1–12, or H3 resolution 0–15
color_scheme "heatmap" "heatmap", "distinct", "random", "wheel", or "sequential"
tileprovider OpenStreetMap A staticmaps.TileProvider for the basemap
size (800, 500) Output (width, height) in pixels
opacity 0.6 Keyword-only fill opacity from 0.0 to 1.0
legend True Keyword-only; False, True, or LegendOptions
count_colors None Keyword-only mapping of positive counts to fixed colors

The heat layer methods mutate the context and return None; they also accept keyword-only legend_label and count_colors. The context legend is enabled by default, and set_legend(False) disables it:

context.add_heat_hashes(lats, lons, precision, color_scheme="heatmap", opacity=0.6)
context.add_heat_h3s(lats, lons, precision, color_scheme="heatmap", opacity=0.6)
context.set_legend(False)

The default provider is staticmaps.tile_provider_OSM. Configure a context's basemap with context.set_tile_provider(provider) and render it with context.render_pillow(width, height). The standalone plotting keyword is spelled tileprovider, without an underscore. See Landfall's custom tile service guide for basemap configuration and its SVG example for exporting a composed map.

Input rules

  • Supply parallel coordinate lists in latitude, longitude order, using decimal degrees. GeoJSON positions commonly use the reverse order.
  • Lists must have matching lengths. Out-of-range coordinates and non-finite values such as NaN and infinity raise ValueError.
  • Use an integer precision in the supported range and one of the named palettes.
  • Opacity must be finite and between 0.0 and 1.0, inclusive.
  • Standalone plotting functions reject empty lists. Adding an empty heat layer to a Context is a no-op; add some content before rendering.
  • Repeated coordinates count as repeated observations. Deduplicate your input first if your analysis should count unique locations instead.

Crossing the antimeridian

H3 maps can contain points on both sides of ±180° longitude:

import heatfall

image = heatfall.plot_heat_h3s(
    lats=[10.0, 10.0, 10.0],
    lons=[179.5, -179.5, 179.5],
    precision=3,
)
image.save("antimeridian.png")

Heatfall splits crossing H3 cell boundaries into closed polygons at the antimeridian, with intersections calculated along spherical edges. Both pieces keep the original cell's observation count and color, and the automatic map extent follows the short span across the seam. Exact 180 and -180 longitude are accepted. These fixes require Heatfall 1.1.0 or newer.

This handling applies to rendered H3 cells. Polygon-to-cell filling of external GeoJSON is outside Heatfall's point API. For polar cells, H3 rendering is clipped to the Web Mercator tile latitude limit of approximately ±85.0511°; these maps do not display the poles.

See geographic considerations for coordinate rules, antimeridian behavior, and polar limits.

Documentation

Read the full documentation on Read the Docs. It includes a visual example gallery, a heat-colored light/dark theme, searchable guides, and an API reference generated from the package.

Guide What you will find
Getting started Installation, coordinates, and your first image
Usage and styling Grid choices, palettes, transparency, and layers
Point data CSV input, coordinate pairs, and count semantics
Basemaps and output Providers, fixed views, rendering without tiles, and SVG
Geography Antimeridian handling and polar limits
Troubleshooting Common errors and reproducible bug reports
API reference Function signatures, defaults, and context heat methods
Development Local checks, documentation builds, and contributing
Releasing Compatibility notes and trusted publishing

Troubleshooting

Symptom What to check
Every cell has the same color Counts may all be equal. Use a coarser precision if you want more aggregation.
Cells appear in the wrong place Check latitude/longitude order and decimal-degree units. For H3, use Heatfall 1.1.0 or newer.
Cells are larger or smaller than expected Geohash and H3 use different precision scales. Adjust within the range for your chosen grid.
Basemap tiles are missing or rendering stalls Check network access and tile-provider availability. Cached tiles can avoid later requests.
Colors differ between runs or maps Palettes are generated per layer. Distinct and random colors can vary, and the number of count levels changes the palette.
add_circles() raises TypeError Pass latitude, longitude, and radii sequences; use [1000] * len(lats) for equal one-kilometer radii.

Keep the provider attribution visible when sharing map images. Heatfall's MIT license covers the package; basemap imagery has its own provider terms.

Develop and contribute

Create a virtual environment, then install the package with its development tools:

git clone https://github.com/eddiethedean/heatfall.git
cd heatfall
python -m venv .venv

Activate it with source .venv/bin/activate on macOS/Linux, or .venv\Scripts\Activate.ps1 in Windows PowerShell. Then run:

python -m pip install -e ".[dev]"
python -m pytest
python -m tox -e ruff,mypy

pytest generates coverage reports. Ordinary rendering tests use mock tiles; tests explicitly marked integration may make real tile requests.

To run the complete Python version matrix, install the matching interpreters and run python -m tox. For a single installed version, use python -m tox -e py311. CI runs tests on Python 3.8–3.13 on Linux, Python 3.13 on macOS and Windows, and checks formatting, linting, types, and package builds.

Build the documentation

Use Python 3.12 or newer for Sphinx and the documentation dependencies:

python -m pip install -e ".[docs]"
python -m sphinx -b html -W --keep-going docs docs/_build/html

Open docs/_build/html/index.html to view the site. With Python 3.13 available, python -m tox -e docs builds it in an isolated environment. CI also builds the documentation and fails on warnings.

The published documentation builds through .readthedocs.yaml. See the documentation development guide for build tools and theme customization.

The README images are actual package output. Reproduce them with:

python examples/generate_doc_maps.py

The generator uses synthetic data and real OpenStreetMap tiles. It needs network access when those tiles are not already cached.

For changes, include a runnable example or a regression test where appropriate, run the checks above, and open a pull request. For bug reports, include your Python and Heatfall versions, a small coordinate sample, the precision, and the traceback.

Maintainers can follow the release guide for compatibility notes, distribution validation, and publishing steps. Pushing a matching vX.Y.Z tag runs the release checks and publishes to PyPI through trusted publishing.

Dependencies and license

Heatfall uses Landfall ≥0.4.2 for map composition and colors, Geodude ≥0.1.1 and PyGeodesy for geohashes, and H3 ≥4.0.0 for H3 cells. Landfall provides the underlying py-staticmaps and Pillow rendering dependencies.

Released under the MIT license. Report bugs and request features in GitHub Issues.

Metadata

Release files for heatfall 1.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for heatfall 1.2.0
File Size Uploaded
heatfall-1.2.0.tar.gz 10.0 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for heatfall 1.2.0
File Interpreter ABI Platform
heatfall-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 10.7 MB

Release files / heatfall-1.2.0.tar.gz

Download URL heatfall-1.2.0.tar.gz
Size 10.0 MB
Tags Source
SHA-256 checksum
How to use checksums
27c7bd021abe5a096a2ad1cdf44fdd334aeb61526dc195f4d1a9516e8804247a
BLAKE2b-256 checksum
How to use checksums
3dec3c9030bcd1da69a542d9bacb87cee5fdc4ea41e422d8d38e783b9f8840c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / heatfall-1.2.0-py3-none-any.whl

Download URL heatfall-1.2.0-py3-none-any.whl
Size 760.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
57d3a95302e51cff6e085d318ea25fffa374d22aceb98fb0a0fb8c225d03ce6a
BLAKE2b-256 checksum
How to use checksums
f5623963217dbefe04208f231462893b4d33ae05290b7eeb31095cf1906b5d12
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.1.0

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page