Public
Edited
Apr 25, 2023
1 fork
Importers
6 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
bengalaru_parks = BubbleMap(parksInBangalore, {
// Required
features: bangalore_osm,
value: bangalore_osm_park_areas,// (d) => d.area, // ⚠️⚠️⚠️ Is it possible to make it so that value isn't "required", but instead auto-calculated as part of the implementation?
position: bangalore_osm_park_locations, // (d) => [d.lon, d.lat], // ⚠️⚠️⚠️ Is it possible to make it so that position isn't "required", but instead auto-calculated as part of the implementation?
title: (d) => d.name,

showTitle: false
})
Insert cell
Insert cell
Insert cell
osm_kathmandu_park_playground = FileAttachment("osm_kathmandu_park_playground.geojson").json()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
parksMap = BubbleMap(parksInBangalore, {
// Required
features: bangalore_osm,
value: bangalore_osm_park_areas,// (d) => d.area, // ⚠️⚠️⚠️ Is it possible to make it so that value isn't "required", but instead auto-calculated as part of the implementation?
position: bangalore_osm_park_locations, // (d) => [d.lon, d.lat], // ⚠️⚠️⚠️ Is it possible to make it so that position isn't "required", but instead auto-calculated as part of the implementation?
title: (d) => d.name,
// Optional
borders: showWards ? bangalore_osm : null,
avoidOverlapping,
width: 960,
height: 720,

showTitle: false
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
BubbleMap(population_srilanka, {
features: map_srilanka, // tanahun, // looks like it works just the same without needing the single tanahun layer
borders: map_srilanka, // might be nice to add some convenience helpers for styling color, etc
position: (d) => {
const feat = map_srilanka.features.find(
(f) => d.name.startsWith(f.properties.tags["name:en"]) // This is specific to our dataset; the visualization will break if this property doesn't exist in your dataset.
);
return feat && centroid(feat);
},
value: (d) => +d["Population Estimate 2021-07-01"],
title: (d) =>
`${d.name.replace(" District", "")}\n${numberWithCommas(
d["Population Estimate 2021-07-01"]
)}`,

showTitle: true,
maxRadius: 25,
// avoidOverlapping: true
})
Insert cell
Insert cell
map_srilanka = FileAttachment("sri-lankan-districts.geo.json.geojson").json()
Insert cell
function numberWithCommas(x) {
// x = x.toString();
// var pattern = /(-?\d+)(\d{3})/;
// while (pattern.test(x)) x = x.replace(pattern, "$1,$2");
// return x;

return d3.format(",.0s")(x) // replace 's' with 'r' for comma separated numbers like 120,000
}
Insert cell
tanahun_municipalities = await FileAttachment("tanahun_muncipalities@1.geojson").json()
Insert cell
population = [
{name: "व्यास न.पा.", population: 70335},
{name: "शुक्लागण्डकी न.पा.", population: 48456},
{name: "भानु न.पा.", population: 45792},
{name: "भिमाद न.पा.", population: 31362},
{name: "आँबु खैरेनी गा.पा.", population: 20768},
{name: "ऋषिङ्ग गा.पा.", population: 25870},
{name: "म्याग्दे गा.पा.", population: 22502},
{name: "बन्दीपुर गा.पा.", population: 20013},
{name: "घिरिङ्ग गा.पा.", population: 19318},
{name: "देवघाट गा.पा.", population: 16131},
]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
BubbleMap(population_srilanka, {
features: map_srilanka, // why does this break here but not in the example above? the data are identical. it seems to have something to do with the projection selector
borders: map_srilanka, //might be nice to add some convenience helpers for styling color, etc
projection: projection_selector_srilanka.rotate([Γ, Φ, Λ]),
position: (d) => {
const feat = map_srilanka.features.find(
(f) => d.name.startsWith(f.properties.tags["name:en"]) // This is specific to our dataset; the visualization will break if this property doesn't exist in your dataset.
);
return feat && centroid(feat);
},
value: (d) => d["Population Estimate 2021-07-01"],
title: (d) => `${d.name}\n${d["Population Estimate 2021-07-01"]}`,

showTitle: title_display_srilanka,
maxRadius: 25
})
Insert cell
map_srilanka
Insert cell
centroid = {
const path = d3.geoPath();
return feature => path.centroid(feature);
}
Insert cell
Insert cell
Insert cell
function BubbleMap(
data,
{
// Required
features, // a GeoJSON feature collection for the background
position = (d) => d, // given d in data, returns the [longitude, latitude]
value = () => undefined, // given d in data, returns the quantitative value

// Optional
width = 640,
height = 400,
margin = 10,
borders, // (Optional) a GeoJSON object for stroking borders
// It is okay to provide the same GeoJSON object used in 'features'

title, // given a datum d, returns the hover text

scale = d3.scaleSqrt, // type of radius scale
domain, // [0, max] values; input of radius scale; must start at zero
maxRadius = 40, // maximum radius of bubbles
nice = false,
ticks = 3,

showTitle,

// ⚠️ Apply rotation to the projection if needed.
// d3.geoTransverseMercator() is good for showing a country or smalles area
// d3.geoWinkel3() or d3.geoEqualEarth() is good to show larger area
projection = d3.geoIdentity().reflectY(true).fitWidth(width, features),

backgroundFill = main.grey["200"], // fill color for background
backgroundStroke = "white", // stroke color for borders
backgroundStrokeWidth, // stroke width for borders
backgroundStrokeOpacity, // stroke width for borders
backgroundStrokeLinecap = "round", // stroke line cap for borders
backgroundStrokeLinejoin = "round", // stroke line join for borders
fill = accent.blue, // fill color for bubbles
fillOpacity = 0.5, // fill opacity for bubbles
stroke = "white", // stroke color for bubbles
strokeWidth = 0.5, // stroke width for bubbles
strokeOpacity, // stroke opacity for bubbles,
titleFill = main.grey["900"],
titleFontSize = 10,
titleFontWeight = "normal",
titleStroke = "white",
titleStrokeWidth = 1.5,
titleStrokeOpacity = 0.6,

avoidOverlapping = false // ⚠️ takes time to render
}
) {
// Compute values.
// const I = d3.map(data, (_, i) => i);
const V = d3.map(data, value).map((d) => (d == null ? NaN : +d));
let P = d3.map(data, position);
const T = title == null ? null : d3.map(data, title);
features = normalizeWindingInPlace({ ...features });
borders = borders != null ? normalizeWindingInPlace({ ...borders }) : borders;

// Compute default domains.
if (domain === undefined) domain = [0, d3.max(V)];

// Construct scales.
let radius = scale(domain, [0, maxRadius]);
if (nice) {
radius = radius.copy().nice();
}

projection = projection.scale === undefined ? projection() : projection;

// Adding margin to chart
// https://github.com/d3/d3-geo/blob/main/README.md#projection_fitSize
projection.fitExtent(
[
[margin, margin],
[width - margin, height - margin]
],
features
);

const pathGenerator = d3.geoPath(projection);

if (avoidOverlapping) {
P = applySimulation(P, V, {
radius,
projection,
width: width - 2 * margin,
height: height - 2 * margin
});
}

// Draw
const svg = DOM.svg(width, height);

d3.select(svg)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.style("background", "white");

d3.select(svg)
.append("g")
.attr("class", "boundaries")
.selectAll(".boundary")
.data(features.features)
.join("path")
.classed("boundary", true)
.attr("d", pathGenerator)
.attr("fill", backgroundFill);

if (borders != null) {
d3.select(svg)
.append("path")
.attr("pointer-events", "none")
.attr("fill", "none")
.attr("stroke", backgroundStroke)
.attr("stroke-linecap", backgroundStrokeLinecap)
.attr("stroke-linejoin", backgroundStrokeLinejoin)
.attr("stroke-width", backgroundStrokeWidth)
.attr("stroke-opacity", backgroundStrokeOpacity)
.attr("d", pathGenerator(borders));
}

const legend = circleLegend({
scale: radius,
marginTop: 0,
marginBottom: 0,
stroke: "#666",
strokeWidth: 0.75,
tickFormat: radius.tickFormat(4, "s"),
tickFont: `10px ${fontFamily}`,
tickStrokeWidth: 0.75,
tickStroke: "#666"
});

let { dimensions: legendDimensions } = legend;

legendDimensions =
legendDimensions == null ? { width: 0, height: 0 } : legendDimensions;
d3.select(svg)
.append("g")
.attr("class", "peripherals")
.attr(
"transform",
`translate(${width - legendDimensions.width - margin},${
height - legendDimensions.height - margin
})`
)
.node()
.append(legend);

const bubbles = d3
.select(svg)
.append("g")
.selectAll(".bubbles")
.data(
d3
.range(data.length)
.filter((i) => P[i])
.sort((i, j) => d3.descending(V[i], V[j]))
)
.join("g")
// .attr("transform", (i) => `translate(${projection(P[i])})`)
.attr(
"transform",
avoidOverlapping
? (i) => `translate(${P[i].x},${P[i].y})`
: (i) => `translate(${projection(P[i])})`
)
.attr("fill", fill)
.attr("fill-opacity", fillOpacity)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity);
bubbles
.append("circle")
.attr("r", (i) => radius(V[i]))
.call(T ? (circle) => circle.append("title").text((i) => T[i]) : () => {});

if (showTitle) {
bubbles
.append("text")
.attr("stroke-width", 0)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("paint-order", "stroke")
.attr("fill", titleFill)
.attr("fill-opacity", 1)
.attr("stroke", titleStroke)
.attr("stroke-width", titleStrokeWidth)
.attr("stroke-opacity", titleStrokeOpacity)
.attr("font-size", titleFontSize)
.attr("font-weight", titleFontWeight)
.style("font-family", fontFamily)
.selectAll("tspan")
.data((i) => (T[i] == null ? [] : `${T[i]}`.split(/\n/g)))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, D) => `${i * 1.5 - D.length / 4}em`)
// .attr("font-size", (d, i, D) =>
// i === D.length - 1 ? titleFontSize * 0.75 : null
// )
.attr("fill-opacity", (d, i, D) => (i === D.length - 1 ? 0.85 : 1))
.text((d) => d);
// .call(T ? (circle) => circle.text((i) => T[i]) : () => {});
}

return svg;
}
Insert cell
// Based on KDO's https://observablehq.com/@karimdouieb/try-to-impeach-this-challenge-accepted
applySimulation = (
positions,
values,
{ radius, projection, width, height, maxIterations = 250, nodePadding = 0.5 }
) => {
const points = positions
.map(projection)
// Initialise to positions instead of (0,0)
.map((p) => {
const [x, y] = p;
return Object.assign(p, { x, y });
});

// prettier-ignore
const simulation = d3
.forceSimulation(points)
.force("cx", d3.forceX().x((d) => width / 2).strength(0.02))
.force("cy", d3.forceY().y((d) => height / 2).strength(0.02))
.force("x", d3.forceX().x((d) => d[0] || 0).strength(0.3))
.force("y", d3.forceY().y((d) => d[1] || 0).strength(0.3))
.force("charge", d3.forceManyBody().strength(-1))
.force("collide", d3.forceCollide().radius((d, i) => radius(values[i]) + nodePadding).strength(1))
.stop();

let i = 0;
while (simulation.alpha() > 0.01 && i < maxIterations) {
simulation.tick();
i++;
}

return simulation.nodes();
}
Insert cell
fontFamily = `-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more