This document was generated on October 31, 2026. It
demonstrates how to transform multi-dimensional categorical data into a
continuous visual stream using a Sankey diagram
rendered via plotly in R.
Sankey diagrams excel at illustrating proportional shifts across categorical stages. Here, we track passenger demographics and survival outcomes from the RMS Titanic disaster of April 1912.
The analysis uses the built-in R Titanic dataset,
derived from the official passenger manifest and historical records
compiled after the sinking.
The dataset is a 4-dimensional cross-tabulated array consisting of 2,201 records categorized across four factors:
Class: Passenger booking class or crew
designation (1st, 2nd, 3rd,
Crew).Sex: Biological sex (Male,
Female).Age: Age category (Child,
Adult).Survived: Final survival status
(No, Yes).The table below aggregates the total passenger counts across passenger class, sex, and final survival outcomes:
library(dplyr)
library(knitr)
# Convert 4D array to data frame
titanic_df <- as.data.frame(Titanic) %>%
filter(Freq > 0)
# Summary table grouped by Class, Sex, and Survived
summary_table <- titanic_df %>%
group_by(Class, Sex, Survived) %>%
summarise(Passengers = sum(Freq), .groups = "drop") %>%
arrange(Class, Sex, Survived)
kable(summary_table, caption = "Titanic Manifest Summary by Class, Sex, and Outcome")| Class | Sex | Survived | Passengers |
|---|---|---|---|
| 1st | Male | No | 118 |
| 1st | Male | Yes | 62 |
| 1st | Female | No | 4 |
| 1st | Female | Yes | 141 |
| 2nd | Male | No | 154 |
| 2nd | Male | Yes | 25 |
| 2nd | Female | No | 13 |
| 2nd | Female | Yes | 93 |
| 3rd | Male | No | 422 |
| 3rd | Male | Yes | 88 |
| 3rd | Female | No | 106 |
| 3rd | Female | Yes | 90 |
| Crew | Male | No | 670 |
| Crew | Male | Yes | 192 |
| Crew | Female | No | 3 |
| Crew | Female | Yes | 20 |
The Sankey diagram maps categorical transitions left-to-right along a two-stage flow:
\[\text{Passenger Class} \longrightarrow \text{Sex} \longrightarrow \text{Survival Outcome}\]
Hover over nodes or connecting flows to inspect exact passenger volumes.
library(plotly)
# Stage 1: Class -> Sex
flow_class_sex <- titanic_df %>%
group_by(Class, Sex) %>%
summarise(value = sum(Freq), .groups = "drop") %>%
rename(source_name = Class, target_name = Sex)
# Stage 2: Sex -> Survived
flow_sex_survived <- titanic_df %>%
group_by(Sex, Survived) %>%
summarise(value = sum(Freq), .groups = "drop") %>%
rename(source_name = Sex, target_name = Survived) %>%
mutate(target_name = paste("Outcome:", target_name))
# Combine stage flows
all_flows <- bind_rows(flow_class_sex, flow_sex_survived)
# Map nodes to 0-indexed vectors for Plotly
unique_nodes <- c("1st", "2nd", "3rd", "Crew",
"Male", "Female",
"Outcome: No", "Outcome: Yes")
node_lookup <- setNames(seq(0, length(unique_nodes) - 1), unique_nodes)
all_flows <- all_flows %>%
mutate(
source = node_lookup[source_name],
target = node_lookup[target_name]
)
# Define node palette
node_colors <- c(
"#2b5c8f", "#3690c0", "#67a9cf", "#a6bddb", # Class nodes
"#8856a7", "#e08214", # Sex nodes
"#d73027", "#1a9850" # Survival outcome nodes
)
# Render Sankey Widget
plot_ly(
type = "sankey",
orientation = "h",
node = list(
label = unique_nodes,
color = node_colors,
pad = 18,
thickness = 22,
line = list(color = "#333333", width = 0.5)
),
link = list(
source = all_flows$source,
target = all_flows$target,
value = all_flows$value
)
) %>%
layout(
title = list(text = "Titanic Passenger Flows: Class → Sex → Survival", font = list(size = 16)),
font = list(size = 12)
)