D3: Small Multiples
Recall that for small multiples, we are partitioning data items, based on discrete attributes. The simplest way to achieve this design in D3 is using … nest. This is precisely the group-by operation. Except here, we are not necessarily aggregating (as we’ve seen with bars)! We want to show all of the data, partitioned into different views.
This permits a fairly modular approach to creating visualizations: nest along the attributes that we are faceting our plots, and then in a rollup, we can specify how we would like to organize our data, downstream, for when we create our individual plots.
D3: Scatterplot Matrix
With scatterplot matrices, each plot is showing all of the data items, using only some of the data attributes, namely all pairs of attributes. So unlike small multiples, we are not grouping data items by an attribute.
This complicates things a bit, but if we carefully approach how we perform data joins, then it is straightforward to create a scatterplot matrix. The main things to consider are:
- We need to perform a data join 3 times: one for columns of our matrix, one for rows of our matrix, and last, for the actual data.
- This implies that in the third data join, we are simply supplying all of our data, but restricted to the pair of attributes that correspond to a given cell in our matrix.
- Thus, the first two data joins are, in some sense, pretty trivial: they setup the matrix! Specifically, they provide us the row and column indices of our matrix.
d3.csv('nba_players.csv')
.then(function(data) {
nba_data = data;
selected_atts = ['Age','Block','Steal','Assist','Two Points','Three Points']
d3.shuffle(selected_atts)
nba_data.forEach(d => {
selected_atts.forEach(att => {
d[att] = +d[att];
})
})
plot_nba();
})
function plot_nba() {
var svg1 = d3.select('#svg1');
var x_range_pad = 40, y_range_pad = 90;
var width = svg1.attr('width'), height = svg1.attr('height');
var att_scale = d3.scaleBand().domain(selected_atts).range([y_range_pad,height-x_range_pad]).paddingInner(0.2);
var plot_height = att_scale.bandwidth();
var x_quantitative_scales = {}, y_quantitative_scales = {};
selected_atts.forEach((att,i) => {
var extent = d3.extent(nba_data, d => d[att]);
x_quantitative_scales[att] = d3.scaleLinear().domain([extent[0],extent[1]]).range([0,plot_height]).nice();
y_quantitative_scales[att] = d3.scaleLinear().domain([extent[0],extent[1]]).range([plot_height,0]).nice();
});
svg1.selectAll('cols').data(selected_atts).enter().append('g')
.attr('transform', d => 'translate('+att_scale(d)+',0)')
.selectAll('rows').data((d,i) => {
var unique_rows = selected_atts.filter((_,j) => i <= j);
return unique_rows.map(d_new => [d,d_new]);
})
.enter().append('g')
.attr('transform', d => 'translate(0,'+att_scale(d[1])+')').attr('class', 'splom')
.selectAll('points').data(att => {
return nba_data.map(d => [d[att[0]], d[att[1]]]);
})
.enter().append('circle')
.attr('r', 1.75).attr('fill', d3.hcl(20,60,70)).attr('opacity', 0.4)
svg1.selectAll('.splom').each(function(att) {
var scale_x = x_quantitative_scales[att[0]], scale_y = y_quantitative_scales[att[1]];
d3.select(this).selectAll('circle').attr('cx', d => scale_x(d[0])).attr('cy', d => scale_y(d[1]))
d3.select(this).append('g').attr('transform', 'translate(0,0)').call(d3.axisLeft(scale_y).ticks(4))
d3.select(this).append('g').attr('transform', 'translate(0,'+plot_height+')').call(d3.axisBottom(scale_x).ticks(4))
})
create_axes_example1(svg1,(height-x_range_pad+20),att_scale)
}