{"spec_id":"area-elevation-profile","library":"altair","language":"python","code":"\"\"\" anyplot.ai\narea-elevation-profile: Terrain Elevation Profile Along Transect\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path to avoid shadowing the altair library\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme-adaptive tokens (Imprint style guide)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette (first series always #009E73)\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Landmark type → Imprint semantic color (water=cyan, sky/peak=blue, earth=ochre, vegetation=lime, built=muted)\nLM_DOMAIN = [\"summit\", \"lake\", \"pass\", \"plateau\", \"town\"]\nLM_COLORS = [\"#4467A3\", \"#2ABCCD\", \"#BD8233\", \"#99B314\", INK_MUTED]\nLM_SHAPES = [\"triangle-up\", \"diamond\", \"cross\", \"square\", \"circle\"]\n\n# --- Data: Alpine hiking trail ~120 km with realistic terrain ---\nnp.random.seed(42)\nnum_points = 480\ndistance = np.linspace(0, 120, num_points)\n\nelevation = 900 + np.zeros(num_points)\nelevation += 1000 * np.sin(distance * np.pi / 60) ** 2\nelevation += 500 * np.sin(distance * np.pi / 30 + 1.2) ** 2\nelevation += 250 * np.sin(distance * np.pi / 15 + 0.5)\nelevation += np.cumsum(np.random.randn(num_points) * 3)\nelevation += np.random.randn(num_points) * 15\nkernel = np.ones(5) / 5\nelevation = np.convolve(elevation, kernel, mode=\"same\")\nelevation = np.clip(elevation, 600, 2800)\n\ndf = pd.DataFrame({\"distance\": distance, \"elevation\": elevation})\n\nlandmarks = pd.DataFrame(\n    {\n        \"name\": [\n            \"Grindelwald (Start)\",\n            \"Bachsee Lake\",\n            \"Faulhorn Summit\",\n            \"Schynige Platte\",\n            \"Kleine Scheidegg\",\n            \"Männlichen Summit\",\n            \"Wengen (End)\",\n        ],\n        \"distance\": [0.0, 18.0, 35.0, 55.0, 75.0, 95.0, 120.0],\n        \"type\": [\"town\", \"lake\", \"summit\", \"plateau\", \"pass\", \"summit\", \"town\"],\n    }\n)\nlandmarks[\"elevation\"] = np.interp(landmarks[\"distance\"], distance, elevation)\nlandmarks[\"label\"] = landmarks.apply(lambda r: f\"{r['name']}\\n{r['elevation']:.0f} m\", axis=1)\n\ny_min = int(np.floor(elevation.min() / 100) * 100)\n\n# --- Chart layers ---\n\n# Terrain silhouette: Imprint sequential gradient (green → blue, bottom-to-top)\narea = (\n    alt.Chart(df)\n    .mark_area(\n        line={\"color\": IMPRINT_PALETTE[0], \"strokeWidth\": 2.5},\n        color=alt.Gradient(\n            gradient=\"linear\",\n            stops=[\n                alt.GradientStop(color=\"rgba(0,158,115,0.05)\", offset=0),\n                alt.GradientStop(color=\"rgba(0,158,115,0.28)\", offset=0.35),\n                alt.GradientStop(color=\"rgba(68,103,163,0.65)\", offset=1),\n            ],\n            x1=1,\n            x2=1,\n            y1=1,\n            y2=0,\n        ),\n    )\n    .encode(\n        x=alt.X(\"distance:Q\", title=\"Distance (km)\", scale=alt.Scale(domain=[0, 120])),\n        y=alt.Y(\"elevation:Q\", title=\"Elevation (m)\", scale=alt.Scale(domain=[y_min, 2800])),\n        tooltip=[\n            alt.Tooltip(\"distance:Q\", title=\"Distance (km)\", format=\".1f\"),\n            alt.Tooltip(\"elevation:Q\", title=\"Elevation (m)\", format=\".0f\"),\n        ],\n    )\n)\n\n# Dashed vertical rules at each landmark\nlandmark_rules = (\n    alt.Chart(landmarks)\n    .mark_rule(strokeWidth=1, strokeDash=[5, 4], opacity=0.35, color=INK_MUTED)\n    .encode(x=\"distance:Q\")\n)\n\n# Landmark points with Imprint semantic colors and shape-by-type\nlandmark_points = (\n    alt.Chart(landmarks)\n    .mark_point(size=120, filled=True, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=\"distance:Q\",\n        y=\"elevation:Q\",\n        shape=alt.Shape(\"type:N\", legend=None, scale=alt.Scale(domain=LM_DOMAIN, range=LM_SHAPES)),\n        color=alt.Color(\"type:N\", legend=None, scale=alt.Scale(domain=LM_DOMAIN, range=LM_COLORS)),\n    )\n)\n\n# Labels: 4 lean layers (start / end / mid-low / mid-high) — common style via _kw\nlm_start = landmarks[landmarks[\"distance\"] == 0.0]\nlm_end = landmarks[landmarks[\"distance\"] == 120.0]\nlm_mid = landmarks[(landmarks[\"distance\"] > 0) & (landmarks[\"distance\"] < 120)]\nlm_mid_low = lm_mid[lm_mid[\"elevation\"] < 1500]\nlm_mid_high = lm_mid[lm_mid[\"elevation\"] >= 1500]\n\n_kw = {\"fontSize\": 12, \"fontWeight\": \"bold\", \"lineBreak\": \"\\n\", \"lineHeight\": 16, \"color\": INK}\n\nlabel_start = (\n    alt.Chart(lm_start)\n    .mark_text(align=\"left\", dx=8, dy=-55, **_kw)\n    .encode(x=\"distance:Q\", y=\"elevation:Q\", text=\"label:N\")\n)\nlabel_end = (\n    alt.Chart(lm_end)\n    .mark_text(align=\"right\", dx=-10, dy=-55, **_kw)\n    .encode(x=\"distance:Q\", y=\"elevation:Q\", text=\"label:N\")\n)\nlabel_mid_low = (\n    alt.Chart(lm_mid_low)\n    .mark_text(align=\"center\", dy=-55, **_kw)\n    .encode(x=\"distance:Q\", y=\"elevation:Q\", text=\"label:N\")\n)\nlabel_mid_high = (\n    alt.Chart(lm_mid_high)\n    .mark_text(align=\"center\", dy=-35, **_kw)\n    .encode(x=\"distance:Q\", y=\"elevation:Q\", text=\"label:N\")\n)\n\n# --- Compose ---\nchart = (\n    alt.layer(area, landmark_rules, landmark_points, label_start, label_mid_low, label_mid_high, label_end)\n    .properties(\n        width=620,\n        height=270,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"Bernese Oberland Trail · area-elevation-profile · altair · anyplot.ai\",\n            fontSize=16,\n            subtitle=\"120 km hiking transect from Grindelwald to Wengen  ·  Vertical exaggeration ~10×\",\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n            anchor=\"start\",\n            offset=10,\n            color=INK,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        gridOpacity=0.15,\n        grid=True,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_axisX(grid=False)\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# --- Save PNG with PAD-only to canonical 3200×1800 ---\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.interactive().save(f\"plot-{THEME}.html\")\n"}