{"spec_id":"voronoi-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nvoronoi-basic: Voronoi Diagram for Spatial Partitioning\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove current directory from path to avoid shadowing altair library\n_current_dir = os.path.dirname(os.path.abspath(__file__))\n_sys_path_backup = sys.path.copy()\nsys.path = [p for p in sys.path if os.path.abspath(p) != _current_dir]\n\ntry:\n    import altair as alt\nfinally:\n    sys.path = _sys_path_backup\n\nimport numpy as np\nimport pandas as pd\nfrom scipy.spatial import Voronoi\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - Generate seed points for weather stations\nnp.random.seed(42)\nn_points = 15\nx_points = np.random.uniform(5, 95, n_points)\ny_points = np.random.uniform(5, 95, n_points)\nlabels = [f\"Station {i + 1}\" for i in range(n_points)]\n\npoints = np.column_stack([x_points, y_points])\n\n# Add boundary points to help with clipping\nx_min, x_max = 0, 100\ny_min, y_max = 0, 100\nmargin = 200\n\nboundary_points = []\nfor px, py in points:\n    boundary_points.append([2 * x_min - margin - px, py])\n    boundary_points.append([2 * x_max + margin - px, py])\n    boundary_points.append([px, 2 * y_min - margin - py])\n    boundary_points.append([px, 2 * y_max + margin - py])\n\nall_points = np.vstack([points, boundary_points])\n\n# Compute Voronoi diagram\nvor = Voronoi(all_points)\n\n# Create filled polygon data for Voronoi cells\npolygon_data = []\nfor point_idx in range(n_points):\n    region_idx = vor.point_region[point_idx]\n    region = vor.regions[region_idx]\n\n    if not region or -1 in region:\n        continue\n\n    vertices = vor.vertices[region]\n    clipped_x = np.clip(vertices[:, 0], x_min, x_max)\n    clipped_y = np.clip(vertices[:, 1], y_min, y_max)\n\n    # Sort vertices by angle for proper polygon ordering\n    center_x = np.mean(clipped_x)\n    center_y = np.mean(clipped_y)\n    angles = np.arctan2(clipped_y - center_y, clipped_x - center_x)\n    sorted_indices = np.argsort(angles)\n\n    sorted_x = clipped_x[sorted_indices]\n    sorted_y = clipped_y[sorted_indices]\n\n    color = IMPRINT[point_idx % len(IMPRINT)]\n    station = labels[point_idx]\n\n    # Add vertices for filled polygon\n    for i in range(len(sorted_x)):\n        polygon_data.append(\n            {\"x\": sorted_x[i], \"y\": sorted_y[i], \"order\": i, \"cell_id\": point_idx, \"station\": station, \"color\": color}\n        )\n    # Close polygon by repeating first vertex\n    polygon_data.append(\n        {\n            \"x\": sorted_x[0],\n            \"y\": sorted_y[0],\n            \"order\": len(sorted_x),\n            \"cell_id\": point_idx,\n            \"station\": station,\n            \"color\": color,\n        }\n    )\n\ndf_polygons = pd.DataFrame(polygon_data)\n\n# Create DataFrame for seed points\ndf_points = pd.DataFrame(\n    {\n        \"x\": x_points,\n        \"y\": y_points,\n        \"label\": labels,\n        \"station\": labels,\n        \"color\": [IMPRINT[i % len(IMPRINT)] for i in range(n_points)],\n    }\n)\n\n# Create filled Voronoi regions\nvoronoi_cells = (\n    alt.Chart(df_polygons)\n    .mark_line(filled=True, opacity=0.50, strokeWidth=2.5)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[x_min - 2, x_max + 2]), title=\"X Coordinate\"),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[y_min - 2, y_max + 2]), title=\"Y Coordinate\"),\n        color=alt.Color(\n            \"station:N\",\n            scale=alt.Scale(domain=labels, range=IMPRINT[:n_points]),\n            legend=alt.Legend(\n                title=\"Weather Stations\",\n                titleFontSize=16,\n                labelFontSize=14,\n                columns=2,\n                orient=\"right\",\n                symbolType=\"square\",\n                symbolSize=150,\n            ),\n        ),\n        order=\"order:O\",\n        detail=\"cell_id:N\",\n        stroke=alt.value(INK_SOFT),\n    )\n)\n\n# Create seed points layer\npoints_layer = (\n    alt.Chart(df_points)\n    .mark_circle(size=300, stroke=PAGE_BG, strokeWidth=3)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        color=alt.Color(\"station:N\", scale=alt.Scale(domain=labels, range=IMPRINT[:n_points]), legend=None),\n        tooltip=[\n            alt.Tooltip(\"label:N\", title=\"Station\"),\n            alt.Tooltip(\"x:Q\", format=\".1f\", title=\"X\"),\n            alt.Tooltip(\"y:Q\", format=\".1f\", title=\"Y\"),\n        ],\n    )\n)\n\n# Add station labels with adjusted positioning to reduce overlap\nlabels_layer = (\n    alt.Chart(df_points)\n    .mark_text(dy=-18, fontSize=13, fontWeight=\"bold\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"label:N\", color=alt.value(INK))\n)\n\n# Combine layers\nchart = (\n    (voronoi_cells + points_layer + labels_layer)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"voronoi-basic · altair · anyplot.ai\", fontSize=28, anchor=\"middle\"),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=18,\n        titleFontSize=22,\n    )\n    .configure_title(color=INK)\n    .configure_legend(\n        fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, labelFontSize=14\n    )\n)\n\n# Save output\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}