{"spec_id":"wireframe-3d-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nwireframe-3d-basic: Basic 3D Wireframe Plot\nLibrary: plotnine 0.15.8 | Python 3.13.15\nQuality: 85/100 | Created: 2026-09-10\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_segment,\n    geom_text,\n    ggplot,\n    labs,\n    scale_alpha_continuous,\n    theme,\n    theme_void,\n)\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\"  # Imprint palette position 1 — ALWAYS first series\n\n# Camera: orthographic projection at elevation 30 deg / azimuth 45 deg, per spec.\n# plotnine has no 3D grammar, so the mesh is projected to 2D screen coordinates\n# ourselves (the same technique any static 3D renderer uses under the hood),\n# then drawn with plotnine's own geom_path / geom_segment / geom_text.\nelev = np.radians(30)\nazim = np.radians(45)\n\nview_dir = np.array([np.cos(elev) * np.cos(azim), np.cos(elev) * np.sin(azim), np.sin(elev)])\nworld_up = np.array([0.0, 0.0, 1.0])\nright_axis = np.cross(view_dir, world_up)\nright_axis /= np.linalg.norm(right_axis)\nup_axis = np.cross(right_axis, view_dir)\n\nZ_LIFT = 1.8  # visual height exaggeration so the shallow membrane displacement reads clearly\n\n\ndef project(x, y, z):\n    px = x * right_axis[0] + y * right_axis[1] + z * Z_LIFT * right_axis[2]\n    py = x * up_axis[0] + y * up_axis[1] + z * Z_LIFT * up_axis[2]\n    return px, py\n\n\ndef depth(x, y, z):\n    \"\"\"Distance along the camera's view direction — larger means closer to\n    the viewer, so it doubles as a painter's-algorithm draw-order key and as\n    the source for depth-based alpha (approximates hidden-line suppression\n    since plotnine has no real depth buffer).\"\"\"\n    return x * view_dir[0] + y * view_dir[1] + z * Z_LIFT * view_dir[2]\n\n\n# Data — circular drumhead vibration mode: displacement z = sin(sqrt(x^2 + y^2))\ngrid_n = 21  # kept modest (20-22) so depth-faded lines stay legible, not a tangle\nx_vals = np.linspace(-6, 6, grid_n)\ny_vals = np.linspace(-6, 6, grid_n)\ngrid_x, grid_y = np.meshgrid(x_vals, y_vals)\ngrid_z = np.sin(np.sqrt(grid_x**2 + grid_y**2))\n\nz_min, z_max = float(grid_z.min()), float(grid_z.max())\nfloor_z = z_min - 0.3\nceil_z = z_max + 0.3\n\ngrid_px, grid_py = project(grid_x, grid_y, grid_z)\ngrid_depth = depth(grid_x, grid_y, grid_z)\n\n# Wireframe mesh as individual edges (not whole rows/columns) so each edge can\n# carry its own depth-based alpha: far-side edges fade low, near-side edges\n# stay opaque, which reads as an approximate hidden-line-suppressed surface\n# instead of a flat tangle of fully superimposed lines.\nedges = []\nfor i in range(grid_n):\n    for j in range(grid_n - 1):\n        edges.append(\n            {\n                \"px\": grid_px[i, j],\n                \"py\": grid_py[i, j],\n                \"pxend\": grid_px[i, j + 1],\n                \"pyend\": grid_py[i, j + 1],\n                \"edge_depth\": (grid_depth[i, j] + grid_depth[i, j + 1]) / 2,\n            }\n        )\nfor j in range(grid_n):\n    for i in range(grid_n - 1):\n        edges.append(\n            {\n                \"px\": grid_px[i, j],\n                \"py\": grid_py[i, j],\n                \"pxend\": grid_px[i + 1, j],\n                \"pyend\": grid_py[i + 1, j],\n                \"edge_depth\": (grid_depth[i, j] + grid_depth[i + 1, j]) / 2,\n            }\n        )\n# Sort back-to-front so later (nearer, higher-alpha) edges paint over earlier\n# (farther, lower-alpha) ones — plotnine draws geom_segment rows in data order.\nmesh_edges = pd.DataFrame(edges).sort_values(\"edge_depth\", ignore_index=True)\n\n# Axis box: three edges meeting at the (x=6, y=-6) corner. This corner sits off\n# the camera's azimuth-45 view axis (unlike the diagonally opposite (-6, -6)\n# corner, which projects to dead screen-center and would drag the axis frame\n# straight through the densest part of the mesh), so the frame reads as a\n# distinct side reference instead of cutting through the data.\naxis_lines = pd.DataFrame(\n    {\n        \"x\": [6, 6, 6],\n        \"y\": [-6, -6, -6],\n        \"z\": [floor_z, floor_z, floor_z],\n        \"xend\": [-6, 6, 6],\n        \"yend\": [-6, 6, -6],\n        \"zend\": [floor_z, floor_z, ceil_z],\n    }\n)\naxis_lines[\"px\"], axis_lines[\"py\"] = project(axis_lines[\"x\"], axis_lines[\"y\"], axis_lines[\"z\"])\naxis_lines[\"pxend\"], axis_lines[\"pyend\"] = project(axis_lines[\"xend\"], axis_lines[\"yend\"], axis_lines[\"zend\"])\n\nx_breaks = np.array([-6, -3, 0, 3, 6])\ny_breaks = np.array([-6, -3, 0, 3, 6])\nz_breaks = np.array([-1, 0, 1])\n\nticks = pd.concat(\n    [\n        pd.DataFrame({\"x\": x_breaks, \"y\": -9.6, \"z\": floor_z, \"label\": [f\"{v:g}\" for v in x_breaks]}),\n        pd.DataFrame({\"x\": 9.6, \"y\": y_breaks, \"z\": floor_z, \"label\": [f\"{v:g}\" for v in y_breaks]}),\n    ],\n    ignore_index=True,\n)\nticks[\"px\"], ticks[\"py\"] = project(ticks[\"x\"], ticks[\"y\"], ticks[\"z\"])\n\n# Z ticks sit on the vertical axis line itself. A short leader segment (tick\n# mark) connects each label back to the axis line so it reads as belonging to\n# the Z axis rather than as a stray fourth axis (previously offset -13 with\n# no connector, leaving the labels visually stranded).\nZ_TICK_LEADER = 1.2\nZ_TICK_LABEL_GAP = 0.6\nz_axis_px, z_axis_py = project(6, -6, z_breaks)\nz_ticks = pd.DataFrame(\n    {\"px\": z_axis_px - Z_TICK_LEADER - Z_TICK_LABEL_GAP, \"py\": z_axis_py, \"label\": [f\"{v:g}\" for v in z_breaks]}\n)\nz_tick_leaders = pd.DataFrame(\n    {\"px\": z_axis_px, \"py\": z_axis_py, \"pxend\": z_axis_px - Z_TICK_LEADER, \"pyend\": z_axis_py}\n)\n\naxis_labels = pd.DataFrame(\n    {\n        \"x\": [-9.4, 6, 6],\n        \"y\": [-6, 9.4, -6],\n        \"z\": [floor_z, floor_z, ceil_z + 1.0],\n        \"label\": [\"X (cm)\", \"Y (cm)\", \"Z (mm)\"],\n    }\n)\naxis_labels[\"px\"], axis_labels[\"py\"] = project(axis_labels[\"x\"], axis_labels[\"y\"], axis_labels[\"z\"])\n\n# Plot\nplot = (\n    ggplot()\n    + geom_segment(\n        aes(x=\"px\", y=\"py\", xend=\"pxend\", yend=\"pyend\", alpha=\"edge_depth\"),\n        mesh_edges,\n        color=BRAND,\n        size=0.3,\n        show_legend=False,\n    )\n    + scale_alpha_continuous(range=(0.08, 0.85))\n    + geom_segment(aes(x=\"px\", y=\"py\", xend=\"pxend\", yend=\"pyend\"), axis_lines, color=INK_SOFT, size=0.6)\n    + geom_segment(aes(x=\"px\", y=\"py\", xend=\"pxend\", yend=\"pyend\"), z_tick_leaders, color=INK_SOFT, size=0.6)\n    + geom_text(aes(\"px\", \"py\", label=\"label\"), ticks, color=INK_SOFT, size=3.3)\n    + geom_text(aes(\"px\", \"py\", label=\"label\"), z_ticks, color=INK_SOFT, size=3.3, ha=\"right\")\n    + geom_text(aes(\"px\", \"py\", label=\"label\"), axis_labels, color=INK, size=3.6, fontweight=\"bold\")\n    + labs(title=\"wireframe-3d-basic · python · plotnine · anyplot.ai\")\n    + coord_fixed(ratio=1)\n    + theme_void(base_size=7)\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(color=INK, size=12, ha=\"center\"),\n        figure_size=(8, 4.5),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}