Mysterious javascript bug that is depending on what plot object names are used in R shiny app

In the app below there is some weird mystery going on: Why can I not use certain names for my plots?

The app was build as a dummy app to change the color of traces in plots. 1 button affects trace x in 2 plots, and there is one button for each trace.

The button names contain the name of both plots, and the trace nr they target.

When the plots are named plot1, plot2, plot3, plot4, it all works. but the actual program it is intended for uses different plot names. Changing all instances of plot1 into 'FP1plot' in the code works, changing plot3 into 'FP2plot' still works, but most names used to replace plot3 and plot 4 DO NOT work, and also using several other names to replace plot1 or plot2 with suddenly cause the plot to break and not render anymore.

Me and a more experienced javascript user who helped me develop this dummy app, are both clueless what causes this random behaviour that some names work, and others do not.

The intended names I wish to use are: 'FP1plot', 'CleanFP1', 'FP2plot', 'CleanFP2' in order.

here is the bugged shiny app with the javascript in it:

library(plotly)
library(shiny)
library(colourpicker)
library(htmlwidgets)

jscolor <- c(
  "function toggleColor(id){",
  "  var color = document.getElementById(id).value;", # get the color of the colourpicker
  "  var ids = id.split('-');", # split the ids
  "  var plotAid = ids[2];", #get the id of plotA (FP1plot or 3)
  "  var plotBid = ids[3];", #get the id of plotB (plot2 or 4)
  "  var index = parseInt(ids[4]) -1;", #get the trace number to target
  "  var plotA = document.getElementById(plotAid);", #get the plot element
  "  var dataA = plotA.data;", #access the plot data
  "  var markerA = dataA[index].marker;", #access the plot's markers
  "  markerA.color = color;",  # set the marker color
  "  Plotly.restyle(plotA, {marker: markerA}, [index]);", #restyle plotA
  "  var plotB = document.getElementById(plotBid);", # repeat steps for plot2
  "  var dataB = plotB.data;",
  "  var markerB = dataB[index].marker;",
  "  markerB.color = color;",
  "  Plotly.restyle(plotB, {marker: markerB}, [index]);",
  "};"
)

colourInput2 <- function(inputId, label, value = "white", 
                         showColour = c("both", "text", "background"), 
                         palette = c("square", "limited"), allowedCols = NULL,
                         allowTransparent = FALSE, returnName = FALSE, 
                         onchange){
  input <- colourInput(inputId, label, value, showColour, palette, 
                       allowedCols, allowTransparent, returnName)
  attribs <- c(input$children[[2]]$attribs, onchange = onchange)
  input$children[[2]]$attribs <- attribs
  input
}


ui <- fluidPage(
  tags$head(
    tags$script(HTML(jscolor))  ## to add the javascript to the app
  ),
  fluidRow(
    column(4,plotlyOutput("FP1plot")),
    column(4,plotlyOutput("plot2")),
    column(4,uiOutput('buttons_color_1')
    )
  ),
  fluidRow(
    column(4,plotlyOutput("FP2plot")),
    column(4,plotlyOutput("plot4")),
    column(4,uiOutput('buttons_color_2'))
  )
)

server <- function(input, output, session) {
  #functions to make colorinput IDs
  COLElement_1 <-    function(idx){sprintf("COL_button_FP1plot_plot2_%d",idx)}
  COLElement_2 <-    function(idx){sprintf("COL_button_FP2plot_plot4_%d",idx)}


  TheColors <- c('#383838', '#5b195b','#1A237E', '#000080', '#224D17', '#cccc00', '#b37400',  '#990000', 
                 '#505050',  '#a02ca0',  '#000099', '#2645e0', '#099441', '#e5e500', '#cc8400', '#cc0000', 
                 '#737373', '#e53fe5', '#0000FF', '#4479e1',  '#60A830', '#ffff00','#e69500', '#ff0000', 
                 '#b2b2b2', '#eb6ceb', '#6666ff', '#d0a3ff', '#9FDA40',  '#ffff7f', '#ffa500', '#ff4c4c',
                 '#d9d9d9',  '#f198f1',  '#C5CAE9','#BBDEFB','#D9DF1D', '#ffffcc','#ffc04d', '#ff9999')

  values <- reactiveValues(colors1 = TheColors, colors2 = sort(TheColors))
  lapply(c(1:2), function(i) {
    output[[paste('buttons_color_', i,sep = '')]] <- renderUI({
      inputs <- lapply(1:3, function(x) {  ## 3 in my app changes based on clustering output of my model
        Idname <- if(i == 1) { COLElement_1(x) } else {COLElement_2(x) }
        colour_input <- colourInput2(inputId = Idname, label = NULL,
                                     palette = "limited", allowedCols = TheColors,
                                     value = isolate(values[[paste('colors', i, sep = '')]][x]),
                                     showColour = "background", returnName = FALSE, 
                                     onchange = "toggleColor(this.id)")
        div(colour_input,
            style = "height: 30px; width: 30px; border-radius: 6px;  border-width: 2px; text-align:center; padding: 0px; display:block; margin: 10px"
        )
      })
      do.call(tagList, inputs)
    })
    # useless: outputOptions(output, paste('buttons_color_', i,sep = ''), suspendWhenHidden=FALSE)
  })


  myplotly <- function(THEPLOT, xvar, setnr) {
    markersize <- 2
    markerlegendsize <- 10
    colors <- isolate ({values[[paste('colors', setnr, sep = '')]]  })
    p <- plot_ly(source = paste('plotlyplot', THEPLOT, sep = '.'))
    p <-  add_trace(p, data = mtcars, x = mtcars[[xvar]], y = ~mpg, type = 'scatter', mode = 'markers', color = ~as.factor(cyl), colors = colors)
    p <- layout(p, title = 'mtcars group by cyl with switching colors')
    p <- plotly_build(p)
    p 
  }

  output$FP1plot <- renderPlotly({ myplotly('FP1plot', 'hp', 1) })
  output$plot2 <- renderPlotly({ myplotly('plot2', 'disp', 1)})
  output$FP2plot <- renderPlotly({ myplotly('FP2plot','hp', 2)})
  output$plot4 <- renderPlotly({ myplotly('plot4', 'disp', 2)})

}

