I'm trying to take a d3 example script and turn it into a plotting function using r2d3. I can use r2d3's "options" parameter to pass strings into the d3 script, but I can't figure out a way to pass in expressions (which, I think, is necessary to make it into a generic plotting function). Working from the example here https://bl.ocks.org/john-guerra/17fe498351a3e70929e2e36d081e1067 my .js script is called scatter_matrix.js:
// !preview r2d3 data=small_cars, d3_version = "3", css = "scatter_matrix.css", options = list(exclude = "mpg")
// r2d3: https://rstudio.github.io/r2d3
//
var size = 200,
padding = 20;
var x = d3.scale.linear()
.range([padding / 2, size - padding / 2]);
var y = d3.scale.linear()
.range([size - padding / 2, padding / 2]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(6);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(6);
var color = d3.scale.linear()
.domain([9, 30])
.range(["red", "green"]);
// I might need to include more of the lines outside this functions, inside the function?
r2d3.onRender(function(data, svg, width, height, options, error) {
if (error) throw error;
var domainByTrait = {},
traits = d3.keys(data[0]).filter(function(d) { return d !== options.exclude; }),
n = traits.length;
traits.forEach(function(trait) {
domainByTrait[trait] = d3.extent(data, function(d) { return d[trait]; });
});
xAxis.tickSize(size * n);
yAxis.tickSize(-size * n);
var brush = d3.svg.brush()
.x(x)
.y(y)
.on("brushstart", brushstart)
.on("brush", brushmove)
.on("brushend", brushend);
svg.selectAll(".x.axis")
.data(traits)
.enter().append("g")
.attr("class", "x axis")
.attr("transform", function(d, i) { return "translate(" + (n - i - 1) * size + ",0)"; })
.each(function(d) { x.domain(domainByTrait[d]); d3.select(this).call(xAxis); });
svg.selectAll(".y.axis")
.data(traits)
.enter().append("g")
.attr("class", "y axis")
.attr("transform", function(d, i) { return "translate(0," + i * size + ")"; })
.each(function(d) { y.domain(domainByTrait[d]); d3.select(this).call(yAxis); });
var cell = svg.selectAll(".cell")
.data(cross(traits, traits))
.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d) { return "translate(" + (n - d.i - 1) * size + "," + d.j * size + ")"; })
.each(plot);
// Titles for the diagonal.
cell.filter(function(d) { return d.i === d.j; }).append("text")
.attr("x", padding)
.attr("y", padding)
.attr("dy", ".71em")
.text(function(d) { return d.x; });
cell.call(brush);
function plot(p) {
var cell = d3.select(this);
x.domain(domainByTrait[p.x]);
y.domain(domainByTrait[p.y]);
cell.append("rect")
.attr("class", "frame")
.attr("x", padding / 2)
.attr("y", padding / 2)
.attr("width", size - padding)
.attr("height", size - padding);
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d[p.x]); })
.attr("cy", function(d) { return y(d[p.y]); })
.attr("r", 4)
.style("opacity", 0.5)
.style("fill", function(d) { return color(d.mpg); });
}
var brushCell;
// Clear the previously-active brush, if any.
function brushstart(p) {
if (brushCell !== this) {
d3.select(brushCell).call(brush.clear());
x.domain(domainByTrait[p.x]);
y.domain(domainByTrait[p.y]);
brushCell = this;
}
}
// Highlight the selected circles.
function brushmove(p) {
var e = brush.extent();
svg.selectAll("circle").classed("hidden", function(d) {
return e[0][0] > d[p.x] || d[p.x] > e[1][0]
|| e[0][1] > d[p.y] || d[p.y] > e[1][1];
});
}
// If the brush is empty, select all circles.
function brushend() {
if (brush.empty()) svg.selectAll(".hidden").classed("hidden", false);
}
});
function cross(a, b) {
var c = [], n = a.length, m = b.length, i, j;
for (i = -1; ++i < n;) for (j = -1; ++j < m;) c.push({x: a[i], i: i, y: b[j], j: j});
return c;
}
and scatter_matrix.css is:
svg {
font: 10px sans-serif;
padding: 10px;
}
.axis,
.frame {
shape-rendering: crispEdges;
}
.axis line {
stroke: #ddd;
}
.axis path {
display: none;
}
.cell text {
font-weight: bold;
text-transform: capitalize;
}
.frame {
fill: none;
stroke: #aaa;
}
circle {
fill-opacity: .125;
}
circle.hidden {
fill: #ccc !important;
}
.extent {
fill: #000;
fill-opacity: .125;
stroke: #fff;
}
So, as you can see, I've changed
var domainByTrait = {},
traits = d3.keys(data[0]).filter(function(d) { return d !== "species"; }),
n = traits.length;
To
var domainByTrait = {},
traits = d3.keys(data[0]).filter(function(d) { return d !== options.exclude; }),
n = traits.length;
With
// !preview r2d3 data=small_cars, d3_version = "3", css = "scatter_matrix.css", options = list(exclude = "mpg")
making it possible to then making it possible to choose which variable to exclude from the plot. However, lower down there's
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d[p.x]); })
.attr("cy", function(d) { return y(d[p.y]); })
.attr("r", 4)
.style("opacity", 0.5)
.style("fill", function(d) { return color(d.mpg); });
}
which is hard-coded into the script. I tried using
options = list(exclude = "mpg", col = expr(d.mpg))
and then having
.style("fill", function(d) { return color(options.col); });
}
But that didn't work. I'm using to using quasiquotation in R to solve this type of stuff, but I'm not sure if there's a way to do that in JS? d3 is amazing, but to be able to package the scripts up into functions would be... super amazing.
To reproduce this plot you should just need
small_cars <- mtcars %>% select(1:5)
r2d3::r2d3("catter_matrix.js", data=small_cars, d3_version = "3", css = "scatter_matrix.css", options = list(exclude = "mpg"))
Actually, I think I've figured this out. You can use javascript's eval() function to parse strings into code. So, if I change scatter_matrix.js to
So, basically just swapping in
eval(options.col)ford.mpgand then added acol = "d.mpg"to the r2d3 options list. Also I realised the range for colours was hard-coded, so swapped ineval(options.col_range)for[9,30]and added a col_range element to the r2d3 options list. Having done that, you can then write a function in R, likewhich allows you to pick your data and variable of interest from R, like