Jay Mar Jay Mar - 3 months ago 12
HTML Question

Adding jointplot to NBA shot chart using d3js

I'm trying to add a jointplot to a NBA shot chart,

so it would look like this :

enter image description here

The problem is that I can't get the bars to work properly, and I really don't know why this is happening.

It would be great if, someone guides me what do I have to change in the jointplot in order to work with the NBA chart.

NBA shot chart :

http://jsbin.com/guqiviniji/edit?html,output

Jointplot implementation :



<!DOCTYPE html>
<meta charset="utf-8">

<head>
<style>
.bar {
fill: steelblue;
}

.bar:hover {
fill: brown;
}

.axis {
font: 10px sans-serif;
}

.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}

.x.axis path {
display: none;
}
</style>
</head>

<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
// set the dimensions and margins of the graph
var margin = {
top: 80,
right: 80,
bottom: 20,
left: 20
},
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;

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

var g = svg.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");

var x = d3.scale.linear().range([0, width]).domain([0, 10]),
y = d3.scale.linear().range([height, 0]).domain([0, 10]);

// Add the X Axis
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.svg.axis().orient("bottom").scale(x));

// Add the Y Axis
g.append("g")
.attr("class", "axis")
.call(d3.svg.axis().orient("left").scale(y));

var random = d3.random.normal(0, 1.2),
data = d3.range(100).map(function() {
return [random() + 5, random() + 5];
});

g.selectAll(".point")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return x(d[0]);
})
.attr("cy", function(d) {
return y(d[1]);
})
.attr("r", 7)
.style("fill", "steelblue")
.style("stroke", "lightgray");

// top histogram
var gTop = svg.append("g")
.attr("transform",
"translate(" + margin.left + "," + 0 + ")");

var xBins = d3.layout.histogram()
.range(x.domain())
.bins(x.ticks(10))
.value(function(d) {
return d[0];
})(data);

var xy = d3.scale.linear()
.domain([0, d3.max(xBins, function(d) {
return d.length;
})])
.range([margin.top, 0]);

var xBar = gTop.selectAll(".bar")
.data(xBins)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) {
return "translate(" + x(d.x) + "," + xy(d.length) + ")";
});

var bWidth = x(xBins[0].dx) - 1;
xBar.append("rect")
.attr("x", 1)
.attr("width", bWidth)
.attr("height", function(d) {
return margin.top - xy(d.length);
})
.style("fill", "steelblue");

xBar.append("text")
.attr("dy", ".75em")
.attr("y", 2)
.attr("x", bWidth / 2)
.attr("text-anchor", "middle")
.text(function(d) {
return d.length < 4 ? "" : d.length;
})
.style("fill", "white")
.style("font", "9px sans-serif");

// right histogram
var gRight = svg.append("g")
.attr("transform",
"translate(" + (margin.left + width) + "," + margin.top + ")");

var yBins = d3.layout.histogram()
.range(y.domain())
.bins(y.ticks(10))
.value(function(d) {
return d[1];
})(data);

console.log(yBins);

var yx = d3.scale.linear()
.domain([0, d3.max(yBins, function(d) {
return d.length;
})])
.range([0, margin.right]);

var yBar = gRight.selectAll(".bar")
.data(yBins)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) {
return "translate(" + 0 + "," + y(d.x + d.dx) + ")";
});

yBar.append("rect")
.attr("y", 1)
.attr("width", function(d) {
return yx(d.length);
})
.attr("height", bWidth)
.style("fill", "steelblue");

yBar.append("text")
.attr("dx", "-.75em")
.attr("y", bWidth / 2 + 1)
.attr("x", function(d) {
return yx(d.length);
})
.attr("text-anchor", "middle")
.text(function(d) {
return d.length < 4 ? "" : d.length;
})
.style("fill", "white")
.style("font", "9px sans-serif");
</script>
</body>

</html>




Answer

Since I put a lot of effort into answering this question in the comments over here, I guess it's worth it to re-hash it as an answer on this question.

I really see two large problems with the code in the jsbin link.

First, a strong reliance on magic numbers. It looks like the OP used trial and error to get the shots placed properly for a specific height/width of his chart. This, of course, means that adding margins or changing sizes requires a lot of effort in developing new magic numbers. I solve this reliance below by scaling the original magic numbers by percents as the chart height and width change. This is a bit of a hack but seems to work.

Second, in the original "joint plot" sample I developed, I binned the data using the user-space data. I then needed x and y scales to map the user-space bins back to pixel locations. In the OPs jsbin, though, he is using pixel-space data to bin the data. This is, of course, completely valid, and in fact, is probably better as the placement of the bars becomes easier. You don't need to re-introduce more scales to place them.

Finally, I did a bit more work to make the bin size adjustable.

Here's some code for the right histogram:

    // right histogram
    var gRight = r_svg.append("g")
      .attr("transform",
        "translate(" + (margins.left + max.x) + "," + margins.top + ")");

    var m = d3.max(histogramData, function(d) { return d[1] });
    var yBins = d3.layout.histogram()
      .range([0, m])
      .bins(d3.range(0, m + 20, 20)) //<-- bin size is more adjustable, lower the 20, 20 for more bins
      .value(function(d) {
        return d[1];
      })(histogramData); //<-- data has pixel values

    var yx = d3.scale.linear()
      .domain([0, d3.max(yBins, function(d) {
        return d.length;
      })])
      .range([0, margins.right]);

    var yBar = gRight.selectAll(".bar")
      .data(yBins)
      .enter().append("g")
      .attr("class", "bar")
      .attr("transform", function(d) {
        return "translate(" + 0 + "," + (d.x) + ")"; //<-- pixel values just place at d.x
      });

    yBar.append("rect")
      .attr("y", 1)
      .attr("width", function(d) {
        return yx(d.length);
      })
      .attr("height", bWidth)
      .style("fill", "steelblue");

    yBar.append("text")
      .attr("dx", "-.75em")
      .attr("y", bWidth / 2 + 1)
      .attr("x", function(d) {
        return yx(d.length);
      })
      .attr("text-anchor", "middle")
      .text(function(d) {
        return d.length < 15 ? "" : d.length;
      })
      .style("fill", "white")
      .style("font", "9px sans-serif");

Running code is here.