Journalism in the face of Political Violence
The state of Journalism in Gaza
Karim K. Kardous

When “a [plot] is worth a thousands words”.

Show the code
library(tidyverse)
library(ggiraph)
library(patchwork)
library(showtext)
library(magick)
library(grid)
library(cowplot)
library(ggtext)

theme_set_custom <- function() {
  
  # loading google fonts
  sysfonts::font_add_google("Libre Franklin", "franklin")
  
  # using here::here() to get absolute path, and check if file exists first
  font_path <- "../../renv/library/macos/R-4.5/aarch64-apple-darwin20/sysfonts/fonts/Libre_Franklin/static/LibreFranklin-Medium.ttf"
  
  if (file.exists(font_path)) {
    sysfonts::font_add(family = "franklin-medium", regular = font_path)
  } else {
    # Fallback to Google Fonts version
    sysfonts::font_add_google("Libre Franklin", "franklin-medium")
  }
  
  showtext::showtext_auto()
  
  # applying ggplot2 theme
  ggplot2::theme_set(
    ggplot2::theme_minimal(base_family = "franklin") +
      ggplot2::theme(
        panel.background = ggplot2::element_rect(fill = "#f8f8f8", color = NA),
        plot.background = ggplot2::element_rect(fill = "#f8f8f8", color = NA)
      )
  )
}
theme_set_custom()

base_table <- tibble(
  area = c('Gaza Strip', 'Mexico', 'Syria', 'Pakistan', 'Lebanon', 'Colombia', 'Ukraine', 'Sudan', 'DRC', 'India'),
  kills = c(240, 21, 17, 12, 11, 10, 10, 9, 7, 6)
) |> 
  arrange(desc(kills))

# create waffle data for each area
waffle_data <- base_table |> 
  mutate(
    remainders = kills %% 7,
    needs_padding = if_else(kills > 7 & remainders > 0, 1, 0),
    complete_sequence_7_rows = if_else(needs_padding == 1, kills - remainders + 7, kills),
    padding_needed = complete_sequence_7_rows - kills,
    no_of_rows = ceiling(complete_sequence_7_rows / 7)
  )

# create expanded data for each tile position
create_waffle_positions <- function(area_name, kills, complete_sequence_7_rows, padding_needed) {
  total_tiles <- complete_sequence_7_rows
  death_tiles <- kills
  
  positions <- tibble(
    tile_id = 1:total_tiles,
    area = area_name,
    type = c(rep("deaths", death_tiles), rep("padding", padding_needed)),
    row_pos = ceiling(tile_id / 7),
    col_pos = ((tile_id - 1) %% 7) + 1
  ) |>
    filter(type == "deaths")  # only keeping death tiles for visualization
  # another way is to use {waffle} package but i had to 'white out' non death tiles after adding remainders to the actual no. of kills per area
  # (waffle lacks a certain customization capability on this front) 
  
  return(positions)
}

# generate positions for all areas
all_positions <- pmap_dfr(
  list(waffle_data$area, waffle_data$kills, waffle_data$complete_sequence_7_rows, waffle_data$padding_needed),
  create_waffle_positions
)

