Public
Edited
May 8
Insert cell
Insert cell
breastCancerData = FileAttachment("Breast_Cancer (1).csv").csv({typed: true})

Insert cell
Object.keys(breastCancerData[0])

Insert cell
breastCancerData.slice(0, 5)

Insert cell
filteredData = breastCancerData.filter(d =>
d["Tumor Size"] != null &&
d["Survival Months"] != null &&
d["Reginol Node Positive"] != null &&
d["Status"] !== ""
)

Insert cell
Plot.plot({
marks: [
Plot.dot(filteredData, {
x: d => +d["Tumor Size"],
y: d => +d["Survival Months"],
r: d => +d["Reginol Node Positive"] * 2,
fill: d => d.Status,
title: d => `Age: ${d.Age}\nGrade: ${d.Grade}`
})
],
width: 700,
height: 500,
x: {label: "Tumor Size (mm)"},
y: {label: "Survival Months"},
color: {legend: true}
})

Insert cell
viewof selectedAge = Inputs.range([30, 90], {step: 1, label: "Filter by Age"})

Insert cell
filteredAgeData = breastCancerData.filter(d =>
+d.Age === selectedAge &&
+d["Tumor Size"] > 0 &&
+d["Survival Months"] > 0 &&
+d["Reginol Node Positive"] >= 0 &&
d["Status"] !== ""
)

Insert cell
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>`;

// Dropdown for age bins
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>`;

// Toggle between color modes
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;
}

