Want to build your own d3 shot charts?

Intoducing an open source d3 chart to build all kinds of shot charts, and hopefully enable other visualizes on top of basketball courts

Viraj Sanghvi • February 26, 2015
Photo: csessums

As I mentioned in my last post, its my hope that there's a better toolset for creating basketball related visualizations in the community than is currently possible. There are many visualizations of basketball related data that we haven't seen because we just haven't done the legwork of creating good foundations to build off of. My hope is to build that base through my own needs in analysis.

Because I really don't know what that foundation looks like, I'm starting off with real world use cases, and shot charts are the most popular of them. You can now build your shot charts off of my d3.basketball-shot-chart, and fork it to your heart's content. It's only alpha quality, but I think it'll improve and be abstracted fairly quickly- basically pending how busy I am with my day job.

As mentioned, all of the charts at our Shot Charts Tool are built off of this library, but to give a quick walkthough to how you'd use it, see the examples and walkthrough below.

Prereqs

The following assumes minimal web development and javascript knowledge. If you're new to those, or d3, I highly recommend getting more familiar with those before beginning as it'll be hard to extend functionality without the proper knowledge. If you're just looking for a quick way to get started, check out the docs and complete single page example.

Data

You need shot chart data, and this doesn't get you any closer to it. Assuming JSON serialization, you, at the very least, need an array of objects, each of which describe a shot: its location on the court (x/y coordinates), as well as whether that shot was made. You'll find these datasets become quite large, and you can aggregate the makes and attempts at each location on the court. Additionally, you may want to be displaying the accuracy in relation to the accuracy of the population at large, which might require additional data.

For this example, we'll use Lebron James' 2013-14 shot chart data, which looks a bit like this:

[{"x":2,"y":9,"made":3,"attempts":3,"z":1.77},{"x":2,"y":8,"made":0,"attempts":4,"z":-0.96},{"x":2,"y":4,"made":0,"attempts":1,"z":-0.9}, ...]

As you can tell, each location on the court has details about the number of shots attempted, made, and a z value which is the number of standard deviations away from the mean field goal percentage at this spot. We'll use a combination of these to make our example shot charts, and you can find the entire dataset within the html of this page. You'll notice that the coordinates in this dataset are quite coarse, and this is something to think about when you start collecting your own data.

Shot Charts!

Once we have the data, we can create charts quite quickly. Here we create a default chart from this data.

d3.select(document.getElementById('chart1'))
  .append("svg")
    .chart("BasketballShotChart", {
      // set svg width
      width: 600, 
      // set title
      title: 'Lebron James 2013-14',
    })
      .draw(data);

As you can see, the heat map we're using is just considering the field goal percentage across the domain of 0% to 100%, which doesn't really tell us that Lebron is shooting better than anyone else, just that it gets harder to make a shot further away from the basket. To get at that chart, we can configure options to pull our z-value, and change the domain to something that reflects a normal distribution (I chose 2.5 standard deviations away from the mean because it gives a good heat range spread and is fairly close to the 2-3 standard deviation range we'd expect to use so we don't overindex on outliers).

var heatRange = ['#5458A2', '#6689BB', '#FADC97', '#F08460', '#B02B48'];
  d3.select(document.getElementById('chart2'))
    .append("svg")
      .chart("BasketballShotChart", {
        width: 600, 
        title: 'Lebron James 2013-14',
        // instead of makes/attempts, use z
        hexagonFillValue: function(d) {  return d.z; }, 
        // switch heat scale domain to [-2.5, 2.5] to reflect range of z values
        heatScale: d3.scale.quantile()
          .domain([-2.5, 2.5])
          .range(heatRange),
        // update our binning algorithm to properly join z values
        // here, we update the z value to be proportional to the events of each point
        hexagonBin: function (point, bin) {
          var currentZ = bin.z || 0;
          var totalAttempts = bin.attempts || 0;
          var totalZ = currentZ * totalAttempts;

          var attempts = point.attempts || 1;
          bin.attempts = totalAttempts + attempts;
          bin.z = (totalZ + (point.z * attempts))/bin.attempts;
        },
      })
        .draw(data);   

This looks pretty busy because we're showing all data points, so lets clean that up. And if we want to change the colors of the heat range on top of that, we can update that option. Note that this is how we switch up the heat range for the defensive shot charts on this site.

d3.select(document.getElementById('chart3'))
  .append("svg")
    .chart("BasketballShotChart", {
      width: 600, 
      title: 'Lebron James 2013-14',
      hexagonFillValue: function(d) {  return d.z; }, 
      // reverse the heat range to map our z values to other colors
      heatScale: d3.scale.quantile()
        .domain([-2.5, 2.5])
        .range(heatRange.reverse()),
      hexagonBin: function (point, bin) {
        var currentZ = bin.z || 0;
        var totalAttempts = bin.attempts || 0;
        var totalZ = currentZ * totalAttempts;

        var attempts = point.attempts || 1;
        bin.attempts = totalAttempts + attempts;
        bin.z = (totalZ + (point.z * attempts))/bin.attempts;
      },
      // update radius threshold to at least 4 shots to clean up the chart
      hexagonRadiusThreshold: 4,
    })
      .draw(data); 

Where to go from here?

Please do check out the docs and example over at github for more information. I'm hoping this is straightforward enough for people to be able to leverage, and there are lots of knobs already if you just want to consume and customize. Over time, this library will definitely improve, and I'm hoping some of that comes from you! (read: PRs welcome!)