{"spec_id":"map-route-path","library":"echarts","language":"javascript","code":"// anyplot.ai\n// map-route-path: Route Path Map\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic): TransAmerica-style cycling route -----\n// Named waypoints along a coast-to-coast bicycle route, west to east.\nconst ANCHORS = [\n  { name: \"Astoria, OR\", lon: -123.83, lat: 46.19, elev: 10 },\n  { name: \"Portland, OR\", lon: -122.68, lat: 45.52, elev: 50 },\n  { name: \"The Dalles, OR\", lon: -121.18, lat: 45.6, elev: 100 },\n  { name: \"John Day, OR\", lon: -118.95, lat: 44.42, elev: 3080 },\n  { name: \"Baker City, OR\", lon: -117.83, lat: 44.77, elev: 3451 },\n  { name: \"Boise, ID\", lon: -116.2, lat: 43.62, elev: 2704 },\n  { name: \"Twin Falls, ID\", lon: -114.47, lat: 42.56, elev: 3745 },\n  { name: \"Jackson, WY\", lon: -110.76, lat: 43.48, elev: 6237 },\n  { name: \"Togwotee Pass, WY\", lon: -110.06, lat: 43.75, elev: 9658 },\n  { name: \"Dubois, WY\", lon: -109.62, lat: 43.53, elev: 6917 },\n  { name: \"Rawlins, WY\", lon: -107.24, lat: 41.79, elev: 6742 },\n  { name: \"Hoosier Pass, CO\", lon: -106.06, lat: 39.36, elev: 11542 },\n  { name: \"Pueblo, CO\", lon: -104.61, lat: 38.25, elev: 4692 },\n  { name: \"Larned, KS\", lon: -99.1, lat: 38.18, elev: 2016 },\n  { name: \"Chanute, KS\", lon: -95.46, lat: 37.68, elev: 972 },\n  { name: \"Farmington, MO\", lon: -90.42, lat: 37.78, elev: 928 },\n  { name: \"Elizabethtown, KY\", lon: -85.86, lat: 37.69, elev: 771 },\n  { name: \"Berea, KY\", lon: -84.3, lat: 37.57, elev: 1043 },\n  { name: \"Christiansburg, VA\", lon: -80.41, lat: 37.13, elev: 2076 },\n  { name: \"Charlottesville, VA\", lon: -78.48, lat: 38.03, elev: 480 },\n  { name: \"Yorktown, VA\", lon: -76.51, lat: 37.24, elev: 10 },\n];\n\n// Fixed-seed LCG so the \"raw GPS noise\" is reproducible across renders.\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\n\n// Densify each anchor-to-anchor leg into noisy intermediate fixes, mimicking\n// a real GPS track, then smooth with a moving average to clean the noise —\n// the anchors themselves stay exact so the named waypoints keep their place.\nconst STEPS = 5;\nconst rawTrack = [{ ...ANCHORS[0] }];\nfor (let i = 0; i < ANCHORS.length - 1; i++) {\n  const a = ANCHORS[i];\n  const b = ANCHORS[i + 1];\n  for (let s = 1; s < STEPS; s++) {\n    const f = s / STEPS;\n    rawTrack.push({\n      lon: a.lon + (b.lon - a.lon) * f + (rand() - 0.5) * 0.05,\n      lat: a.lat + (b.lat - a.lat) * f + (rand() - 0.5) * 0.05,\n      elev: a.elev + (b.elev - a.elev) * f + (rand() - 0.5) * 220,\n    });\n  }\n  rawTrack.push({ ...b });\n}\n\nconst track = rawTrack.map((p, i) => {\n  if (i === 0 || i === rawTrack.length - 1) return p;\n  const prev = rawTrack[i - 1];\n  const next = rawTrack[i + 1];\n  return {\n    lon: (prev.lon + p.lon + next.lon) / 3,\n    lat: (prev.lat + p.lat + next.lat) / 3,\n    elev: (prev.elev + p.elev + next.elev) / 3,\n  };\n});\n\nconst elevations = track.map((p) => p.elev);\nconst minElev = Math.min(...elevations);\nconst maxElev = Math.max(...elevations);\n\nconst routeSegments = track.slice(0, -1).map((p, i) => {\n  const n = track[i + 1];\n  return {\n    coords: [\n      [p.lon, p.lat],\n      [n.lon, n.lat],\n    ],\n    value: (p.elev + n.elev) / 2,\n  };\n});\n\n// Direction arrows every N points, rotated to the bearing of the next fix.\nconst ARROW_STEP = 12;\nconst arrowData = [];\nfor (let i = ARROW_STEP; i < track.length - ARROW_STEP / 2; i += ARROW_STEP) {\n  const p = track[i];\n  const n = track[i + 1];\n  const bearing = Math.atan2(n.lon - p.lon, n.lat - p.lat) * (180 / Math.PI);\n  arrowData.push({ value: [p.lon, p.lat], symbolRotate: bearing });\n}\n\nconst start = track[0];\nconst finish = track[track.length - 1];\n\n// Hand-simplified continental-US coastline (lon/lat vertex list) so the route\n// reads against a base map — no bundled GeoJSON or network fetch is\n// available to this offline browser runtime, so this is a low-vertex\n// approximation, not authoritative coastline data.\nconst usOutline = [\n  [-124, 48], [-124, 42], [-121, 39], [-117, 33], [-115, 32.5],\n  [-111, 31.3], [-106, 31.8], [-103, 29], [-99, 26.5], [-97, 26],\n  [-94, 29.5], [-89, 29], [-85, 29.5], [-82, 25], [-80, 26],\n  [-81, 31], [-79, 33], [-76, 35], [-75, 38], [-74, 40],\n  [-71, 41], [-70, 43], [-67, 45], [-69, 47], [-75, 45],\n  [-83, 42], [-87, 45], [-90, 47], [-95, 49], [-104, 49],\n  [-114, 49], [-123, 49], [-124, 48],\n];\n\n// --- Init --------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------\nconst title = \"TransAmerica Bicycle Trail · map-route-path · javascript · echarts · anyplot.ai\";\nconst titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    subtext: \"Simulated GPS track, Astoria OR → Yorktown VA · smoothed from noisy raw fixes · arrows show travel direction\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (p) =>\n      p.seriesType === \"lines\" ? `${Math.round(p.data.value).toLocaleString()} ft` : p.name,\n  },\n  visualMap: {\n    type: \"continuous\",\n    min: minElev,\n    max: maxElev,\n    seriesIndex: 1,\n    orient: \"vertical\",\n    right: 24,\n    top: \"middle\",\n    itemWidth: 14,\n    itemHeight: 160,\n    text: [\"High elevation (ft)\", \"Low elevation (ft)\"],\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    inRange: { color: t.seq },\n    calculable: false,\n    hoverLink: false,\n  },\n  grid: { left: 100, right: 150, top: 190, bottom: 110, containLabel: true },\n  dataZoom: [\n    { type: \"inside\", xAxisIndex: 0, filterMode: \"none\" },\n    { type: \"inside\", yAxisIndex: 0, filterMode: \"none\" },\n  ],\n  xAxis: {\n    type: \"value\",\n    min: -128,\n    max: -68,\n    interval: 10,\n    name: \"Longitude\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, formatter: (v) => `${Math.abs(v)}°W` },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    min: 24,\n    max: 50,\n    interval: 5,\n    name: \"Latitude\",\n    nameLocation: \"middle\",\n    nameGap: 55,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, formatter: (v) => `${v}°N` },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Landmass\",\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      silent: true,\n      z: 1,\n      data: [0],\n      renderItem: (params, api) => ({\n        type: \"polygon\",\n        shape: { points: usOutline.map((p) => api.coord(p)) },\n        style: { fill: t.grid, opacity: 0.6, stroke: t.inkSoft, lineWidth: 1, strokeOpacity: 0.4 },\n      }),\n    },\n    {\n      name: \"Route\",\n      type: \"lines\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      symbol: [\"none\", \"none\"],\n      lineStyle: { width: 5, opacity: 0.9 },\n      data: routeSegments,\n      z: 2,\n    },\n    {\n      name: \"Direction\",\n      type: \"scatter\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      silent: true,\n      symbol: \"triangle\",\n      symbolSize: 15,\n      itemStyle: { color: t.muted, opacity: 0.8 },\n      data: arrowData,\n      z: 3,\n    },\n    {\n      name: \"Start\",\n      type: \"scatter\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      symbol: \"circle\",\n      symbolSize: 26,\n      data: [{ value: [start.lon, start.lat], name: \"Start\" }],\n      itemStyle: { color: t.palette[0], borderColor: t.pageBg, borderWidth: 2, opacity: 1 },\n      label: { show: true, formatter: \"Start\", position: \"top\", color: t.ink, fontSize: 14, fontWeight: 600 },\n      z: 4,\n    },\n    {\n      name: \"Finish\",\n      type: \"scatter\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      symbol: \"rect\",\n      symbolSize: 22,\n      data: [{ value: [finish.lon, finish.lat], name: \"Finish\" }],\n      itemStyle: { color: t.palette[4], borderColor: t.pageBg, borderWidth: 2, opacity: 1 },\n      label: { show: true, formatter: \"Finish\", position: \"top\", color: t.ink, fontSize: 14, fontWeight: 600 },\n      z: 4,\n    },\n  ],\n});\n"}