{"spec_id":"area-elevation-profile","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\narea-elevation-profile: Terrain Elevation Profile Along Transect\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_area,\n    geom_line,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (Imprint palette — theme-adaptive chrome)\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# Slope steepness colors — semantic exception from Imprint palette:\n# green=easy/flat, amber(warning anchor)=moderate, red=steep/strenuous\nSLOPE_FLAT = \"#009E73\"  # Imprint position 1 — green, flat/gentle\nSLOPE_MOD = \"#DDCC77\"  # Imprint amber anchor — warning/caution, moderate slope\nSLOPE_STEEP = \"#AE3030\"  # Imprint position 5 — matte red, steep/strenuous\n\n# Data — Alpine hiking trail elevation profile (120 km)\nnp.random.seed(42)\nn_points = 480\ndistance = np.linspace(0, 120, n_points)\n\n# Realistic terrain: broad valley shape + ridges + noise\nbase_elevation = 1200\nbroad_shape = 600 * np.sin(distance * np.pi / 120)\nridge1 = 400 * np.exp(-((distance - 35) ** 2) / 80)\nridge2 = 550 * np.exp(-((distance - 65) ** 2) / 120)\nridge3 = 350 * np.exp(-((distance - 95) ** 2) / 60)\nvalley = -300 * np.exp(-((distance - 50) ** 2) / 50)\nnoise = np.cumsum(np.random.randn(n_points) * 3)\nelevation = base_elevation + broad_shape + ridge1 + ridge2 + ridge3 + valley + noise\nelevation = np.clip(elevation, 800, None)\n\ndf = pd.DataFrame({\"distance\": distance, \"elevation\": elevation})\n\n# Slope — smoothed rolling average to prevent rapid color switching\nslope_smooth = pd.Series(np.abs(np.gradient(elevation, distance))).rolling(window=25, center=True, min_periods=1).mean()\nslope_category = pd.cut(slope_smooth, bins=[0, 15, 40, np.inf], labels=[\"Flat / Gentle\", \"Moderate\", \"Steep\"])\ndf[\"slope_category\"] = slope_category\n\n# Landmarks\nlandmark_names = [\n    \"Talbach Village\",\n    \"Steinberg Pass\",\n    \"Grünsee Lake\",\n    \"Hochwand Summit\",\n    \"Felsentor Saddle\",\n    \"Alpenhof Hut\",\n    \"Gipfelkreuz Peak\",\n    \"Bergdorf Village\",\n]\nlandmark_distances = [0, 20, 38, 50, 65, 80, 95, 120]\nlandmark_elevations = [float(np.interp(d, distance, elevation)) for d in landmark_distances]\n\n# Two-tier label staggering: alternate high/low rows in the dense 20-65 km region\n# Positions 1,3 (Steinberg, Hochwand) go high; 2,4 (Grünsee, Felsentor) go low\n# Last label (Bergdorf Village) is right-aligned (hjust=1) so it doesn't overflow\nnudge_y = [200, 440, 160, 500, 180, 380, 440, 200]\nnudge_x = [-2, -5, 11, -5, 4, 1, 1, 0]\nlabel_hjust = [0, 0, 0, 0, 0, 0, 0, 1]\nlandmarks_df = pd.DataFrame(\n    {\n        \"distance\": landmark_distances,\n        \"elevation\": landmark_elevations,\n        \"name\": landmark_names,\n        \"label_y\": [e + n for e, n in zip(landmark_elevations, nudge_y, strict=True)],\n        \"label_x\": [d + n for d, n in zip(landmark_distances, nudge_x, strict=True)],\n        \"hjust\": label_hjust,\n    }\n)\n\n# Vertical landmark lines from terrain surface to baseline\ny_floor = int(min(elevation)) - 50\ny_max = int(max(elevation))\n\nsegments_df = pd.DataFrame(\n    {\"x\": landmark_distances, \"y\": landmark_elevations, \"yend\": [y_floor] * len(landmark_distances)}\n)\n\n# Title scaled for 91-char string: round(16 × 67/91) = 12\ntitle_str = \"Alpine Trail Elevation Profile · area-elevation-profile · python · letsplot · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot(df, aes(x=\"distance\", y=\"elevation\"))\n    # Terrain silhouette fill\n    + geom_area(fill=\"#4467A3\", alpha=0.35)\n    # Profile line colored by slope steepness\n    + geom_line(\n        aes(color=\"slope_category\"),\n        size=1.5,\n        tooltips=layer_tooltips()\n        .line(\"Elevation: @elevation m\")\n        .line(\"Distance: @distance km\")\n        .line(\"Slope: @slope_category\"),\n    )\n    + scale_color_manual(values=[SLOPE_FLAT, SLOPE_MOD, SLOPE_STEEP], name=\"Slope Steepness\")\n    # Vertical dashed landmark lines\n    + geom_segment(\n        data=segments_df,\n        mapping=aes(x=\"x\", y=\"yend\", xend=\"x\", yend=\"y\"),\n        color=INK_MUTED,\n        size=0.5,\n        linetype=\"dashed\",\n        inherit_aes=False,\n    )\n    # Landmark points — circle outline with page-background fill\n    + geom_point(\n        data=landmarks_df,\n        mapping=aes(x=\"distance\", y=\"elevation\"),\n        size=5,\n        color=SLOPE_FLAT,\n        fill=PAGE_BG,\n        shape=21,\n        stroke=2.0,\n        inherit_aes=False,\n        tooltips=layer_tooltips().line(\"@name\").line(\"Elevation: @elevation m\").line(\"Distance: @distance km\"),\n    )\n    # Dotted connector lines from labels to points\n    + geom_segment(\n        data=landmarks_df,\n        mapping=aes(x=\"distance\", y=\"elevation\", xend=\"label_x\", yend=\"label_y\"),\n        color=INK_MUTED,\n        size=0.35,\n        linetype=\"dotted\",\n        inherit_aes=False,\n    )\n    # Landmark labels — size 5mm ≈ 14pt, upright; last label right-aligned to avoid overflow\n    + geom_text(\n        data=landmarks_df,\n        mapping=aes(x=\"label_x\", y=\"label_y\", label=\"name\", hjust=\"hjust\"),\n        size=5,\n        color=INK,\n        fontface=\"bold\",\n        inherit_aes=False,\n    )\n    # Axis scales\n    + scale_x_continuous(name=\"Distance (km)\", breaks=list(range(0, 121, 20)), limits=[-3, 126])\n    + scale_y_continuous(\n        name=\"Elevation (m)\", limits=[y_floor, y_max + 400], breaks=list(range(1000, y_max + 400, 200))\n    )\n    + labs(title=title_str, subtitle=\"120 km Alpine hiking transect — 8 landmarks — vertical exaggeration ~10×\")\n    # Canvas — hard rule: ggsize(800, 450) + scale=4 → 3200 × 1800 px\n    + ggsize(800, 450)\n    + theme_minimal()\n    + theme(\n        axis_text=element_text(size=10, color=INK_SOFT),\n        axis_title=element_text(size=12, color=INK),\n        plot_title=element_text(size=12, color=INK, face=\"bold\"),\n        plot_subtitle=element_text(size=10, color=INK_SOFT),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=12, color=INK, face=\"bold\"),\n        legend_position=\"bottom\",\n        panel_grid_major_y=element_line(color=INK, size=0.15),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    )\n)\n\n# Save — scale=4 produces 3200 × 1800 px PNG; path=\".\" saves to current directory\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}