Skip to content

Commit 22a6e22

Browse files
feat(altair): implement contour-density (#2585)
## Implementation: `contour-density` - altair Implements the **altair** version of `contour-density`. **File:** `plots/contour-density/implementations/altair.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20593353049)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 62756f7 commit 22a6e22

2 files changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""pyplots.ai
2+
contour-density: Density Contour Plot
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 88/100 | Created: 2025-12-30
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
from scipy.stats import gaussian_kde
11+
12+
13+
# Data - simulating temperature and humidity measurements showing natural clusters
14+
np.random.seed(42)
15+
16+
# Create three distinct clusters representing different climate conditions
17+
n1 = 150
18+
temp1 = np.random.normal(15, 4, n1)
19+
humidity1 = np.random.normal(30, 8, n1)
20+
21+
n2 = 200
22+
temp2 = np.random.normal(25, 5, n2)
23+
humidity2 = np.random.normal(55, 10, n2)
24+
25+
n3 = 150
26+
temp3 = np.random.normal(38, 4, n3)
27+
humidity3 = np.random.normal(75, 8, n3)
28+
29+
# Combine data
30+
temperature = np.concatenate([temp1, temp2, temp3])
31+
humidity = np.concatenate([humidity1, humidity2, humidity3])
32+
33+
# Compute 2D KDE for density estimation
34+
xy = np.vstack([temperature, humidity])
35+
kde = gaussian_kde(xy)
36+
37+
# Create grid for density estimation
38+
n_grid = 60
39+
x_grid = np.linspace(temperature.min() - 5, temperature.max() + 5, n_grid)
40+
y_grid = np.linspace(humidity.min() - 5, humidity.max() + 5, n_grid)
41+
xx, yy = np.meshgrid(x_grid, y_grid)
42+
positions = np.vstack([xx.ravel(), yy.ravel()])
43+
z = kde(positions).reshape(xx.shape)
44+
45+
# Create filled contour visualization using binned density levels
46+
n_levels = 10
47+
z_min, z_max = z.min(), z.max()
48+
levels = np.linspace(z_min, z_max, n_levels + 1)
49+
z_binned = np.digitize(z, levels) - 1
50+
z_binned = np.clip(z_binned, 0, n_levels - 1)
51+
52+
# Create grid cell data for filled contours
53+
step_x = x_grid[1] - x_grid[0]
54+
step_y = y_grid[1] - y_grid[0]
55+
56+
grid_data = pd.DataFrame({"x": xx.ravel(), "y": yy.ravel(), "density_level": z_binned.ravel()})
57+
58+
# Create filled contour chart using mark_rect with binning
59+
x_domain = [float(temperature.min() - 6), float(temperature.max() + 6)]
60+
y_domain = [float(humidity.min() - 6), float(humidity.max() + 6)]
61+
62+
filled_contours = (
63+
alt.Chart(grid_data)
64+
.mark_rect()
65+
.encode(
66+
x=alt.X(
67+
"x:Q",
68+
bin=alt.Bin(step=step_x),
69+
scale=alt.Scale(domain=x_domain),
70+
title="Temperature (°C)",
71+
axis=alt.Axis(labelFontSize=18, titleFontSize=22, grid=True, gridOpacity=0.3),
72+
),
73+
y=alt.Y(
74+
"y:Q",
75+
bin=alt.Bin(step=step_y),
76+
scale=alt.Scale(domain=y_domain),
77+
title="Humidity (%)",
78+
axis=alt.Axis(labelFontSize=18, titleFontSize=22, grid=True, gridOpacity=0.3),
79+
),
80+
color=alt.Color(
81+
"mean(density_level):Q",
82+
scale=alt.Scale(scheme="blues"),
83+
title="Density",
84+
legend=alt.Legend(titleFontSize=20, labelFontSize=16, gradientLength=400, gradientThickness=25),
85+
),
86+
)
87+
)
88+
89+
# Add contour line effect by layering darker outlines at level boundaries
90+
contour_data = []
91+
for i in range(n_grid - 1):
92+
for j in range(n_grid - 1):
93+
current = z_binned[i, j]
94+
# Check if adjacent cells have different levels (boundary)
95+
if i < n_grid - 1 and z_binned[i + 1, j] != current:
96+
contour_data.append({"x": x_grid[j], "y": (y_grid[i] + y_grid[i + 1]) / 2, "level": current})
97+
if j < n_grid - 1 and z_binned[i, j + 1] != current:
98+
contour_data.append({"x": (x_grid[j] + x_grid[j + 1]) / 2, "y": y_grid[i], "level": current})
99+
100+
if contour_data:
101+
contour_df = pd.DataFrame(contour_data)
102+
contour_points = (
103+
alt.Chart(contour_df)
104+
.mark_circle(size=8, opacity=0.6, color="#1a4d80")
105+
.encode(x=alt.X("x:Q", scale=alt.Scale(domain=x_domain)), y=alt.Y("y:Q", scale=alt.Scale(domain=y_domain)))
106+
)
107+
chart = alt.layer(filled_contours, contour_points)
108+
else:
109+
chart = filled_contours
110+
111+
# Configure the chart with proper sizing
112+
chart = chart.properties(
113+
width=1600, height=900, title=alt.Title(text="contour-density · altair · pyplots.ai", fontSize=28, anchor="middle")
114+
).configure_view(strokeWidth=0)
115+
116+
# Save as PNG (1600 * 3 = 4800, 900 * 3 = 2700)
117+
chart.save("plot.png", scale_factor=3.0)
118+
119+
# Save as interactive HTML
120+
chart.save("plot.html")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: altair
2+
specification_id: contour-density
3+
created: '2025-12-30T09:33:23Z'
4+
updated: '2025-12-30T10:27:05Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20593353049
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/contour-density/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/contour-density/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/contour-density/altair/plot.html
13+
quality_score: 88
14+
review:
15+
strengths:
16+
- Good visual design with blues color scheme providing clear density gradient
17+
- Proper axis labels with units (Temperature °C, Humidity %)
18+
- Realistic climate data with three distinct clusters demonstrating density patterns
19+
- Well-proportioned layout with balanced margins and proper legend placement
20+
- Correct title format following pyplots.ai convention
21+
weaknesses:
22+
- Contour lines (mark_rule) are not visible in the output - the filled density background
23+
renders but the isoline overlay does not appear
24+
- Code violates KISS principle by using custom extract_contours function instead
25+
of simpler approach

0 commit comments

Comments
 (0)