|
| 1 | +""" |
| 2 | +surface-basic: Basic 3D Surface Plot |
| 3 | +Library: altair |
| 4 | +""" |
| 5 | + |
| 6 | +import altair as alt |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | + |
| 10 | + |
| 11 | +# Data - create a smooth surface z = sin(x) * cos(y) |
| 12 | +np.random.seed(42) |
| 13 | + |
| 14 | +# Grid setup - 40x40 for smooth surface |
| 15 | +n_points = 40 |
| 16 | +x = np.linspace(-3, 3, n_points) |
| 17 | +y = np.linspace(-3, 3, n_points) |
| 18 | +X, Y = np.meshgrid(x, y) |
| 19 | + |
| 20 | +# Surface function |
| 21 | +Z = np.sin(X) * np.cos(Y) |
| 22 | + |
| 23 | +# 3D to 2D projection (elevation=25, azimuth=45) |
| 24 | +elev_rad = np.radians(25) |
| 25 | +azim_rad = np.radians(45) |
| 26 | + |
| 27 | +# Rotation around z-axis (azimuth) |
| 28 | +X_rot = X * np.cos(azim_rad) - Y * np.sin(azim_rad) |
| 29 | +Y_rot = X * np.sin(azim_rad) + Y * np.cos(azim_rad) |
| 30 | + |
| 31 | +# Rotation around x-axis (elevation) and project |
| 32 | +X_proj = X_rot |
| 33 | +Z_proj = Y_rot * np.sin(elev_rad) + Z * np.cos(elev_rad) |
| 34 | + |
| 35 | +# Build rectangles using actual quad corners for proper filling |
| 36 | +rect_data = [] |
| 37 | +for i in range(n_points - 1): |
| 38 | + for j in range(n_points - 1): |
| 39 | + # Use actual quad corners for x and y extents |
| 40 | + x1 = min(X_proj[i, j], X_proj[i, j + 1], X_proj[i + 1, j], X_proj[i + 1, j + 1]) |
| 41 | + x2 = max(X_proj[i, j], X_proj[i, j + 1], X_proj[i + 1, j], X_proj[i + 1, j + 1]) |
| 42 | + y1 = min(Z_proj[i, j], Z_proj[i, j + 1], Z_proj[i + 1, j], Z_proj[i + 1, j + 1]) |
| 43 | + y2 = max(Z_proj[i, j], Z_proj[i, j + 1], Z_proj[i + 1, j], Z_proj[i + 1, j + 1]) |
| 44 | + |
| 45 | + # Average z for color (original Z, not projected) |
| 46 | + avg_z = (Z[i, j] + Z[i, j + 1] + Z[i + 1, j + 1] + Z[i + 1, j]) / 4 |
| 47 | + |
| 48 | + # Depth for sorting (painter's algorithm - back to front) |
| 49 | + depth = (Y_rot[i, j] + Y_rot[i, j + 1] + Y_rot[i + 1, j + 1] + Y_rot[i + 1, j]) / 4 |
| 50 | + |
| 51 | + rect_data.append({"x1": x1, "x2": x2, "y1": y1, "y2": y2, "z": avg_z, "depth": depth}) |
| 52 | + |
| 53 | +# Sort by depth (back to front - painter's algorithm) |
| 54 | +rect_data.sort(key=lambda r: r["depth"], reverse=True) |
| 55 | + |
| 56 | +# Assign order for rendering |
| 57 | +for idx, r in enumerate(rect_data): |
| 58 | + r["order"] = idx |
| 59 | + |
| 60 | +df_rects = pd.DataFrame(rect_data) |
| 61 | + |
| 62 | +# Create surface chart with filled rectangles |
| 63 | +surface = ( |
| 64 | + alt.Chart(df_rects) |
| 65 | + .mark_rect(strokeWidth=0.5, stroke="#306998", strokeOpacity=0.3) |
| 66 | + .encode( |
| 67 | + x=alt.X("x1:Q", axis=alt.Axis(title="X (projected)", labelFontSize=18, titleFontSize=22)), |
| 68 | + y=alt.Y("y1:Q", axis=alt.Axis(title="Z (height)", labelFontSize=18, titleFontSize=22)), |
| 69 | + x2=alt.X2("x2:Q"), |
| 70 | + y2=alt.Y2("y2:Q"), |
| 71 | + color=alt.Color( |
| 72 | + "z:Q", |
| 73 | + scale=alt.Scale(scheme="viridis"), |
| 74 | + legend=alt.Legend(title="Z Value", titleFontSize=20, labelFontSize=16), |
| 75 | + ), |
| 76 | + order=alt.Order("order:Q"), |
| 77 | + ) |
| 78 | +) |
| 79 | + |
| 80 | +# Combine into final chart |
| 81 | +chart = ( |
| 82 | + surface.properties(width=1600, height=900, title=alt.Title("surface-basic · altair · pyplots.ai", fontSize=28)) |
| 83 | + .configure_axis(grid=True, gridOpacity=0.3, gridDash=[6, 4]) |
| 84 | + .configure_view(strokeWidth=0) |
| 85 | +) |
| 86 | + |
| 87 | +# Save outputs |
| 88 | +chart.save("plot.png", scale_factor=3.0) |
| 89 | +chart.save("plot.html") |
0 commit comments