rstudio / r2d3

R Interface to D3 Visualizations
https://rstudio.github.io/r2d3
Other
516 stars 105 forks source link

re-initialize r2d3() in shiny renderd3? #52

Open AWKruijt opened 5 years ago

AWKruijt commented 5 years ago

Hi all,

first of, thanks! r2d3 is amazing and inspired me to my first attempts at d3.js: a line graph with draggable points. Now I am trying to incorporate this in a shiny where users can modify various settings. However: it seems that every input made through the shiny inputs results in a new instance of the d3.js plot.... initially I "hackslved' this by giving the plot on opaque background but soon came to realize that all the 'former' instances of the d3.js remain active (and start throwing warnings when the number of pp (input$npp) is changed, because the new data does not fit the old instance's datastructure, which of course eventually leads to clogged up memory.

So: is there a way to reset/reinitialize/shutdownandrestart the d3.js (and its datastructure) within a shiny session?

Many thanks in advance!

Shiny code:

library(shiny)
library(r2d3)
library(data.table)

# Define UI for application that shows the D3 plot and returns the updated correlation:
ui <- fluidPage(
  titlePanel(""),
  sidebarLayout(

    sidebarPanel(style = "background-color: #ffffff; border-color: gray92 ; border-width: 2px", width = 3, 
                 numericInput("npp",
                             "number of 'participants'",
                             value = 8),
                 numericInput("meanBI",
                              "mean bias'",
                              value = 20),
                 numericInput("sdBI",
                              "sd bias'",
                              value = 0),
                 numericInput("meanRT",
                              "mean response times'",
                              value = 20),
                 numericInput("sdRT",
                              "sd response times'",
                              value = 10)
    ),
   mainPanel(
        fluidRow(
          h3("corr:"),
          verbatimTextOutput("corrComputed")),
       fluidRow(
          d3Output("d3"), 
          h4("drag the dots..."))
)))

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

    observeEvent(input$npp, {
## I guess that here I need some statement that results in resetting the d3.js 
## simply removing all datastructures (dfw, df, and dataD3Adjusted) doesn't do the trick. 
  })

  observeEvent(c(input$npp, input$meanBI, input$sdBI, input$meanRT, input$sdRT), {

  npp <<- input$npp
  meanBI <- input$meanBI
  sdBI <- input$sdBI
  meanRT <- input$meanRT
  sdRT <- input$sdRT

df <- data.table(ID = seq(1,npp), 
               BI = rnorm(npp, meanBI, sdBI) )
df$IT = rnorm(npp, meanRT, sdRT) + .5*df$BI 
df$CT = df$IT - df$BI

dfw <- melt(df, id.vars = c("ID", "BI"), variable.name = "trialtype", value.name = "RT")
dfw$tt <- as.numeric(dfw$trialtype)

output$corrComputed <- renderText({
    cor(dfw$RT [dfw$trialtype == "IT"], dfw$RT [dfw$trialtype == "CT"])
  })

  output$d3 <- renderD3({
        r2d3(data=dfw, d3_version = 4, script = "dragmultilinechart.js")
  }) 

observeEvent(input$dataD3Adjusted, {

   dataD3Adjusted <<- input$dataD3Adjusted
    dfw$RT <- as.numeric(dataD3Adjusted [names(dataD3Adjusted) == "RT"])

    output$corrComputed <- renderText({
    cor(dfw$RT [dfw$trialtype == "IT"], dfw$RT [dfw$trialtype == "CT"])
    })

  })
  })
}

# Run the application 
shinyApp(ui = ui, server = server)

