{"spec_id":"map-marker-clustered","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nmap-marker-clustered: Clustered Marker Map\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_fill_identity,\n    scale_size_continuous,\n    theme,\n)\nfrom scipy.cluster.hierarchy import fcluster, linkage\nfrom scipy.spatial import ConvexHull\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — first series is always #009E73\nCATEGORY_COLORS = {\"Retail\": \"#009E73\", \"Restaurant\": \"#C475FD\", \"Service\": \"#AE3030\", \"Entertainment\": \"#4467A3\"}\n\n# Data: US West Coast retail store locations\nnp.random.seed(42)\n\ncity_centers = {\n    \"Seattle\": (47.6, -122.3),\n    \"Portland\": (45.5, -122.7),\n    \"San Francisco\": (37.8, -122.4),\n    \"Los Angeles\": (34.1, -118.2),\n    \"San Diego\": (32.7, -117.2),\n}\n\n# Per-city weighted categories ensure each cluster has a visually distinct dominant color\ncategory_types = [\"Retail\", \"Restaurant\", \"Service\", \"Entertainment\"]\ncity_weights = {\n    \"Seattle\": [0.70, 0.10, 0.10, 0.10],  # Retail  → #009E73\n    \"Portland\": [0.10, 0.70, 0.10, 0.10],  # Restaurant → #C475FD\n    \"San Francisco\": [0.10, 0.10, 0.70, 0.10],  # Service → #AE3030\n    \"Los Angeles\": [0.10, 0.10, 0.10, 0.70],  # Entertainment → #4467A3\n    \"San Diego\": [0.70, 0.10, 0.10, 0.10],  # Retail → #009E73 (far from Seattle)\n}\n\nn_points = 300\nlats, lons, cats = [], [], []\n\nfor _ in range(n_points):\n    city = np.random.choice(list(city_centers.keys()))\n    clat, clon = city_centers[city]\n    lats.append(clat + np.random.normal(0, 0.04))\n    lons.append(clon + np.random.normal(0, 0.04))\n    cats.append(np.random.choice(category_types, p=city_weights[city]))\n\ndf = pd.DataFrame({\"lat\": lats, \"lon\": lons, \"category\": cats})\n\n# Hierarchical clustering — force exactly 5 clusters (one per city)\nZ = linkage(df[[\"lat\", \"lon\"]].values, method=\"ward\")\ndf[\"cluster\"] = fcluster(Z, t=5, criterion=\"maxclust\")\n\ncluster_markers = (\n    df.groupby(\"cluster\")\n    .agg(\n        lat=(\"lat\", \"mean\"),\n        lon=(\"lon\", \"mean\"),\n        count=(\"cluster\", \"size\"),\n        category=(\"category\", lambda x: x.mode().iloc[0]),\n    )\n    .reset_index()\n)\ncluster_markers[\"label\"] = cluster_markers[\"count\"].astype(str)\n\n# Convex hull polygons showing each cluster's geographic extent\nhull_rows = []\nfor cluster_id in sorted(df[\"cluster\"].unique()):\n    pts = df[df[\"cluster\"] == cluster_id][[\"lon\", \"lat\"]].values\n    if len(pts) >= 3:\n        try:\n            hull = ConvexHull(pts)\n            verts = pts[hull.vertices]\n            verts = np.vstack([verts, verts[0]])\n            cat = cluster_markers.loc[cluster_markers[\"cluster\"] == cluster_id, \"category\"].iloc[0]\n            for lon_v, lat_v in verts:\n                hull_rows.append({\"lon\": lon_v, \"lat\": lat_v, \"cluster\": cluster_id, \"category\": cat})\n        except Exception:\n            pass\n\nhull_df = pd.DataFrame(hull_rows)\nhull_df[\"fill_color\"] = hull_df[\"category\"].map(CATEGORY_COLORS)\n\n# Simplified West Coast state outlines for geographic context\nstate_boundaries = pd.concat(\n    [\n        pd.DataFrame(\n            {\n                \"lat\": [45.5, 49.0, 49.0, 47.5, 46.2, 45.5, 45.5],\n                \"lon\": [-117.0, -117.0, -123.5, -124.7, -124.0, -123.9, -117.0],\n                \"state\": \"WA\",\n            }\n        ),\n        pd.DataFrame(\n            {\n                \"lat\": [42.0, 45.5, 45.5, 44.0, 42.0, 42.0],\n                \"lon\": [-117.0, -117.0, -123.9, -124.3, -124.5, -117.0],\n                \"state\": \"OR\",\n            }\n        ),\n        pd.DataFrame(\n            {\n                \"lat\": [42.0, 40.5, 38.0, 36.0, 34.5, 33.5, 32.5, 32.5, 42.0],\n                \"lon\": [-124.4, -124.4, -123.0, -121.9, -120.5, -118.0, -117.1, -114.6, -114.6],\n                \"state\": \"CA\",\n            }\n        ),\n    ],\n    ignore_index=True,\n)\n\n# City reference labels for geographic orientation\ncity_label_df = pd.DataFrame(\n    [{\"lat\": lat + 0.65, \"lon\": lon, \"name\": city} for city, (lat, lon) in city_centers.items()]\n)\n\n# Plot\nplot = (\n    ggplot(cluster_markers, aes(x=\"lon\", y=\"lat\"))\n    + geom_polygon(\n        data=hull_df,\n        mapping=aes(x=\"lon\", y=\"lat\", group=\"cluster\", fill=\"fill_color\"),\n        alpha=0.13,\n        color=INK_SOFT,\n        size=0.25,\n    )\n    + geom_path(\n        data=state_boundaries, mapping=aes(x=\"lon\", y=\"lat\", group=\"state\"), color=INK_SOFT, size=0.4, alpha=0.5\n    )\n    + geom_text(data=city_label_df, mapping=aes(x=\"lon\", y=\"lat\", label=\"name\"), color=INK_MUTED, size=8.5, va=\"bottom\")\n    + geom_point(aes(size=\"count\", color=\"category\"), alpha=0.85, stroke=1.2)\n    + geom_text(aes(label=\"label\"), size=8, color=\"white\", fontweight=\"bold\")\n    + scale_color_manual(values=CATEGORY_COLORS, name=\"Category\")\n    + scale_fill_identity()\n    + scale_size_continuous(range=(6, 13), name=\"Points in cluster\")\n    + coord_fixed(ratio=0.7, xlim=(-125.5, -116.5), ylim=(31.5, 50.5))\n    + labs(title=\"map-marker-clustered · python · plotnine · anyplot.ai\", x=\"Longitude (°)\", y=\"Latitude (°N)\")\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_line(color=INK, size=0.3, alpha=0.10),\n        panel_grid_minor=element_line(color=INK, size=0.2, alpha=0.05),\n        panel_border=element_blank(),\n        axis_title=element_text(color=INK, size=10),\n        axis_text=element_text(color=INK_SOFT, size=8),\n        axis_line=element_blank(),\n        axis_ticks=element_blank(),\n        plot_title=element_text(color=INK, size=12),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(color=INK_SOFT, size=8),\n        legend_title=element_text(color=INK, size=9),\n        legend_position=\"right\",\n        legend_box=\"vertical\",\n    )\n    + guides(\n        color=guide_legend(override_aes={\"size\": 7, \"alpha\": 1}),\n        size=guide_legend(override_aes={\"color\": INK_SOFT, \"alpha\": 0.8}),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}