Skip to content

Commit 105a1ce

Browse files
feat(altair): implement contour-map-geographic (#3941)
## Implementation: `contour-map-geographic` - altair Implements the **altair** version of `contour-map-geographic`. **File:** `plots/contour-map-geographic/implementations/altair.py` **Parent Issue:** #3772 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21093797329)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 4744625 commit 105a1ce

2 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
""" pyplots.ai
2+
contour-map-geographic: Contour Lines on Geographic Map
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 82/100 | Created: 2026-01-17
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data - Generate temperature-like data for Europe region
13+
np.random.seed(42)
14+
15+
# Create dense grid covering wider Europe region to fill map canvas
16+
lon_range = np.linspace(-25, 55, 140)
17+
lat_range = np.linspace(30, 72, 90)
18+
lon_grid, lat_grid = np.meshgrid(lon_range, lat_range)
19+
20+
# Generate temperature pattern (decreases with latitude, varies with longitude)
21+
temperature = (
22+
30
23+
- 0.6 * (lat_grid - 30) # Cooler as you go north
24+
+ 3 * np.sin((lon_grid + 10) / 15) # East-west variation (Gulf Stream effect)
25+
+ 2 * np.cos(lat_grid / 10) # Additional pattern
26+
- 5 * np.exp(-((lat_grid - 47) ** 2 + (lon_grid - 10) ** 2) / 100) # Alps cold spot
27+
- 3 * np.exp(-((lat_grid - 65) ** 2 + (lon_grid - 25) ** 2) / 150) # Scandinavian cold
28+
+ np.random.normal(0, 0.3, lon_grid.shape) # Minimal noise for smoother contours
29+
)
30+
31+
# Clip to realistic range
32+
temperature = np.clip(temperature, -15, 35)
33+
34+
# Create DataFrame
35+
df = pd.DataFrame(
36+
{"longitude": lon_grid.flatten(), "latitude": lat_grid.flatten(), "temperature": temperature.flatten()}
37+
)
38+
39+
# Define contour levels for labeling
40+
contour_levels = [-10, -5, 0, 5, 10, 15, 20, 25, 30]
41+
42+
# Load world countries for geographic context
43+
countries = alt.topo_feature("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json", "countries")
44+
45+
# Map projection settings - centered on Europe with better coverage
46+
projection_params = {"type": "mercator", "scale": 550, "center": [15, 52]}
47+
48+
# Base map - world countries with subtle styling
49+
base = (
50+
alt.Chart(countries)
51+
.mark_geoshape(fill="#E8E8E8", stroke="#AAAAAA", strokeWidth=0.5)
52+
.project(**projection_params)
53+
.properties(width=1600, height=900)
54+
)
55+
56+
# Create smooth filled contour visualization using mark_square for cleaner coverage
57+
filled_contours = (
58+
alt.Chart(df)
59+
.mark_square(size=200, opacity=0.92)
60+
.encode(
61+
longitude="longitude:Q",
62+
latitude="latitude:Q",
63+
color=alt.Color(
64+
"temperature:Q",
65+
scale=alt.Scale(scheme="redyellowblue", reverse=True, domain=[-10, 30]),
66+
legend=alt.Legend(
67+
title="Temperature (°C)",
68+
titleFontSize=20,
69+
labelFontSize=16,
70+
gradientLength=450,
71+
gradientThickness=30,
72+
orient="right",
73+
offset=20,
74+
),
75+
),
76+
tooltip=[
77+
alt.Tooltip("longitude:Q", format=".1f", title="Longitude"),
78+
alt.Tooltip("latitude:Q", format=".1f", title="Latitude"),
79+
alt.Tooltip("temperature:Q", format=".1f", title="Temperature (°C)"),
80+
],
81+
)
82+
.project(**projection_params)
83+
.properties(width=1600, height=900)
84+
)
85+
86+
# Create contour line data by identifying boundary points between temperature bins
87+
# Use tighter threshold for cleaner isolines
88+
contour_data = []
89+
for level in contour_levels[1:-1]: # Skip extreme ends
90+
mask = np.abs(df["temperature"] - level) < 0.5
91+
level_points = df[mask].copy()
92+
level_points["contour_value"] = level
93+
level_points["contour_label"] = f"{level}°C"
94+
contour_data.append(level_points)
95+
96+
contour_df = pd.concat(contour_data, ignore_index=True)
97+
98+
# Contour lines layer - more prominent strokes forming isolines
99+
contour_lines = (
100+
alt.Chart(contour_df)
101+
.mark_circle(size=25, opacity=0.85)
102+
.encode(longitude="longitude:Q", latitude="latitude:Q", color=alt.value("#1a1a1a"))
103+
.project(**projection_params)
104+
.properties(width=1600, height=900)
105+
)
106+
107+
# Create contour labels at strategic positions
108+
# Sample representative points along each contour level for labeling
109+
label_data = []
110+
for level in [0, 10, 20]:
111+
level_points = contour_df[contour_df["contour_value"] == level]
112+
if len(level_points) > 0:
113+
# Select points at different longitudes for label placement
114+
for target_lon in [-10, 10, 30]:
115+
closest_idx = (level_points["longitude"] - target_lon).abs().idxmin()
116+
point = level_points.loc[closest_idx].copy()
117+
label_data.append({"longitude": point["longitude"], "latitude": point["latitude"], "label": f"{level}°C"})
118+
119+
label_df = pd.DataFrame(label_data)
120+
121+
# Contour value labels with higher contrast
122+
contour_labels = (
123+
alt.Chart(label_df)
124+
.mark_text(fontSize=18, fontWeight="bold", fill="#000000", stroke="#FFFFFF", strokeWidth=3)
125+
.encode(longitude="longitude:Q", latitude="latitude:Q", text="label:N")
126+
.project(**projection_params)
127+
.properties(width=1600, height=900)
128+
)
129+
130+
# Create latitude/longitude axis labels for map edges
131+
# Longitude labels along bottom
132+
lon_labels_data = pd.DataFrame({"longitude": [-20, -10, 0, 10, 20, 30, 40, 50], "latitude": [31] * 8})
133+
lon_labels_data["label"] = lon_labels_data["longitude"].apply(
134+
lambda x: f"{abs(x)}°{'W' if x < 0 else 'E'}" if x != 0 else "0°"
135+
)
136+
137+
lon_axis_labels = (
138+
alt.Chart(lon_labels_data)
139+
.mark_text(fontSize=14, fontWeight="normal", fill="#333333", dy=12)
140+
.encode(longitude="longitude:Q", latitude="latitude:Q", text="label:N")
141+
.project(**projection_params)
142+
.properties(width=1600, height=900)
143+
)
144+
145+
# Latitude labels along left edge
146+
lat_labels_data = pd.DataFrame({"longitude": [-24] * 5, "latitude": [35, 45, 55, 65, 70]})
147+
lat_labels_data["label"] = lat_labels_data["latitude"].apply(lambda x: f"{x}°N")
148+
149+
lat_axis_labels = (
150+
alt.Chart(lat_labels_data)
151+
.mark_text(fontSize=14, fontWeight="normal", fill="#333333", dx=-12, align="right")
152+
.encode(longitude="longitude:Q", latitude="latitude:Q", text="label:N")
153+
.project(**projection_params)
154+
.properties(width=1600, height=900)
155+
)
156+
157+
# Layer all components: base map, filled contours, contour lines, labels, axis labels
158+
chart = (
159+
alt.layer(base, filled_contours, contour_lines, contour_labels, lon_axis_labels, lat_axis_labels)
160+
.properties(
161+
width=1600,
162+
height=900,
163+
title=alt.Title("contour-map-geographic · altair · pyplots.ai", fontSize=28, anchor="middle", color="#333333"),
164+
)
165+
.configure_view(strokeWidth=0)
166+
)
167+
168+
# Save outputs
169+
chart.save("plot.png", scale_factor=3.0)
170+
chart.save("plot.html")
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
library: altair
2+
specification_id: contour-map-geographic
3+
created: '2026-01-17T11:55:07Z'
4+
updated: '2026-01-18T22:37:15Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21093797329
7+
issue: 3772
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/contour-map-geographic/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/contour-map-geographic/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/contour-map-geographic/altair/plot.html
13+
quality_score: 82
14+
review:
15+
strengths:
16+
- Good use of Altair geographic projection capabilities (Mercator with custom center/scale)
17+
- Effective layer composition combining basemap, temperature fill, contour markers,
18+
and labels
19+
- Colorblind-friendly diverging color scheme (redyellowblue)
20+
- Interactive tooltips showing exact coordinates and temperature
21+
- Temperature labels with white stroke provide good contrast
22+
- Realistic temperature pattern with regional variations (Alps, Scandinavia cold
23+
spots)
24+
weaknesses:
25+
- Contour lines are represented as scattered point markers rather than true smooth
26+
isolines - fundamental Altair limitation
27+
- 'Title format incorrect: includes European Temperature Contours prefix instead
28+
of just the required spec-id format'
29+
- Filled contours use mark_square which creates a pixelated appearance rather than
30+
smooth continuous fills
31+
- Altair cannot natively produce true isolines without external contour computation
32+
image_description: The plot displays a geographic contour map of European temperature
33+
data. The visualization covers Europe from approximately 25°W to 55°E longitude
34+
and 30°N to 72°N latitude. Temperature values are shown using a red-yellow-blue
35+
color scheme (reversed, so red=warm, blue=cold) with values ranging from -10°C
36+
to 30°C. The southern regions (Mediterranean) appear in deep red (~25-30°C), transitioning
37+
through orange and yellow in central Europe (~10-20°C), to cooler blue tones in
38+
northern Scandinavia (~0-5°C). A light gray geographic basemap shows world country
39+
outlines. Contour "lines" are represented as small black circles marking temperature
40+
isoline boundaries. Temperature labels (0°C, 10°C, 20°C) appear with white stroke
41+
for contrast. A colorbar legend on the right displays the temperature scale with
42+
"Temperature (°C)" title. Longitude labels (20°W to 50°E) appear along the bottom
43+
edge, and latitude labels (35°N to 70°N) along the left edge. The title reads
44+
"European Temperature Contours · contour-map-geographic · altair · pyplots.ai"
45+
which includes an extra prefix before the required spec-id format. The temperature
46+
data uses a dense grid of square markers to create a filled contour effect, with
47+
black circle markers overlaid to indicate contour level boundaries.
48+
criteria_checklist:
49+
visual_quality:
50+
score: 32
51+
max: 40
52+
items:
53+
- id: VQ-01
54+
name: Text Legibility
55+
score: 8
56+
max: 10
57+
passed: true
58+
comment: Title and colorbar labels readable; contour labels with white stroke;
59+
axis labels slightly small
60+
- id: VQ-02
61+
name: No Overlap
62+
score: 8
63+
max: 8
64+
passed: true
65+
comment: No overlapping text elements
66+
- id: VQ-03
67+
name: Element Visibility
68+
score: 6
69+
max: 8
70+
passed: true
71+
comment: Temperature grid visible but contour lines as scattered circles are
72+
not true isolines
73+
- id: VQ-04
74+
name: Color Accessibility
75+
score: 5
76+
max: 5
77+
passed: true
78+
comment: Red-yellow-blue diverging scheme is colorblind-friendly
79+
- id: VQ-05
80+
name: Layout Balance
81+
score: 3
82+
max: 5
83+
passed: true
84+
comment: Good canvas utilization but data doesn't fully extend to map edges
85+
- id: VQ-06
86+
name: Axis Labels
87+
score: 2
88+
max: 2
89+
passed: true
90+
comment: Latitude/longitude labels with directional indicators
91+
- id: VQ-07
92+
name: Grid & Legend
93+
score: 0
94+
max: 2
95+
passed: false
96+
comment: No visible grid lines on map
97+
spec_compliance:
98+
score: 19
99+
max: 25
100+
items:
101+
- id: SC-01
102+
name: Plot Type
103+
score: 6
104+
max: 8
105+
passed: true
106+
comment: Shows temperature over geography but contour lines are point-based
107+
approximations
108+
- id: SC-02
109+
name: Data Mapping
110+
score: 5
111+
max: 5
112+
passed: true
113+
comment: Latitude/longitude correctly mapped with Mercator projection
114+
- id: SC-03
115+
name: Required Features
116+
score: 2
117+
max: 5
118+
passed: false
119+
comment: Missing true smooth contour lines - uses scattered points instead
120+
- id: SC-04
121+
name: Data Range
122+
score: 3
123+
max: 3
124+
passed: true
125+
comment: Full temperature range visible from -10 to 30°C
126+
- id: SC-05
127+
name: Legend Accuracy
128+
score: 2
129+
max: 2
130+
passed: true
131+
comment: Colorbar correctly labeled with Temperature (°C)
132+
- id: SC-06
133+
name: Title Format
134+
score: 1
135+
max: 2
136+
passed: false
137+
comment: Title includes extra prefix instead of just spec-id format
138+
data_quality:
139+
score: 18
140+
max: 20
141+
items:
142+
- id: DQ-01
143+
name: Feature Coverage
144+
score: 7
145+
max: 8
146+
passed: true
147+
comment: Shows temperature gradient with regional variations (Alps, Scandinavia)
148+
- id: DQ-02
149+
name: Realistic Context
150+
score: 7
151+
max: 7
152+
passed: true
153+
comment: European temperature distribution is realistic and neutral
154+
- id: DQ-03
155+
name: Appropriate Scale
156+
score: 4
157+
max: 5
158+
passed: true
159+
comment: Temperature range -10 to 30°C is plausible for European climate
160+
code_quality:
161+
score: 9
162+
max: 10
163+
items:
164+
- id: CQ-01
165+
name: KISS Structure
166+
score: 3
167+
max: 3
168+
passed: true
169+
comment: 'Linear code structure: imports → data → plot → save'
170+
- id: CQ-02
171+
name: Reproducibility
172+
score: 3
173+
max: 3
174+
passed: true
175+
comment: Uses np.random.seed(42)
176+
- id: CQ-03
177+
name: Clean Imports
178+
score: 2
179+
max: 2
180+
passed: true
181+
comment: Only altair, numpy, pandas imported
182+
- id: CQ-04
183+
name: No Deprecated API
184+
score: 1
185+
max: 1
186+
passed: true
187+
comment: Uses current Altair API
188+
- id: CQ-05
189+
name: Output Correct
190+
score: 0
191+
max: 1
192+
passed: false
193+
comment: Saves plot.png but also plot.html
194+
library_features:
195+
score: 4
196+
max: 5
197+
items:
198+
- id: LF-01
199+
name: Distinctive Features
200+
score: 4
201+
max: 5
202+
passed: true
203+
comment: Uses geographic projections, topo_feature, layer composition, tooltips;
204+
lacks native contour support
205+
verdict: APPROVED
206+
impl_tags:
207+
dependencies: []
208+
techniques:
209+
- layer-composition
210+
- hover-tooltips
211+
- html-export
212+
- annotations
213+
patterns:
214+
- data-generation
215+
- matrix-construction
216+
- iteration-over-groups
217+
dataprep: []
218+
styling:
219+
- custom-colormap
220+
- alpha-blending

0 commit comments

Comments
 (0)