and the js:

 // !preview r2d3 data= data.frame(ID = c(1,1,2,2,3,3,4,4,5,5), tt = c(1, 2, 1, 2, 1, 2, 1, 2, 1, 2), RT = c(14.4,      19.3, 22.0, 27.0, 20.7, 25.74, 16.9, 21.9, 18.6, 23.6))

 // svg.append("rect")
 //    .attr("width", "100%")
 //    .attr("height", "100%")
 //    .attr("fill", "white");

 var dById = d3.nest()
   .key(function(d) {
     return d.ID;
   })
   .entries(data);

 var margin = {
     top: 40,
     right: 40,
     bottom: 40,
     left: 40
   },
   width = 450 - margin.left - margin.right,
   height = 300 - margin.top - margin.bottom;

 var color = d3.scaleOrdinal()
   .range(["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99"]);

 var x = d3.scaleLinear()
   .range([0.25 * width, 0.75 * width])
   .domain([1, 2]);

 // I think that a continuous scale is required for the drag to work, but
 // add a second ordinal x axis scale to show instead of the linear one:
 var xOrd = d3.scaleOrdinal()
   .range([0.25 * width, 0.75 * width])
   .domain(["IT", "CT"]);

 var yrange = d3.max(data, function(d) {return d.RT;}) - d3.min(data, function(d) {return d.RT;});  

 var y = d3.scaleLinear()
   .rangeRound([height, 0])
   .domain([d3.min(data, function(d) {return d.RT;})  - 0.3*yrange, d3.max(data, function(d) {return d.RT;})  + 0.3     *yrange ]);

 var xAxis = d3.axisBottom(xOrd), // use the Ordinal x scale
   yAxis = d3.axisLeft(y);

 // Define the line by data variables
 var connectLine = d3.line()
   .x(function(d) {
     return x(d.tt);
   })
   .y(function(d) {
     return y(d.RT);
   });

 svg.append('rect')
   .attr('class', 'zoom')
   .attr('cursor', 'move')
   .attr('fill', 'none')
   .attr('pointer-events', 'all')
   .attr('width', width)
   .attr('height', height)
   .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

 var focus = svg.append("g")
   .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

 focus.selectAll('lines')
   .data(dById)
   .enter().append("path")
   .attr("class", "line")
   .attr("d", function(d) {
     return connectLine(d.values);
   })
   .attr("stroke", function(d) {
     return color(d.key);
   })
   .attr('stroke-width', 4);

 focus.selectAll('circles')
   .data(dById)
   .enter().append("g")
   .attr("class", "dots")
   .selectAll("circle")
   .data(function(d) {
     return d.values;
   })
   .enter().append("circle")
   .attr("cx", function(d) {
     return x(d.tt);
   })
   .attr("cy", function(d) {
     return y(d.RT);
   })
   .attr("r", 6)
   .style('cursor', 'pointer')
   .attr("fill", function(d) {
     return color(d.ID);
   })
   .attr("stroke", function(d) {
     return color(d.ID);
   });

 focus.append('g')
   .attr('class', 'axis axis--x')
   .attr('transform', 'translate(0,' + height + ')')
   .call(xAxis);

 focus.append('g')
   .attr('class', 'axis axis--y')
   .call(yAxis);

 /// drag stuff: 

 let drag = d3.drag()
   .on('start', dragstarted)
   .on('drag', dragged)
   .on('end', dragended);

 focus.selectAll('circle')
   .call(drag);

 // focus.selectAll('line')
 //    .call(drag);

 function dragstarted(d) {
   d3.select(this).raise().classed('active', true);
 }

 function dragged(d) {
   dragNewY = y.invert(d3.event.y);
   d.RT = dragNewY;
   d3.select(this)
     .attr('cx', d => x(d.tt))
     .attr('cy', d => y(d.RT));

   focus.selectAll('path').attr("d", function(d) {
     return connectLine(d.values);
   }); 
 }

 function dragended(d) {

   Shiny.setInputValue(
   "dataD3Adjusted",
  //dById,
  data,
   {priority: "event"}
   );

   d3.select(this).classed('active', false);

 //.attr("d", function(d) {return d; });

 }
AWKruijt commented 5 years ago

Just found this, which I think is the same issue: https://stackoverflow.com/questions/44348963/replace-existing-r-shiny-htmlwidget-data-with-new-data

The responses there didn't help me much though :/ I suspect that each call to r2d3 creates a new htmlwidget (each of them using the same set of bindings? given that the old instances respond to new input too?) and that what's needed is to delete either just the old widget or the widget plus the bindings from within shiny before making a new call to r2d3 - correct?

jonkatz2 commented 5 years ago

This is 6 months late, but it might help someone else with the same question. I think your problem is in your d3 script. For each draw-iteration you add points and lines, and you should also remove unused values from the prior iteration. The d3 update pattern (https://bl.ocks.org/mbostock/3808218) shows a select, then a data join, then a merge, then a remove. In my limited experience r2d3 doesn't make good use of the merge function so I find just the select, join, and remove to be the way to go, and (maybe incorrectly) I just go ahead and remove all elements before drawing new ones for each iteration. for example, your lines are:

focus.selectAll('lines')
   .data(dById)
   .enter().append("path")
   .attr("class", "line")
   .attr("d", function(d) {
     return connectLine(d.values);
   })
   .attr("stroke", function(d) {
     return color(d.key);
   })
   .attr('stroke-width', 4);

To clear all existing lines and draw new ones you could try:

 // start with the svg object provided by r2d3
svg.selectAll('lines').remove();
focus.selectAll('lines')
   .data(dById)
   .enter().append("path")
   .attr("class", "line")
   .attr("d", function(d) {
     return connectLine(d.values);
   })
   .attr("stroke", function(d) {
     return color(d.key);
   })
   .attr('stroke-width', 4);

You might be able to simplify this even more by removing all g elements at the top of your d3 script.