async function influenzaChart() {
const margin = { top: 100, right: 30, bottom: 80, left: 60 };
const width = 1000 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
const rawData = await FileAttachment("sum_total_influenza_by_week_age@1.csv").csv({ typed: true });
const processedData = rawData.map(d => ({
Week: +d.Week,
AgeGroup: d["Age Group"],
Cases: +d.Sum_Total_Influenza_A
}));
const ageGroups = d3.group(processedData, d => d.AgeGroup);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width + margin.left + margin.right, height + margin.top + margin.bottom])
.attr("style", "max-width: 100%; height: auto;");
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("Influenza A Cases Surveillance 2009–2024: Weekly Totals by Age Group");
const x = d3.scaleLinear()
.domain([0.5, 52.5])
.range([margin.left, width + margin.left]);
const y = d3.scaleLinear()
.domain([0, d3.max(processedData, d => d.Cases)]).nice()
.range([height + margin.top, margin.top]);
// Add season backgrounds
const seasons = [
{ name: "Winter", start: 48, end: 52.5, color: "lightblue" },
{ name: "Winter", start: 1, end: 8.99999, color: "lightblue" },
{ name: "Spring", start: 9, end: 21.99999, color: "lightgreen" },
{ name: "Summer", start: 22, end: 34.99999, color: "lightpink" },
{ name: "Fall", start: 35, end: 47.99999, color: "moccasin" }
];
svg.selectAll(".season")
.data(seasons)
.enter().append("rect")
.attr("class", "season")
.attr("x", d => x(d.start))
.attr("width", d => x(d.end) - x(d.start))
.attr("y", margin.top)
.attr("height", height)
.attr("fill", d => d.color)
.attr("opacity", 0.2);
// Create line generator
const line = d3.line()
.x(d => x(d.Week))
.y(d => y(d.Cases));
// Add lines
const color = d3.scaleOrdinal(d3.schemeCategory10);
const ageGroupNames = ["0-4 yr", "5-24 yr", "25-64 yr", "65+ yr"];
ageGroupNames.forEach(ageGroup => {
svg.append("path")
.datum(ageGroups.get(ageGroup))
.attr("fill", "none")
.attr("stroke", color(ageGroup))
.attr("stroke-width", 2)
.attr("d", line);
});
// Add highest and lowest season points
// ageGroupNames.forEach(ageGroup => {
// const groupData = ageGroups.get(ageGroup);
// seasons.forEach(season => {
// const seasonData = groupData.filter(d =>
// d.Week >= Math.floor(season.start) && d.Week <= Math.floor(season.end)
// );
// if (seasonData.length) {
// const max = d3.max(seasonData, d => d.Cases);
// const min = d3.min(seasonData, d => d.Cases);
// seasonData.forEach(d => {
// if (d.Cases === max || d.Cases === min) {
// svg.append("circle")
// .attr("cx", x(d.Week))
// .attr("cy", y(d.Cases))
// .attr("r", 4)
// .attr("fill", color(ageGroup))
// .attr("stroke", "white")
// .attr("stroke-width", 1.5);
// }
// });
// }
// });
// });
// Add axes
svg.append("g")
.attr("transform", `translate(0,${height + margin.top})`)
.call(d3.axisBottom(x).ticks(11).tickFormat(d => (d % 5 === 0 ? d : "")));
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
// Bold x and y axis titles
svg.append("text")
.attr("text-anchor", "middle")
.attr("transform", `translate(${margin.left - 40}, ${height / 2 + margin.top}) rotate(-90)`)
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Total Influenza A Cases");
svg.append("text")
.attr("x", width / 2 + margin.left)
.attr("y", height + margin.top + 40)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text("Week");
// Tooltip and vertical line
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("padding", "8px")
.style("border", "1px solid #ddd")
.style("background", "white")
.style("display", "none");
const verticalLine = svg.append("line")
.attr("stroke", "#000")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "4 2")
.style("display", "none");
svg.on("mousemove", (event) => {
const [mx] = d3.pointer(event);
const week = Math.round(x.invert(mx));
const season = seasons.find(s => week >= Math.floor(s.start) && week <= Math.floor(s.end));
const currentData = processedData.filter(d => d.Week === week);
if (currentData.length > 0) {
verticalLine
.attr("x1", x(week))
.attr("x2", x(week))
.attr("y1", margin.top)
.attr("y2", height + margin.top)
.style("display", "block");
tooltip.style("display", "block")
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY - 20}px`)
.html(`
<strong>Week:</strong> ${week}<br>
<strong>Season:</strong> ${season ? season.name : "Unknown"}<br>
${currentData.map(d => `<strong>${d.AgeGroup}:</strong> ${d3.format(",")(d.Cases)}`).join("<br>")}
`);
}
}).on("mouseleave", () => {
verticalLine.style("display", "none");
tooltip.style("display", "none");
});
// Combined Season Legend
const seasonLegend = svg.append("g")
.attr("transform", `translate(${width - margin.left - 635},${height + margin.top + 50})`);
const uniqueSeasons = [
{ name: "Winter", color: "lightblue" },
{ name: "Spring", color: "lightgreen" },
{ name: "Summer", color: "lightpink" },
{ name: "Fall", color: "moccasin" }
];
uniqueSeasons.forEach((season, i) => {
seasonLegend.append("rect")
.attr("x", i * 100)
.attr("width", 19)
.attr("height", 19)
.attr("fill", season.color)
.attr("opacity", 0.2);
seasonLegend.append("text")
.attr("x", i * 100 + 24)
.attr("y", 14)
.text(season.name);
});
const legend = svg.append("g")
.attr("transform", `translate(${(width - ageGroupNames.length * 120) / 2}, 60)`);
ageGroupNames.forEach((ageGroup, i) => {
legend.append("rect")
.attr("x", i * 120)
.attr("width", 19)
.attr("height", 19)
.attr("fill", color(ageGroup));
legend.append("text")
.attr("x", i * 120 + 24)
.attr("y", 14)
.style("font-size", "12px")
.text(ageGroup);
});
return svg.node();
}