Understanding SVG coordinate space

For the most part, SVG coordinates are normal cartesian, or X/Y coordinates. X is the horizontal axis and Y is the vertical.

Things start to differ from your high school math class though when we talk about the coordinate space's fixed reference point, or origin. When you first learned about X and Y coordinates, you probably saw them drawn like a plus sign.

Easy enough, but you can't put the origin in the center when it comes to screen coordinates. What would the center even be in a context where the available space can change due to browser resizes and other user interactions?

As a result, we kind of just use the bottom right quadrant of a traditional coordinate space. The origin, or 0,0, is effectively at the "top left" corner of our available area, typically the browser window. But in the image above, the Y values get smaller as you move "down", going from 0 at the origin down into negative numbers. It would be pretty strange if all Y values had to be negative (because positive values would be out of view "above" the viewport), so SVG inverts the Y axis.

In SVG (and canvas, etc.), 0,0 is the top left corner. X values increase as they move to the right and Y values increase as they move down.

Original image source

Why does it matter?

For the most part, SVG's flipped Y axis just serves to make things intuitive. Thinking of distance from an origin as always being positive simplifies a lot of the mental models you need while programming visual things.

One place you have to be keenly aware of it, however, is when creating a column chart.

Let's discuss the relevant code for drawing those rectangles (taken from this example).

const height = 600
 
const yScale = d3.scaleLinear()
  .domain([0, 100])
  .range([height, 0]) // this is the important part

When specifying our Y scale's range we invert the minimum and maximum values, specifying height, or 600, as the minimum and 0 as the maximum. This leaves us with a scale function that will provide "inverted" values like below. Remember our domain is from 0 to 100, so 25 is one quarter of our maximum value.

yScale(0)  // 600
yScale(25) // 450
// and so on...

So passing the minimum value in our domain will return what would typically be the maximum value in our range.

Hmm, why would we do that?

Well, since axes are created from scales (d3.axisLeft(yScale)), inverting is necessary to ensure the axis labels are arranged the way you'd expect. In the image above, you can see 0 is at the bottom and 100 is at the top. If we hadn't inverted our scales, those would be reversed.

But wait...

If we're going to use an inverted scale it's going to affect our drawing code too. Since our scale function effectively returns the amount of space a given rectangle should have rather than it's size, we have to account for that when creating the rect elements.

.attr('y', d => yScale(d[subject]))
.attr('height', d => height - yScale(d[subject]));

So we set the Y attribute by passing the data value through our yScale, and we set the height by subtracting that same scaled value from the height we defined for the chart.

This is something I always have to stop and think about or look up, even after doing it for years. Even though it's not technically accurate, the mental model I tend to use is to think of the rectangles as being created below the chart's baseline like carrots, and then effectively pulling them up into view.

Another way to think about it is that setting the y attribute is like moving the "drawing pen" down to where the top of the bar will be, and then setting the height "grows" the bar down to the baseline.

That's it! Happy hacking!