{"spec_id":"map-marker-clustered","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nmap-marker-clustered: Clustered Marker Map\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path so \"import bokeh\" finds the\n# installed package instead of this file (also named bokeh.py).\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir]\n\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, LabelSet, Legend, LegendItem, WMTSTileSource\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\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# Map background — placeholder color visible before tiles load\nMAP_BG = \"#E8EEF2\" if THEME == \"light\" else \"#1C2E35\"\n\n# Tile basemap — CartoDB Positron (light) or Dark Matter (dark)\nTILE_URL = (\n    \"https://a.basemaps.cartocdn.com/light_all/{Z}/{X}/{Y}.png\"\n    if THEME == \"light\"\n    else \"https://a.basemaps.cartocdn.com/dark_all/{Z}/{X}/{Y}.png\"\n)\n\n# Imprint palette — canonical imprint slot order (green, lavender, blue, ochre)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data — coffee shop chain locations across NYC neighborhoods\nnp.random.seed(42)\n\nneighborhoods = [\n    {\"name\": \"Downtown\", \"lat\": 40.758, \"lon\": -73.985, \"stores\": 45},\n    {\"name\": \"Midtown\", \"lat\": 40.755, \"lon\": -73.975, \"stores\": 35},\n    {\"name\": \"Upper East\", \"lat\": 40.773, \"lon\": -73.965, \"stores\": 28},\n    {\"name\": \"Upper West\", \"lat\": 40.785, \"lon\": -73.976, \"stores\": 22},\n    {\"name\": \"Chelsea\", \"lat\": 40.742, \"lon\": -74.000, \"stores\": 18},\n    {\"name\": \"SoHo\", \"lat\": 40.723, \"lon\": -73.998, \"stores\": 25},\n    {\"name\": \"Financial\", \"lat\": 40.707, \"lon\": -74.011, \"stores\": 32},\n    {\"name\": \"Brooklyn Heights\", \"lat\": 40.696, \"lon\": -73.993, \"stores\": 20},\n    {\"name\": \"Williamsburg\", \"lat\": 40.714, \"lon\": -73.961, \"stores\": 30},\n    {\"name\": \"DUMBO\", \"lat\": 40.703, \"lon\": -73.988, \"stores\": 15},\n]\n\ncategories = [\"Coffee\", \"Express\", \"Roastery\", \"Reserve\"]\ncategory_weights = [0.5, 0.3, 0.15, 0.05]\ncategory_colors = dict(zip(categories, IMPRINT, strict=False))\n\nall_lats, all_lons, all_categories = [], [], []\n\nfor hood in neighborhoods:\n    n = hood[\"stores\"]\n    all_lats.extend(hood[\"lat\"] + np.random.normal(0, 0.008, n))\n    all_lons.extend(hood[\"lon\"] + np.random.normal(0, 0.008, n))\n    all_categories.extend(np.random.choice(categories, n, p=category_weights))\n\nlats = np.array(all_lats)\nlons = np.array(all_lons)\nstore_categories = np.array(all_categories)\n\n# Convert lat/lon → Web Mercator (inline)\nk = 6378137\nmercator_x = lons * (k * np.pi / 180.0)\nmercator_y = np.log(np.tan((90 + lats) * np.pi / 360.0)) * k\n\n# Grid-based clustering — 3500m cells reduce overlap in dense areas\ngrid_size = 3500  # metres\ngrid_xi = np.floor(mercator_x / grid_size).astype(int)\ngrid_yi = np.floor(mercator_y / grid_size).astype(int)\ncluster_ids = grid_xi * 10000 + grid_yi\nunique_clusters = np.unique(cluster_ids)\n\ncluster_cx, cluster_cy, cluster_counts, cluster_dominant = [], [], [], []\nfor cid in unique_clusters:\n    mask = cluster_ids == cid\n    cluster_cx.append(mercator_x[mask].mean())\n    cluster_cy.append(mercator_y[mask].mean())\n    cluster_counts.append(mask.sum())\n    cats = store_categories[mask]\n    unique_c, counts_c = np.unique(cats, return_counts=True)\n    cluster_dominant.append(unique_c[counts_c.argmax()])\n\ncluster_cx = np.array(cluster_cx)\ncluster_cy = np.array(cluster_cy)\ncluster_counts = np.array(cluster_counts)\n\n# Scale marker size by cluster count\nmin_sz, max_sz = 45, 110\nnorm = (cluster_counts - cluster_counts.min() + 1) / (cluster_counts.max() - cluster_counts.min() + 1)\ncluster_sizes = min_sz + np.sqrt(norm) * (max_sz - min_sz)\n\n# Build figure — landscape 3200×1800, no toolbar for PNG accuracy\np = figure(\n    width=3200,\n    height=1800,\n    x_range=(-8242000, -8208000),\n    y_range=(4958000, 4998000),\n    x_axis_type=\"mercator\",\n    y_axis_type=\"mercator\",\n    title=\"map-marker-clustered · python · bokeh · anyplot.ai\",\n    toolbar_location=None,\n    tooltips=[(\"Stores\", \"@count\"), (\"Dominant Type\", \"@category\")],\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Tile basemap — adds streets/boundaries geographic context\np.add_tile(WMTSTileSource(url=TILE_URL, attribution=\"© OpenStreetMap contributors © CARTO\"))\n\n# Faint individual store markers\nindividual_source = ColumnDataSource(data={\"x\": mercator_x, \"y\": mercator_y, \"category\": store_categories})\np.scatter(\n    x=\"x\", y=\"y\", source=individual_source, size=8, fill_color=IMPRINT[0], fill_alpha=0.20, line_color=None\n)\n\n# Per-category cluster renderers for legend (all 4 categories always present)\nlegend_items = []\nfor cat, color in category_colors.items():\n    cat_mask = np.array([d == cat for d in cluster_dominant])\n    if cat_mask.any():\n        src = ColumnDataSource(\n            data={\n                \"x\": cluster_cx[cat_mask],\n                \"y\": cluster_cy[cat_mask],\n                \"size\": cluster_sizes[cat_mask],\n                \"count\": cluster_counts[cat_mask],\n                \"count_label\": [str(c) for c in cluster_counts[cat_mask]],\n                \"category\": [cat] * int(cat_mask.sum()),\n            }\n        )\n        renderer = p.scatter(\n            x=\"x\", y=\"y\", source=src, size=\"size\", fill_color=color, fill_alpha=0.88, line_color=\"white\", line_width=2.5\n        )\n    else:\n        # Off-screen phantom point so the legend glyph renders with the right color\n        src = ColumnDataSource(\n            data={\"x\": [-9.9e8], \"y\": [-9.9e8], \"size\": [80.0], \"count\": [0], \"count_label\": [\"\"], \"category\": [cat]}\n        )\n        renderer = p.scatter(\n            x=\"x\", y=\"y\", source=src, size=\"size\", fill_color=color, fill_alpha=0.88, line_color=\"white\", line_width=2.5\n        )\n    legend_items.append(LegendItem(label=cat, renderers=[renderer]))\n\n# Cluster count labels\ncluster_source = ColumnDataSource(\n    data={\n        \"x\": cluster_cx,\n        \"y\": cluster_cy,\n        \"count\": cluster_counts,\n        \"size\": cluster_sizes,\n        \"count_label\": [str(c) for c in cluster_counts],\n        \"category\": cluster_dominant,\n    }\n)\np.add_layout(\n    LabelSet(\n        x=\"x\",\n        y=\"y\",\n        text=\"count_label\",\n        source=cluster_source,\n        text_font_size=\"24pt\",\n        text_font_style=\"bold\",\n        text_color=\"white\",\n        text_align=\"center\",\n        text_baseline=\"middle\",\n    )\n)\n\n# Neighborhood name labels\nhood_labels_source = ColumnDataSource(\n    data={\n        \"x\": [hood[\"lon\"] * (k * np.pi / 180.0) for hood in neighborhoods],\n        \"y\": [np.log(np.tan((90 + hood[\"lat\"]) * np.pi / 360.0)) * k - 1200 for hood in neighborhoods],\n        \"name\": [hood[\"name\"] for hood in neighborhoods],\n    }\n)\np.add_layout(\n    LabelSet(\n        x=\"x\",\n        y=\"y\",\n        text=\"name\",\n        source=hood_labels_source,\n        text_font_size=\"20pt\",\n        text_color=INK_MUTED,\n        text_align=\"center\",\n        text_baseline=\"top\",\n    )\n)\n\n# Legend\nlegend = Legend(\n    items=legend_items,\n    location=\"top_left\",\n    title=\"Store Type\",\n    title_text_font_size=\"34pt\",\n    title_text_font_style=\"bold\",\n    title_text_color=INK,\n    label_text_font_size=\"34pt\",\n    label_text_color=INK_SOFT,\n    glyph_height=48,\n    glyph_width=48,\n    spacing=10,\n    padding=18,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.92,\n    border_line_color=INK_SOFT,\n    border_line_width=1,\n)\np.add_layout(legend, \"right\")\n\n# Typography\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\n\np.xaxis.axis_label = \"Longitude\"\np.yaxis.axis_label = \"Latitude\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.08\np.ygrid.grid_line_alpha = 0.08\n\n# Theme-adaptive background (tile placeholder + border area)\np.background_fill_color = MAP_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\np.outline_line_width = 1\n\n# Save HTML (interactive artifact)\nhtml_path = f\"plot-{THEME}.html\"\noutput_file(html_path, title=\"map-marker-clustered · python · bokeh · anyplot.ai\")\nsave(p)\n\n# Screenshot via headless Chrome (Selenium 4 / Selenium Manager)\n# Bokeh 3.9.0 adds a Notifications bar (~139 px) above the figure in the HTML\n# page even when toolbar_location=None. Use element screenshot to capture just\n# the figure div at its declared 3200×1800 size.\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H + 200}\",  # extra vertical space so figure isn't clipped\n    \"--hide-scrollbars\",\n    \"--force-device-scale-factor=1\",\n):\n    opts.add_argument(arg)\n\nfrom selenium.webdriver.common.by import By\n\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H + 200)\ndriver.get(f\"file://{Path(html_path).resolve()}\")\ntime.sleep(5)  # extended wait for tile layer to load\n\n# Try to screenshot just the Bokeh figure element to avoid the notifications bar\ntry:\n    fig_elem = driver.find_element(By.CSS_SELECTOR, \".bk-Figure\")\n    fig_elem.screenshot(f\"plot-{THEME}.png\")\nexcept Exception:\n    driver.save_screenshot(f\"plot-{THEME}.png\")\n\ndriver.quit()\n"}