Using d3.scale

Scales! They aren’t just for playing recorder in music class, they’re an integral part to becoming a d3 master.

They also make life a heck of a lot easier.

Why we need scales

Okay, so let’s say we’re making a bar graph. We have:

HTML

<svg class="bar" width="120" height="100"></svg>

Data

var datapoints = [50, 2, 3, 4, 5];

So our svg is 120 pixels wide, and our largest data value is 5. I think it makes sense to have every bar be d * 20 pixels wide, since that way the largest bar is 100 pixels long!

Open example in new window

<svg class="bar" width="150" height="100"></svg>
<script>
  var datapoints = [1, 2, 3, 4, 5];
  var svg = d3.select('.bar');
  var rects = svg.selectAll('rect')
                  .data(datapoints)   // bind our data
                  .enter()            // grab the 'new' data points
                  .append('rect')     // append the bars
                  .attr('x', 0)       // set the left hand side to 0
                  .attr('y', function(d, i) {
                    return i * 15;    // space them 15 pixels apart
                  })
                  .attr('height', 10) // each bar is 10 pixels tall
                  .attr('width', function(d) {
                    return d * 20;    // and now the width
                  });
</script>

Open example in new window

Round 2

Oh but wait! We got a new data point.

var datapoints = [1, 2, 3, 4, 5, 50];

Okay, cool. That’s fine, no big deal! Our biggest value is 50 now, so if we’re trying to make the biggest bar 100 pixels, then that’s going to be d * 2. Since, of course, 50 * 2 is 100.

Open example in new window

<svg class="bar" width="150" height="100"></svg>
<script>
  var datapoints = [1, 2, 3, 4, 5, 50];
  var svg = d3.select('.bar');
  var rects = svg.selectAll('rect')
                  .data(datapoints)   // bind our data
                  .enter()            // grab the 'new' data points
                  .append('rect')     // append the bars
                  .attr('x', 0)       // set the left hand side to 0
                  .attr('y', function(d, i) {
                    return i * 15;    // space them 15 pixels apart
                  })
                  .attr('height', 10) // each bar is 10 pixels tall
                  .attr('width', function(d) {
                    return d * 2;    // and now the width
                  });
</script>

Open example in new window

Round 3

Oh but wait! Our editor called, she said that we got a new svg that’s 410 pixels wide, and one of our data points changed!

<svg class="bar" width="410" height="100"></svg>
var datapoints = [560, 20, 30, 40, 50, 150];

Okay, so now our equation is… I don’t know. Something something math? Maybe it we aim for 400 pixels wide, we’ll use… 400 / 560 which is 0.72, so the equation will be d * 0.72. I guess? Maybe?

Open example in new window

<svg class="bar" width="410" height="100"></svg>
<script>
  var datapoints = [560, 20, 30, 40, 50, 150];
  var svg = d3.select('.bar');
  var rects = svg.selectAll('rect')
                  .data(datapoints)   // bind our data
                  .enter()            // grab the 'new' data points
                  .append('rect')     // append the bars
                  .attr('x', 0)       // set the left hand side to 0
                  .attr('y', function(d, i) {
                    return i * 15;    // space them 15 pixels apart
                  })
                  .attr('height', 10) // each bar is 10 pixels tall
                  .attr('width', function(d) {
                    return d * 2;    // and now the width
                  });
</script>

Open example in new window

Round 4

Oh but wait! Now apparently we need to label the left-hand side, so we need to start like 20 pixels in, so we now only have 400 - 20 = 380 pixels to work with. And we apparently are supposed to start the bars from 10 instead of 0 which means some other sort of math I can’t think about right now and oh god why is everything so bad.

The reason is: because we haven’t been using scales

The magic of scales

d3.scale() take one range of numbers and convert them into another range. Technically it’s a method that creates a function, but who cares about that?

!!!FOR EXAMPLE!!!

Let’s go back to our first example. Biggest data point of 5, trying to take up 100 or so pixels with the bar.

HTML

<svg class="bar" width="120" height="100"></svg>

Data

var datapoints = [50, 2, 3, 4, 5];

Here’s how we use d3.scale to switch between the two. It takes the input of 0-5 (a.k.a. the domain) and maps it to the output of 0-100 (a.k.a. the range). Any value you send it is converted between those two number ranges.

Open example in new window

<script>
var scale = d3.scale.linear().domain([0,5]).range([0,100]);

document.writeln("Using the scale...<br>");
document.writeln("1 becomes ", scale(1), "<br>");
document.writeln("2 becomes ", scale(2), "<br>");
document.writeln("3.5 becomes ", scale(3.5), "<br>");
document.writeln("4.75 becomes ", scale(4.75), "<br>");
document.writeln("5 becomes ", scale(5), "<br>");
</script>

Open example in new window

NOTE: document.writeln is how you print to the screen in JavaScript. console.log is its infinitely more useful older, cooler sibling.

Let’s say you wanted a bar that was 410 pixels long, but your largest value is 560. No need to do math, just rely on d3.scale! It will convert 0 through 560 into the appropriate values between 0 and 410 pixels.

Open example in new window

<script>
var scale = d3.scale.linear().domain([0,560]).range([0,410]);

document.writeln("Using the scale...<br>");
document.writeln("10 becomes ", scale(10), "<br>");
document.writeln("20 becomes ", scale(20), "<br>");
document.writeln("35 becomes ", scale(35), "<br>");
document.writeln("475 becomes ", scale(475), "<br>");
document.writeln("560 becomes ", scale(500), "<br>");
</script>

Open example in new window

NOTE: Don’t forget the square brackets inside of .domain and .range! It isn’t .domain(0, 20), it’s .domain([0, 20]). Don’t ask me why.

