text: d3v3.VirtualScroller = function() {
      var enter = null,
          update = null,
          exit = null,
          data = [],
          dataid = null,
          svg = null,
          viewport = null,
          totalRows = 0,
          position = 0,
          rowHeight = 24,
          totalHeight = 0,
          minHeight = 0,
          viewportHeight = 0,
          visibleRows = 0,
          delta = 0,
          dispatch = d3v3.dispatch("pageDown", "pageUp");
  
      function virtualscroller(container) {
          function render(resize) {
              if (resize) { // re-calculate height of viewport and # of visible row
                  viewportHeight = parseInt(viewport.style("height"));
                  visibleRows = Math.ceil(viewportHeight / rowHeight) + 1; // add 1 more row for extra overlap; avoids visible add/remove at top/bottom 
              }
              var scrollTop = viewport.node().scrollTop;
              totalHeight = Math.max(minHeight, (totalRows * rowHeight));
              svg.style("height", totalHeight + "px") // both style and attr height values seem to be respected
                  .attr("height", totalHeight);
              var lastPosition = position;
              position = Math.floor(scrollTop / rowHeight);
              delta = position - lastPosition;
              scrollRenderFrame(position);
          }
  
  
          function scrollRenderFrame(scrollPosition) {
              container.attr("transform", "translate(0," + (scrollPosition * rowHeight) + ")"); // position viewport to stay visible
              var position0 = Math.max(0, Math.min(scrollPosition, totalRows - visibleRows + 1)), // calculate positioning (use + 1 to offset 0 position vs totalRow count diff) 
                  position1 = position0 + visibleRows;
              container.each(function() { // slice out visible rows from data and display
                  var rowSelection = container.selectAll(".row")
                      .data(data.slice(position0, Math.min(position1, totalRows)), dataid);
                  rowSelection.exit().call(exit).remove();
                  rowSelection.enter().append("g")
                      .attr("class", "row")
                      .call(enter);
                  rowSelection.order();
                  var rowUpdateSelection = container.selectAll(".row:not(.transitioning)"); // do not position .transitioning elements
                  rowUpdateSelection.call(update);
                  rowUpdateSelection.each(function(d, i) {
                      d3v3.select(this).attr("transform", function(d) {
                          return "translate(0," + ((i * rowHeight)) + ")";
                      });
                  });
              });
  
              if (position1 > (data.length - visibleRows)) { // dispatch events 
                  dispatch.pageDown({
                      delta: delta
                  });
              } else if (position0 < visibleRows) {
                  dispatch.pageUp({
                      delta: delta
                  });
              }
          }
  
          virtualscroller.render = render; // make render function publicly visible 
          viewport.on("scroll.virtualscroller", render); // call render on scrolling event
          render(true); // call render() to start
      }
  
      virtualscroller.render = function(resize) { // placeholder function that is overridden at runtime
      };
  
      virtualscroller.data = function(_, __) {
          if (!arguments.length) return data;
          data = _;
          dataid = __;
          return virtualscroller;
      };
  
      virtualscroller.dataid = function(_) {
          if (!arguments.length) return dataid;
          dataid = _;
          return virtualscroller;
      };
  
      virtualscroller.enter = function(_) {
          if (!arguments.length) return enter;
          enter = _;
          return virtualscroller;
      };
  
      virtualscroller.update = function(_) {
          if (!arguments.length) return update;
          update = _;
          return virtualscroller;
      };
  
      virtualscroller.exit = function(_) {
          if (!arguments.length) return exit;
          exit = _;
          return virtualscroller;
      };
  
      virtualscroller.totalRows = function(_) {
          if (!arguments.length) return totalRows;
          totalRows = _;
          return virtualscroller;
      };
  
      virtualscroller.rowHeight = function(_) {
          if (!arguments.length) return rowHeight;
          rowHeight = +_;
          return virtualscroller;
      };
  
      virtualscroller.totalHeight = function(_) {
          if (!arguments.length) return totalHeight;
          totalHeight = +_;
          return virtualscroller;
      };
  
      virtualscroller.minHeight = function(_) {
          if (!arguments.length) return minHeight;
          minHeight = +_;
          return virtualscroller;
      };
  
      virtualscroller.position = function(_) {
          if (!arguments.length) return position;
          position = +_;
          if (viewport) {
              viewport.node().scrollTop = position;
          }
          return virtualscroller;
      };
  
      virtualscroller.svg = function(_) {
          if (!arguments.length) return svg;
          svg = _;
          return virtualscroller;
      };
  
      virtualscroller.viewport = function(_) {
          if (!arguments.length) return viewport;
          viewport = _;
          return virtualscroller;
      };
  
      virtualscroller.delta = function() {
          return delta;
      };
  
      d3v3.rebind(virtualscroller, dispatch, "on");
  
      return virtualscroller;
  };