Public
Edited
Apr 30
Insert cell
Insert cell
Insert cell
// Load the Titanic dataset from an attached CSV file
data = FileAttachment("tested.csv").csv({typed: true})
Insert cell
Insert cell
Insert cell
// Grouping survivors into age buckets of 10 years each
ageBuckets = d3.bin()
.value(d => d.Age)
.thresholds(d3.range(0, 90, 10))(
data.filter(d => d.Survived === 1 && d.Age != null)
)
Insert cell
Insert cell
// For each age bucket, an object with the count and midpoint for plotting
survivalByAgeGroup = ageBuckets.map(bin => ({
ageRange: `${bin.x0}-${bin.x1}`,
count: bin.length,
midpoint: (bin.x0 + bin.x1) / 2
}))
Insert cell
Insert cell
// line area chart
chart1 = {
const width = 600, height = 320;
const margin = { top: 40, right: 20, bottom: 50, left: 50 };
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const x = d3.scaleLinear()
.domain(d3.extent(survivalByAgeGroup, d => d.midpoint))
.range([margin.left, width - margin.right]);

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

// area
const area = d3.area()
.x(d => x(d.midpoint))
.y0(y(0))
.y1(d => y(d.count));

// line
const line = d3.line()
.x(d => x(d.midpoint))
.y(d => y(d.count));

// axes
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(9).tickFormat(d => `${d}s`));

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

// X-axis label
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 10)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text("Age (in years)");

// Y-axis label
svg.append("text")
.attr("x", -height / 2)
.attr("y", 15)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text("Number of Survivors");

// title
svg.append("text")
.attr("x", width / 2)
.attr("y", margin.top / 2)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Survivor Count by Age Range");

svg.append("path")
.datum(survivalByAgeGroup)
.attr("fill", "lightsteelblue")
.attr("d", area);

svg.append("path")
.datum(survivalByAgeGroup)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);

return svg.node();
}

Insert cell
Insert cell
Insert cell
// Preparing data as nodes and links for class to sex to survival
sankeyData = (() => {
const nodes = [];
const links = [];

const classes = ["1st", "2nd", "3rd"];
const sexes = ["male", "female"];
const outcomes = ["survived", "died"];

const nodeIndex = {};
let index = 0;

for (const cls of classes) {
nodeIndex[cls] = index++;
nodes.push({ name: cls });
}
for (const sex of sexes) {
nodeIndex[sex] = index++;
nodes.push({ name: sex });
}
for (const out of outcomes) {
nodeIndex[out] = index++;
nodes.push({ name: out });
}

const dataMap = d3.rollups(
data,
v => v.length,
d => `class-${d.Pclass}`,
d => d.Sex,
d => d.Survived === 1 ? "survived" : "died"
);

for (const [clsKey, sexList] of dataMap) {
const cls = { "class-1": "1st", "class-2": "2nd", "class-3": "3rd" }[clsKey];
for (const [sex, outcomes] of sexList) {
const total = d3.sum(outcomes, d => d[1]);
links.push({ source: nodeIndex[cls], target: nodeIndex[sex], value: total });
for (const [outcome, count] of outcomes) {
links.push({ source: nodeIndex[sex], target: nodeIndex[outcome], value: count });
}
}
}

return { nodes, links };
})();

Insert cell
// Importign D3 Sankey module
d3sankey = require("d3-sankey@0.12.3")

Insert cell
Insert cell
// Drawing Sankey diagram with improved labeling and layout
chart2 = {
const { nodes, links } = sankeyData;

const sankeyLayout = d3sankey.sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 30], [699, 370]]); // allow space for title

const { nodes: layoutNodes, links: layoutLinks } = sankeyLayout({
nodes: nodes.map(d => Object.assign({}, d)),
links: links.map(d => Object.assign({}, d))
});

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

// title
svg.append("text")
.attr("x", 700 / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Titanic Passenger Flow: Class → Gender → Survival");

// Sankey links (flow paths)
svg.append("g")
.selectAll("path")
.data(layoutLinks)
.join("path")
.attr("d", d3sankey.sankeyLinkHorizontal())
.attr("stroke", "#69b3a2")
.attr("stroke-width", d => Math.max(1, d.width))
.attr("fill", "none")
.attr("stroke-opacity", 0.5);

// nodes
svg.append("g")
.selectAll("rect")
.data(layoutNodes)
.join("rect")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("height", d => d.y1 - d.y0)
.attr("width", d => d.x1 - d.x0)
.attr("fill", d => {
if (["1st", "2nd", "3rd"].includes(d.name)) return "#5DADE2"; // class = blue
if (["male", "female"].includes(d.name)) return "#F5B041"; // gender = orange
return d.name === "survived" ? "#58D68D" : "#EC7063"; // outcome = green/red
});

// node labels
svg.append("g")
.selectAll("text")
.data(layoutNodes)
.join("text")
.attr("x", d => d.x0 < 350 ? d.x1 + 6 : d.x0 - 6) // adjust based on side
.attr("y", d => (d.y0 + d.y1) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", d => d.x0 < 350 ? "start" : "end")
.attr("font-size", "11px")
.text(d => d.name);

return svg.node();
}

Insert cell
Insert cell
Insert cell
// Binning survivors by Fare into 10 intervals
fareBins = d3.bin()
.value(d => d.Fare)
.thresholds(10)(
data.filter(d => d.Survived === 1 && d.Fare != null)
)

Insert cell
Insert cell
//histogram showing distribution of fares paid by survivors
chart5 = {
const width = 500, height = 320;
const margin = { top: 40, right: 20, bottom: 50, left: 50 };
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const x = d3.scaleLinear()
.domain(d3.extent(fareBins.flatMap(d => [d.x0, d.x1])))
.nice()
.range([margin.left, width - margin.right]);

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

svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(10).tickFormat(d => `$${d.toFixed(0)}`));

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

svg.append("text")
.attr("x", width / 2)
.attr("y", height - 10)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text("Fare Paid (USD)");

svg.append("text")
.attr("x", -height / 2)
.attr("y", 15)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text("Number of Survivors");

svg.append("text")
.attr("x", width / 2)
.attr("y", margin.top / 2)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Distribution of Fares Paid by Survivors");

// Bars
svg.selectAll("rect")
.data(fareBins)
.join("rect")
.attr("x", d => x(d.x0))
.attr("y", d => y(d.length))
.attr("width", d => x(d.x1) - x(d.x0) - 1)
.attr("height", d => y(0) - y(d.length))
.attr("fill", "mediumseagreen");

return svg.node();
}
Insert cell
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