Andrey
Andrey

Reputation: 1496

Guarantee the number of ticks in D3

I have a X axis that has this kind of data:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

Sequential numbers. I want to have a bottom axis with X ticks but want to always include the first and the last item—which, in this case is: 1 and 15 respectively.

The problem with d3.ticks is that it sometimes doesn't return the last tick. And I can't use nice: true because all ticks should be numbers that exist in the data set.

Thoughts?

EDIT: The goal is to find an even space between ticks considering that the first and the last tick should be always present.

Upvotes: 1

Views: 339

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

In your specific situation you can use d3.range to generate the ticks, that you'll pass to your axis' tickValues method. For instance:

const scale = d3.scaleLinear([0, 15], [0, 1]);
const numberOfTicks = 7;
const tickStep = (scale.domain()[1] - scale.domain()[0]) / (numberOfTicks - 1);
const myTicks = d3.range(scale.domain()[0], scale.domain()[1] + tickStep, tickStep);

console.log(myTicks)
<script src="https://d3js.org/d3.v5.min.js"></script>

However, due to the floating-point precision issues, you may have an unexpected length for your array depending on the combination of domain/number of ticks. For instance, using [8,15] as the domain and 7 ticks:

const scale = d3.scaleLinear([8, 15], [0, 1]);
const numberOfTicks = 7;
const tickStep = (scale.domain()[1] - scale.domain()[0]) / (numberOfTicks - 1);
const myTicks = d3.range(scale.domain()[0], scale.domain()[1] + tickStep, tickStep);

console.log(myTicks)
<script src="https://d3js.org/d3.v5.min.js"></script>

As you can see, we have 8 elements in the array, not 7.

For dealing with those cases, you can just pass the number of ticks to d3.range and deal with the math using map:

const scale = d3.scaleLinear().domain([8, 15]);
const numberOfTicks = 7;
const tickStep = (scale.domain()[1] - scale.domain()[0]) / (numberOfTicks - 1);
const myTicks = d3.range(numberOfTicks)
  .map(d => scale.domain()[0] + d * tickStep);

console.log(myTicks);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Integers

The previous examples generate evenly spaced ticks. However, I suppose you want integers as values. In that case, pay attention to the fact that you cannot have both integers and evenly spaced ticks for all possible combinations of domain and number of ticks.

To get integers, just round the values:

const scale = d3.scaleLinear([0, 15], [0, 1]);
const numberOfTicks = 7;
const tickStep = (scale.domain()[1] - scale.domain()[0]) / (numberOfTicks - 1);
const myTicks = d3.range(scale.domain()[0], scale.domain()[1] + tickStep, tickStep)
  .map(d => Math.round(d))

console.log(myTicks)
<script src="https://d3js.org/d3.v5.min.js"></script>

And here the same approach as the third snippet, doing all the math in the map and using [8,15] as an example:

const scale = d3.scaleLinear().domain([8, 15]);
const numberOfTicks = 7;
const tickStep = (scale.domain()[1] - scale.domain()[0]) / (numberOfTicks - 1);
const myTicks = d3.range(numberOfTicks)
  .map(d => Math.round(scale.domain()[0] + d * tickStep));

console.log(myTicks);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Upvotes: 6

Related Questions