map = {
let currentEvents = []
let lastMouseX, lastMouseY;
const popover = d3.select("body").append("div")
.attr("class", "popover")
.style("position", "absolute")
.style("visibility", "hidden");
const height = Math.min(width, 720);
const colorScale = d3.scaleLog()
.domain([1, 10, 100, 1000, 10000, 50000])
.range(["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"]);
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);
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])
.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();
}