Public
Edited
Sep 30, 2024
Insert cell
Insert cell
Insert cell
map = {
let currentEvents = []
let lastMouseX, lastMouseY;

const popover = d3.select("body").append("div")
.attr("class", "popover")
.style("position", "absolute")
.style("visibility", "hidden");
// Specify the chart’s dimensions.
const height = Math.min(width, 720); // Observable sets a responsive *width*

const colorScale = d3.scaleLog()
.domain([1, 10, 100, 1000, 10000, 50000])
.range(["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"]);

// Prepare a canvas.
const dpr = window.devicePixelRatio ?? 1;
const canvas = d3.create("canvas").attr("width", dpr * width).attr("height", dpr * height).style("width", `${width}px`);
const context = canvas.node().getContext("2d");
context.scale(dpr, dpr);
// Create a projection and a path generator.
const projection = d3.geoOrthographic().fitExtent([[10, 10], [width - 10, height - 10]], {type: "Sphere"});
const path = d3.geoPath(projection, context);
const tilt = 20;

function renderEventFeatures(currentTime) {
const radiusScale = d3.scaleLinear()
.domain([0, 75340]) // Adjust based on your data
.range([0.1, 3]);
currentEvents.forEach(feature => {
const baseRadius = radiusScale(feature.properties.best);
const animatedRadius = getAnimatedRadius(baseRadius, feature.properties.start, currentTime);
const circle = d3.geoCircle()
.center(feature.geometry.coordinates)
.radius(animatedRadius || baseRadius);
context.beginPath();
path(circle());
context.fillStyle = 'rgba(255, 0, 0, 0.6)';
context.fill();
});
}

function renderHemisphere(fillStyle, globalAlpha) {
context.save();
context.globalAlpha = globalAlpha;

countryGeo.features.forEach(feature => {
// Begin a new path for the country shape
context.beginPath();
// Draw the country shape
path(feature);
// Set the fill style based on the 'deaths' property
context.fillStyle = feature.properties.deaths ? colorScale(feature.properties.deaths) : fillStyle; // Default color if no deaths
// Fill the country shape
context.fill();
// Optionally, add a stroke to each country
context.stroke();
})

renderEventFeatures()

context.beginPath(), path(borders), context.strokeStyle = "#fff", context.lineWidth = 0.5, context.stroke();
context.beginPath(), path({type: "Sphere"}), context.strokeStyle = "#000", context.lineWidth = 1.5, context.stroke();

context.restore();
}

const projectionRotate = projection.rotate()

function zoomed(event) {
const k = event.transform.k
projection.scale(k < 200 | k > 1200 ? projection.scale()[0] : k);
render(); // Redraw the map with the updated projection
}
// Create a zoom behavior
const zoom = d3.zoom().scaleExtent([200, 1200]).on("zoom", zoomed);
// Modify the setInterval part to pass the entire event object
let currentDay = 1;
async function addEventsAsync() {
while (currentDay <= 365) {
removePassEvents()

const day = dayjs().year(year).dayOfYear(currentDay);
const currents = events.filter(i => day.isSame(i.date_start, 'day'));
const now = Date.now()
currents.forEach(event => {
event.start = now
currentEvents.push(transformEventToFeature(event))
const countryIndex = countryMap[event.country]
if(countryIndex) {
// Ensure that the deaths property is initialized
if (!countries[countryIndex].properties.deaths) {
countries[countryIndex].properties.deaths = 0;
}
// Now safely add to the deaths property
countries[countryIndex].properties.deaths += Number(event.best);
}
});
const hoveredCountry = getCountryAtPosition(lastMouseX, lastMouseY);
updatePopover(hoveredCountry, lastMouseX, lastMouseY);
await new Promise(resolve => setTimeout(resolve, 1200)); // Wait for 300 milliseconds before processing the next day
currentDay++;
}
}

addEventsAsync()

function drawProgressBar() {
const progressBarHeight = 20; // Height of the progress bar
const progressBarY = height - progressBarHeight; // Y position of the progress bar
const progressBarWidth = (currentDay / 364) * width; // Width based on current day

// Draw the background of the progress bar
context.fillStyle = "#e0e0e0";
context.fillRect(0, progressBarY, width, progressBarHeight);

// Draw the progress
context.fillStyle = "#fc8d59";
context.fillRect(0, progressBarY, progressBarWidth, progressBarHeight);
}

function drawLenged() {
const legendData = colorScale.domain(); // Get domain of your scale, e.g., [1, 10, 100, 1000, 10000]
const legendWidth = 50; // Width of legend item
const legendHeight = 20; // Height of legend item
const legendMargin = 5; // Margin between items
let legendX = width - 350; // Starting X position of the legend
const legendY = height - 50; // Y position of the legend
// Draw legend items
legendData.forEach((value, index) => {
// Set color for each legend item
context.fillStyle = colorScale(value);
// Draw the rectangle
context.fillRect(legendX, legendY, legendWidth, legendHeight);
// Draw the text
context.font = '12px Arial';
context.fillStyle = 'black'; // Or any color that is visible on your legend item
context.fillText(value.toString(), legendX + legendWidth - context.measureText(value.toString()).width / 2, legendY - legendHeight + 10);
// Increment x position for next item
legendX += legendWidth + legendMargin
});
}

function getAnimatedRadius(baseRadius, startTime, currentTime) {
const duration = 2000; // Duration of one cycle of expansion and contraction
const elapsed = (currentTime - startTime) % duration;
const expansion = Math.sin((elapsed / duration) * 2 * Math.PI); // Sine wave for smooth oscillation
return baseRadius + expansion * 0.5; // Adjust '2' to control max expansion
}

function animate() {
const currentTime = Date.now();

// Clear the canvas
context.clearRect(0, 0, width, height);

render();

renderEventFeatures(currentTime); // Pass the current time

requestAnimationFrame(animate);
}

function removePassEvents() {
const duration = 5000
const now = Date.now()
currentEvents = currentEvents.filter(event => {
return (event.start_doy <= currentDay && event.end_doy >= currentDay) || (now - event.properties.start) < duration
})
}

function render() {
// Clear the canvas
context.clearRect(0, 0, width, height);

// Set clip angle for back hemisphere and render it
projection.clipAngle(180);
renderHemisphere("#e0e0e0", 0.5); // Light color and lower opacity for back hemisphere

// Set clip angle for front hemisphere and render it
projection.clipAngle(90);
renderHemisphere("#ccc", 1); // Normal color and opacity for front hemisphere

drawProgressBar();
drawLenged();

const currentDate = dayjs().year(year).dayOfYear(currentDay).format('MMMM D, YYYY'); // Format the date as needed
context.font = '30px Arial'; // Set the font size and type
context.fillStyle = 'black'; // Set the text color
context.fillText(currentDate, 0, height - 40); // Position the text at bottom left corner
}

function dragged(event) {
render(); // Redraw the map
}
// You might need to adjust the sensitivity based on your needs
const sensitivity = 75;

animate();

function getCountryAtPosition(mouseX, mouseY) {
const transform = d3.zoomTransform(canvas);
const coords = projection.invert([ (mouseX - transform.x) / transform.k, (mouseY - transform.y) / transform.k ]);
if (!coords) return null;
return countryGeo.features.find(feature => d3.geoContains(feature, coords));
}

function updatePopover(country, mouseX, mouseY) {
if (country) {
const events = currentEvents.filter(i => i.properties.country === country.properties.name)
let articlesHTML = events.map(event => {
const title = newsMap[Number(event.properties.id)];
const truncatedTitle = title.length > 100 ? `${title.substring(0, 100)}...` : title;
return `<tr><td>"${truncatedTitle}"<td> <td><span class="number">${event.properties.best}</span></td></tr>`;
}).join('');
popover.html(`<h3>${country.properties.name}</h3>
<div><strong>Total Deaths: </strong> <strong class="number">${country.properties.deaths || 0}</strong></div>
<table>
${articlesHTML ? `
<tr>
<td><strong>Source</strong></td>
<td><strong style="text-align: right;">Deaths</strong></td>
</tr>
` : ''}
${articlesHTML}
</table>`)
.style("left", `${mouseX + 30}px`)
.style("top", `${mouseY + 20}px`)
.style("visibility", "visible");
} else {
popover.style("visibility", "hidden");
}
}
function hidePopover() {
popover.style("visibility", "hidden");
}

function transformEventToFeature(event) {
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [parseFloat(event.longitude), parseFloat(event.latitude)]
},
properties: {
id: event.id,
year: event.year,
country: event.country,
date_start: event.date_start,
date_end: event.date_end,
best: parseInt(event.best),
start: event.start
}
}
}

d3.select(context.canvas)
.on("mousemove", function(event) {
[lastMouseX, lastMouseY] = d3.pointer(event);
const hoveredCountry = getCountryAtPosition(lastMouseX, lastMouseY);
updatePopover(hoveredCountry, lastMouseX, lastMouseY);
});
return d3.select(context.canvas).call(drag(projection).on("drag.render", dragged)).call(zoom)
.call(render)
.node();
}
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

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