Public
Edited
Mar 19
Insert cell
Insert cell
data1 = FileAttachment("data-3@3.csv").csv({typed: true})
Insert cell
data = data1.slice(0,25)
Insert cell
function TwoChartInfluenza(data) {
const width = 1200;
const height = 700;
const margin = { top: 80, right: 300, bottom: 40, left: 80 };

// Make the first (top) chart taller
const topChartHeight = 300;
const gap = 30;
const bottomChartHeight = 250;
const innerWidth = width - margin.left - margin.right;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

// Introductory text at the top
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Click on a bar or point to see the proportions of different kinds of Influenza A in each year");

// Glow filter
const defs = svg.append("defs");
const filter = defs.append("filter")
.attr("id", "glow")
.attr("width", "300%")
.attr("height", "300%")
.attr("x", "-100%")
.attr("y", "-100%");
filter.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", "6")
.attr("result", "blurOut");
filter.append("feFlood")
.attr("flood-color", "yellow")
.attr("result", "colorOut");
filter.append("feComposite")
.attr("in", "colorOut")
.attr("in2", "blurOut")
.attr("operator", "in")
.attr("result", "glow");
const feMerge = filter.append("feMerge");
feMerge.append("feMergeNode").attr("in", "glow");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");

// Shared X Scale
const xScale = d3.scaleBand()
.domain(data.map(d => d["Year"]))
.range([0, innerWidth])
.padding(0.2);

// Helper for mouse position → year
function invertBand(mouseX) {
let closestYear = null;
let minDist = Infinity;
for (const yr of xScale.domain()) {
const center = xScale(yr) + xScale.bandwidth() / 2;
const dist = Math.abs(mouseX - center);
if (dist < minDist) {
minDist = dist;
closestYear = yr;
}
}
return closestYear;
}

// TOP CHART (Bar Chart)
const topChartG = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

const topInnerHeight = topChartHeight - margin.top;
const maxSpecimen = d3.max(data, d => d["Total Specimen"]);
const yTopScale = d3.scaleLinear()
.domain([0, maxSpecimen])
.range([topInnerHeight, 0])
.nice();

topChartG.append("g")
.attr("transform", `translate(0,${topInnerHeight})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d")));

topChartG.append("g")
.call(d3.axisLeft(yTopScale).tickFormat(d3.format(",")));

// Title for top chart
topChartG.append("text")
.attr("x", innerWidth / 2)
.attr("y", -5)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Number of Specimens Tested vs. Positive by Year in the United States");

// Y Label for top chart
topChartG.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -topInnerHeight / 2)
.attr("y", -55)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Number of People");

// X axis label for top chart
topChartG.append("text")
.attr("x", innerWidth / 2)
.attr("y", topInnerHeight + 35)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Year");

const barGroups = topChartG.selectAll(".barGroup")
.data(data)
.join("g")
.attr("class", "barGroup")
.attr("transform", d => `translate(${xScale(d["Year"])},0)`)
.on("click", function(event, d) {
topChartG.selectAll(".barGroup").selectAll("rect")
.attr("filter", null);
bottomChartG.selectAll(".linePoint")
.attr("filter", null);
d3.select(this).selectAll("rect")
.attr("filter", "url(#glow)");
updatePie(d);
});

barGroups.append("rect")
.attr("fill", "#ccc")
.attr("width", xScale.bandwidth())
.attr("y", d => yTopScale(d["Total Specimen"]))
.attr("height", d => topInnerHeight - yTopScale(d["Total Specimen"]));

barGroups.append("rect")
.attr("fill", "red")
.attr("width", xScale.bandwidth())
.attr("y", d => yTopScale(d["Number of Positive Cases"]))
.attr("height", d => topInnerHeight - yTopScale(d["Number of Positive Cases"]));

// BOTTOM CHART (Line Chart)
const bottomChartOffset = margin.top + topChartHeight + gap;
const bottomChartG = svg.append("g")
.attr("transform", `translate(${margin.left}, ${bottomChartOffset})`);

const bottomInnerHeight = bottomChartHeight - margin.bottom;
const maxPerc = d3.max(data, d => d["Percentage"]);
const yBottomScale = d3.scaleLinear()
.domain([0, maxPerc])
.range([bottomInnerHeight, 0])
.nice();

bottomChartG.append("g")
.attr("transform", `translate(0,${bottomInnerHeight})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d")));

bottomChartG.append("g")
.call(d3.axisLeft(yBottomScale).tickFormat(d3.format(".0%")));

bottomChartG.append("text")
.attr("x", innerWidth / 2)
.attr("y", -5)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Percentage Positive by Year");

// Title
bottomChartG.append("text")
.attr("x", innerWidth / 2)
.attr("y", -5)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Percentage Positive by Year");
bottomChartG.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -bottomInnerHeight / 2)
.attr("y", -55)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Positivity Rate");

bottomChartG.append("text")
.attr("x", innerWidth / 2)
.attr("y", bottomInnerHeight + 35)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Year");

const lineGenerator = d3.line()
.x(d => xScale(d["Year"]) + xScale.bandwidth() / 2)
.y(d => yBottomScale(d["Percentage"]));

bottomChartG.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("d", lineGenerator);

bottomChartG.append("line")
.attr("x1", 0)
.attr("x2", innerWidth)
.attr("y1", yBottomScale(0.10))
.attr("y2", yBottomScale(0.10))
.attr("stroke", "black")
.attr("stroke-dasharray", "8,4");

bottomChartG.selectAll(".linePoint")
.data(data)
.join("circle")
.attr("class", "linePoint")
.attr("cx", d => xScale(d["Year"]) + xScale.bandwidth() / 2)
.attr("cy", d => yBottomScale(d["Percentage"]))
.attr("r", 3)
.attr("fill", "red")
.on("click", function(event, d) {
bottomChartG.selectAll(".linePoint")
.attr("filter", null);
topChartG.selectAll(".barGroup").selectAll("rect")
.attr("filter", null);
d3.select(this)
.attr("filter", "url(#glow)");
updatePie(d);
});

const totalChartHeight = topChartHeight + gap + bottomChartHeight - margin.bottom;
const hoverLine = svg.append("line")
.attr("stroke", "#000")
.attr("stroke-dasharray", "3,3")
.style("display", "none")
.attr("y1", margin.top)
.attr("y2", margin.top + totalChartHeight);

const mainTooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("padding", "8px")
.style("background", "rgba(255, 255, 255, 0.9)")
.style("border", "1px solid #ccc")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("display", "none");

const overlay = svg.append("rect")
.attr("x", margin.left)
.attr("y", margin.top)
.attr("width", innerWidth)
.attr("height", totalChartHeight)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("mousemove", (event) => {
const [mx] = d3.pointer(event, svg.node());
const xPos = mx - margin.left;
const hoveredYear = invertBand(xPos);
if (!hoveredYear) return;
const lineX = xScale(hoveredYear) + xScale.bandwidth() / 2 + margin.left;
hoverLine.style("display", null)
.attr("x1", lineX)
.attr("x2", lineX);
const row = data.find(d => d["Year"] === hoveredYear);
if (!row) return;
mainTooltip
.style("display", "block")
.style("left", (event.pageX + 12) + "px")
.style("top", (event.pageY + 12) + "px")
.html(`
<div><strong>Year:</strong> ${row["Year"]}</div>
<div><strong>Total Specimen:</strong> ${d3.format(",")(row["Total Specimen"])}</div>
<div><strong>Positive Cases:</strong> ${d3.format(",")(row["Number of Positive Cases"])}</div>
<div><strong>Percentage:</strong> ${d3.format(".2%")(row["Percentage"])}</div>
`);
})
.on("mouseleave", () => {
hoverLine.style("display", "none");
mainTooltip.style("display", "none");
});
overlay.lower();

const pieCenterX = width - 155,
pieCenterY = height / 2 - 50,
pieRadius = 100;

const pieG = svg.append("g")
.attr("transform", `translate(${pieCenterX}, ${pieCenterY})`);

const colorScale = d3.scaleOrdinal()
.domain(["H1N1", "H1", "H3", "H5", "Subtyping not performed"])
.range(d3.schemeCategory10);

const pie = d3.pie()
.sort(null)
.value(d => d.value);

const arcGen = d3.arc()
.innerRadius(0)
.outerRadius(pieRadius);

const pieTooltip = d3.select("body").append("div")
.attr("class", "pieTooltip")
.style("position", "absolute")
.style("padding", "6px 8px")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "#fff")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("display", "none");

