2

I have created a htmlwidget for ony of my R shiny application. In my widget I am unable to replace the existing data with new data, whenever the widget render function is called multiple times, this resulted in a bug and the old data was not removed and new data is appended with old data.

I created a reproducible example which shows my problem. When you click on 'ui2' button, the application goes to another ui, and from that ui when you click on 'reset' button, it will take you back to default ui and you can see that there will be two histograms in the same container instead of one. This is because I am unable to replace the old data with new data in my htmlwidget.

Reproducible shiny app:

#library(devtools)
#install_github('radhikesh/d3Viz')
library(shiny)
library(d3Viz)
ui <- shinyUI(fluidPage(

fluidRow(
column(12,uiOutput("page"))) 
))

ui1 <- function() {
fluidPage(

fluidRow( column(11, 
               div(
                 id = "ResetBtn5", actionButton("ui2", "ui2", style="color: 
#fff; background-color: #337ab7; border-color: #2e6da4")
               ))),

br(),

fluidRow(
column(width = 6, d3HistogramOutput("d3Hist"))
),

br(),
br(),
br(),
br(),
br(),
br(),

fluidRow(
column(12, DT::dataTableOutput("DataAric"))
)

)}

ui2 <- function() {
fluidPage(

fluidRow( column(11, 
                 div(
                   id = "ResetBtn5", actionButton("ResetButtonAric", 
"Reset", style="color: #fff; background-color: #337ab7; border-color: 
#2e6da4")
                 ))),

br(),


fluidRow(
  column(width = 6, d3HistogramOutput("d3Hist2"))
)


)}


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

dataset <- data.frame(lpu = c('Apple','Banana','Apple'), amount = 
                      c(20,10,10))

output$page <- renderUI({
div(class = "outer", do.call(bootstrapPage, c("", ui1())))
})



output$d3Hist <- renderD3Histogram({ 
k <- input$DataAric_rows_all
if (length(k) > 0)
{

  trialAnnotate <- data.frame(table(dataset[k, "lpu"]))
  dataset = data.frame(lpu = trialAnnotate$Var1,amount = trialAnnotate$Freq)
  d3Histogram(dataset = dataset)

 }
})

output$DataAric <-
DT::renderDataTable(
dataset,
  options = list(
    pageLength = nrow( dataset),
    bLengthChange=F
  ),
  rownames = FALSE,
  escape = FALSE
)

output$d3Hist2 <- renderD3Histogram({ 

dataset <- data.frame(lpu = c('Apple','Banana','Orange'), amount = 
                        c(30,20,25))
d3Histogram(dataset = dataset)

})

observeEvent(input$ui2, {


output$page <- renderUI({
  div(class = "outer", do.call(bootstrapPage, c("", ui2())))
})


})

observeEvent(input$ResetButtonAric, {


output$page <- renderUI({
  div(class = "outer", do.call(bootstrapPage, c("", ui1())))
})


})

})

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

d3Histogram.js code

