{"spec_id":"wireframe-3d-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// wireframe-3d-basic: Basic 3D Wireframe Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 97/100 | Created: 2026-08-04\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 100, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Ripple surface z = sin(sqrt(x^2 + y^2)) sampled on an evenly spaced grid.\nconst GRID_N = 30;\nconst AXIS_RANGE = 5.2;\nconst Z_EXAGGERATION = 1.7; // visual-only height boost so ripples read clearly\n\nconst xs = d3.range(GRID_N).map((i) => -AXIS_RANGE + (2 * AXIS_RANGE * i) / (GRID_N - 1));\nconst ys = d3.range(GRID_N).map((i) => -AXIS_RANGE + (2 * AXIS_RANGE * i) / (GRID_N - 1));\nconst rippleHeight = (x, y) => Math.sin(Math.sqrt(x * x + y * y));\nconst zGrid = ys.map((y) => xs.map((x) => rippleHeight(x, y)));\nconst zFlat = zGrid.flat();\nconst zRawMin = d3.min(zFlat);\nconst zRawMax = d3.max(zFlat);\n\n// --- Camera: elevation/azimuth orthographic projection ----------------------\nconst ELEVATION = 38;\nconst AZIMUTH = 42;\nconst elRad = (ELEVATION * Math.PI) / 180;\nconst azRad = (AZIMUTH * Math.PI) / 180;\n\nconst camDir = [Math.cos(elRad) * Math.cos(azRad), Math.cos(elRad) * Math.sin(azRad), Math.sin(elRad)];\nconst cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];\nconst normalize = (v) => {\n  const len = Math.hypot(v[0], v[1], v[2]);\n  return [v[0] / len, v[1] / len, v[2] / len];\n};\nconst dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n\nconst worldUp = [0, 0, 1];\nconst right = normalize(cross(worldUp, camDir));\nconst up = normalize(cross(camDir, right));\n\nconst project = (x, y, z) => [dot([x, y, z], right), dot([x, y, z], up)];\nconst depthOf = (x, y, z) => dot([x, y, z], camDir);\n\n// --- Grid points in view space ------------------------------------------------\nconst points = zGrid.map((row, j) =>\n  row.map((zRaw, i) => {\n    const xd = xs[i];\n    const yd = ys[j];\n    const zd = zRaw * Z_EXAGGERATION;\n    const [vx, vy] = project(xd, yd, zd);\n    return { vx, vy, depth: depthOf(xd, yd, zd) };\n  })\n);\n\nconst meshLines = [];\nfor (let j = 0; j < GRID_N; j++) {\n  meshLines.push({ pts: points[j], depth: d3.mean(points[j], (d) => d.depth) });\n}\nfor (let i = 0; i < GRID_N; i++) {\n  const col = points.map((row) => row[i]);\n  meshLines.push({ pts: col, depth: d3.mean(col, (d) => d.depth) });\n}\nmeshLines.sort((a, b) => a.depth - b.depth); // far to near — later draws sit on top\n\nconst depthExtent = d3.extent(points.flat(), (d) => d.depth);\nconst opacityScale = d3.scaleLinear().domain(depthExtent).range([0.3, 0.95]).clamp(true);\n\n// --- Axis frame (floor corner behind the mesh, relative to the camera) ------\nconst xMin = xs[0];\nconst xMax = xs[GRID_N - 1];\nconst yMin = ys[0];\nconst yMax = ys[GRID_N - 1];\nconst zMinScaled = zRawMin * Z_EXAGGERATION;\nconst zMaxScaled = zRawMax * Z_EXAGGERATION;\n\n// Pick the floor corner that projects furthest to screen-left, so the axis\n// frame traces the mesh's visual silhouette instead of cutting through it.\nlet anchorX = xMin;\nlet anchorY = yMin;\nlet bestVx = Infinity;\nfor (const cx of [xMin, xMax]) {\n  for (const cy of [yMin, yMax]) {\n    const [vx] = project(cx, cy, zMinScaled);\n    if (vx < bestVx) {\n      bestVx = vx;\n      anchorX = cx;\n      anchorY = cy;\n    }\n  }\n}\nconst xAxisOtherEnd = anchorX === xMin ? xMax : xMin;\nconst yAxisOtherEnd = anchorY === yMin ? yMax : yMin;\nconst outwardXSign = anchorX > xAxisOtherEnd ? 1 : -1;\nconst outwardYSign = anchorY > yAxisOtherEnd ? 1 : -1;\nconst TICK_LEN = 0.7;\nconst LABEL_LEN = 2.4;\n// Z ticks step diagonally away from the shared corner (not along the X or Y\n// axis direction) so they don't crowd the other two axes' own tick labels.\nconst Z_TICK_LEN = TICK_LEN * 0.72;\nconst Z_LABEL_LEN = LABEL_LEN * 0.72;\n\nconst axisLines = [\n  [\n    [anchorX, anchorY, zMinScaled],\n    [xAxisOtherEnd, anchorY, zMinScaled],\n  ],\n  [\n    [anchorX, anchorY, zMinScaled],\n    [anchorX, yAxisOtherEnd, zMinScaled],\n  ],\n  [\n    [anchorX, anchorY, zMinScaled],\n    [anchorX, anchorY, zMaxScaled],\n  ],\n];\n\n// Ticks landing close to the shared corner crowd the Z axis's own tick\n// column (all three axes converge there) — drop those, they're redundant\n// next to the corner anyway.\nconst CORNER_GUARD = 0.15;\nconst xTicks = d3.ticks(xMin, xMax, 4)\n  .filter((v) => Math.abs(v - anchorX) > CORNER_GUARD * (xMax - xMin))\n  .map((v) => ({\n    a: [v, anchorY, zMinScaled],\n    b: [v, anchorY + outwardYSign * TICK_LEN, zMinScaled],\n    label: [v, anchorY + outwardYSign * LABEL_LEN, zMinScaled],\n    text: d3.format(\".0f\")(v),\n  }));\nconst yTicks = d3.ticks(yMin, yMax, 4)\n  .filter((v) => Math.abs(v - anchorY) > CORNER_GUARD * (yMax - yMin))\n  .map((v) => ({\n    a: [anchorX, v, zMinScaled],\n    b: [anchorX + outwardXSign * TICK_LEN, v, zMinScaled],\n    label: [anchorX + outwardXSign * LABEL_LEN, v, zMinScaled],\n    text: d3.format(\".0f\")(v),\n  }));\nconst zTicks = d3.ticks(zRawMin, zRawMax, 4).map((v) => ({\n  a: [anchorX, anchorY, v * Z_EXAGGERATION],\n  b: [anchorX + outwardXSign * Z_TICK_LEN, anchorY + outwardYSign * Z_TICK_LEN, v * Z_EXAGGERATION],\n  label: [anchorX + outwardXSign * Z_LABEL_LEN, anchorY + outwardYSign * Z_LABEL_LEN, v * Z_EXAGGERATION],\n  text: d3.format(\".1f\")(v),\n}));\n\nconst axisLabels = [\n  { pos: [xAxisOtherEnd, anchorY + outwardYSign * 2.4, zMinScaled], text: \"X\" },\n  { pos: [anchorX + outwardXSign * 2.4, yAxisOtherEnd, zMinScaled], text: \"Y\" },\n  { pos: [anchorX + outwardXSign * 2.4, anchorY, zMaxScaled], text: \"Z\" },\n];\n\n// --- Fit view-space extent (mesh + axis frame + tick stubs) into the mount --\nconst extentSource = [\n  ...points.flat().map((d) => [d.vx, d.vy]),\n  ...axisLines.flatMap(([a, b]) => [project(...a), project(...b)]),\n  ...xTicks.flatMap((tk) => [project(...tk.a), project(...tk.label)]),\n  ...yTicks.flatMap((tk) => [project(...tk.a), project(...tk.label)]),\n  ...zTicks.flatMap((tk) => [project(...tk.a), project(...tk.label)]),\n  ...axisLabels.map((l) => project(...l.pos)),\n];\nconst extMinX = d3.min(extentSource, (d) => d[0]);\nconst extMaxX = d3.max(extentSource, (d) => d[0]);\nconst extMinY = d3.min(extentSource, (d) => d[1]);\nconst extMaxY = d3.max(extentSource, (d) => d[1]);\nconst midX = (extMinX + extMaxX) / 2;\nconst midY = (extMinY + extMaxY) / 2;\nconst fitScale = 0.93 * Math.min(iw / (extMaxX - extMinX), ih / (extMaxY - extMinY));\nconst toScreen = ([vx, vy]) => [\n  margin.left + iw / 2 + (vx - midX) * fitScale,\n  margin.top + ih / 2 - (vy - midY) * fitScale,\n];\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Wireframe mesh, painted back-to-front for a plausible depth cue --------\nconst line = d3\n  .line()\n  .x((d) => d[0])\n  .y((d) => d[1]);\n\nconst mesh = svg.append(\"g\").attr(\"stroke\", t.palette[0]).attr(\"fill\", \"none\").attr(\"stroke-width\", 1.6);\nmesh\n  .selectAll(\"path\")\n  .data(meshLines)\n  .join(\"path\")\n  .attr(\"d\", (d) => line(d.pts.map((p) => toScreen([p.vx, p.vy]))))\n  .attr(\"stroke-opacity\", (d) => opacityScale(d.depth));\n\n// --- Axis frame ----------------------------------------------------------------\nconst axisGroup = svg.append(\"g\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2);\naxisGroup\n  .selectAll(\"line\")\n  .data(axisLines)\n  .join(\"line\")\n  .attr(\"x1\", (d) => toScreen(project(...d[0]))[0])\n  .attr(\"y1\", (d) => toScreen(project(...d[0]))[1])\n  .attr(\"x2\", (d) => toScreen(project(...d[1]))[0])\n  .attr(\"y2\", (d) => toScreen(project(...d[1]))[1]);\n\nconst tickGroup = svg.append(\"g\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.4);\nconst allTicks = [...xTicks, ...yTicks, ...zTicks];\ntickGroup\n  .selectAll(\"line\")\n  .data(allTicks)\n  .join(\"line\")\n  .attr(\"x1\", (d) => toScreen(project(...d.a))[0])\n  .attr(\"y1\", (d) => toScreen(project(...d.a))[1])\n  .attr(\"x2\", (d) => toScreen(project(...d.b))[0])\n  .attr(\"y2\", (d) => toScreen(project(...d.b))[1]);\n\n// Screen-space corner-overlap guard: CORNER_GUARD above only filters ticks by\n// 3D data-space distance to the anchor, but a tick far from the anchor in\n// data space can still project close to the shared corner in 2D at this\n// camera angle. Push any tick label whose *projected* position lands inside\n// a pixel radius of the projected corner further out along the corner->label\n// direction, so no label ever renders on top of the mesh silhouette there.\nconst anchorScreen = toScreen(project(anchorX, anchorY, zMinScaled));\nconst CORNER_PX_RADIUS = 42;\nfor (const tk of allTicks) {\n  const [sx, sy] = toScreen(project(...tk.label));\n  let dx = sx - anchorScreen[0];\n  let dy = sy - anchorScreen[1];\n  let dist = Math.hypot(dx, dy);\n  if (dist < 1e-6) {\n    const [bx, by] = toScreen(project(...tk.b));\n    dx = bx - anchorScreen[0];\n    dy = by - anchorScreen[1];\n    dist = Math.hypot(dx, dy) || 1;\n  }\n  if (dist < CORNER_PX_RADIUS) {\n    const scale = (CORNER_PX_RADIUS * 1.15) / dist;\n    tk.labelScreen = [anchorScreen[0] + dx * scale, anchorScreen[1] + dy * scale];\n  } else {\n    tk.labelScreen = [sx, sy];\n  }\n}\n\nsvg\n  .append(\"g\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .selectAll(\"text\")\n  .data(allTicks)\n  .join(\"text\")\n  .attr(\"x\", (d) => d.labelScreen[0])\n  .attr(\"y\", (d) => d.labelScreen[1])\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .text((d) => d.text);\n\nsvg\n  .append(\"g\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"19px\")\n  .style(\"font-weight\", \"600\")\n  .selectAll(\"text\")\n  .data(axisLabels)\n  .join(\"text\")\n  .attr(\"x\", (d) => toScreen(project(...d.pos))[0])\n  .attr(\"y\", (d) => toScreen(project(...d.pos))[1])\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .text((d) => d.text);\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"wireframe-3d-basic · javascript · d3 · anyplot.ai\");\n"}