Public
Edited
Mar 12
Fork of Simple D3
2 forks
Insert cell
Insert cell
data = FileAttachment("data@3.csv").csv({typed: true})
Insert cell
// data = data_.slice(0, 52)
Insert cell
function PatientLineChart(data) {
// Create a container for the chart
const width = 800;
const height = 400;
const marginTop = 50;
const marginRight = 50;
const marginBottom = 50;
const marginLeft = 80;

// Create the SVG
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", `0 0 ${width} ${height}`);

// Create the chart group with margins
const g = svg.append("g")
.attr("transform", `translate(${marginLeft},${marginTop})`);

// Adjust dimensions for the inner drawing area
const innerWidth = width - marginLeft - marginRight;
const innerHeight = height - marginTop - marginBottom;

// Create scales
const xScale = d3.scaleLinear()
.domain([1, d3.max(data, d => d.Week)])
.range([0, innerWidth]);

const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.Sum_A)])
.range([innerHeight, 0]);

// Create line generator
const line = d3.line()
.x(d => xScale(d.Week))
.y(d => yScale(d.Sum_A));

// Add x-axis
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d")));

// Add y-axis
g.append("g")
.call(d3.axisLeft(yScale).tickFormat(d3.format(",")));

// Add line path
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);

// Add x-axis label
g.append("text")
.attr("x", innerWidth / 2)
.attr("y", innerHeight + 40)
.attr("text-anchor", "middle")
.text("Week");

// Add y-axis label
g.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -60)
.attr("text-anchor", "middle")
.text("Total Patient");

// Add title
g.append("text")
.attr("x", innerWidth / 2)
.attr("y", -20)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.text("Weekly Patient Numbers in 2010-2024");

return svg.node();
}
Insert cell
chart = PatientLineChart(data)
Insert cell
viewof numParticles = Inputs.range([100, 1000], {value: 300, step: 10, label: "Number of particles"})
Insert cell
viewof infectionRadius = Inputs.range([2, 20], {value: 8, step: 0.5, label: "Infection radius"})
Insert cell
// Define shared variables


// Create simulation class that can be reused
Simulation = class {
constructor(width, height, numParticles, infectionRadius, infectionRate, title) {
this.width = width;
this.height = height;
this.numParticles = numParticles;
this.infectionRadius = infectionRadius;
this.infectionRate = infectionRate;
this.title = title;
this.particles = [];
this.setup();
}
setup() {
this.particles = [];
for (let i = 0; i < this.numParticles; i++) {
this.particles.push(new Particle(
random(this.width),
random(this.height),
random() < 0.05,
this.infectionRate
));
}
}
draw(context) {
context.fillStyle = "#f0f0f0";
context.fillRect(0, 0, this.width, this.height);
// Draw title
context.fillStyle = "black";
context.font = "16px sans-serif";
context.fillText(this.title, this.width / 2, 20);
// Update and draw particles
for (let p of this.particles) {
p.move(this.width, this.height);
p.display(context);
p.infectOthers(this.particles, this.infectionRadius);
}
}
}




Insert cell
// Helper function for distance calculation
dist = (x1, y1, x2, y2) => Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));

Insert cell
// Helper function for random number
random = (min, max) => {
if (max === undefined) {
if (min === undefined) return Math.random();
return Math.random() * min;
}
return min + Math.random() * (max - min);
}
Insert cell
// Particle class
Particle = class {
constructor(x, y, infected, infectionRate) {
this.x = x;
this.y = y;
this.vx = random(-1.5, 1.5);
this.vy = random(-1.5, 1.5);
this.infected = infected;
this.infectionRate = infectionRate;
}
move(width, height) {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > width) this.vx *= -1;
if (this.y < 0 || this.y > height) this.vy *= -1;
}
display(context) {
context.fillStyle = this.infected ? 'red' : 'blue';
context.beginPath();
context.arc(this.x, this.y, 3, 0, Math.PI * 2);
context.fill();
}
infectOthers(particles, infectionRadius) {
if (!this.infected) return;
for (let other of particles) {
if (!other.infected && dist(this.x, this.y, other.x, other.y) < infectionRadius) {
if (Math.random() < this.infectionRate) {
other.infected = true;
}
}
}
}
}
Insert cell
// CELL - No Mask Simulation
noMaskSimulation = {
restartSimulation; // 这会创建一个依赖
const width = 300;
const height = 300;
const sim = new Simulation(width, height, numParticles, infectionRadius, 0.078, "");
const canvas = DOM.canvas(width, height);
const context = canvas.getContext("2d");
function update() {
sim.draw(context);
requestAnimationFrame(update);
}
update();
return canvas;
}
Insert cell
maskSimulation = {
// 添加依赖,使cell在restartSimulation更改时重新运行
restartSimulation; // 这会创建一个依赖
const width = 300;
const height = 300;
const sim = new Simulation(width, height, numParticles, infectionRadius, 0.0351,"");
const canvas = DOM.canvas(width, height);
const context = canvas.getContext("2d");
function update() {
sim.draw(context);
requestAnimationFrame(update);
}
update();
return canvas;
}
Insert cell
Insert cell
layout = {
return html`
<div style="display: flex; justify-content: space-between; width: 100%;">
<div style="text-align: center; width: 48%;">
<h3>NOT Wearing Mask</h3>
${noMaskSimulation}
</div>
<div style="text-align: center; width: 48%;">
<h3>Wearing Mask</h3>
${maskSimulation}
</div>
</div>
`;
}
Insert cell
Insert cell
// First, let's parse the CSV data for both datasets
raceData = {
const data = `Race/Ethnicity,2019-20,2020-21,2021-22,2022-23
Hispanic or Latino,47.7,41.5,42.5,46
2 or more races (Not Hispanic or Latino),56,44.5,42,57.5
American Indian or Alaska Native only (Not Hispanic or Latino),48.7,46.5,55.6,54.4
Asian only (Not Hispanic or Latino),57.3,59.5,61.1,63
Black or African American only (Not Hispanic or Latino),41.7,37.2,37.5,44.9
White only (Not Hispanic or Latino),54.2,54.4,52.3,51.6`;

return d3.csvParse(data).map(d => ({
group: d["Race/Ethnicity"],
"2019-20": +d["2019-20"],
"2020-21": +d["2020-21"],
"2021-22": +d["2021-22"],
"2022-23": +d["2022-23"]
}));
}



