Bordering countries graph

Author
Published

November 4, 2024

In this post we’re going to build up this graph which shows all countries with at least one border and the connections that remain after removing countries with only a single border.

To solve this smoothly I was very pleased to discover the newly [December 2023] added guide_custom() functionality of {ggplot2}. Of course, this just works effortlessly with the {ggraph} extension for graph visualisation.

Connected Countries with tidygraph and ggraph

I’ve been thinking about TidyTuesday datasets with country data and how it could be interesting to use country borders as a component of the chart making process. And what’s better than working on an actual TidyTuesday visualisation than getting distracted with something tangential to it?

In my utility package {cjhRutils} I have a tidygraph object containing the nodes and edges of the connected countries, the code can be found in this script. After loading the package (alongside {tidygrapph}) we can see our dataset:

library("tidyverse")
library("tidygraph")
library("cjhRutils")
library("ggtext")

ggraph_bordering_countries
# A tbl_graph: 173 nodes and 608 edges
#
# A directed simple graph with 26 components
#
# A tibble: 173 × 8
     id iso_a2 iso_a3 name                 name_long name_en region_wb continent
  <int> <chr>  <chr>  <chr>                <chr>     <chr>   <chr>     <chr>    
1     1 AE     ARE    United Arab Emirates United A… United… Middle E… Asia     
2     2 AF     AFG    Afghanistan          Afghanis… Afghan… South As… Asia     
3     3 AL     ALB    Albania              Albania   Albania Europe &… Europe   
4     4 AM     ARM    Armenia              Armenia   Armenia Europe &… Asia     
5     5 AO     AGO    Angola               Angola    Angola  Sub-Saha… Africa   
6     6 AQ     ATA    Antarctica           Antarcti… Antarc… Antarcti… Antarcti…
# ℹ 167 more rows
#
# A tibble: 608 × 3
   from    to border_region             
  <int> <int> <chr>                     
1     1   120 Middle East & North Africa
2     1   136 Middle East & North Africa
3     2    34 Cross Region              
# ℹ 605 more rows

The graph contains all countries with at least one connection, let’s filter the graph to only include countries with two border or more… and visualise that naively with {ggraph}

Code
library("ggraph")

set.seed(1)
ggraph_bordering_countries %>%
  activate(nodes) %>%
  mutate(node_degree = tidygraph::centrality_degree()) %>%
  filter(node_degree > 1) %>%
  ggraph(layout = 'nicely') +
  geom_node_point(aes(colour = region_wb)) +
  geom_edge_link(aes(colour = border_region))

Custom ggplot2 legends with guide_custom()

The guide/legend for that chart is a little bit complicated. Let’s look at why:

  1. Nodes are coloured by the continent the node belongs to.

  2. Edges are coloured by if the two nodes belong to the same continent.

  3. There is a single node from “North America” but no edges with the border_region of “North America”

  4. There are edges that need to be coloured “Cross Region” but no nodes with that colour.

To solve this I thought of using my old trick of hijacking an unused aesthetic and manipulating its guide. However! That’s not really possible in this case, so I googled for alternatives and was extremeley geom_custom() was added in late 2023. In the chart below I’ve used guide_custom() to add a red line that I can use to label cross regional borders.

set.seed(1)
gg_graph_for_coords <- ggraph_bordering_countries %>%
  activate(nodes) %>%
  mutate(node_degree = tidygraph::centrality_degree()) %>%
  filter(node_degree > 1) %>%
  ggraph(layout = 'nicely') +
  geom_node_point(aes(colour = region_wb)) +
  geom_node_label(aes(label = iso_a2), size = 0) +
  geom_edge_link() +
  guides(custom = guide_custom(
    title = "Cross regional borders",
    grob = grid::linesGrob(
      x = unit(c(0, 5.4), "cm"),
      y = unit(c(0, 0), "cm"),
      gp = grid::gpar(col = '#F44336', lwd = 3)
    )
  ))

gg_graph_for_coords

In the final chart I’d like to add a label to the US to explain why it’s included but Canada isn’t. So let’s grab the coordinates of the node so I can use them to help figure out where to place the label

ggplot_build(gg_graph_for_coords)$data[[3]] %>%
  filter(label == "US") %>%
  select(x, y) %>%
  as_tibble()
# A tibble: 0 × 2
# ℹ 2 variables: x <dbl>, y <dbl>

Nice. Now we can think about beautification. I’ve chosen to use colours from the <coolors.co> service and have found a subjective balance of colours that I think looks good based on how many nodes are in each group. To ensure a little bit of sense to the colours, I’ll order them as a factor so that the group with the most nodes appears at the top of the legend.

Code
# https://coolors.co/448aff-1565c0-009688-8bc34a-ffc107-ff9800-f44336-ad1457
vec_colours <- c(
  "Cross Region" = "#F44336",
  "East Asia & Pacific" = "#1565C0",
  "Europe & Central Asia" = "#009688",
  "Latin America & Caribbean" = "#8BC34A",
  "Middle East & North Africa" = "#FFC107",
  "North America" = "#FF9800",
  "South Asia" = "#448AFF",
  "Sub-Saharan Africa" = "#AD1457"
)

