|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | | -import altair as alt |
8 | | -import numpy as np |
9 | | -import pandas as pd |
| 7 | +import importlib |
| 8 | +import os |
| 9 | +import sys |
10 | 10 |
|
11 | 11 |
|
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) |
16 | 29 | X, Y = np.meshgrid(x, y) |
17 | 30 |
|
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 | +) |
20 | 38 |
|
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()}) |
23 | 40 |
|
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) |
26 | 43 | segments = [] |
27 | 44 |
|
28 | 45 | for level in levels: |
29 | 46 | for i in range(len(y) - 1): |
30 | 47 | 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] |
33 | 49 | x0, x1, y0, y1 = x[j], x[j + 1], y[i], y[i + 1] |
34 | 50 |
|
35 | | - # Calculate which corners are above/below level (binary case) |
36 | 51 | case = int(z00 >= level) | (int(z10 >= level) << 1) | (int(z01 >= level) << 2) | (int(z11 >= level) << 3) |
37 | 52 |
|
38 | 53 | if case == 0 or case == 15: |
39 | 54 | continue |
40 | 55 |
|
41 | | - # Find edge crossings via linear interpolation |
42 | 56 | edges = [] |
43 | | - if (case & 1) != (case >> 1) & 1: # Bottom edge (z00 to z10) |
| 57 | + if (case & 1) != (case >> 1) & 1: |
44 | 58 | t = (level - z00) / (z10 - z00) if z10 != z00 else 0.5 |
45 | 59 | 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: |
47 | 61 | t = (level - z10) / (z11 - z10) if z11 != z10 else 0.5 |
48 | 62 | 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: |
50 | 64 | t = (level - z01) / (z11 - z01) if z11 != z01 else 0.5 |
51 | 65 | 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: |
53 | 67 | t = (level - z00) / (z01 - z00) if z01 != z00 else 0.5 |
54 | 68 | edges.append((x0 + t * (x1 - x0), y0)) |
55 | 69 |
|
56 | | - # Connect edge crossings as line segments |
57 | 70 | if len(edges) >= 2: |
58 | 71 | 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)} |
60 | 73 | ) |
61 | | - if len(edges) == 4: # Saddle point case |
| 74 | + if len(edges) == 4: |
62 | 75 | 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 | + } |
64 | 83 | ) |
65 | 84 |
|
66 | 85 | df_lines = pd.DataFrame(segments) |
67 | 86 |
|
68 | | -# Filled contour background using rect marks |
| 87 | +# Plot — filled contour background |
69 | 88 | filled = ( |
70 | 89 | alt.Chart(df_fill) |
71 | 90 | .mark_rect() |
72 | 91 | .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)"), |
75 | 94 | color=alt.Color( |
76 | | - "mean(z):Q", |
| 95 | + "mean(elevation):Q", |
77 | 96 | 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), |
80 | 99 | ), |
81 | 100 | ) |
82 | 101 | ) |
83 | 102 |
|
84 | | -# Contour lines overlaid with contrasting white color for visibility |
| 103 | +# Thin contour lines (all levels) |
85 | 104 | lines = ( |
86 | 105 | 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") |
88 | 107 | .encode(x="x1:Q", y="y1:Q", x2="x2:Q", y2="y2:Q") |
89 | 108 | ) |
90 | 109 |
|
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") |
98 | 118 | ) |
99 | 119 |
|
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 | +) |
102 | 144 |
|
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