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/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..a4c1819 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,39 @@ 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: 35, + blur: 25, + maxZoom: 17, + max: 0.08, + minOpacity: 0.45, + gradient: { 0.0: '#87ceeb', 0.2: '#00c853', 0.45: '#aeea00', 0.65: '#ffd600', 0.82: '#ff6d00', 1.0: '#dd2c00' }, + }); + _heatLayer.addTo(_map); + } + function updateLayers() { if (_zLayer) { _map.removeLayer(_zLayer); _zLayer = null; } if (_zoneGroup) { _map.removeLayer(_zoneGroup); _zoneGroup = null; } @@ -274,6 +309,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 +415,17 @@ window.CombinedView = (function () { const panel = document.getElementById('panel'); panel.innerHTML = ` +
+ + +
+
@@ -461,6 +513,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 +596,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 +616,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; } },