{"spec_id":"contour-filled","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncontour-filled: Filled Contour Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-11\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\n\n# Remove script directory from path to avoid name collision with pygal package\n_script_dir = str(Path(__file__).parent)\nsys.path = [p for p in sys.path if p != _script_dir]\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\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\"\n\n# Data: Mathematical function - Gaussian peaks on a 2D surface\nnp.random.seed(42)\nn_points = 80\nx = np.linspace(-3, 3, n_points)\ny = np.linspace(-3, 3, n_points)\nX, Y = np.meshgrid(x, y)\n\nZ = (\n    1.5 * np.exp(-((X - 1) ** 2 + (Y - 1) ** 2))\n    - 1.0 * np.exp(-((X + 1) ** 2 + (Y + 1) ** 2))\n    + 0.8 * np.exp(-((X - 1) ** 2 + (Y + 1.5) ** 2) / 0.5)\n    + 0.5 * np.exp(-((X + 1.5) ** 2 + (Y - 0.5) ** 2) / 0.8)\n)\n\nz_min, z_max = Z.min(), Z.max()\n\n# Diverging colormap (blue-white-red) - same for both themes, only chrome adapts\ncolormap = [\n    \"#08306b\",\n    \"#08519c\",\n    \"#2171b5\",\n    \"#4292c6\",\n    \"#6baed6\",\n    \"#9ecae1\",\n    \"#c6dbef\",\n    \"#f7f7f7\",\n    \"#fddbc7\",\n    \"#f4a582\",\n    \"#d6604d\",\n    \"#b2182b\",\n    \"#67001f\",\n]\n\n# Style for 4800x2700 canvas\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_SOFT,\n    colors=(\"#009E73\",),\n    title_font_size=28,\n    legend_font_size=16,\n    label_font_size=22,\n    value_font_size=18,\n    font_family=\"sans-serif\",\n)\n\n# Create base XY chart\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"contour-filled · pygal · anyplot.ai\",\n    show_legend=False,\n    margin=120,\n    margin_top=200,\n    margin_bottom=200,\n    margin_left=300,\n    margin_right=350,\n    show_x_labels=False,\n    show_y_labels=False,\n    show_x_guides=False,\n    show_y_guides=False,\n    x_title=\"\",\n    y_title=\"\",\n)\n\n# Plot dimensions (matching chart margins)\nplot_x = 300\nplot_y = 200\nplot_width = 4800 - 300 - 350\nplot_height = 2700 - 200 - 200\n\n# Cell size for higher resolution\ncell_w = plot_width / (n_points - 1)\ncell_h = plot_height / (n_points - 1)\n\n# Build SVG content for filled contour\nsvg_parts = []\n\n# Draw filled cells\nn_levels = 25\nlevels = np.linspace(z_min, z_max, n_levels + 1)\n\nfor i in range(n_points - 1):\n    for j in range(n_points - 1):\n        cell_val = (Z[i, j] + Z[i, j + 1] + Z[i + 1, j] + Z[i + 1, j + 1]) / 4\n\n        # Inline color interpolation\n        if z_max == z_min:\n            color = colormap[len(colormap) // 2]\n        else:\n            norm = max(0, min(1, (cell_val - z_min) / (z_max - z_min)))\n            pos = norm * (len(colormap) - 1)\n            i1, i2 = int(pos), min(int(pos) + 1, len(colormap) - 1)\n            frac = pos - i1\n            c1, c2 = colormap[i1], colormap[i2]\n            r = int(int(c1[1:3], 16) + (int(c2[1:3], 16) - int(c1[1:3], 16)) * frac)\n            g = int(int(c1[3:5], 16) + (int(c2[3:5], 16) - int(c1[3:5], 16)) * frac)\n            b = int(int(c1[5:7], 16) + (int(c2[5:7], 16) - int(c1[5:7], 16)) * frac)\n            color = f\"#{r:02x}{g:02x}{b:02x}\"\n\n        cx = plot_x + j * cell_w\n        cy = plot_y + plot_height - (i + 1) * cell_h\n        svg_parts.append(\n            f'<rect x=\"{cx:.1f}\" y=\"{cy:.1f}\" width=\"{cell_w + 0.5:.1f}\" '\n            f'height=\"{cell_h + 0.5:.1f}\" fill=\"{color}\" stroke=\"none\"/>'\n        )\n\n# Draw contour lines using marching squares (increased opacity to 0.75)\nline_levels = np.linspace(z_min, z_max, 12)[1:-1]\n\nfor level in line_levels:\n    for i in range(n_points - 1):\n        for j in range(n_points - 1):\n            z00, z01 = Z[i, j], Z[i, j + 1]\n            z10, z11 = Z[i + 1, j], Z[i + 1, j + 1]\n\n            case = 0\n            if z00 >= level:\n                case |= 1\n            if z01 >= level:\n                case |= 2\n            if z11 >= level:\n                case |= 4\n            if z10 >= level:\n                case |= 8\n\n            if case == 0 or case == 15:\n                continue\n\n            x0 = plot_x + j * cell_w\n            y0 = plot_y + plot_height - (i + 1) * cell_h\n\n            # Edge midpoints\n            left_t = 0.5 if abs(z10 - z00) < 1e-10 else (level - z00) / (z10 - z00)\n            right_t = 0.5 if abs(z11 - z01) < 1e-10 else (level - z01) / (z11 - z01)\n            top_t = 0.5 if abs(z11 - z10) < 1e-10 else (level - z10) / (z11 - z10)\n            bottom_t = 0.5 if abs(z01 - z00) < 1e-10 else (level - z00) / (z01 - z00)\n\n            left = (x0, y0 + cell_h * left_t)\n            right = (x0 + cell_w, y0 + cell_h * right_t)\n            top = (x0 + cell_w * top_t, y0 + cell_h)\n            bottom = (x0 + cell_w * bottom_t, y0)\n\n            segments = []\n            if case in [1, 14]:\n                segments.append((left, bottom))\n            elif case in [2, 13]:\n                segments.append((bottom, right))\n            elif case in [3, 12]:\n                segments.append((left, right))\n            elif case in [4, 11]:\n                segments.append((right, top))\n            elif case == 5:\n                segments.append((left, top))\n                segments.append((bottom, right))\n            elif case in [6, 9]:\n                segments.append((bottom, top))\n            elif case in [7, 8]:\n                segments.append((left, top))\n            elif case == 10:\n                segments.append((left, bottom))\n                segments.append((right, top))\n\n            for (x1, y1), (x2, y2) in segments:\n                svg_parts.append(\n                    f'<line x1=\"{x1:.1f}\" y1=\"{y1:.1f}\" x2=\"{x2:.1f}\" y2=\"{y2:.1f}\" '\n                    f'stroke=\"{INK}\" stroke-width=\"2\" stroke-opacity=\"0.75\"/>'\n                )\n\n# Axis frame\nsvg_parts.append(\n    f'<rect x=\"{plot_x}\" y=\"{plot_y}\" width=\"{plot_width}\" height=\"{plot_height}\" '\n    f'fill=\"none\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n)\n\n# X-axis labels and ticks\nn_x_ticks = 7\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 = x[0] + frac * (x[-1] - x[0])\n    svg_parts.append(\n        f'<line x1=\"{tick_x:.1f}\" y1=\"{tick_y}\" x2=\"{tick_x:.1f}\" y2=\"{tick_y + 15}\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n    )\n    svg_parts.append(\n        f'<text x=\"{tick_x:.1f}\" y=\"{tick_y + 55}\" text-anchor=\"middle\" fill=\"{INK_SOFT}\" '\n        f'style=\"font-size:36px;font-family:sans-serif\">{val:.1f}</text>'\n    )\n\n# X-axis title\nsvg_parts.append(\n    f'<text x=\"{plot_x + plot_width / 2}\" y=\"{plot_y + plot_height + 130}\" text-anchor=\"middle\" '\n    f'fill=\"{INK}\" style=\"font-size:44px;font-weight:bold;font-family:sans-serif\">X Position (a.u.)</text>'\n)\n\n# Y-axis labels and ticks\nn_y_ticks = 7\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 = y[0] + frac * (y[-1] - y[0])\n    svg_parts.append(\n        f'<line x1=\"{tick_x - 15}\" y1=\"{tick_y:.1f}\" x2=\"{tick_x}\" y2=\"{tick_y:.1f}\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n    )\n    svg_parts.append(\n        f'<text x=\"{tick_x - 25}\" y=\"{tick_y + 12:.1f}\" text-anchor=\"end\" fill=\"{INK_SOFT}\" '\n        f'style=\"font-size:36px;font-family:sans-serif\">{val:.1f}</text>'\n    )\n\n# Y-axis title (rotated)\ny_title_x = plot_x - 180\ny_title_y = plot_y + plot_height / 2\nsvg_parts.append(\n    f'<text x=\"{y_title_x}\" y=\"{y_title_y}\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:44px;font-weight:bold;font-family:sans-serif\" '\n    f'transform=\"rotate(-90, {y_title_x}, {y_title_y})\">Y Position (a.u.)</text>'\n)\n\n# Colorbar\ncb_width = 50\ncb_height = plot_height * 0.85\ncb_x = plot_x + plot_width + 60\ncb_y = plot_y + (plot_height - cb_height) / 2\n\n# Colorbar gradient\nn_cb_segments = 80\nseg_h = cb_height / n_cb_segments\nfor i in range(n_cb_segments):\n    seg_val = z_max - (z_max - z_min) * i / (n_cb_segments - 1)\n\n    # Inline color interpolation for colorbar\n    if z_max == z_min:\n        seg_color = colormap[len(colormap) // 2]\n    else:\n        norm = max(0, min(1, (seg_val - z_min) / (z_max - z_min)))\n        pos = norm * (len(colormap) - 1)\n        i1, i2 = int(pos), min(int(pos) + 1, len(colormap) - 1)\n        frac = pos - i1\n        c1, c2 = colormap[i1], colormap[i2]\n        r = int(int(c1[1:3], 16) + (int(c2[1:3], 16) - int(c1[1:3], 16)) * frac)\n        g = int(int(c1[3:5], 16) + (int(c2[3:5], 16) - int(c1[3:5], 16)) * frac)\n        b = int(int(c1[5:7], 16) + (int(c2[5:7], 16) - int(c1[5:7], 16)) * frac)\n        seg_color = f\"#{r:02x}{g:02x}{b:02x}\"\n\n    seg_y = cb_y + i * seg_h\n    svg_parts.append(\n        f'<rect x=\"{cb_x}\" y=\"{seg_y:.1f}\" width=\"{cb_width}\" height=\"{seg_h + 1:.1f}\" fill=\"{seg_color}\"/>'\n    )\n\n# Colorbar border\nsvg_parts.append(\n    f'<rect x=\"{cb_x}\" y=\"{cb_y}\" width=\"{cb_width}\" height=\"{cb_height}\" fill=\"none\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n)\n\n# Colorbar labels\nn_cb_labels = 5\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 + 12\n    svg_parts.append(\n        f'<text x=\"{cb_x + cb_width + 15}\" y=\"{label_y:.1f}\" fill=\"{INK_SOFT}\" '\n        f'style=\"font-size:32px;font-family:sans-serif\">{val:.2f}</text>'\n    )\n\n# Colorbar title\ncb_title_x = cb_x + cb_width / 2\ncb_title_y = cb_y - 30\nsvg_parts.append(\n    f'<text x=\"{cb_title_x}\" y=\"{cb_title_y}\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:38px;font-weight:bold;font-family:sans-serif\">Intensity</text>'\n)\n\n# Combine all SVG parts\ncustom_svg = \"\\n\".join(svg_parts)\n\n# Add dummy data point (required by pygal)\nchart.add(\"\", [(0, 0)])\n\n# Render base chart and inject custom SVG\nbase_svg = chart.render(is_unicode=True)\n\n# Insert custom contour SVG before the closing </svg> tag\noutput_svg = base_svg.replace(\"</svg>\", f\"{custom_svg}\\n</svg>\")\n\n# Save SVG (optional, for debugging)\nwith open(f\"plot-{THEME}.svg\", \"w\", encoding=\"utf-8\") as f:\n    f.write(output_svg)\n\n# Convert to PNG using cairosvg\ncairosvg.svg2png(bytestring=output_svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>contour-filled - pygal</title>\n    <style>\n        body {{ margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: {PAGE_BG}; }}\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"}