{"spec_id":"wireframe-3d-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nwireframe-3d-basic: Basic 3D Wireframe Plot\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: this file is named altair.py, so remove its directory\n# from sys.path before importing the altair package.\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p and os.path.abspath(p) != _this_dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nDIV_MID = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"  # Imprint diverging-cmap midpoint\n\n# Data - ripple surface z = sin(sqrt(x^2 + y^2))\ngrid_size = 30\nx_vals = np.linspace(-5, 5, grid_size)\ny_vals = np.linspace(-5, 5, grid_size)\nX_grid, Y_grid = np.meshgrid(x_vals, y_vals)\nZ = np.sin(np.sqrt(X_grid**2 + Y_grid**2))\n\n\ndef isometric_projection(x, y, z, elevation=30, azimuth=45):\n    \"\"\"Project 3D coordinates to 2D using an isometric view.\"\"\"\n    el_rad, az_rad = np.radians(elevation), np.radians(azimuth)\n    cos_el, sin_el = np.cos(el_rad), np.sin(el_rad)\n    cos_az, sin_az = np.cos(az_rad), np.sin(az_rad)\n    x_2d = x * cos_az - y * sin_az\n    y_2d = (x * sin_az + y * cos_az) * sin_el + z * cos_el\n    return x_2d, y_2d\n\n\nx_proj, y_proj = isometric_projection(X_grid, Y_grid, Z)\n\n# Wireframe edges - mesh lines along both grid directions\nlines_data = []\nfor i in range(grid_size):\n    for j in range(grid_size - 1):\n        lines_data.append(\n            {\n                \"x_proj\": x_proj[i, j],\n                \"y_proj\": y_proj[i, j],\n                \"x_proj_next\": x_proj[i, j + 1],\n                \"y_proj_next\": y_proj[i, j + 1],\n                \"z\": (Z[i, j] + Z[i, j + 1]) / 2,\n            }\n        )\nfor i in range(grid_size - 1):\n    for j in range(grid_size):\n        lines_data.append(\n            {\n                \"x_proj\": x_proj[i, j],\n                \"y_proj\": y_proj[i, j],\n                \"x_proj_next\": x_proj[i + 1, j],\n                \"y_proj_next\": y_proj[i + 1, j],\n                \"z\": (Z[i, j] + Z[i + 1, j]) / 2,\n            }\n        )\nmesh_df = pd.DataFrame(lines_data)\n\n# X/Y reference axes - offset outside the mesh footprint so they read cleanly.\n# Z gets its own, farther-out corner (AXIS_OFFSET_Z) so its ladder doesn't land\n# on top of the X-axis \"0\" and Y-axis \"0\" ticks and crowd the shared corner.\nAXIS_OFFSET = -8\nAXIS_OFFSET_Z = AXIS_OFFSET - 2.5\naxis_lines = []\nxp0, yp0 = isometric_projection(-5, AXIS_OFFSET, 0)\nxp1, yp1 = isometric_projection(5, AXIS_OFFSET, 0)\naxis_lines.append({\"x_proj\": xp0, \"y_proj\": yp0, \"x_proj_next\": xp1, \"y_proj_next\": yp1})\nxp0, yp0 = isometric_projection(AXIS_OFFSET, -5, 0)\nxp1, yp1 = isometric_projection(AXIS_OFFSET, 5, 0)\naxis_lines.append({\"x_proj\": xp0, \"y_proj\": yp0, \"x_proj_next\": xp1, \"y_proj_next\": yp1})\naxis_df = pd.DataFrame(axis_lines)\n\n# Tick marks (-5, 0, 5) along each reference axis\nticks = []\nfor t in (-5, 0, 5):\n    xp, yp = isometric_projection(t, AXIS_OFFSET, 0)\n    ticks.append({\"x_proj\": xp, \"y_proj\": yp, \"label\": str(t)})\nfor t in (-5, 0, 5):\n    xp, yp = isometric_projection(AXIS_OFFSET, t, 0)\n    ticks.append({\"x_proj\": xp, \"y_proj\": yp, \"label\": str(t)})\nticks_df = pd.DataFrame(ticks)\n\n# Axis name labels at the tip of each reference axis\nxp, yp = isometric_projection(6.3, AXIS_OFFSET, 0)\naxis_x_name = pd.DataFrame([{\"x_proj\": xp, \"y_proj\": yp, \"label\": \"X\"}])\nxp, yp = isometric_projection(AXIS_OFFSET, 6.3, 0)\naxis_y_name = pd.DataFrame([{\"x_proj\": xp, \"y_proj\": yp, \"label\": \"Y\"}])\n\n# Z reference axis - vertical tick ladder at its own farther-out corner\n# (AXIS_OFFSET_Z) so Z gets real spatial ticks like X and Y, without\n# clustering on top of the X-axis \"0\" / Y-axis \"0\" tick labels\nxp0, yp0 = isometric_projection(AXIS_OFFSET_Z, AXIS_OFFSET_Z, -1.3)\nxp1, yp1 = isometric_projection(AXIS_OFFSET_Z, AXIS_OFFSET_Z, 1.3)\nz_axis_df = pd.DataFrame([{\"x_proj\": xp0, \"y_proj\": yp0, \"x_proj_next\": xp1, \"y_proj_next\": yp1}])\n\nz_ticks = []\nfor t in (-1, 0, 1):\n    xp, yp = isometric_projection(AXIS_OFFSET_Z, AXIS_OFFSET_Z, t)\n    z_ticks.append({\"x_proj\": xp, \"y_proj\": yp, \"label\": str(t)})\nz_ticks_df = pd.DataFrame(z_ticks)\n\nxp, yp = isometric_projection(AXIS_OFFSET_Z, AXIS_OFFSET_Z, 1.6)\naxis_z_name = pd.DataFrame([{\"x_proj\": xp, \"y_proj\": yp, \"label\": \"Z\"}])\n\ntitle = \"wireframe-3d-basic · python · altair · anyplot.ai\"\n\n# Wireframe mesh - height (Z) mapped through the Imprint diverging cmap since\n# the ripple surface oscillates around 0 (troughs vs. peaks)\nmesh = (\n    alt.Chart(mesh_df)\n    .mark_line(strokeWidth=1.1, opacity=0.75)\n    .encode(\n        x=alt.X(\"x_proj:Q\", axis=None),\n        y=alt.Y(\"y_proj:Q\", axis=None),\n        x2=\"x_proj_next:Q\",\n        y2=\"y_proj_next:Q\",\n        color=alt.Color(\n            \"z:Q\",\n            scale=alt.Scale(range=[\"#AE3030\", DIV_MID, \"#4467A3\"], domainMid=0, interpolate=\"rgb\"),\n            title=\"Height (Z)\",\n        ),\n    )\n)\n\naxes = (\n    alt.Chart(pd.concat([axis_df, z_axis_df], ignore_index=True))\n    .mark_line(strokeWidth=1.5, color=INK_SOFT, opacity=0.6)\n    .encode(x=alt.X(\"x_proj:Q\", axis=None), y=alt.Y(\"y_proj:Q\", axis=None), x2=\"x_proj_next:Q\", y2=\"y_proj_next:Q\")\n)\n\ntick_labels = (\n    alt.Chart(ticks_df)\n    .mark_text(fontSize=10, color=INK_SOFT, dy=18)\n    .encode(x=alt.X(\"x_proj:Q\", axis=None), y=alt.Y(\"y_proj:Q\", axis=None), text=\"label:N\")\n)\n\n# Z ticks sit on a near-vertical axis line at its own farther-out corner, so\n# offset horizontally (dx) rather than vertically (dy) like the X/Y ticks.\nz_tick_labels = (\n    alt.Chart(z_ticks_df)\n    .mark_text(fontSize=10, color=INK_SOFT, dx=-20)\n    .encode(x=alt.X(\"x_proj:Q\", axis=None), y=alt.Y(\"y_proj:Q\", axis=None), text=\"label:N\")\n)\n\naxis_names = (\n    alt.Chart(pd.concat([axis_x_name, axis_y_name], ignore_index=True))\n    .mark_text(fontSize=13, fontWeight=\"bold\", color=INK, dy=16)\n    .encode(x=alt.X(\"x_proj:Q\", axis=None), y=alt.Y(\"y_proj:Q\", axis=None), text=\"label:N\")\n)\n\nz_axis_name = (\n    alt.Chart(axis_z_name)\n    .mark_text(fontSize=13, fontWeight=\"bold\", color=INK, dx=-16)\n    .encode(x=alt.X(\"x_proj:Q\", axis=None), y=alt.Y(\"y_proj:Q\", axis=None), text=\"label:N\")\n)\n\nchart = (\n    alt.layer(mesh, axes, tick_labels, z_tick_labels, axis_names, z_axis_name)\n    .properties(width=620, height=320, background=PAGE_BG, title=alt.Title(title, fontSize=16))\n    .configure_view(continuousWidth=620, continuousHeight=320, fill=PAGE_BG, strokeWidth=0)\n    .configure_title(color=INK)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n        padding=10,\n    )\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200x1800\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save HTML\nchart.save(f\"plot-{THEME}.html\")\n"}