3

Related to a previous post I made, I would like to update points in one Highcharter plot to change color when invoking a click event on a separate, but synchronized, Highcharter plot in R Shiny, in both directions. I am able to synchronize two Highcharter plots in R Shiny, but when the user toggles an input in the UI, the JavaScript for the click event no longer works (both in synchronization and in the actual click event for either chart). See this reproducible example:

##PACKAGES
library(shiny)
library(shinyWidgets)
library(shinyjs)
library(dplyr)
library(tidyverse)
library(albersusa) ## remotes::install_github("hrbrmstr/albersusa")
library(highcharter)
library(usdata)
library(data.table)

states <- data.frame(
  name  = rep(state.abb,4),
  state = rep(state.name,4),
  metric = c(rep("YES",100),rep("NO",100)),
  value  = sample(100:5000,200)
)

#for synchronizing Highcharts 1 and 2
testJS <- JS("function() {
                       let currentY = this.name
                       charts = Highcharts.charts;
                       charts.forEach(function(chart, index) {
                       chart.series.forEach(function(series, seriesIndex) {
                       series.points.forEach(function(point, pointsIndex) {
                       if (point.name == currentY) {
                               point.setState('hover');
                               point.update({color:'red'})
                       }
                       })
                       });
                       });
        }")

ui <- fluidPage(
  
  tags$script(src = "https://code.highcharts.com/mapdata/countries/us/us-all.js"),
  
  fluidRow(
    radioButtons(inputId = "toggle",label="toggle it", 
                 choices = c("YES","NO")),
    column(width=5,highchartOutput("map1")),
    column(width=5,highchartOutput("bar1"))
    )
  )

server <- function(input, output, session) {
  
  #create rate change
  df1_num<- reactive({
    states %>%
    filter(metric == input$toggle) %>%
    group_by(name) %>%
    mutate(
      first = dplyr::first(value),
      last = dplyr::last(value)
    ) %>%
    distinct(metric,state,first,last) %>%
    mutate(
      #increase/decrease rate change
      rate  = round(((last-first)/first)*100,1),
    )
  })
  
  #HIGHCHART 1 (map)
  output$map1 <- renderHighchart({
    #US map of percent change in population trends
    hcmap("countries/us/us-all",
          data   = df1_num(),
          joinBy = c("hc-a2","name"),
          value  = "rate",
          borderColor = "#8d8d8d",
          nullColor = "#D3D3D3",
          download_map_data = FALSE
    ) %>%
      hc_plotOptions(series = list(
        point = list(
          events = list(
            click = testJS
          )
        )
      )
      )
  })
  
  #HIGHCHART 2 (barchart)
  output$bar1 <- renderHighchart({
    
    test <- df1_num() %>%
      arrange(rate) %>%
      rowid_to_column("orderrate")
    setDT(test)[,testit:=.GRP,by=orderrate]
    
    highchart() %>%
      hc_add_series(
        data = test,
        hcaes(
          x     = state,
          y     = rate,
          group = orderrate),
        showInLegend = FALSE,
        type = "bar",
        color = "blue",
        polar = FALSE
      ) %>%
      hc_plotOptions(series = list(
        point = list(
          events = list(
            click = testJS
          )
        )
      )
      )
  })

}

shinyApp(ui = ui, server = server)

In my previous post, I received an excellent answer that fixes the R Shiny user input change by retaining the click event functionality, however this solution does not synchronize the charts (replace the value testJS with):

JS("function(){
       this.update({color:'red'})
    }")

I realize my main struggle here is creating harmony between the amazing Highcharter wrapper and raw Highcharts JS, so any ideas (or clear direction I'm simply unaware of) would be so helpful!

Thanks!

thothal
  • 16,690
  • 3
  • 36
  • 71
jcoder
  • 35
  • 6

1 Answers1

3

Problem

The problem is that apparently, whenever you change the shiny inputs a new highcharter object is added to Highchart.charts and the previous ones are not deleted but set to undefined. Thus, in your first forEach loop you loop over undefined array items whcih do not have (for obvious reasons) a series slot.

Solution

Hence, you must make sure that chart is truthy, to execute the inner loop only if it is indeed a chart. A simple if clause will do the trick:

testJS <- JS("
function() {
    let currentY = this.name;
    let charts = Highcharts.charts;
    charts.forEach(function(chart, index) {
        if (chart) { // add this
            chart.series.forEach(function(series, seriesIndex) {
                series.points.forEach(function(point, pointsIndex) {
                    if (point.name == currentY) {
                        point.setState('hover');
                        point.update({
                            color: 'red'
                        })
                    }
                })
            });
        }
    });
}")

How did I find out?

Whenever dealing with JavaScript the developer console is your friend , press Ctrl-Shift-J to bring it up. In your original code you will see an error message which gives you:

VM58:6 Uncaught TypeError: Cannot read properties of undefined (reading 'series')
   at eval (eval at tryEval (htmlwidgets.js:252), <anonymous>:6:22)
   at Array.forEach (<anonymous>)
   at h.eval (eval at tryEval (htmlwidgets.js:252), <anonymous>:4:15)
   at highcharts.js:18
   at Array.forEach (<anonymous>)
   at B (highcharts.js:18)
   at h.g.firePointEvent (highcharts.js:280)
   at a.onContainerClick (highcharts.js:308)

This gives you a hint that you are trying to read a series property of an undefined. Adding console.log(charts) to your code and re-executing shows that

(2) [a, a] // before input change
(4) [undefined, undefined, a, a] // after input change

indicating that once you press a shiny control (and re-render the highchart) a new object is added ti charts but the old ones are not deleted, but set to undefined which lead to the solution proposed.

thothal
  • 16,690
  • 3
  • 36
  • 71