block by micahstubbs 89e0d8802a2a43d438540e7b05444e62

Baseball Scatterplot Matrix II

Full Screen

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.

index.html

<!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>