Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
AWKruijt opened this issue Jan 23, 2019 · 2 comments
Open

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

AWKruijt opened this issue Jan 23, 2019 · 2 comments

Comments

@AWKruijt
Copy link

AWKruijt commented Jan 23, 2019

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
Copy link
Author

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
Copy link

jonkatz2 commented Jun 28, 2019

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.

lgsilvaesilva added a commit to voronoys/barchartraceR2D3 that referenced this issue Mar 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants