{"spec_id":"contour-decision-boundary","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncontour-decision-boundary: Decision Boundary Classifier Visualization\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-16\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\nfrom sklearn.datasets import make_moons\nfrom sklearn.svm import SVC\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_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette\nBRAND = \"#009E73\"  # First series\nCOLORS = (\"#009E73\", \"#C475FD\")  # Two classes\n\n# Data: Generate synthetic classification data (moon shapes)\nnp.random.seed(42)\nX, y = make_moons(n_samples=150, noise=0.25, random_state=42)\n\n# Train SVM classifier\nclf = SVC(kernel=\"rbf\", C=1.0, gamma=\"scale\")\nclf.fit(X, y)\n\n# Create mesh grid for decision boundary\nh = 0.02  # Step size\nx_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5\ny_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5\nxx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))\n\n# Get predictions on mesh grid\nZ = clf.predict(np.c_[xx.ravel(), yy.ravel()])\nZ = Z.reshape(xx.shape)\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_MUTED,\n    colors=COLORS,\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n    stroke_width=3,\n)\n\n# Create base XY chart\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"contour-decision-boundary · pygal · anyplot.ai\",\n    show_legend=False,\n    margin=120,\n    margin_top=200,\n    margin_bottom=250,\n    margin_left=350,\n    margin_right=400,\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 = 350\nplot_y = 200\nplot_width = 4800 - 350 - 400\nplot_height = 2700 - 200 - 250\n\n\n# Helper function to map data coordinates to SVG coordinates\ndef data_to_svg(data_x, data_y):\n    svg_x = plot_x + (data_x - x_min) / (x_max - x_min) * plot_width\n    svg_y = plot_y + plot_height - (data_y - y_min) / (y_max - y_min) * plot_height\n    return svg_x, svg_y\n\n\n# Build SVG content\nsvg_parts = []\n\n# Draw decision boundary regions (filled cells)\nn_rows, n_cols = Z.shape\ncell_w = plot_width / (n_cols - 1)\ncell_h = plot_height / (n_rows - 1)\n\n# Alpha values for background regions\nregion_opacity = 0.4\n\nfor i in range(n_rows - 1):\n    for j in range(n_cols - 1):\n        # Use the class prediction for this cell\n        cell_class = Z[i, j]\n        # Use Okabe-Ito colors for regions\n        color = COLORS[int(cell_class)]\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\" opacity=\"{region_opacity}\"/>'\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_MUTED}\" stroke-width=\"3\"/>'\n)\n\n# Draw training points on top\nmarker_size = 18\nfor idx in range(len(X)):\n    px, py = X[idx]\n    svg_x, svg_y = data_to_svg(px, py)\n    point_class = y[idx]\n    color = COLORS[point_class]\n\n    # Predict class for this point to check if correctly classified\n    pred = clf.predict([[px, py]])[0]\n    is_correct = pred == point_class\n\n    # Use different marker for correct vs incorrect\n    if is_correct:\n        # Filled circle for correctly classified\n        svg_parts.append(\n            f'<circle cx=\"{svg_x:.1f}\" cy=\"{svg_y:.1f}\" r=\"{marker_size}\" '\n            f'fill=\"{color}\" stroke=\"{INK}\" stroke-width=\"2\"/>'\n        )\n    else:\n        # X marker for misclassified\n        svg_parts.append(\n            f'<circle cx=\"{svg_x:.1f}\" cy=\"{svg_y:.1f}\" r=\"{marker_size}\" '\n            f'fill=\"{color}\" stroke=\"#E53935\" stroke-width=\"4\"/>'\n        )\n        size = marker_size * 0.7\n        svg_parts.append(\n            f'<line x1=\"{svg_x - size:.1f}\" y1=\"{svg_y - size:.1f}\" '\n            f'x2=\"{svg_x + size:.1f}\" y2=\"{svg_y + size:.1f}\" stroke=\"#E53935\" stroke-width=\"3\"/>'\n        )\n        svg_parts.append(\n            f'<line x1=\"{svg_x + size:.1f}\" y1=\"{svg_y - size:.1f}\" '\n            f'x2=\"{svg_x - size:.1f}\" y2=\"{svg_y + size:.1f}\" stroke=\"#E53935\" stroke-width=\"3\"/>'\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_min + frac * (x_max - x_min)\n    svg_parts.append(\n        f'<line x1=\"{tick_x:.1f}\" y1=\"{tick_y}\" x2=\"{tick_x:.1f}\" y2=\"{tick_y + 20}\" '\n        f'stroke=\"{INK_MUTED}\" stroke-width=\"3\"/>'\n    )\n    svg_parts.append(\n        f'<text x=\"{tick_x:.1f}\" y=\"{tick_y + 65}\" text-anchor=\"middle\" fill=\"{INK}\" '\n        f'style=\"font-size:42px;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 + 140}\" text-anchor=\"middle\" '\n    f'fill=\"{INK}\" style=\"font-size:48px;font-weight:bold;font-family:sans-serif\">Feature 1</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_min + frac * (y_max - y_min)\n    svg_parts.append(\n        f'<line x1=\"{tick_x - 20}\" y1=\"{tick_y:.1f}\" x2=\"{tick_x}\" y2=\"{tick_y:.1f}\" '\n        f'stroke=\"{INK_MUTED}\" stroke-width=\"3\"/>'\n    )\n    svg_parts.append(\n        f'<text x=\"{tick_x - 30}\" y=\"{tick_y + 14:.1f}\" text-anchor=\"end\" fill=\"{INK}\" '\n        f'style=\"font-size:42px;font-family:sans-serif\">{val:.1f}</text>'\n    )\n\n# Y-axis title (rotated)\ny_title_x = plot_x - 200\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:48px;font-weight:bold;font-family:sans-serif\" '\n    f'transform=\"rotate(-90, {y_title_x}, {y_title_y})\">Feature 2</text>'\n)\n\n# Legend\nlegend_x = plot_x + plot_width + 50\nlegend_y = plot_y + 50\n\n# Class 0 legend\nsvg_parts.append(\n    f'<circle cx=\"{legend_x + 20}\" cy=\"{legend_y}\" r=\"20\" fill=\"{COLORS[0]}\" stroke=\"{INK}\" stroke-width=\"2\"/>'\n)\nsvg_parts.append(\n    f'<text x=\"{legend_x + 55}\" y=\"{legend_y + 12}\" fill=\"{INK}\" '\n    f'style=\"font-size:42px;font-family:sans-serif\">Class 0</text>'\n)\n\n# Class 1 legend\nsvg_parts.append(\n    f'<circle cx=\"{legend_x + 20}\" cy=\"{legend_y + 70}\" r=\"20\" fill=\"{COLORS[1]}\" stroke=\"{INK}\" stroke-width=\"2\"/>'\n)\nsvg_parts.append(\n    f'<text x=\"{legend_x + 55}\" y=\"{legend_y + 82}\" fill=\"{INK}\" '\n    f'style=\"font-size:42px;font-family:sans-serif\">Class 1</text>'\n)\n\n# Misclassified legend\nsvg_parts.append(\n    f'<circle cx=\"{legend_x + 20}\" cy=\"{legend_y + 150}\" r=\"20\" fill=\"{INK_MUTED}\" stroke=\"#E53935\" stroke-width=\"4\"/>'\n)\nsize = 14\nsvg_parts.append(\n    f'<line x1=\"{legend_x + 20 - size}\" y1=\"{legend_y + 150 - size}\" '\n    f'x2=\"{legend_x + 20 + size}\" y2=\"{legend_y + 150 + size}\" stroke=\"#E53935\" stroke-width=\"3\"/>'\n)\nsvg_parts.append(\n    f'<line x1=\"{legend_x + 20 + size}\" y1=\"{legend_y + 150 - size}\" '\n    f'x2=\"{legend_x + 20 - size}\" y2=\"{legend_y + 150 + size}\" stroke=\"#E53935\" stroke-width=\"3\"/>'\n)\nsvg_parts.append(\n    f'<text x=\"{legend_x + 55}\" y=\"{legend_y + 162}\" fill=\"{INK}\" '\n    f'style=\"font-size:42px;font-family:sans-serif\">Misclassified</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 SVG before the closing </svg> tag\noutput_svg = base_svg.replace(\"</svg>\", f\"{custom_svg}\\n</svg>\")\n\n# Save SVG and 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-decision-boundary - 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"}