{"spec_id":"contour-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncontour-basic: Basic Contour Plot\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-06-25\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\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\"\nLINE_COLOR = \"#FAF8F1\" if THEME == \"light\" else \"#F0EFE8\"\nLINE_OPACITY = 0.70 if THEME == \"light\" else 0.85\n\n# Imprint sequential colormap: #009E73 (brand green) → #4467A3 (blue)\n# Single-polarity sequential for all-positive elevation data (no natural midpoint).\nSEQ_R0, SEQ_G0, SEQ_B0 = 0, 158, 115  # #009E73 — brand green (low elevation)\nSEQ_R1, SEQ_G1, SEQ_B1 = 68, 103, 163  # #4467A3 — blue (high elevation)\n\n# Data — topographic elevation map of a 10 km × 10 km mountain region\nnp.random.seed(42)\nn_points = 80\nx = np.linspace(0, 10, n_points)\ny = np.linspace(0, 10, n_points)\nX, Y = np.meshgrid(x, y)\n\nelevation = (\n    850 * np.exp(-((X - 7) ** 2 + (Y - 7) ** 2) / 4.0)\n    + 550 * np.exp(-((X - 2.5) ** 2 + (Y - 3) ** 2) / 3.0)\n    - 180 * np.exp(-((X - 5) ** 2 + (Y - 5) ** 2) / 8.0)\n    + 12 * X\n    + 350\n)\n\nz_min, z_max = float(elevation.min()), float(elevation.max())\nprimary_peak = (7.0, 7.0)\nsecondary_peak = (2.5, 3.0)\nprimary_elev = int(\n    round(\n        float(\n            850 * np.exp(0)\n            + 550 * np.exp(-((7 - 2.5) ** 2 + (7 - 3) ** 2) / 3.0)\n            - 180 * np.exp(-((7 - 5) ** 2 + (7 - 5) ** 2) / 8.0)\n            + 12 * 7\n            + 350\n        )\n    )\n)\nsecondary_elev = int(\n    round(\n        float(\n            850 * np.exp(-((2.5 - 7) ** 2 + (3 - 7) ** 2) / 4.0)\n            + 550 * np.exp(0)\n            - 180 * np.exp(-((2.5 - 5) ** 2 + (3 - 5) ** 2) / 8.0)\n            + 12 * 2.5\n            + 350\n        )\n    )\n)\n\n# Canvas: 3200×1800 (style guide canonical landscape size)\nCANVAS_W, CANVAS_H = 3200, 1800\nMARGIN_L, MARGIN_R = 260, 420\nMARGIN_T, MARGIN_B = 160, 180\n\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\n\n# Peak marker colors: INK (theme-adaptive neutral) treats peaks as structural\n# annotations rather than categorical data series, avoiding green-on-green invisibility\n# while using the correct semantic anchor for landmark overlays.\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=(INK, INK_MUTED),\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    tooltip_font_size=28,\n    legend_font_size=32,\n    stroke_width=4,\n    opacity=\".95\",\n    opacity_hover=\".65\",\n    transition=\"200ms ease-in\",\n)\n\n# Pygal XY chart: peak markers with native hover tooltips retained in HTML export.\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=(0, 10),\n    range=(0, 10),\n    dots_size=24,\n    stroke=False,\n    truncate_label=-1,\n)\n\nchart.add(\"Primary Peak\", [{\"value\": primary_peak, \"label\": f\"Primary Peak · {primary_elev} m\"}])\nchart.add(\"Secondary Peak\", [{\"value\": secondary_peak, \"label\": f\"Secondary Peak · {secondary_elev} m\"}])\n\n# Render pygal to read back exact plot-box placement from peak dot pixel positions.\nbase_svg = chart.render(is_unicode=True)\n\ndot_re = re.compile(r'<circle cx=\"([-\\d.]+)\" cy=\"([-\\d.]+)\"[^>]*class=\"dot')\npeaks = [(float(cx), float(cy)) for cx, cy in dot_re.findall(base_svg)]\n(p1x, p1y), (p2x, p2y) = peaks[0], peaks[1]\nx_scale = (p1x - p2x) / (primary_peak[0] - secondary_peak[0])\nx_off = p1x - primary_peak[0] * x_scale\ny_scale = (p1y - p2y) / (primary_peak[1] - secondary_peak[1])\ny_off = p1y - primary_peak[1] * y_scale\n\nplot_x = MARGIN_L + x_off\nplot_y = MARGIN_T + y_off + 10 * y_scale\nplot_width = 10 * x_scale\nplot_height = -10 * y_scale\n\ncell_w = plot_width / (n_points - 1)\ncell_h = plot_height / (n_points - 1)\n\nsvg_parts = []\n\n# Filled contour — Imprint sequential colormap (#009E73 → #4467A3)\ncell_mean = (elevation[:-1, :-1] + elevation[:-1, 1:] + elevation[1:, :-1] + elevation[1:, 1:]) / 4\ncell_t = np.clip((cell_mean - z_min) / (z_max - z_min), 0.0, 1.0)\ncell_r = np.clip(np.round(SEQ_R0 + (SEQ_R1 - SEQ_R0) * cell_t), 0, 255).astype(int)\ncell_g = np.clip(np.round(SEQ_G0 + (SEQ_G1 - SEQ_G0) * cell_t), 0, 255).astype(int)\ncell_b = np.clip(np.round(SEQ_B0 + (SEQ_B1 - SEQ_B0) * cell_t), 0, 255).astype(int)\n\nfor i in range(n_points - 1):\n    for j in range(n_points - 1):\n        cx = plot_x + j * cell_w\n        cy = plot_y + plot_height - (i + 1) * cell_h\n        color = f\"#{cell_r[i, j]:02x}{cell_g[i, j]:02x}{cell_b[i, j]:02x}\"\n        svg_parts.append(\n            f'<rect x=\"{cx:.2f}\" y=\"{cy:.2f}\" width=\"{cell_w + 0.6:.2f}\" '\n            f'height=\"{cell_h + 0.6:.2f}\" fill=\"{color}\" stroke=\"none\"/>'\n        )\n\n# Marching-squares contour extraction\nminor_levels = list(range(400, 1251, 50))\nmajor_levels = list(range(400, 1251, 200))\nmajor_set = set(major_levels)\nall_levels = sorted(set(minor_levels + major_levels))\n\nmajor_segments_by_level = {}\n\nfor lvl in all_levels:\n    is_major = lvl in major_set\n    segments = []\n    for i in range(n_points - 1):\n        for j in range(n_points - 1):\n            z00 = elevation[i, j]\n            z01 = elevation[i, j + 1]\n            z10 = elevation[i + 1, j]\n            z11 = elevation[i + 1, j + 1]\n\n            case = 0\n            if z00 >= lvl:\n                case |= 1\n            if z01 >= lvl:\n                case |= 2\n            if z11 >= lvl:\n                case |= 4\n            if z10 >= lvl:\n                case |= 8\n            if case == 0 or case == 15:\n                continue\n\n            x0 = plot_x + j * cell_w\n            y_bot = plot_y + plot_height - i * cell_h\n            y_top = plot_y + plot_height - (i + 1) * cell_h\n\n            fl = 0.5 if abs(z10 - z00) < 1e-10 else (lvl - z00) / (z10 - z00)\n            fr = 0.5 if abs(z11 - z01) < 1e-10 else (lvl - z01) / (z11 - z01)\n            ft = 0.5 if abs(z11 - z10) < 1e-10 else (lvl - z10) / (z11 - z10)\n            fb = 0.5 if abs(z01 - z00) < 1e-10 else (lvl - z00) / (z01 - z00)\n\n            left = (x0, y_bot - cell_h * fl)\n            right = (x0 + cell_w, y_bot - cell_h * fr)\n            top = (x0 + cell_w * ft, y_top)\n            bottom = (x0 + cell_w * fb, y_bot)\n\n            if case == 1 or case == 14:\n                segments.append((left, bottom))\n            elif case == 2 or case == 13:\n                segments.append((bottom, right))\n            elif case == 3 or case == 12:\n                segments.append((left, right))\n            elif case == 4 or case == 11:\n                segments.append((right, top))\n            elif case == 5:\n                segments.append((left, top))\n                segments.append((bottom, right))\n            elif case == 6 or case == 9:\n                segments.append((bottom, top))\n            elif case == 7 or case == 8:\n                segments.append((left, top))\n            elif case == 10:\n                segments.append((left, bottom))\n                segments.append((right, top))\n\n    stroke_w = 3 if is_major else 1.5\n    stroke_op = LINE_OPACITY if is_major else 0.35\n    for (x1, y1), (x2, y2) in segments:\n        svg_parts.append(\n            f'<line x1=\"{x1:.2f}\" y1=\"{y1:.2f}\" x2=\"{x2:.2f}\" y2=\"{y2:.2f}\" '\n            f'stroke=\"{LINE_COLOR}\" stroke-width=\"{stroke_w}\" stroke-opacity=\"{stroke_op}\"/>'\n        )\n\n    if is_major:\n        major_segments_by_level[lvl] = segments\n\n# Contour level labels — maximize distance from placed anchors.\n# A buffer ring of ghost points around the primary peak fans labels away from the\n# high-elevation crowding zone, reducing overlap near the 1200 m contour.\nlabel_font_px = 38\nplaced_positions = [\n    (p1x, p1y),\n    (p2x, p2y),\n    (p1x - 110, p1y),\n    (p1x + 110, p1y),\n    (p1x, p1y - 110),\n    (p1x, p1y + 110),\n    (p1x - 80, p1y - 80),\n    (p1x + 80, p1y - 80),\n    (p1x - 80, p1y + 80),\n    (p1x + 80, p1y + 80),\n    (p1x - 140, p1y - 40),\n    (p1x + 140, p1y - 40),\n    (p1x - 40, p1y - 140),\n    (p1x + 40, p1y - 140),\n]\nfor lvl, segs in major_segments_by_level.items():\n    if not segs:\n        continue\n    best_cx, best_cy, best_score = 0.0, 0.0, -1.0\n    for (x1, y1), (x2, y2) in segs:\n        mx, my = (x1 + x2) / 2, (y1 + y2) / 2\n        nearest_sq = min((mx - px) ** 2 + (my - py) ** 2 for px, py in placed_positions)\n        if nearest_sq > best_score:\n            best_score = nearest_sq\n            best_cx, best_cy = mx, my\n    cx, cy = best_cx, best_cy\n    placed_positions.append((cx, cy))\n    text = f\"{lvl} m\"\n    svg_parts.append(\n        f'<text x=\"{cx:.2f}\" y=\"{cy + 11:.2f}\" text-anchor=\"middle\" '\n        f'fill=\"none\" stroke=\"{PAGE_BG}\" stroke-width=\"7\" stroke-linejoin=\"round\" '\n        f'style=\"font-size:{label_font_px}px;font-family:{font};font-weight:600\">{text}</text>'\n    )\n    svg_parts.append(\n        f'<text x=\"{cx:.2f}\" y=\"{cy + 11:.2f}\" text-anchor=\"middle\" '\n        f'fill=\"{INK}\" '\n        f'style=\"font-size:{label_font_px}px;font-family:{font};font-weight:600\">{text}</text>'\n    )\n\n# L-shaped frame (left + bottom only)\nsvg_parts.append(\n    f'<line x1=\"{plot_x:.2f}\" y1=\"{plot_y:.2f}\" x2=\"{plot_x:.2f}\" '\n    f'y2=\"{plot_y + plot_height:.2f}\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n)\nsvg_parts.append(\n    f'<line x1=\"{plot_x:.2f}\" y1=\"{plot_y + plot_height:.2f}\" '\n    f'x2=\"{plot_x + plot_width:.2f}\" y2=\"{plot_y + plot_height:.2f}\" '\n    f'stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n)\n\n# X-axis ticks + labels\nn_x_ticks = 6\nfor i in range(n_x_ticks):\n    frac = i / (n_x_ticks - 1)\n    tick_x = plot_x + frac * plot_width\n    tick_y = plot_y + plot_height\n    val = frac * 10\n    svg_parts.append(\n        f'<line x1=\"{tick_x:.2f}\" y1=\"{tick_y:.2f}\" x2=\"{tick_x:.2f}\" '\n        f'y2=\"{tick_y + 10:.2f}\" stroke=\"{INK_SOFT}\" stroke-width=\"1.5\"/>'\n    )\n    svg_parts.append(\n        f'<text x=\"{tick_x:.2f}\" y=\"{tick_y + 44:.2f}\" text-anchor=\"middle\" '\n        f'fill=\"{INK_SOFT}\" style=\"font-size:38px;font-family:{font}\">{val:.0f}</text>'\n    )\n\nsvg_parts.append(\n    f'<text x=\"{plot_x + plot_width / 2:.2f}\" y=\"{plot_y + plot_height + 110:.2f}\" '\n    f'text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:44px;font-weight:500;font-family:{font}\">Distance East (km)</text>'\n)\n\n# Y-axis ticks + labels\nn_y_ticks = 6\nfor i in range(n_y_ticks):\n    frac = i / (n_y_ticks - 1)\n    tick_y = plot_y + plot_height - frac * plot_height\n    tick_x = plot_x\n    val = frac * 10\n    svg_parts.append(\n        f'<line x1=\"{tick_x - 10:.2f}\" y1=\"{tick_y:.2f}\" x2=\"{tick_x:.2f}\" '\n        f'y2=\"{tick_y:.2f}\" stroke=\"{INK_SOFT}\" stroke-width=\"1.5\"/>'\n    )\n    svg_parts.append(\n        f'<text x=\"{tick_x - 18:.2f}\" y=\"{tick_y + 13:.2f}\" text-anchor=\"end\" '\n        f'fill=\"{INK_SOFT}\" style=\"font-size:38px;font-family:{font}\">{val:.0f}</text>'\n    )\n\ny_title_x = plot_x - 130\ny_title_y = plot_y + plot_height / 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:44px;font-weight:500;font-family:{font}\" '\n    f'transform=\"rotate(-90, {y_title_x:.2f}, {y_title_y:.2f})\">Distance North (km)</text>'\n)\n\n# Colorbar — right of plot, Imprint sequential colormap (green=low, blue=high)\ncb_width = 48\ncb_height = int(plot_height * 0.80)\ncb_x = plot_x + plot_width + 80\ncb_y = plot_y + (plot_height - cb_height) / 2\n\nn_cb_segments = 120\ncb_t = np.clip(1.0 - np.arange(n_cb_segments) / (n_cb_segments - 1), 0.0, 1.0)\ncb_r = np.clip(np.round(SEQ_R0 + (SEQ_R1 - SEQ_R0) * cb_t), 0, 255).astype(int)\ncb_g = np.clip(np.round(SEQ_G0 + (SEQ_G1 - SEQ_G0) * cb_t), 0, 255).astype(int)\ncb_b = np.clip(np.round(SEQ_B0 + (SEQ_B1 - SEQ_B0) * cb_t), 0, 255).astype(int)\nseg_h = cb_height / n_cb_segments\nfor i in range(n_cb_segments):\n    color = f\"#{cb_r[i]:02x}{cb_g[i]:02x}{cb_b[i]:02x}\"\n    seg_y = cb_y + i * seg_h\n    svg_parts.append(\n        f'<rect x=\"{cb_x:.2f}\" y=\"{seg_y:.2f}\" width=\"{cb_width}\" '\n        f'height=\"{seg_h + 0.6:.2f}\" fill=\"{color}\" stroke=\"none\"/>'\n    )\n\nsvg_parts.append(\n    f'<rect x=\"{cb_x:.2f}\" y=\"{cb_y:.2f}\" width=\"{cb_width}\" height=\"{cb_height}\" '\n    f'fill=\"none\" stroke=\"{INK_SOFT}\" stroke-width=\"1.2\"/>'\n)\n\nn_cb_labels = 6\nfor i in range(n_cb_labels):\n    frac = i / (n_cb_labels - 1)\n    val = z_max - (z_max - z_min) * frac\n    label_y = cb_y + frac * cb_height + 13\n    svg_parts.append(\n        f'<text x=\"{cb_x + cb_width + 16:.2f}\" y=\"{label_y:.2f}\" fill=\"{INK_SOFT}\" '\n        f'style=\"font-size:38px;font-family:{font}\">{int(round(val))}</text>'\n    )\n\ncb_title_x = cb_x + cb_width + 160\ncb_title_y = cb_y + cb_height / 2\nsvg_parts.append(\n    f'<text x=\"{cb_title_x:.2f}\" y=\"{cb_title_y:.2f}\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:40px;font-weight:500;font-family:{font}\" '\n    f'transform=\"rotate(90, {cb_title_x:.2f}, {cb_title_y:.2f})\">Elevation (m)</text>'\n)\n\n# Peak marker halos — PAGE_BG ring makes INK dots visible against colored contour surface.\n# Rendered after fill/lines (on top in SVG order) but before pygal dots (which render last).\nfor px, py in [(p1x, p1y), (p2x, p2y)]:\n    svg_parts.append(\n        f'<circle cx=\"{px:.2f}\" cy=\"{py:.2f}\" r=\"42\" fill=\"{PAGE_BG}\" stroke=\"{INK_SOFT}\" stroke-width=\"2.5\"/>'\n    )\n\n# Title\ntitle_text = \"contour-basic · python · pygal · anyplot.ai\"\nbg_rect = f'<rect x=\"0\" y=\"0\" width=\"{CANVAS_W}\" height=\"{CANVAS_H}\" fill=\"{PAGE_BG}\" stroke=\"none\"/>'\ntitle_svg_elem = (\n    f'<text x=\"{CANVAS_W / 2:.2f}\" y=\"105\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:66px;font-weight:600;font-family:{font}\">'\n    f\"{title_text}</text>\"\n)\n\ncustom_svg = \"\\n\".join([bg_rect, title_svg_elem] + svg_parts)\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\ncairosvg.svg2png(bytestring=output_svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>contour-basic · 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"}