Skip to contents
library(ggchord2)
library(ggplot2)
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

Simple chord diagram

trade <- data.frame(
  source = c("USA", "USA", "China", "China", "Germany", "Germany"),
  target = c("China", "Germany", "USA", "Germany", "USA", "China"),
  freq   = c(50, 30, 60, 25, 35, 20)
)

ggplot(
  data = trade,
  mapping = aes(
    source = source,
    target = target,
    freq = freq
  )
) +
  geom_chord_diagram()

Colour by source node:

ggplot(
  data = trade,
  mapping = aes(
    source = source,
    target = target,
    freq = freq,
    fill = source
  )
) +
  geom_chord_diagram()

Facet by year:

trade_ts <- rbind(
  cbind(trade, year = 2022),
  data.frame(
    source = c("USA", "China", "Germany"),
    target = c("China", "USA", "USA"),
    freq   = c(65, 70, 40),
    year   = 2023
  )
)

ggplot(
  data = trade_ts,
  mapping = aes(
    source = source,
    target = target,
    freq = freq,
    fill = source
  )
) +
  geom_chord_diagram() +
  facet_wrap(~year)

Asymmetric flows

flows <- data.frame(
  source = c("Europe", "Europe", "Asia", "Asia", "Americas", "Americas"),
  target = c("Asia", "Americas", "Europe", "Americas", "Europe", "Asia"),
  freq   = c(12, 8, 15, 6, 9, 5)
)

ggplot(
  data = flows,
  mapping = aes(
    source = source,
    target = target,
    freq = freq,
    fill = source
  )
) +
  geom_chord_diagram(
    gap_degree   = 3,
    inner_radius = 0.55,
    outer_radius = 0.72
  )

Including flows from a source into itself:

flows <- data.frame(
  source = c("Europe", "Europe", "Asia", "Asia"),
  target = c("Europe", "Americas", "Europe", "Americas"),
  freq   = c(12, 8, 15, 17)
)

ggplot(
  data = flows,
  mapping = aes(
    source = source,
    target = target,
    freq = freq,
    fill = source
  )
) +
  geom_chord_diagram(
    gap_degree   = 3,
    inner_radius = 0.55,
    outer_radius = 0.72
  )

Individual layers for more control

g <- ggplot(
  data = trade,
  mapping = aes(
    source = source,
    target = target,
    freq = freq
  )
) +
  geom_chord_arcs(
    mapping = aes(fill = source),
    alpha = 0.5
  ) +
  geom_chord_sectors(
    mapping = aes(fill = source),
    colour = "white",
    linewidth = 0.8
  ) +
  geom_chord_labels(
    mapping = aes(colour = source),
    size = 4
  )
g

Add ggplot2 styling:

g +
  scale_colour_brewer(palette = "Dark2") +
  scale_fill_brewer(palette = "Dark2") +
  coord_fixed() +
  theme_void() +
  theme(legend.position = "none")

If you have many segments, there may not be enough space for the labels.

set.seed(123)
n <- 15
flows <- expand.grid(
  source = 1:n,
  target = 1:n
) |>
  filter(source < target) |>
  mutate(
    source = paste0("Category ", LETTERS[source]),
    target = paste0("Category ", LETTERS[target]),
    freq = rpois(((n^2 - n) / 2), 4)
  )
g <- ggplot(
  data = flows,
  mapping = aes(
    source = source,
    target = target,
    freq = freq
  )
) +
  geom_chord_arcs(
    mapping = aes(fill = source),
    alpha = 0.5
  ) +
  geom_chord_sectors(
    mapping = aes(fill = source),
    colour = "white",
    linewidth = 0.8
  ) +
  coord_fixed() +
  theme_void() +
  theme(legend.position = "none")

g +
  geom_chord_labels(
    mapping = aes(colour = source),
    size = 4
  )

You can use perpendicular labels instead (you may need to adjust the x and y limits to fit all the labels in):

g +
  geom_chord_labels_perp(
    mapping = aes(colour = source),
    size = 4
  ) +
  scale_x_continuous(limits = c(-1.5, 1.5)) +
  scale_y_continuous(limits = c(-1.5, 1.5))

Or just use curved labels:

g +
  geom_chord_labels_curve(
    mapping = aes(colour = source),
    size = 4
  ) +
  scale_x_continuous(limits = c(-1.5, 1.5))