You need to use x, y, cx and cy to position your elements on the page, but how do you translate your data (number of ducks, weight of gold, etc) into a number of pixels on the screen? SCALES!

How scales work

When you use a scale, you need to decide on three things.

  1. Scale type: What type of scale are you using?
  2. Domain: What values are you going to send the scale? (from your data)
  3. Range: What values do you expect to get back? (pixels on screen)

For example d3.scaleLinear().domain([0,10]).range([0,960]): it’s a linear scale that expects inputs between 0 and 10 and translates them into numbers between 0 to 960. If I send it 10, it’ll send me back 960. If I send it 5, it’ll return 480.

What scale do I use?

If your data is quantitative, it’s usually a scaleLinear(). If you’re dealing with a circle’s radius, though, it’s scaleSqrt().

If your data is nominal, it gets a little more complicated with d3.ordinal(), d3.scalePoint(), d3.scaleBand() etc.

NOTE: In the examples below, ... means I’ve hidden some code from you. If you’d like to view the code, first click the Open Example In New Window link. Then select View from your top menu, then Developer > View Source.

Numeric/quantitative variables on the x or y axis

When you’re working with quantitative variables on a plane, you generally use d3.scaleLinear().

Look at how I use the scale for cx to position both the circles and the text in the example below.

Open example in new window

...

var datapoints = [
  { year: 1992, title: "A" },
  { year: 1997, title: "B" },
  { year: 1999, title: "C" },
  { year: 2002, title: "D" }
];

// 1990-2010 will be evenly spread between 0 and the width of the viz
var xPositionScale = d3.scaleLinear().domain([1990,2010]).range([0, width]);

...

svg.selectAll("circle")
  .data(datapoints)
  .enter()
  .append("circle")
  .attr("cy", 25)
  .attr("r", 5)
  .attr("cx", function(d) {
    return xPositionScale(d.year);
  });

svg.selectAll("text")
  .data(datapoints)
  .enter()
  .append("text")
  .attr("text-anchor", "middle")
  .attr("y", 50)
  .attr("r", 10)
  .attr("x", function(d) {
    return xPositionScale(d.year)
  })
  .text(function(d) {
    return d.year;
  });

Open example in new window

Nominal/categorical variables on the x or y axis (POINTS ONLY)

If you’re putting evenly spaced circles on the screen, a.k.a. putting a nominal category on a planar variable (x or y), you’ll use d3.scalePoint(). You give it a list of all of your quantitative variables and it evenly spaces them out for you.

Look at how I use the scale to set the cx for circles and x for the text below.

Open example in new window

...

var datapoints = [
  { name: "Julia", ducks: 50 },
  { name: "Roger", ducks: 11 },
  { name: "Timpani", ducks: 125 }
];
// Julia, Roger and Timpani will be evenly spread between 0 and the width of the viz
var xPositionScale = d3.scalePoint().domain(["Julia", "Roger", "Timpani"]).range([0, width]);
...

svg.selectAll("circle")
  .data(datapoints)
  .enter()
  .append("circle")
  .attr("cy", 25)
  .attr("r", 10)
  .attr("cx", function(d) {
    return xPositionScale(d.name);
  })
svg.selectAll("text")
  .data(datapoints)
  .enter()
  .append("text")
  .attr("text-anchor", "middle")
  .attr("y", 50)
  .attr("r", 10)
  .attr("x", function(d) {
    return xPositionScale(d.name)
  })
  .text(function(d) {
    return d.name
  });

Open example in new window

Instead of giving the list of names manually, you can also use .map to translate the list of data points into a list of names.

var datapoints = [
  { name: "Julia", ducks: 50 },
  { name: "Roger", ducks: 11 },
  { name: "Timpani", ducks: 125 }
];
var names = datapoints.map( function(d) { return d.name })
var xPositionScale = d3.scalePoint().domain(names).range([0, width]);

Nominal/categorical variables on the x or y axis (BARS ONLY)

If you’re putting evenly spaced bars on the screen, a.k.a. putting a nominal category on a planar variable (x or y), you’ll use d3.scaleBand(). You give it a list of all of your quantitative variables and it evenly spaces out the bars out for you.

Read the documentation if you’d like

Along with the x, this also gives you the width of your rectangles (or if you’re going on the y axis, the height). If you don’t want all of the bars squished together, you’ll want to add .padding(0.1) when you’re working on the scaleBand.

Open example in new window

...
var datapoints = [
  { name: "Julia", ducks: 50 },
  { name: "Roger", ducks: 11 },
  { name: "Timpani", ducks: 125 }
];
// Julia, Roger and Timpani will be evenly spread between 0 and the width of the viz
var xPositionScale = d3.scaleBand().domain(["Julia", "Roger", "Timpani"]).range([0, width]).padding(0.1);
...

