2

I'm looking for a way to link actionButton clicks to the legend status true/legendonly that doesn't cause a re-rendering of the plotly object but instead alters the widget. Current demo app at the bottom does achieve the link, but does so through full redrawing of the plot.

I'm aiming to link the buttons that indicate keep/drop a cluster to the visualization of the data in a plot in both direction in such a way that the plot is updated, not rendered. My current solution does cause full rendering.

The interaction is that i.e. Buttons change legend/plot & legend changes buttons.

I added some images to explain the workflows.

I have build a test version for a bigger plot in my even bigger actual app, where the user has this view:

currentapp

Here the user can choose which clusters to remove for further processing by means of the in/out buttons.

Thanks to the previous question here I now have a test app where: - 1 clicking the legend changes the plot, and the button status on the left, so the user can use the plot to make the IN/OUT choices - 2 Whenever the plot re-renders, it now also reactivates the previous show/hide status of each trace.

point 1 is this work flow: enter image description here point two is simply the plot code using this line of code before the onRender

  if(values$colors) { for(i in seq_along(p1$x$data)){
  p1$x$data[[i]]$visible <- values$legenditems[[p1$x$data[[i]]$name]]}
}
 p1 %>% onRender(js, data = "tracesPlot1")

There is currently also a third interaction which causes traces to become hidden when the user clicks a button. That approach is the issue here. It currently follows the orange flow in the diagram below, but i'm hoping to change that by a javascript solution that avoids re-rendering of the plot: enter image description here

THE DEMO APP

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

js <- c(
  "function(el, x, inputName){",
  "  var id = el.getAttribute('id');",
  "  var d3 = Plotly.d3;",
  "  el.on('plotly_restyle', function(evtData) {",
  "    var out = {};",
  "    d3.select('#' + id + ' g.legend').selectAll('.traces').each(function(){",
  "      var trace = d3.select(this)[0][0].__data__[0].trace;",
  "      out[trace.name] = trace.visible;",
  "    });",
  "    Shiny.setInputValue(inputName, out);",
  "  });",
  "}")

YNElement <-    function(idx){sprintf("YesNo_button-%d", idx)}

