Public
Edited
May 6, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
1900_2021_DISASTERS.xlsx - emdat data.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
catastrophes = FileAttachment("CatastrophesFilled@2.json").json()
Insert cell
catastrophes
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
nest(Catastrophes, d => d.DisasterSubgroup, d => d.DisasterType, d => d.DisasterSubtype)
Insert cell
Insert cell
import { toc } from "@nebrius/indented-toc" // This component generates the Table of Contents
Insert cell
/*cities = CatastrophesByYear.map(d => [
d.Location,
{
latitude: d.Latitude,
longitude: d.Longitude,
country: d.Country,
region: d.Region,
//continent: d.Continent,
//disasterSubgroup: d.DisasterSubgroup,
//disasterType: d.DisasterType,
//disasterSubType: d.DisasterSubtype,
}
]);*/
Insert cell
data = nest(CatastrophesByYear, d => d.Continent, d => d.Region, d => d.Country, d => d.ISO)
Insert cell
catastrophesCoordinates = FileAttachment("CatastrophesForMapDisplay@12.json").json()
Insert cell
catastrophesCoordinatesByYear = FileAttachment("CatastrophesForMapDisplayByYear.json").json()
Insert cell
markersData = selection === "All" ? catastrophesCoordinates : catastrophesCoordinatesByYear[yearDynamic - 1990]
Insert cell
catastropheColorScale = ["limegreen", "red", "orange", "lightblue", "yellow"]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
world = FileAttachment("countries-50m.json").json()
Insert cell
landData = topojson.feature(world, world.objects.land)
Insert cell
countries = FileAttachment("Countries@1.json").json()
Insert cell
countrymesh = topojson.mesh(world, world.objects.countries, (a, b) => a !== b);
Insert cell
Insert cell
function createFilterFromMap(map, nameToSearchFor){
let keys = Array.from(map.get(nameToSearchFor).keys());
let keysString = "(\"ISO_A3\" ILIKE '%" + keys.join("%' OR \"ISO_A3\" ILIKE '%") + "%')";
return keysString;
}
Insert cell
isoByContinents = nest(CatastrophesByYear, d => d.Continent, d => d.ISO)
Insert cell
isoByRegions = nest(CatastrophesByYear, d => d.Region, d => d.ISO)
Insert cell
//Example usage for filter:
exampleFilter = createFilterFromMap(isoByContinents, "Africa")
Insert cell
regions = FileAttachment("regions.json").json()
Insert cell
continents = FileAttachment("continents.json").json()
Insert cell
Insert cell
import {zoomAbleWorld} from "@fhooe-infovis/nested-zoomable-map"
Insert cell
import {zoomAbleMapObject} from "@fhooe-infovis/nested-zoomable-map"
Insert cell
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
//Code from: https://observablehq.com/@d3/color-legend
function Swatches(color, {
columns = null,
format,
unknown: formatUnknown,
swatchSize = 15,
swatchWidth = swatchSize,
swatchHeight = swatchSize,
marginLeft = 0
} = {}) {
const id = `-swatches-${Math.random().toString(16).slice(2)}`;
const unknown = formatUnknown == null ? undefined : color.unknown();
const unknowns = unknown == null || unknown === d3.scaleImplicit ? [] : [unknown];
const domain = color.domain().concat(unknowns);
if (format === undefined) format = x => x === unknown ? formatUnknown : x;

function entity(character) {
return `&#${character.charCodeAt(0).toString()};`;
}

if (columns !== null) return htl.html`<div style="display: flex; align-items: center; margin-left: ${+marginLeft}px; min-height: 33px; font: 10px sans-serif;">
<style>

.${id}-item {
break-inside: avoid;
display: flex;
align-items: center;
padding-bottom: 1px;
}

.${id}-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - ${+swatchWidth}px - 0.5em);
}

.${id}-swatch {
width: ${+swatchWidth}px;
height: ${+swatchHeight}px;
margin: 0 0.5em 0 0;
}

</style>
<div style=${{width: "100%", columns}}>${domain.map(value => {
const label = `${format(value)}`;
return htl.html`<div class=${id}-item>
<div class=${id}-swatch style=${{background: color(value)}}></div>
<div class=${id}-label title=${label}>${label}</div>
</div>`;
})}
</div>
</div>`;

return htl.html`<div style="display: flex; align-items: center; min-height: 33px; margin-left: ${+marginLeft}px; font: 10px sans-serif;">
<style>

.${id} {
display: inline-flex;
align-items: center;
margin-right: 1em;
}

.${id}::before {
content: "";
width: ${+swatchWidth}px;
height: ${+swatchHeight}px;
margin-right: 0.5em;
background: var(--color);
}

</style>
<div>${domain.map(value => htl.html`<span class="${id}" style="--color: ${color(value)}">${format(value)}</span>`)}</div>`;
}
Insert cell
//Whole section from: https://observablehq.com/@mbostock/group & https://observablehq.com/@mbostock/nested-groups
function noop() {}
Insert cell
function identity(x) { return x; }
Insert cell
group = {
const reduce = (p, v) => (p.push(v), p);
const init = () => [];
return function group(values, keyof) {
return groupReduce(values, keyof, reduce, init);
};
}
Insert cell
function groupReduce(values, keyof = identity, reduce, init = noop) {
const map = new Map();
let index = -1;
for (const value of values) {
const key = keyof(value, ++index, values);
map.set(key, reduce(map.has(key) ? map.get(key) : init(key), value, index, values));
}
return map;
}
Insert cell
function nest(values, ...keys) {
return (function regroup(values, i) {
if (i >= keys.length) return values;
const map = group(values, keys[i]);
return new Map(Array.from(map, ([k, v]) => [k, regroup(v, i + 1)]));
})(values, 0);
}
Insert cell
//https://observablehq.com/@d3/world-choropleth/2
import {addTooltips} from "@mkfreeman/plot-tooltip"
Insert cell
import {Legend} from "@d3/color-legend"
Insert cell
function cloneMap(map) {
const clonedMap = new Map();
for (const [key, value] of map) {
if (value instanceof Map) {
clonedMap.set(key, cloneMap(value));
} else {
clonedMap.set(key, value);
}
}
return clonedMap;
}
Insert cell
function cloneObject(obj) {
if (typeof obj !== 'object' || obj === null) {
// If the value is not an object, return it directly
return obj;
}

const clonedObj = Array.isArray(obj) ? [] : {}; // Determine if obj is an array or an object
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// Recursively clone nested objects
clonedObj[key] = cloneObject(obj[key]);
}
}
return clonedObj;
}
Insert cell
function createTooltip(d){
const location = d.Location ? `Location: ${d.Location}` : "";
const country = d.Country ? `(${d.Country.replace(" (the)", "")})` : "";
const seq = d.Seq ? `Nr. ${d.Seq}` : "";
const disasterSubgroup = d.DisasterSubgroup ? `${d.DisasterSubgroup} Catastrophe` : "";
const disasterSubtype = d.DisasterSubtype ? `(${d.DisasterSubtype})` : "";
const disasterSubsubtype = d.DisasterSubsubtype ? `-${d.DisasterSubsubtype}` : "";
const startMonth = d.StartMonth ? `Time: ${d.StartMonth}/` : "";
const startYear = d.StartYear ? `${d.StartYear}` : "";
const endMonth = d.EndMonth ? `-${d.EndMonth}/` : "";
const endYear = d.EndYear ? `${d.EndYear}` : "";
const totalAffected = d.TotalAffected ? `Total Affected: ${d.TotalAffected}` : "";
//const latitude = d.Latitude ? `Latitude: ${d.Latitude}/` : "";
//const longitude = d.Longitude ? `Longitude: ${d.Longitude}` : "";

return `${location} ${country}\n${seq} ${disasterSubgroup} ${disasterSubtype} ${disasterSubsubtype}\n${startMonth}${startYear}${endMonth}${endYear}\n${totalAffected}`
//\n${latitude}${longitude}`
}
Insert cell
function createZoomAbleMap(chosenWidth=900, chosenHeight=500, minZoom=0.5, maxZoom=100, projection=d3.geoMercator(), data=zoomAbleMapObject, fillColor="red", strokeColor="white", fillColorNone="#444", names = false, textColor = "grey", fontSize = 1, coordinates = zoomAbleWorld, markersData=Catastrophes, radius=1, markersFillColor="orange") {
const width = chosenWidth;
const height = chosenHeight;

const zoom = d3.zoom()
.scaleExtent([minZoom, maxZoom])
.on("zoom", zoomed);

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: 100%;")
.on("click", reset);

const path = d3.geoPath(projection);

const g = svg.append("g");

let land = null;
let states = null;
let textLabels = null;
let markers = null;
function update(data){
g.selectAll("*").remove();
if (!data.features) {
console.error("Data features are null or undefined");
return;
}

land = g.append("g")
.attr("fill", "grey")
.selectAll("path")
.data(landData.features)
.join("path")
.attr("d", path)
.attr("cursor", "pointer")

states = g.append("g")
.attr("fill", fillColorNone)
.attr("cursor", "pointer")
.attr("stroke", strokeColor)
.attr("stroke-width", 0.1)
.selectAll("path")
.data(data.features)
.join("path")
.attr("d", path)
.on("click", clicked);

textLabels = g.append("g")
.attr("pointer-events", "none")
.selectAll("text")
.data(data.features)
.join("text")
.attr("text-anchor", "middle")
.style("font-size", fontSize)
.attr("fill", textColor)
.text(d => d.properties.name)
.attr("transform", d => `translate(${path.centroid(d)})`);
states.append("title").text(d => d.properties.name);

markers = g.selectAll("circle")
.data(markersData)
.join("g");

// Append circles
markers.append("circle")
.attr("cx", d => projection([d.Longitude, d.Latitude])[0])
.attr("cy", d => projection([d.Longitude, d.Latitude])[1])
.attr("r", radius)
.attr("fill", function(d){
if(d.DisasterSubgroup === "Biological"){return "limegreen"}
else if(d.DisasterSubgroup === "Climatological"){return "red"}
else if(d.DisasterSubgroup === "Geophysical"){return "orange"}
else if(d.DisasterSubgroup === "Hydrological"){return "lightblue"}
else if(d.DisasterSubgroup === "Meteorological"){return "yellow"}
});
markers.append("title")
.text(d => createTooltip(d));

g.append("path")
.attr("fill", "none")
.attr("stroke", strokeColor)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 10)
.attr("d", path(countrymesh.features));
}
update(data);
svg.call(zoom);