shinyApp(ui, server)

Updated app with workaround from S.L. but plots now render with wrong color on startup

enter image description here

library(plotly)
library(shiny)
library(colourpicker)
library(htmlwidgets)

jscolor <- c(
  "function toggleColor(id){",
  "  var color = document.getElementById(id).value;", # get the color of the colourpicker
  "  var ids = id.split('_');", # split the id
  "  var plotAid = ids[2];", #get the id of plotA (plotw or 3)
  "  var plotBid = ids[3];", #get the id of plotB (CleanFP1 or 4)
  "  var index = parseInt(ids[4]) - 1;", #get the trace number to target
  "  var plotA = document.getElementById(plotAid);", #get the plot element
  "  if(plotA.innerHTML !== ''){",
  "    var dataA = plotA.data;", #access the plot data
  "    var markerA = dataA[index].marker;", #access the plot's markers
  "    markerA.color = color;",  # set the marker color
  "    Plotly.restyle(plotA, {marker: markerA}, [index]);", #restyle plotA
  "  }",
  "  var plotB = document.getElementById(plotBid);", # repeat steps for CleanFP1
  "  if(plotB.innerHTML !== ''){",
  "    var dataB = plotB.data;",
  "    var markerB = dataB[index].marker;",
  "    markerB.color = color;",
  "    Plotly.restyle(plotB, {marker: markerB}, [index]);",
  "  }",
  "};"
)

colourInput2 <- function(inputId, label, value = "white", 
                         showColour = c("both", "text", "background"), 
                         palette = c("square", "limited"), allowedCols = NULL,
                         allowTransparent = FALSE, returnName = FALSE, 
                         onchange){
  input <- colourInput(inputId, label, value, showColour, palette, 
                       allowedCols, allowTransparent, returnName)
  attribs <- c(input$children[[2]]$attribs, onchange = onchange)
  input$children[[2]]$attribs <- attribs
  input
}


ui <- fluidPage(
  tags$head(
    tags$script(HTML(jscolor))  ## to add the javascript to the app
  ),
  fluidRow(
    column(4,plotlyOutput("FP1plot")),
    column(4,plotlyOutput("CleanFP1")),
    column(4,uiOutput('buttons_color_1')
    )
  ),
  fluidRow(
    column(4,plotlyOutput("FP2plot")),
    column(4,plotlyOutput("CleanFP2")),
    column(4,uiOutput('buttons_color_2'))
  )
)