# create individual waffle charts with labels (no spacers for alignment)
create_interactive_waffle <- function(area_name, kills, no_of_rows) {
  
  # filter positions for area argument
  area_positions <- all_positions |> filter(area == area_name)
  # determine svg path based on area
  svg_path <- if(area_name == 'Gaza Strip') 'person-svgrepo.svg' else 'person-svgrepo-orange.svg'
  # read and prepare svg
  svg_image <- magick::image_read_svg(svg_path, width = 120, height = 120)
  
  p <- ggplot(area_positions, aes(x = col_pos, y = row_pos)) +
    geom_point_interactive(
      aes(
        data_id = area,
        tooltip = paste0(
          '<span style="color: white !important; font-weight: normal; display: block; padding: 8px 12px 4px 12px; white-space: nowrap;">', 
          area_name, 
          '</span>',
          '<span style="color: #2D2D2D !important; font-weight: normal; display: block; padding: 4px 12px 8px 12px; white-space: nowrap;">',
          'Journalists killed: ', kills,
          '</span>'
        )
      ),
      size = 15,
      alpha = 0,
      color = "transparent"
    ) +
    scale_x_continuous(limits = c(0.5, 7.5), expand = c(0, 0)) +
    scale_y_continuous(limits = c(-1.5, max(waffle_data$no_of_rows) + 0.5), expand = c(0, 0)) +  # same y-limits for all
    theme_void() +
    theme(
      plot.margin = margin(t = 5, r = 10, b = 20, l = 10, unit = "pt")  # increased margins for spacing
    ) +
    # add area name at bottom
    geom_text(
      data = tibble(x = 4, y = -1, label = area_name),
      aes(x = x, y = y, label = label),
      hjust = 0.5,
      vjust = -1,
      size = 6,
      family = 'franklin',
      inherit.aes = FALSE
    ) + 
    geom_text(
      data = tibble(x = 4, y = if_else(kills == 240, 35, 5), label = kills),
      aes(x = x, y = y, label = label),
      hjust = if_else(kills == 6, 1.9, 0.5),
      vjust = if_else(kills != 240, 3, .5),
      size = 4.5,
      family = 'franklin',
      fontface = 'bold',
      inherit.aes = FALSE
    )
  
  # add svg annotations for each death tile
  svg_embedings <- map(1:nrow(area_positions), ~ {
    pos <- area_positions[.x, ]
    annotation_custom(
      grob = grid::rasterGrob(image = svg_image),
      xmin = pos$col_pos - 0.4, xmax = pos$col_pos + 0.4,
      ymin = pos$row_pos - 0.4, ymax = pos$row_pos + 0.4
    )
  })

  
  # add all svgs
  p <- p + svg_embedings
  
  return(p)
}

# create all waffle charts 
waffle_charts <- pmap(
  list(waffle_data$area, waffle_data$kills, waffle_data$no_of_rows),
  create_interactive_waffle
)

# create main plot
main_plot <- wrap_plots(waffle_charts, nrow = 1) +
  plot_annotation(
    title = "Top 10 countries and territories with the most journalists killed by political violence\n",
    subtitle = "7 October 2023 — 29 August 2025<br>Journalists killed <img src='person-grey.png' width='16' height='16'> = 1<br>",
    caption = 'Data and Plot Replicated from ACLED',
    theme = theme(
      plot.title = element_text(size = 28, hjust = 0, family = 'franklin', face = "bold", color = '#2D2D2D', margin = margin(t = -5, b = 5)),
      plot.subtitle = ggtext::element_markdown(size = 18, hjust = 0, family = 'franklin', color = '#5A5A5A', margin = margin(t = -4, b = 4)),
      plot.caption.position = 'panel',
      plot.caption = element_text(size = 18, hjust = 0, family = 'franklin', color = '#5A5A5A')
    )
  )

# create interactive plot
interactive_plot <- ggiraph::girafe(
  ggobj = main_plot,
  options = list(
    ggiraph::opts_hover(css = "opacity:0.8;"),
    ggiraph::opts_hover_inv(css = "opacity:0.3;"), # not working because of svg embedings
    ggiraph::opts_tooltip(
      css = "
        background: linear-gradient(to bottom, #2D2D2D 0%, #2D2D2D 50%, white 50%, white 100%);
        padding: 0;
        border-radius: 1px;
        font-family: 'franklin';
        font-size: 10px;
        box-shadow: 0 2px 8px rgba(0,0,0,0.3);
        border: none;
        margin: 0;
        min-width: 75px;
        width: auto;
      "
    ),
    ggiraph::opts_sizing(rescale = TRUE),
    ggiraph::opts_toolbar(saveaspng = FALSE)
  )
)

interactive_plot