Interactive map - pan, zoom, slider filter and tooltips are enabled.
Latest measurement of Internet speed around the world with ranking by country.
Live data from speedtest.net. Inspiration from visualcapitalist.

Select    
#' rvest + echarty demo

library(dplyr); library(rvest); library(xml2); library(echarty)
wp <- read_html('https://www.speedtest.net/global-index')
asof <- wp |> html_nodes('.month') |> head(1) |> xml_text()
e <- rng <- list(); etype <- c('Mobile','Cable'); erank <- c('rkmob','rkcab')
for(i in 1:2) {
  wt <- wp |> html_nodes('.country-results') |> magrittr::extract2(i+2) |> 
    html_table(header=TRUE) |> na.omit()
  wt <- wt[,-2]
  names(wt) <- c(erank[i],'country',etype[i])
  e[[i]] <- wt
  rng[[i]] <- c(min(wt[etype[i]]), max(wt[etype[i]]))
}
tmp <- full_join(e[[1]], e[[2]])
for(cry in c('Dem. Rep. Korea','Iceland','Greenland','Niger','Chad','Guinea','Central African Rep.','S. Sudan'))
  tmp <- tmp |> rbind(c('?',cry,0,'?',0))      # from world.js add some missing ones
tmp <- tmp |> mutate(country = case_when(    # change mismatching
  country=='Dominican Republic'  ~'Dominican Rep.',
  country=='Laos'   ~'Lao PDR',
  country=='South Korea'   ~'Korea',
  country=='Republic of the Union of Myanmar'   ~'Myanmar',
  country=='Western Sahara'   ~'W. Sahara',  
  country=='Equatorial Guinea'  ~'Eq. Guinea',
  country=='DR Congo'  ~'Dem. Rep. Congo',
  country=='Czechia'  ~'Czech Rep.',
  country=='North Macedonia'  ~'Macedonia',
  country=='Bosnia and Herzegovina'  ~'Bosnia and Herz.',
  TRUE ~ country)) |>
  mutate(rkmob = ifelse(is.na(rkmob), '?', rkmob)) |>
  mutate(Mobile = ifelse(is.na(Mobile), 0, Mobile)) |>
  mutate(Mobile = as.numeric(unname(Mobile)), Cable = as.numeric(unname(Cable)))
for(i in 1:2) {
  p <- ec.init(load='world')
  p$x$opts$title <- list(show=TRUE, text=paste('Global',etype[i],'Speed'), subtext=paste('as of',asof))
  p$x$opts$backgroundColor <- 'LightSteelBlue' 
  p$x$opts$geo$zoom <- 1.5
  p$x$opts$series[[1]]$data <- lapply(ec.data(tmp, 'names'), function(d) 
    list(name=d$country, value=unlist(unname(d[etype[i]])), rank=unlist(unname(d[erank[i]]))) )
  p$x$opts$visualMap <- list(type='continuous', calculable=TRUE, text=c('Mbps',''), 
                             range=rng[[i]], min=0, max=max(tmp[etype[i]]),
                             inRange= list(color=c('darkviolet', 'orangered', 'yellow')),
                             outOfRange= list(color='Grey') )
  p$x$opts$tooltip <- list(
    backgroundColor= 'rgba(30,30,30,0.5)', textStyle= list(color='#eee'),
    formatter=ec.clmn('%@<br /><b>%@</b> Mbps, rank  <b>%@</b>','name','value','rank') )
  p$x$opts$toolbox <- list(feature= list(saveAsImage= list(show= TRUE),
                                         dataView= list(readOnly= TRUE)) )
  p$x$opts$name <- etype[i]      # for radio-button selection
  e[[i]] <- p
}
e[[1]]$x$o2 <- e[[2]]$x$opts        # save 2nd plot
e[[1]]   # show 1st

Bypassing Shiny Server again. Here is the JavaScript code


var chopt = {p1:null, p2:null};
document.addEventListener("DOMContentLoaded", function() {
    const pp = getData();
    chopt.p1 = pp.x.opts;
    chopt.p2 = pp.x.o2;
    // widgets do Eval just once on init, so we need to do it here 'manually'
    chopt.p1.tooltip.formatter = chopt.p2.tooltip.formatter = eval('('+chopt.p1.tooltip.formatter+')');
});
function getData() {
  try {
    // find the htmlwidget
    const inlineJsonElement = document.querySelector(
      `script[type="application/json"][data-for^=htmlwidget-]`
    );
    const data = JSON.parse(inlineJsonElement.textContent);
    return data;
  } catch (err) {
    console.error(`Couldn't read JSON data from htmlwidget`, err);
  }
}
function rlick(myRadio) {
    
    let selectedValue = myRadio.value;

    let wt = document.querySelector("[id^=htmlwidget-]");  
        // get the ECharts object and replace its options
        let p = get_e_charts(wt.id);
        let oprev = p.getOption();

        let opti = null;
        switch(selectedValue) {
        case chopt.p1.name:  
            opti = chopt.p1;
            break;
        case chopt.p2.name:
            opti = chopt.p2;
            break;
        default:
            alert('selection not found');
        }
        if (opti!=null) {
      // preserve user's zoom/center values
          opti.geo.zoom = oprev.geo[0].zoom;
          opti.geo.center = oprev.geo[0].center;
          p.setOption(opti, true);
        }
};