Skip to content

Commit bcf9d42

Browse files
feat(altair): implement contour-basic (#5331)
## Implementation: `contour-basic` - python/altair Implements the **python/altair** version of `contour-basic`. **File:** `plots/contour-basic/implementations/python/altair.py` **Parent Issue:** #855 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24867307514)* --------- 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 b48a7b3 commit bcf9d42

2 files changed

Lines changed: 265 additions & 176 deletions

File tree

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,146 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
contour-basic: Basic Contour Plot
3-
Library: altair 6.0.0 | Python 3.13.11
4-
Quality: 98/100 | Created: 2025-12-23
3+
Library: altair 6.1.0 | Python 3.14.4
4+
Quality: 86/100 | Updated: 2026-04-24
55
"""
66

7-
import altair as alt
8-
import numpy as np
9-
import pandas as pd
7+
import importlib
8+
import os
9+
import sys
1010

1111

12-
# Data - 2D Gaussian function on meshgrid
13-
np.random.seed(42)
14-
x = np.linspace(-3, 3, 80)
15-
y = np.linspace(-3, 3, 80)
12+
# Drop script directory from sys.path so the `altair` package resolves, not this file
13+
sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.dirname(os.path.abspath(__file__))]
14+
alt = importlib.import_module("altair")
15+
np = importlib.import_module("numpy")
16+
pd = importlib.import_module("pandas")
17+
18+
19+
# Theme tokens
20+
THEME = os.getenv("ANYPLOT_THEME", "light")
21+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
22+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
23+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
24+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
25+
26+
# Data — simulated topographic elevation map of a 10km x 10km mountain region
27+
x = np.linspace(0, 10, 80)
28+
y = np.linspace(0, 10, 80)
1629
X, Y = np.meshgrid(x, y)
1730

18-
# Two overlapping Gaussian peaks for interesting contour patterns
19-
Z = np.exp(-((X - 1) ** 2 + (Y - 1) ** 2)) + 0.8 * np.exp(-((X + 1) ** 2 + (Y + 0.5) ** 2))
31+
elevation = (
32+
850 * np.exp(-((X - 7) ** 2 + (Y - 7) ** 2) / 4.0)
33+
+ 550 * np.exp(-((X - 2.5) ** 2 + (Y - 3) ** 2) / 3.0)
34+
- 180 * np.exp(-((X - 5) ** 2 + (Y - 5) ** 2) / 8.0)
35+
+ 12 * X
36+
+ 350
37+
)
2038

21-
# Flatten to DataFrame for filled contour background
22-
df_fill = pd.DataFrame({"x": X.ravel(), "y": Y.ravel(), "z": Z.ravel()})
39+
df_fill = pd.DataFrame({"x": X.ravel(), "y": Y.ravel(), "elevation": elevation.ravel()})
2340

24-
# Extract contour line segments using marching squares algorithm
25-
levels = np.linspace(0.1, 1.6, 10)
41+
# Contour line segments via marching squares
42+
levels = np.arange(400, 1251, 100)
2643
segments = []
2744

2845
for level in levels:
2946
for i in range(len(y) - 1):
3047
for j in range(len(x) - 1):
31-
# Get 4 corners of cell
32-
z00, z10, z01, z11 = Z[i, j], Z[i + 1, j], Z[i, j + 1], Z[i + 1, j + 1]
48+
z00, z10, z01, z11 = elevation[i, j], elevation[i + 1, j], elevation[i, j + 1], elevation[i + 1, j + 1]
3349
x0, x1, y0, y1 = x[j], x[j + 1], y[i], y[i + 1]
3450

35-
# Calculate which corners are above/below level (binary case)
3651
case = int(z00 >= level) | (int(z10 >= level) << 1) | (int(z01 >= level) << 2) | (int(z11 >= level) << 3)
3752

3853
if case == 0 or case == 15:
3954
continue
4055

41-
# Find edge crossings via linear interpolation
4256
edges = []
43-
if (case & 1) != (case >> 1) & 1: # Bottom edge (z00 to z10)
57+
if (case & 1) != (case >> 1) & 1:
4458
t = (level - z00) / (z10 - z00) if z10 != z00 else 0.5
4559
edges.append((x0, y0 + t * (y1 - y0)))
46-
if (case >> 1) & 1 != (case >> 3) & 1: # Right edge (z10 to z11)
60+
if (case >> 1) & 1 != (case >> 3) & 1:
4761
t = (level - z10) / (z11 - z10) if z11 != z10 else 0.5
4862
edges.append((x0 + t * (x1 - x0), y1))
49-
if (case >> 2) & 1 != (case >> 3) & 1: # Top edge (z01 to z11)
63+
if (case >> 2) & 1 != (case >> 3) & 1:
5064
t = (level - z01) / (z11 - z01) if z11 != z01 else 0.5
5165
edges.append((x1, y0 + t * (y1 - y0)))
52-
if (case & 1) != (case >> 2) & 1: # Left edge (z00 to z01)
66+
if (case & 1) != (case >> 2) & 1:
5367
t = (level - z00) / (z01 - z00) if z01 != z00 else 0.5
5468
edges.append((x0 + t * (x1 - x0), y0))
5569

56-
# Connect edge crossings as line segments
5770
if len(edges) >= 2:
5871
segments.append(
59-
{"x1": edges[0][0], "y1": edges[0][1], "x2": edges[1][0], "y2": edges[1][1], "level": level}
72+
{"x1": edges[0][0], "y1": edges[0][1], "x2": edges[1][0], "y2": edges[1][1], "level": float(level)}
6073
)
61-
if len(edges) == 4: # Saddle point case
74+
if len(edges) == 4:
6275
segments.append(
63-
{"x1": edges[2][0], "y1": edges[2][1], "x2": edges[3][0], "y2": edges[3][1], "level": level}
76+
{
77+
"x1": edges[2][0],
78+
"y1": edges[2][1],
79+
"x2": edges[3][0],
80+
"y2": edges[3][1],
81+
"level": float(level),
82+
}
6483
)
6584

6685
df_lines = pd.DataFrame(segments)
6786

68-
# Filled contour background using rect marks
87+
# Plot — filled contour background
6988
filled = (
7089
alt.Chart(df_fill)
7190
.mark_rect()
7291
.encode(
73-
x=alt.X("x:Q", bin=alt.Bin(maxbins=80), title="X Value"),
74-
y=alt.Y("y:Q", bin=alt.Bin(maxbins=80), title="Y Value"),
92+
x=alt.X("x:Q", bin=alt.Bin(maxbins=80), title="Distance East (km)"),
93+
y=alt.Y("y:Q", bin=alt.Bin(maxbins=80), title="Distance North (km)"),
7594
color=alt.Color(
76-
"mean(z):Q",
95+
"mean(elevation):Q",
7796
scale=alt.Scale(scheme="viridis"),
78-
title="Z Value",
79-
legend=alt.Legend(titleFontSize=20, labelFontSize=18, gradientLength=500, gradientThickness=30),
97+
title="Elevation (m)",
98+
legend=alt.Legend(titleFontSize=22, labelFontSize=18, gradientLength=600, gradientThickness=28),
8099
),
81100
)
82101
)
83102

84-
# Contour lines overlaid with contrasting white color for visibility
103+
# Thin contour lines (all levels)
85104
lines = (
86105
alt.Chart(df_lines)
87-
.mark_rule(strokeWidth=3, opacity=1.0, color="white")
106+
.mark_rule(strokeWidth=1.2, opacity=0.35, color="white")
88107
.encode(x="x1:Q", y="y1:Q", x2="x2:Q", y2="y2:Q")
89108
)
90109

91-
# Combine filled regions and contour lines (lines layered on top)
92-
chart = (
93-
(filled + lines)
94-
.properties(width=1420, height=785, title="contour-basic · altair · pyplots.ai")
95-
.configure_title(fontSize=32, anchor="middle")
96-
.configure_axis(labelFontSize=20, titleFontSize=24, tickSize=10)
97-
.configure_view(strokeWidth=0)
110+
# Emphasised contour lines every 200 m
111+
major_mask = (df_lines["level"] % 200 == 0) if not df_lines.empty else pd.Series([], dtype=bool)
112+
df_major = df_lines[major_mask].copy() if not df_lines.empty else df_lines
113+
114+
major_lines = (
115+
alt.Chart(df_major)
116+
.mark_rule(strokeWidth=2.2, opacity=0.95, color="white")
117+
.encode(x="x1:Q", y="y1:Q", x2="x2:Q", y2="y2:Q")
98118
)
99119

100-
# Save as PNG (scale_factor=3.0 for 4800x2700)
101-
chart.save("plot.png", scale_factor=3.0)
120+
chart = (
121+
(filled + lines + major_lines)
122+
.properties(
123+
width=1420,
124+
height=785,
125+
title=alt.Title(
126+
"Mountain Terrain · contour-basic · altair · anyplot.ai", fontSize=28, anchor="middle", color=INK
127+
),
128+
background=PAGE_BG,
129+
)
130+
.configure_view(fill=PAGE_BG, stroke=None)
131+
.configure_axis(
132+
domainColor=INK_SOFT,
133+
tickColor=INK_SOFT,
134+
gridColor=INK,
135+
gridOpacity=0.10,
136+
labelColor=INK_SOFT,
137+
titleColor=INK,
138+
labelFontSize=18,
139+
titleFontSize=22,
140+
tickSize=8,
141+
)
142+
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
143+
)
102144

103-
# Save interactive HTML
104-
chart.save("plot.html")
145+
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
146+
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)