Public
Edited
May 1
Insert cell
Insert cell
d3 = require("d3@7", "d3-sankey@0.12")
Insert cell
data = d3.csvParse(await FileAttachment("tested.csv").text(),
d3.autoType)
Insert cell
/** this pie chart shows the number of passengers that survived according to the fare they paid to board the titanic. The fares are split into groups for every fifty dollars. Each slice on the pie chart is a group of passengers that survived from a specific fare group. All passengers that survived and paid more than 250 dollars for their fare were categorized into one group, in order to keep the chart readable. */
piechart = {
const width = 928;
const height = Math.min(width, 700);

// groups fares into increments of $50
const maxFareGroup = fare => {
const bucket = Math.floor(fare / 50) * 50;
return bucket > 250 ? 250 : bucket;
};

// assigns passengers who survived into a specific fare group
const fareGroups = Array.from(
d3.rollup(
data.filter( d => d.Survived === 1),
v => v.length,
d => maxFareGroup(d.Fare)
),
([fareBucket, count]) => ({ fareBucket, count })
).sort((a, b) => d3.ascending(a.fareBucket, b.fareBucket)); //chatgpt created this variable to group the number of passengers by the port they embarked

// creates the labels for each slice in the pie chart
const pieData = fareGroups.map(d => ({
name: d.fareBucket === 250
? "$250+"
: `$${d.fareBucket} - $${d.fareBucket + 50}`,
value: d.count
}));

// defines the colors for each slice
const color = d3.scaleOrdinal()
.domain(pieData.map(d => d.name))
.range(d3.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), pieData.length).reverse())

// creates the pie layout
const pie = d3.pie()
.sort(null)
.value(d => d.value);

// defines the arc for each slice
const arc = d3.arc()
.innerRadius(0)
.outerRadius(Math.min(width, height)/ 2 - 1);

// defines the placement of each of the label according to radius
const labelRadius = arc.outerRadius()() * 0.8;

const arcLabel = d3.arc()
.innerRadius(labelRadius)
.outerRadius(labelRadius);

const arcs = pie(pieData);

// defines the svg container
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;");

svg.append("g")
.attr("stroke", "white")
.selectAll()
.data(arcs)
.join("path")
.attr("fill", d => color(d.data.name))
.attr("d", arc)
.append("title")
.text(d => `${d.data.name}: ${d.data.value} survivors`);

svg.append("g")
.attr("text-anchor", "middle")
.selectAll()
.data(arcs)
.join("text")
.attr("transform", d => `translate(${arcLabel.centroid(d)})`)
.call(text => text.append("tspan")
.attr("y", "-0.4em")
.attr("font-weight", "bold")
.text(d => d.data.name))
.call(text => text.filter(d => (d.endAngle - d.startAngle) > 0.05).append("tspan")
.attr("x", 0)
.attr("y", "0.7em")
.attr("fill-opacity", 0.7)
.text(d => `${d.data.value}`));

return svg.node();

}
Insert cell
/** this stacked area chart shows the number of passengers in each class, according to their gender. The passengers are split into groups based on their class, and then further split up by their gender. The orange area shows the number of female passengers in class 1, 2, and 3. The blue area shows the number of male passengers in class 1, 2, and 3. */
areachart = {
// Specify the chart’s dimensions.
const width = 928;
const height = 500;
const marginTop = 10;
const marginRight = 10;
const marginBottom = 20;
const marginLeft = 40;

// groups the data by class and sex
const group = d3.rollup(
data,
v => v.length,
d => +d.Pclass,
d => d.Sex.toLowerCase().trim()
);

// defines the class and sex categories
const pclass = [1, 2, 3];
const sexes = ["male", "female"];

// formats the grouped data
const stackedgroups = pclass.map(Pclass => {
const sexMap = group.get(Pclass) || new Map();
const entry = { Pclass };
for (const sex of sexes) {
entry[sex] = sexMap.get(sex) || 0;
}
return entry;
});

const keys = ["male", "female"];
// Determine the series that need to be stacked.
const series = d3.stack()
.keys(keys)
(stackedgroups);// distinct series keys, in input order

// defines the x-axis
const x = d3.scalePoint()
.domain([1, 2, 3])
.range([marginLeft, width - marginRight]);

// defines the y-axis
const y = d3.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))])
.nice()
.range([height - marginBottom, marginTop]);

// defines the color scheme for the male and female areas
const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeTableau10);

// defines the area for the graph
const area = d3.area()
.x(d => x(d.data.Pclass))
.y0(d => y(d[0]))
.y1(d => y(d[1]));

// defines the svg container
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

svg.append("g")
.attr("transform", `translate(${marginLeft}, 0)`)
.call(d3.axisLeft(y).ticks(height / 80))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Persons in a designated class"));

svg.append("g")
.selectAll()
.data(series)
.join("path")
.attr("fill", d => color(d.key))
.attr("d", area)
.append("title")
.text(d => d.key);

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0).tickFormat(d => `Class ${d}`));

// adds a legend to the area chart
const legend = svg.append("g")
.attr("transform", `translate(${width - marginRight - 150}, ${marginTop})`)
.selectAll("g")
.data(series)
.join("g")
.attr("transform", (d, i) => `translate(0, ${i * 20})`);
legend.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", d => color(d.key));

legend.append("text")
.attr("x", 20)
.attr("y", 12)
.attr("fill", "currentColor")
.text(d => d.key);
return Object.assign(svg.node(), {scales: {color}});

}

