Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding invertExtent in a threshold scale

I'm working my way through Mike Bostock's choropleth map (slowly) and am writing down each bit as I go to make sure I understand it. I am having trouble understanding one bit in particular. The example is online here: https://bl.ocks.org/mbostock/4060606

At one point in the code, Bostock has a range of colors, and is adding their inverse extents (the min and max values that would produce that color if entered into the color scale) to a map. For each "d" (which is an array of two colors) he uses color.invertExtent(d) to get the minimum and maximum value. At the end of the function, he returns the "d" value (which is now an array of two numbers, the min and the max) to a map. This I understand.

g.selectAll("rect")
  .data(color.range().map(function(d) {
      d = color.invertExtent(d);
      if (d[0] == null) d[0] = x.domain()[0];
      if (d[1] == null) d[1] = x.domain()[1];
      return d;
    }))

However, he also includes two "if" blocks that I do not understand. Why are they necessary? Why would the d[0] or d[1] of this two-color array ever be equal to "null"? And why is he assigning them to the x.domain[0] (which in this case is 600) and x.domain[1] which is 860. In what case would "null" even be the result?

like image 274
Harrison Cramer Avatar asked Mar 08 '23 04:03

Harrison Cramer


1 Answers

This is actually described in the documentation. If you look at threshold.invertExtent, you'll see:

Returns the extent of values in the domain [x0, x1] for the corresponding value in the range, representing the inverse mapping from range to domain. This method is useful for interaction, say to determine the value in the domain that corresponds to the pixel location under the mouse. For example:

var color = d3.scaleThreshold()
    .domain([0, 1])
    .range(["red", "white", "green"]);

color.invertExtent("red"); // [undefined, 0]
color.invertExtent("white"); // [0, 1]
color.invertExtent("green"); // [1, undefined]

Do you see those undefined? They are proper expected for the first and last value of the range.

The issue is that, for the rectangles' enter selection, you cannot use an array with undefined or null (undefined == null is true). So, what those ifs do is converting this array:

[[undefined,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9, undefined]]

Into this one:

[[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,10]]

Here is a live demo. First, without the ifs:

var scale = d3.scaleThreshold()
  .domain(d3.range(1, 9, 1))
  .range(d3.range(9));

console.log(scale.range().map(function(d) {
  d = scale.invertExtent(d);
  return d;
}))
<script src="https://d3js.org/d3.v4.min.js"></script>

Now with them:

var scale = d3.scaleThreshold()
  .domain(d3.range(1, 9, 1))
  .range(d3.range(9));

console.log(scale.range().map(function(d) {
  d = scale.invertExtent(d);
  if (d[0] == null) d[0] = 0;
  if (d[1] == null) d[1] = 9;
  return d;
}))
<script src="https://d3js.org/d3.v4.min.js"></script>

Finally, the key to really understand that is understanding how a threshold scale works. This is the most important part, pay attention to the number of elements:

If the number of values in the scale’s range is N+1, the number of values in the scale’s domain must be N. If there are fewer than N elements in the domain, the additional values in the range are ignored. If there are more than N elements in the domain, the scale may return undefined for some inputs.

like image 181
Gerardo Furtado Avatar answered Mar 11 '23 19:03

Gerardo Furtado