{"spec_id":"area-mountain-panorama","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\narea-mountain-panorama: Mountain Panorama Profile with Labeled Peaks\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-30\n\"\"\"\n\nimport os\nimport re\nimport sys\n\n\n# Script filename shadows the installed `pygal` package when run as `python pygal.py`;\n# dropping the script directory from sys.path lets the real package resolve.\nsys.path.pop(0)\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens (Imprint palette, theme-adaptive chrome)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nSKY_TOP = \"#BDD8EC\" if THEME == \"light\" else \"#1B2A3D\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"\n\n# Data — Bernese Oberland (Switzerland), thirteen peaks viewed W → E from north\npeaks = [\n    (\"Gspaltenhorn\", 15, 3437),\n    (\"Blümlisalp\", 32, 3660),\n    (\"Doldenhorn\", 50, 3638),\n    (\"Balmhorn\", 67, 3698),\n    (\"Altels\", 82, 3629),\n    (\"Wildstrubel\", 105, 3244),\n    (\"Schreckhorn\", 135, 4078),\n    (\"Finsteraarhorn\", 158, 4274),\n    (\"Eiger\", 192, 3967),\n    (\"Mönch\", 210, 4107),\n    (\"Jungfrau\", 230, 4158),\n    (\"Silberhorn\", 248, 3695),\n    (\"Grosshorn\", 265, 3754),\n]\n\n# Jagged silhouette: midpoint-displacement fractal base + asymmetric tent peaks\n# (spec explicitly prohibits Gaussian bumps — this ridge reads as alpine rock)\nnp.random.seed(42)\nN = 1024\nangle = np.linspace(0, 280, N)\n\nmdp_ridge = np.zeros(N)\nmdp_ridge[0] = 3100.0\nmdp_ridge[N - 1] = 3000.0\nstep = N - 1\namp = 310.0\nwhile step > 1:\n    half = step // 2\n    for i in range(0, N - step, step):\n        midval = (mdp_ridge[i] + mdp_ridge[i + step]) / 2.0\n        mdp_ridge[i + half] = midval + float(np.random.randn()) * amp\n    amp *= 0.62\n    step = half\nmdp_ridge = np.clip(mdp_ridge, 2750.0, 3450.0)\n\n# Asymmetric tent functions — distinct left/right slope ratios per peak for variety\nasym = [0.60, 0.80, 0.55, 0.75, 0.65, 0.85, 0.50, 0.70, 0.60, 0.90, 0.65, 0.75, 0.80]\nridge = mdp_ridge.copy()\nfor idx, (_name, pos, elev) in enumerate(peaks):\n    left_w = 7.0 + (elev - 3000) / 200.0\n    right_w = left_w * asym[idx]\n    peak_h = elev - 2600.0\n    bump = np.where(\n        angle <= pos,\n        peak_h * np.maximum(0.0, 1.0 - np.abs(angle - pos) / left_w),\n        peak_h * np.maximum(0.0, 1.0 - np.abs(angle - pos) / right_w),\n    )\n    ridge = np.maximum(ridge, 2600.0 + bump)\n\n# Fine-grain jaggedness along the full ridgeline\nridge += np.random.randn(N) * 18.0\n\n# Secondary depth ridge (more distant range peeking above main ridge valleys — DE-01 depth)\nnp.random.seed(99)\ndepth_ridge = np.zeros(N)\ndepth_ridge[0] = 3050.0\ndepth_ridge[N - 1] = 2950.0\nstep = N - 1\namp_d = 240.0\nwhile step > 1:\n    half = step // 2\n    for i in range(0, N - step, step):\n        midval = (depth_ridge[i] + depth_ridge[i + step]) / 2.0\n        depth_ridge[i + half] = midval + float(np.random.randn()) * amp_d\n    amp_d *= 0.62\n    step = half\ndepth_ridge = np.clip(depth_ridge, 2800.0, 3550.0)\ndepth_ridge += np.random.randn(N) * 10.0\n\n# Canvas — 3200×1800 landscape (hard contract per pygal library prompt)\nCANVAS_W, CANVAS_H = 3200, 1800\nMARGIN_L, MARGIN_R = 200, 80\nMARGIN_T, MARGIN_B = 170, 130\n\nY_FLOOR, Y_CEIL = 2400, 5000\nX_MIN, X_MAX = 0, 280\n\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\n\n# Imprint palette — first series is brand green (#009E73), peaks series uses INK\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=\"transparent\",\n    foreground=INK_SOFT,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(BRAND, INK if THEME == \"light\" else \"#F0EFE8\"),\n    font_family=font,\n    title_font_family=font,\n    label_font_family=font,\n    major_label_font_family=font,\n    tooltip_font_family=font,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    tooltip_font_size=22,\n    stroke_width=2,\n    opacity=\".95\",\n    opacity_hover=\".75\",\n    transition=\"200ms ease-in\",\n)\n\nchart = pygal.XY(\n    width=CANVAS_W,\n    height=CANVAS_H,\n    style=custom_style,\n    show_legend=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    show_x_guides=False,\n    show_y_guides=False,\n    margin_left=MARGIN_L,\n    margin_right=MARGIN_R,\n    margin_top=MARGIN_T,\n    margin_bottom=MARGIN_B,\n    xrange=(X_MIN, X_MAX),\n    range=(Y_FLOOR, Y_CEIL),\n    fill=True,\n    show_dots=False,\n    stroke_style={\"width\": 2},\n    truncate_label=-1,\n)\n\nridge_data = [{\"value\": (float(a), float(e))} for a, e in zip(angle, ridge, strict=False)]\nchart.add(\"Skyline\", ridge_data)\n\npeak_data = [{\"value\": (float(pos), float(elev)), \"label\": f\"{name} · {elev:,} m\"} for name, pos, elev in peaks]\nchart.add(\"Peaks\", peak_data, show_dots=True, dots_size=7, stroke=False, fill=False)\n\n# Render pygal SVG; back-compute plot-box from the two extreme summit dots\nbase_svg = chart.render(is_unicode=True)\n\ndot_re = re.compile(r'<circle cx=\"([-\\d.]+)\" cy=\"([-\\d.]+)\"[^>]*class=\"dot')\ndots = [(float(cx), float(cy)) for cx, cy in dot_re.findall(base_svg)]\n(p1x, p1y) = dots[0]\n(p2x, p2y) = dots[-1]\nref_a, ref_b = peaks[0], peaks[-1]\n\n# Linear mapping: data → SVG pixel (calibrated from dot positions)\nx_scale = (p2x - p1x) / (ref_b[1] - ref_a[1])\nx_off = p1x - ref_a[1] * x_scale\ny_scale = (p2y - p1y) / (ref_b[2] - ref_a[2])\ny_off = p1y - ref_a[2] * y_scale\n\n# Plot-box corners via inlined transform (svg = data * scale + offset)\nplot_x_left = X_MIN * x_scale + x_off\nplot_x_right = X_MAX * x_scale + x_off\nplot_y_top = Y_CEIL * y_scale + y_off\nplot_y_bottom = Y_FLOOR * y_scale + y_off\nplot_w = plot_x_right - plot_x_left\nplot_h = plot_y_bottom - plot_y_top\n\n# SVG chrome — injected before pygal's plot group so silhouette and dots stay on top\nsvg_parts = [\n    f'<rect x=\"0\" y=\"0\" width=\"{CANVAS_W}\" height=\"{CANVAS_H}\" fill=\"{PAGE_BG}\" stroke=\"none\"/>',\n    f\"\"\"<defs>\n        <linearGradient id=\"skyGrad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop offset=\"0%\" stop-color=\"{SKY_TOP}\"/>\n            <stop offset=\"100%\" stop-color=\"{PAGE_BG}\"/>\n        </linearGradient>\n        <clipPath id=\"plotClip\">\n            <rect x=\"{plot_x_left:.2f}\" y=\"{plot_y_top:.2f}\" width=\"{plot_w:.2f}\" height=\"{plot_h:.2f}\"/>\n        </clipPath>\n    </defs>\"\"\",\n    f'<rect x=\"{plot_x_left:.2f}\" y=\"{plot_y_top:.2f}\" '\n    f'width=\"{plot_w:.2f}\" height=\"{plot_h:.2f}\" fill=\"url(#skyGrad)\" stroke=\"none\"/>',\n]\n\n# Secondary depth ridge polygon (distant muted ridge visible above main valleys)\ndepth_fill = \"#A8BAB4\" if THEME == \"light\" else \"#2C3E38\"\ndepth_pts = \" \".join(f\"{angle[i] * x_scale + x_off:.1f},{depth_ridge[i] * y_scale + y_off:.1f}\" for i in range(N))\ndepth_pts += f\" {plot_x_right:.1f},{plot_y_bottom:.1f} {plot_x_left:.1f},{plot_y_bottom:.1f}\"\nsvg_parts.append(\n    f'<polygon points=\"{depth_pts}\" fill=\"{depth_fill}\" opacity=\"0.60\" clip-path=\"url(#plotClip)\" stroke=\"none\"/>'\n)\n\n# Title — scaled for long descriptive string (formula: round(66 * 67 / len(title)))\ntitle_str = \"Bernese Oberland · area-mountain-panorama · python · pygal · anyplot.ai\"\ntitle_fs = max(44, round(66 * 67 / len(title_str)))\nsvg_parts.append(\n    f'<text x=\"{CANVAS_W / 2:.2f}\" y=\"92\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:{title_fs}px;font-weight:500;font-family:{font}\">'\n    f\"{title_str}</text>\"\n)\n\n# Subtitle\nsvg_parts.append(\n    f'<text x=\"{CANVAS_W / 2:.2f}\" y=\"136\" text-anchor=\"middle\" fill=\"{INK_SOFT}\" '\n    f'style=\"font-size:28px;font-family:{font}\">'\n    f\"Thirteen peaks of the Swiss Bernese Oberland, viewed W → E from the north</text>\"\n)\n\n# Y-axis gridlines + tick labels\ny_ticks = [2500, 3000, 3500, 4000, 4500]\nfor v in y_ticks:\n    ty = v * y_scale + y_off\n    svg_parts.append(\n        f'<text x=\"{plot_x_left - 16:.2f}\" y=\"{ty + 11:.2f}\" text-anchor=\"end\" '\n        f'fill=\"{INK_SOFT}\" style=\"font-size:40px;font-family:{font}\">{v:,}</text>'\n    )\n    svg_parts.append(\n        f'<line x1=\"{plot_x_left:.2f}\" y1=\"{ty:.2f}\" '\n        f'x2=\"{plot_x_right:.2f}\" y2=\"{ty:.2f}\" '\n        f'stroke=\"{INK}\" stroke-opacity=\"0.10\" stroke-width=\"1.2\"/>'\n    )\n\n# Y-axis title (rotated)\ny_title_x = plot_x_left - 130\ny_title_y = plot_y_top + plot_h / 2\nsvg_parts.append(\n    f'<text x=\"{y_title_x:.2f}\" y=\"{y_title_y:.2f}\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:38px;font-family:{font}\" '\n    f'transform=\"rotate(-90,{y_title_x:.2f},{y_title_y:.2f})\">Elevation (m)</text>'\n)\n\n# Compass bearings on x-axis\ncompass_ticks = [(28, \"W\"), (80, \"SW\"), (140, \"S\"), (200, \"SE\"), (265, \"E\")]\nfor ang_val, label in compass_ticks:\n    tx = ang_val * x_scale + x_off\n    svg_parts.append(\n        f'<text x=\"{tx:.2f}\" y=\"{plot_y_bottom + 48:.2f}\" text-anchor=\"middle\" '\n        f'fill=\"{INK_SOFT}\" style=\"font-size:30px;font-family:{font}\">{label}</text>'\n    )\n\n# L-shaped axis frame\nsvg_parts.append(\n    f'<line x1=\"{plot_x_left:.2f}\" y1=\"{plot_y_top:.2f}\" '\n    f'x2=\"{plot_x_left:.2f}\" y2=\"{plot_y_bottom:.2f}\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n)\nsvg_parts.append(\n    f'<line x1=\"{plot_x_left:.2f}\" y1=\"{plot_y_bottom:.2f}\" '\n    f'x2=\"{plot_x_right:.2f}\" y2=\"{plot_y_bottom:.2f}\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n)\n\n# Peak labels — 4 tiers; Jungfrau (focal) pinned to topmost tier\nLABEL_TIERS = [4350, 4510, 4670, 4820]\nfor i, (name, pos, elev) in enumerate(peaks):\n    is_focal = name == \"Jungfrau\"\n    tier_idx = 3 if is_focal else (i % 3)\n    tier_y_data = LABEL_TIERS[tier_idx]\n\n    sx = pos * x_scale + x_off\n    sy_summit = elev * y_scale + y_off\n    sy_label = tier_y_data * y_scale + y_off\n\n    leader_color = INK if is_focal else INK_SOFT\n    leader_op = 0.85 if is_focal else 0.40\n    leader_w = 1.8 if is_focal else 1.0\n\n    svg_parts.append(\n        f'<line x1=\"{sx:.2f}\" y1=\"{sy_summit - 4:.2f}\" '\n        f'x2=\"{sx:.2f}\" y2=\"{sy_label + 16:.2f}\" '\n        f'stroke=\"{leader_color}\" stroke-opacity=\"{leader_op}\" stroke-width=\"{leader_w}\"/>'\n    )\n\n    name_fs = 32 if is_focal else 24\n    elev_fs = 26 if is_focal else 22\n    name_weight = \"700\" if is_focal else \"600\"\n    name_color = INK if is_focal else INK_SOFT\n    elev_color = INK_SOFT if is_focal else INK_MUTED\n\n    svg_parts.append(\n        f'<text x=\"{sx:.2f}\" y=\"{sy_label:.2f}\" text-anchor=\"middle\" fill=\"{name_color}\" '\n        f'style=\"font-size:{name_fs}px;font-weight:{name_weight};font-family:{font}\">'\n        f\"{name}</text>\"\n    )\n    svg_parts.append(\n        f'<text x=\"{sx:.2f}\" y=\"{sy_label + name_fs + 4:.2f}\" text-anchor=\"middle\" '\n        f'fill=\"{elev_color}\" style=\"font-size:{elev_fs}px;font-family:{font}\">'\n        f\"{elev:,} m</text>\"\n    )\n\ncustom_svg = \"\\n\".join(svg_parts)\n\n# Inject chrome before pygal's plot group so silhouette and interactive dots stay on top\nplot_group_idx = base_svg.find('class=\"plot\"')\nif plot_group_idx != -1:\n    insert_idx = base_svg.rfind(\"<g\", 0, plot_group_idx)\n    output_svg = base_svg[:insert_idx] + custom_svg + \"\\n\" + base_svg[insert_idx:]\nelse:\n    output_svg = base_svg.replace(\"</svg>\", f\"{custom_svg}\\n</svg>\")\n\n# Clip the pygal fill to the plot-box so it doesn't bleed into the bottom margin\noutput_svg = output_svg.replace('class=\"plot\"', 'class=\"plot\" clip-path=\"url(#plotClip)\"', 1)\n\ncairosvg.svg2png(bytestring=output_svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=CANVAS_W)\n\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>area-mountain-panorama · python · pygal · anyplot.ai</title>\n    <style>\n        body {{ margin: 0; background: {PAGE_BG}; display: flex;\n                justify-content: center; align-items: center; min-height: 100vh; }}\n        .chart {{ max-width: 100%; height: auto; }}\n    </style>\n</head>\n<body>\n    <figure class=\"chart\">\n        {output_svg}\n    </figure>\n</body>\n</html>\n\"\"\"\n\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(html_content)\n"}