From 70419472464049c4c70b4e02157e0a5795a595b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 10:40:55 +0000 Subject: [PATCH 1/4] Add density heatmap view for darkstore locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Markers / Heatmap toggle to the sidebar panel. In heatmap mode, leaflet.heat renders a kernel-density heatmap over all enabled brands (Zepto, Blinkit, Swiggy) using a purple→pink→amber→white gradient. Brand toggles continue to work in both modes — switching a brand off removes its points from the heatmap as well. - index.html: load leaflet.heat@0.2.0 from unpkg CDN - view-combined.js: buildHeatmapPoints(), updateHeatmap(), view-mode toggle buttons wired to _showHeatmap state, unmount cleanup - style.css: .view-mode-row / .view-mode-btn / .view-mode-btn--active https://claude.ai/code/session_01WpjxgB9aGfxvatcKrYHL5J --- public/index.html | 1 + public/style.css | 42 +++++++++++++++++++++++ public/view-combined.js | 75 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/public/index.html b/public/index.html index 80b2216..082a232 100644 --- a/public/index.html +++ b/public/index.html @@ -170,6 +170,7 @@

Data sources & m + diff --git a/public/style.css b/public/style.css index 503a4c8..e117c80 100644 --- a/public/style.css +++ b/public/style.css @@ -1625,4 +1625,46 @@ button.sidebar-footer-link:focus-visible { /* Brighten the dark map up */ [data-theme="dark"] .leaflet-tile-pane { filter: brightness(1.5) contrast(1.1); +} + +/* ── VIEW MODE TOGGLE (Markers / Heatmap) ── */ +.view-mode-row { + display: flex; + gap: 6px; + padding: 14px 14px 4px; +} + +.view-mode-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 7px 10px; + border-radius: 8px; + border: 1px solid var(--border-hi); + background: var(--surface2); + color: var(--muted2); + font-family: 'DM Sans', sans-serif; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.18s, color 0.18s, border-color 0.18s, box-shadow 0.18s; +} + +.view-mode-btn:hover { + background: var(--surface3); + color: var(--text); +} + +.view-mode-btn--active { + background: var(--z); + border-color: var(--z); + color: #ffffff; + box-shadow: 0 0 14px var(--z-glow); +} + +.view-mode-btn--active:hover { + background: var(--z-l); + border-color: var(--z-l); } \ No newline at end of file diff --git a/public/view-combined.js b/public/view-combined.js index fe2f4f5..01748b5 100644 --- a/public/view-combined.js +++ b/public/view-combined.js @@ -21,6 +21,8 @@ window.CombinedView = (function () { let _showZeptoGeofence = false; let _removeBlinkitImprecise = false; let _onZoomEnd = null; + let _heatLayer = null; + let _showHeatmap = false; const SWIGGY_ORANGE = '#FC8019'; const SWIGGY_ORANGE_L = '#FF9F43'; @@ -266,6 +268,38 @@ window.CombinedView = (function () { return lg; } + /* ── BUILD HEATMAP POINTS ── */ + function buildHeatmapPoints() { + const pts = []; + if (_showZ) { + _zData.forEach(s => { if (s.lat && s.lng) pts.push([s.lat, s.lng, 1]); }); + } + if (_showB) { + const list = _removeBlinkitImprecise ? _bData.filter(s => s.accuracy <= 100) : _bData; + list.forEach(s => { if (s.coordinates) pts.push([s.coordinates[0], s.coordinates[1], 1]); }); + } + if (_showS) { + (_sData || []).forEach(s => { + if (s.coordinates && s.coordinates.length >= 2) pts.push([s.coordinates[0], s.coordinates[1], 1]); + }); + } + return pts; + } + + function updateHeatmap() { + if (_heatLayer) { _map.removeLayer(_heatLayer); _heatLayer = null; } + if (!_showHeatmap) return; + const pts = buildHeatmapPoints(); + _heatLayer = L.heatLayer(pts, { + radius: 28, + blur: 18, + maxZoom: 17, + max: 1.0, + gradient: { 0.15: '#1e0a3c', 0.35: '#7c3aed', 0.55: '#db2777', 0.75: '#f59e0b', 1.0: '#ffffff' }, + }); + _heatLayer.addTo(_map); + } + function updateLayers() { if (_zLayer) { _map.removeLayer(_zLayer); _zLayer = null; } if (_zoneGroup) { _map.removeLayer(_zoneGroup); _zoneGroup = null; } @@ -274,6 +308,12 @@ window.CombinedView = (function () { if (_bCluster) { _map.removeLayer(_bCluster); _bCluster = null; } if (_sLayer) { _map.removeLayer(_sLayer); _sLayer = null; } + if (_showHeatmap) { + updateHeatmap(); + return; + } + updateHeatmap(); // clears stale heat layer when switching back to markers + if (_showZ) { _zLayer = L.markerClusterGroup({ maxClusterRadius: 55, @@ -374,6 +414,17 @@ window.CombinedView = (function () { const panel = document.getElementById('panel'); panel.innerHTML = ` +
+ + +
+
@@ -461,6 +512,28 @@ window.CombinedView = (function () { } syncDisabled(); + function syncViewModeButtons() { + const mBtn = document.getElementById('viewMarkers'); + const hBtn = document.getElementById('viewHeatmap'); + if (mBtn) mBtn.classList.toggle('view-mode-btn--active', !_showHeatmap); + if (hBtn) hBtn.classList.toggle('view-mode-btn--active', _showHeatmap); + } + + document.getElementById('viewMarkers').addEventListener('click', () => { + if (_showHeatmap) { + _showHeatmap = false; + syncViewModeButtons(); + updateLayers(); + } + }); + document.getElementById('viewHeatmap').addEventListener('click', () => { + if (!_showHeatmap) { + _showHeatmap = true; + syncViewModeButtons(); + updateLayers(); + } + }); + document.getElementById('enableZepto').addEventListener('change', e => { _showZ = e.target.checked; const gf = document.getElementById('zeptoGeofence'); @@ -522,6 +595,7 @@ window.CombinedView = (function () { _showZ = true; _showB = true; _showS = true; + _showHeatmap = false; // Defaults (requested): geofence on, imprecise markers removed. _showZeptoGeofence = true; _removeBlinkitImprecise = true; @@ -541,6 +615,7 @@ window.CombinedView = (function () { if (_previewZoneGroup) { _map.removeLayer(_previewZoneGroup); _previewZoneGroup = null; } if (_bCluster) { _map.removeLayer(_bCluster); _bCluster = null; } if (_sLayer) { _map.removeLayer(_sLayer); _sLayer = null; } + if (_heatLayer) { _map.removeLayer(_heatLayer); _heatLayer = null; } if (_onZoomEnd) { _map.off('zoomend', _onZoomEnd); _onZoomEnd = null; } }, From 30143225944acb215ab448b9a059e883f3d07121 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 10:47:24 +0000 Subject: [PATCH 2/4] Boost heatmap visibility and update README - Lower max to 0.08 so each point contributes much more intensity - Raise minOpacity to 0.45 so low-density areas still show colour - Increase radius/blur slightly for smoother blobs - Gradient now starts from full opacity at 0.0 (no transparent floor) - README: full description, feature list, data table, stack, deploy guide https://claude.ai/code/session_01WpjxgB9aGfxvatcKrYHL5J --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++- public/view-combined.js | 9 ++++---- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9dd6a96..d68cbc9 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# Dark Store Mappings +# darkstores.intel — Quick Commerce Intelligence + +An interactive map of dark store locations across India for Zepto, Blinkit, and Swiggy (Instamart). + +**Live:** [darkstores-heatmap.vercel.app](https://darkstores-heatmap.vercel.app) + +![All India Dark Stores](public/all-india-dark-stores.png) + +## Features + +- **14,000+ store locations** across Zepto, Blinkit, and Swiggy Instamart +- **Heatmap view** — density visualization showing quick-commerce coverage intensity across India +- **Marker view** — clustered pins with per-store popups and Google Maps links +- **Zepto delivery zones** — geofence polygons shown at high zoom +- **Blinkit accuracy filter** — toggle imprecise (trilaterated) markers +- **CSV export** — download raw coordinates for each brand +- **Dark / light theme** — persisted to localStorage + +## Data + +| Brand | Stores | Scraped | +|---|---|---| +| Zepto | ~9,000 | 14–15 March 2026 | +| Blinkit | ~2,400 | 15–17 March 2026 | +| Swiggy Instamart | ~3,000 | 18–19 March 2026 | + +Locations sourced from public-facing APIs. Blinkit coordinates are trilaterated (0–50m accuracy for most stores). All figures are a lower bound. + +Read the [technical write-up on Medium](https://jatin-dot-py.medium.com/how-i-scraped-most-dark-stores-in-india-blinkit-zepto-swiggy-instamart-ad939ff17af9). + +## Stack + +- Vanilla JS + [Leaflet.js](https://leafletjs.com) + [leaflet.heat](https://github.com/Leaflet/Leaflet.heat) + [MarkerCluster](https://github.com/Leaflet/Leaflet.markercluster) +- CARTO map tiles (dark + light) +- Static files — no build step, no server + +## Run locally + +```bash +cd public +python3 -m http.server 3000 +# open http://localhost:3000 +``` + +## Deploy + +Any static host works (Vercel, Netlify, Cloudflare Pages, GitHub Pages). Set the root/publish directory to `public/`. diff --git a/public/view-combined.js b/public/view-combined.js index 01748b5..8cb4839 100644 --- a/public/view-combined.js +++ b/public/view-combined.js @@ -291,11 +291,12 @@ window.CombinedView = (function () { if (!_showHeatmap) return; const pts = buildHeatmapPoints(); _heatLayer = L.heatLayer(pts, { - radius: 28, - blur: 18, + radius: 35, + blur: 25, maxZoom: 17, - max: 1.0, - gradient: { 0.15: '#1e0a3c', 0.35: '#7c3aed', 0.55: '#db2777', 0.75: '#f59e0b', 1.0: '#ffffff' }, + max: 0.08, + minOpacity: 0.45, + gradient: { 0.0: '#2e0068', 0.25: '#7c3aed', 0.5: '#db2777', 0.75: '#f59e0b', 1.0: '#ffffff' }, }); _heatLayer.addTo(_map); } From 05619993083f19619d01ba9ced42aaca9bc0a940 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 10:52:14 +0000 Subject: [PATCH 3/4] Switch heatmap to red-hot / blue-cold gradient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blue (#0000ff) = 0 density → cyan → green → yellow → orange → red (#ff0000) = peak density https://claude.ai/code/session_01WpjxgB9aGfxvatcKrYHL5J --- public/view-combined.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/view-combined.js b/public/view-combined.js index 8cb4839..7f282a4 100644 --- a/public/view-combined.js +++ b/public/view-combined.js @@ -296,7 +296,7 @@ window.CombinedView = (function () { maxZoom: 17, max: 0.08, minOpacity: 0.45, - gradient: { 0.0: '#2e0068', 0.25: '#7c3aed', 0.5: '#db2777', 0.75: '#f59e0b', 1.0: '#ffffff' }, + gradient: { 0.0: '#0000ff', 0.25: '#00bfff', 0.5: '#00ff88', 0.7: '#ffdd00', 0.85: '#ff6600', 1.0: '#ff0000' }, }); _heatLayer.addTo(_map); } From 6807c79cebeb8e32823707ac887b200ae0d0e383 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 14:20:20 +0000 Subject: [PATCH 4/4] =?UTF-8?q?Update=20heatmap=20gradient:=20sky=20blue?= =?UTF-8?q?=20=E2=86=92=20green=20=E2=86=92=20yellow=20=E2=86=92=20orange?= =?UTF-8?q?=20=E2=86=92=20red?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.0 #87ceeb sky blue (0 stores) 0.2 #00c853 green 0.45 #aeea00 lime 0.65 #ffd600 yellow 0.82 #ff6d00 orange 1.0 #dd2c00 deep red (peak density) No purple anywhere in the scale. https://claude.ai/code/session_01WpjxgB9aGfxvatcKrYHL5J --- public/view-combined.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/view-combined.js b/public/view-combined.js index 7f282a4..a4c1819 100644 --- a/public/view-combined.js +++ b/public/view-combined.js @@ -296,7 +296,7 @@ window.CombinedView = (function () { maxZoom: 17, max: 0.08, minOpacity: 0.45, - gradient: { 0.0: '#0000ff', 0.25: '#00bfff', 0.5: '#00ff88', 0.7: '#ffdd00', 0.85: '#ff6600', 1.0: '#ff0000' }, + gradient: { 0.0: '#87ceeb', 0.2: '#00c853', 0.45: '#aeea00', 0.65: '#ffd600', 0.82: '#ff6d00', 1.0: '#dd2c00' }, }); _heatLayer.addTo(_map); }