A geographic heatmap visualizes spatial density or intensity values across a map using continuous color gradients. Unlike choropleth maps that color discrete regions, this plot shows smooth density variations computed from point data or gridded values. The color intensity at each location represents the concentration or magnitude of the underlying data, making it ideal for identifying hotspots, clusters, and spatial patterns in geographic data.

""" anyplot.ai
heatmap-geographic: Geographic Heatmap for Spatial Density
Library: altair 6.1.0 | Python 3.13.13
Quality: 81/100 | Updated: 2026-05-19
"""
import os
import altair as alt
import numpy as np
import pandas as pd
# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
LAND_FILL = "#e8e8e8" if THEME == "light" else "#3A3A35"
LAND_STROKE = "#999999" if THEME == "light" else "#6A6A60"
# Data - European cities with population-like density values
np.random.seed(42)
cities = [
# (name, lat, lon, spread, n_points, weight_base)
("London", 51.5, -0.1, 0.8, 80, 1.5),
("Paris", 48.9, 2.3, 0.6, 70, 1.4),
("Berlin", 52.5, 13.4, 0.7, 55, 1.2),
("Madrid", 40.4, -3.7, 0.5, 50, 1.1),
("Rome", 41.9, 12.5, 0.4, 45, 1.0),
("Vienna", 48.2, 16.4, 0.4, 35, 0.9),
("Amsterdam", 52.4, 4.9, 0.3, 40, 1.0),
("Brussels", 50.8, 4.4, 0.3, 35, 0.9),
("Warsaw", 52.2, 21.0, 0.5, 40, 0.8),
("Prague", 50.1, 14.4, 0.3, 30, 0.8),
("Stockholm", 59.3, 18.1, 0.4, 30, 0.7),
("Munich", 48.1, 11.6, 0.3, 35, 0.9),
("Milan", 45.5, 9.2, 0.4, 40, 1.0),
("Barcelona", 41.4, 2.2, 0.4, 45, 1.0),
("Lisbon", 38.7, -9.1, 0.4, 30, 0.8),
]
data_rows = []
for _name, lat, lon, spread, n, weight in cities:
lats = np.random.normal(lat, spread, n)
lons = np.random.normal(lon, spread * 1.2, n)
values = np.random.exponential(weight, n) * 10
for i in range(n):
data_rows.append({"latitude": lats[i], "longitude": lons[i], "value": values[i]})
# Add scattered rural points
n_rural = 200
rural_lats = np.random.uniform(36, 62, n_rural)
rural_lons = np.random.uniform(-10, 25, n_rural)
rural_values = np.random.exponential(0.3, n_rural) * 5
for i in range(n_rural):
data_rows.append({"latitude": rural_lats[i], "longitude": rural_lons[i], "value": rural_values[i]})
df = pd.DataFrame(data_rows)
# Load world countries from natural earth (Vega datasets URL)
countries_url = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
countries = alt.topo_feature(countries_url, "countries")
# Basemap layer - theme-adaptive country fills
basemap = (
alt.Chart(countries)
.mark_geoshape(fill=LAND_FILL, stroke=LAND_STROKE, strokeWidth=0.8)
.project(type="mercator", scale=600, center=[10, 50])
)
# Heatmap layer - overlapping circles with viridis colormap
heatmap_points = (
alt.Chart(df)
.mark_circle(opacity=0.5)
.encode(
longitude="longitude:Q",
latitude="latitude:Q",
size=alt.Size("value:Q", scale=alt.Scale(range=[100, 1500]), legend=None),
color=alt.Color(
"value:Q",
scale=alt.Scale(scheme="viridis", domain=[0, 30]),
legend=alt.Legend(
title="Density (%)",
titleFontSize=18,
labelFontSize=14,
gradientLength=300,
gradientThickness=20,
orient="right",
),
),
tooltip=[
alt.Tooltip("latitude:Q", format=".2f", title="Lat"),
alt.Tooltip("longitude:Q", format=".2f", title="Lon"),
alt.Tooltip("value:Q", format=".1f", title="Value"),
],
)
.project(type="mercator", scale=600, center=[10, 50])
)
# Combine layers with theme-adaptive chrome
chart = (
alt.layer(basemap, heatmap_points)
.properties(
background=PAGE_BG,
width=1600,
height=900,
title=alt.Title(
"European Activity Density · heatmap-geographic · python · altair · anyplot.ai",
fontSize=28,
anchor="middle",
),
)
.configure_view(fill=PAGE_BG, strokeWidth=0)
.configure_title(color=INK, fontSize=28)
.configure_legend(
fillColor=ELEVATED_BG,
strokeColor=INK_SOFT,
titleColor=INK,
labelColor=INK_SOFT,
titleFontSize=18,
labelFontSize=14,
padding=12,
cornerRadius=4,
)
)
# Save outputs
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
chart.save(f"plot-{THEME}.html")
Part of Geographic Heatmap for Spatial Density on anyplot.ai.