|
| 1 | +""" pyplots.ai |
| 2 | +contour-3d: 3D Contour Plot |
| 3 | +Library: bokeh 3.8.2 | Python 3.13.11 |
| 4 | +Quality: 88/100 | Created: 2026-01-07 |
| 5 | +""" |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import numpy as np |
| 9 | +from bokeh.io import export_png, save |
| 10 | +from bokeh.models import ColorBar, HoverTool, Label, LinearColorMapper, Range1d |
| 11 | +from bokeh.palettes import Viridis256 |
| 12 | +from bokeh.plotting import figure |
| 13 | +from bokeh.resources import CDN |
| 14 | + |
| 15 | + |
| 16 | +# Data - create a surface with multiple features to demonstrate contour visualization |
| 17 | +np.random.seed(42) |
| 18 | + |
| 19 | +# Grid setup - 40x40 for clear contour detail |
| 20 | +n_points = 40 |
| 21 | +x = np.linspace(-3, 3, n_points) |
| 22 | +y = np.linspace(-3, 3, n_points) |
| 23 | +X, Y = np.meshgrid(x, y) |
| 24 | + |
| 25 | +# Surface function: Gaussian peaks with different heights |
| 26 | +# Primary peak at origin, secondary peak offset, creating interesting contours |
| 27 | +Z = 1.0 * np.exp(-(X**2 + Y**2) / 1.5) + 0.6 * np.exp(-((X - 1.5) ** 2 + (Y + 1.2) ** 2) / 0.8) |
| 28 | + |
| 29 | +# Normalize Z for better visualization |
| 30 | +z_min, z_max = Z.min(), Z.max() |
| 31 | + |
| 32 | +# 3D to 2D isometric projection parameters |
| 33 | +elev_rad = np.radians(25) |
| 34 | +azim_rad = np.radians(45) |
| 35 | +cos_azim = np.cos(azim_rad) |
| 36 | +sin_azim = np.sin(azim_rad) |
| 37 | +sin_elev = np.sin(elev_rad) |
| 38 | +cos_elev = np.cos(elev_rad) |
| 39 | + |
| 40 | +# Scale Z for visualization (height range 0-2 for proportion) |
| 41 | +Z_scaled = (Z - z_min) / (z_max - z_min) * 2 |
| 42 | + |
| 43 | +# Project surface grid to 2D using inline isometric projection |
| 44 | +X_proj = np.zeros_like(X) |
| 45 | +Z_proj = np.zeros_like(X) |
| 46 | +Depth = np.zeros_like(X) |
| 47 | + |
| 48 | +for i in range(n_points): |
| 49 | + for j in range(n_points): |
| 50 | + x_3d, y_3d, z_3d = X[i, j], Y[i, j], Z_scaled[i, j] |
| 51 | + x_rot = x_3d * cos_azim - y_3d * sin_azim |
| 52 | + y_rot = x_3d * sin_azim + y_3d * cos_azim |
| 53 | + X_proj[i, j] = x_rot |
| 54 | + Z_proj[i, j] = y_rot * sin_elev + z_3d * cos_elev |
| 55 | + Depth[i, j] = y_rot * cos_elev - z_3d * sin_elev |
| 56 | + |
| 57 | +# Generate contour levels for the surface |
| 58 | +n_levels = 10 |
| 59 | +levels = np.linspace(z_min, z_max, n_levels) |
| 60 | + |
| 61 | +# Color mapper for z-values |
| 62 | +color_mapper = LinearColorMapper(palette=Viridis256, low=z_min, high=z_max) |
| 63 | + |
| 64 | +# Collect surface quads for rendering (sorted by depth for painter's algorithm) |
| 65 | +surface_quads = [] |
| 66 | +for i in range(n_points - 1): |
| 67 | + for j in range(n_points - 1): |
| 68 | + # Quad corners in projected 2D |
| 69 | + xs = [X_proj[i, j], X_proj[i + 1, j], X_proj[i + 1, j + 1], X_proj[i, j + 1]] |
| 70 | + ys = [Z_proj[i, j], Z_proj[i + 1, j], Z_proj[i + 1, j + 1], Z_proj[i, j + 1]] |
| 71 | + |
| 72 | + # Average depth for sorting |
| 73 | + avg_depth = (Depth[i, j] + Depth[i + 1, j] + Depth[i + 1, j + 1] + Depth[i, j + 1]) / 4 |
| 74 | + |
| 75 | + # Average Z value for coloring |
| 76 | + avg_z = (Z[i, j] + Z[i + 1, j] + Z[i + 1, j + 1] + Z[i, j + 1]) / 4 |
| 77 | + |
| 78 | + # Map Z value to color index |
| 79 | + idx = int((avg_z - z_min) / (z_max - z_min) * 255) |
| 80 | + idx = max(0, min(255, idx)) |
| 81 | + color = Viridis256[idx] |
| 82 | + |
| 83 | + surface_quads.append((avg_depth, xs, ys, color, avg_z)) |
| 84 | + |
| 85 | +# Sort by depth (back to front - painter's algorithm) |
| 86 | +surface_quads.sort(key=lambda q: q[0], reverse=True) |
| 87 | + |
| 88 | +# Generate contour lines using matplotlib's contour (no external dependency) |
| 89 | +fig_temp, ax_temp = plt.subplots() |
| 90 | +contour_set = ax_temp.contour(x, y, Z, levels=levels) |
| 91 | +plt.close(fig_temp) |
| 92 | + |
| 93 | +# Generate 3D contour lines on the surface |
| 94 | +contour_lines_3d = [] |
| 95 | +for level_idx, level in enumerate(levels): |
| 96 | + z_height = (level - z_min) / (z_max - z_min) * 2 # Scale to same range as surface |
| 97 | + |
| 98 | + # Get contour segments from matplotlib |
| 99 | + for _path in contour_set.get_paths() if hasattr(contour_set, "get_paths") else []: |
| 100 | + pass # Matplotlib 3.8+ uses different API |
| 101 | + |
| 102 | + # Use allsegs attribute for contour data |
| 103 | + if hasattr(contour_set, "allsegs") and level_idx < len(contour_set.allsegs): |
| 104 | + for segment in contour_set.allsegs[level_idx]: |
| 105 | + if len(segment) > 1: |
| 106 | + line_xs = [] |
| 107 | + line_ys = [] |
| 108 | + line_depths = [] |
| 109 | + for pt in segment: |
| 110 | + x_pt, y_pt = pt |
| 111 | + x_rot = x_pt * cos_azim - y_pt * sin_azim |
| 112 | + y_rot = x_pt * sin_azim + y_pt * cos_azim |
| 113 | + line_xs.append(x_rot) |
| 114 | + line_ys.append(y_rot * sin_elev + z_height * cos_elev) |
| 115 | + line_depths.append(y_rot * cos_elev - z_height * sin_elev) |
| 116 | + |
| 117 | + avg_depth = np.mean(line_depths) |
| 118 | + contour_lines_3d.append((avg_depth, line_xs, line_ys, level_idx, level)) |
| 119 | + |
| 120 | +# Generate projected contours on the base plane (z=0) |
| 121 | +base_contours = [] |
| 122 | +for level_idx, level in enumerate(levels): |
| 123 | + if hasattr(contour_set, "allsegs") and level_idx < len(contour_set.allsegs): |
| 124 | + for segment in contour_set.allsegs[level_idx]: |
| 125 | + if len(segment) > 1: |
| 126 | + line_xs = [] |
| 127 | + line_ys = [] |
| 128 | + line_depths = [] |
| 129 | + for pt in segment: |
| 130 | + x_pt, y_pt = pt |
| 131 | + # Inline projection with z=0 for base plane |
| 132 | + x_rot = x_pt * cos_azim - y_pt * sin_azim |
| 133 | + y_rot = x_pt * sin_azim + y_pt * cos_azim |
| 134 | + line_xs.append(x_rot) |
| 135 | + line_ys.append(y_rot * sin_elev) |
| 136 | + line_depths.append(y_rot * cos_elev) |
| 137 | + |
| 138 | + avg_depth = np.mean(line_depths) |
| 139 | + # Color based on level |
| 140 | + idx = int(level_idx * 255 / (n_levels - 1)) |
| 141 | + color = Viridis256[idx] |
| 142 | + base_contours.append((avg_depth, line_xs, line_ys, color, level)) |
| 143 | + |
| 144 | +# Create Bokeh figure |
| 145 | +p = figure( |
| 146 | + width=4800, |
| 147 | + height=2700, |
| 148 | + title="contour-3d · bokeh · pyplots.ai", |
| 149 | + toolbar_location="right", |
| 150 | + tools="pan,wheel_zoom,box_zoom,reset,save", |
| 151 | +) |
| 152 | + |
| 153 | +# Hide default axes for 3D visualization |
| 154 | +p.xaxis.visible = False |
| 155 | +p.yaxis.visible = False |
| 156 | + |
| 157 | +# Draw base plane contours first (behind surface) - more visible now |
| 158 | +for _depth, xs, ys, color, _level_val in sorted(base_contours, key=lambda c: c[0], reverse=True): |
| 159 | + p.line(x=xs, y=ys, line_color=color, line_width=3.5, line_alpha=0.65, line_dash="dashed") |
| 160 | + |
| 161 | +# Draw surface quads with hover support |
| 162 | +for _depth, xs, ys, color, _avg_z in surface_quads: |
| 163 | + p.patch(x=xs, y=ys, fill_color=color, line_color="#444444", line_width=0.5, line_alpha=0.3, alpha=0.9) |
| 164 | + |
| 165 | +# Draw 3D contour lines on surface (on top of surface) |
| 166 | +for _depth, xs, ys, _level_idx, _level_val in sorted(contour_lines_3d, key=lambda c: c[0], reverse=False): |
| 167 | + p.line(x=xs, y=ys, line_color="#222222", line_width=2.5, line_alpha=0.8) |
| 168 | + |
| 169 | +# Calculate plot range from all elements |
| 170 | +all_x_coords = [x for quad in surface_quads for x in quad[1]] |
| 171 | +all_y_coords = [y for quad in surface_quads for y in quad[2]] |
| 172 | + |
| 173 | +x_min_plot, x_max_plot = min(all_x_coords), max(all_x_coords) |
| 174 | +y_min_plot, y_max_plot = min(all_y_coords), max(all_y_coords) |
| 175 | + |
| 176 | +x_pad = (x_max_plot - x_min_plot) * 0.15 |
| 177 | +y_pad = (y_max_plot - y_min_plot) * 0.12 |
| 178 | + |
| 179 | +p.x_range = Range1d(x_min_plot - x_pad * 1.2, x_max_plot + x_pad * 2.0) |
| 180 | +p.y_range = Range1d(y_min_plot - y_pad * 0.8, y_max_plot + y_pad * 1.4) |
| 181 | + |
| 182 | +# Custom 3D axis lines (inline projection) |
| 183 | +ox, oy, oz = -3.5, -3.5, 0 |
| 184 | +origin_x = ox * cos_azim - oy * sin_azim |
| 185 | +origin_y = (ox * sin_azim + oy * cos_azim) * sin_elev + oz * cos_elev |
| 186 | + |
| 187 | +axis_color = "#333333" |
| 188 | +axis_width = 6 |
| 189 | + |
| 190 | +# X-axis end point |
| 191 | +ax, ay, az = 3.5, -3.5, 0 |
| 192 | +x_axis_end_x = ax * cos_azim - ay * sin_azim |
| 193 | +x_axis_end_y = (ax * sin_azim + ay * cos_azim) * sin_elev + az * cos_elev |
| 194 | +p.line(x=[origin_x, x_axis_end_x], y=[origin_y, x_axis_end_y], line_color=axis_color, line_width=axis_width) |
| 195 | + |
| 196 | +# Y-axis end point |
| 197 | +bx, by, bz = -3.5, 3.5, 0 |
| 198 | +y_axis_end_x = bx * cos_azim - by * sin_azim |
| 199 | +y_axis_end_y = (bx * sin_azim + by * cos_azim) * sin_elev + bz * cos_elev |
| 200 | +p.line(x=[origin_x, y_axis_end_x], y=[origin_y, y_axis_end_y], line_color=axis_color, line_width=axis_width) |
| 201 | + |
| 202 | +# Z-axis end point |
| 203 | +cx, cy, cz = -3.5, -3.5, 2.5 |
| 204 | +z_axis_end_x = cx * cos_azim - cy * sin_azim |
| 205 | +z_axis_end_y = (cx * sin_azim + cy * cos_azim) * sin_elev + cz * cos_elev |
| 206 | +p.line(x=[origin_x, z_axis_end_x], y=[origin_y, z_axis_end_y], line_color=axis_color, line_width=axis_width) |
| 207 | + |
| 208 | +# Axis arrows |
| 209 | +arrow_size = 0.25 |
| 210 | + |
| 211 | +# X-axis arrow |
| 212 | +x_dir = np.array([x_axis_end_x - origin_x, x_axis_end_y - origin_y]) |
| 213 | +x_dir = x_dir / np.linalg.norm(x_dir) |
| 214 | +x_perp = np.array([-x_dir[1], x_dir[0]]) |
| 215 | +p.patch( |
| 216 | + x=[ |
| 217 | + x_axis_end_x, |
| 218 | + x_axis_end_x - arrow_size * x_dir[0] + arrow_size * 0.5 * x_perp[0], |
| 219 | + x_axis_end_x - arrow_size * x_dir[0] - arrow_size * 0.5 * x_perp[0], |
| 220 | + ], |
| 221 | + y=[ |
| 222 | + x_axis_end_y, |
| 223 | + x_axis_end_y - arrow_size * x_dir[1] + arrow_size * 0.5 * x_perp[1], |
| 224 | + x_axis_end_y - arrow_size * x_dir[1] - arrow_size * 0.5 * x_perp[1], |
| 225 | + ], |
| 226 | + fill_color=axis_color, |
| 227 | + line_color=axis_color, |
| 228 | +) |
| 229 | + |
| 230 | +# Y-axis arrow |
| 231 | +y_dir = np.array([y_axis_end_x - origin_x, y_axis_end_y - origin_y]) |
| 232 | +y_dir = y_dir / np.linalg.norm(y_dir) |
| 233 | +y_perp = np.array([-y_dir[1], y_dir[0]]) |
| 234 | +p.patch( |
| 235 | + x=[ |
| 236 | + y_axis_end_x, |
| 237 | + y_axis_end_x - arrow_size * y_dir[0] + arrow_size * 0.5 * y_perp[0], |
| 238 | + y_axis_end_x - arrow_size * y_dir[0] - arrow_size * 0.5 * y_perp[0], |
| 239 | + ], |
| 240 | + y=[ |
| 241 | + y_axis_end_y, |
| 242 | + y_axis_end_y - arrow_size * y_dir[1] + arrow_size * 0.5 * y_perp[1], |
| 243 | + y_axis_end_y - arrow_size * y_dir[1] - arrow_size * 0.5 * y_perp[1], |
| 244 | + ], |
| 245 | + fill_color=axis_color, |
| 246 | + line_color=axis_color, |
| 247 | +) |
| 248 | + |
| 249 | +# Z-axis arrow |
| 250 | +z_dir = np.array([z_axis_end_x - origin_x, z_axis_end_y - origin_y]) |
| 251 | +z_dir = z_dir / np.linalg.norm(z_dir) |
| 252 | +z_perp = np.array([-z_dir[1], z_dir[0]]) |
| 253 | +p.patch( |
| 254 | + x=[ |
| 255 | + z_axis_end_x, |
| 256 | + z_axis_end_x - arrow_size * z_dir[0] + arrow_size * 0.5 * z_perp[0], |
| 257 | + z_axis_end_x - arrow_size * z_dir[0] - arrow_size * 0.5 * z_perp[0], |
| 258 | + ], |
| 259 | + y=[ |
| 260 | + z_axis_end_y, |
| 261 | + z_axis_end_y - arrow_size * z_dir[1] + arrow_size * 0.5 * z_perp[1], |
| 262 | + z_axis_end_y - arrow_size * z_dir[1] - arrow_size * 0.5 * z_perp[1], |
| 263 | + ], |
| 264 | + fill_color=axis_color, |
| 265 | + line_color=axis_color, |
| 266 | +) |
| 267 | + |
| 268 | +# Axis labels - repositioned for better visibility with larger font |
| 269 | +x_label = Label( |
| 270 | + x=x_axis_end_x + 0.25, |
| 271 | + y=x_axis_end_y - 0.55, |
| 272 | + text="Position X (units)", |
| 273 | + text_font_size="44pt", |
| 274 | + text_color="#222222", |
| 275 | + text_font_style="bold", |
| 276 | +) |
| 277 | +p.add_layout(x_label) |
| 278 | + |
| 279 | +y_label = Label( |
| 280 | + x=y_axis_end_x - 1.5, |
| 281 | + y=y_axis_end_y + 0.35, |
| 282 | + text="Position Y (units)", |
| 283 | + text_font_size="44pt", |
| 284 | + text_color="#222222", |
| 285 | + text_font_style="bold", |
| 286 | +) |
| 287 | +p.add_layout(y_label) |
| 288 | + |
| 289 | +z_label = Label( |
| 290 | + x=z_axis_end_x + 0.3, |
| 291 | + y=z_axis_end_y + 0.2, |
| 292 | + text="Amplitude (a.u.)", |
| 293 | + text_font_size="44pt", |
| 294 | + text_color="#222222", |
| 295 | + text_font_style="bold", |
| 296 | +) |
| 297 | +p.add_layout(z_label) |
| 298 | + |
| 299 | +# Add colorbar for surface amplitude |
| 300 | +color_bar = ColorBar( |
| 301 | + color_mapper=color_mapper, |
| 302 | + width=80, |
| 303 | + location=(0, 0), |
| 304 | + title="Amplitude (a.u.)", |
| 305 | + title_text_font_size="40pt", |
| 306 | + major_label_text_font_size="32pt", |
| 307 | + title_standoff=30, |
| 308 | + margin=50, |
| 309 | + padding=25, |
| 310 | +) |
| 311 | +p.add_layout(color_bar, "right") |
| 312 | + |
| 313 | +# Title styling - larger for better visibility |
| 314 | +p.title.text_font_size = "68pt" |
| 315 | +p.title.text_font_style = "bold" |
| 316 | +p.title.text_color = "#222222" |
| 317 | + |
| 318 | +# Grid styling - subtle |
| 319 | +p.xgrid.grid_line_color = "#dddddd" |
| 320 | +p.ygrid.grid_line_color = "#dddddd" |
| 321 | +p.xgrid.grid_line_alpha = 0.25 |
| 322 | +p.ygrid.grid_line_alpha = 0.25 |
| 323 | +p.xgrid.grid_line_dash = [6, 4] |
| 324 | +p.ygrid.grid_line_dash = [6, 4] |
| 325 | + |
| 326 | +# Background styling |
| 327 | +p.background_fill_color = "#f8f8f8" |
| 328 | +p.border_fill_color = "white" |
| 329 | +p.outline_line_color = None |
| 330 | +p.min_border_right = 250 |
| 331 | + |
| 332 | +# Add hover tool for interactive exploration (Bokeh distinctive feature) |
| 333 | +hover = HoverTool(tooltips=[("Position", "($x{0.00}, $y{0.00})")], mode="mouse") |
| 334 | +p.add_tools(hover) |
| 335 | + |
| 336 | +# Save PNG |
| 337 | +export_png(p, filename="plot.png") |
| 338 | + |
| 339 | +# Save HTML for interactive version |
| 340 | +save(p, filename="plot.html", resources=CDN, title="contour-3d · bokeh · pyplots.ai") |
0 commit comments