cities = FileAttachment("data/leap-cities-index-2026.csv").csv({typed: true})
india = FileAttachment("data/india-states-lite.json").json()
cityPins = FileAttachment("data/cities-coords.json").json()
// Map CSV state names onto the GeoJSON's NAME_1 spellings (official-boundary map)
norm = ({
"Jammu & Kashmir": "Jammu and Kashmir",
"Puducherry (UT)": "Puducherry",
"Dadra and Nagar Haveli (UT)": "Dadra and Nagar Haveli and Daman and Diu"
})
byState = d3.rollup(
cities,
v => ({ mean: d3.mean(v, d => d["Composite index"]), n: v.length }),
d => norm[d.State] ?? d.State
)
indiaMap = {
const W = Math.min(680, width)
const H = 720
const color = d3.scaleLinear()
.domain([32, 50, 68])
.range(["#EAD2C0", "#C06B41", "#7E3819"])
.clamp(true)
const projection = d3.geoMercator().fitSize([W, H - 40], india)
const path = d3.geoPath(projection)
const svg = d3.create("svg")
.attr("viewBox", [0, 0, W, H])
.attr("width", W)
.style("max-width", "100%")
.style("height", "auto")
.style("font-family", "Inter, system-ui, sans-serif")
const tooltip = d3.select("body").append("div")
.style("position", "absolute").style("background", "rgba(42,37,32,0.92)")
.style("color", "#fff").style("padding", "6px 10px").style("border-radius", "6px")
.style("font-size", "12px").style("line-height", "1.4").style("pointer-events", "none")
.style("opacity", 0).style("z-index", 1000)
svg.append("g")
.selectAll("path")
.data(india.features)
.join("path")
.attr("d", path)
.attr("fill", d => { const r = byState.get(d.properties.NAME_1); return r ? color(r.mean) : "#E3DDD2" })
.attr("stroke", "#FBF7F0").attr("stroke-width", 0.6)
.on("mousemove", (event, d) => {
const r = byState.get(d.properties.NAME_1)
tooltip.style("opacity", 1)
.html(r
? `<strong>${d.properties.NAME_1}</strong><br>Avg composite index: ${r.mean.toFixed(1)}<br>${r.n} ${r.n === 1 ? "city" : "cities"} in index`
: `<strong>${d.properties.NAME_1}</strong><br>no cities in index`)
.style("left", (event.pageX + 12) + "px")
.style("top", (event.pageY - 20) + "px")
d3.select(event.currentTarget).attr("stroke", "#2A2520").attr("stroke-width", 1.3)
})
.on("mouseout", (event) => {
tooltip.style("opacity", 0)
d3.select(event.currentTarget).attr("stroke", "#FBF7F0").attr("stroke-width", 0.6)
})
// City pins
svg.append("g")
.selectAll("circle")
.data(cityPins.filter(d => projection([d.lng, d.lat])))
.join("circle")
.attr("cx", d => projection([d.lng, d.lat])[0])
.attr("cy", d => projection([d.lng, d.lat])[1])
.attr("r", 3.4)
.attr("fill", "#241F1B")
.attr("stroke", "#FBF7F0")
.attr("stroke-width", 0.9)
.attr("opacity", 0.95)
.style("cursor", "pointer")
.on("mousemove", (event, d) => {
event.stopPropagation()
tooltip.style("opacity", 1)
.html(`<strong>${d.City}</strong>, ${d.State}<br>Composite index: ${d.composite.toFixed(1)}<br>Rank ${d.rank} of 233 · Tier ${d.tier}`)
.style("left", (event.pageX + 12) + "px")
.style("top", (event.pageY - 20) + "px")
d3.select(event.currentTarget).attr("r", 5).attr("fill", "#B4552E").attr("opacity", 1)
})
.on("mouseout", (event) => {
tooltip.style("opacity", 0)
d3.select(event.currentTarget).attr("r", 2.6).attr("fill", "#2A2520").attr("opacity", 0.85)
})
// Legend
const lg = svg.append("g").attr("transform", `translate(12, ${H - 30})`)
const defs = svg.append("defs")
const grad = defs.append("linearGradient").attr("id", "leapgrad")
grad.selectAll("stop").data([[0, "#EAD2C0"], [0.5, "#C06B41"], [1, "#7E3819"]]).join("stop")
.attr("offset", d => `${d[0] * 100}%`).attr("stop-color", d => d[1])
lg.append("rect").attr("width", 160).attr("height", 10).attr("rx", 2).attr("fill", "url(#leapgrad)")
lg.append("text").attr("y", 24).attr("font-size", 11).attr("fill", "#6B615A").text("32")
lg.append("text").attr("x", 160).attr("y", 24).attr("text-anchor", "end").attr("font-size", 11).attr("fill", "#6B615A").text("68")
lg.append("text").attr("y", -6).attr("font-size", 11).attr("fill", "#2A2520").text("Avg. composite index")
return svg.node()
}The LEAP Cities Index 2026 is a comparable ranking of 233 Tier-2 and Tier-3 Indian cities outside the eight largest metros (94 Tier-2 and 139 Tier-3, across 23 states and union territories). India’s national accounts do not estimate output below the state level, so there is no official measure of the size or sectoral composition of a city’s economy — the index fills that gap. It is built from 43 variables from official and administrative sources, organised into eight pillars, each mapped to a sector of the Gross Value Added (GVA) classification, and combined into a single composite score (0–100).
State shading = average composite index across the cities scored in that state (2026). Grey = no cities in the index. Hover for details. Boundaries: udit-001/india-maps-data (official Indian boundaries, incl. Ladakh).
Citation: Patnaik, M. (2026). LEAP Cities India 2026 [Data set]. Zenodo. https://doi.org/10.5281/zenodo.21744737