{"spec_id":"wireframe-3d-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nwireframe-3d-basic: Basic 3D Wireframe Plot\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Fix shadowing: remove current directory from path before importing bokeh\nwhile sys.path and (sys.path[0] == \"\" or sys.path[0] == os.path.dirname(__file__)):\n    sys.path.pop(0)\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColorBar, ColumnDataSource, HoverTool, Label, LinearColorMapper, Range1d\nfrom bokeh.plotting import figure\nfrom bokeh.transform import transform\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nBRAND = \"#009E73\"\n\n\ndef _lerp_hex(c0, c1, t):\n    \"\"\"Interpolate between two hex colors at t in [0, 1].\"\"\"\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    r, g, b = (int(round(a + (b - a) * t)) for a, b in ((r0, r1), (g0, g1), (b0, b1)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\ndef project_point(px, py, pz, elev_rad, azim_rad):\n    \"\"\"Project a single 3D point to 2D screen space (azimuth rotation, then elevation tilt).\"\"\"\n    x_rot = px * np.cos(azim_rad) - py * np.sin(azim_rad)\n    y_rot = px * np.sin(azim_rad) + py * np.cos(azim_rad)\n    x2d = x_rot\n    y2d = y_rot * np.sin(elev_rad) + pz * np.cos(elev_rad)\n    return x2d, y2d\n\n\ndef project_grid(gx, gy, gz, elev_rad, azim_rad):\n    \"\"\"Vectorized projection of grid arrays to 2D screen space.\"\"\"\n    x_rot = gx * np.cos(azim_rad) - gy * np.sin(azim_rad)\n    y_rot = gx * np.sin(azim_rad) + gy * np.cos(azim_rad)\n    x2d = x_rot\n    y2d = y_rot * np.sin(elev_rad) + gz * np.cos(elev_rad)\n    return x2d, y2d\n\n\ndef draw_axis_ticks(fig, origin_xy, end_xy, axis_max, color, text_color, width, n_ticks=4, tick_length=0.2):\n    \"\"\"Draw evenly-spaced perpendicular tick marks with numeric value labels along a projected 2D axis segment.\"\"\"\n    ox, oy = origin_xy\n    ex, ey = end_xy\n    direction = np.array([ex - ox, ey - oy])\n    norm = np.linalg.norm(direction)\n    if norm == 0:\n        return\n    direction = direction / norm\n    perp = np.array([-direction[1], direction[0]])\n    for i in range(1, n_ticks + 1):\n        t = i / n_ticks\n        tx = ox + t * (ex - ox)\n        ty = oy + t * (ey - oy)\n        fig.line(\n            x=[tx - tick_length * perp[0], tx + tick_length * perp[0]],\n            y=[ty - tick_length * perp[1], ty + tick_length * perp[1]],\n            line_color=color,\n            line_width=width,\n        )\n        fig.add_layout(\n            Label(\n                x=tx + tick_length * 1.8 * perp[0],\n                y=ty + tick_length * 1.8 * perp[1],\n                text=f\"{axis_max * t:.2g}\",\n                text_font_size=\"24pt\",\n                text_color=text_color,\n            )\n        )\n\n\n# Data - ripple surface z = sin(sqrt(x^2 + y^2))\nn_points = 30\nx = np.linspace(-4, 4, n_points)\ny = np.linspace(-4, 4, n_points)\nX, Y = np.meshgrid(x, y)\nR = np.sqrt(X**2 + Y**2)\nZ = np.sin(R)\n\n# 3D to 2D projection (elevation=30, azimuth=45)\nelev_rad = np.radians(30)\nazim_rad = np.radians(45)\nX_proj, Z_proj = project_grid(X, Y, Z, elev_rad, azim_rad)\n\n# Wireframe lines along x-direction (rows) and y-direction (columns)\nrow_xs = [X_proj[i, :].tolist() for i in range(n_points)]\nrow_ys = [Z_proj[i, :].tolist() for i in range(n_points)]\ncol_xs = [X_proj[:, j].tolist() for j in range(n_points)]\ncol_ys = [Z_proj[:, j].tolist() for j in range(n_points)]\n\nall_xs = row_xs + col_xs\nall_ys = row_ys + col_ys\n\n# Height-based coloring: average z per line, mapped through the imprint_seq\n# ramp (brand green -> blue) so troughs and peaks of the ripple read at a glance.\nrow_avg_z = [float(np.mean(Z[i, :])) for i in range(n_points)]\ncol_avg_z = [float(np.mean(Z[:, j])) for j in range(n_points)]\navg_z = row_avg_z + col_avg_z\ndirections = [\"row (x-slice)\"] * n_points + [\"column (y-slice)\"] * n_points\nline_idx = list(range(n_points)) * 2\n\nz_min, z_max = float(Z.min()), float(Z.max())\nANYPLOT_SEQ256 = [_lerp_hex(BRAND, \"#4467A3\", t / 255.0) for t in range(256)]\ncolor_mapper = LinearColorMapper(palette=ANYPLOT_SEQ256, low=z_min, high=z_max)\n\nsource = ColumnDataSource(data={\"xs\": all_xs, \"ys\": all_ys, \"direction\": directions, \"idx\": line_idx, \"avg_z\": avg_z})\n\n# Create Bokeh figure - 3200x1800 landscape (canonical canvas)\np = figure(\n    width=3200,\n    height=1800,\n    title=\"wireframe-3d-basic · python · bokeh · anyplot.ai\",\n    toolbar_location=None,  # bokeh's default toolbar adds ~30-50px above the canvas\n    tools=\"\",\n    min_border_top=110,  # room for 50pt title\n)\n\n# Hide default axes since we're doing custom 3D axis visualization\np.xaxis.visible = False\np.yaxis.visible = False\n\n# Draw wireframe, colored by average height per line, with hover tooltips\nwireframe = p.multi_line(\n    xs=\"xs\", ys=\"ys\", source=source, line_color=transform(\"avg_z\", color_mapper), line_width=2.5, line_alpha=0.85\n)\np.add_tools(\n    HoverTool(\n        renderers=[wireframe],\n        tooltips=[(\"Slice\", \"@direction\"), (\"Grid index\", \"@idx\"), (\"Avg height (z)\", \"@avg_z{0.00}\")],\n        line_policy=\"nearest\",\n    )\n)\n\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    title=\"z height\",\n    title_text_color=INK_SOFT,\n    title_text_font_size=\"30pt\",\n    major_label_text_color=INK_SOFT,\n    major_label_text_font_size=\"26pt\",\n    background_fill_color=PAGE_BG,\n    label_standoff=12,\n    width=24,\n    location=(0, 0),\n)\np.add_layout(color_bar, \"right\")\n\n# Custom 3D axis lines positioned at the projected origin\norigin_x, origin_y = project_point(0, 0, 0, elev_rad, azim_rad)\n\naxis_color = INK_SOFT\naxis_width = 4\n\n# Draw all three schematic arms at a shared visual length spanning the plotted\n# range (matching the x,y data extent) so the compass reads as a real axis\n# rather than a tiny floating stub; each arm's tick labels below are scaled\n# to that dimension's own true data extent so the numbers stay meaningful.\naxis_length = float(np.max(np.abs(X)))\nx_extent = float(np.max(np.abs(X)))\ny_extent = float(np.max(np.abs(Y)))\nz_extent = float(np.max(np.abs(Z)))\n\nx_axis_end_x, x_axis_end_y = project_point(axis_length, 0, 0, elev_rad, azim_rad)\ny_axis_end_x, y_axis_end_y = project_point(0, axis_length, 0, elev_rad, azim_rad)\nz_axis_end_x, z_axis_end_y = project_point(0, 0, axis_length, elev_rad, azim_rad)\n\n# Set appropriate ranges with padding for axes and labels — must also cover\n# the schematic axis arms above, not just the mesh, or the Z arm (tallest\n# projected element) gets clipped against the fixed Range1d bounds.\nx_min = min(min(min(xs) for xs in all_xs), origin_x, x_axis_end_x, y_axis_end_x, z_axis_end_x)\nx_max = max(max(max(xs) for xs in all_xs), origin_x, x_axis_end_x, y_axis_end_x, z_axis_end_x)\ny_min = min(min(min(ys) for ys in all_ys), origin_y, x_axis_end_y, y_axis_end_y, z_axis_end_y)\ny_max = max(max(max(ys) for ys in all_ys), origin_y, x_axis_end_y, y_axis_end_y, z_axis_end_y)\n\nx_pad = (x_max - x_min) * 0.20\ny_pad = (y_max - y_min) * 0.25\n\np.x_range = Range1d(x_min - x_pad, x_max + x_pad)\np.y_range = Range1d(y_min - y_pad * 1.2, y_max + y_pad)\n\n# Draw axis lines from projected origin\np.line(x=[origin_x, x_axis_end_x], y=[origin_y, x_axis_end_y], line_color=axis_color, line_width=axis_width)\np.line(x=[origin_x, y_axis_end_x], y=[origin_y, y_axis_end_y], line_color=axis_color, line_width=axis_width)\np.line(x=[origin_x, z_axis_end_x], y=[origin_y, z_axis_end_y], line_color=axis_color, line_width=axis_width)\n\n# Add axis tick marks with real coordinate-value labels\norigin_xy = (origin_x, origin_y)\ndraw_axis_ticks(p, origin_xy, (x_axis_end_x, x_axis_end_y), x_extent, axis_color, INK_SOFT, 2)\ndraw_axis_ticks(p, origin_xy, (y_axis_end_x, y_axis_end_y), y_extent, axis_color, INK_SOFT, 2)\ndraw_axis_ticks(p, origin_xy, (z_axis_end_x, z_axis_end_y), z_extent, axis_color, INK_SOFT, 2)\n\n# Add axis labels\nx_label = Label(\n    x=x_axis_end_x + 0.3, y=x_axis_end_y - 0.3, text=\"X\", text_font_size=\"42pt\", text_color=INK, text_font_style=\"bold\"\n)\np.add_layout(x_label)\n\ny_label = Label(\n    x=y_axis_end_x - 0.7, y=y_axis_end_y + 0.3, text=\"Y\", text_font_size=\"42pt\", text_color=INK, text_font_style=\"bold\"\n)\np.add_layout(y_label)\n\nz_label = Label(\n    x=z_axis_end_x + 0.3, y=z_axis_end_y + 0.2, text=\"Z\", text_font_size=\"42pt\", text_color=INK, text_font_style=\"bold\"\n)\np.add_layout(z_label)\n\n# Add formula annotation in center-left area where it will be visible\nformula_label = Label(\n    x=x_min + x_pad * 0.5,\n    y=y_max - y_pad * 0.3,\n    text=\"z = sin(√(x² + y²))\",\n    text_font_size=\"34pt\",\n    text_color=INK_SOFT,\n    text_font_style=\"italic\",\n)\np.add_layout(formula_label)\n\n# Styling for 3200x1800 px\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"bold\"\np.title.text_color = INK\n\n# Disable grid for cleaner 3D appearance\np.xgrid.visible = False\np.ygrid.visible = False\n\n# Theme-adaptive background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Get script directory for saving files\nscript_dir = Path(__file__).parent\noutput_dir = script_dir\n\n# Save HTML\nhtml_file = output_dir / f\"plot-{THEME}.html\"\noutput_file(str(html_file))\nsave(p)\n\n# Screenshot with Selenium\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{html_file.resolve()}\")\n# headless Chrome's --window-size sets the OUTER window, which still reserves a\n# phantom title-bar height even headless; pin the viewport exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(str(output_dir / f\"plot-{THEME}.png\"))\ndriver.quit()\n"}