diff --git a/plots/contour-3d/implementations/bokeh.py b/plots/contour-3d/implementations/bokeh.py new file mode 100644 index 0000000000..a981671125 --- /dev/null +++ b/plots/contour-3d/implementations/bokeh.py @@ -0,0 +1,340 @@ +""" pyplots.ai +contour-3d: 3D Contour Plot +Library: bokeh 3.8.2 | Python 3.13.11 +Quality: 88/100 | Created: 2026-01-07 +""" + +import matplotlib.pyplot as plt +import numpy as np +from bokeh.io import export_png, save +from bokeh.models import ColorBar, HoverTool, Label, LinearColorMapper, Range1d +from bokeh.palettes import Viridis256 +from bokeh.plotting import figure +from bokeh.resources import CDN + + +# Data - create a surface with multiple features to demonstrate contour visualization +np.random.seed(42) + +# Grid setup - 40x40 for clear contour detail +n_points = 40 +x = np.linspace(-3, 3, n_points) +y = np.linspace(-3, 3, n_points) +X, Y = np.meshgrid(x, y) + +# Surface function: Gaussian peaks with different heights +# Primary peak at origin, secondary peak offset, creating interesting contours +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) + +# Normalize Z for better visualization +z_min, z_max = Z.min(), Z.max() + +# 3D to 2D isometric projection parameters +elev_rad = np.radians(25) +azim_rad = np.radians(45) +cos_azim = np.cos(azim_rad) +sin_azim = np.sin(azim_rad) +sin_elev = np.sin(elev_rad) +cos_elev = np.cos(elev_rad) + +# Scale Z for visualization (height range 0-2 for proportion) +Z_scaled = (Z - z_min) / (z_max - z_min) * 2 + +# Project surface grid to 2D using inline isometric projection +X_proj = np.zeros_like(X) +Z_proj = np.zeros_like(X) +Depth = np.zeros_like(X) + +for i in range(n_points): + for j in range(n_points): + x_3d, y_3d, z_3d = X[i, j], Y[i, j], Z_scaled[i, j] + x_rot = x_3d * cos_azim - y_3d * sin_azim + y_rot = x_3d * sin_azim + y_3d * cos_azim + X_proj[i, j] = x_rot + Z_proj[i, j] = y_rot * sin_elev + z_3d * cos_elev + Depth[i, j] = y_rot * cos_elev - z_3d * sin_elev + +# Generate contour levels for the surface +n_levels = 10 +levels = np.linspace(z_min, z_max, n_levels) + +# Color mapper for z-values +color_mapper = LinearColorMapper(palette=Viridis256, low=z_min, high=z_max) + +# Collect surface quads for rendering (sorted by depth for painter's algorithm) +surface_quads = [] +for i in range(n_points - 1): + for j in range(n_points - 1): + # Quad corners in projected 2D + xs = [X_proj[i, j], X_proj[i + 1, j], X_proj[i + 1, j + 1], X_proj[i, j + 1]] + ys = [Z_proj[i, j], Z_proj[i + 1, j], Z_proj[i + 1, j + 1], Z_proj[i, j + 1]] + + # Average depth for sorting + avg_depth = (Depth[i, j] + Depth[i + 1, j] + Depth[i + 1, j + 1] + Depth[i, j + 1]) / 4 + + # Average Z value for coloring + avg_z = (Z[i, j] + Z[i + 1, j] + Z[i + 1, j + 1] + Z[i, j + 1]) / 4 + + # Map Z value to color index + idx = int((avg_z - z_min) / (z_max - z_min) * 255) + idx = max(0, min(255, idx)) + color = Viridis256[idx] + + surface_quads.append((avg_depth, xs, ys, color, avg_z)) + +# Sort by depth (back to front - painter's algorithm) +surface_quads.sort(key=lambda q: q[0], reverse=True) + +# Generate contour lines using matplotlib's contour (no external dependency) +fig_temp, ax_temp = plt.subplots() +contour_set = ax_temp.contour(x, y, Z, levels=levels) +plt.close(fig_temp) + +# Generate 3D contour lines on the surface +contour_lines_3d = [] +for level_idx, level in enumerate(levels): + z_height = (level - z_min) / (z_max - z_min) * 2 # Scale to same range as surface + + # Get contour segments from matplotlib + for _path in contour_set.get_paths() if hasattr(contour_set, "get_paths") else []: + pass # Matplotlib 3.8+ uses different API + + # Use allsegs attribute for contour data + if hasattr(contour_set, "allsegs") and level_idx < len(contour_set.allsegs): + for segment in contour_set.allsegs[level_idx]: + if len(segment) > 1: + line_xs = [] + line_ys = [] + line_depths = [] + for pt in segment: + x_pt, y_pt = pt + x_rot = x_pt * cos_azim - y_pt * sin_azim + y_rot = x_pt * sin_azim + y_pt * cos_azim + line_xs.append(x_rot) + line_ys.append(y_rot * sin_elev + z_height * cos_elev) + line_depths.append(y_rot * cos_elev - z_height * sin_elev) + + avg_depth = np.mean(line_depths) + contour_lines_3d.append((avg_depth, line_xs, line_ys, level_idx, level)) + +# Generate projected contours on the base plane (z=0) +base_contours = [] +for level_idx, level in enumerate(levels): + if hasattr(contour_set, "allsegs") and level_idx < len(contour_set.allsegs): + for segment in contour_set.allsegs[level_idx]: + if len(segment) > 1: + line_xs = [] + line_ys = [] + line_depths = [] + for pt in segment: + x_pt, y_pt = pt + # Inline projection with z=0 for base plane + x_rot = x_pt * cos_azim - y_pt * sin_azim + y_rot = x_pt * sin_azim + y_pt * cos_azim + line_xs.append(x_rot) + line_ys.append(y_rot * sin_elev) + line_depths.append(y_rot * cos_elev) + + avg_depth = np.mean(line_depths) + # Color based on level + idx = int(level_idx * 255 / (n_levels - 1)) + color = Viridis256[idx] + base_contours.append((avg_depth, line_xs, line_ys, color, level)) + +# Create Bokeh figure +p = figure( + width=4800, + height=2700, + title="contour-3d · bokeh · pyplots.ai", + toolbar_location="right", + tools="pan,wheel_zoom,box_zoom,reset,save", +) + +# Hide default axes for 3D visualization +p.xaxis.visible = False +p.yaxis.visible = False + +# Draw base plane contours first (behind surface) - more visible now +for _depth, xs, ys, color, _level_val in sorted(base_contours, key=lambda c: c[0], reverse=True): + p.line(x=xs, y=ys, line_color=color, line_width=3.5, line_alpha=0.65, line_dash="dashed") + +# Draw surface quads with hover support +for _depth, xs, ys, color, _avg_z in surface_quads: + p.patch(x=xs, y=ys, fill_color=color, line_color="#444444", line_width=0.5, line_alpha=0.3, alpha=0.9) + +# Draw 3D contour lines on surface (on top of surface) +for _depth, xs, ys, _level_idx, _level_val in sorted(contour_lines_3d, key=lambda c: c[0], reverse=False): + p.line(x=xs, y=ys, line_color="#222222", line_width=2.5, line_alpha=0.8) + +# Calculate plot range from all elements +all_x_coords = [x for quad in surface_quads for x in quad[1]] +all_y_coords = [y for quad in surface_quads for y in quad[2]] + +x_min_plot, x_max_plot = min(all_x_coords), max(all_x_coords) +y_min_plot, y_max_plot = min(all_y_coords), max(all_y_coords) + +x_pad = (x_max_plot - x_min_plot) * 0.15 +y_pad = (y_max_plot - y_min_plot) * 0.12 + +p.x_range = Range1d(x_min_plot - x_pad * 1.2, x_max_plot + x_pad * 2.0) +p.y_range = Range1d(y_min_plot - y_pad * 0.8, y_max_plot + y_pad * 1.4) + +# Custom 3D axis lines (inline projection) +ox, oy, oz = -3.5, -3.5, 0 +origin_x = ox * cos_azim - oy * sin_azim +origin_y = (ox * sin_azim + oy * cos_azim) * sin_elev + oz * cos_elev + +axis_color = "#333333" +axis_width = 6 + +# X-axis end point +ax, ay, az = 3.5, -3.5, 0 +x_axis_end_x = ax * cos_azim - ay * sin_azim +x_axis_end_y = (ax * sin_azim + ay * cos_azim) * sin_elev + az * cos_elev +p.line(x=[origin_x, x_axis_end_x], y=[origin_y, x_axis_end_y], line_color=axis_color, line_width=axis_width) + +# Y-axis end point +bx, by, bz = -3.5, 3.5, 0 +y_axis_end_x = bx * cos_azim - by * sin_azim +y_axis_end_y = (bx * sin_azim + by * cos_azim) * sin_elev + bz * cos_elev +p.line(x=[origin_x, y_axis_end_x], y=[origin_y, y_axis_end_y], line_color=axis_color, line_width=axis_width) + +# Z-axis end point +cx, cy, cz = -3.5, -3.5, 2.5 +z_axis_end_x = cx * cos_azim - cy * sin_azim +z_axis_end_y = (cx * sin_azim + cy * cos_azim) * sin_elev + cz * cos_elev +p.line(x=[origin_x, z_axis_end_x], y=[origin_y, z_axis_end_y], line_color=axis_color, line_width=axis_width) + +# Axis arrows +arrow_size = 0.25 + +# X-axis arrow +x_dir = np.array([x_axis_end_x - origin_x, x_axis_end_y - origin_y]) +x_dir = x_dir / np.linalg.norm(x_dir) +x_perp = np.array([-x_dir[1], x_dir[0]]) +p.patch( + x=[ + x_axis_end_x, + x_axis_end_x - arrow_size * x_dir[0] + arrow_size * 0.5 * x_perp[0], + x_axis_end_x - arrow_size * x_dir[0] - arrow_size * 0.5 * x_perp[0], + ], + y=[ + x_axis_end_y, + x_axis_end_y - arrow_size * x_dir[1] + arrow_size * 0.5 * x_perp[1], + x_axis_end_y - arrow_size * x_dir[1] - arrow_size * 0.5 * x_perp[1], + ], + fill_color=axis_color, + line_color=axis_color, +) + +# Y-axis arrow +y_dir = np.array([y_axis_end_x - origin_x, y_axis_end_y - origin_y]) +y_dir = y_dir / np.linalg.norm(y_dir) +y_perp = np.array([-y_dir[1], y_dir[0]]) +p.patch( + x=[ + y_axis_end_x, + y_axis_end_x - arrow_size * y_dir[0] + arrow_size * 0.5 * y_perp[0], + y_axis_end_x - arrow_size * y_dir[0] - arrow_size * 0.5 * y_perp[0], + ], + y=[ + y_axis_end_y, + y_axis_end_y - arrow_size * y_dir[1] + arrow_size * 0.5 * y_perp[1], + y_axis_end_y - arrow_size * y_dir[1] - arrow_size * 0.5 * y_perp[1], + ], + fill_color=axis_color, + line_color=axis_color, +) + +# Z-axis arrow +z_dir = np.array([z_axis_end_x - origin_x, z_axis_end_y - origin_y]) +z_dir = z_dir / np.linalg.norm(z_dir) +z_perp = np.array([-z_dir[1], z_dir[0]]) +p.patch( + x=[ + z_axis_end_x, + z_axis_end_x - arrow_size * z_dir[0] + arrow_size * 0.5 * z_perp[0], + z_axis_end_x - arrow_size * z_dir[0] - arrow_size * 0.5 * z_perp[0], + ], + y=[ + z_axis_end_y, + z_axis_end_y - arrow_size * z_dir[1] + arrow_size * 0.5 * z_perp[1], + z_axis_end_y - arrow_size * z_dir[1] - arrow_size * 0.5 * z_perp[1], + ], + fill_color=axis_color, + line_color=axis_color, +) + +# Axis labels - repositioned for better visibility with larger font +x_label = Label( + x=x_axis_end_x + 0.25, + y=x_axis_end_y - 0.55, + text="Position X (units)", + text_font_size="44pt", + text_color="#222222", + text_font_style="bold", +) +p.add_layout(x_label) + +y_label = Label( + x=y_axis_end_x - 1.5, + y=y_axis_end_y + 0.35, + text="Position Y (units)", + text_font_size="44pt", + text_color="#222222", + text_font_style="bold", +) +p.add_layout(y_label) + +z_label = Label( + x=z_axis_end_x + 0.3, + y=z_axis_end_y + 0.2, + text="Amplitude (a.u.)", + text_font_size="44pt", + text_color="#222222", + text_font_style="bold", +) +p.add_layout(z_label) + +# Add colorbar for surface amplitude +color_bar = ColorBar( + color_mapper=color_mapper, + width=80, + location=(0, 0), + title="Amplitude (a.u.)", + title_text_font_size="40pt", + major_label_text_font_size="32pt", + title_standoff=30, + margin=50, + padding=25, +) +p.add_layout(color_bar, "right") + +# Title styling - larger for better visibility +p.title.text_font_size = "68pt" +p.title.text_font_style = "bold" +p.title.text_color = "#222222" + +# Grid styling - subtle +p.xgrid.grid_line_color = "#dddddd" +p.ygrid.grid_line_color = "#dddddd" +p.xgrid.grid_line_alpha = 0.25 +p.ygrid.grid_line_alpha = 0.25 +p.xgrid.grid_line_dash = [6, 4] +p.ygrid.grid_line_dash = [6, 4] + +# Background styling +p.background_fill_color = "#f8f8f8" +p.border_fill_color = "white" +p.outline_line_color = None +p.min_border_right = 250 + +# Add hover tool for interactive exploration (Bokeh distinctive feature) +hover = HoverTool(tooltips=[("Position", "($x{0.00}, $y{0.00})")], mode="mouse") +p.add_tools(hover) + +# Save PNG +export_png(p, filename="plot.png") + +# Save HTML for interactive version +save(p, filename="plot.html", resources=CDN, title="contour-3d · bokeh · pyplots.ai") diff --git a/plots/contour-3d/metadata/bokeh.yaml b/plots/contour-3d/metadata/bokeh.yaml new file mode 100644 index 0000000000..c22e4912a0 --- /dev/null +++ b/plots/contour-3d/metadata/bokeh.yaml @@ -0,0 +1,215 @@ +library: bokeh +specification_id: contour-3d +created: '2026-01-07T20:25:01Z' +updated: '2026-01-07T20:51:18Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 20795211097 +issue: 3230 +python_version: 3.13.11 +library_version: 3.8.2 +preview_url: https://storage.googleapis.com/pyplots-images/plots/contour-3d/bokeh/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/contour-3d/bokeh/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/contour-3d/bokeh/plot.html +quality_score: 88 +review: + strengths: + - Creative 3D projection implementation using painter's algorithm for depth sorting + - Excellent use of Bokeh's ColorBar, HoverTool, and HTML export features + - Clean isometric projection with custom axis arrows + - Base plane contour projection provides good reference per spec requirements + - Viridis colormap is accessible and perceptually uniform + weaknesses: + - Text elements (title, axis labels, colorbar) appear small relative to the high-resolution + output + - Uses deprecated matplotlib allsegs API; should use get_paths() for future compatibility + - Axis labels lack descriptive context (just X, Y, Z instead of meaningful variable + names with units) + - Layout leaves significant white space at the bottom of the canvas + image_description: The plot displays a 3D contour surface visualization using an + isometric projection technique. The surface shows two Gaussian peaks - a primary + peak near the center-back and a smaller secondary peak offset to the upper right. + The surface is colored using the Viridis colormap (purple/dark blue at low values + transitioning through green to yellow at peaks). Dark contour lines are drawn + directly on the 3D surface at regular intervals, clearly showing the isolines. + Dashed contour lines are projected onto the base plane beneath the surface for + reference. Three custom axis arrows point in the X, Y, and Z directions from the + back-left corner. A colorbar on the right displays "Height (Z)" ranging from approximately + 0.2 to 1.0. The title "contour-3d · bokeh · pyplots.ai" appears at the top left. + criteria_checklist: + visual_quality: + score: 34 + max: 40 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 10 + passed: true + comment: Title and axis labels readable but relatively small for 4800x2700 + output + - id: VQ-02 + name: No Overlap + score: 8 + max: 8 + passed: true + comment: No overlapping text elements; all labels positioned clearly + - id: VQ-03 + name: Element Visibility + score: 8 + max: 8 + passed: true + comment: Surface quads, contour lines, and base plane contours all clearly + visible + - id: VQ-04 + name: Color Accessibility + score: 5 + max: 5 + passed: true + comment: Viridis colormap is colorblind-safe with excellent perceptual uniformity + - id: VQ-05 + name: Layout Balance + score: 3 + max: 5 + passed: true + comment: Plot well-centered but significant white space at bottom + - id: VQ-06 + name: Axis Labels + score: 1 + max: 2 + passed: true + comment: Labels show X, Y, Z but lack descriptive context with units + - id: VQ-07 + name: Grid & Legend + score: 2 + max: 2 + passed: true + comment: Subtle grid lines, colorbar well-placed on right + spec_compliance: + score: 23 + max: 25 + items: + - id: SC-01 + name: Plot Type + score: 8 + max: 8 + passed: true + comment: Correct 3D contour plot with surface and isolines + - id: SC-02 + name: Data Mapping + score: 5 + max: 5 + passed: true + comment: X, Y, Z correctly assigned; proper meshgrid structure + - id: SC-03 + name: Required Features + score: 4 + max: 5 + passed: true + comment: Has surface contours, base plane projection, colorbar; rotation not + applicable for static PNG + - id: SC-04 + name: Data Range + score: 3 + max: 3 + passed: true + comment: All data visible within the projected view + - id: SC-05 + name: Legend Accuracy + score: 2 + max: 2 + passed: true + comment: Colorbar correctly shows amplitude scale + - id: SC-06 + name: Title Format + score: 1 + max: 2 + passed: true + comment: Format correct but title text appears too small in final render + data_quality: + score: 18 + max: 20 + items: + - id: DQ-01 + name: Feature Coverage + score: 7 + max: 8 + passed: true + comment: Shows multiple peaks with different heights, clear contour gradients + - id: DQ-02 + name: Realistic Context + score: 6 + max: 7 + passed: true + comment: Generic amplitude surface suitable for many domains + - id: DQ-03 + name: Appropriate Scale + score: 5 + max: 5 + passed: true + comment: Normalized 0-1 amplitude scale is appropriate; grid points (40x40) + provide clear detail + code_quality: + score: 8 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: true + comment: Code is linear but uses loops for projection; necessarily complex + for 3D simulation + - id: CQ-02 + name: Reproducibility + score: 3 + max: 3 + passed: true + comment: np.random.seed(42) set correctly + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: All imports used; matplotlib only for contour extraction + - id: CQ-04 + name: No Deprecated API + score: 0 + max: 1 + passed: false + comment: Uses contour_set.allsegs which is deprecated in matplotlib 3.8+ + - id: CQ-05 + name: Output Correct + score: 1 + max: 1 + passed: true + comment: Saves as plot.png and plot.html + library_features: + score: 5 + max: 5 + items: + - id: LF-01 + name: Distinctive Features + score: 5 + max: 5 + passed: true + comment: Implements HoverTool, ColorBar, LinearColorMapper, export_png, interactive + HTML export with CDN resources + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - colorbar + - 3d-projection + - hover-tooltips + - html-export + - patches + patterns: + - data-generation + - matrix-construction + - iteration-over-groups + dataprep: + - normalization + styling: + - custom-colormap + - alpha-blending + - grid-styling