Build a bottom axis
In the previous lesson, we learned how to manage margins effectively in our chart. Now, let's explore how to create a AxisBottom
react component that draws a bottom axis!
🔍 More about scaleLinear()
In the previous lessons we talked a lot about the scaleLinear() function of d3.js.
You should perfectly understand the code below. If not, go back to the scale module of this course!
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
console.log(xScale(0)) // 0
console.log(xScale(100)) // 500
What I haven't mentioned yet is that the xScale
function includes a few additional methods that are quite useful:
xScale.range()
returns the range of the scale, which is[0, 500]
in this case.xScale.ticks(10)
generates an array of approximately 10 evenly spaced values along the axis. This function is quite intelligent, producing nicely rounded numbers, which can be a lifesaver.xScale.domain()
provides the input domain of the scale ([0, 100]
)
Example 🧐
xScale.ticks(2) // [0, 50, 100]
xScale.ticks(5) // [0, 20, 40, 60, 80]
xScale.ticks(9) // [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
xScale.ticks(10) // [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
See?
The .ticks()
method doesn't always return the exact number of ticks you specify. Instead, it identifies the most suitable value close to your target to ensure your axis looks polished and visually appealing!
Let's draw! ✏️
Now that we know where the ticks are going to be, we just need to draw a long horizontal line, and a multitude of small ticks with their labels at those positions!
Here is a sandbox with a very minimal example. Take a bit of time to read the code carefully!
- The horizontal line is made using a
line
element that takes the fullboundsWidth
. xScale.ticks()
is used to start a loop: 1 iteration per tick!- For each tick, a
g
element wraps aline
and atext
element forming the tick.
🎁 Reusable Bottom Axis Component
This bottom axis will likely be used across multiple charts in your project, so let’s develop a reusable component named AxisBottom
.
The AxisBottom
component accepts several properties:
xScale
: The scale that the axis will represent.pixelsPerTick
: Instead of specifying the number of ticks, it's better to define the pixels per tick. This approach ensures a consistent appearance, regardless of whether the chart is displayed on a large screen or a mobile device!
This code is inspired by Amelia Wattenberger's blog post!
// AxisBottom.tsx
import { ScaleLinear } from 'd3';
type AxisBottomProps = {
xScale: ScaleLinear<number, number>;
pixelsPerTick: number;
};
// tick length
const TICK_LENGTH = 6;
export const AxisBottom = ({ xScale, pixelsPerTick }: AxisBottomProps) => {
const range = xScale.range();
const width = range[1] - range[0];
const numberOfTicksTarget = Math.floor(width / pixelsPerTick);
return (
<>
{/* Main horizontal line */}
<line
x1={range[0]}
y1={0}
x2={range[1]}
y2={0}
stroke="currentColor"
fill="none"
/>
{/* Ticks and labels */}
{xScale.ticks(numberOfTicksTarget).map((value) => (
<g key={value} transform={`translate(${xScale(value)}, 0)`}>
<line y2={TICK_LENGTH} stroke="currentColor" />
<text
key={value}
style={{
fontSize: '10px',
textAnchor: 'middle',
transform: 'translateY(20px)',
}}
>
{value}
</text>
</g>
))}
</>
);
};
↕️ Positioning the Axis
Now that we have a functional axis, it needs to be positioned correctly—at the bottom of the bounding area.
To achieve this, we can wrap the BottomAxis
call in a g
element and apply a vertical translation. Here’s what the code looks like:
<g transform={`translate(0, ${boundsHeight})`}>
<AxisBottom xScale={xScale} pixelsPerTick={60} />
</g>
Exercices
We got axes! 🪓
If you've followed the previous exercises, you now know how to add a bottom axis to your graph.
Adding a left axis works in much the same way! Wrap it in an AxisLeft
component, and you're good to go!
Take a moment to review the example code below:
This axis is rendered without using d3.js to render.
code for the Y axis react component
import { ScaleLinear } from 'd3';
type AxisLeftProps = {
yScale: ScaleLinear<number, number>;
pixelsPerTick: number;
};
// tick length
const TICK_LENGTH = 6;
export const AxisLeft = ({ yScale, pixelsPerTick }: AxisLeftProps) => {
const range = yScale.range();
const height = range[0] - range[1];
const numberOfTicksTarget = Math.floor(height / pixelsPerTick);
return (
<>
{/* Main vertical line */}
<path
d={['M', 0, range[0], 'L', 0, range[1]].join(' ')}
fill="none"
stroke="currentColor"
/>
{/* Ticks and labels */}
{yScale.ticks(numberOfTicksTarget).map((value, i) => (
<g key={value} transform={`translate(0, ${yScale(value)})`}>
<line x2={-TICK_LENGTH} stroke="currentColor" />
<text
key={value}
style={{
fontSize: '10px',
textAnchor: 'middle',
transform: 'translateX(-20px)',
}}
>
{value}
</text>
</g>
))}
</>
);
};