{"spec_id":"heatmap-mandelbrot","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nheatmap-mandelbrot: Mandelbrot Set Fractal Visualization\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-30\n\"\"\"\n\nimport base64\nimport os\nimport sys\nfrom io import BytesIO\nfrom pathlib import Path\n\n\nsys.path = [p for p in sys.path if p != str(Path(__file__).parent)]\n\nimport numpy as np\nfrom PIL import Image\nfrom pygal.graph.graph import Graph\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\"\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# Imprint sequential colormap: brand green (#009E73) → blue (#4467A3)\n_SEQ_START = (0, 158, 115)  # #009E73\n_SEQ_END = (68, 103, 163)  # #4467A3\n\n# Data — Mandelbrot set on the complex plane\nx_min, x_max = -2.5, 1.0\ny_min, y_max = -1.25, 1.25\nmax_iter = 200\nbailout = 256\ngrid_w, grid_h = 1400, 1000\n\nreal = np.linspace(x_min, x_max, grid_w)\nimag = np.linspace(y_max, y_min, grid_h)\nc = real[np.newaxis, :] + 1j * imag[:, np.newaxis]\n\nz = np.zeros_like(c)\nescape_iter = np.full(c.shape, max_iter, dtype=np.float64)\nmask = np.ones(c.shape, dtype=bool)\n\nfor i in range(max_iter):\n    z[mask] = z[mask] ** 2 + c[mask]\n    escaped = mask & (np.abs(z) > bailout)\n    log_zn = np.log(np.abs(z[escaped]))\n    nu = np.log(log_zn / np.log(bailout)) / np.log(2)\n    escape_iter[escaped] = i + 1 - nu\n    mask[escaped] = False\n\nexterior = escape_iter < max_iter\n\n# Imprint sequential LUT: #009E73 → #4467A3 (1024 stops)\nlut_size = 1024\nt_vals = np.linspace(0, 1, lut_size)\nlut = np.zeros((lut_size, 3), dtype=np.uint8)\nfor ch, (s, e) in enumerate(zip(_SEQ_START, _SEQ_END, strict=True)):\n    lut[:, ch] = np.round(s + (e - s) * t_vals).astype(np.uint8)\n\n# Log-normalized color mapping (exterior = Imprint seq; interior = black)\ncell_colors = np.zeros((*c.shape, 3), dtype=np.uint8)\nlog_min, log_max = 0.0, 1.0\nif np.any(exterior):\n    iter_vals = escape_iter[exterior]\n    log_vals = np.log(iter_vals + 1)\n    log_min, log_max = log_vals.min(), log_vals.max()\n    span = log_max - log_min\n    normalized = (log_vals - log_min) / span if span > 0 else np.zeros_like(log_vals)\n    indices = np.clip((normalized * (lut_size - 1)).astype(int), 0, lut_size - 1)\n    cell_colors[exterior] = lut[indices]\n\n# Encode heatmap as PNG data URI for SVG embedding\nheatmap_img = Image.fromarray(cell_colors)\nbuf = BytesIO()\nheatmap_img.save(buf, format=\"PNG\")\nheatmap_data_uri = \"data:image/png;base64,\" + base64.b64encode(buf.getvalue()).decode()\n\n# Title — 48 chars < 67 baseline, no scaling needed\ntitle_str = \"heatmap-mandelbrot · python · pygal · anyplot.ai\"\ntitle_fontsize = round(66 * 67 / len(title_str)) if len(title_str) > 67 else 66\n\n\n# Module-level function holds all SVG generation logic — keeps class minimal\ndef _draw_heatmap_overlay(self):\n    gw = self.view.width\n    gh = self.view.height\n\n    pad_left, pad_top = 200, 80\n    pad_right, pad_bottom = 200, 100\n    x_span = self._x_range[1] - self._x_range[0]\n    y_span = self._y_range[1] - self._y_range[0]\n\n    avail_w = gw - pad_left - pad_right\n    avail_h = gh - pad_top - pad_bottom\n    if avail_w / avail_h > x_span / y_span:\n        plot_h = avail_h\n        plot_w = plot_h * x_span / y_span\n    else:\n        plot_w = avail_w\n        plot_h = plot_w * y_span / x_span\n\n    px, py = pad_left, pad_top\n    root = self.svg.node(self.nodes[\"plot\"], class_=\"mandelbrot-heatmap\")\n\n    # Embedded heatmap image\n    ns = \"http://www.w3.org/1999/xlink\"\n    img = self.svg.node(root, \"image\", x=px, y=py, width=plot_w, height=plot_h)\n    img.attrib[\"{%s}href\" % ns] = self._heatmap_uri\n    img.attrib[\"preserveAspectRatio\"] = \"none\"\n\n    # Plot border\n    self.svg.node(\n        root, \"rect\", x=px, y=py, width=plot_w, height=plot_h, style=f\"fill:none;stroke:{INK_SOFT};stroke-width:2\"\n    )\n\n    # Subtitle — mathematical formula in italic serif, sized to match axis titles\n    sub = self.svg.node(\n        root,\n        \"text\",\n        x=px + plot_w / 2,\n        y=py - 14,\n        style=(\n            f\"font-size:43px;font-style:italic;font-weight:300;\"\n            f\"font-family:'Georgia',serif;fill:{INK_MUTED};letter-spacing:1px\"\n        ),\n    )\n    sub.text = \"zₙ₊₁ = zₙ² + c · escape time, smooth coloring\"\n    sub.attrib[\"text-anchor\"] = \"middle\"\n\n    # Reference hairline at Im=0 — marks the real axis\n    im0_frac = (self._y_range[1] - 0.0) / y_span\n    im0_y = py + im0_frac * plot_h\n    self.svg.node(\n        root,\n        \"line\",\n        x1=px,\n        y1=im0_y,\n        x2=px + plot_w,\n        y2=im0_y,\n        style=f\"stroke:{INK_SOFT};stroke-width:1;stroke-dasharray:6,4;opacity:0.5\",\n    )\n    im0_lbl = self.svg.node(\n        root, \"text\", x=px + 10, y=im0_y - 9, style=f\"font-size:25px;font-family:sans-serif;fill:{INK};opacity:0.85\"\n    )\n    im0_lbl.text = \"Im = 0\"\n\n    # Period-2 bulb label — placed in colored exterior above the bulb (Im ≈ 0.37)\n    # so the label uses INK against green and is readable in both themes\n    p2_re = -1.0\n    ann_im = 0.37  # exterior above the bulb top (~Im 0.25)\n    ann_frac_x = (p2_re - self._x_range[0]) / x_span\n    ann_frac_y = (self._y_range[1] - ann_im) / y_span\n    ann_cx = px + ann_frac_x * plot_w\n    ann_cy = py + ann_frac_y * plot_h\n    bulb_top_im = 0.26\n    bulb_top_frac_y = (self._y_range[1] - bulb_top_im) / y_span\n    bulb_top_y = py + bulb_top_frac_y * plot_h\n    self.svg.node(\n        root,\n        \"line\",\n        x1=ann_cx,\n        y1=ann_cy + 6,\n        x2=ann_cx,\n        y2=bulb_top_y - 6,\n        style=f\"stroke:{INK};stroke-width:1.5;opacity:0.6\",\n    )\n    ann = self.svg.node(\n        root, \"text\", x=ann_cx, y=ann_cy, style=f\"font-size:25px;font-family:sans-serif;fill:{INK};opacity:0.85\"\n    )\n    ann.text = \"Period-2 bulb\"\n    ann.attrib[\"text-anchor\"] = \"middle\"\n\n    # X-axis ticks and labels\n    for val in [-2.5, -2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0]:\n        frac = (val - self._x_range[0]) / x_span\n        tx = px + frac * plot_w\n        ty = py + plot_h\n        self.svg.node(root, \"line\", x1=tx, y1=ty, x2=tx, y2=ty + 14, style=f\"stroke:{INK_SOFT};stroke-width:2\")\n        lbl = self.svg.node(\n            root, \"text\", x=tx, y=ty + 52, style=f\"font-size:34px;font-family:sans-serif;fill:{INK_SOFT}\"\n        )\n        lbl.text = f\"{val:.1f}\"\n        lbl.attrib[\"text-anchor\"] = \"middle\"\n\n    # X-axis title\n    xl = self.svg.node(\n        root,\n        \"text\",\n        x=px + plot_w / 2,\n        y=py + plot_h + 90,\n        style=f\"font-size:44px;font-weight:600;font-family:sans-serif;fill:{INK}\",\n    )\n    xl.text = \"Real Axis (Re)\"\n    xl.attrib[\"text-anchor\"] = \"middle\"\n\n    # Y-axis ticks and labels\n    for val in [-1.0, -0.5, 0.0, 0.5, 1.0]:\n        frac = (self._y_range[1] - val) / y_span\n        ty = py + frac * plot_h\n        self.svg.node(root, \"line\", x1=px - 14, y1=ty, x2=px, y2=ty, style=f\"stroke:{INK_SOFT};stroke-width:2\")\n        label = f\"{val:+.1f}i\" if val != 0 else \"0.0i\"\n        lbl = self.svg.node(\n            root, \"text\", x=px - 24, y=ty + 12, style=f\"font-size:34px;font-family:sans-serif;fill:{INK_SOFT}\"\n        )\n        lbl.text = label\n        lbl.attrib[\"text-anchor\"] = \"end\"\n\n    # Y-axis title (rotated)\n    ylx = px - 170\n    yly = py + plot_h / 2\n    yl = self.svg.node(\n        root, \"text\", x=ylx, y=yly, style=f\"font-size:44px;font-weight:600;font-family:sans-serif;fill:{INK}\"\n    )\n    yl.text = \"Imaginary Axis (Im)\"\n    yl.attrib[\"text-anchor\"] = \"middle\"\n    yl.attrib[\"transform\"] = f\"rotate(-90, {ylx}, {yly})\"\n\n    # Colorbar gradient\n    cb_x = px + plot_w + 30\n    cb_w = 40\n    cb_top = py + 40\n    cb_h = plot_h - 80\n    n_seg = 100\n\n    for s in range(n_seg):\n        t = 1.0 - s / (n_seg - 1)\n        ci = min(int(t * (self._lut_sz - 1)), self._lut_sz - 1)\n        r, g, b = self._colorbar_lut[ci]\n        sy = cb_top + s * cb_h / n_seg\n        self.svg.node(\n            root,\n            \"rect\",\n            x=cb_x,\n            y=sy,\n            width=cb_w,\n            height=cb_h / n_seg + 1,\n            style=f\"fill:#{r:02x}{g:02x}{b:02x};stroke:none\",\n        )\n\n    self.svg.node(\n        root, \"rect\", x=cb_x, y=cb_top, width=cb_w, height=cb_h, style=f\"fill:none;stroke:{INK_SOFT};stroke-width:1.5\"\n    )\n\n    # Colorbar tick labels\n    log_span = self._log_range[1] - self._log_range[0]\n    for iter_val in [1, 5, 10, 25, 50, 100, 200]:\n        log_val = np.log(iter_val + 1)\n        if log_val < self._log_range[0] or log_val > self._log_range[1]:\n            continue\n        t_c = (log_val - self._log_range[0]) / log_span if log_span > 0 else 0\n        frac = 1.0 - t_c\n        ty = cb_top + frac * cb_h\n        self.svg.node(\n            root, \"line\", x1=cb_x + cb_w, y1=ty, x2=cb_x + cb_w + 8, y2=ty, style=f\"stroke:{INK_SOFT};stroke-width:1.5\"\n        )\n        lbl = self.svg.node(\n            root, \"text\", x=cb_x + cb_w + 14, y=ty + 10, style=f\"font-size:28px;font-family:sans-serif;fill:{INK_SOFT}\"\n        )\n        lbl.text = str(iter_val)\n\n    # Colorbar title\n    cbt = self.svg.node(\n        root,\n        \"text\",\n        x=cb_x - 5,\n        y=cb_top - 18,\n        style=f\"font-size:32px;font-weight:600;font-family:sans-serif;fill:{INK}\",\n    )\n    cbt.text = \"Iterations\"\n    cbt.attrib[\"text-anchor\"] = \"start\"\n\n    # In-set legend\n    lg_y = cb_top + cb_h + 38\n    self.svg.node(\n        root, \"rect\", x=cb_x, y=lg_y, width=26, height=26, style=f\"fill:#000000;stroke:{INK_SOFT};stroke-width:1\"\n    )\n    lg = self.svg.node(\n        root, \"text\", x=cb_x + 38, y=lg_y + 20, style=f\"font-size:26px;font-family:sans-serif;fill:{INK_SOFT}\"\n    )\n    lg.text = \"In set\"\n\n\n# Minimal pygal Graph subclass — routing stubs only, logic lives in _draw_heatmap_overlay\nclass _HeatmapGraph(Graph):\n    _adapters = []\n    _compute = _compute_x_labels = _compute_y_labels = _compute_x_labels_major = _compute_y_labels_major = lambda self: (\n        None\n    )\n    _plot = _draw_heatmap_overlay\n\n\n# Pygal Style — theme-adaptive Imprint palette\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=(\"#009E73\",),\n    title_font_size=title_fontsize,\n    title_font_family=\"sans-serif\",\n    label_font_size=44,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n)\n\n# Create chart using pygal's rendering pipeline\nchart = _HeatmapGraph(\n    width=2400,\n    height=2400,\n    style=custom_style,\n    title=title_str,\n    show_legend=False,\n    show_x_guides=False,\n    show_y_guides=False,\n    print_values=False,\n    margin=30,\n    spacing=10,\n)\n\n# Bind heatmap data to chart instance (avoids __init__ override)\nchart._heatmap_uri = heatmap_data_uri\nchart._x_range = (x_min, x_max)\nchart._y_range = (y_min, y_max)\nchart._colorbar_lut = lut\nchart._log_range = (log_min, log_max)\nchart._lut_sz = lut_size\n\nchart.add(\"In set\", [1])\n\n# Save PNG and interactive HTML (both theme-suffixed)\nchart.render_to_png(f\"plot-{THEME}.png\")\n\nsvg_content = chart.render().decode(\"utf-8\")\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>heatmap-mandelbrot · python · pygal · anyplot.ai</title>\n    <style>\n        body {{ margin: 0; display: flex; justify-content: center; align-items: center;\n               min-height: 100vh; background: {PAGE_BG}; }}\n        .chart {{ max-width: 100%; height: auto; }}\n    </style>\n</head>\n<body>\n    <figure class=\"chart\">\n        {svg_content}\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"}