block by armollica 599ec84b819b1aedf0b4

NBA Attendance

Full Screen

Average annual attendance for all NBA team 2001-2016. Hover to see individual teams attendance.

Note the Wizards big uptick in the 2001-2003 years due to MJ. The impact of Lebron is clear when you look at attendance patterns for both the Cavaliers (2003-2010, 2014-present) and the Heat (2010-2014). The effects of the Great Recession (2007-2009) on attendance are also evident.

Data for 2016 is current as of Feb 21, 2016. Data source: ESPN.com

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <style>
      body {
        font: 12px sans-serif;
      }

      .axis line,
      .axis path {
        fill: none;
        stroke: #000;
        stroke-width: 1px;
      }

      .line {
        fill: none;
        stroke: #000;
        stroke-width: 1px;
        opacity: 0.1;
        -webkit-transition: stroke-width 0.2s, opacity 0.2s;
        -ms-transition: stroke-width 0.2s, opacity 0.2s;
        transition: stroke-width 0.2s, opacity 0.2s;
      }

      .line.hovered {
        stroke-width: 2px;
        opacity: 1;
      }

      .polygon {
        fill-opacity: 0;
        stroke-opacity: 0;
      }

      .marker {
        fill: none;
        stroke: #ff0000;
        stroke-width: 1px;
      }

      .team-name {
        opacity: 0.05;
        -webkit-transition: opacity 0.2s;
        -ms-transition: opacity 0.2s;
        transition: opacity 0.2s;
      }

      .team-name.hovered {
        opacity: 1;
      }
    </style>
  </head>
  <body>
    <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script>
      var margin = { top: 10, left: 75, bottom: 30, right: 75 },
          width = 960 - margin.left - margin.right,
          height = 400 - margin.top - margin.bottom;

      var scale = {
        x: d3.scale.linear().range([0, width]),
        y: d3.scale.linear().range([height, 0])
      };

      var access = {
        x: function(d) { return d.year; },
        y: function(d) { return d.avg; }
      };

      var value = {
        x: function(d) { return scale.x(access.x(d)); },
        y: function(d) { return scale.y(access.y(d)); }
      };

      var axis = {
        x: d3.svg.axis().scale(scale.x).orient("bottom")
            .tickFormat(d3.format(""))
            .ticks(15),
        y: d3.svg.axis().scale(scale.y).orient("left")
      };

      var line = d3.svg.line()
        .x(value.x)
        .y(value.y)
        .interpolate("monotone");

      var voronoi = d3.geom.voronoi()
        .clipExtent([[0, 0], [width, height]])
        .x(value.x)
        .y(value.y);

      var svg = d3.select("body").append("svg")
          .attr("width", width + margin.left + margin.right)
          .attr("height", height + margin.top + margin.bottom)
        .append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

      var xMarker = svg.append("circle")
        .attr("class", "x marker")
        .attr("r", 0);

      var yMarker = svg.append("circle")
        .attr("class", "y marker")
        .attr("r", 0);

      d3.json("data.json", function(error, data){
        if (error) throw error;

        data.forEach(function(d) {
          d.avg = +(d.avg.replace(",", ""));
          d.year = +d.year;
        });

        var minimum = {
          x: d3.min(data, access.x),
          y: d3.min(data, access.y)
        };

        scale.x.domain(d3.extent(data, access.x));
        scale.y.domain(d3.extent(data, access.y));

        groupedByTeam = d3.nest()
          .key(function(d) { return d.team; })
          .entries(data);

        svg.append("g").attr("class", "x axis")
          .attr("transform", "translate(0," + height + ")")
          .call(axis.x);
        svg.append("g").attr("class", "y axis")
            .call(axis.y)
          .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", ".71em")
            .style("text-anchor", "end")
            .style("text-shadow", "1px 1px #fff")
            .text("Attendance");

        var lines = svg.selectAll(".line")
            .data(groupedByTeam)
          .enter().append("path")
            .attr("class", "line")
            .attr("d", function(d) { return line(d.values); });

        var teamNames = svg.append("g").selectAll(".team-name")
            .data(data.filter(function(d) { return d.year === 2016; }))
          .enter().append("text")
            .attr("class", "team-name")
            .attr("x", value.x)
            .attr("y", value.y)
            .attr("dx", 3)
            .attr("dy", ".32em")
            .text(function(d) { return d.team; });

        var polygons = svg.selectAll(".polygon")
          .data(voronoi(data));

        polygons.enter().append("path")
          .attr("class", "polygon")
          .attr("d", function(d) {
            if (d !== undefined)
              return "M" + d.join("L") + "Z";
          })
          .on("mouseenter", function(d) {
            var team = d.point.team;

            lines.classed("hovered", function(d) { return d.key === team; });

            xMarker
              .attr("r", 3)
              .attr("cx", scale.x(d.point.year))
              .attr("cy", scale.y(minimum.y));

            yMarker
              .attr("r", 3)
              .attr("cx", scale.x(minimum.x))
              .attr("cy", scale.y(d.point.avg));

            teamNames
              .classed("hovered", function(d) { return d.team === team; });
          })
          .on("mouseleave", function() {
            lines.classed("hovered", false)
            xMarker.attr("r", 0)
            yMarker.attr("r", 0)
            teamNames.classed("hovered", false);
          });

      });
    </script>
  </body>
</html>

scrape-attendance.js

var fs = require("fs");
var cheerio = require("cheerio");
var d3 = {
  request: require("d3-request").request,
  queue: require("d3-queue").queue
};

var queue = d3.queue();
for (var year = 2001; year <= 2016; year++) {
  queue.defer(getAttendance, year);
}
queue.awaitAll(function(error, results) {
  if (error) throw error;
  var data = results.reduce(function(a, b) { return a.concat(b); });
  fs.writeFile("data.json", JSON.stringify(data), function(error) {
    if (error) throw error;
    console.log("Data saved.");
  })
});

function getAttendance(year, callback) {
  var column_titles = [
    "rank", "team",
    "home_games", "home_total", "home_avg", "home_pct",
    "road_games", "road_avg", "road_pct",
    "games", "avg", "pct"
  ];
  var url = "http://espn.go.com/nba/attendance/_/year/" + year;
  var data = [];
  d3.request(url, function(error, response) {
    if (error) callback(error);

    var $ = cheerio.load(response.responseText);

    var container = $("#my-teams-table");

    var rows = container
      .find("tr")
      .filter(function() {
        return !$(this).attr("class").endsWith("head");
      });

    rows.each(function(i) {
      data[i] = {};
      $(this).find("td")
        .each(function(j) {
          var title = column_titles[j];
          data[i][title] = $(this).text();
        });
    });

    data.forEach(function(d) { d.year = year; });

    callback(null, data);
  });
}