Public
Edited
Nov 26, 2023
1 fork
Insert cell
Insert cell
// A wide selection of color legends
import {legend} from "@d3/color-legend"
Insert cell
// A slider with two ends and adjustable offsets
import {offsetInterval} from "@mootari/offset-slider"
Insert cell
// Visualisation title cards for the Trase front-end.
import {titleCard} from "@trase/title-card"
Insert cell
// Parse CSV file
billionaires_maps = d3.csvParse(await FileAttachment('billionaires_rename_countries.csv').text())
Insert cell
// Store variables
billionaires_maps_selection = billionaires_maps.map(d => ({
country: d.country,
country_coordinates: [d.latitude_country, d.longitude_country],
rank: d.rank,
worth: d.finalWorth,
name: d.personName,
industry: d.industries,
gender: d.gender,
age: d.age
}))
Insert cell
// Offsets for ranks
ranks = [1, 10, 50, 100, 200, 300, 400, 500];
Insert cell
// Offsets for ages
ages = [30, 40, 50, 60, 70, 80, 90, 100]
Insert cell
// Get all unique industries
uniqueIndustries = new Set(billionaires_maps.flatMap(d =>
Array.isArray(d.industries) ? d.industries : [d.industries]
));
Insert cell
// Sort all unique industries alphabetically (also include 'All' for all industries)
industries = ['All', ...uniqueIndustries].sort();
Insert cell
// Display industries
viewof industry = DOM.select(industries);
Insert cell
// Filter map based on the chosen industry
billionaires_maps_filtered = industry === "All" ?
billionaires_maps_selection :
billionaires_maps_selection.filter(d => d.industry === industry)
Insert cell
// Filter map based on the chosen gender
billionaires_maps_filtered_Gender = gender === 'All' ?
billionaires_maps_filtered:
billionaires_maps_filtered.filter(d => d.gender === genderMap.get(gender))
Insert cell
// Filter map based on the chosen rank and age ranges
billionaires_maps_filtered_rank_age = billionaires_maps_filtered_Gender.filter(d =>
d.rank >= rank_range[0] && d.rank <= rank_range[1] &&
d.age >= age_range[0] && d.age <= age_range[1]
)
Insert cell
// Group billionaires by country
billionaires_map_counts = d3.group(billionaires_maps_filtered_rank_age, d => d.country)
Insert cell
// Creates a map with countries as keys and a single-element array containing the count of billionaires as values
new_billionaires_counts = new Map([...billionaires_map_counts].map(([key, value]) => [key, [value.length]]));
Insert cell
// Creates a map with countries as keys and the count of billionaires as numeric values
billionaires_counts = new Map([...billionaires_map_counts].map(([key, value]) => [key, value.length]));
Insert cell
colorScale = d3.scaleSequential()
.domain([0, d3.max(billionaires_counts.values())])
.interpolator(d3.interpolateBlues);
Insert cell
// Legend for the color scale "colorScale" with a title "Billionaires Count per Country".
color_legend = legend({
color: colorScale,
title: "Billionaires Count per Country",
});
Insert cell
colorScale1 = d3.scaleSequential()
.domain([0, 1])
.interpolator(d3.interpolateBlues);
Insert cell
color_legend1 = legend({
color: colorScale1,
title: "Billionaires Count per Country",
tickValues: [0, 1],
tickFormat: d3.format(".0f")
})
Insert cell
colorScale2 = d3.scaleSequential()
.domain([0, 2])
.interpolator(d3.interpolateBlues);
Insert cell
color_legend2 = legend({
color: colorScale2,
title: "Billionaires Count per Country",
tickValues: [0, 1, 2],
tickFormat: d3.format(".0f")
})
Insert cell
colorScale3 = d3.scaleSequential()
.domain([0, 3])
.interpolator(d3.interpolateBlues);
Insert cell
color_legend3 = legend({
color: colorScale3,
title: "Billionaires Count per Country",
tickValues: [0, 1, 2, 3],
tickFormat: d3.format(".0f")
})
Insert cell
colorScale4 = d3.scaleSequential()
.domain([0, 4])
.interpolator(d3.interpolateBlues);
Insert cell
color_legend4 = legend({
color: colorScale4,
title: "Billionaires Count per Country",
tickValues: [0, 1, 2, 3, 4],
tickFormat: d3.format(".0f")
})
Insert cell
topojson = require("topojson-client@3")
Insert cell
// Loading a JSON file containing world atlas data
world = d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json")
Insert cell
// Convert the TopoJSON data into a GeoJSON feature collection
countries = topojson.feature(world, world.objects.countries)
Insert cell
// Natural Earth projection
projection = d3.geoNaturalEarth1()
Insert cell
// Convert geographic data into SVG path elements that can be rendered on a map
path = d3.geoPath(projection)
Insert cell
// Display genders with a default value of 'All'
viewof gender = Inputs.radio(["All", "Male", "Female"], {
value:"All"
})
Insert cell
// Maps full strings of genders to respective abbreviations
genderMap = new Map(Object.entries({
'Male': 'M',
'Female': 'F',
'All': 'All'
}));
Insert cell
// Displays the offset slider for rank
viewof rank_range = offsetInterval(ranks, {
formatValue: rank => rank.toString(),
value: [ranks[0], ranks[ranks.length - 1]], // default to full range
});
Insert cell
// Displays the offset slider for age
viewof age_range = offsetInterval(ages, {
formatValue: ages => ages.toString(),
value: [ages[0], ages[ages.length - 1]], // default to full range
});
Insert cell
Insert cell
titleCard1 = titleCard({
title: "Which Nations Harbor the Wealthiest?",
subtitle: "An Interactive Color-Coded Mapping of Billionaires Worldwide.",
extra: html`
<style>
.filters-layout {
display: flex;
justify-content: start;
align-items: flex-start;
}
.filter-group {
display: flex;
flex-direction: column;
margin-right: 20px;
}
.filter-label {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
}
.filter-container {
margin-bottom: 10px;
}
.sliders {
display: flex;
flex-direction: column;
justify-content: start;
}
.footnote {
font-size: 12px;
margin-top: 20px;
color: #555; /* Adjust the color as necessary */
}
/* Other styles */
</style>
<div class="filters-layout">
<div class="filter-group">
<div class="filter-container">
<label class="filter-label">Select an industry:</label>
${viewof industry}
</div>
<div class="filter-container">
<label class="filter-label">Select a gender:</label>
${viewof gender}
</div>
</div>
<div class="sliders">
<div class="filter-container">
<label class="filter-label">Select a rank range:</label>
${viewof rank_range}
</div>
<div class="filter-container">
<label class="filter-label">Select an age range:</label>
${viewof age_range}
</div>
</div>
</div>
<div class="footnote">
Hover on a country to see counts, double-click for more detailed information, and press 'esc' to go back to the world map.
</div>
`,
width: 1
});

