an ES2015 fork of @syntagmatic’s nice Baseball Scatterplot Matrix
this iteration also adds an slider to adjust the fill-opacity
or the SVG circles used as scatterplot marks. The bl.ocks:
helped me debug the HTML5 <input type='range'>
element I use for the slider control.
Scatterplot matrix design invented by J. A. Hartigan and described in his 1975 paper Printer graphics for clustering. 1986 baseball statistics data from the R package Visualizing Categorical Data.
Based on Scatterplot Matrix Brushing, but still awaiting the d3.v4 brush component.
<!DOCTYPE html>
<meta charset='utf-8'>
<style>
svg {
font: 10px sans-serif;
padding: 12px;
}
.axis,
.frame {
shape-rendering: crispEdges;
}
.axis line {
stroke: #e8e8e8;
}
.axis path {
display: none;
}
.axis text {
fill: #999;
}
.cell text {
font-weight: bold;
text-transform: capitalize;
font-size: 15px;
fill: #222;
}
.frame {
fill: none;
stroke: #aaa;
}
.diagonal {
stroke: none;
fill: #fff;
fill-opacity: 0.8;
}
circle.hidden {
fill: #ccc !important;
}
.extent {
fill: #000;
fill-opacity: .125;
stroke: #fff;
}
</style>
<body>
<script src='https://d3js.org/d3.v4.0.0-alpha.35.js'></script>
<script src='https://npmcdn.com/babel-core@5.8.34/browser.min.js'></script>
<script lang='babel' type='text/babel'>
const traits = ['assist86', 'hits86', 'homer86', 'runs86', 'rbi86', 'walks86'];
const n = traits.length;
const width = 960;
const size = (width / n) - 12;
const padding = 24;
// setup the page
d3.select('body').append('div')
.attr('width', size * n + padding)
.attr('height', size * n + padding)
.attr('id', 'vis')
.style('top', -20)
.style('position', 'relative');
let x = d3.scaleLinear()
.range([padding / 2, size - padding / 2]);
let y = d3.scaleLinear()
.range([size - padding / 2, padding / 2]);
let xAxis = d3.axisBottom()
.scale(x)
.ticks(5);
let yAxis = d3.axisLeft()
.scale(y)
.ticks(5);
const color = d3.scaleCategory20();
d3.csv('Baseball.csv', (error, data) => {
if (error) throw error;
data.forEach(d => {
traits.forEach(trait => {
return d[trait] = +d[trait];
});
});
let domainByTrait = {};
traits.forEach(trait => {
domainByTrait[trait] = d3.extent(data, d => d[trait]);
});
xAxis.tickSize(size * n);
yAxis.tickSize(-size * n);
let svg = d3.select('div#vis').append('svg')
.attr('width', size * n + padding)
.attr('height', size * n + padding)
.datum({
x: width / 2,
y: 960
})
.append('g')
.attr('transform', `translate(${padding}, ${padding / 2})`);
svg.selectAll('.x.axis')
.data(traits)
.enter().append('g')
.attr('class', 'x axis')
.attr('transform', (d, i) => 'translate(' + (n - i - 1) * size + ',0)')
.each(function (d) {
x.domain(domainByTrait[d]).nice();
d3.select(this).call(xAxis);
});
svg.selectAll('.y.axis')
.data(traits)
.enter().append('g')
.attr('class', 'y axis')
.attr('transform', (d, i) => `translate(0, ${i * size})`)
.each(function (d) {
y.domain(domainByTrait[d]);
d3.select(this).call(yAxis);
});
let cell = svg.selectAll('.cell')
.data(cross(traits, traits))
.enter().append('g')
.attr('class', 'cell')
.attr('transform', d => `translate(${(n - d.i - 1) * size}, ${d.j * size})`)
.each(plot);
// Titles for the diagonal.
cell.filter(d => d.i === d.j).append('text')
.attr('x', size/2)
.attr('y', size/2)
.attr('text-anchor', 'middle')
.text(d => d.x);
//cell.call(brush);
function plot(p) {
let cell = d3.select(this);
x.domain(domainByTrait[p.x]);
y.domain(domainByTrait[p.y]);
cell.append('rect')
.attr('class', 'frame')
.classed('diagonal', d => d.i === d.j)
.attr('x', padding / 2)
.attr('y', padding / 2)
.attr('width', size - padding)
.attr('height', size - padding);
cell.filter(d => d.i !== d.j) // hide diagonal marks
.selectAll('circle')
.data(data)
.enter().append('circle')
.classed('marks', true)
.attr('cx', d => x(d[p.x]))
.attr('cy', d => y(d[p.y]))
.attr('r', 2.5)
.style('fill', d => color(d.posit86))
.style('fill-opacity', 0.356);
}
});
function cross(a, b) {
let 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;
}
// slider
d3.select('body').append('input')
.attr('type', 'range')
.attr('min', 0)
.attr('max', 1)
.attr('value', 0.356)
.attr('step', 0.001)
.style('top', '0px')
.style('left', `${padding}px`)
.style('height', '36px')
.style('width', `${915 - padding}px`)
.style('position', 'relative')
.attr('id', 'slider');
d3.select('#slider')
.on('input', function() {
update(+this.value);
});
function update(sliderValue) {
// adjust the text on the range slider
d3.select('#nRadius-value').text(sliderValue);
d3.select('#nRadius').property('value', sliderValue);
// update the circle radius
d3.selectAll('.marks')
.style('fill-opacity', sliderValue);
}
</script>