// Create charts


Insert cell
ageData = {
const data = `Age Groups,2019-20,2020-21,2021-22,2022-23
6 months-4 years,67.3,59.3,53,57.8
5-17 years,54.8,44.8,46.3,47.7
18-64 years,43.3,42.4,41.7,43.3
65 years and over,73.9,77.1,75.4,75.1`;

return d3.csvParse(data).map(d => ({
group: d["Age Groups"],
"2019-20": +d["2019-20"],
"2020-21": +d["2020-21"],
"2021-22": +d["2021-22"],
"2022-23": +d["2022-23"]
}));
}
Insert cell
// Function to create a line chart
function createVaccinationChart(data, title) {
// Chart dimensions
const width = 1000;
const height = 500;
const marginTop = 50;
const marginRight = 400;
const marginBottom = 80; // Increased for bold axis title
const marginLeft = 60;
// Create SVG
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;");
// Add title
svg.append("text")
.attr("x", width / 2)
.attr("y", marginTop / 2)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text(`Persons Vaccinated Against Seasonal Influenza by ${title} (2019-2023)`);
// Parse years and format them for x-axis
const years = ["2019-20", "2020-21", "2021-22", "2022-23"];
// Set up scales with padding
const x = d3.scalePoint()
.domain(years)
.range([marginLeft, width - marginRight])
.padding(0.5); // Add padding to create space on both sides
const y = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
// Define line generator
const line = d3.line()
.x(d => x(d.year))
.y(d => y(d.value));
// Process data for line generation
const processedData = data.map(d => {
return {
group: d.group,
values: years.map(year => ({
year,
value: d[year]
}))
};
});
// Create color scale
const color = d3.scaleOrdinal()
.domain(processedData.map(d => d.group))
.range(d3.schemeCategory10);
// Add X-axis
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
// Add bold X-axis title in the middle
svg.append("text")
.attr("x", width / 2)
.attr("y", height - marginBottom + 40)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Year");
// Add Y-axis
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).tickFormat(d => d + "%"));
// Add bold Y-axis title in the middle
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -(height - marginBottom + marginTop) / 2)
.attr("y", marginLeft - 40)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Percentage Vaccinated");
// Add target line (70%)
svg.append("line")
.attr("x1", marginLeft)
.attr("y1", y(70))
.attr("x2", width - marginRight)
.attr("y2", y(70))
.attr("stroke", "gray")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "5,5");
// Add target label
svg.append("text")
.attr("x", width - marginRight + 10)
.attr("y", y(70) + 4)
.attr("fill", "gray")
.text("Target: 70%");
// Create groups for lines and points
const groups = svg.append("g")
.selectAll("path")
.data(processedData)
.join("g");
// Add lines
const lines = groups.append("path")
.attr("fill", "none")
.attr("stroke", d => color(d.group))
.attr("stroke-width", 2)
.attr("d", d => line(d.values))
.attr("data-group", d => d.group);
// Add points
const points = groups.selectAll("circle")
.data(d => d.values.map(v => ({...v, group: d.group})))
.join("circle")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.value))
.attr("r", 5)
.attr("fill", d => color(d.group))
.attr("stroke", "white")
.attr("stroke-width", 1)
.attr("data-group", d => d.group);
// Create tooltip div
const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("background-color", "white")
.style("border", "1px solid #ddd")
.style("border-radius", "4px")
.style("padding", "10px")
.style("box-shadow", "0 2px 4px rgba(0,0,0,0.1)")
.style("font-size", "12px")
.style("visibility", "hidden");
// Add interaction
points
.on("mouseover", function(event, d) {
// Show tooltip
tooltip
.style("visibility", "visible")
.html(`<strong>Period:</strong> ${d.year}<br>
<strong>Group:</strong> ${d.group}<br>
<strong>Percentage:</strong> ${d.value}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
// Highlight the current group
const currentGroup = d.group;
lines.style("opacity", l => l.group === currentGroup ? 1 : 0.2);
points.style("opacity", p => p.group === currentGroup ? 1 : 0.2);
})
.on("mouseout", function() {
// Hide tooltip
tooltip.style("visibility", "hidden");
// Restore all lines and points
lines.style("opacity", 1);
points.style("opacity", 1);
});
// Move legend to bottom right
const legendX = width - marginRight + 20;
const legendY = height - marginBottom - (processedData.length * 20) - 10;
const legend = svg.append("g")
.attr("transform", `translate(${legendX}, ${legendY})`);
processedData.forEach((d, i) => {
const legendRow = legend.append("g")
.attr("transform", `translate(0, ${i * 20})`);
legendRow.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", color(d.group));
legendRow.append("text")
.attr("x", 20)
.attr("y", 10)
.text(d.group);
});
return svg.node();
}
Insert cell
raceChart = createVaccinationChart(raceData, "Race/Ethnicity")
Insert cell
ageChart = createVaccinationChart(ageData, "Age Group")
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