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

耗尽温柔 提交于 2019-12-02 09:01:16

问题


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

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)

Supplement issues:
Plotly.restyle turns out to have the nasty habit of replacing all marker properties when you run it. In this case, we now succesfully update the color, but we loose all size and other style attributes as they are replaced by default values when Plotly.restyle runs without input for these properties. To get a fully working javascript solution, it seems we also need to collect the size input and such to fully restyle the markers.....


回答1:


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);


来源:https://stackoverflow.com/questions/55149416/mysterious-javascript-bug-that-is-depending-on-what-plot-object-names-are-used-i

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!