Insert cell
chartRefined = {
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;">
${ageBins.map(bin => html`<option value="${bin.label}">${bin.label}</option>`)}
</select>`;

const toggleColorRace = html`<label style="position:absolute; top:10px; left:160px;">
<input type="checkbox" checked style="vertical-align:middle;" />
<span style="font-size:14px;">Color by Race</span>
</label>`;

container.appendChild(selectAge);
container.appendChild(toggleColorRace);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

container.appendChild(svg.node());

const x = d3.scaleLinear()
.domain(d3.extent(breastCancerData, d => +d["Reginol Node Positive"]))
.nice()
.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 colorRace = d3.scaleOrdinal()
.domain([...new Set(breastCancerData.map(d => d["Race"]))])
.range(d3.schemeCategory10);

const colorSolid = "#888";

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("x", width - margin.right)
.attr("y", -10)
.attr("fill", "black")
.attr("text-anchor", "end")
.text("Regional Node Positive");

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(showRace) {
legend.selectAll("*").remove();
if (!showRace) return;

colorRace.domain().forEach((label, i) => {
legend.append("circle")
.attr("cx", 0)
.attr("cy", i * 20)
.attr("r", 6)
.attr("fill", colorRace(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, showRaceColor) {
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["Reginol Node Positive"] >= 0
);

const t = svg.transition().duration(800);

const circles = svg.selectAll("circle.bubble")
.data(data, d => d.ID || d["Tumor Size"] + d["Age"]);

circles.enter()
.append("circle")
.attr("class", "bubble")
.attr("cx", d => x(+d["Reginol Node Positive"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", 0)
.attr("fill", d => showRaceColor ? colorRace(d["Race"]) : colorSolid)
.attr("opacity", 0.7)
.append("title")
.text(d => `Age: ${d.Age}\nGrade: ${d.Grade}\nStatus: ${d.Status}\nEstrogen: ${d["Estrogen Status"]}\nProgesterone: ${d["Progesterone Status"]}`)
.transition(t)
.attr("r", d => r(+d["Tumor Size"]));

circles.transition(t)
.attr("cx", d => x(+d["Reginol Node Positive"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", d => r(+d["Tumor Size"]))
.attr("fill", d => showRaceColor ? colorRace(d["Race"]) : colorSolid);

circles.exit()
.transition().duration(500)
.attr("r", 0)
.remove();

updateLegend(showRaceColor);
}

// Initial draw
updateChart(selectAge.value, toggleColorRace.querySelector("input").checked);

// Event listeners
selectAge.addEventListener("change", () => {
updateChart(selectAge.value, toggleColorRace.querySelector("input").checked);
});

toggleColorRace.querySelector("input").addEventListener("change", () => {
updateChart(selectAge.value, toggleColorRace.querySelector("input").checked);
});

return container;
}

Insert cell
chartRefinedSlider = {
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 ageExtent = d3.extent(breastCancerData, d => +d.Age);
const slider = Inputs.range(ageExtent, {
label: "Filter by Age",
step: 1,
value: 50
});

const toggleColorRace = html`<label style="margin-left: 20px;">
<input type="checkbox" checked />
<span style="font-size:14px;">Color by Race</span>
</label>`;

const controls = html`<div style="position: absolute; top: 10px; left: 10px;"></div>`;
controls.appendChild(slider);
controls.appendChild(toggleColorRace);
container.appendChild(controls);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
container.appendChild(svg.node());

const x = d3.scaleLinear()
.domain(d3.extent(breastCancerData, d => +d["Reginol Node Positive"]))
.nice()
.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 colorRace = d3.scaleOrdinal()
.domain([...new Set(breastCancerData.map(d => d["Race"]))])
.range(d3.schemeCategory10);
const colorSolid = "#888";

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("x", width - margin.right)
.attr("y", -10)
.attr("fill", "black")
.attr("text-anchor", "end")
.text("Regional Node Positive");

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(showRace) {
legend.selectAll("*").remove();
if (!showRace) return;

colorRace.domain().forEach((label, i) => {
legend.append("circle")
.attr("cx", 0)
.attr("cy", i * 20)
.attr("r", 6)
.attr("fill", colorRace(label));

legend.append("text")
.attr("x", 12)
.attr("y", i * 20 + 5)
.text(label)
.style("font-size", "12px")
.attr("alignment-baseline", "middle");
});
}

function updateChart(ageValue, showRaceColor) {
const data = breastCancerData.filter(d =>
+d["Age"] === ageValue &&
+d["Tumor Size"] > 0 &&
+d["Survival Months"] > 0 &&
+d["Reginol Node Positive"] >= 0
);

const t = svg.transition().duration(800);

const circles = svg.selectAll("circle.bubble")
.data(data, d => d.ID || d["Tumor Size"] + d["Age"]);

circles.enter()
.append("circle")
.attr("class", "bubble")
.attr("cx", d => x(+d["Reginol Node Positive"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", 0)
.attr("fill", d => showRaceColor ? colorRace(d["Race"]) : colorSolid)
.attr("opacity", 0.7)
.append("title")
.text(d => `Age: ${d.Age}\nGrade: ${d.Grade}\nStatus: ${d.Status}\nEstrogen: ${d["Estrogen Status"]}\nProgesterone: ${d["Progesterone Status"]}`)
.transition(t)
.attr("r", d => r(+d["Tumor Size"]));

circles.transition(t)
.attr("cx", d => x(+d["Reginol Node Positive"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", d => r(+d["Tumor Size"]))
.attr("fill", d => showRaceColor ? colorRace(d["Race"]) : colorSolid);

circles.exit()
.transition().duration(500)
.attr("r", 0)
.remove();

updateLegend(showRaceColor);
}

// Initial render
updateChart(slider.value, toggleColorRace.querySelector("input").checked);

slider.addEventListener("input", () => {
updateChart(slider.value, toggleColorRace.querySelector("input").checked);
});

toggleColorRace.querySelector("input").addEventListener("change", () => {
updateChart(slider.value, toggleColorRace.querySelector("input").checked);
});

return container;
}

Insert cell
chartFinal = {
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 validAges = breastCancerData
.map(d => +d.Age)
.filter(age => !isNaN(age))
.sort((a, b) => a - b);

const ageMin = d3.min(validAges);
const ageMax = d3.max(validAges);

const slider = Inputs.range([ageMin, ageMax], {
label: "Filter by Age",
step: 1,
value: 50
});

const toggleColorRace = html`<label style="margin-left: 20px;">
<input type="checkbox" checked />
<span style="font-size:14px;">Color by Race</span>
</label>`;

const controls = html`<div style="position: absolute; top: 10px; left: 10px;"></div>`;
controls.appendChild(slider);
controls.appendChild(toggleColorRace);
container.appendChild(controls);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
container.appendChild(svg.node());

const x = d3.scalePoint()
.domain(["1", "2", "3"])
.range([margin.left, width - margin.right])
.padding(0.5);

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([4, 40]); // exaggerate more

const colorRace = d3.scaleOrdinal()
.domain([...new Set(breastCancerData.map(d => d["Race"]))])
.range(d3.schemeCategory10);
const colorSolid = "#999";

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.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(showRace) {
legend.selectAll("*").remove();
if (!showRace) return;

colorRace.domain().forEach((label, i) => {
legend.append("circle")
.attr("cx", 0)
.attr("cy", i * 20)
.attr("r", 6)
.attr("fill", colorRace(label));

legend.append("text")
.attr("x", 12)
.attr("y", i * 20 + 5)
.text(label)
.style("font-size", "12px")
.attr("alignment-baseline", "middle");
});
}

function updateChart(ageValue, showRaceColor) {
const data = breastCancerData.filter(d =>
+d.Age === ageValue &&
+d["Tumor Size"] > 0 &&
+d["Survival Months"] > 0 &&
d["Grade"] !== "" &&
["1", "2", "3"].includes(d["Grade"].toString())
);

const t = svg.transition().duration(800);
const colorScale = showRaceColor ? colorRace : () => colorSolid;

const circles = svg.selectAll("circle.bubble")
.data(data, d => d.ID || d["Tumor Size"] + d["Age"]);

circles.enter()
.append("circle")
.attr("class", "bubble")
.attr("cx", d => x(d["Grade"].toString()))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", 0)
.attr("fill", d => colorScale(d["Race"]))
.attr("opacity", 0.75)
.append("title")
.text(d =>
`Age: ${d.Age}
Grade: ${d.Grade}
Tumor Size: ${d["Tumor Size"]}
Survival: ${d["Survival Months"]} months
Status: ${d["Status"]}
Estrogen: ${d["Estrogen Status"]}
Progesterone: ${d["Progesterone Status"]}`
)
.transition(t)
.attr("r", d => r(+d["Tumor Size"]));

circles.transition(t)
.attr("cx", d => x(d["Grade"].toString()))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", d => r(+d["Tumor Size"]))
.attr("fill", d => colorScale(d["Race"]));

circles.exit()
.transition().duration(500)
.attr("r", 0)
.remove();

updateLegend(showRaceColor);
}

updateChart(slider.value, toggleColorRace.querySelector("input").checked);

slider.addEventListener("input", () => {
updateChart(slider.value, toggleColorRace.querySelector("input").checked);
});

toggleColorRace.querySelector("input").addEventListener("change", () => {
updateChart(slider.value, toggleColorRace.querySelector("input").checked);
});

return container;
}

Insert cell
chartSplitByRace = {
const margin = {top: 40, right: 40, bottom: 40, left: 60};
const width = 300;
const height = 400;
const races = ["White", "Black", "Other"];

const container = html`<div style="display:flex; flex-direction: column;"></div>`;

const slider = Inputs.range(
d3.extent(breastCancerData, d => +d.Age).map(Math.floor),
{label: "Filter by Age", step: 1, value: 50}
);
container.appendChild(slider);

const chartsWrapper = html`<div style="display: flex; gap: 20px;"></div>`;
container.appendChild(chartsWrapper);

// Scales shared across charts
const x = d3.scalePoint()
.domain(["1", "2", "3"])
.range([margin.left, width - margin.right])
.padding(0.5);

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([4, 18]);

function createChart(race) {
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

// Axes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.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");

// Chart title
svg.append("text")
.attr("x", width / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text(race);

chartsWrapper.appendChild(svg.node());

return svg;
}

const chartMap = new Map();
races.forEach(race => {
chartMap.set(race, createChart(race));
});

function updateAll(ageValue) {
races.forEach(race => {
const svg = chartMap.get(race);
const data = breastCancerData.filter(d =>
d.Race === race &&
+d.Age === ageValue &&
+d["Survival Months"] > 0 &&
+d["Tumor Size"] > 0 &&
d["Grade"] && ["1", "2", "3"].includes(d["Grade"].toString())
);

const t = svg.transition().duration(600);

const bubbles = svg.selectAll("circle.bubble")
.data(data, d => d.ID || d.Age + d["Tumor Size"]);

bubbles.enter()
.append("circle")
.attr("class", "bubble")
.attr("cx", d => x(d["Grade"].toString()))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", 0)
.attr("fill", "#4682b4")
.attr("opacity", 0.7)
.append("title")
.text(d =>
`Age: ${d.Age}
Grade: ${d.Grade}
Tumor Size: ${d["Tumor Size"]}
Survival: ${d["Survival Months"]} months
Status: ${d["Status"]}
Estrogen: ${d["Estrogen Status"]}
Progesterone: ${d["Progesterone Status"]}`
)
.transition(t)
.attr("r", d => r(+d["Tumor Size"]));

bubbles.transition(t)
.attr("cx", d => x(d["Grade"].toString()))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", d => r(+d["Tumor Size"]));

bubbles.exit()
.transition().duration(500)
.attr("r", 0)
.remove();
});
}

updateAll(slider.value);
slider.addEventListener("input", () => updateAll(slider.value));

return container;
}

Insert cell
chartByRaceTumorX = {
const margin = {top: 40, right: 40, bottom: 40, left: 60};
const width = 300;
const height = 400;
const races = ["White", "Black", "Other"];
const raceColors = {
White: "#1f77b4",
Black: "#ff7f0e",
Other: "#2ca02c"
};

const container = html`<div style="display:flex; flex-direction: column;"></div>`;

const slider = Inputs.range(
d3.extent(breastCancerData, d => +d.Age).map(Math.floor),
{label: "Filter by Age", step: 1, value: 50}
);
container.appendChild(slider);

const chartsWrapper = html`<div style="display: flex; gap: 20px;"></div>`;
container.appendChild(chartsWrapper);

const x = d3.scaleLinear()
.domain(d3.extent(breastCancerData, d => +d["Tumor Size"]))
.nice()
.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]);

function createChart(race) {
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("x", width - margin.right)
.attr("y", -10)
.attr("fill", "black")
.attr("text-anchor", "end")
.text("Tumor Size");

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");

svg.append("text")
.attr("x", width / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text(race);

chartsWrapper.appendChild(svg.node());
return svg;
}

const chartMap = new Map();
races.forEach(race => {
chartMap.set(race, createChart(race));
});

function updateAll(ageValue) {
races.forEach(race => {
const svg = chartMap.get(race);

const data = breastCancerData.filter(d =>
d.Race === race &&
+d.Age === ageValue &&
+d["Survival Months"] > 0 &&
+d["Tumor Size"] > 0
);

const t = svg.transition().duration(600);

const bubbles = svg.selectAll("circle.bubble")
.data(data, d => d.ID || d.Age + d["Tumor Size"]);

bubbles.enter()
.append("circle")
.attr("class", "bubble")
.attr("cx", d => x(+d["Tumor Size"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", 0)
.attr("fill", raceColors[race])
.attr("opacity", 0.7)
.append("title")
.text(d =>
`Age: ${d.Age}
Grade: ${d.Grade}
Tumor Size: ${d["Tumor Size"]}
Survival: ${d["Survival Months"]} months
Status: ${d["Status"]}
Estrogen: ${d["Estrogen Status"]}
Progesterone: ${d["Progesterone Status"]}`
)
.transition(t)
.attr("r", 6); // Uniform size

bubbles.transition(t)
.attr("cx", d => x(+d["Tumor Size"]))
.attr("cy", d => y(+d["Survival Months"]));

bubbles.exit()
.transition().duration(500)
.attr("r", 0)
.remove();
});
}

updateAll(slider.value);
slider.addEventListener("input", () => updateAll(slider.value));

return container;
}

Insert cell
breastCancerData.filter(d =>
+d["Tumor Size"] > 0 &&
+d["Survival Months"] > 0 &&
["White", "Black", "Other"].includes(d.Race)
)
.slice(0, 5)

Insert cell
chartTumorVsSurvival = {
const margin = {top: 50, right: 200, bottom: 50, left: 60};
const width = 800;
const height = 500;

const svg = d3.create("svg").attr("width", width).attr("height", height);

const x = d3.scaleLinear()
.domain(d3.extent(breastCancerData, d => +d["Tumor Size"]))
.nice()
.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["Reginol Node Positive"]))
.range([3, 25]);

const color = d3.scaleOrdinal()
.domain(["White", "Black", "Other"])
.range(["#1f77b4", "#ff7f0e", "#2ca02c"]);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("x", width - margin.right)
.attr("y", -10)
.attr("text-anchor", "end")
.text("Tumor Size");

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("text-anchor", "end")
.text("Survival Months");

svg.selectAll("circle")
.data(breastCancerData)
.enter()
.append("circle")
.attr("cx", d => x(+d["Tumor Size"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", d => r(+d["Reginol Node Positive"]))
.attr("fill", d => color(d["Race"]))
.attr("opacity", 0.7)
.append("title")
.text(d =>
`Age: ${d.Age}
Grade: ${d.Grade}
Tumor Size: ${d["Tumor Size"]}
Nodes: ${d["Reginol Node Positive"]}
Survival: ${d["Survival Months"]} months
Status: ${d["Status"]}
Estrogen: ${d["Estrogen Status"]}
Progesterone: ${d["Progesterone Status"]}`
);

return svg.node();
}

Insert cell
chartTumorVsSurvivalByRace = {
const margin = {top: 40, right: 40, bottom: 40, left: 60};
const width = 300;
const height = 400;
const races = ["White", "Black", "Other"];
const raceColors = {
White: "#1f77b4",
Black: "#ff7f0e",
Other: "#2ca02c"
};

const container = html`<div style="display:flex; flex-direction: column;"></div>`;

const ageExtent = d3.extent(breastCancerData, d => +d.Age);
const slider = Inputs.range(ageExtent.map(Math.floor), {
label: "Filter by Age (±3 years)",
step: 1,
value: 50
});
container.appendChild(slider);

const chartsWrapper = html`<div style="display: flex; gap: 20px;"></div>`;
container.appendChild(chartsWrapper);

const x = d3.scaleLinear()
.domain(d3.extent(breastCancerData, d => +d["Tumor Size"]))
.nice()
.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["Reginol Node Positive"]))
.range([3, 25]);

function createChart(race) {
const svg = d3.create("svg").attr("width", width).attr("height", height);

// Axes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("x", width - margin.right)
.attr("y", -10)
.attr("fill", "black")
.attr("text-anchor", "end")
.text("Tumor Size");

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");

// Title
svg.append("text")
.attr("x", width / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text(race);

chartsWrapper.appendChild(svg.node());
return svg;
}

const chartMap = new Map();
races.forEach(race => {
chartMap.set(race, createChart(race));
});

function updateAll(ageValue) {
races.forEach(race => {
const svg = chartMap.get(race);
const data = breastCancerData.filter(d =>
d.Race === race &&
Math.abs(+d.Age - ageValue) <= 3 &&
+d["Tumor Size"] > 0 &&
+d["Survival Months"] > 0 &&
+d["Reginol Node Positive"] >= 0
);

const t = svg.transition().duration(600);

const bubbles = svg.selectAll("circle.bubble")
.data(data, d => d.ID || d.Age + d["Tumor Size"]);

bubbles.enter()
.append("circle")
.attr("class", "bubble")
.attr("cx", d => x(+d["Tumor Size"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", 0)
.attr("fill", raceColors[race])
.attr("opacity", 0.7)
.append("title")
.text(d =>
`Age: ${d.Age}
Grade: ${d.Grade}
Tumor Size: ${d["Tumor Size"]}
Nodes: ${d["Reginol Node Positive"]}
Survival: ${d["Survival Months"]} months
Status: ${d["Status"]}
Estrogen: ${d["Estrogen Status"]}
Progesterone: ${d["Progesterone Status"]}`
)
.transition(t)
.attr("r", d => r(+d["Reginol Node Positive"]));

bubbles.transition(t)
.attr("cx", d => x(+d["Tumor Size"]))
.attr("cy", d => y(+d["Survival Months"]))
.attr("r", d => r(+d["Reginol Node Positive"]));

bubbles.exit()
.transition().duration(500)
.attr("r", 0)
.remove();
});
}

updateAll(slider.value);
slider.addEventListener("input", () => updateAll(slider.value));

return container;
}

Insert cell
chartHeatmapSurvival = {
const data = breastCancerData.map(d => ({
grade: String(d.Grade).trim(),
estrogen: String(d["Estrogen Status"]).trim(),
progesterone: String(d["Progesterone Status"]).trim(),
survival: +d["Survival Months"]
}));

const groups = d3.rollup(
data,
v => d3.median(v, d => d.survival),
d => `${d.estrogen} / ${d.progesterone}`,
d => d.grade
);

const hormoneCombos = Array.from(new Set(data.map(d => `${d.estrogen} / ${d.progesterone}`))).sort();
const grades = Array.from(new Set(data.map(d => d.grade))).sort();

const matrix = hormoneCombos.map(hormoneCombo => {
return grades.map(grade => groups.get(hormoneCombo)?.get(grade) ?? null);
});

const width = 700, height = 400;
const svg = d3.create("svg").attr("width", width).attr("height", height);
const cellSize = 40;

const color = d3.scaleSequential()
.domain([20, 100])
.interpolator(d3.interpolateRdYlGn);

hormoneCombos.forEach((hc, i) => {
grades.forEach((grade, j) => {
const val = matrix[i][j];
svg.append("rect")
.attr("x", j * cellSize + 100)
.attr("y", i * cellSize + 40)
.attr("width", cellSize)
.attr("height", cellSize)
.attr("fill", val ? color(val) : "#eee");

svg.append("text")
.attr("x", j * cellSize + 120)
.attr("y", i * cellSize + 65)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("fill", "#000")
.text(val ? val.toFixed(0) : "");
});

svg.append("text")
.attr("x", 90)
.attr("y", i * cellSize + 65)
.attr("text-anchor", "end")
.attr("font-size", "11px")
.text(hc);
});

grades.forEach((grade, j) => {
svg.append("text")
.attr("x", j * cellSize + 120)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text(grade);
});

svg.append("text")
.attr("x", width / 2)
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.text("Median Survival by Grade and Hormone Status");

return svg.node();
}

Insert cell
chartGroupedBarSurvival = {
const data = breastCancerData.map(d => ({
grade: String(d.Grade).trim(),
estrogen: d["Estrogen Status"].trim(),
status: d["Status"].trim(),
survival: +d["Survival Months"]
}));

const grouped = d3.rollups(
data,
v => d3.median(v, d => d.survival),
d => d.grade,
d => d.estrogen,
d => d.status
);

const flat = [];
grouped.forEach(([grade, eGroups]) => {
eGroups.forEach(([estrogen, sGroups]) => {
sGroups.forEach(([status, medianSurvival]) => {
flat.push({grade, estrogen, status, medianSurvival});
});
});
});

const width = 800, height = 400, margin = {top: 50, right: 30, bottom: 50, left: 60};
const svg = d3.create("svg").attr("width", width).attr("height", height);

const x0 = d3.scaleBand()
.domain(flat.map(d => d.grade))
.range([margin.left, width - margin.right])
.paddingInner(0.2);

const x1 = d3.scaleBand()
.domain(["Positive", "Negative"])
.range([0, x0.bandwidth()])
.padding(0.1);

const y = d3.scaleLinear()
.domain([0, d3.max(flat, d => d.medianSurvival)]).nice()
.range([height - margin.bottom, margin.top]);

const color = d3.scaleOrdinal()
.domain(["Alive", "Dead"])
.range(["#4daf4a", "#e41a1c"]);

svg.append("g")
.selectAll("g")
.data(flat)
.join("g")
.attr("transform", d => `translate(${x0(d.grade) + x1(d.estrogen)},0)`)
.append("rect")
.attr("y", d => y(d.medianSurvival))
.attr("height", d => y(0) - y(d.medianSurvival))
.attr("width", x1.bandwidth())
.attr("fill", d => color(d.status))
.append("title")
.text(d => `${d.grade} - ${d.estrogen} - ${d.status}: ${d.medianSurvival.toFixed(1)} months`);

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x0));

svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));

svg.append("text")
.attr("x", width / 2)
.attr("y", margin.top - 30)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.text("Median Survival by Grade and Estrogen Status (Grouped by Alive/Dead)");

return svg.node();
}

Insert cell
viewof selectedAgeGroup = Inputs.radio(
["20–39", "40–49", "50–59", "60–69", "70–79", "80+"],
{label: "Select Age Group"}
)
Insert cell
chartHeatmapByAge = {
const age = selectedAgeGroup ?? ["20–39", "40–49", "50–59", "60–69", "70–79", "80+"][selectedAgeGroup];

const data = breastCancerData
.filter(d => d["Age Group"] === age)
.map(d => ({
grade: String(d.Grade).trim(),
estrogen: String(d["Estrogen Status"]).trim(),
progesterone: String(d["Progesterone Status"]).trim(),
survival: +d["Survival Months"]
}));

const groups = d3.rollup(
data,
v => d3.median(v, d => d.survival),
d => `${d.estrogen} / ${d.progesterone}`,
d => d.grade
);

const hormoneCombos = Array.from(new Set(data.map(d => `${d.estrogen} / ${d.progesterone}`))).sort();
const grades = Array.from(new Set(data.map(d => d.grade))).sort();
const matrix = hormoneCombos.map(hc => grades.map(g => groups.get(hc)?.get(g) ?? null));

const width = 700, height = 400;
const svg = d3.create("svg").attr("width", width).attr("height", height);
const cellSize = 40;

const color = d3.scaleSequential().domain([20, 100]).interpolator(d3.interpolateRdYlGn);

hormoneCombos.forEach((hc, i) => {
grades.forEach((grade, j) => {
const val = matrix[i][j];
svg.append("rect")
.attr("x", j * cellSize + 100)
.attr("y", i * cellSize + 40)
.attr("width", cellSize)
.attr("height", cellSize)
.attr("fill", val ? color(val) : "#eee");

svg.append("text")
.attr("x", j * cellSize + 120)
.attr("y", i * cellSize + 65)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.text(val ? val.toFixed(0) : "");
});
svg.append("text")
.attr("x", 90)
.attr("y", i * cellSize + 65)
.attr("text-anchor", "end")
.attr("font-size", "11px")
.text(hc);
});

grades.forEach((grade, j) => {
svg.append("text")
.attr("x", j * cellSize + 120)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text(grade);
});

svg.append("text")
.attr("x", width / 2)
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.text(`Median Survival by Grade & Hormone Status (Age Group: ${age})`);

return svg.node();
}

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