temporalFlowMap = {
const width = 1200;
const height = 700;
const controlHeight = 100;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height + controlHeight);
const mapG = svg.append("g");
const controlG = svg.append("g")
.attr("transform", `translate(0, ${height})`);
const projection = d3.geoNaturalEarth1()
.scale(150)
.translate([width / 2, height / 2]);
const path = d3.geoPath().projection(projection);
mapG.append("g")
.selectAll("path")
.data(geoJSON.features)
.enter().append("path")
.attr("d", path)
.attr("fill", "#f8f9fa")
.attr("stroke", "#dee2e6")
.attr("stroke-width", 0.5);
const filteredData = aiddata.filter(d =>
top20Donors.includes(d.donor) && top10Recipients.includes(d.recipient)
);
const yearRange = d3.extent(filteredData, d => d.yearInt);
const years = d3.range(yearRange[0], yearRange[1] + 1);
const yearlyData = d3.group(filteredData, d => d.yearInt);
const timeScale = d3.scaleLinear()
.domain(yearRange)
.range([50, width - 50]);
const timeline = controlG.append("g")
.attr("transform", "translate(0, 30)");
timeline.append("line")
.attr("x1", timeScale.range()[0])
.attr("x2", timeScale.range()[1])
.attr("y1", 20)
.attr("y2", 20)
.attr("stroke", "#6c757d")
.attr("stroke-width", 2);
timeline.selectAll(".year-tick")
.data(years.filter((d, i) => i % 3 === 0))
.enter().append("g")
.attr("class", "year-tick")
.attr("transform", d => `translate(${timeScale(d)}, 20)`)
.each(function(d) {
d3.select(this).append("circle")
.attr("r", 3)
.attr("fill", "#6c757d");
d3.select(this).append("text")
.attr("y", 15)
.attr("text-anchor", "middle")
.style("font-size", "10px")
.text(d);
});
const yearIndicator = timeline.append("g")
.attr("class", "year-indicator");
yearIndicator.append("circle")
.attr("r", 6)
.attr("fill", "#007bff")
.attr("cy", 20);
const yearLabel = yearIndicator.append("text")
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.style("font-size", "14px")
.attr("fill", "#007bff");
const flowsG = mapG.append("g").attr("class", "flows");
const pointsG = mapG.append("g").attr("class", "points");
function updateMap(year) {
const currentData = yearlyData.get(year) || [];
const flows = d3.rollup(
currentData,
v => d3.sum(v, d => d.amount),
d => d.donor,
d => d.recipient
);
const arcData = [];
flows.forEach((recipients, donor) => {
const donorCoords = getCountryCoordinates(donor, geoJSON);
if (donorCoords) {
recipients.forEach((amount, recipient) => {
const recipientCoords = getCountryCoordinates(recipient, geoJSON);
if (recipientCoords && amount > 0) {
arcData.push({
source: donorCoords,
target: recipientCoords,
donor,
recipient,
amount
});
}
});
}
});
const maxAmount = d3.max(arcData, d => d.amount) || 1;
const strokeScale = d3.scaleLinear()
.domain([0, maxAmount])
.range([1, 10]);
function createArc(source, target) {
const sourceProj = projection(source);
const targetProj = projection(target);
if (!sourceProj || !targetProj) return null;
const dx = targetProj[0] - sourceProj[0];
const dy = targetProj[1] - sourceProj[1];
const dr = Math.sqrt(dx * dx + dy * dy) * 0.4;
return `M${sourceProj[0]},${sourceProj[1]}A${dr},${dr} 0 0,1 ${targetProj[0]},${targetProj[1]}`;
}
const arcs = flowsG.selectAll(".flow-arc")
.data(arcData, d => `${d.donor}-${d.recipient}`);
arcs.exit()
.transition()
.duration(500)
.attr("opacity", 0)
.remove();
arcs.enter().append("path")
.attr("class", "flow-arc")
.attr("d", d => createArc(d.source, d.target))
.attr("fill", "none")
.attr("stroke", "#ff6b35")
.attr("opacity", 0)
.transition()
.duration(500)
.attr("opacity", 0.6)
.attr("stroke-width", d => strokeScale(d.amount));
arcs.transition()
.duration(500)
.attr("stroke-width", d => strokeScale(d.amount))
.attr("opacity", 0.6);
const countryTotals = new Map();
arcData.forEach(d => {
countryTotals.set(d.donor, (countryTotals.get(d.donor) || 0) + d.amount);
countryTotals.set(d.recipient, (countryTotals.get(d.recipient) || 0) + d.amount);
});
const pointData = [];
countryTotals.forEach((total, country) => {
const coords = getCountryCoordinates(country, geoJSON);
if (coords) {
pointData.push({
country,
coords: projection(coords),
total,
isDonor: top20Donors.includes(country)
});
}
});
const maxTotal = d3.max(pointData, d => d.total) || 1;
const radiusScale = d3.scaleSqrt()
.domain([0, maxTotal])
.range([2, 12]);
const points = pointsG.selectAll(".country-point")
.data(pointData.filter(d => d.coords), d => d.country);
points.exit()
.transition()
.duration(500)
.attr("r", 0)
.remove();
points.enter().append("circle")
.attr("class", "country-point")
.attr("cx", d => d.coords[0])
.attr("cy", d => d.coords[1])
.attr("r", 0)
.attr("fill", d => d.isDonor ? "#2196f3" : "#ff9800")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.transition()
.duration(500)
.attr("r", d => radiusScale(d.total));
points.transition()
.duration(500)
.attr("r", d => radiusScale(d.total));
yearIndicator.attr("transform", `translate(${timeScale(year)}, 0)`);
yearLabel.text(year);
}
const controls = controlG.append("g")
.attr("transform", "translate(50, 60)");
let currentYearIndex = 0;
let isPlaying = false;
let playInterval;
const playButton = controls.append("g")
.style("cursor", "pointer");
playButton.append("circle")
.attr("r", 20)
.attr("fill", "#007bff")
.attr("stroke", "#fff")
.attr("stroke-width", 2);
const playIcon = playButton.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.style("font-size", "16px")
.style("fill", "white")
.text("▶");
playButton.on("click", function() {
if (isPlaying) {
clearInterval(playInterval);
isPlaying = false;
playIcon.text("▶");
} else {
isPlaying = true;
playIcon.text("⏸");
playInterval = setInterval(() => {
currentYearIndex = (currentYearIndex + 1) % years.length;
updateMap(years[currentYearIndex]);
if (currentYearIndex === years.length - 1) {
setTimeout(() => {
clearInterval(playInterval);
isPlaying = false;
playIcon.text("▶");
}, 1000);
}
}, 800);
}
});
const slider = controls.append("g")
.attr("transform", "translate(60, 0)");
const sliderScale = d3.scaleLinear()
.domain([0, years.length - 1])
.range([0, 300]);
slider.append("line")
.attr("x1", 0)
.attr("x2", 300)
.attr("stroke", "#6c757d")
.attr("stroke-width", 4);
const sliderHandle = slider.append("circle")
.attr("r", 8)
.attr("fill", "#007bff")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.call(d3.drag()
.on("drag", function(event) {
const x = Math.max(0, Math.min(300, event.x));
const index = Math.round(sliderScale.invert(x));
currentYearIndex = index;
d3.select(this).attr("cx", sliderScale(index));
updateMap(years[index]);
})
);
updateMap(years[0]);
return svg.node();
}