Insert cell
graph = {
// Initialize the SVG dimensions
const width = 960;
const height = 600;
// Create a SVG element and appends it to the DOM.
const svg = d3.select(DOM.svg(width, height));
const g = svg.append("g");
// Set the panning and zooming limits
const panningLimit = [[-100, 0], [width + 100, height]];
const zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent(panningLimit)
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
// Create a container for the list of billionaires
const listContainer = d3.create("div")
.style("width", "200px")
.style("height", "500px")
.style("overflow", "auto")
.style("border", "1px solid #ccc")
.style("padding", "10px")
.style("display", "none");
// Create a tooltip for displaying country-specific information
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background", "#f9f9f9")
.style("border", "1px solid #e0e0e0")
.style("border-radius", "5px")
.style("padding", "10px")
.style("font-family", "Arial, sans-serif")
.style("font-size", "14px")
.style("box-shadow", "2px 2px 6px 0px rgba(0,0,0,0.3)")
.style("font-weight", "bold")
.style("text-align", "center");

// Add two span elements within the tooltip for text and count
tooltip.selectAll("span")
.data(["text", "count"])
.enter().append("span")
.style("display", "block")
.style("margin-top", "8px");

// Function to display the tooltip on mouseover
function showTooltip(event, d) {
const countryName = d.properties.name;
const count = new_billionaires_counts.get(countryName) || 0;
let text = `<span style="font-size: 18px; color: #333;">${countryName}</span><br><span>Number of billionaires: <span style="color: #ff8c00; font-weight: bold;">${count}</span></span><br><span style="font-size: 10px; color: #999;">Double click to see more information</span>`;
tooltip.html(text)
.style("visibility", "visible")
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
}
// Function to hide the tooltip on mouseout
function hideTooltip() {
tooltip.style("visibility", "hidden");
}

let max = 0;
// Select and display the map data with interactive features
g.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter().append("path")
.attr("d", path)
.style("fill", d => {
const value = new_billionaires_counts.get(d.properties.name) || 0;
max = Math.max(max, value);
return colorScale(value);
})
.style("stroke", "black")
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip)
.on("dblclick", function(event, d) {
tooltip.style("visibility", "hidden");
const clickedCountryName = d.properties.name;
const billionairesInCountry = billionaires_map_counts.get(clickedCountryName) || 0;
updateBillionairesList(billionairesInCountry);
g.selectAll("path").remove();
g.append("path")
.datum(d)
.attr("d", path)
.style("fill", d => {
const value = new_billionaires_counts.get(d.properties.name) || 0;
return colorScale(value);
})
.style("stroke", "black");
listContainer.style("display", "block");

// Event listener for the 'Escape' key
d3.select("body").on("keydown", (event) => {
if (event.key === "Escape") {
listContainer.style("display", "none");
g.selectAll("path").remove();
svg.call(zoom);

// Rebind the country data to path elements and re-apply attributes and styles
g.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter().append("path")
.attr("d", path)
.style("fill", d => {
const value = new_billionaires_counts.get(d.properties.name) || 0;
return colorScale(value);
})
.style("stroke", "black")
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip)
.on("dblclick", function(event, d) {
tooltip.style("visibility", "hidden");
const clickedCountryName = d.properties.name;
const billionairesInCountry = billionaires_map_counts.get(clickedCountryName) || 0;
updateBillionairesList(billionairesInCountry);
g.selectAll("path").remove();
g.append("path")
.datum(d)
.attr("d", path)
.style("fill", d => {
const value = new_billionaires_counts.get(d.properties.name) || 0;
return colorScale(value);
})
.style("stroke", "black");
listContainer.style("display", "block");
});
}
});
});

