chartDynamic = {
const margin = {top: 70, right: 200, bottom: 50, left: 60};
const width = 800;
const height = 500;
const container = html`<div style="position: relative;"></div>`;
const ageBins = [
{ label: "30–39", min: 30, max: 39 },
{ label: "40–49", min: 40, max: 49 },
{ label: "50–59", min: 50, max: 59 },
{ label: "60–69", min: 60, max: 69 },
{ label: "70–79", min: 70, max: 79 },
{ label: "80+", min: 80, max: 120 }
];
const selectAge = html`<select style="position:absolute; top:10px; left:10px; font-size:14px; z-index:10;">
${ageBins.map(bin => html`<option value="${bin.label}">${bin.label}</option>`)}
</select>`;
const colorBy = html`<select style="position:absolute; top:10px; left:140px; font-size:14px; z-index:10;">
<option value="Status">Color by Status</option>
<option value="Race">Color by Race</option>
</select>`;
container.appendChild(selectAge);
container.appendChild(colorBy);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
container.appendChild(svg.node());
// Axes: x = Grade, y = Survival
const x = d3.scaleLinear()
.domain([1, 3]) // Grade range
.range([margin.left, width - margin.right]);
const y = d3.scaleLinear()
.domain(d3.extent(breastCancerData, d => +d["Survival Months"]))
.nice()
.range([height - margin.bottom, margin.top]);
const r = d3.scaleSqrt()
.domain(d3.extent(breastCancerData, d => +d["Tumor Size"]))
.range([3, 25]);
const colorStatus = d3.scaleOrdinal()
.domain(["Alive", "Dead"])
.range(["#1f77b4", "#d62728"]);
const colorRace = d3.scaleOrdinal()
.domain([...new Set(breastCancerData.map(d => d["Race"]))])
.range(d3.schemeCategory10);
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(3).tickFormat(d => `Grade ${d}`))
.append("text")
.attr("x", width - margin.right)
.attr("y", -10)
.attr("fill", "black")
.attr("text-anchor", "end")
.text("Grade");
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -margin.top)
.attr("y", 15)
.attr("fill", "black")
.attr("text-anchor", "end")
.text("Survival Months");
const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right + 20}, ${margin.top})`);
function updateLegend(mode) {
legend.selectAll("*").remove();
const domain = mode === "Status" ? colorStatus.domain() : colorRace.domain();
const scale = mode === "Status" ? colorStatus : colorRace;
domain.forEach((label, i) => {
legend.append("circle")
.attr("cx", 0)
.attr("cy", i * 20)
.attr("r", 6)
.attr("fill", scale(label));
legend.append("text")
.attr("x", 12)
.attr("y", i * 20 + 5)
.text(label)
.style("font-size", "12px")
.attr("alignment-baseline", "middle");
});
}
function updateChart(ageLabel, colorMode) {
const bin = ageBins.find(b => b.label === ageLabel);
const data = breastCancerData.filter(d =>
+d["Age"] >= bin.min &&
+d["Age"] <= bin.max &&
+d["Tumor Size"] > 0 &&
+d["Survival Months"] > 0 &&
+d["Grade"] >= 1 &&
+d["Grade"] <= 3
);
const colorScale = colorMode === "Status" ? colorStatus : colorRace;
const circles = svg.selectAll("circle.bubble")
.data(data, d => d.ID || d["Tumor Size"] + d["Age"]);
const t = svg.transition().duration(800);
circles.enter()
.append("circle")
.attr("class", "bubble")
.attr("cx", d => x(+d["Grade"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", 0)
.attr("fill", d => colorScale(d[colorMode]))
.attr("opacity", 0.7)
.append("title")
.text(d => `Age: ${d.Age}\nTumor Size: ${d["Tumor Size"]}`)
.transition(t)
.attr("r", d => r(+d["Tumor Size"]));
circles.transition(t)
.attr("cx", d => x(+d["Grade"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", d => r(+d["Tumor Size"]))
.attr("fill", d => colorScale(d[colorMode]));
circles.exit()
.transition().duration(500)
.attr("r", 0)
.remove();
updateLegend(colorMode);
}
// Initial draw
updateChart(selectAge.value, colorBy.value);
// Event listeners
selectAge.addEventListener("change", () => {
updateChart(selectAge.value, colorBy.value);
});
colorBy.addEventListener("change", () => {
updateChart(selectAge.value, colorBy.value);
});
return container;
}