Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

d3.ticks returns too much ticks #61

Closed
alexanderby opened this issue Jul 12, 2017 · 7 comments
Closed

d3.ticks returns too much ticks #61

alexanderby opened this issue Jul 12, 2017 · 7 comments

Comments

@alexanderby
Copy link

d3.ticks(0, 320, 23);
// > Array (33)
//  [0, 10, 20, 30, 40, 50, 60, 70, 80, 90,
//  100, 110, 120, 130, 140, 150, 160, 170, 180, 190,
//  200, 210, 220, 230, 240, 250, 260, 270, 280, 290,
//  300, 310, 320]

I would prefer that ticks count would never be exceeded. This will prevent axis ticks overlap, when ticks count is calculated based on displayed tick size.

@mbostock
Copy link
Member

This is the expected behavior. The given count is a hint, not an upper bound on the number of returned ticks. If you want an upper bound, I suggest looking at the implementation and forking it.

@alexanderby
Copy link
Author

Let me disagree, this is the unexpected behavior. I've tried to calculate how stable this function is.
In worst cases it has 100% error (e.g. d3.ticks(0, 6, 2) returns 4 ticks, d3.ticks(0, 22, 7) returns 12 ticks). Error >25% occured in 23% of cases, >50% in 3% of cases. I think I'm not the only who didn't understand why axis ticks sometimes overlap each other, but now I know the reason.

const countMin = 2;
const countMax = 100;
const step = 1;
const start = 0;
const max = 100;
const threshold = 0.25;

let maxError = -Infinity;
let worst = null;
let total = 0;
let failures = 0;

for (let end = start + step; end <= max; end += step) {
  for (let count = countMin; count <= countMax; count++) {
    let ticks = d3.ticks(start, end, count);
    let error = (ticks.length - count) / count;
    if (error > 0 && error > maxError) {
      maxError = error;
      worst = {
        start,
        end,
        count,
        result: ticks.length
      };
    }
    total++;
    if (error > threshold) {
      failures++;
    }
  }
}

const percent = (x) => `${(x * 100).toFixed()}%`;
console.log(`> Worst result: d3.ticks(${worst.start}, ${worst.end}, ${worst.count})`);
console.log(`> Array(${worst.result}) (error ${percent(maxError)})`);
console.log(`> Error more than ${percent(threshold)} in ${percent(failures / total)} cases`);

@mbostock
Copy link
Member

mbostock commented Jul 12, 2017

Happy to consider a change that improves the log-error rate but I believe the current implementation is optimal given the constraints.

By “expected” I mean that the behavior is documented in the README (and has remained consistent since D3’s first release). If we wanted this functionality it would probably need to be a different method than d3.ticks (and by extension scale.ticks and axis.ticks), so it would be very difficult to change while retaining backwards compatibility.

@mbostock
Copy link
Member

To address the two worst cases you mentioned:

d3.ticks(0, 6, 2) tries to generate 3 (count + 1) ticks. The two closest possible results are [0, 2, 4, 6] (4 ticks) and [0, 5] (2 ticks). The log errors of these possibilities are |log(4) - log(3)| = 0.287682 and |log(2) - log(3)| = 0.405465 respectively. Log error is used to evaluate the length of the generated ticks array relative to the desired length. The first result is 4/3 = e^0.287682 = 1.333× too big; the second result is 3/2 = e^0.405465 = 1.5× too small. Therefore [0, 2, 4, 6] is considered the optimal result.

Likewise d3.ticks(0, 22, 7) tries to generate 8 ticks. The closest two possible results are [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22] (12 ticks, log error of 0.405465) and [0, 5, 10, 15, 20] (5 ticks, log error of 0.470003). So the 12-tick result is considered optimal.

@alexanderby
Copy link
Author

I understand that this solution is based on log error, but I would prefer that error calculation would be based on aesthetics as far as finally we want to beautifully draw axis ticks. If I pass 2 ticks to d3.ticks(0, 6, 2) I expect to get [0,6]. d3.ticks(0, 22, 7) should return [0, 5, 10, 15, 20], it is more suitable by ticks count, doesn't exceed limit and is visually better, because tickstep=2 is less readable).

I've already familiar with D3 v3 implementation, I'll read your notes considering ticks-issues and maybe come with a solution. Currently I workaround this by decreasing ticks count in a loop until resulting ticks count becomes lower.

Here is a visualization of errors, there seems to be some dependency.

@mbostock
Copy link
Member

mbostock commented Jul 12, 2017

d3.ticks isn’t used only for axis ticks; it’s used for a variety of other purposes where human-readable numbers are desired, such as for histogram.thresholds and contours.thresholds. Also, this API has no understanding of how “wide” a tick is, or how long an axis is. If you want to prevent overlapping axis ticks, you’re probably better off doing it at the display layer, say by post-processing the SVG generated by d3-axis.

@aarthi0808
Copy link

aarthi0808 commented Dec 6, 2017

We are also facing the same kind of issues in V4 while drawing axes. In V3, as error calculation was different, ticks generated was better to read than in current version. For generating ticks, we are thinking to use V3 tick calculation method.

It would be better if you provide some other solution. @mbostock

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants