Sankey Diagrams

library(networkD3)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
# Make a connection data frame
links <- data.frame(
  source=c("group_A","group_A", "group_B", "group_C", "group_C", "group_E"), 
  target=c("group_C","group_D", "group_E", "group_F", "group_G", "group_H"), 
  value=c(2,3, 2, 3, 1, 3)
)
 
# From these flows we need to create a node data frame: it lists every entities involved in the flow
nodes <- data.frame(
  name=c(as.character(links$source), as.character(links$target)) %>% 
    unique()
)

# With networkD3, connection must be provided using id, not using real name like in the links dataframe. So we need to reformat it.
links$IDsource <- match(links$source, nodes$name)-1 
links$IDtarget <- match(links$target, nodes$name)-1
 
 
# Make the Network. I call my colour scale with the colourScale argument
p <- sankeyNetwork(Links = links, Nodes = nodes, Source = "IDsource", Target = "IDtarget", 
              Value = "value", NodeID = "name")
p
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.1     ✔ readr     2.2.0
## ✔ ggplot2   4.0.2     ✔ stringr   1.6.0
## ✔ lubridate 1.9.5     ✔ tibble    3.3.1
## ✔ purrr     1.2.1     ✔ tidyr     1.3.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(viridis)
## Loading required package: viridisLite
library(patchwork)
library(circlize)
## ========================================
## circlize version 0.4.18
## CRAN page: https://cran.r-project.org/package=circlize
## Github page: https://github.com/jokergoo/circlize
## Documentation: https://jokergoo.github.io/circlize_book/book/
## 
## If you use it in published research, please cite:
## Gu, Z. circlize implements and enhances circular visualization
##   in R. Bioinformatics 2014.
## 
## This message can be suppressed by:
##   suppressPackageStartupMessages(library(circlize))
## ========================================
# Load dataset from github
data <- read.table("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/13_AdjacencyDirectedWeighted.csv", header=TRUE)
# Package
library(networkD3)

# I need a long format
data_long <- data %>%
  rownames_to_column %>%
  gather(key = 'key', value = 'value', -rowname) %>%
  filter(value > 0)
colnames(data_long) <- c("source", "target", "value")
data_long$target <- paste(data_long$target, " ", sep="")

# From these flows we need to create a node data frame: it lists every entities involved in the flow
nodes <- data.frame(name=c(as.character(data_long$source), as.character(data_long$target)) %>% unique())

# With networkD3, connection must be provided using id, not using real name like in the links dataframe.. So we need to reformat it.
data_long$IDsource=match(data_long$source, nodes$name)-1
data_long$IDtarget=match(data_long$target, nodes$name)-1

# prepare colour scale
ColourScal ='d3.scaleOrdinal() .range(["#FDE725FF","#B4DE2CFF","#6DCD59FF","#35B779FF","#1F9E89FF","#26828EFF","#31688EFF","#3E4A89FF","#482878FF","#440154FF"])'

# Make the Network
sankeyNetwork(Links = data_long, Nodes = nodes,
                     Source = "IDsource", Target = "IDtarget",
                     Value = "value", NodeID = "name",
                     sinksRight=FALSE, colourScale=ColourScal, nodeWidth=40, fontSize=13, nodePadding=20)

Arc Diagrams

An arc diagram is a special kind of network graph. It is consituted by nodes that represent entities and by links that show relationships between entities. In arc diagrams, nodes are displayed along a single axis and links are represented with arcs.

Here is a 2D vs arc example.

library(tidyverse)
library(viridis)
library(patchwork)
library(igraph)
## 
## Attaching package: 'igraph'
## The following object is masked from 'package:circlize':
## 
##     degree
## The following objects are masked from 'package:lubridate':
## 
##     %--%, union
## The following objects are masked from 'package:purrr':
## 
##     compose, simplify
## The following object is masked from 'package:tidyr':
## 
##     crossing
## The following object is masked from 'package:tibble':
## 
##     as_data_frame
## The following objects are masked from 'package:dplyr':
## 
##     as_data_frame, groups, union
## The following objects are masked from 'package:stats':
## 
##     decompose, spectrum
## The following object is masked from 'package:base':
## 
##     union
library(ggraph)
library(colormap)

# A really simple edge list
links=data.frame(
    source=c("A", "A", "A", "A", "B"),
    target=c("B", "C", "D", "F","E")
    )

# Transform to a igraph object
mygraph <- graph_from_data_frame(links)

# Make the usual network diagram
p1 <-  ggraph(mygraph) +
  geom_edge_link(edge_colour="black", edge_alpha=0.3, edge_width=0.2) +
  geom_node_point( color="#69b3a2", size=5) +
  geom_node_text( aes(label=name), repel = TRUE, size=8, color="#69b3a2") +
  theme_void() +
  theme(
    legend.position="none",
    plot.margin=unit(rep(2,4), "cm")
  )
## Using "tree" as default layout
# Make a cord diagram
p2 <-  ggraph(mygraph, layout="linear") +
  geom_edge_arc(edge_colour="black", edge_alpha=0.3, edge_width=0.2) +
  geom_node_point( color="#69b3a2", size=5) +
  geom_node_text( aes(label=name), repel = FALSE, size=8, color="#69b3a2", nudge_y=-0.1) +
  theme_void() +
  theme(
    legend.position="none",
    plot.margin=unit(rep(2,4), "cm")
  )

p1 + p2

Let’s look at more complicated examples: https://www.data-to-viz.com/graph/arc.html

Hierarchial Edge Bundling

Here is an example showing the same dataset with and without the use of bundling. The use of straight line on the left results in a cluttered figure that makes impossible to read the connection. The use of bundling on the right makes a neat figure:

# Libraries
library(tidyverse)
library(viridis)
library(patchwork)
library(ggraph)
library(igraph)

# The flare dataset is provided in ggraph
edges <- flare$edges
vertices <- flare$vertices %>% arrange(name) %>% mutate(name=factor(name, name))
connections <- flare$imports

# Preparation to draw labels properly:
vertices$id=NA
myleaves=which(is.na( match(vertices$name, edges$from) ))
nleaves=length(myleaves)
vertices$id[ myleaves ] = seq(1:nleaves)
vertices$angle= 90 - 360 * vertices$id / nleaves
vertices$hjust<-ifelse( vertices$angle < -90, 1, 0)
vertices$angle<-ifelse(vertices$angle < -90, vertices$angle+180, vertices$angle)

# Build a network object from this dataset:
mygraph <- graph_from_data_frame(edges, vertices = vertices)

# The connection object must refer to the ids of the leaves:
from = match( connections$from, vertices$name)
to = match( connections$to, vertices$name)

# Basic dendrogram
p1=ggraph(mygraph, layout = 'dendrogram', circular = TRUE) +
    geom_edge_link(size=0.4, alpha=0.1) +
    geom_node_text(aes(x = x*1.01, y=y*1.01, filter = leaf, label=shortName, angle = angle, hjust=hjust), size=1.5, alpha=1) +
    coord_fixed() +
    theme_void() +
    theme(
      legend.position="none",
      plot.margin=unit(c(0,0,0,0),"cm"),
    ) +
    expand_limits(x = c(-1.2, 1.2), y = c(-1.2, 1.2))
## Warning in geom_edge_link(size = 0.4, alpha = 0.1): Ignoring unknown
## parameters: `edge_size`
p2=ggraph(mygraph, layout = 'dendrogram', circular = TRUE) +
    geom_conn_bundle(data = get_con(from = from, to = to), alpha = 0.1, colour="#69b3a2") +
    geom_node_text(aes(x = x*1.01, y=y*1.01, filter = leaf, label=shortName, angle = angle, hjust=hjust), size=1.5, alpha=1) +
    coord_fixed() +
    theme_void() +
    theme(
      legend.position="none",
      plot.margin=unit(c(0,0,0,0),"cm"),
    ) +
    expand_limits(x = c(-1.2, 1.2), y = c(-1.2, 1.2))

p1 + p2

Some more examples: https://www.data-to-viz.com/graph/edge_bundling.html

Paleteer and Color Selection

Why color choice matters

Good color choices make figures easier to read and interpret. In general:

  • use qualitative palettes for categories
  • use sequential palettes for low-to-high numeric values
  • use diverging palettes when values split around a meaningful midpoint

Install and load packages

#install.packages("paletteer")
#install.packages("ggplot2")

library(ggplot2)
library(paletteer)

head(palettes_d_names) #discrete palettes
## # A tibble: 6 × 5
##   package palette      length type       novelty
##   <chr>   <chr>         <int> <chr>      <lgl>  
## 1 amerika Dem_Ind_Rep3      3 divergent  FALSE  
## 2 amerika Dem_Ind_Rep5      5 divergent  FALSE  
## 3 amerika Dem_Ind_Rep7      7 divergent  FALSE  
## 4 amerika Democrat          3 sequential FALSE  
## 5 amerika Republican        3 sequential FALSE  
## 6 awtools a_palette         8 sequential TRUE
head(palettes_c_names) #continuous palettes
## # A tibble: 6 × 3
##   package  palette               type      
##   <chr>    <chr>                 <chr>     
## 1 ggthemes Blue-Green Sequential sequential
## 2 ggthemes Blue Light            sequential
## 3 ggthemes Orange Light          sequential
## 4 ggthemes Blue                  sequential
## 5 ggthemes Orange                sequential
## 6 ggthemes Green                 sequential
head(palettes_dynamic_names) # palettes that can generate a variable number of colors
##       package    palette length       type
## 1 cartography   blue.pal     20 sequential
## 2 cartography orange.pal     20 sequential
## 3 cartography    red.pal     20 sequential
## 4 cartography  brown.pal     20 sequential
## 5 cartography  green.pal     20 sequential
## 6 cartography purple.pal     20 sequential
df_cat <- data.frame(
  group = c("A", "B", "C", "D"),
  value = c(12, 18, 9, 15)
)

df_cont <- data.frame(
  x = 1:10,
  y = c(3, 5, 6, 8, 9, 11, 12, 15, 16, 18)
)

ggplot(df_cat, aes(x = group, y = value, fill = group)) +
  geom_col() +
  scale_fill_paletteer_d("beyonce::X101") + #discrete palette
  theme_minimal()

ggplot(mtcars, aes(x = wt, y = mpg, color = hp)) +
  geom_point(size = 3) +
  scale_color_paletteer_c("viridis::plasma") + #continuous palette
  theme_minimal()

df_div <- data.frame(
  x = letters[1:6],
  change = c(-3, -1, 0, 2, 4, 6)
)

#Use a diverging palette when the data have a meaningful center, such as zero, average change, or control value.

ggplot(df_div, aes(x = x, y = change, fill = change)) +
  geom_col() +
  scale_fill_paletteer_c("scico::vik") + #divergent palette
  theme_minimal()

Qualitative palettes

Use for:

  • species
  • treatment groups
  • tissue types
  • countries
  • categories with no order

Sequential palettes

Use for:

  • abundance
  • expression level
  • temperature
  • concentration
  • any low-to-high variable

Diverging palettes

Use for:

  • fold change around zero
  • difference from a control
  • positive vs negative effects

Tips for choosing colors well

  1. Do not use too many categories at once
  2. Make sure colors are easy to distinguish
  3. Avoid relying on red/green alone
  4. Use color to support the message, not distract from it
  5. Keep palette choices consistent across related figures
  6. Example of manual extraction from paletteer

Homework

Part 1. Color selection with paletteer

Before making your figures, choose one palette you think works well for categorical data and one that works well for ordered or numeric data.

Task 1

Use paletteer to explore palettes and answer the following:

data(palettes_d_names) 
data(palettes_c_names) 
data(palettes_dynamic_names)
  • What palette did you choose for categorical data? ggthemes::Blue-Teal

  • What palette did you choose for numeric data? beyonce:X101

  • In 3 to 5 sentences, explain why those choices make sense. I chose Blue-Teal because I like blue, it’s colorblind friendly, and it looks nice next to dark text I chose beyonce::X101 because it looks nice and it’s colorblind friendly, and I like beyonce Requirements

  • Include the code you used to preview or extract palettes

  • Include 1 to 2 sentences on why color choice matters in scientific graphics

Color choice matters because your graphs need to be accessible to people with vision differences and they need to be pleasing to the eye if being presented.

Part 2. Bubble Maps

Create a bubble map in ggplot.