HTMLWidgets.widget({
  
  name: 'd3Histogram',
  
  type: 'output',
  
  renderOnNullValue: true,
  
  factory: function(el, width, height) {
    
    return {
      
      renderValue: function(x) {
        
        var container = d3.select(el).append("div").attr("id", "container");
        
        var barPadding = 1;
        
        /*
          var data=[
            {"lpu":"lpu1","amount":"20"},
            {"lpu":"lpu2","amount":"40"},
            {"lpu":"lpu3","amount":"60"},
            {"lpu":"lpu4","amount":"10"},
            {"lpu":"lpu5","amount":"80"},
            {"lpu":"lpu6","amount":"30"},
            {"lpu":"lpu7","amount":"20"},
            {"lpu":"lpu8","amount":"40"},
            {"lpu":"lpu9","amount":"60"},
            {"lpu":"lpu10","amount":"10"},
            {"lpu":"lpu11","amount":"80"},
            {"lpu":"lpu12","amount":"30"},
            {"lpu":"lpu13","amount":"20"},
            {"lpu":"lpu14","amount":"40"},
            {"lpu":"lpu15","amount":"60"},
            {"lpu":"lpu16","amount":"10"},
            {"lpu":"lpu17","amount":"80"},
            {"lpu":"lpu18","amount":"30"}
            ];
          */
            
            var dataset = HTMLWidgets.dataframeToD3(x.dataset);
            
            var width=2000,
            height=300;
            //radius=100,
            // padding=100;
            
            var margin = {top: 100, right: 50, bottom: 40, left: 50};
            
            var xLPU=d3.scale.ordinal();
            var yLPU=d3.scale.linear();
            
            var xLPUAxis = d3.svg.axis()
            .scale(xLPU)
            .orient("bottom");
            
            var yLPUAxis = d3.svg.axis()
            .scale(yLPU)
            .orient("left")
            .ticks(20, "??????.");
            
            
            var svg1 = d3.select("#container").append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
            
            xLPU.domain(dataset.map(function(d){return d.lpu;}))
            .rangeBands([0, width]);
            yLPU.domain([0,d3.max(dataset, function(d) { return d.amount; })])
            .range([height,0]);
            svg1.append("g")
            .attr("class","x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(xLPUAxis);      
            
            svg1.append('g')
            .attr('class','y axis')
            .call(yLPUAxis);
            
            
            svg1.selectAll('rect').data(dataset).enter().append('rect')
            .attr('x', function(d) {
              return xLPU(d.lpu);
            })
            .attr('y',function(d) {
              return yLPU(d.amount);
            })
            .attr('width',xLPU.rangeBand()-3)
            .attr('height', function(d) {
              return height - yLPU(d.amount);
            })
            .attr('fill','teal');
            // .attr("fill", function(d) {
              // return "rgb(0, 0, " + (d * 10) + ")";
              //  });
            
            svg1.selectAll(".bartext")
            .data(dataset)
            .enter()
            .append("text")
            .attr("class", "bartext")
            .attr("text-anchor", "middle")
            .attr("fill", "black")
            .attr("x", function(d,i) {
              return xLPU(d.lpu)+xLPU.rangeBand()/2;
            })
            .attr("y", function(d,i) {
              return yLPU(d.amount) + (-3);
            })
            .text(function(d){
              return d.amount   
            });
            
            svg1.selectAll('.axis line, .axis path')
            .style({'stroke': 'Black', 'fill': 'none', 'stroke-width': '2px'});
            
            svg1.append("text")      // text label for the x axis
            //.attr("x", 265 )
            //.attr("y", 240 )
            //.style("text-anchor", "middle")
            .attr("transform", "translate(" + (width / 2) + " ," + (height + margin.bottom) + ")")
            .text("Year")
            .attr('font-family','sans-serif')
            .attr('font-size','15px')
            .attr('fill','black');
            
            
            svg1.append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 0 - margin.left)
            .attr("x",0 - (height / 2))
            .attr("dy", "1em")
            .style("text-anchor", "middle")
            .text("Count")
            .attr('font-family','sans-serif')
            .attr('font-size','15px')
            .attr('fill','black');
            
            
            
      },
      
      resize: function(width, height) {
        
        
      }
      
    };
  }
});

I think, I know the problem, that I have to replace the old data with new data in my java-script binding, but I am new to java-script, and couldn't figure it out. Any help will be much appreciated.

Thanks

Radhikesh

radhikesh93
  • 870
  • 9
  • 25

2 Answers2

0

I am not sure if this is the most efficient answer, but I had the same issue and solved it by removing the child nodes of the container before creating the new child.

Try using the following while loop at the beginning of the renderValue function

while (document.getElementById(el.id).hasChildNodes()) {
    document.getElementById(el.id).removeChild(document.getElementById(el.id).lastChild);
}
mbillig
  • 16
  • 3
0

Your 'htmlwidget` sends messages regularly to the browser including data. This means it will bind new data to the DOM and the javascript will run again. While the data was used to draw the SVG, it still exists independent of the SVG. The issue is not about the new data (R took care of that), it's about the old SVG maintaining it's state.

In your code, you have no way to remove the existing plot/svg (even if new data was sent) in your JS file. I usually use something like:

this.element.innerHTML = '';

where this is the current scope of the widget. element is el in the widget. Setting innerHTML = ''; clears the DOM element and allows your script to append a new image.

A workflow for your widget should include this kind cleaning prior to any further drawing. More complex widgets can transition() from the previous state to the new state in d3 but likely requires a custom binding.

Ryan Morton
  • 2,605
  • 1
  • 16
  • 19