svg.selectAll("rect")
  .data(datapoints)
  .enter()
  .append("rect")
  .attr("y", 10)
  .attr("fill", "black")
  .attr("height", 10)
  .attr("width", xPositionScale.bandwidth())
  .attr("x", function(d) {
    return xPositionScale(d.name);
  })

Open example in new window

Do you want to set the size/length based on the ducks? Read down below below!

Quantitative variables as colors

Open example in new window

...
var datapoints = [
  { name: "Mylo", ducks: 15, team: "Mallards" },
  { name: "Harper", ducks: 60, team: "Mallards" },
  { name: "Bowling Ball", ducks: 125, team: "Bills" }
];
// 0 ducks is beige
// 125 ducks is red
var colorScale = d3.scaleLinear().domain([0, 125]).range(['beige', 'red']);
...
svg.selectAll("circle")
  .data(datapoints)
  .enter()
  .append("circle")
...
  .attr("fill", function(d) {
    return colorScale(d.ducks);
  })
...

Open example in new window

Nominal variables as colors

If you’d like to take categories and turn them into colors, you use a d3.scaleOrdinal(d3.schemeCategory10), which is horrible to read, yes. You don’t need to specify a domain or range - it just works automatically, and it comes with default colors which we’ll just use for now.

Open example in new window

...
var datapoints = [
  { name: "Mylo", ducks: 15, team: "Mallards" },
  { name: "Harper", ducks: 60, team: "Bills" },
  { name: "Bowling Ball", ducks: 125, team: "Mallards" }
];

// Don't need to specify domain or range
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
...

svg.selectAll("circle")
  .data(datapoints)
  .enter()
  .append("circle")
...
  .attr("fill", function(d) {
    return colorScale(d.team);
  })
...

Open example in new window

Quantitative variables as size/area (circles)

When you’re dealing with area of circles, you increase the radius not linearly but rather as a square root. Don’t worry about why yet, just know it’s how you do it.

Open example in new window

...
var datapoints = [
  { name: "Mylo", ducks: 15, team: "Mallards" },
  { name: "Harper", ducks: 60, team: "Bills" },
  { name: "Bowling Ball", ducks: 125, team: "Mallards" }
];

// If you have 0 ducks, you have 0 radius
// If you have 200 ducks, your radius will be 30
// We're using a scaleSqrt because it's a circle
var circleRadiusScale = d3.scaleSqrt().domain([0,200]).range([0, 30]);
...
svg.selectAll("circle")
  .data(datapoints)
  .enter()
  .append("circle")
...
  .attr("r", function(d) {
    return circleRadiusScale(d.ducks);
  })

Open example in new window

Quantitative variables as size/length (bars)

Remember when we used d3.scaleBand() up above to space out some bars? The bars get pretty boring if you don’t bring something quantitative in. So let’s space the bars out with one scale, then change their size/length with another scale.

We’re going to use a d3.scaleLinear() because we want the bars to increase in size/length just as the data points are increasing.

Open example in new window

...
var datapoints = [
  { name: "Julia", ducks: 50 },
  { name: "Roger", ducks: 11 },
  { name: "Timpani", ducks: 125 }
];
// If you have 0 ducks, 0 height. 125 ducks will give you 50 pixels of height.
var heightScale = d3.scaleLinear().domain([0, 125]).range([0, 50]);
// Julia, Roger and Timpani will be evenly spread between 0 and the width of the viz
// and we'll give a little bit of padding, too
var xPositionScale = d3.scaleBand().domain(["Julia", "Roger", "Timpani"]).range([0, width]).padding(0.1);
...

svg.selectAll("rect")
  .data(datapoints)
  .enter()
  .append("rect")
  .attr("y", 10)
  .attr("fill", "black")
  .attr("width", xPositionScale.bandwidth())
  .attr("x", function(d) {
    return xPositionScale(d.name);
  })
  .attr("height", function(d) {
    return heightScale(d.ducks);
  })

Open example in new window

But hey… Did you notice that the bars are, uh, going in the wrong direction? That’s because x and y set the top left-hand side, and height grows DOWN from there, so if y is always the same, our bars always going to be growing DOWN from the same line on the y axis.

The solution is instead of basing our y on a set number, we say “start drawing the rect from [bar height] pixels from the bottom of the screen”, a.k.a. height - heightScale(y.ducks). If you don’t get it, that’s perfectly fine, just memorize it for now.

Open example in new window

...
var datapoints = [
  { name: "Julia", ducks: 50 },
  { name: "Roger", ducks: 11 },
  { name: "Timpani", ducks: 125 }
];
// If you have 0 ducks, 0 height. 125 ducks will give you 50 pixels of height.
var heightScale = d3.scaleLinear().domain([0, 125]).range([0, 50]);
...

svg.selectAll("rect")
  .data(datapoints)
  .enter()
  .append("rect")
...
  .attr("y", function(d) {
    return height - heightScale(d.ducks);
  })
...
  .attr("height", function(d) {
    return heightScale(d.ducks);
  })

Open example in new window