function reset() {
states.transition().style("fill", null);
setTimeout(() => {data = zoomAbleMapObject; fontSize = 10; update(data);}, 750);
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity,
d3.zoomTransform(svg.node()).invert([width, height])
);
}

function clicked(event, d) {
console.log(d)
if(d.properties.name != "Antarctica"){
const [[x0, y0], [x1, y1]] = path.bounds(d);
event.stopPropagation();
states.transition().style("fill", null);
d3.select(this).transition().style("fill", fillColor);
console.log(d)
if(d.properties.name === "South America"){
d = d.features[0]
}
if(data.territoryType !== "Region"){
setTimeout(() => {
data = d;
const transform = d3.zoomTransform(svg.node());
const fontSizeScaled = Math.min((fontSize / transform.k) * 10, 2);
if(data.territoryType == "Country"){
fontSizeScaled = Math.min((fontSize / transform.k) * 10, 1);
}
radius = Math.max(0.01, Math.min((radius / transform.k) * 2, 1))
update(data);
textLabels.style("font-size", fontSizeScaled);}, 750);
}
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(Math.min(100, 0.9 / Math.max((x1 - x0) / width, (y1 - y0) / height)))
.translate(-(x0 + x1) / 2, -(y0 + y1) / 2),
d3.pointer(event, svg.node()),
);
}
}

function zoomed(event) {
const {transform} = event;
g.attr("transform", transform);
g.attr("stroke-width", 10);// / transform.k);
const fontSizeScaled = Math.min((fontSize / transform.k), 2);
textLabels.style("font-size", fontSizeScaled);
textLabels.attr("fill", textColor);
// Update marker radius on zoom
markers.selectAll("circle")
.attr("r", Math.max(0.01, Math.min((radius / transform.k) * 2, 1)));
}

return svg.node();
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more