server <- function(input, output, session) {
  #functions to make colorinput IDs
  COLElement_1 <-    function(idx){sprintf("COL_button_FP1plot_CleanFP1_%d",idx)}
  COLElement_2 <-    function(idx){sprintf("COL_button_FP2plot_CleanFP2_%d",idx)}

  TheColors <- c('#383838',  '#000080',  '#b37400',
                 '#737373', '#e53fe5', '#0000FF', '#4479e1',  '#60A830', '#ffff00','#e69500', '#ff0000', 
                 '#b2b2b2', '#eb6ceb', '#6666ff', '#d0a3ff', '#9FDA40',  '#ffff7f', '#ffa500', '#ff4c4c',
                 '#d9d9d9',  '#f198f1',  '#C5CAE9','#BBDEFB','#D9DF1D', '#ffffcc','#ffc04d', '#ff9999')

  values <- reactiveValues(colors1 = TheColors, colors2 = sort(TheColors))
  lapply(c(1:2), function(i) {
    output[[paste('buttons_color_', i,sep = '')]] <- renderUI({
      inputs <- lapply(1:3, function(x) {  ## 3 in my app changes based on clustering output of my model
        Idname <- if(i == 1) { COLElement_1(x) } else {COLElement_2(x) }
        colour_input <- colourInput2(inputId = Idname, label = NULL,
                                     palette = "limited", allowedCols = TheColors,
                                     value = isolate(values[[paste('colors', i, sep = '')]][x]),
                                     showColour = "background", returnName = FALSE, 
                                     onchange = "toggleColor(this.id)")
        div(colour_input,
            style = "height: 30px; width: 30px; border-radius: 6px;  border-width: 2px; text-align:center; padding: 0px; display:block; margin: 10px"
        )
      })
      do.call(tagList, inputs)
    })
    # useless: outputOptions(output, paste('buttons_color_', i,sep = ''), suspendWhenHidden=FALSE)
  })


  myplotly <- function(THEPLOT, xvar, setnr) {
    markersize <- 2
    markerlegendsize <- 10
    colors <- isolate ({values[[paste('colors', setnr, sep = '')]]  })
    p <- plot_ly(source = paste('plotlyplot', THEPLOT, sep = '.'))
    p <-  add_trace(p, data = mtcars, x = mtcars[[xvar]], y = ~mpg, type = 'scatter', mode = 'markers', color = ~as.factor(cyl), colors = colors)
    p <- layout(p, title = 'mtcars group by cyl with switching colors')
    p <- plotly_build(p)
    p 
  }

  output$FP1plot <- renderPlotly({ myplotly('FP1plot', 'hp', 1) })
  output$CleanFP1 <- renderPlotly({ myplotly('CleanFP1', 'disp', 1)})
  output$FP2plot <- renderPlotly({ myplotly('FP2plot','hp', 2)})
  output$CleanFP2 <- renderPlotly({ myplotly('CleanFP2', 'disp', 2)})

}

shinyApp(ui, server)

1 answer

  • answered 2019-03-14 11:18 Stéphane Laurent

    I don't understand why the behavior depends on the ids but here is a workaround:

    jscolor <- c(
      "function toggleColor(id){",
      "  var color = document.getElementById(id).value;", # get the color of the colourpicker
      "  var ids = id.split('_');", # split the id
      "  var plotAid = ids[2];", #get the id of plotA (plotw or 3)
      "  var plotBid = ids[3];", #get the id of plotB (plot2 or 4)
      "  var index = parseInt(ids[4]) - 1;", #get the trace number to target
      "  var plotA = document.getElementById(plotAid);", #get the plot element
      "  if(plotA.innerHTML !== ''){",
      "    var dataA = plotA.data;", #access the plot data
      "    var markerA = dataA[index].marker;", #access the plot's markers
      "    markerA.color = color;",  # set the marker color
      "    Plotly.restyle(plotA, {marker: markerA}, [index]);", #restyle plotA
      "  }",
      "  var plotB = document.getElementById(plotBid);", # repeat steps for plot2
      "  if(plotB.innerHTML !== ''){",
      "    var dataB = plotB.data;",
      "    var markerB = dataB[index].marker;",
      "    markerB.color = color;",
      "    Plotly.restyle(plotB, {marker: markerB}, [index]);",
      "  }",
      "};"
    )
    

    EDIT

    There's a problem on startup with this workaround, as noted by the OP. Here is another workaround. It just adds a delay of 1ms.

    jscolor <- c(
      "function toggleColor0(id){",
      "  var color = document.getElementById(id).value;", # get the color of the colourpicker
      "  var ids = id.split('_');", # split the id
      "  var plotAid = ids[2];", #get the id of plotA (plotw or 3)
      "  var plotBid = ids[3];", #get the id of plotB (plot2 or 4)
      "  var index = parseInt(ids[4]) - 1;", #get the trace number to target
      "  var plotA = document.getElementById(plotAid);", #get the plot element
      "  var dataA = plotA.data;", #access the plot data
      "  var markerA = dataA[index].marker;", #access the plot's markers
      "  markerA.color = color;",  # set the marker color
      "  Plotly.restyle(plotA, {marker: markerA}, [index]);", #restyle plotA
      "  var plotB = document.getElementById(plotBid);", # repeat steps for plot2
      "  var dataB = plotB.data;",
      "  var markerB = dataB[index].marker;",
      "  markerB.color = color;",
      "  Plotly.restyle(plotB, {marker: markerB}, [index]);",
      "};",
      "function toggleColor(id){",
      "  setTimeout(function(){toggleColor0(id);}, 1);",
      "}"
    )
    

    In fact this also works with a delay of 0ms:

    setTimeout(function(){toggleColor0(id);}, 0);