Skip to main content

jbrnbrg

JavaScript in RStudio with r2d3

RStudio, an IDE for R, has made tons of improvements over the past 5 years and one of them is the ability to run non-R code (including the output from said non-R code) inline, within an RMarkdown document.

In light of this functionality, today’s post will run through an example of how to run JavaScript - specifically, d3.js, a JS library for “manipulating documents based on data” - within RStudio using the library r2d3. Let’s jump in:

library(r2d3)          # R interface to D3 visualizations    
library(tidyverse)     # data wrangle
library(glue)          # string manipulation
# via https://bl.ocks.org/heybignick/3faf257bbbbc7743bb72310d03b86ee8
myjson_path <- glue("https://raw.githubusercontent.com/",
                    "jbrnbrg/msda-datasets/master/miserables.json")

myjson <- jsonlite::read_json(myjson_path)
f_script <- here::here("assets\\postdatasets\\d3jsinrstudio\\forcegraph.js")
s_script <- here::here("assets\\postdatasets\\d3jsinrstudio\\forcegraph.css")

# explicitly call out the locations of your d3js and css
# within the r2d3 function: 
r2d3(data = myjson, d3_version = 4, script = f_script, css = s_script)

Above I’ve plotted an interactive, force-directed graph - try clicking and dragging on the nodes to move them around.

I recommend checking out the D3 gallery that contains a number of chart options that you might not find within ggplot options. With D3, one can utilize chart types that are totally custom and not available from any existing package.

JavaScript for forcegraph.js

As an aside, with RStudio and r2d3 you type code for d3js directly in the markdown document by including d3 in the chunk header (i.e. the chunk header would look like {d3} instead of the usual {r}) - this allows you to include d3js inline within the markdown and see the results as you work - pretty cool!

// Based on: https://bl.ocks.org/heybignick/3faf257bbbbc7743bb72310d03b86ee8

var color = d3.scaleOrdinal(d3.schemeCategory20);
var radius = 5;


var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

r2d3.onRender(function(graph, svg, width, height, options) {
  var link = svg.append("g")
      .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
      .attr("stroke-width", function(d) { return Math.sqrt(d.value); });

  var node = svg.append("g")
      .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
      .attr("r", radius)
      .attr("fill", function(d) { return color(d.group); })
      .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended));

  node.append("text")
      .text(function(d) { return d.id; });

  simulation
      .nodes(graph.nodes)
      .on("tick", ticked);

  simulation.force("link")
      .links(graph.links);

  function ticked() {
    //constrains the nodes to be within a box
    node
      .attr("cx", function(d) { return d.x = Math.max(radius, Math.min(width - radius, d.x)); })
      .attr("cy", function(d) { return d.y = Math.max(radius, Math.min(height - radius, d.y)); });
      
    link
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
  }
});

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}

CSS for forcegraphs.css

.links line {
  stroke: #999;
  stroke-opacity: 0.6;
}

.nodes circle {
  stroke: #fff;
  stroke-width: 1.5px;
}