{"spec_id":"wireframe-3d-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// wireframe-3d-basic: Basic 3D Wireframe Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 83/100 | Created: 2026-08-04\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: ripple surface z = sin(sqrt(x^2 + y^2)) on a 27x27 grid ---------\nconst GRID_N = 27;\nconst RANGE = 6;\nconst grid = Array.from({ length: GRID_N }, (_, i) => -RANGE + (2 * RANGE * i) / (GRID_N - 1));\nconst zAt = (x, y) => Math.sin(Math.sqrt(x * x + y * y));\nconst zGrid = grid.map((y) => grid.map((x) => zAt(x, y)));\nlet zMin = Infinity;\nlet zMax = -Infinity;\nzGrid.forEach((row) =>\n  row.forEach((z) => {\n    if (z < zMin) zMin = z;\n    if (z > zMax) zMax = z;\n  })\n);\n\n// --- 3D -> 2D orthographic projection (elevation 28°, azimuth 40°) ---------\n// Highcharts core has no chart3d/highcharts-3d module, so the mesh is\n// projected by hand into plain (x, y) screen coordinates and drawn as\n// ordinary `line` series — the same math a native 3D engine applies before\n// rasterizing, just computed here instead of in an unavailable add-on.\nconst ELEV = (28 * Math.PI) / 180;\nconst AZIM = (40 * Math.PI) / 180;\nconst cosAz = Math.cos(AZIM);\nconst sinAz = Math.sin(AZIM);\nconst cosEl = Math.cos(ELEV);\nconst sinEl = Math.sin(ELEV);\n\nconst project = (x, y, z) => {\n  const xr = x * cosAz + y * sinAz;\n  const yr = -x * sinAz + y * cosAz;\n  const zScreen = yr * sinEl + z * cosEl;\n  return [xr, zScreen];\n};\n\n// --- Color: height-based gradient along the imprint_seq colormap -----------\nconst hexToRgb = (hex) => {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n};\nconst seqLo = hexToRgb(t.seq[0]);\nconst seqHi = hexToRgb(t.seq[1]);\nconst heightColor = (z) => {\n  const f = (z - zMin) / (zMax - zMin);\n  const r = Math.round(seqLo[0] + (seqHi[0] - seqLo[0]) * f);\n  const g = Math.round(seqLo[1] + (seqHi[1] - seqLo[1]) * f);\n  const b = Math.round(seqLo[2] + (seqHi[2] - seqLo[2]) * f);\n  return `rgb(${r}, ${g}, ${b})`;\n};\n\n// --- Mesh: one line series per grid row and per grid column ----------------\nconst projected = grid.map((y, iy) => grid.map((x, ix) => project(x, y, zGrid[iy][ix])));\n\nconst meshSeries = [];\ngrid.forEach((_, iy) => {\n  const avgZ = zGrid[iy].reduce((a, b) => a + b, 0) / GRID_N;\n  meshSeries.push({\n    type: \"line\",\n    data: projected[iy],\n    color: heightColor(avgZ),\n    lineWidth: 1.3,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n  });\n});\ngrid.forEach((_, ix) => {\n  const colZs = grid.map((_, iy) => zGrid[iy][ix]);\n  const avgZ = colZs.reduce((a, b) => a + b, 0) / GRID_N;\n  const colPoints = grid.map((_, iy) => projected[iy][ix]);\n  meshSeries.push({\n    type: \"line\",\n    data: colPoints,\n    color: heightColor(avgZ),\n    lineWidth: 1.3,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n  });\n});\n\n// --- Axis frame: an L-shaped X/Y/Z reference resting below the surface -----\nconst floorZ = zMin - (zMax - zMin) * 0.35;\nconst topZ = zMax + (zMax - zMin) * 0.2;\nconst corner = project(-RANGE, -RANGE, floorZ);\nconst xEnd = project(RANGE, -RANGE, floorZ);\nconst yEnd = project(-RANGE, RANGE, floorZ);\nconst zEnd = project(-RANGE, -RANGE, topZ);\n\nconst axisFrameSeries = [\n  { type: \"line\", data: [corner, xEnd], color: t.inkSoft, lineWidth: 2, marker: { enabled: false }, enableMouseTracking: false, showInLegend: false },\n  { type: \"line\", data: [corner, yEnd], color: t.inkSoft, lineWidth: 2, marker: { enabled: false }, enableMouseTracking: false, showInLegend: false },\n  { type: \"line\", data: [corner, zEnd], color: t.inkSoft, lineWidth: 2, marker: { enabled: false }, enableMouseTracking: false, showInLegend: false },\n];\n\n// --- Axis ticks + titles as labeled points (core dataLabels, no modules) ---\n// The three axes share one corner, so ticks placed there are offset in\n// different directions per axis (down / left / left) to keep the labels from\n// stacking on top of each other. The two \"-RANGE\" ticks (one per axis) sit\n// exactly on top of that shared corner point, so they get an extra push\n// beyond the normal offset to stay independently legible.\nconst tickVals = [-RANGE, 0, RANGE];\nconst tickPoints = [];\ntickVals.forEach((v) => {\n  const [sx, sy] = project(v, -RANGE, floorZ);\n  const cornerBoost = v === -RANGE ? 16 : 0;\n  tickPoints.push({ x: sx, y: sy, name: String(v), dataLabels: { x: 0, y: 18 + cornerBoost } });\n});\ntickVals.forEach((v) => {\n  const [sx, sy] = project(-RANGE, v, floorZ);\n  const cornerBoost = v === -RANGE ? 16 : 0;\n  tickPoints.push({ x: sx, y: sy, name: String(v), dataLabels: { x: -22 - cornerBoost, y: 0 } });\n});\n[zMin, 0, zMax].forEach((v) => {\n  const [sx, sy] = project(-RANGE, -RANGE, v);\n  tickPoints.push({ x: sx, y: sy, name: v.toFixed(1), dataLabels: { x: -26, y: 0 } });\n});\n\nconst [xTitleX, xTitleY] = project(RANGE * 1.18, -RANGE, floorZ);\nconst [yTitleX, yTitleY] = project(-RANGE, RANGE * 1.18, floorZ);\nconst [zTitleX, zTitleY] = project(-RANGE, -RANGE, topZ * 1.12);\nconst titlePoints = [\n  { x: xTitleX, y: xTitleY, name: \"X\" },\n  { x: yTitleX, y: yTitleY, name: \"Y\" },\n  { x: zTitleX, y: zTitleY, name: \"Z\" },\n];\n\nconst labelSeries = [\n  {\n    type: \"scatter\",\n    data: tickPoints,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n    dataLabels: {\n      enabled: true,\n      format: \"{point.name}\",\n      allowOverlap: true,\n      style: { color: t.inkSoft, fontSize: \"13px\", textOutline: \"none\" },\n    },\n  },\n  {\n    type: \"scatter\",\n    data: titlePoints,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n    dataLabels: {\n      enabled: true,\n      format: \"{point.name}\",\n      allowOverlap: true,\n      style: { color: t.ink, fontSize: \"16px\", fontWeight: \"600\", textOutline: \"none\" },\n    },\n  },\n];\n\n// --- Axis bounds: fit every projected coordinate with padding --------------\nconst allX = [];\nconst allY = [];\n[...projected.flat(), corner, xEnd, yEnd, zEnd, ...tickPoints, ...titlePoints].forEach((p) => {\n  const px = Array.isArray(p) ? p[0] : p.x;\n  const py = Array.isArray(p) ? p[1] : p.y;\n  allX.push(px);\n  allY.push(py);\n});\nconst padX = (Math.max(...allX) - Math.min(...allX)) * 0.08;\nconst padY = (Math.max(...allY) - Math.min(...allY)) * 0.08;\n\n// --- Chart -------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"wireframe-3d-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"z = sin(&#8730;(x&sup2; + y&sup2;)) ripple surface\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    visible: false,\n    min: Math.min(...allX) - padX,\n    max: Math.max(...allX) + padX,\n    startOnTick: false,\n    endOnTick: false,\n  },\n  yAxis: {\n    visible: false,\n    min: Math.min(...allY) - padY,\n    max: Math.max(...allY) + padY,\n    startOnTick: false,\n    endOnTick: false,\n    title: { text: null },\n  },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [...meshSeries, ...axisFrameSeries, ...labelSeries],\n});\n"}