{"spec_id":"map-route-path","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nmap-route-path: Route Path Map\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 81/100 | Updated: 2026-05-21\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data - Simulated hiking trail GPS track (Central Park loop, NYC)\nnp.random.seed(42)\n\nn_points = 100\nt = np.linspace(0, 2 * np.pi, n_points)\n\n# Base path: elongated loop shape\nbase_lon = -73.97 + 0.015 * np.sin(t)\nbase_lat = 40.77 + 0.025 * np.cos(t) * (1 + 0.3 * np.sin(2 * t))\n\n# Add small GPS noise for realism\nlon = base_lon + np.random.normal(0, 0.0005, n_points)\nlat = base_lat + np.random.normal(0, 0.0003, n_points)\n\n# Elapsed-time label for each waypoint (45-minute hike, ~1 point per 27 s)\ntotal_min = 45\ntimestamps = [f\"{int(i * total_min / (n_points - 1)):02d}min\" for i in range(n_points)]\n\n# Style\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=6,\n)\n\n# Chart — XY chart maps longitude/latitude as Cartesian coordinates;\n# truncate_label=-1 prevents pygal from auto-truncating axis tick labels,\n# x_label_rotation=45 gives each label room to render in full\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"map-route-path · python · pygal · anyplot.ai\",\n    x_title=\"Longitude (degrees)\",\n    y_title=\"Latitude (degrees)\",\n    show_dots=False,\n    stroke_style={\"width\": 6, \"linecap\": \"round\", \"linejoin\": \"round\"},\n    show_x_guides=True,\n    show_y_guides=True,\n    legend_at_bottom=True,\n    legend_box_size=32,\n    truncate_legend=-1,\n    truncate_label=-1,\n    x_label_rotation=45,\n    value_formatter=lambda v: f\"Lon {v[0]:.4f}°, Lat {v[1]:.4f}°\" if isinstance(v, (tuple, list)) else str(v),\n)\n\n# Explicit sparse x-axis tick positions — 7 values avoids density-driven truncation\nchart.x_labels = np.linspace(lon.min(), lon.max(), 7).tolist()\n\n# START marker — brand green (Okabe-Ito pos 1), oversized dot for visual prominence\nchart.add(\n    \"START\",\n    [{\"value\": (float(lon[0]), float(lat[0])), \"label\": f\"Trail head — {timestamps[0]}\"}],\n    dots_size=22,\n    stroke=False,\n    show_dots=True,\n)\n\n# FINISH marker — vermillion (Okabe-Ito pos 2), medium dot clearly smaller than START\nchart.add(\n    \"FINISH\",\n    [{\"value\": (float(lon[-1]), float(lat[-1])), \"label\": f\"Trail end — {timestamps[-1]}\"}],\n    dots_size=14,\n    stroke=False,\n    show_dots=True,\n)\n\n# Route segments — time-progression color (Okabe-Ito positions 3–7)\n# Stroke width follows a bell curve to suggest pace variation: slower at start/end,\n# peak effort in the middle third of the hike\nn_segments = 5\nsegment_size = n_points // n_segments\nseg_labels = [\"0–9 min\", \"9–18 min\", \"18–27 min\", \"27–36 min\", \"36–45 min\"]\nseg_widths = [5, 6, 8, 6, 5]\n\nfor i in range(n_segments):\n    start_idx = i * segment_size\n    end_idx = min((i + 1) * segment_size + 1, n_points)\n    segment_data = [\n        {\"value\": (float(lon[j]), float(lat[j])), \"label\": f\"Waypoint {j + 1} at {timestamps[j]}\"}\n        for j in range(start_idx, end_idx)\n    ]\n    chart.add(\n        seg_labels[i], segment_data, stroke_style={\"width\": seg_widths[i], \"linecap\": \"round\", \"linejoin\": \"round\"}\n    )\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}