library(tidyverse)library(ggiraph)library(patchwork)library(showtext)library(magick)library(grid)library(cowplot)library(ggtext)theme_set_custom<-function(){# loading google fontssysfonts::font_add_google("Libre Franklin", "franklin")# using here::here() to get absolute path, and check if file exists firstfont_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 versionsysfonts::font_add_google("Libre Franklin", "franklin-medium")}showtext::showtext_auto()# applying ggplot2 themeggplot2::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 areawaffle_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 positioncreate_waffle_positions<-function(area_name, kills, complete_sequence_7_rows, padding_needed){total_tiles<-complete_sequence_7_rowsdeath_tiles<-killspositions<-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 areasall_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 argumentarea_positions<-all_positions|>filter(area==area_name)# determine svg path based on areasvg_path<-if(area_name=='Gaza Strip')'person-svgrepo.svg'else'person-svgrepo-orange.svg'# read and prepare svgsvg_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 alltheme_void()+theme( plot.margin =margin(t =5, r =10, b =20, l =10, unit ="pt")# increased margins for spacing)+# add area name at bottomgeom_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 tilesvg_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 svgsp<-p+svg_embedingsreturn(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 plotmain_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 plotinteractive_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 embedingsggiraph::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