ui <- fluidPage(
  fluidRow(
    column(2,
           h5("Keep/Drop choices linked to colorscheme 1"),
           uiOutput('YNbuttons')
    ),
    column(8,
           plotlyOutput("plot1")
    ),
    column(2,
           h5('Switch grouping'),
           actionButton(inputId = 'Switch', label = icon('refresh'), style = "color: #f7ad6e;   background-color: white;  border-color: #f7ad6e;
                        height: 40px; width: 40px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px")
           ), style = "margin-top:150px"
    ),
  verbatimTextOutput("tracesPlot1"),
  verbatimTextOutput("tracesPlot2")

  )

server <- function(input, output, session) {
  values <- reactiveValues(colors = T, NrOfTraces = length(unique(mtcars$cyl)))


  output$plot1 <- renderPlotly({
    print('plotting!')
    if(values$colors) { colors <- c('red', 'blue', 'green') } else {colors <- c('black', 'orange', 'gray')}
    p1 <- plot_ly()
    p1 <-  add_trace(p1, data = mtcars, x = ~disp, y = ~mpg, type = 'scatter', mode = 'markers', color = ~as.factor(cyl), colors = colors)
    p1 <- layout(p1, title = 'mtcars group by cyl with switching colors')
    p1 <- plotly_build(p1)

    if(values$colors) { for(i in seq_along(p1$x$data)){
      p1$x$data[[i]]$visible <- values$legenditems[[p1$x$data[[i]]$name]]}
    }
     p1 %>% onRender(js, data = "tracesPlot1")
  })


  observeEvent(input$Switch, { values$colors <- !values$colors    })

  ##### THIS BLOCK links buttons -> plot, but causes it to render all over again
### this interaction is what I would like to replace by javascript

    observeEvent(values$dYNbs_cyl_el, {
      legenditems <- values$dYNbs_cyl_el
      legenditems[which(legenditems == FALSE)] <- 'legendonly'
      legenditems[which(legenditems == TRUE )] <- 'TRUE'
      names(legenditems) <- sort(unique(mtcars$cyl))
      values$legenditems <- as.list(legenditems)
    })


  observeEvent(values$NrOfTraces, { 
    values$dYNbs_cyl_el <- rep(T,values$NrOfTraces)
    names(values$dYNbs_cyl_el) <- sapply(1:values$NrOfTraces, function(x) {YNElement(x)})
  })

  output$YNbuttons <- renderUI({
    req(values$NrOfTraces)
    lapply(1:values$NrOfTraces, function(el) {
      YNb <- YNElement(el)
      if(values$dYNbs_cyl_el[[YNb]] == T ) {
        div(actionButton(inputId = YNb, label = icon("check"), style = "color: #339FFF;   background-color: white;  border-color: #339FFF;height: 34px; width: 34px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px"))
      } else {
        div(actionButton(inputId = YNb, label = icon("times"), style = "color: #ff4d4d;   background-color: white;  border-color: #ff4d4d;height: 34px; width: 34px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px"))
      }
    })
  })  

  flipYNb_FP1 <- function(idx){
    YNb <- YNElement(idx)
    values$dYNbs_cyl_el[[YNb]] <- !values$dYNbs_cyl_el[[YNb]]
  }

  observe({
    lapply(1:values$NrOfTraces, function(ob) {
      YNElement <- YNElement(ob)
      observeEvent(input[[YNElement]], {
        flipYNb_FP1(ob)
      }, ignoreInit = T)
    })
  })

  observeEvent(input$tracesPlot1, {
    listTraces <- input$tracesPlot1
    values$legenditems <- listTraces ## this line would save the legend status even if we remove the observer for the values$dYNbs_cyl_el list
    listTracesTF <- gsub('legendonly', FALSE, listTraces)
    listTracesTF <- as.logical(listTracesTF)
    lapply(1:values$NrOfTraces, function(el) {
      if(el <= length(listTracesTF)) {
        YNb <- YNElement(el)
        if(values$dYNbs_cyl_el[[YNb]] != listTracesTF[el]) {
          values$dYNbs_cyl_el[[YNb]] <- listTracesTF[el]
        }
      }
    })
  })

  output$tracesPlot1 <- renderPrint({ unlist(input$tracesPlot1)  })
  output$tracesPlot2 <- renderPrint({ unlist(values$legenditems)  })


}
shinyApp(ui, server)

UPDATED Test app, with attempt to use the answer. not working still

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

# js <- c(
#   "function(el, x, inputName){",
#   "  var id = el.getAttribute('id');",
#   "  var d3 = Plotly.d3;",
#   "  el.on('plotly_restyle', function(evtData) {",
#   "    var out = {};",
#   "    d3.select('#' + id + ' g.legend').selectAll('.traces').each(function(){",
#   "      var trace = d3.select(this)[0][0].__data__[0].trace;",
#   "      out[trace.name] = trace.visible;",
#   "    });",
#   "    Shiny.setInputValue(inputName, out);",
#   "  });",
#   "}")


js2 <- c(
"function(el, x, inputName){",
"  var id = el.getAttribute('id');",
"  if(id == inputName){",
"    var data = el.data;",
"    $('[id^=btn]').on('click', function() {",
"      var index = parseInt(this.id.split('-')[1]);",
"       var trace = index -1; ",
"      var v0 = data[trace].visible || true;",
"      var v = v0 == true ? 'legendonly' : true;",
"      Plotly.restyle(el, {visible: v}, [trace]);",
"    });",
"  }",
"}")


YNElement <-    function(idx){sprintf("btn-%d", idx)}

ui <- fluidPage(
  fluidRow(
    column(2,
           h5("Keep/Drop choices linked to colorscheme 1"),
           uiOutput('YNbuttons')
    ),
    column(8,
           plotlyOutput("plot1")
    ),
    column(2,
           h5('Switch grouping'),
           actionButton(inputId = 'Switch', label = icon('refresh'), style = "color: #f7ad6e;   background-color: white;  border-color: #f7ad6e;
                        height: 40px; width: 40px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px")
           ), style = "margin-top:150px"
    ),
  verbatimTextOutput("tracesPlot1"),
  verbatimTextOutput("tracesPlot2")

  )

server <- function(input, output, session) {
  values <- reactiveValues(colors = T, NrOfTraces = length(unique(mtcars$cyl)))

  output$plot1 <- renderPlotly({
    print('plotting!')

    values$legenditemNames <- sort(unique(mtcars$cyl))

    if(values$colors) { colors <- c('red', 'blue', 'green') } else {colors <- c('black', 'orange', 'gray')}
    p1 <- plot_ly()
    p1 <-  add_trace(p1, data = mtcars, x = ~disp, y = ~mpg, type = 'scatter', mode = 'markers', color = ~as.factor(cyl), colors = colors)
    p1 <- layout(p1, title = 'mtcars group by cyl with switching colors')
    p1 <- plotly_build(p1)

    if(values$colors) { for(i in seq_along(p1$x$data)){
      p1$x$data[[i]]$visible <- values$legenditems[[p1$x$data[[i]]$name]]}
    }
     p1 %>% onRender(js2, data = "tracesPlot1")
  })


  observeEvent(input$Switch, { values$colors <- !values$colors    })

  ##### THIS BLOCK links buttons -> plot, but causes it to render all over again
    # observeEvent(values$dYNbs_cyl_el, {
    #   legenditems <- values$dYNbs_cyl_el
    #   legenditems[which(legenditems == FALSE)] <- 'legendonly'
    #   legenditems[which(legenditems == TRUE )] <- 'TRUE'
    #   names(legenditems) <- values$legenditemNames
    #   values$legenditems <- as.list(legenditems)
    # })


  observeEvent(values$NrOfTraces, { 
    values$dYNbs_cyl_el <- rep(T,values$NrOfTraces)
    names(values$dYNbs_cyl_el) <- sapply(1:values$NrOfTraces, function(x) {YNElement(x)})
  })

  output$YNbuttons <- renderUI({
    req(values$NrOfTraces)
    lapply(1:values$NrOfTraces, function(el) {
      YNb <- YNElement(el)
      if(values$dYNbs_cyl_el[[YNb]] == T ) {
        div(actionButton(inputId = YNb, label = icon("check"), style = "color: #339FFF;   background-color: white;  border-color: #339FFF;height: 34px; width: 34px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px"))
      } else {
        div(actionButton(inputId = YNb, label = icon("times"), style = "color: #ff4d4d;   background-color: white;  border-color: #ff4d4d;height: 34px; width: 34px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px"))
      }
    })
  })  

  flipYNb_FP1 <- function(idx){
    YNb <- YNElement(idx)
    values$dYNbs_cyl_el[[YNb]] <- !values$dYNbs_cyl_el[[YNb]]
  }

  observe({
    lapply(1:values$NrOfTraces, function(ob) {
      YNElement <- YNElement(ob)
      observeEvent(input[[YNElement]], {
        flipYNb_FP1(ob)
      }, ignoreInit = T)
    })
  })

  observeEvent(input$tracesPlot1, {
    listTraces <- input$tracesPlot1
    values$legenditems <- listTraces
    listTracesTF <- gsub('legendonly', FALSE, listTraces)
    listTracesTF <- as.logical(listTracesTF)
    lapply(1:values$NrOfTraces, function(el) {
      if(el <= length(listTracesTF)) {
        YNb <- YNElement(el)
        if(values$dYNbs_cyl_el[[YNb]] != listTracesTF[el]) {
          values$dYNbs_cyl_el[[YNb]] <- listTracesTF[el]
        }
      }
    })
  })

  output$tracesPlot1 <- renderPrint({ unlist(input$tracesPlot1)  })
  output$tracesPlot2 <- renderPrint({ unlist(values$legenditems)  })


}
shinyApp(ui, server)
Mark
  • 2,789
  • 1
  • 26
  • 66
  • Are you sure this is possible? From what I read, shiny keep re-rendering stuff (on-demand). – Roman Luštrik Feb 24 '19 at 09:39
  • Yes in previous questions we use javascript to alter single data points of a plot, or restyle the axis of an existing object https://stackoverflow.com/questions/47365402/change-a-single-point-in-a-plotly-scatter3d-in-r-shiny and https://stackoverflow.com/questions/45968501/shiny-update-the-clicked-point-to-highlight-it?noredirect=1&lq=1 and there are various more on making alterations to plotly by means of javascript – Mark Feb 24 '19 at 09:46
  • Yeah, that's what I figured. – Roman Luštrik Feb 24 '19 at 09:47

1 Answers1

3

Could you try this:

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

js <- c(
  "function(el, x){",
  "  var data = el.data;",
  "  $('#btn').on('click', function() {",
  "    var traceName = $('#selector').val();",
  "    $.each(data, function(index,value){",
  "      if(value.name == traceName){",
  "        var v0 = data[index].visible || true;",
  "        var v = v0 == true ? 'legendonly' : true;",
  "        Plotly.restyle(el, {visible: v}, [index]);",
  "      }",
  "    });",
  "  });",
  "}")

ui <- fluidPage(
  plotlyOutput("plot"),
  selectInput("selector", "", choices = c("drat", "wt", "qsec")),
  actionButton("btn", "Show/hide")
)

server <- function(input, output, session) {

  output$plot <- renderPlotly({
    p <- plot_ly()
    for(name in c("drat", "wt", "qsec"))
    {
      p = add_markers(p, x = as.numeric(mtcars$cyl), y = as.numeric(mtcars[[name]]), name = name)
    }
    p %>% onRender(js)
  })

}

shinyApp(ui, server)

enter image description here


In case you have multiple plots:

js <- c(
  "function(el, x, plotid){",
  "  var id = el.getAttribute('id');",
  "  if(id == plotid){",
  "    var data = el.data;",
  "    $('#btn').on('click', function() {",
  "      var traceName = $('#selector').val();",
  "      $.each(data, function(index,value){",
  "        if(value.name == traceName){",
  "          var v0 = data[index].visible || true;",
  "          var v = v0 == true ? 'legendonly' : true;",
  "          Plotly.restyle(el, {visible: v}, [index]);",
  "        }",
  "      });",
  "    });",
  "  }",
  "}")

then

  output$plot <- renderPlotly({
    p <- plot_ly()
    for(name in c("drat", "wt", "qsec"))
    {
      p = add_markers(p, x = as.numeric(mtcars$cyl), y = as.numeric(mtcars[[name]]), name = name)
    }
    p %>% onRender(js, data = "plot")
  })

In case you have multiple buttons, one for each trace:

js <- c(
  "function(el, x, plotid){",
  "  var id = el.getAttribute('id');",
  "  if(id == plotid){",
  "    var data = el.data;",
  "    $('[id^=btn]').on('click', function() {",
  "      var index = parseInt(this.id.split('-')[1]);",
  "      var v0 = data[index].visible || true;",
  "      var v = v0 == true ? 'legendonly' : true;",
  "      Plotly.restyle(el, {visible: v}, [index]);",
  "    });",
  "  }",
  "}")

ui <- fluidPage(
  plotlyOutput("plot"),
  actionButton("btn-0", "drat"),
  actionButton("btn-1", "wt")
)

server <- function(input, output, session) {

  output$plot <- renderPlotly({
    p <- plot_ly()
    for(name in c("drat", "wt", "qsec"))
    {
      p = add_markers(p, x = as.numeric(mtcars$cyl), y = as.numeric(mtcars[[name]]), name = name)
    }
    p %>% onRender(js, data = "plot")
  })

}

shinyApp(ui, server)

That does not work for your example. That's because the buttons are created via renderUI, and they do not exist yet when the plot is rendered the first time.

The only solution I've been able to find is the following one. Instead of attaching the onclick-event listeners in the callback of the plotly, I attach them in the onclick attribute of the buttons:

js <- c(
  "function toggleLegend(id){",
  "  var plot = document.getElementById('plot1');",
  "  var data = plot.data;",
  "  var index = parseInt(id.split('-')[1]) - 1;",
  "  var v0 = data[index].visible || true;",
  "  var v = v0 == true ? 'legendonly' : true;",
  "  Plotly.restyle(plot, {visible: v}, [index]);",
  "}")

ui <- fluidPage(
  tags$head(
    tags$script(HTML(js))
  ),
  fluidRow(
    ......

  output$YNbuttons <- renderUI({
    req(values$NrOfTraces)
    lapply(1:values$NrOfTraces, function(el) {
      YNb <- YNElement(el)
      if(values$dYNbs_cyl_el[[YNb]] == TRUE) {
        div(actionButton(inputId = YNb, label = icon("check"), 
                         style = "color: #339FFF;   background-color: white;  border-color: #339FFF;height: 34px; width: 34px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px", 
                         onclick = "toggleLegend(this.id);"))
      } else {
        div(actionButton(inputId = YNb, label = icon("times"), 
                         style = "color: #ff4d4d;   background-color: white;  border-color: #ff4d4d;height: 34px; width: 34px; border-radius: 6px;  border-width: 2px; text-align: center;  line-height: 50%; padding: 0px; display:block; margin: 2px", 
                         onclick = "toggleLegend(this.id);"))
      }
    })
  })  

And no use of onRender.

But that works for one plot only. If you want to link the buttons to multiple plots, assuming the buttons id have form btn-plot2-5, then do:

js <- c(
  "function toggleLegend(id){",
  "  var ids = id.split('-');",
  "  var plotid = ids[1];",
  "  var index = parseInt(ids[2])-1;",
  "  var plot = document.getElementById(plotid);",
  "  var data = plot.data;",
  "  var v0 = data[index].visible || true;",
  "  var v = v0 == true ? 'legendonly' : true;",
  "  Plotly.restyle(plot, {visible: v}, [index]);",
  "}")
Stéphane Laurent
  • 75,186
  • 15
  • 119
  • 225
  • mmm that does work, but is quite different that listening to a list of buttons with IDs Perhaps it is easier if I pm you about it so we can discuss? – Mark Feb 24 '19 at 11:03
  • hey Stephane, i was just about to answer you that it's great to have the plot specific edit, and the multiple buttons is getting in the right direction, but what I have in my situation is 1 button representing one trace (not a different version of the plot as in your example. if you look at my dummy app, the 3 buttons on the right represent trace 1, 2, 3. The trick with the stripsplit should be perfect. I shall edit it a bit I guess since my buttons run from 1-n and traces i know from 0-n – Mark Feb 24 '19 at 11:55
  • ok, I added this: " var trace = index -1; ", and replaced index with trace down from there in the javascript which works in your test app, but I found out I can't get it to work in my test app, due to the very different approach of how you make the plotly plot with add_markers in a loop. Is there a way to simply link the javascript to toggle traces by number rather than by name? That way it doesn't matter whether (in my real app) the traces are 1,2,3etc or 'name1', name2, .... the buttons always end with a nr, and the traces always have a serial nr from 0-n – Mark Feb 24 '19 at 12:10
  • *"toggle traces by number"* : that's exactly what the code does. I don't understand the problem. – Stéphane Laurent Feb 25 '19 at 07:56
  • I added an edit with my type of approach to making the plot, and your javascript, but I can't get it to work in this case – Mark Feb 25 '19 at 08:17
  • 1
    Hi @Mark. In the `data` argument of `onRender`, you have to pass the id of the plot : `data = "plot1"`, not `data = "tracesPlot1"`. – Stéphane Laurent Feb 25 '19 at 08:41
  • Got it working in your version, but not in mine... I've send the code to you on linkedin – Mark Feb 25 '19 at 20:44
  • @Mark See my edit. That works if you want to link the buttons to only one plot. Do you need that for multiple plots? – Stéphane Laurent Feb 26 '19 at 08:48
  • Yeah I have 3 copies of this plot function in my app, all the same except for id and linked to unique setting buttons – Mark Feb 26 '19 at 16:06
  • @Mark So we can repeat the same JS code for each plot ? See my edit to see what I mean. – Stéphane Laurent Feb 26 '19 at 16:14
  • Ah no, to clarify, there will be plot1 plot2 plot3 with each buttons called i.e. plot1-btn-1, plot1-btn-2........ Plot3-btn-3. Each plot it's own buttons and the number of buttons varies per plot depending on the model outcome for clustering – Mark Feb 26 '19 at 17:08
  • And each plot will thus also create its own, uniquely named list of legend statuses from the other js function (which we need to make work simultaneously with the js listening to the buttons so that we listen to both legend to change buttons, and to buttons to change legend – Mark Feb 26 '19 at 17:09
  • @Mark Ok. If the buttons id contain the plot id that's nice. See my edit. – Stéphane Laurent Feb 26 '19 at 17:26
  • hey Stephane! Not sure if you checked your Linkedin messages. I got it to work, both directions without loops being triggered, but I have one or 2 new minor follow up issues. Can we talk on chat at some point? – Mark Mar 08 '19 at 13:14