{"spec_id":"wireframe-3d-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// wireframe-3d-basic: Basic 3D Wireframe Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.1\n// Quality: 89/100 | Created: 2026-08-04\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data: ripple surface z = sinc(r) = sin(r)/r on a 24x24 grid -----------\n// RANGE is chosen so the diagonal (the farthest grid corner) reaches close to\n// one full sin() period (2*pi) — a clean central peak plus one surrounding\n// ring, without a truncated third lobe fraying the corners. Dividing by r\n// (the sinc form, naturally 1 at r=0) keeps the apex a smooth dome instead of\n// the sharp cusp a raw sin(r) surface has at the center.\nconst GRID_N = 24;\nconst RANGE = 4.4;\nconst step = (2 * RANGE) / (GRID_N - 1);\nconst xs = Array.from({ length: GRID_N }, (_, i) => -RANGE + i * step);\nconst ys = Array.from({ length: GRID_N }, (_, i) => -RANGE + i * step);\nconst zGrid = xs.map((x) =>\n  ys.map((y) => {\n    const r = Math.sqrt(x * x + y * y);\n    return r === 0 ? 1 : Math.sin(r) / r;\n  })\n);\nconst zFlat = zGrid.flat();\nconst zMin = Math.min(...zFlat);\nconst zMax = Math.max(...zFlat);\n\n// --- Camera: orthographic axonometric projection (elevation + azimuth) -----\nconst ELEVATION = (30 * Math.PI) / 180;\nconst AZIMUTH = (45 * Math.PI) / 180;\nconst sinAz = Math.sin(AZIMUTH);\nconst cosAz = Math.cos(AZIMUTH);\nconst sinEl = Math.sin(ELEVATION);\nconst cosEl = Math.cos(ELEVATION);\nconst ZSCALE = 0.6; // compresses height relative to the x/y footprint\n\n// Normalizes (x, y, zData) to a unit-ish cube, then rotates onto the view\n// plane. Depth (toward/away from the camera) is returned alongside the 2D\n// screen offset so lines can be drawn back-to-front with a subtle depth fade.\nfunction projectRaw(x, y, zData) {\n  const xn = x / RANGE;\n  const yn = y / RANGE;\n  const zn = ((zData - (zMin + zMax) / 2) / (zMax - zMin)) * 2 * ZSCALE;\n  const screenX = -xn * sinAz + yn * cosAz;\n  const screenY = -xn * cosAz * sinEl - yn * sinAz * sinEl + zn * cosEl;\n  const depth = xn * cosEl * cosAz + yn * cosEl * sinAz + zn * sinEl;\n  return { screenX, screenY, depth };\n}\n\n// --- Fit the projected bounding box into the mount, leaving title room -----\nconst corners = [];\nfor (const x of [-RANGE, RANGE]) {\n  for (const y of [-RANGE, RANGE]) {\n    for (const z of [zMin, zMax]) corners.push(projectRaw(x, y, z));\n  }\n}\nconst sxs = corners.map((c) => c.screenX);\nconst sys = corners.map((c) => c.screenY);\nconst boxW = Math.max(...sxs) - Math.min(...sxs);\nconst boxH = Math.max(...sys) - Math.min(...sys);\nconst boxCx = (Math.max(...sxs) + Math.min(...sxs)) / 2;\nconst boxCy = (Math.max(...sys) + Math.min(...sys)) / 2;\n\nconst TOP_MARGIN = 110;\nconst SIDE_MARGIN = 90;\nconst BOTTOM_MARGIN = 70;\nconst drawW = size.width - 2 * SIDE_MARGIN;\nconst drawH = size.height - TOP_MARGIN - BOTTOM_MARGIN;\n// Extra padding so axis ticks/labels (drawn slightly outside the data cube)\n// stay within the mount.\nconst PAD = 1.35;\nconst scale = Math.min(drawW / (boxW * PAD), drawH / (boxH * PAD));\nconst originX = size.width / 2 - boxCx * scale;\nconst originY = TOP_MARGIN + drawH / 2 + boxCy * scale;\n\nfunction toPixel(x, y, zData) {\n  const { screenX, screenY, depth } = projectRaw(x, y, zData);\n  return { px: originX + screenX * scale, py: originY - screenY * scale, depth };\n}\n\n// --- Wireframe mesh: one polyline per grid row and per grid column ---------\nconst BRAND = t.palette[0];\nconst meshLines = [];\nfor (let i = 0; i < GRID_N; i += 1) {\n  const rowPts = xs.map((x, xi) => toPixel(x, ys[i], zGrid[xi][i]));\n  const rowDepth = rowPts.reduce((s, p) => s + p.depth, 0) / rowPts.length;\n  meshLines.push({ points: rowPts.map((p) => [p.px, p.py]), depth: rowDepth });\n}\nfor (let j = 0; j < GRID_N; j += 1) {\n  const colPts = ys.map((y, k) => toPixel(xs[j], y, zGrid[j][k]));\n  const colDepth = colPts.reduce((s, p) => s + p.depth, 0) / colPts.length;\n  meshLines.push({ points: colPts.map((p) => [p.px, p.py]), depth: colDepth });\n}\n// Back-to-front draw order plus a depth-based opacity fade gives a gentle\n// see-through, near-lines-brighter cue without any hidden-line removal.\nmeshLines.sort((a, b) => a.depth - b.depth);\nconst depths = meshLines.map((l) => l.depth);\nconst dMin = Math.min(...depths);\nconst dMax = Math.max(...depths);\nconst meshElements = meshLines.map((l) => {\n  const tDepth = dMax > dMin ? (l.depth - dMin) / (dMax - dMin) : 1;\n  return {\n    type: \"polyline\",\n    shape: { points: l.points },\n    style: { stroke: BRAND, lineWidth: 1.6, fill: \"none\", opacity: 0.4 + 0.5 * tDepth },\n    silent: true,\n  };\n});\n\n// --- Axis frame: three edges of the bounding box, ticks + labels -----------\nconst AXIS_COLOR = t.inkSoft;\nconst TICK_LEN = 0.35; // in data units along the outward axis direction\nconst axisElements = [];\n\nfunction axisLine(p1, p2) {\n  const points = [p1, p2].map(([x, y, z]) => {\n    const p = toPixel(x, y, z);\n    return [p.px, p.py];\n  });\n  axisElements.push({\n    type: \"polyline\",\n    shape: { points },\n    style: { stroke: AXIS_COLOR, lineWidth: 2, fill: \"none\" },\n    silent: true,\n  });\n}\n\nfunction tickMark(base, outward) {\n  const p1 = toPixel(...base);\n  const p2 = toPixel(...outward);\n  axisElements.push({\n    type: \"polyline\",\n    shape: { points: [[p1.px, p1.py], [p2.px, p2.py]] },\n    style: { stroke: AXIS_COLOR, lineWidth: 2, fill: \"none\" },\n    silent: true,\n  });\n}\n\nfunction tickLabel(pos, text, align) {\n  const p = toPixel(...pos);\n  axisElements.push({\n    type: \"text\",\n    style: {\n      text,\n      x: p.px,\n      y: p.py,\n      fill: t.inkSoft,\n      fontSize: 13,\n      align: align || \"center\",\n      verticalAlign: \"middle\",\n    },\n    silent: true,\n  });\n}\n\nfunction axisTitle(pos, text, pixelOffset) {\n  const p = toPixel(...pos);\n  const dx = (pixelOffset && pixelOffset[0]) || 0;\n  const dy = (pixelOffset && pixelOffset[1]) || 0;\n  axisElements.push({\n    type: \"text\",\n    style: {\n      text,\n      x: p.px + dx,\n      y: p.py + dy,\n      fill: t.ink,\n      fontSize: 17,\n      fontWeight: \"bold\",\n      align: \"center\",\n      verticalAlign: \"middle\",\n    },\n    silent: true,\n  });\n}\n\n// X and Y sit on the front-bottom corner of the data box — the corner\n// whose projected screen position is farthest from (behind) the\n// camera-facing surface, i.e. lowest on screen — so their ticks/labels\n// never compete with the mesh for space. With this camera (elevation 30,\n// azimuth 45) that is (+RANGE, +RANGE, zMin); ticks point further\n// outward, away from the data box.\nconst CORNER_X = RANGE;\nconst CORNER_Y = RANGE;\nconst zAxisBase = zMin;\nconst axisTicks = [-4, -2, 0, 2, 4];\n\n// X axis (varies x, fixed y = CORNER_Y, z = zMin) — ticks extend in +y\naxisLine([-RANGE, CORNER_Y, zAxisBase], [RANGE, CORNER_Y, zAxisBase]);\naxisTicks.forEach((v) => {\n  tickMark([v, CORNER_Y, zAxisBase], [v, CORNER_Y + TICK_LEN, zAxisBase]);\n  tickLabel([v, CORNER_Y + TICK_LEN * 2.2, zAxisBase], String(v));\n});\naxisTitle([0, CORNER_Y + TICK_LEN * 2.2, zAxisBase], \"X\", [0, 34]);\n\n// Y axis (varies y, fixed x = CORNER_X, z = zMin) — ticks extend in +x\naxisLine([CORNER_X, -RANGE, zAxisBase], [CORNER_X, RANGE, zAxisBase]);\naxisTicks.forEach((v) => {\n  tickMark([CORNER_X, v, zAxisBase], [CORNER_X + TICK_LEN, v, zAxisBase]);\n  tickLabel([CORNER_X + TICK_LEN * 2.2, v, zAxisBase], String(v));\n});\naxisTitle([CORNER_X + TICK_LEN * 2.2, 0, zAxisBase], \"Y\", [34, 0]);\n\n// Z sits on its own corner (+RANGE, -RANGE) rather than the X/Y corner:\n// azimuth 45 puts every x=y point (including the surface peak at x=y=0) on\n// the same vertical screen line, so a Z axis rising from (+RANGE, +RANGE)\n// climbs straight up behind the peak. (+RANGE, -RANGE) has x + y = 0, which\n// cancels the xy term in the screen-Y projection entirely — the column\n// stays pinned to the box's far-left screen edge for its whole height,\n// clear of the mesh above.\nconst Z_CORNER_X = RANGE;\nconst Z_CORNER_Y = -RANGE;\naxisLine([Z_CORNER_X, Z_CORNER_Y, zMin], [Z_CORNER_X, Z_CORNER_Y, zMax]);\nconst zTicks = [zMin, (zMin + zMax) / 2, zMax];\nzTicks.forEach((v) => {\n  tickMark([Z_CORNER_X, Z_CORNER_Y, v], [Z_CORNER_X, Z_CORNER_Y - TICK_LEN, v]);\n  // Guard the near-zero midpoint: floating-point rounding can leave it as a\n  // tiny negative value, which .toFixed(1) would otherwise render as \"-0.0\".\n  const label = (Math.abs(v) < 1e-9 ? 0 : v).toFixed(1);\n  tickLabel([Z_CORNER_X, Z_CORNER_Y - TICK_LEN * 2.2, v], label, \"center\");\n});\naxisTitle([Z_CORNER_X, Z_CORNER_Y, zMax], \"Z\", [0, -34]);\n\n// --- Init + option -----------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"wireframe-3d-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  graphic: { elements: [...meshElements, ...axisElements] },\n});\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}