Requirements

  1. plot at least 5 locations
  2. use bubble size to represent a numeric value
  3. use color intentionally with a palette you selected
  4. include text labels for the locations
  5. include a title
  6. include a short caption or note explaining what size and color represent

Acceptable options

  • one map that includes both bubble size and labels
  • or two versions of the same map, one emphasizing color and one emphasizing labels

Suggested skills

  • geom_point()
  • geom_text() or geom_label()
  • coord_cartesian() if you want to zoom in
  • theme_void() or another clean theme
library(giscoR)
library(dplyr)
library(ggplot2)
library(ggrepel)
library(paletteer)
Poland <- gisco_get_countries(country = "Poland", resolution = 1)
library(maps)
## 
## Attaching package: 'maps'
## The following object is masked from 'package:viridis':
## 
##     unemp
## The following object is masked from 'package:purrr':
## 
##     map
data <- world.cities %>% filter(country.etc == "Poland")

data %>%
  arrange(pop) %>%
  mutate(name = factor(name, unique(name))) %>%
  ggplot() +
  geom_sf(data = Poland, fill = "grey", alpha = 0.3) +
  geom_point(aes(x = long, y = lat, size = pop, color = pop), alpha = 0.9) +
  geom_text_repel(
    data = data %>% arrange(pop) %>% tail(10),
    aes(x = long, y = lat, label = name), size = 6, color = "black"
  )+
    geom_point(
    data = data %>% arrange(pop) %>% tail(10), aes(x = long, y = lat),
    color = "red", size = 3
  ) +
  scale_color_paletteer_c("ggthemes::Blue-Teal") +
  scale_size(range = c(3, 12)) +
  theme_void() +
  theme(legend.position = "none")

Part 3. Map variation with icon labels.

Make one variation of your map using icon labels.

Requirements

  1. include at least 3 labeled locations
  2. labels can be marker icons, point symbols, or custom styled labels
  3. keep it readable and not overcrowded
  4. write 2 to 4 sentences explaining how this labeling style changes the way the map feels or reads

Examples of what counts

  • a leaflet map with popup markers
  • a static map with special point shapes and labels
  • a map using marker icons or custom annotation
library(ggimage)
cities <- data.frame(
  name = c("Warsaw", "Lodz", "Cracow", "Poznan", "Wroclaw"),
  lon = c(21.02, 19.46, 19.96, 16.90, 17.03),
  lat = c(52.26, 51.77, 50.06, 52.40, 51.11),
  image = c(
    "https://www.iconpacks.net/icons/2/free-location-icon-2952-thumb.png",
    "https://www.iconpacks.net/icons/2/free-location-icon-2952-thumb.png",  
    "https://www.iconpacks.net/icons/2/free-location-icon-2952-thumb.png",
    "https://www.iconpacks.net/icons/2/free-location-icon-2952-thumb.png",
    "https://www.iconpacks.net/icons/2/free-location-icon-2952-thumb.png"
  )
)
data %>%
  arrange(pop) %>%
  mutate(name = factor(name, unique(name))) %>%
  ggplot() +
  geom_sf(data = Poland, fill = "grey", alpha = 0.3) +
  geom_point(aes(x = long, y = lat, size = pop, color = pop), alpha = 0.9) +
  geom_text_repel(
    data = data %>% arrange(pop) %>% tail(10),
    aes(x = long, y = lat, label = name), size = 5, color = "black"
  )+
    geom_point(
    data = data %>% arrange(pop) %>% tail(10), aes(x = long, y = lat),
    color = "red", size = 3
  ) +
  scale_color_paletteer_c("ggthemes::Blue-Teal") +
  scale_size(range = c(3, 12)) +
   geom_image(data = cities,
             aes(x = lon, y = lat, image = image),
             size = 0.06) +
  theme_void() +
  theme(legend.position = "none")

This labeling style allows for more visual direction on points where you want people to focus. For this example, I put location markers on the five largest Polish cities so people would be drawn to them first. ### Part 4. Basic Sankey Diagram

You may invent a small dataset if needed. Keep it simple.

Example ideas

  1. students moving from major to career path
  2. genes grouped into pathway categories
  3. citation flow from field to topic
  4. patients moving from diagnosis group to treatment group

