{"spec_id":"area-elevation-profile","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\narea-elevation-profile: Terrain Elevation Profile Along Transect\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\nimport re\nimport xml.etree.ElementTree as ET\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens — Imprint palette\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Landmark type colors — Imprint palette with semantic cues\nTYPE_COLORS = {\n    \"summit\": \"#AE3030\",  # matte red — elevation peak, highest difficulty\n    \"pass\": \"#BD8233\",  # ochre — earthy mountain col\n    \"hut\": \"#C475FD\",  # lavender — alpine shelter, distinctive\n    \"valley\": \"#99B314\",  # lime — green valley floor\n    \"town\": \"#4467A3\",  # blue — settlement / trailhead\n}\n\n# Data — Alpine hiking trail elevation profile (120 km transect)\nnp.random.seed(42)\ndistances_km = np.linspace(0, 120, 200)\n\nbase_terrain = (\n    800\n    + 600 * np.sin(distances_km * np.pi / 40) ** 2\n    + 400 * np.sin(distances_km * np.pi / 25 + 1.2) ** 2\n    + 300 * np.exp(-((distances_km - 55) ** 2) / 80)\n    + 500 * np.exp(-((distances_km - 85) ** 2) / 120)\n    - 200 * np.exp(-((distances_km - 30) ** 2) / 50)\n)\nnoise = np.random.normal(0, 25, len(distances_km))\nelevation_m = base_terrain + noise\nelevation_m = np.maximum(elevation_m, 650)\n\nlandmarks = [\n    {\"name\": \"Grindelwald\", \"km\": 0, \"type\": \"town\"},\n    {\"name\": \"Kleine Scheidegg\", \"km\": 22, \"type\": \"pass\"},\n    {\"name\": \"Lauterbrunnen\", \"km\": 38, \"type\": \"valley\"},\n    {\"name\": \"Mürren\", \"km\": 55, \"type\": \"summit\"},\n    {\"name\": \"Blüemlisalp Hut\", \"km\": 72, \"type\": \"hut\"},\n    {\"name\": \"Hohtürli Pass\", \"km\": 85, \"type\": \"pass\"},\n    {\"name\": \"Kandersteg\", \"km\": 120, \"type\": \"town\"},\n]\n\nfor lm in landmarks:\n    idx = np.argmin(np.abs(distances_km - lm[\"km\"]))\n    lm[\"elev\"] = float(elevation_m[idx])\n\n# Title length: 80 chars → fontsize = round(66 × 67 / 80) = 55\nTITLE = \"Bernese Oberland Traverse · area-elevation-profile · python · pygal · anyplot.ai\"\n\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_PALETTE,\n    font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    title_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    title_font_size=55,\n    label_font_size=56,\n    major_label_font_size=44,\n    value_font_size=36,\n    legend_font_size=44,\n    legend_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    label_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    major_label_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    value_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    opacity=0.70,\n    opacity_hover=0.85,\n    stroke_opacity=1.0,\n    stroke_opacity_hover=1.0,\n    tooltip_font_size=32,\n    tooltip_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    tooltip_border_radius=8,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    title=TITLE,\n    x_title=\"Distance (km)\",\n    y_title=\"Elevation (m)\",\n    style=custom_style,\n    fill=True,\n    show_dots=False,\n    stroke_style={\"width\": 4},\n    show_y_guides=True,\n    show_x_guides=False,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_box_size=30,\n    value_formatter=lambda x: f\"{x:,.0f} m\",\n    x_value_formatter=lambda x: f\"{x:.0f} km\",\n    interpolate=\"cubic\",\n    interpolation_precision=300,\n    min_scale=5,\n    max_scale=10,\n    margin_bottom=90,\n    margin_left=90,\n    margin_right=120,\n    margin_top=50,\n    spacing=12,\n    tooltip_fancy_mode=True,\n    range=(550, 2100),\n    xrange=(0, 125),\n    show_minor_x_labels=False,\n    show_minor_y_labels=False,\n    truncate_legend=-1,\n    dynamic_print_values=True,\n    print_values=False,\n    show_x_labels=True,\n    show_y_labels=True,\n    x_labels_major_count=7,\n    y_labels_major_count=6,\n)\n\n# Elevation profile — first series uses Imprint position 1 (#009E73)\nprofile_data = [\n    {\"value\": (float(d), float(e)), \"label\": f\"{d:.1f} km — {e:.0f} m\"}\n    for d, e in zip(distances_km, elevation_m, strict=True)\n]\nchart.add(\"Elevation Profile\", profile_data)\n\n# Landmark markers — colored by type in SVG post-processing below\nlandmark_data = [\n    {\"value\": (float(lm[\"km\"]), lm[\"elev\"]), \"label\": f\"{lm['name']} — {lm['elev']:.0f} m\"} for lm in landmarks\n]\nchart.add(\"Landmarks\", landmark_data, fill=False, show_dots=True, dots_size=20, stroke=False)\n\n# Save interactive HTML with pygal's native tooltip support\nchart.render_to_file(f\"plot-{THEME}.html\")\n\n# Render SVG for annotation post-processing\nsvg_bytes = chart.render()\nSVG_NS = \"http://www.w3.org/2000/svg\"\nET.register_namespace(\"\", SVG_NS)\nET.register_namespace(\"xlink\", \"http://www.w3.org/1999/xlink\")\nroot = ET.fromstring(svg_bytes)\n\nparent_map = {child: parent for parent in root.iter() for child in parent}\ncircles = sorted(\n    [c for c in root.iter(f\"{{{SVG_NS}}}circle\") if float(c.get(\"r\", \"0\")) > 3], key=lambda c: float(c.get(\"cx\", \"0\"))\n)\n\n# Find plot bottom from horizontal guide paths (format: \"M0.0 Y h...\")\nplot_bottom_y = 0\nfor path in root.iter(f\"{{{SVG_NS}}}path\"):\n    cls = path.get(\"class\", \"\")\n    d_attr = path.get(\"d\", \"\")\n    if \"guide\" in cls and \" h\" in d_attr:\n        m = re.match(r\"M[\\d.]+ ([\\d.]+) h\", d_attr)\n        if m:\n            plot_bottom_y = max(plot_bottom_y, float(m.group(1)))\n\n# Stagger closely-spaced labels to avoid overlap\nlabel_offsets = {\n    \"Grindelwald\": -44,\n    \"Kleine Scheidegg\": -44,\n    \"Lauterbrunnen\": -44,\n    \"Mürren\": -56,\n    \"Blüemlisalp Hut\": -74,\n    \"Hohtürli Pass\": -44,\n    \"Kandersteg\": -44,\n}\n\nns = f\"{{{SVG_NS}}}\"\nfor i, circle in enumerate(circles[: len(landmarks)]):\n    parent_elem = parent_map.get(circle)\n    if parent_elem is None:\n        continue\n\n    cx, cy = float(circle.get(\"cx\", \"0\")), float(circle.get(\"cy\", \"0\"))\n    lm = landmarks[i]\n    lm_color = TYPE_COLORS.get(lm[\"type\"], \"#BD8233\")\n    y_off = label_offsets.get(lm[\"name\"], -44)\n\n    # Override circle fill to landmark-type color for clear visual differentiation\n    circle.set(\"fill\", lm_color)\n    circle.set(\"stroke\", PAGE_BG)\n    circle.set(\"stroke-width\", \"3\")\n\n    # Text anchor by position along transect\n    if i == 0:\n        anchor, dx = \"start\", 14\n    elif i == len(landmarks) - 1:\n        anchor, dx = \"end\", -14\n    else:\n        anchor, dx = \"middle\", 0\n\n    # Vertical dashed marker line from dot to plot baseline\n    vline = ET.SubElement(parent_elem, f\"{ns}line\")\n    for attr, val in [(\"x1\", cx), (\"y1\", cy), (\"x2\", cx), (\"y2\", plot_bottom_y)]:\n        vline.set(attr, f\"{val:.1f}\")\n    vline.set(\"stroke\", lm_color)\n    vline.set(\"stroke-width\", \"2.5\")\n    vline.set(\"stroke-dasharray\", \"8,5\")\n    vline.set(\"opacity\", \"0.65\")\n\n    # Landmark name — bold, colored by type\n    name_el = ET.SubElement(parent_elem, f\"{ns}text\")\n    name_el.set(\"x\", f\"{cx + dx:.1f}\")\n    name_el.set(\"y\", f\"{cy + y_off:.1f}\")\n    name_el.set(\"text-anchor\", anchor)\n    name_el.set(\"font-size\", \"32\")\n    name_el.set(\"font-family\", \"DejaVu Sans, Helvetica, Arial, sans-serif\")\n    name_el.set(\"fill\", lm_color)\n    name_el.set(\"font-weight\", \"bold\")\n    name_el.text = lm[\"name\"]\n\n    # Elevation value — muted secondary label below name\n    elev_el = ET.SubElement(parent_elem, f\"{ns}text\")\n    elev_el.set(\"x\", f\"{cx + dx:.1f}\")\n    elev_el.set(\"y\", f\"{cy + y_off + 32:.1f}\")\n    elev_el.set(\"text-anchor\", anchor)\n    elev_el.set(\"font-size\", \"30\")\n    elev_el.set(\"font-family\", \"DejaVu Sans, Helvetica, Arial, sans-serif\")\n    elev_el.set(\"fill\", INK_MUTED)\n    elev_el.text = f\"{lm['elev']:.0f} m\"\n\n# Vertical exaggeration note — bottom-right, more prominent than previous version\nnote_el = ET.SubElement(root, f\"{ns}text\")\nnote_el.set(\"x\", \"3080\")\nnote_el.set(\"y\", f\"{plot_bottom_y + 54:.0f}\")\nnote_el.set(\"text-anchor\", \"end\")\nnote_el.set(\"font-size\", \"32\")\nnote_el.set(\"font-family\", \"DejaVu Sans, Helvetica, Arial, sans-serif\")\nnote_el.set(\"fill\", INK_SOFT)\nnote_el.set(\"font-style\", \"italic\")\nnote_el.text = \"Vertical exaggeration ~10× for terrain visibility\"\n\n# Render final PNG\ncairosvg.svg2png(bytestring=ET.tostring(root, encoding=\"unicode\").encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n"}