{"spec_id":"map-marker-clustered","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nmap-marker-clustered: Clustered Marker Map\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\nfrom sklearn.cluster import AgglomerativeClustering\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 — canonical order, 4 categories\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#AE3030\", \"#4467A3\"]\ncategory_names = [\"Coffee Shop\", \"Restaurant\", \"Bookstore\", \"Gym\"]\ncategory_palette = dict(zip(category_names, IMPRINT, strict=True))\n\n# Data — business locations across NYC region\nnp.random.seed(42)\nn_points = 500\n\nn_neighborhoods = 8\nneighborhood_centers = np.random.uniform(-0.5, 0.5, (n_neighborhoods, 2))\npoints_per_neighborhood = n_points // n_neighborhoods\n\nlats, lons, categories = [], [], []\nfor i, center in enumerate(neighborhood_centers):\n    n_pts = points_per_neighborhood + (n_points % n_neighborhoods if i == 0 else 0)\n    lat = np.random.normal(center[0], 0.08, n_pts) + 40.7\n    lon = np.random.normal(center[1], 0.08, n_pts) - 74.0\n    lats.extend(lat)\n    lons.extend(lon)\n    categories.extend(np.random.choice(category_names, n_pts))\n\ndf = pd.DataFrame({\"lat\": lats, \"lon\": lons, \"category\": categories})\n\n# Apply hierarchical clustering to group nearby markers\ncoords = df[[\"lat\", \"lon\"]].values\nclustering = AgglomerativeClustering(n_clusters=None, distance_threshold=0.18, linkage=\"ward\")\ndf[\"cluster\"] = clustering.fit_predict(coords)\n\n# Cluster centers, sizes and dominant category\ncluster_stats = (\n    df.groupby(\"cluster\")\n    .agg(\n        lat_center=(\"lat\", \"mean\"),\n        lon_center=(\"lon\", \"mean\"),\n        count=(\"lat\", \"size\"),\n        dominant_category=(\"category\", lambda x: x.mode().iloc[0]),\n    )\n    .reset_index()\n)\n\n# Compute actual cluster size thresholds for accurate legend labels\ncount_min = int(cluster_stats[\"count\"].min())\ncount_p33 = int(cluster_stats[\"count\"].quantile(0.33))\ncount_p66 = int(cluster_stats[\"count\"].quantile(0.66))\ncount_max = int(cluster_stats[\"count\"].max())\n\n# Set seaborn theme with theme-adaptive chrome\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.10,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Geographic context — stylized NYC region water boundaries\nhudson_river = [(-74.05, 40.70), (-74.02, 40.85), (-73.95, 41.00), (-73.90, 41.15)]\nli_sound = [(-73.80, 40.85), (-73.70, 40.95), (-73.55, 41.05)]\ncoast = [(-74.20, 40.55), (-74.00, 40.50), (-73.80, 40.58), (-73.60, 40.62)]\n\nfor segment in [hudson_river, li_sound, coast]:\n    xs, ys = zip(*segment, strict=True)\n    ax.plot(xs, ys, color=\"#a8d4e6\", linewidth=1.5, alpha=0.4, zorder=0, solid_capstyle=\"round\")\n\nland_fill = \"#f5f5dc\" if THEME == \"light\" else \"#2a2a20\"\nland_patch = mpatches.Polygon(\n    [(-74.45, 40.0), (-74.45, 41.25), (-73.35, 41.25), (-73.35, 40.0)],\n    facecolor=land_fill,\n    edgecolor=\"none\",\n    alpha=0.3,\n    zorder=-1,\n)\nax.add_patch(land_patch)\n\n# KDE density contours — seaborn-distinctive statistical overlay showing point density\nsns.kdeplot(data=df, x=\"lon\", y=\"lat\", levels=6, color=INK_MUTED, alpha=0.35, linewidths=0.9, ax=ax, zorder=1)\n\n# Background layer — individual points at minimal alpha\nsns.scatterplot(\n    data=df, x=\"lon\", y=\"lat\", hue=\"category\", palette=category_palette, s=6, alpha=0.08, ax=ax, legend=False\n)\n\n# Foreground layer — cluster markers sized by count\nsns.scatterplot(\n    data=cluster_stats,\n    x=\"lon_center\",\n    y=\"lat_center\",\n    size=\"count\",\n    hue=\"dominant_category\",\n    palette=category_palette,\n    sizes=(50, 500),\n    alpha=0.85,\n    edgecolor=PAGE_BG,\n    linewidth=0.8,\n    ax=ax,\n    legend=False,\n)\n\n# Count labels on clusters\nfor _, row in cluster_stats.iterrows():\n    if row[\"count\"] > 1:\n        ax.annotate(\n            str(int(row[\"count\"])),\n            (row[\"lon_center\"], row[\"lat_center\"]),\n            ha=\"center\",\n            va=\"center\",\n            fontsize=8,\n            fontweight=\"bold\",\n            color=\"white\",\n            zorder=10,\n        )\n\n# Category legend\ncat_handles = [\n    mpatches.Patch(facecolor=category_palette[cat], edgecolor=PAGE_BG, linewidth=0.5, label=cat)\n    for cat in category_names\n]\ncat_legend = ax.legend(\n    handles=cat_handles,\n    title=\"Business Type\",\n    loc=\"upper left\",\n    fontsize=8,\n    title_fontsize=9,\n    framealpha=0.95,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n)\ncat_legend.get_title().set_color(INK)\nfor text in cat_legend.get_texts():\n    text.set_color(INK_SOFT)\n\n# Size legend — labels derived from actual computed cluster size ranges\nsmall_label = f\"{count_min} pt\" if count_min == count_p33 else f\"{count_min}–{count_p33} pts\"\nmedium_label = f\"{count_p33 + 1} pt\" if count_p33 + 1 == count_p66 else f\"{count_p33 + 1}–{count_p66} pts\"\nlarge_label = f\"{count_p66 + 1}–{count_max} pts\" if count_p66 + 1 < count_max else f\"{count_max}+ pts\"\n\nsize_handles = [\n    Line2D([0], [0], marker=\"o\", color=\"w\", markerfacecolor=INK_SOFT, markersize=5, alpha=0.7, label=small_label),\n    Line2D([0], [0], marker=\"o\", color=\"w\", markerfacecolor=INK_SOFT, markersize=9, alpha=0.7, label=medium_label),\n    Line2D([0], [0], marker=\"o\", color=\"w\", markerfacecolor=INK_SOFT, markersize=13, alpha=0.7, label=large_label),\n]\nsize_legend = ax.legend(\n    handles=size_handles,\n    title=\"Cluster Size\",\n    loc=\"lower left\",\n    fontsize=8,\n    title_fontsize=9,\n    framealpha=0.95,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n)\nsize_legend.get_title().set_color(INK)\nfor text in size_legend.get_texts():\n    text.set_color(INK_SOFT)\nax.add_artist(cat_legend)\n\n# Style\nax.set_xlabel(\"Longitude (°)\", fontsize=10, color=INK)\nax.set_ylabel(\"Latitude (°)\", fontsize=10, color=INK)\nax.set_title(\"map-marker-clustered · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\n\nsns.despine(ax=ax, left=False, bottom=False)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.spines[\"bottom\"].set_color(INK_SOFT)\nax.grid(True, alpha=0.10, linestyle=\"-\", linewidth=0.6, color=INK)\n\n# Summary annotation (lower right, away from size legend)\nax.text(\n    0.98,\n    0.02,\n    f\"{n_points} locations · {len(cluster_stats)} clusters\",\n    transform=ax.transAxes,\n    fontsize=7,\n    ha=\"right\",\n    va=\"bottom\",\n    style=\"italic\",\n    color=INK_MUTED,\n    bbox={\"boxstyle\": \"round,pad=0.3\", \"facecolor\": ELEVATED_BG, \"alpha\": 0.85, \"edgecolor\": INK_SOFT},\n)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}