{"spec_id":"map-marker-clustered","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nmap-marker-clustered: Clustered Marker Map\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.patches import FancyArrowPatch, Rectangle\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# Geographic context colors (theme-adaptive)\nOCEAN_BG = \"#C4D9E8\" if THEME == \"light\" else \"#192633\"\nLAND_BG = \"#EDE8D4\" if THEME == \"light\" else \"#2A2820\"\nBORDER_COL = \"#AAAAAA\" if THEME == \"light\" else \"#555550\"\n\n# anyplot categorical palette (positions 1-3)\ncat_names = [\"Retail\", \"Grocery\", \"Electronics\"]\ncat_colors = {\"Retail\": \"#009E73\", \"Grocery\": \"#C475FD\", \"Electronics\": \"#AE3030\"}\n\n# Data: European store locations clustered by city\nnp.random.seed(42)\ncity_centers = [\n    (48.8566, 2.3522),  # Paris\n    (51.5074, -0.1278),  # London\n    (52.5200, 13.4050),  # Berlin\n    (41.9028, 12.4964),  # Rome\n    (40.4168, -3.7038),  # Madrid\n    (48.2082, 16.3738),  # Vienna\n    (50.0755, 14.4378),  # Prague\n    (52.3676, 4.9041),  # Amsterdam\n]\nn_points_per_city = [45, 50, 35, 40, 30, 25, 20, 35]\n\nlats, lons, categories = [], [], []\nfor (lat, lon), n_points in zip(city_centers, n_points_per_city, strict=True):\n    lats.extend(np.random.normal(lat, 0.8, n_points))\n    lons.extend(np.random.normal(lon, 1.2, n_points))\n    categories.extend(np.random.choice(cat_names, n_points))\n\nlats = np.array(lats)\nlons = np.array(lons)\ncategories = np.array(categories)\n\n# Grid-based clustering (simulates zoom-level clustering)\ngrid_size_lat = 3.0\ngrid_size_lon = 4.0\nlat_bins = np.floor((lats - 36) / grid_size_lat).astype(int)\nlon_bins = np.floor((lons + 12) / grid_size_lon).astype(int)\ncell_ids = lat_bins * 100 + lon_bins\n\ncluster_centers, cluster_sizes, cluster_dominant_cat = [], [], []\nfor cell_id in np.unique(cell_ids):\n    mask = cell_ids == cell_id\n    cluster_cats = categories[mask]\n    unique_cats, counts = np.unique(cluster_cats, return_counts=True)\n    cluster_centers.append((np.mean(lats[mask]), np.mean(lons[mask])))\n    cluster_sizes.append(int(np.sum(mask)))\n    cluster_dominant_cat.append(unique_cats[np.argmax(counts)])\n\ncluster_centers = np.array(cluster_centers)\ncluster_sizes = np.array(cluster_sizes)\ncluster_dominant_cat = np.array(cluster_dominant_cat)\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(OCEAN_BG)\n\n# Simplified European land polygon\neurope_coast_lon = [\n    -10,\n    -9,\n    -9.5,\n    -8,\n    -5,\n    -2,\n    0,\n    2,\n    3,\n    5,\n    7,\n    9,\n    10,\n    12,\n    13,\n    15,\n    16,\n    18,\n    20,\n    22,\n    22,\n    20,\n    18,\n    15,\n    12,\n    10,\n    8,\n    5,\n    3,\n    0,\n    -2,\n    -5,\n    -8,\n    -10,\n    -10,\n]\neurope_coast_lat = [\n    36,\n    37,\n    40,\n    42,\n    43,\n    44,\n    46,\n    47,\n    50,\n    52,\n    54,\n    55,\n    54,\n    52,\n    50,\n    48,\n    46,\n    44,\n    42,\n    40,\n    56,\n    56,\n    55,\n    54,\n    55,\n    55,\n    54,\n    52,\n    50,\n    51,\n    52,\n    48,\n    44,\n    40,\n    36,\n]\nax.fill(europe_coast_lon, europe_coast_lat, color=LAND_BG, alpha=0.9, zorder=1)\n\n# British Isles — Great Britain and Ireland (fills the map's western extent)\ngb_lon = [-5.7, 1.8, 1.5, 0.0, -2.0, -4.0, -5.5, -5.0, -5.5, -5.7]\ngb_lat = [50.0, 51.2, 53.0, 54.5, 55.0, 55.8, 55.0, 54.0, 52.0, 50.0]\nax.fill(gb_lon, gb_lat, color=LAND_BG, alpha=0.9, zorder=1)\n\nie_lon = [-10.5, -8.0, -6.0, -6.0, -6.5, -7.5, -10.0, -10.5]\nie_lat = [51.5, 51.5, 52.0, 53.0, 54.5, 55.0, 54.5, 51.5]\nax.fill(ie_lon, ie_lat, color=LAND_BG, alpha=0.9, zorder=1)\n\n# Country boundary lines\nax.plot([-2, 3], [42.5, 42.5], color=BORDER_COL, linewidth=0.5, alpha=0.4, zorder=2)\nax.plot([6, 8, 8], [49, 49, 47], color=BORDER_COL, linewidth=0.5, alpha=0.4, zorder=2)\nax.plot([15, 15], [51, 54], color=BORDER_COL, linewidth=0.5, alpha=0.4, zorder=2)\nax.plot([6, 10, 14], [46, 47, 46], color=BORDER_COL, linewidth=0.5, alpha=0.4, zorder=2)\n\n# Subtle coordinate grid\nax.grid(True, alpha=0.10, linestyle=\"--\", color=INK, linewidth=0.5, zorder=3)\n\n# Individual data points (semi-transparent density backdrop)\nfor cat in cat_names:\n    mask = categories == cat\n    ax.scatter(lons[mask], lats[mask], c=cat_colors[cat], alpha=0.20, s=12, edgecolors=\"none\", zorder=4)\n\n# Cluster markers — logarithmic size scaling prevents over-large small-count markers\nfor center, size, cat in zip(cluster_centers, cluster_sizes, cluster_dominant_cat, strict=True):\n    lat, lon = center\n    marker_size = 150 + np.log1p(size) * 80\n    ax.scatter(lon, lat, s=marker_size, c=cat_colors[cat], alpha=0.88, edgecolors=PAGE_BG, linewidths=1.5, zorder=5)\n    ax.annotate(str(size), (lon, lat), fontsize=7, fontweight=\"bold\", ha=\"center\", va=\"center\", color=\"white\", zorder=6)\n\n# Data storytelling: annotate the London cluster (highest-count city) as the key insight\nlondon_ref_lat, london_ref_lon = 51.5074, -0.1278\ncity_dist = np.sqrt((cluster_centers[:, 0] - london_ref_lat) ** 2 + (cluster_centers[:, 1] - london_ref_lon) ** 2)\nlargest_idx = np.argmin(city_dist)\nlc_lon = float(cluster_centers[largest_idx][1])\nlc_lat = float(cluster_centers[largest_idx][0])\nax.annotate(\n    f\"London hub · {cluster_sizes[largest_idx]} stores\",\n    xy=(lc_lon, lc_lat),\n    xytext=(lc_lon + 6, lc_lat + 0.5),\n    fontsize=6,\n    color=INK,\n    ha=\"left\",\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK_MUTED, \"lw\": 0.8, \"shrinkB\": 10},\n    bbox={\"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.85, \"boxstyle\": \"round,pad=0.3\"},\n    zorder=7,\n)\n\n# Dashed zoom-box around London area — signals the inset region\nlondon_zoom_rect = Rectangle(\n    (-2.5, 49.8), 4.7, 3.7, fill=False, edgecolor=INK_SOFT, linewidth=0.7, linestyle=\"dashed\", alpha=0.65, zorder=6\n)\nax.add_patch(london_zoom_rect)\n\n# North arrow using FancyArrowPatch (matplotlib.patches cartographic convention)\nnorth_arrow = FancyArrowPatch(\n    (20.5, 37.5), (20.5, 39.2), arrowstyle=\"->\", color=INK_SOFT, mutation_scale=8, linewidth=1.2, zorder=7\n)\nax.add_patch(north_arrow)\nax.text(20.5, 39.6, \"N\", fontsize=7, fontweight=\"bold\", color=INK_SOFT, ha=\"center\", va=\"bottom\", zorder=7)\n\n# Legend\nlegend_handles = [\n    ax.scatter([], [], c=cat_colors[cat], s=60, label=cat, edgecolors=PAGE_BG, linewidths=1) for cat in cat_names\n]\nleg = ax.legend(\n    handles=legend_handles,\n    loc=\"upper left\",\n    fontsize=7,\n    framealpha=0.95,\n    title=\"Store Type\",\n    title_fontsize=8,\n    borderpad=0.6,\n    labelspacing=0.4,\n)\nif leg:\n    leg.get_frame().set_facecolor(ELEVATED_BG)\n    leg.get_frame().set_edgecolor(INK_SOFT)\n    plt.setp(leg.get_texts(), color=INK_SOFT)\n    leg.get_title().set_color(INK)\n\n# Geographic reference labels\nax.annotate(\"Atlantic\\nOcean\", (-9, 46), fontsize=7, style=\"italic\", color=INK_MUTED, ha=\"center\", alpha=0.8, zorder=4)\nax.annotate(\n    \"Mediterranean Sea\", (5, 37.5), fontsize=7, style=\"italic\", color=INK_MUTED, ha=\"center\", alpha=0.8, zorder=4\n)\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 · matplotlib · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\n\nax.set_xlim(-12, 22)\nax.set_ylim(36, 56)\n\n# Inset axes: expanded view of London — demonstrates the spec's \"zoom\" dual-state concept\nlondon_mask = (lats > 49.8) & (lats < 53.5) & (lons > -2.5) & (lons < 2.2)\nax_inset = ax.inset_axes([0.70, 0.58, 0.28, 0.35])\nax_inset.set_facecolor(OCEAN_BG)\nax_inset.fill(gb_lon, gb_lat, color=LAND_BG, alpha=0.9, zorder=1)\nfor cat in cat_names:\n    m = london_mask & (categories == cat)\n    if np.any(m):\n        ax_inset.scatter(\n            lons[m], lats[m], c=cat_colors[cat], s=16, alpha=0.80, edgecolors=\"white\", linewidths=0.4, zorder=3\n        )\nax_inset.set_xlim(-2.5, 2.2)\nax_inset.set_ylim(49.8, 53.5)\nax_inset.set_title(\"London → expanded\", fontsize=5.5, color=INK, pad=2)\nax_inset.tick_params(axis=\"both\", labelsize=4.5, colors=INK_MUTED)\nfor s in (\"top\", \"right\"):\n    ax_inset.spines[s].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax_inset.spines[s].set_color(INK_SOFT)\n# District labels to represent individual marker context\nfor dname, dlon, dlat in [(\"Central\", -0.12, 51.50), (\"East End\", 0.85, 51.52), (\"North\", -0.10, 52.30)]:\n    ax_inset.text(dlon, dlat, dname, fontsize=5, color=INK_MUTED, ha=\"center\", va=\"bottom\", alpha=0.9, zorder=5)\n\n# Summary annotation\nax.text(\n    0.98,\n    0.02,\n    f\"Total: {len(lats)} locations · {len(cluster_sizes)} 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\nfig.subplots_adjust(left=0.09, right=0.98, top=0.93, bottom=0.11)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}