Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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/`.
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ <h2 id="dataDisclaimerTitle" class="data-disclaimer__title">Data sources &amp; m

<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
<script src="view-combined.js"></script>
<script src="app.js"></script>
</body>
Expand Down
42 changes: 42 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
76 changes: 76 additions & 0 deletions public/view-combined.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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; }
Expand All @@ -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,
Expand Down Expand Up @@ -374,6 +415,17 @@ window.CombinedView = (function () {
const panel = document.getElementById('panel');
panel.innerHTML = `

<div class="view-mode-row">
<button type="button" class="view-mode-btn${!_showHeatmap ? ' view-mode-btn--active' : ''}" id="viewMarkers">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
Markers
</button>
<button type="button" class="view-mode-btn${_showHeatmap ? ' view-mode-btn--active' : ''}" id="viewHeatmap">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M8 12c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4"/><circle cx="12" cy="12" r="2"/></svg>
Heatmap
</button>
</div>

<div class="brand-card brand-card--zepto" id="brandZepto">
<div class="brand-card-top">
<div class="brand-card-identity">
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand All @@ -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; }
},

Expand Down