{"spec_id":"voronoi-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nvoronoi-basic: Voronoi Diagram for Spatial Partitioning\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 73/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport xml.etree.ElementTree as ET\n\nimport cairosvg\nimport numpy as np\nfrom scipy.spatial import Voronoi\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\"\nBRAND = \"#009E73\"\n\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\nnp.random.seed(42)\npoints = np.random.uniform(0.5, 10.5, size=(25, 2))\n\nvor = Voronoi(points)\n\nWIDTH = 4800\nHEIGHT = 2700\nmargin_left = 400\nmargin_right = 200\nmargin_top = 300\nmargin_bottom = 250\n\nplot_width = WIDTH - margin_left - margin_right\nplot_height = HEIGHT - margin_top - margin_bottom\n\nx_min, x_max = 0.5, 10.5\ny_min, y_max = 0.5, 10.5\n\n\ndef data_to_svg(x, y):\n    svg_x = margin_left + (x - x_min) / (x_max - x_min) * plot_width\n    svg_y = margin_top + (y_max - y) / (y_max - y_min) * plot_height\n    return svg_x, svg_y\n\n\ndef voronoi_finite_polygons_2d(vor, radius=None):\n    if vor.points.shape[1] != 2:\n        raise ValueError(\"Requires 2D input\")\n\n    new_regions = []\n    new_vertices = vor.vertices.tolist()\n\n    center = vor.points.mean(axis=0)\n    if radius is None:\n        radius = vor.points.ptp().max() * 2\n\n    all_ridges = {}\n    for (p1, p2), (v1, v2) in zip(vor.ridge_points, vor.ridge_vertices, strict=True):\n        all_ridges.setdefault(p1, []).append((p2, v1, v2))\n        all_ridges.setdefault(p2, []).append((p1, v1, v2))\n\n    for p1, region in enumerate(vor.point_region):\n        vertices = vor.regions[region]\n\n        if all(v >= 0 for v in vertices):\n            new_regions.append(vertices)\n            continue\n\n        ridges = all_ridges[p1]\n        new_region = [v for v in vertices if v >= 0]\n\n        for p2, v1, v2 in ridges:\n            if v2 < 0:\n                v1, v2 = v2, v1\n            if v1 >= 0:\n                continue\n\n            t = vor.points[p2] - vor.points[p1]\n            t /= np.linalg.norm(t)\n            n = np.array([-t[1], t[0]])\n\n            midpoint = vor.points[[p1, p2]].mean(axis=0)\n            direction = np.sign(np.dot(midpoint - center, n)) * n\n            far_point = vor.vertices[v2] + direction * radius\n\n            new_region.append(len(new_vertices))\n            new_vertices.append(far_point.tolist())\n\n        vs = np.asarray([new_vertices[v] for v in new_region])\n        c = vs.mean(axis=0)\n        angles = np.arctan2(vs[:, 1] - c[1], vs[:, 0] - c[0])\n        new_region = np.array(new_region)[np.argsort(angles)].tolist()\n\n        new_regions.append(new_region)\n\n    return new_regions, np.asarray(new_vertices)\n\n\ndef clip_polygon_to_box(vertices, x_min, x_max, y_min, y_max):\n    def inside_edge(p, edge):\n        x, y = p\n        if edge == \"left\":\n            return x >= x_min\n        if edge == \"right\":\n            return x <= x_max\n        if edge == \"bottom\":\n            return y >= y_min\n        if edge == \"top\":\n            return y <= y_max\n\n    def intersect(p1, p2, edge):\n        x1, y1 = p1\n        x2, y2 = p2\n        dx = x2 - x1\n        dy = y2 - y1\n\n        if edge == \"left\":\n            t = (x_min - x1) / (dx + 1e-12)\n        elif edge == \"right\":\n            t = (x_max - x1) / (dx + 1e-12)\n        elif edge == \"bottom\":\n            t = (y_min - y1) / (dy + 1e-12)\n        else:\n            t = (y_max - y1) / (dy + 1e-12)\n\n        return (x1 + t * dx, y1 + t * dy)\n\n    output = list(vertices)\n\n    for edge in [\"left\", \"right\", \"bottom\", \"top\"]:\n        if len(output) == 0:\n            break\n        input_list = output\n        output = []\n\n        for i in range(len(input_list)):\n            current = input_list[i]\n            prev = input_list[i - 1]\n\n            if inside_edge(current, edge):\n                if not inside_edge(prev, edge):\n                    output.append(intersect(prev, current, edge))\n                output.append(current)\n            elif inside_edge(prev, edge):\n                output.append(intersect(prev, current, edge))\n\n    return output\n\n\nregions, vertices = voronoi_finite_polygons_2d(vor, radius=20)\n\nsvg_ns = \"http://www.w3.org/2000/svg\"\nET.register_namespace(\"\", svg_ns)\n\nsvg_root = ET.Element(\"svg\", xmlns=svg_ns, width=str(WIDTH), height=str(HEIGHT), viewBox=f\"0 0 {WIDTH} {HEIGHT}\")\nsvg_root.set(\"style\", f\"background-color: {PAGE_BG};\")\n\ntitle_elem = ET.SubElement(svg_root, \"text\")\ntitle_elem.set(\"x\", str(WIDTH / 2))\ntitle_elem.set(\"y\", \"150\")\ntitle_elem.set(\"text-anchor\", \"middle\")\ntitle_elem.set(\"fill\", INK)\ntitle_elem.set(\"font-size\", \"28\")\ntitle_elem.set(\"font-family\", \"sans-serif\")\ntitle_elem.set(\"font-weight\", \"500\")\ntitle_elem.text = \"voronoi-basic · pygal · anyplot.ai\"\n\nx_label = ET.SubElement(svg_root, \"text\")\nx_label.set(\"x\", str(margin_left + plot_width / 2))\nx_label.set(\"y\", str(HEIGHT - 80))\nx_label.set(\"text-anchor\", \"middle\")\nx_label.set(\"fill\", INK)\nx_label.set(\"font-size\", \"22\")\nx_label.set(\"font-family\", \"sans-serif\")\nx_label.text = \"X Coordinate (units)\"\n\ny_label = ET.SubElement(svg_root, \"text\")\ny_label.set(\"x\", \"110\")\ny_label.set(\"y\", str(margin_top + plot_height / 2))\ny_label.set(\"text-anchor\", \"middle\")\ny_label.set(\"fill\", INK)\ny_label.set(\"font-size\", \"22\")\ny_label.set(\"font-family\", \"sans-serif\")\ny_label.set(\"transform\", f\"rotate(-90, 110, {margin_top + plot_height / 2})\")\ny_label.text = \"Y Coordinate (units)\"\n\nplot_bg = ET.SubElement(svg_root, \"rect\")\nplot_bg.set(\"x\", str(margin_left))\nplot_bg.set(\"y\", str(margin_top))\nplot_bg.set(\"width\", str(plot_width))\nplot_bg.set(\"height\", str(plot_height))\nplot_bg.set(\"fill\", PAGE_BG)\nplot_bg.set(\"stroke\", INK_SOFT)\nplot_bg.set(\"stroke-width\", \"2\")\n\ndefs = ET.SubElement(svg_root, \"defs\")\nclip_path = ET.SubElement(defs, \"clipPath\", id=\"plot-area\")\nclip_rect = ET.SubElement(clip_path, \"rect\")\nclip_rect.set(\"x\", str(margin_left))\nclip_rect.set(\"y\", str(margin_top))\nclip_rect.set(\"width\", str(plot_width))\nclip_rect.set(\"height\", str(plot_height))\n\ngrid_g = ET.SubElement(svg_root, \"g\")\ngrid_g.set(\"class\", \"grid\")\n\ngrid_color = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\nfor x_val in np.arange(1, 11, 1):\n    sx, _ = data_to_svg(x_val, 0)\n    _, sy_top = data_to_svg(0, y_max)\n    _, sy_bot = data_to_svg(0, y_min)\n    line = ET.SubElement(grid_g, \"line\")\n    line.set(\"x1\", f\"{sx:.1f}\")\n    line.set(\"y1\", f\"{sy_top:.1f}\")\n    line.set(\"x2\", f\"{sx:.1f}\")\n    line.set(\"y2\", f\"{sy_bot:.1f}\")\n    line.set(\"stroke\", INK_SOFT)\n    line.set(\"stroke-width\", \"1\")\n    line.set(\"opacity\", \"0.15\")\n\nfor y_val in np.arange(1, 11, 1):\n    _, sy = data_to_svg(0, y_val)\n    sx_left, _ = data_to_svg(x_min, 0)\n    sx_right, _ = data_to_svg(x_max, 0)\n    line = ET.SubElement(grid_g, \"line\")\n    line.set(\"x1\", f\"{sx_left:.1f}\")\n    line.set(\"y1\", f\"{sy:.1f}\")\n    line.set(\"x2\", f\"{sx_right:.1f}\")\n    line.set(\"y2\", f\"{sy:.1f}\")\n    line.set(\"stroke\", INK_SOFT)\n    line.set(\"stroke-width\", \"1\")\n    line.set(\"opacity\", \"0.15\")\n\nfor x_val in range(1, 11, 2):\n    sx, _ = data_to_svg(x_val, 0)\n    tick_label = ET.SubElement(svg_root, \"text\")\n    tick_label.set(\"x\", f\"{sx:.1f}\")\n    tick_label.set(\"y\", str(margin_top + plot_height + 60))\n    tick_label.set(\"text-anchor\", \"middle\")\n    tick_label.set(\"fill\", INK_SOFT)\n    tick_label.set(\"font-size\", \"18\")\n    tick_label.set(\"font-family\", \"sans-serif\")\n    tick_label.text = str(x_val)\n\nfor y_val in range(1, 11, 2):\n    _, sy = data_to_svg(0, y_val)\n    tick_label = ET.SubElement(svg_root, \"text\")\n    tick_label.set(\"x\", str(margin_left - 40))\n    tick_label.set(\"y\", f\"{sy + 8:.1f}\")\n    tick_label.set(\"text-anchor\", \"end\")\n    tick_label.set(\"fill\", INK_SOFT)\n    tick_label.set(\"font-size\", \"18\")\n    tick_label.set(\"font-family\", \"sans-serif\")\n    tick_label.text = str(y_val)\n\ncells_g = ET.SubElement(svg_root, \"g\")\ncells_g.set(\"class\", \"voronoi-cells\")\ncells_g.set(\"clip-path\", \"url(#plot-area)\")\n\nfor i, region in enumerate(regions):\n    if not region or len(region) < 3:\n        continue\n\n    poly_verts = [vertices[v] for v in region]\n    clipped = clip_polygon_to_box(poly_verts, x_min, x_max, y_min, y_max)\n\n    if len(clipped) < 3:\n        continue\n\n    svg_points = [data_to_svg(x, y) for x, y in clipped]\n    points_str = \" \".join([f\"{x:.1f},{y:.1f}\" for x, y in svg_points])\n\n    polygon = ET.SubElement(cells_g, \"polygon\")\n    polygon.set(\"points\", points_str)\n    color = IMPRINT[i % len(IMPRINT)]\n    polygon.set(\"fill\", color)\n    polygon.set(\"fill-opacity\", \"0.6\")\n    polygon.set(\"stroke\", INK_SOFT)\n    polygon.set(\"stroke-width\", \"2\")\n\n    title = ET.SubElement(polygon, \"title\")\n    title.text = f\"Region {i + 1}: ({vor.points[i][0]:.2f}, {vor.points[i][1]:.2f})\"\n\npoints_g = ET.SubElement(svg_root, \"g\")\npoints_g.set(\"class\", \"seed-points\")\n\nfor i, (px, py) in enumerate(vor.points):\n    sx, sy = data_to_svg(px, py)\n\n    outer = ET.SubElement(points_g, \"circle\")\n    outer.set(\"cx\", f\"{sx:.1f}\")\n    outer.set(\"cy\", f\"{sy:.1f}\")\n    outer.set(\"r\", \"16\")\n    outer.set(\"fill\", PAGE_BG)\n    outer.set(\"stroke\", INK_SOFT)\n    outer.set(\"stroke-width\", \"2\")\n\n    inner = ET.SubElement(points_g, \"circle\")\n    inner.set(\"cx\", f\"{sx:.1f}\")\n    inner.set(\"cy\", f\"{sy:.1f}\")\n    inner.set(\"r\", \"12\")\n    color = IMPRINT[i % len(IMPRINT)]\n    inner.set(\"fill\", color)\n\n    title = ET.SubElement(inner, \"title\")\n    title.text = f\"Point {i + 1}: ({px:.2f}, {py:.2f})\"\n\nsvg_output = ET.tostring(svg_root, encoding=\"unicode\")\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(svg_output)\n\ncairosvg.svg2png(bytestring=svg_output.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n"}