// Function to update the list of billionaires based on the selected country
function updateBillionairesList(billionaires) {
// List update logic based on the billionaires data
listContainer.selectAll("div").remove();
if (!billionaires || billionaires.length === 0) {
listContainer.append("div")
.text("Be the first one to pave the way!")
.style("padding", "20px 20px 10px 20px");

listContainer.append("div")
.text("Press 'esc' to go back")
.style("padding", "10px 20px 20px 20px")
.style("color", "grey")
.style("font-size", "14px");
} else {
const billionairesDiv = listContainer.selectAll("div")
.data(billionaires, d => d.name);
billionairesDiv.enter()
.append("div")
.merge(billionairesDiv)
.style("padding", "20px")
.style("border-bottom", "1px solid #ddd")
.html(d => `
<div style="font-size: 22px;">${d.name}</div>
<div style="color: orange;">Rank: ${d.rank}</div>
<div style="color: orange;">Worth: $${d.worth/1000}B</div>
<div style="font-size: 14px;">Gender: ${d.gender}</div>
<div style="font-size: 14px;">Age: ${d.age}</div>
<div style="font-size: 14px;">Industry: ${d.industry}</div>
`);
billionairesDiv.exit().remove();
listContainer.style("overflow", "auto");
// Addition of a title for the list container
const titleContainer = listContainer.insert("div", ":first-child")
.style("font-size", "24px")
.style("font-weight", "bold");

titleContainer.append("span")
.text("Billionaires List")
.style("color", "black")
.append("br");

titleContainer.append("span")
.text("Press 'esc' to go back")
.style("font-size", "14px")
.style("color", "grey");

titleContainer.style("position", "sticky")
.style("top", "0")
.style("background", "white");
}
}
// Append a color legend to the SVG
const legend = svg.append("g")
.attr("transform", `translate(${width - 450}, 400)`);
if (max === 1) {
legend.append(() => color_legend1);
} else if (max === 2){
legend.append(() => color_legend2);
} else if (max === 3) {
legend.append(() => color_legend3);
} else if (max === 4) {
legend.append(() => color_legend4);
} else {
legend.append(() => color_legend);
}
// Create a main container for the visualization with a white background and flex display
const container = d3.create("div")
.style("display", "flex")
.style("background-color", "white");

// Append the SVG and list container to the main container
container.append(() => svg.node());
container.append(() => listContainer.node());

// Return the main container for display
return container.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