Using d3.scale in practice

You set up your d3.scale up top, and then you use it inside of your function.

Open example in new window

<svg class="bar" width="410" height="100"></svg>
<script>
  var datapoints = [560, 20, 30, 40, 50, 150];
  // Create a scale to take 0-560 as input and return 0-400 as output
  var scale = d3.scale.linear().domain([0,560]).range(0, 400);
  var svg = d3.select('.bar');
  var rects = svg.selectAll('rect')
                  .data(datapoints)   // bind our data
                  .enter()            // grab the 'new' data points
                  .append('rect')     // append the bars
                  .attr('x', 0)       // set the left hand side to 0
                  .attr('y', function(d, i) {
                    return i * 15;    // space them 15 pixels apart
                  })
                  .attr('height', 10) // each bar is 10 pixels tall
                  .attr('width', function(d) {
                    return scale(d);    // USE THE SCALE!!!
                  });
</script>

Open example in new window

Examples

Example: Round 1

We’re going to do three things to make this especially awesome.

  1. Use d3.max to get the largest value being graphed
  2. Set the width of your svg through d3
  3. Set up your scale automatically using 1. and 2.

Open example in new window

<svg class="bar"></svg>
<script>
  var states = [
                {'name' :  'New York', 'total_area': 54555, 'land_area': 47126, 'p_water': 0.136}, 
                {'name' : 'Texas', 'total_area': 268596, 'land_area': 261232, 'p_water': 0.027}, 
                {'name' : 'Arizona', 'total_area': 113990, 'land_area': 113594, 'p_water': 0.003}, 
                {'name' : 'California', 'total_area': 163695, 'land_area': 155779, 'p_water': 0.048}, 
                {'name' : 'Mississippi', 'total_area': 48432, 'land_area': 46923, 'p_water': 0.031}, 
                {'name' : 'New Jersey', 'total_area': 8723, 'land_area': 7354 	, 'p_water': 0.157}, 
               ];

  // LOOK AT THIS
  var width = 400;
  var height = 120;
  
  // LOOK AT THIS
  // Set up the SVG
  // Set the height and width via d3!
  var svg = d3.select('.bar').attr('height', height).attr('width', width);

  // LOOK AT THIS
  // Get the largest area
  // d3.max is usually taught as d3.max([10, 2, 3, 4]) => 10
  // but frankly you're always using it with lists of dictionaries
  // so why not start now?
  var largest_area = d3.max(states, function(d) { return d['total_area'] });

  // LOOK AT THIS
  // Create a scale to take 0-max area as input, return 0-svg's width as output
  var scale = d3.scale.linear().domain([0, largest_area]).range([0, width]);
  
  // Now no matter how the data changes or the width of the svg changes,
  // it'll always adjust automatically! THAT IS CRAZY.
  
  var rects = svg.selectAll('rect')
                  .data(states)   // bind our data
                  .enter()            // grab the 'new' data points
                  .append('rect')     // append the bars
                  .attr('x', 0)       // set the left hand side to 0
                  .attr('y', function(d, i) {
                    return i * 15;    // space them 15 pixels apart
                  })
                  .attr('height', 10) // each bar is 10 pixels tall
                  .attr('width', function(d) {
                    // LOOK AT THIS
                    return scale(d['total_area']);    // USE THE SCALE!!!
                  });
</script>

Open example in new window

Example: Round 2

Now I’m just going to cut and paste the same code, but change var width = 400 to be var width = 80. I’m also going to remove the state with the largest area.

Bask in the gentle glow of its adaptability!

Open example in new window

<svg class="bar"></svg>
<script>
  var states = [
                {'name' :  'New York', 'total_area': 54555, 'land_area': 47126, 'p_water': 0.136}, 
                {'name' : 'Arizona', 'total_area': 113990, 'land_area': 113594, 'p_water': 0.003}, 
                {'name' : 'California', 'total_area': 163695, 'land_area': 155779, 'p_water': 0.048}, 
                {'name' : 'Mississippi', 'total_area': 48432, 'land_area': 46923, 'p_water': 0.031}, 
                {'name' : 'New Jersey', 'total_area': 8723, 'land_area': 7354 	, 'p_water': 0.157}, 
               ];
  var width = 80;
  var height = 120;
  
  // Set up the SVG
  // Set the height and width via d3!
  var svg = d3.select('.bar').attr('height', height).attr('width', width);

  // Get the largest area
  // d3.max is usually taught as d3.max([10, 2, 3, 4]) => 10
  // but frankly you're always using it with lists of dictionaries
  // so why not start now?
  var largest_area = d3.max(states, function(d) { return d['total_area'] });

  // Create a scale to take 0-max area as input, return 0-svg's width as output
  var scale = d3.scale.linear().domain([0, largest_area]).range([0, width]);
  
  // Now no matter how the data changes or the width of the svg changes,
  // it'll always adjust automatically! THAT IS CRAZY.
  
  var rects = svg.selectAll('rect')
                  .data(states)   // bind our data
                  .enter()            // grab the 'new' data points
                  .append('rect')     // append the bars
                  .attr('x', 0)       // set the left hand side to 0
                  .attr('y', function(d, i) {
                    return i * 15;    // space them 15 pixels apart
                  })
                  .attr('height', 10) // each bar is 10 pixels tall
                  .attr('width', function(d) {
                    return scale(d['total_area']);    // USE THE SCALE!!!
                  });
</script>

Open example in new window

See how it got all small and appropriate? Jeez that is so neat.

Non-numeric scales

…I’ll get to this one. TODO.

Want to hear when I release new things?
My infrequent and sporadic newsletter can help with that.