function getSubtypeData(row) {
return [
{ label: "H1N1", value: row["H1N1"] || 0 },
{ label: "H1", value: row["H1"] || 0 },
{ label: "H3", value: row["H3"] || 0 },
{ label: "H5", value: row["H5"] || 0 },
{ label: "Subtyping not performed", value: row["Subtyping not performed or Unable to Subtype"] || 0 }
];
}

function updatePie(row) {
const pieData = pie(getSubtypeData(row));

const arcs = pieG.selectAll(".arcSlice")
.data(pieData, d => d.data.label);

arcs.enter().append("path")
.attr("class", "arcSlice")
.attr("fill", d => colorScale(d.data.label))
.attr("stroke", "#fff")
.attr("stroke-width", 1)
.on("mouseover", (event, d) => {
const total = d3.sum(pieData, d => d.data.value);
const perc = total ? d.data.value / total : 0;
pieTooltip.style("display", "block")
.html(`Year: ${row["Year"]}<br>Subtype: ${d.data.label}<br>Percentage: ${d3.format(".2%")(perc)}`);
})
.on("mousemove", (event) => {
pieTooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY + 10) + "px");
})
.on("mouseleave", () => {
pieTooltip.style("display", "none");
})
.merge(arcs)
.transition()
.duration(500)
.attrTween("d", function(d) {
const i = d3.interpolate(this._current || { startAngle: 0, endAngle: 0 }, d);
this._current = i(0);
return t => arcGen(i(t));
});