Build one Sankey diagram.

Requirements

  • include at least 3 source categories
  • include at least 3 destination categories
  • show flow values
  • use color intentionally
  • include a short figure caption explaining what the flows mean

Keep it basic: It does not need to be interactive or highly customized. The goal is to understand the structure.

library(networkD3)
library(htmlwidgets)
## 
## Attaching package: 'htmlwidgets'
## The following object is masked from 'package:networkD3':
## 
##     JS
library(htmltools)


# Make a connection data frame
links <- data.frame(
  source=c("Biology","Biology", "Math", "Physics", "Physics", "Chemistry"), 
  target=c("Researcher","Professor", "Teacher", "NGO", "Military", "Government"),
  value=c(3,5, 2, 4, 1, 6)
)
 
nodes <- data.frame(
  name=c(as.character(links$source), as.character(links$target)) %>% 
    unique()
)

links$IDsource <- match(links$source, nodes$name)-1 
links$IDtarget <- match(links$target, nodes$name)-1
 
sank <- sankeyNetwork(Links = links, Nodes = nodes, Source = "IDsource", Target = "IDtarget", 
              Value = "value", NodeID = "name", )

sank <- appendContent(sank, tags$p("Flow of college STEM majors to various careers"))
sank

Flow of college STEM majors to various careers

Part 5. Hierarchial edge building graph

Create one hierarchical edge bundling graph. You may use an example dataset from a package, adapt class example code, or create a very small hierarchy yourself.

Build one edge bundling graph.

Requirements

  1. include a hierarchical structure
  2. include linked nodes
  3. use color intentionally
  4. include a short paragraph answering:
  • What does the hierarchy represent?
  • What do the edges represent?
  • Why might edge bundling be useful compared with drawing all lines directly?

Note:This is meant to be an introduction, not a perfect polished figure. It is okay to rely on a tutorial example and then make small changes.

library(tidygraph)
## 
## Attaching package: 'tidygraph'
## The following object is masked from 'package:igraph':
## 
##     groups
## The following object is masked from 'package:stats':
## 
##     filter
library(viridis)
library(patchwork)
library(ggraph)
library(igraph)
library(dplyr)

edges <- flare$edges
vertices <- flare$vertices %>% arrange(name) %>% mutate(name=factor(name, name))
connections <- flare$imports

vertices$id=NA
myleaves=which(is.na( match(vertices$name, edges$from) ))
nleaves=length(myleaves)
vertices$id[ myleaves ] = seq(1:nleaves)
vertices$angle= 90 - 360 * vertices$id / nleaves
vertices$hjust<-ifelse( vertices$angle < -90, 1, 0)
vertices$angle<-ifelse(vertices$angle < -90, vertices$angle+180, vertices$angle)

mygraph <- graph_from_data_frame(edges, vertices = vertices)
from = match( connections$from, vertices$name)

to = match( connections$to, vertices$name)
graph=ggraph(mygraph, layout = 'dendrogram', circular = TRUE) +
    geom_conn_bundle(data = get_con(from = from, to = to), alpha = 0.1) +
    geom_node_text(aes(x = x*1.01, y=y*1.01, filter = leaf, label=shortName, angle = angle, hjust=hjust), size=1.5, alpha=1) +
    coord_fixed() +
    theme_void() +
    theme(
      legend.position="none",
      plot.margin=unit(c(0,0,0,0),"cm"),
    ) +
    expand_limits(x = c(-1.2, 1.2), y = c(-1.2, 1.2))

graph +
  geom_conn_bundle(data = get_con(from = from, to = to), width=1, alpha=0.2, aes(colour=..index..)) +
  scale_edge_colour_distiller(palette = "RdPu") +
  theme(legend.position = "none")
## Warning: The dot-dot notation (`..index..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(index)` instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

The hierarchy in the flare dataset represents a classification of software components organized into nested categories, where higher-level nodes group related lower-level functions or modules. The edges represent dependencies between these components, specifically, which modules import or rely on others. Edge bundling is useful here because many components are connected across different branches of the hierarchy and without bundling these connections would appear as a really dense web of overlapping lines. By grouping similar paths together into smooth curves, edge bundling reduces visual clutter and makes it easier to see overall patterns.