{"spec_id":"map-route-path","library":"d3","language":"javascript","code":"// anyplot.ai\n// map-route-path: Route Path Map\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: deterministic mountain-trail GPS track ---------------------------\n// Small linear-congruential generator so the waypoint wander is reproducible\n// (the browser has no seeded RNG).\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst n = 220;\nconst waypoints = [];\nlet lat = 46.82;\nlet lon = -121.58;\nlet heading = 95; // degrees from north — overall eastbound traverse through the pass\nfor (let i = 0; i < n; i++) {\n  const progress = i / (n - 1);\n  if (i > 0) {\n    // Switchback climb: the target bearing oscillates north/south of due\n    // east so the trail zigzags up the pass (realistic for a mountain\n    // trail, and it fills the canvas — a near-straight bearing leaves the\n    // north/south half of the map empty).\n    const targetHeading = 95 + 55 * Math.sin(progress * Math.PI * 6);\n    heading += (targetHeading - heading) * 0.15 + (rand() - 0.5) * 4;\n    const stepM = 45 + rand() * 20; // meters between consecutive GPS fixes\n    lat += (stepM * Math.cos((heading * Math.PI) / 180)) / 111320;\n    lon += (stepM * Math.sin((heading * Math.PI) / 180)) / (111320 * Math.cos((lat * Math.PI) / 180));\n  }\n  // Single climb-then-descend profile: trailhead -> pass -> different trailhead\n  const elevation = 1450 + 950 * Math.sin(Math.PI * progress) + (rand() - 0.5) * 25;\n  waypoints.push({ sequence: i, lat, lon, elevation });\n}\n\nfunction haversineM(a, b) {\n  const R = 6371000;\n  const rad = Math.PI / 180;\n  const dLat = (b.lat - a.lat) * rad;\n  const dLon = (b.lon - a.lon) * rad;\n  const s = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * rad) * Math.cos(b.lat * rad) * Math.sin(dLon / 2) ** 2;\n  return 2 * R * Math.asin(Math.sqrt(s));\n}\nlet totalDistanceM = 0;\nlet elevationGainM = 0;\nfor (let i = 1; i < waypoints.length; i++) {\n  totalDistanceM += haversineM(waypoints[i - 1], waypoints[i]);\n  const delta = waypoints[i].elevation - waypoints[i - 1].elevation;\n  if (delta > 0) elevationGainM += delta;\n}\nconst totalDistanceKm = totalDistanceM / 1000;\nconst peak = waypoints.reduce((a, b) => (b.elevation > a.elevation ? b : a));\nconst elevExtent = d3.extent(waypoints, (d) => d.elevation);\nconst elevColor = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain(elevExtent);\n\n// --- Layout ------------------------------------------------------------\nconst margin = { top: 140, right: 240, bottom: 110, left: 50 };\nconst mapW = width - margin.left - margin.right;\nconst mapH = height - margin.top - margin.bottom;\n\n// --- Projection: local Mercator fit to the track's bounding box ------------\nconst routeLine = { type: \"LineString\", coordinates: waypoints.map((d) => [d.lon, d.lat]) };\nconst projection = d3.geoMercator().fitExtent(\n  [\n    [30, 30],\n    [mapW - 30, mapH - 30],\n  ],\n  routeLine,\n);\nconst path = d3.geoPath(projection);\n\nconst lonExtent = d3.extent(waypoints, (d) => d.lon);\nconst latExtent = d3.extent(waypoints, (d) => d.lat);\nconst pad = 0.01;\nconst graticule = d3\n  .geoGraticule()\n  .extent([\n    [lonExtent[0] - pad, latExtent[0] - pad],\n    [lonExtent[1] + pad, latExtent[1] + pad],\n  ])\n  .step([0.02, 0.02]);\n\n// --- SVG mount -----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\nconst mapG = g.append(\"g\");\n\n// Map panel — delineates the geographic frame the route sits in\nmapG\n  .append(\"rect\")\n  .attr(\"x\", -10)\n  .attr(\"y\", -10)\n  .attr(\"width\", mapW + 20)\n  .attr(\"height\", mapH + 20)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// Lon/lat graticule — geographic reference grid for spatial context\nmapG\n  .append(\"path\")\n  .datum(graticule)\n  .attr(\"d\", path)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 0.7)\n  .attr(\"stroke-opacity\", 0.6);\n\n// Elevation contour rings around the pass — abstract topographic context\n// derived from the same elevation model as the route color (not real survey\n// data), drawn as true great-circle rings via d3.geoCircle so they stay\n// undistorted under the Mercator projection.\nconst contourLevels = [\n  { elevM: peak.elevation - 100, radiusKm: 0.35 },\n  { elevM: peak.elevation - 300, radiusKm: 0.75 },\n  { elevM: peak.elevation - 550, radiusKm: 1.2 },\n  { elevM: peak.elevation - 800, radiusKm: 1.7 },\n];\nfor (const c of contourLevels) {\n  const ring = d3\n    .geoCircle()\n    .center([peak.lon, peak.lat])\n    .radius(c.radiusKm / 111.32)();\n  mapG\n    .append(\"path\")\n    .datum(ring)\n    .attr(\"d\", path)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 0.8)\n    .attr(\"stroke-opacity\", 0.28)\n    .attr(\"stroke-dasharray\", \"2,3\");\n}\n// Label a couple of rings off to the southwest — away from the eastbound\n// route line so the numbers don't sit on top of the path or the arrows.\nconst labelBearing = (210 * Math.PI) / 180;\nfor (const c of [contourLevels[0], contourLevels[3]]) {\n  const labelLon = peak.lon + ((c.radiusKm / 111.32) * Math.sin(labelBearing)) / Math.cos((peak.lat * Math.PI) / 180);\n  const labelLat = peak.lat + (c.radiusKm / 111.32) * Math.cos(labelBearing);\n  const [lx, ly] = projection([labelLon, labelLat]);\n  mapG\n    .append(\"text\")\n    .attr(\"x\", lx)\n    .attr(\"y\", ly)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"11px\")\n    .text(`${Math.round(c.elevM)} m`);\n}\n\n// Route — consecutive segments colored by midpoint elevation\nconst segments = [];\nfor (let i = 0; i < waypoints.length - 1; i++) {\n  segments.push({\n    line: {\n      type: \"LineString\",\n      coordinates: [\n        [waypoints[i].lon, waypoints[i].lat],\n        [waypoints[i + 1].lon, waypoints[i + 1].lat],\n      ],\n    },\n    elevation: (waypoints[i].elevation + waypoints[i + 1].elevation) / 2,\n  });\n}\nmapG\n  .selectAll(\"path.route-segment\")\n  .data(segments)\n  .join(\"path\")\n  .attr(\"class\", \"route-segment\")\n  .attr(\"d\", (d) => path(d.line))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (d) => elevColor(d.elevation))\n  .attr(\"stroke-width\", 4)\n  .attr(\"stroke-linecap\", \"round\")\n  .attr(\"stroke-linejoin\", \"round\");\n\n// Direction arrows — travel-direction indicators along the path (static, not\n// simulated interactivity: each arrow is a fixed triangle oriented to the\n// local tangent).\nconst arrowStep = 28;\nconst arrows = [];\nfor (let i = arrowStep; i < waypoints.length - 4; i += arrowStep) {\n  const p0 = projection([waypoints[i - 2].lon, waypoints[i - 2].lat]);\n  const p1 = projection([waypoints[i + 2].lon, waypoints[i + 2].lat]);\n  const mid = projection([waypoints[i].lon, waypoints[i].lat]);\n  arrows.push({ x: mid[0], y: mid[1], angle: (Math.atan2(p1[1] - p0[1], p1[0] - p0[0]) * 180) / Math.PI });\n}\nmapG\n  .selectAll(\"path.arrow\")\n  .data(arrows)\n  .join(\"path\")\n  .attr(\"class\", \"arrow\")\n  .attr(\"d\", \"M -6,-5 L 7,0 L -6,5 Z\")\n  .attr(\"transform\", (d) => `translate(${d.x},${d.y}) rotate(${d.angle})`)\n  .attr(\"fill\", t.pageBg)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1)\n  .attr(\"fill-opacity\", 0.9);\n\n// Start (brand green circle) / finish (matte red square) endpoints\nconst startPt = projection([waypoints[0].lon, waypoints[0].lat]);\nconst endPt = projection([waypoints[waypoints.length - 1].lon, waypoints[waypoints.length - 1].lat]);\n\nmapG\n  .append(\"circle\")\n  .attr(\"cx\", startPt[0])\n  .attr(\"cy\", startPt[1])\n  .attr(\"r\", 11)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5);\nmapG\n  .append(\"text\")\n  .attr(\"x\", startPt[0])\n  .attr(\"y\", startPt[1] - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Start\");\n\nconst endSize = 16;\nmapG\n  .append(\"rect\")\n  .attr(\"x\", endPt[0] - endSize / 2)\n  .attr(\"y\", endPt[1] - endSize / 2)\n  .attr(\"width\", endSize)\n  .attr(\"height\", endSize)\n  .attr(\"fill\", t.palette[4])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5);\nmapG\n  .append(\"text\")\n  .attr(\"x\", endPt[0])\n  .attr(\"y\", endPt[1] - 20)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Finish\");\n\n// North arrow + distance scale bar\nconst chromeY = mapH + 30;\nmapG\n  .append(\"path\")\n  .attr(\"d\", \"M 0,-16 L 7,8 L 0,3 L -7,8 Z\")\n  .attr(\"transform\", `translate(20,${chromeY - 10})`)\n  .attr(\"fill\", t.inkSoft);\nmapG\n  .append(\"text\")\n  .attr(\"x\", 20)\n  .attr(\"y\", chromeY + 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(\"N\");\n\nconst meanLat = d3.mean(waypoints, (d) => d.lat);\nconst meanLon = d3.mean(waypoints, (d) => d.lon);\nconst degPerKm = 1000 / (111320 * Math.cos((meanLat * Math.PI) / 180));\nconst [sx0] = projection([meanLon, meanLat]);\nconst [sx1] = projection([meanLon + degPerKm, meanLat]);\nconst scaleBarPx = Math.abs(sx1 - sx0);\nconst scaleX = 60;\nmapG\n  .append(\"line\")\n  .attr(\"x1\", scaleX)\n  .attr(\"x2\", scaleX + scaleBarPx)\n  .attr(\"y1\", chromeY)\n  .attr(\"y2\", chromeY)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2);\nfor (const x of [scaleX, scaleX + scaleBarPx]) {\n  mapG\n    .append(\"line\")\n    .attr(\"x1\", x)\n    .attr(\"x2\", x)\n    .attr(\"y1\", chromeY - 5)\n    .attr(\"y2\", chromeY + 5)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 2);\n}\nmapG\n  .append(\"text\")\n  .attr(\"x\", scaleX + scaleBarPx / 2)\n  .attr(\"y\", chromeY + 20)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(\"1 km\");\n\n// --- Legend: elevation gradient + endpoint markers --------------------------\nconst legendW = 26;\nconst legendH = mapH * 0.45;\nconst legendX = mapW + 55;\nconst legendY = mapH * 0.06;\nconst nStops = 10;\n\nsvg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"elev-gradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y2\", \"0%\")\n  .selectAll(\"stop\")\n  .data(d3.range(nStops + 1))\n  .join(\"stop\")\n  .attr(\"offset\", (d) => `${(d / nStops) * 100}%`)\n  .attr(\"stop-color\", (d) => elevColor(elevExtent[0] + (d / nStops) * (elevExtent[1] - elevExtent[0])));\n\nconst legendG = g.append(\"g\").attr(\"transform\", `translate(${legendX},${legendY})`);\nconst legendTop = 26;\nlegendG\n  .append(\"rect\")\n  .attr(\"y\", legendTop)\n  .attr(\"width\", legendW)\n  .attr(\"height\", legendH)\n  .attr(\"fill\", \"url(#elev-gradient)\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nconst legendScale = d3.scaleLinear().domain(elevExtent).range([legendH + legendTop, legendTop]);\nconst legendAxis = d3\n  .axisRight(legendScale)\n  .ticks(5)\n  .tickFormat((d) => `${Math.round(d)}`);\nconst legendAxisG = legendG.append(\"g\").attr(\"transform\", `translate(${legendW},0)`).call(legendAxis);\nlegendAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nlegendAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nlegendAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nlegendG\n  .append(\"text\")\n  .attr(\"x\", legendW / 2)\n  .attr(\"y\", -6)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Elevation (m)\");\n\nconst startLegendY = legendTop + legendH + 55;\nlegendG\n  .append(\"circle\")\n  .attr(\"cx\", legendW / 2)\n  .attr(\"cy\", startLegendY)\n  .attr(\"r\", 8)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\nlegendG\n  .append(\"text\")\n  .attr(\"x\", legendW + 14)\n  .attr(\"y\", startLegendY + 4)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Start\");\n\nconst finishLegendY = startLegendY + 32;\nlegendG\n  .append(\"rect\")\n  .attr(\"x\", legendW / 2 - 7)\n  .attr(\"y\", finishLegendY - 7)\n  .attr(\"width\", 14)\n  .attr(\"height\", 14)\n  .attr(\"fill\", t.palette[4])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\nlegendG\n  .append(\"text\")\n  .attr(\"x\", legendW + 14)\n  .attr(\"y\", finishLegendY + 4)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Finish\");\n\n// --- Title -------------------------------------------------------------------\nconst title = \"Backcountry Trail Traverse · map-route-path · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.round(22 * (title.length > 67 ? 67 / title.length : 1));\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 76)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"GPS track of a point-to-point mountain hike, colored by elevation\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 102)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(`${totalDistanceKm.toFixed(1)} km · ${Math.round(elevationGainM)} m elevation gain · ${waypoints.length} waypoints`);\n"}