Marius
Marius

Reputation: 974

How to get axis ticks out of d3 without rendering in SVG?

I'm implementing a 2d chart using canvas. I want to reuse d3's logic for generating the chart's axes. d3 does quite a lot of good work in generating axes and I'd like to take advantage of it.

(Note: For backward-compatibility reasons I'm stuck using d3v3 for the time being.)

Consider the following d3 code:

let scale = d3.time.scale()
    .range([0, width])
    .domain([lo, hi]);
let axis = d3.svg.axis()
    .scale(scale)
    .ticks(num_ticks)
    .tickSize(10)
    .orient("bottom");

I can render this into a chart div with:

svg.selectAll('.x-axis').call(axis);

I want to be able to programmatically get the tick data out of axis, including the formatted labels, by writing a function that behaves as follows:

ticks = get_axis_ticks(axis)

ticks should hold each tick position (as a Date in this particular case) and the corresponding d3-generated label.

[[Date(...), "Wed 19"],
 [Date(...), "Fri 21"],
 [Date(...), "Apr 23"],
 [Date(...), "Tue 25"],
 ...]

I could then use this data to paint an axis on my canvas.

I've dug into d3v3 source (in particular here: https://github.com/d3/d3/blob/v3.5.17/src/svg/axis.js) but I find it very difficult to tease apart the logic from the SVG manipulation.

Help would be much appreciated.

Upvotes: 3

Views: 586

Answers (2)

Marius
Marius

Reputation: 974

I was able to get this working. I found the time formatting strategy in the d3 docs: https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md#format_multi I believe this is the strategy that d3 itself uses by default when users do not provide a custom format.

I learned that simply calling scale.ticks(N) will return Nish ticks suitable for rendering on an axis. These tick values are chosen on natural boundaries. E.g., if you're working with a time axis, these ticks will be aligned on minute, hour, day boundaries.

Here's a solution:

let time_format = d3.time.format.multi([
  [".%L", d => d.getMilliseconds()],
  [":%S", d => d.getSeconds()],
  ["%I:%M", d => d.getMinutes()],
  ["%I %p", d => d.getHours()],
  ["%a %d", d => d.getDay() && d.getDate() !== 1],
  ["%b %d", d => d.getDate() !== 1],
  ["%B", d => d.getMonth()],
  ["%Y", () => true]
]);

let get_ticks = (width, lo, hi, num_ticks) => {
  let scale = d3.time.scale().range([0, width]).domain([lo, hi]);
  return scale.ticks(num_ticks).map(t => [t, time_format(t)]);
};

Upvotes: 1

Alex Fallenstedt
Alex Fallenstedt

Reputation: 2093

One idea I have is to use the scale function you have created to generate the ticks you desire and push them into an array.

As a very simple example, if you would like 10 ticks, each incrementing by a unit of 1, you could do something like this: https://jsfiddle.net/Q5Jag/3148/

//define dummy values
var lo = 1;
var hi = 10;
var width = 512
var scale = d3.time.scale()
    .range([0, width])
    .domain([lo, hi]);

//define your function
var get_x_axis = function() {
    let axisArr = [];
    for(var i = 0; i < 10; i++ ) {
    //calculate your value
    axisArr.push(scale(i))
  }
  return axisArr
}

//call it
let axisTicks = get_x_axis()

//log it
console.log(axisTicks)

I'm not sure if this is what you're looking for, but if you need further help just ask.

Upvotes: 1

Related Questions