Insert cell
/** this sankey diagram shows the number of passengers that survived/perished according to their age group. The ages are split into groups for every ten years. Each node is a group of passengers that either survived or persished from a specific age group. Each link connects the group of passengers from their age group to whether that group survived or perished. */
sankeychart = {
const width = 928;
const height = 720;

// specifies the dimensions of the diagram
const sankey = d3.sankey()
.nodeSort(null)
.linkSort(null)
.nodeWidth(4)
.nodePadding(20)
.extent([[0, 5], [width, height - 5]])

// separates the groups of perished/survived passengers by color
const color = d3.scaleOrdinal()
.domain(["Survived", "Perished"])
.range(["#91bca8", "#73a1bd"])
.unknown("#ccc");

//defines the svg container
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");

// specifies the nodes and links created in the graph function
const {nodes, links} = sankey({
nodes: graph.nodes.map(d => Object.create(d)),
links: graph.links.map(d => Object.create(d))
});


svg.append("g")
.selectAll("rect")
.data(nodes)
.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)
.append("title")
.text(d => `${d.name}\n${d.value.toLocaleString()}`);

svg.append("g")
.attr("fill", "none")
.selectAll("g")
.data(links)
.join("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke", d => color(d.names[1]))
.attr("stroke-width", d => d.width)
.style("mix-blend-mode", "multiply")
.append("title")
.text(d => `${d.names.join(" → ")}\n${d.value.toLocaleString()}`);

svg.append("g")
.style("font", "10px sans-serif")
.selectAll("text")
.data(nodes)
.join("text")
.attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
.attr("y", d => (d.y1 + d.y0) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
.text(d => d.name)
.append("tspan")
.attr("fill-opacity", 0.7)
.text(d => ` ${d.value.toLocaleString()}`);

return svg.node();
}

Insert cell
/** This function sorts the passengers into groups based on their age. */
agegroups = age => {
if (age < 10) return "0-9";
if (age < 20) return "10-19";
if (age < 30) return "20-29";
if (age < 40) return "30-39";
if (age < 50) return "40-49";
if (age < 60) return "50-59";
return "60+";
}

Insert cell
/** This function takes the previously sorted passengers and assigns each passenger to a group based on their age group and whether they survived. Each group is then given a total count, depending on how many passengers are in each group. */
filterData = Array.from(
d3.rollup(
data,
v => v.length,
d => agegroups(+d.Age),
d => d.Survived === 1 ? "Survived" : "Perished"
),
([ageGroup, entries]) =>
Array.from(entries, ([survival, count]) => ({
"Age Group": ageGroup,
"Survival": survival,
value: count
}))
).flat();
Insert cell
data.columns = ["Age Group", "Survival", "value"];
Insert cell
/** The graph functions creates a node for each passenger in each age group. Then the link connects each passenger to the other side of the sankey diagram, which directs whether they survived or not. */
graph = {
const keys = ["Age Group", "Survival"];
let index = -1;
const nodes = [];
const nodeByKey = new d3.InternMap([], JSON.stringify);
const indexByKey = new d3.InternMap([], JSON.stringify);
const links = [];

// creates a node for each age group
for (const k of keys) {
for (const d of filterData) {
const key = [k, d[k]];
if (nodeByKey.has(key)) continue;
const node = { name: d[k] };
nodes.push(node);
nodeByKey.set(key, node);
indexByKey.set(key, ++index);
}
}

// sets up the link so that each group of survived/persished passengers starts at the right age group and ends at the right endpoint (survived or perished)
for (let i = 1; i < keys.length; ++i) {
const a = keys[i - 1];
const b = keys[i];
const prefix = keys.slice(0, i + 1);
const linkByKey = new d3.InternMap([], JSON.stringify);
for (const d of filterData) {
const names = prefix.map(k => d[k]);
const value = d.value || 1;
let link = linkByKey.get(names);
if (link) { link.value += value; continue; }
link = {
source: indexByKey.get([a, d[a]]),
target: indexByKey.get([b, d[b]]),
names,
value
};
links.push(link);
linkByKey.set(names, link);
}
}

return {nodes, links};
}
Insert cell
# Motivation and Insights
The first chart I created was a pie chart that shows the number of survivors based on how much fare they paid to be on the titanic. My motivation for choosing this chart was to show the divide between the wealth of passengers on the titanic, and see how that might have affected survivability. For my second chart, I created a stacked area graph showing the number of male and female passengers in each class. My motivation for creating this graph was to use it side-by-side with the pie chart, to see if the proportion of survivors from different fare groups is reasonable compared to the number of passengers from each class. From the pie chart we can see that the group with the highest number of survivors is the group of passengers that paid less than $50. Since these passengers paid the least for their tickets, we can also assume that they would be in the lowest class, which was class 3. From the stacked area graph, we can see that while class 3 had the most passengers, they also had a larger proportion of survivors than the other classes, considering the few survivors seen in higher fare groups. This is an interesting relationship, considering that my assumption was that the rich would have been prioritized during evacuation and rescues. The third chart I created was a Sankey diagram that shows the number of survivors and and perished passengers according to their age group. An interesting observation from this diagram is that overall, it looks like more people in each age group perished than survived. This is interesting because I would have assumed that passengers in the younger age groups would have a higher chance of surviving, since in most situations, their safety is usually prioritized. When it comes to people in their 20's, they are usually more healthy and fit to take on stressful situations, so it also comes at a surprise that there were not more survivors in this age group. Since these diagrams includes filtering and condensing data from the overall dataset, it would have been more difficult to make these observations without a visualization tool.
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