Skip to content

Commit db30802

Browse files
authored
fix edge case with dodge (#904)
align circle edges filter instead of sort Avoid a floating point error when computing the dodge transform with fixed radius. Add a line to the cars-dodge example to show how it's "based" comment for the funky k=2 small tolerance to avoid floating point errors when a circle doesn't fit *exactly*. Since we're in pixel space it's guaranteed that 1e-6 is "small enough" (and if this doesn't work for someone it's possible to add 0.01 or something to the padding). This approach gives a more compact result in all the test plots.
1 parent ee95211 commit db30802

12 files changed

+1011
-538
lines changed

src/transforms/dodge.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {max} from "d3";
21
import IntervalTree from "interval-tree-1d";
32
import {finite, positive} from "../defined.js";
43
import {identity, number, valueof} from "../options.js";
@@ -59,23 +58,26 @@ function dodge(y, x, anchor, padding, options) {
5958
if (R) R = coerceNumbers(valueof(R.value, scales[R.scale] || identity));
6059
let [ky, ty] = anchor(dimensions);
6160
const compare = ky ? compareAscending : compareSymmetric;
62-
if (ky) ty += ky * ((R ? max(facets, I => max(I, i => R[i])) : r) + padding); else ky = 1;
6361
const Y = new Float64Array(X.length);
6462
const radius = R ? i => R[i] : () => r;
6563
for (let I of facets) {
6664
const tree = IntervalTree();
6765
I = I.filter(R ? i => finite(X[i]) && positive(R[i]) : i => finite(X[i]));
68-
const intervals = new Float64Array(2 * I.length);
66+
const intervals = new Float64Array(2 * I.length + 2);
6967
for (const i of I) {
70-
const l = X[i] - radius(i);
71-
const h = X[i] + radius(i);
68+
const ri = radius(i);
69+
const y0 = ky ? ri + padding : 0; // offset baseline for varying radius
70+
const l = X[i] - ri;
71+
const h = X[i] + ri;
72+
73+
// The first two positions are 0 to test placing the dot on the baseline.
74+
let k = 2;
7275

7376
// For any previously placed circles that may overlap this circle, compute
7477
// the y-positions that place this circle tangent to these other circles.
7578
// https://observablehq.com/@mbostock/circle-offset-along-line
76-
let k = 0;
7779
tree.queryInterval(l - padding, h + padding, ([,, j]) => {
78-
const yj = Y[j];
80+
const yj = Y[j] - y0;
7981
const dx = X[i] - X[j];
8082
const dr = padding + (R ? R[i] + R[j] : 2 * r);
8183
const dy = Math.sqrt(dr * dr - dx * dx);
@@ -84,20 +86,27 @@ function dodge(y, x, anchor, padding, options) {
8486
});
8587

8688
// Find the best y-value where this circle can fit.
87-
out: for (const y of intervals.slice(0, k).sort(compare)) {
89+
let candidates = intervals.slice(0, k);
90+
if (ky) candidates = candidates.filter(y => y >= 0);
91+
out: for (const y of candidates.sort(compare)) {
8892
for (let j = 0; j < k; j += 2) {
89-
if (y > intervals[j] && y < intervals[j + 1]) {
93+
if (intervals[j] + 1e-6 < y && y < intervals[j + 1] - 1e-6) {
9094
continue out;
9195
}
9296
}
93-
Y[i] = y;
97+
Y[i] = y + y0;
9498
break;
9599
}
96100

97101
// Insert the placed circle into the interval tree.
98102
tree.insert([l, h, i]);
99103
}
100-
for (const i of I) Y[i] = Y[i] * ky + ty;
104+
}
105+
if (!ky) ky = 1;
106+
for (const I of facets) {
107+
for (const i of I) {
108+
Y[i] = Y[i] * ky + ty;
109+
}
101110
}
102111
return {data, facets, channels: {
103112
[x]: {value: X},
@@ -112,5 +121,5 @@ function compareSymmetric(a, b) {
112121
}
113122

114123
function compareAscending(a, b) {
115-
return (a < 0) - (b < 0) || (a - b);
124+
return a - b;
116125
}

test/output/carsDodge.svg

Lines changed: 448 additions & 0 deletions
Loading

test/output/dodgeTextRadius.svg

Lines changed: 86 additions & 86 deletions
Loading

test/output/penguinDodge.svg

Lines changed: 60 additions & 60 deletions
Loading

test/output/penguinDodgeHexbin.svg

Lines changed: 54 additions & 54 deletions
Loading

test/output/penguinFacetDodge.svg

Lines changed: 32 additions & 32 deletions
Loading

0 commit comments

Comments
 (0)