arcs.exit().remove();

const labels = pieG.selectAll(".pieLabel")
.data(pieData, d => d.data.label);

labels.enter().append("text")
.attr("class", "pieLabel")
.merge(labels)
.transition()
.duration(500)
.attr("transform", d => `translate(${arcGen.centroid(d)})`)
.attr("text-anchor", "middle")
.attr("fill", "white")
.style("font-size", "13px")
.text(d => {
const total = d3.sum(pieData, d => d.data.value);
const perc = total ? d.data.value / total : 0;
return perc < 0.12 ? "" : d3.format(".2%")(perc);
});

labels.exit().remove();

let pieTitle = svg.selectAll(".pieTitle").data([null]);
pieTitle = pieTitle.enter().append("text")
.attr("class", "pieTitle")
.attr("text-anchor", "middle")
.attr("x", pieCenterX)
.attr("y", pieCenterY - pieRadius - 20)
.style("font-size", "16px")
.style("font-weight", "bold")
.merge(pieTitle);

pieTitle.text("Influenza A Subtypes in " + row["Year"])
.style("display", "block");

let legend = svg.selectAll(".pieLegend").data([null]);
legend = legend.enter().append("g")
.attr("class", "pieLegend")
.attr("transform", `translate(${pieCenterX - pieRadius}, ${pieCenterY + pieRadius + 20})`)
.merge(legend);

legend.selectAll("*").remove();

const subtypes = colorScale.domain();
subtypes.forEach((subtype, i) => {
const legendRow = legend.append("g")
.attr("transform", `translate(0, ${i * 20})`);
legendRow.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", colorScale(subtype));
legendRow.append("text")
.attr("x", 18)
.attr("y", 10)
.style("font-size", "12px")
.text(subtype);
});
}

return svg.node();
}

Insert cell
chart = TwoChartInfluenza(data)
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