Positioning elements with scales
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.
- Scale type: What type of scale are you using?
- Domain: What values are you going to send the scale? (from your data)
- 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 selectView
from your top menu, thenDeveloper
>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.
...
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;
});
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.
...
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
});
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
.
...
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);
})
Do you want to set the size/length based on the ducks? Read down below below!
Quantitative variables as colors
...
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);
})
...
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.
...
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);
})
...
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.
...
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);
})
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.
...
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);
})
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.
...
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);
})