{"spec_id":"area-mountain-panorama","library":"d3","language":"javascript","code":"// anyplot.ai\n// area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks\n// Library: d3 7.9.0 | JavaScript 22.23.0\n// Quality: 91/100 | Created: 2026-06-30\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 120, right: 65, bottom: 62, left: 85 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\nconst theme = window.ANYPLOT_THEME;\n\n// Deterministic Park-Miller LCG (works within JS double precision)\nlet rngSeed = 42;\nconst rand = () => { rngSeed = (rngSeed * 16807) % 2147483647; return rngSeed / 2147483647; };\n\n// 16 Wallis 4000-m peaks — ls/rs: left/right slope (m/°), lv: label level, ann: annotate\nconst peakDefs = [\n  { name: \"Dom\",           angle: 200, elev: 4545, ls: 120, rs: 200, lv: 1, ann: true  },\n  { name: \"Täschhorn\",     angle: 207, elev: 4491, ls: 180, rs: 150, lv: 0, ann: false },\n  { name: \"Alphubel\",      angle: 215, elev: 4206, ls: 100, rs: 120, lv: 0, ann: true  },\n  { name: \"Allalinhorn\",   angle: 225, elev: 4027, ls: 110, rs:  90, lv: 2, ann: true  },\n  { name: \"Rimpfischhorn\", angle: 232, elev: 4199, ls: 140, rs: 120, lv: 0, ann: false },\n  { name: \"Strahlhorn\",    angle: 238, elev: 4190, ls:  90, rs: 110, lv: 1, ann: true  },\n  { name: \"Monte Rosa\",    angle: 248, elev: 4634, ls: 130, rs: 160, lv: 3, ann: true  },\n  { name: \"Liskamm\",       angle: 257, elev: 4527, ls: 180, rs: 200, lv: 2, ann: true  },\n  { name: \"Castor\",        angle: 263, elev: 4223, ls: 200, rs: 200, lv: 0, ann: false },\n  { name: \"Pollux\",        angle: 267, elev: 4092, ls: 200, rs: 180, lv: 0, ann: false },\n  { name: \"Breithorn\",     angle: 273, elev: 4164, ls: 150, rs: 100, lv: 0, ann: true  },\n  { name: \"Matterhorn\",    angle: 283, elev: 4478, ls: 280, rs: 320, lv: 2, ann: true  },\n  { name: \"Dent Blanche\",  angle: 297, elev: 4358, ls: 120, rs: 140, lv: 1, ann: true  },\n  { name: \"Ob. Gabelhorn\", angle: 305, elev: 4063, ls: 130, rs: 110, lv: 0, ann: true  },\n  { name: \"Zinalrothorn\",  angle: 314, elev: 4221, ls: 120, rs: 130, lv: 1, ann: true  },\n  { name: \"Weisshorn\",     angle: 324, elev: 4506, ls: 100, rs:  80, lv: 2, ann: true  },\n];\n\n// Skyline: piecewise-linear tent functions (asymmetric slopes) + rocky jitter\nconst FLOOR = 3100, ANGLE_MIN = 192, ANGLE_MAX = 332, N_SAMPLES = 900;\nconst skyline = Array.from({ length: N_SAMPLES }, (_, i) => {\n  const angle = ANGLE_MIN + (i / (N_SAMPLES - 1)) * (ANGLE_MAX - ANGLE_MIN);\n  let elev = FLOOR;\n  for (const p of peakDefs) {\n    const da = angle - p.angle;\n    const contrib = da < 0 ? p.elev + da * p.ls : p.elev - da * p.rs;\n    if (contrib > elev) elev = contrib;\n  }\n  // Blend in rocky noise only well above valley floor\n  const noise = (rand() - 0.5) * 55;\n  const blend = Math.max(0, Math.min(1, (elev - FLOOR - 80) / 300));\n  return { angle, elev: elev + noise * blend };\n});\n\n// SVG root\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\nconst defs = svg.append(\"defs\");\n\n// Clip to inner area\ndefs.append(\"clipPath\").attr(\"id\", \"clip-inner\")\n  .append(\"rect\").attr(\"width\", iw).attr(\"height\", ih);\n\n// Sky gradient (vertical, top → horizon)\nconst skyGrad = defs.append(\"linearGradient\")\n  .attr(\"id\", \"grad-sky\").attr(\"x1\", \"0\").attr(\"y1\", \"0\").attr(\"x2\", \"0\").attr(\"y2\", \"1\");\nconst skyStops = theme === \"light\"\n  ? [[\"0%\", \"#6FB2D2\"], [\"65%\", \"#BDD9EE\"], [\"100%\", \"#DCEEF8\"]]\n  : [[\"0%\", \"#030A12\"], [\"65%\", \"#0B1A28\"], [\"100%\", \"#15283A\"]];\nskyStops.forEach(([offset, color]) =>\n  skyGrad.append(\"stop\").attr(\"offset\", offset).attr(\"stop-color\", color)\n);\n\n// Silhouette gradient (rock face: lighter at ridge crest, darker at base)\nconst silGrad = defs.append(\"linearGradient\")\n  .attr(\"id\", \"grad-sil\").attr(\"x1\", \"0\").attr(\"y1\", \"0\").attr(\"x2\", \"0\").attr(\"y2\", \"1\");\nsilGrad.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", \"#1E3044\");\nsilGrad.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", \"#0C1520\");\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Sky fill\ng.append(\"rect\").attr(\"clip-path\", \"url(#clip-inner)\")\n  .attr(\"width\", iw).attr(\"height\", ih).attr(\"fill\", \"url(#grad-sky)\");\n\n// Scales\nconst ELEV_MIN = 3050, ELEV_MAX = 5400;\nconst xScale = d3.scaleLinear().domain([ANGLE_MIN, ANGLE_MAX]).range([0, iw]);\nconst yScale = d3.scaleLinear().domain([ELEV_MIN, ELEV_MAX]).range([ih, 0]);\n\n// Silhouette area (fills from bottom up to ridgeline)\ng.append(\"path\").datum(skyline)\n  .attr(\"clip-path\", \"url(#clip-inner)\")\n  .attr(\"d\", d3.area()\n    .x(d => xScale(d.angle)).y0(ih).y1(d => yScale(d.elev))\n    .curve(d3.curveLinear))\n  .attr(\"fill\", \"url(#grad-sil)\");\n\n// Ridgeline outline stroke\ng.append(\"path\").datum(skyline)\n  .attr(\"clip-path\", \"url(#clip-inner)\")\n  .attr(\"d\", d3.line()\n    .x(d => xScale(d.angle)).y(d => yScale(d.elev))\n    .curve(d3.curveLinear))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", theme === \"light\" ? \"#2C3E52\" : \"#476278\")\n  .attr(\"stroke-width\", 1.2);\n\n// Horizontal gridlines\n[3200, 3400, 3600, 3800, 4000, 4200, 4400, 4600].forEach(v => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", yScale(v)).attr(\"y2\", yScale(v))\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 0.7);\n});\n\n// Y axis (elevation in metres)\nconst yAxisG = g.append(\"g\").call(\n  d3.axisLeft(yScale)\n    .tickValues([3200, 3400, 3600, 3800, 4000, 4200, 4400, 4600])\n    .tickFormat(d => `${d} m`)\n    .tickSize(5)\n);\nyAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nyAxisG.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\nyAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// Compass bearing labels along bottom\n[\n  { angle: 200, label: \"SSW\" }, { angle: 225, label: \"SW\" },\n  { angle: 248, label: \"WSW\" }, { angle: 270, label: \"W\" },\n  { angle: 295, label: \"WNW\" }, { angle: 315, label: \"NW\" },\n].forEach(({ angle, label }) => {\n  g.append(\"text\")\n    .attr(\"x\", xScale(angle)).attr(\"y\", ih + 42)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n    .text(label);\n});\n\n// Peak annotations: leader line + name + elevation\n// 4 stagger levels (px above peak apex): 0→38, 1→78, 2→118, 3→158\nconst LEVELS = [38, 78, 118, 158];\nconst lineColor = theme === \"light\" ? \"#8AABBF\" : \"#557080\";\nconst dotColor  = theme === \"light\" ? \"#B0CCDA\" : \"#607888\";\n\npeakDefs.filter(p => p.ann).forEach(peak => {\n  const px = xScale(peak.angle);\n  const py = yScale(peak.elev);\n  const lyOff = LEVELS[peak.lv];\n  const labelY = py - lyOff;\n\n  // Dot at summit\n  g.append(\"circle\").attr(\"cx\", px).attr(\"cy\", py).attr(\"r\", 2.5).attr(\"fill\", dotColor);\n\n  // Dashed leader line from dot up to label baseline\n  g.append(\"line\")\n    .attr(\"x1\", px).attr(\"y1\", py - 5)\n    .attr(\"x2\", px).attr(\"y2\", labelY + 26)\n    .attr(\"stroke\", lineColor).attr(\"stroke-width\", 0.9)\n    .attr(\"stroke-dasharray\", \"3,2\");\n\n  // Peak name\n  g.append(\"text\")\n    .attr(\"x\", px).attr(\"y\", labelY)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink).style(\"font-size\", \"13px\").style(\"font-weight\", \"600\")\n    .text(peak.name);\n\n  // Elevation\n  g.append(\"text\")\n    .attr(\"x\", px).attr(\"y\", labelY + 17)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\")\n    .text(`${peak.elev} m`);\n});\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"area-mountain-panorama · javascript · d3 · anyplot.ai\");\n"}