vec_order_borders <- ggraph_bordering_countries %>%
  activate(edges) %>%
  as_tibble() %>%
  count(border_region, sort = TRUE) %>%
  pull(border_region)

vec_colours <- vec_colours[vec_order_borders]

ggraph_one_edge_plus <- ggraph_bordering_countries %>%
  activate(nodes) %>%
  mutate(node_degree = tidygraph::centrality_degree()) %>%
  filter(node_degree > 1)  %>%
  mutate(region_wb = fct_relevel(region_wb, vec_order_borders)) %>%
  activate(edges) %>%
  mutate(
    border_region = fct_expand(border_region, "North America"),
    border_region = fct_relevel(border_region)
  )

set.seed(1)
gg_graph_before_label <- ggraph_one_edge_plus %>%
  ggraph(layout = 'nicely') +
  aes(colour = region_wb) +
  geom_edge_link(aes(colour = border_region),
                 show.legend = FALSE,
                 edge_width = 0.5) +
  geom_node_point(
    aes(fill = region_wb),
    # show.legend = TRUE,
    colour = "black",
    size = 5,
    pch = 21
  ) +
  geom_node_text(
    aes(
      label = iso_a2,
      colour = ifelse(
        region_wb %in% c("Middle East & North Africa", "North America"),
        "black",
        "white"
      )
    ),
    size = 2.3,
    family = "Source Code Pro",
    fontface = "bold"
  )  +
  scale_colour_identity() +
  scale_edge_colour_manual(values = vec_colours, drop = FALSE) +
  scale_fill_manual(values = vec_colours, drop = FALSE) +
  guides(
    custom = guide_custom(
      title = "Cross regional borders",
      grob = grid::linesGrob(
        x = unit(c(0, 6.9), "cm"),
        y = unit(c(0, 0), "cm"),
        gp = grid::gpar(col = '#F44336', lwd = 3.5)
      )
    ),
    fill = guide_legend(title = "")
  ) +
  labs(
    title = "Who's connected to who?",
    subtitle = "Countries with at least one land border",
    x = "",
    y = "",
    caption = "@charliejhadley | Source: geodatasource.com/addon/country-borders"
  ) +
  theme_minimal(base_size = 12, base_family = "Roboto") +
  theme(
    legend.title = element_text(size = 12 * 1.618),
    panel.grid = element_blank(),
    plot.caption = element_text(
      size = 12,
      family = "Roboto",
      lineheight = 0.5,
      margin = margin(t = -5)
    ),
    plot.title = element_text(
      family = "Roboto",
      size = 12 * 1.618 ^ 3,
      margin = margin(t = 20)
    ),
    plot.subtitle = element_text(size = 12 * 1.618 ^ 2, margin = margin(b = -10)),
    panel.grid.major.y = element_blank(),
    axis.line = element_blank(),
    axis.ticks = element_blank(),
    axis.text = element_blank(),
    legend.text = element_text(size = 12 * 1.618),
    # legend.spacing.y = unit(2.0, "cm"),
    legend.key.size = unit(1, "cm"),
    legend.key = element_rect(color = NA, fill = NA),
    plot.caption.position = "plot"
  )

gg_graph_before_label

Let’s add in my labels, which are manually placed but use the node position extracted earlier to help place them.

gg_graph_countries <- gg_graph_before_label +
  geom_curve(
    data = tibble(
      x = 5.08 - 8.5,
      y = -4.96 ,
      xend = 5.08 - 1.3,
      yend = -4.96 - 0.2
    ),
    aes(x, y, yend = yend, xend = xend),
    
    inherit.aes = FALSE,
    arrow = arrow(length = unit(0.01, "npc")),
    curvature = 0.2,
    angle = 90
  ) +
  geom_label(
    data = tibble(
      x = 5.08 - 8.5,
      y = -4.96 - 0.5,
      label = str_wrap(
        "Canada isn't here. It only has a single land border with the US - which is included as it has two borders",
        30
      )
    ),
    aes(x, y, label = label),
    fill = colorspace::darken("#D8E4EA"),
    label.padding = unit(0.4, "lines"),
    hjust = 0,
    colour = "black",
    inherit.aes = FALSE,
    size = 4
  )

gg_graph_countries %>%
  ggsave(
    quarto_here("gg_graph_countries.png"),
    .,
    width = 4.25 * 3,
    height = 3.4 * 3,
    bg = "#D8E4EA"
  )

Reuse

Citation

BibTeX citation:
@online{hadley2024,
  author = {Hadley, Charlie},
  title = {Bordering Countries Graph},
  date = {2024-11-04},
  url = {https://visibledata.co.uk/posts/2024-10-28_bordering-country-graph/},
  langid = {en}
}
For attribution, please cite this work as:
Hadley, Charlie. 2024. “Bordering Countries Graph.” November 4, 2024. https://visibledata.co.uk/posts/2024-10-28_bordering-country-graph/.