rd3.jshtmlwidgetsr2d3

Moving from r2d3 to htmlwidget


I'm trying to get the right workflow to move something i created using {r2d3} to {htmlwidgets}. For example, the following code works in r2d3 and creates a very simple x-axis:

// set up constants used throughout script
const margin = { top: 80, right: 100, bottom: 80, left: 100 };
const plotWidth = 800 - margin.left - margin.right;
const plotHeight = 400 - margin.top - margin.bottom;

// set width and height of svg element (plot + margin)
svg.attr("width", plotWidth + margin.left + margin.right)
   .attr("height", plotHeight + margin.top + margin.bottom);  // Set plot background opacity

// create plot group and move it
let plotGroup = svg.append("g")
                   .attr("transform",
                         "translate(" + margin.left + "," + margin.top + ")");

// x-axis
let xAxis = d3.scaleLinear()
    .domain([0, 1])
    .range([0, plotWidth]);

// add x-axis to plot
plotGroup.append("g")
   .attr("transform", "translate(0," + plotHeight + ")")
   .call(d3.axisBottom(xAxis).tickFormat(d3.format(".0%")));

enter image description here

However, when I try to create an R package using {htmlwidgets} nothings gets plotted. This is my htmlwidget factory:

function factory(el, width, height) {

  return {
    setupSvg: () => {
      d3.select(el).selectAll('*').remove();
      svg = d3.select(el).append('svg'); // Assign to the higher-scoped svg variable
      return svg;
    },

    draw: function() {
      // set up constants used throughout script
      const margin = { top: 80, right: 100, bottom: 80, left: 100 };
      const plotWidth = 800 - margin.left - margin.right;
      const plotHeight = 400 - margin.top - margin.bottom;

      // set width and height of svg element (plot + margin)
      svg.attr("width", plotWidth + margin.left + margin.right)
         .attr("height", plotHeight + margin.top + margin.bottom);  // Set plot background opacity

      // create plot group and move it
      let plotGroup = svg.append("g")
                         .attr("transform",
                               "translate(" + margin.left + "," + margin.top + ")");

      // x-axis
      let xAxis = d3.scaleLinear()
          .domain([0, 1])
          .range([0, plotWidth]);

      // add x-axis to plot
      plotGroup.append("g")
         .attr("transform", "translate(0," + plotHeight + ")")
         .call(d3.axisBottom(xAxis).tickFormat(d3.format(".0%")));

    },

    renderValue: function() {
      svg = this.setupSvg();
    },

    resize: function(newWidth, newHeight) {
      console.log('resize w, h', newWidth, newHeight);
      width = newWidth;
      height = newHeight;
      // Update the dimensions of the SVG element
      svg.attr('width', newWidth);
      svg.attr('height', newHeight);
      // Re-render the plot
      this.renderValue();
    }
  };
}

But nothing gets plotted. When I inspect the html code i can confirm that:

enter image description here

Finally, this is my R code for calling the widget:

#' Visualize Probabilities
#'
#' A function to visualize probabilities using a lollipop chart
#'
#' @title lollipops
#'
#' @return A HTML widget object.
#' @export
#'
lollipops <-
  function(data,
           width = NULL,
           height = NULL,
           elementId = NULL) {
    # forward options using x
    opts = list(data = dataframeToD3(data.frame(data)))

    # Define sizing policy
    sizingPolicy = htmlwidgets::sizingPolicy(
      defaultWidth = 400,
      defaultHeight = 400,
      browser.fill = TRUE
    )
    # create widget
    htmlwidgets::createWidget(
      name = 'lollipops',
      opts,
      width = width,
      height = height,
      package = 'vizdraws',
      elementId = elementId,
      sizingPolicy = sizingPolicy
    )
  }

What am I missing?


Solution

  • If I am reading correctly, you were really close. Here is a minimal example taking advantage of https://github.com/ramnathv/htmlwidgets/issues/305.

    I think the primary reason your code is not working is the lack of this.draw() in renderValue(). I made a couple of very minor adjustments that I think will improve the finished product. Let me know if you run into any more trouble, and I'll be happy to help.

    library(htmlwidgets)
    library(htmltools)
    
    widget_js <- tags$script(HTML("
    HTMLWidgets.widget({
    
      name: 'lollipops',
    
      type: 'output',
      
      factory: function(el, width, height) {
      
        return {
          svg: null,
          
          setupSvg: function() {
            d3.select(el).selectAll('*').remove();
            // not necessary but more convenient to access later with this.svg
            this.svg = d3.select(el).append('svg'); // Assign to the higher-scoped svg variable
          },
      
          draw: function() {
            // set up constants used throughout script
            const margin = { top: 80, right: 100, bottom: 80, left: 100 };
            const plotWidth = 800 - margin.left - margin.right;
            const plotHeight = 400 - margin.top - margin.bottom;
            const svg = this.svg;
      
            // set width and height of svg element (plot + margin)
            svg.attr('width', plotWidth + margin.left + margin.right)
               .attr('height', plotHeight + margin.top + margin.bottom);  // Set plot background opacity
      
            // create plot group and move it
            let plotGroup = svg.append('g')
                               .attr('transform',
                                     'translate(' + margin.left + ',' + margin.top + ')');
      
            // x-axis
            let xAxis = d3.scaleLinear()
                .domain([0, 1])
                .range([0, plotWidth]);
      
            // add x-axis to plot
            plotGroup.append('g')
               .attr('transform', 'translate(0,' + plotHeight + ')')
               .call(d3.axisBottom(xAxis).tickFormat(d3.format('.0%')));
      
          },
      
          renderValue: function() {
            this.setupSvg();
            this.draw();
          },
      
          resize: function(newWidth, newHeight) {
            console.log('resize w, h', newWidth, newHeight);
            const width = newWidth;
            const height = newHeight;
            // Update the dimensions of the SVG element
            this.svg.attr('width', newWidth);
            this.svg.attr('height', newHeight);
            // Re-render the plot
            this.renderValue();
          }
        };
      }
    })
    "))
    
    browsable(tagList(
      d3r::d3_dep_v7(),
      widget_js,
      htmlwidgets::createWidget('lollipops', x = list(), width = "100%")
    ))