Skip to content

Commit

Permalink
exact log ticks (#253)
Browse files Browse the repository at this point in the history
* fix #234; exact log ticks

* let/const

(see also #212)

Co-authored-by: Philippe Rivière <fil@rezo.net>
  • Loading branch information
mbostock and Fil authored Sep 19, 2021
1 parent 8fd6d25 commit 9f745d0
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 45 deletions.
86 changes: 41 additions & 45 deletions src/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,37 @@ function transformExpn(x) {
return -Math.exp(-x);
}

function pow10(x) {
return isFinite(x) ? +("1e" + x) : x < 0 ? 0 : x;
function pow10(x, k) {
return isFinite(x) ? +(k + "e" + x) : x < 0 ? 0 : x;
}

function exp(x, k) {
return Math.exp(x) * k;
}

function powp(base) {
return base === 10 ? pow10
: base === Math.E ? Math.exp
: function(x) { return Math.pow(base, x); };
: base === Math.E ? exp
: (x, k) => Math.pow(base, x) * k;
}

function logp(base) {
return base === Math.E ? Math.log
: base === 10 && Math.log10
|| base === 2 && Math.log2
|| (base = Math.log(base), function(x) { return Math.log(x) / base; });
|| (base = Math.log(base), (x) => Math.log(x) / base);
}

function reflect(f) {
return function(x) {
return -f(-x);
};
return (x, k) => -f(-x, k);
}

export function loggish(transform) {
var scale = transform(transformLog, transformExp),
domain = scale.domain,
base = 10,
logs,
pows;
const scale = transform(transformLog, transformExp);
const domain = scale.domain;
let base = 10;
let logs;
let pows;

function rescale() {
logs = logp(base), pows = powp(base);
Expand All @@ -69,81 +71,75 @@ export function loggish(transform) {
return arguments.length ? (domain(_), rescale()) : domain();
};

scale.ticks = function(count) {
var d = domain(),
u = d[0],
v = d[d.length - 1],
r;
scale.ticks = count => {
const d = domain();
let u = d[0];
let v = d[d.length - 1];
const r = v < u;

if (r = v < u) i = u, u = v, v = i;
if (r) ([u, v] = [v, u]);

var i = logs(u),
j = logs(v),
p,
k,
t,
n = count == null ? 10 : +count,
z = [];
let i = logs(u);
let j = logs(v);
let k;
let t;
const n = count == null ? 10 : +count;
let z = [];

if (!(base % 1) && j - i < n) {
i = Math.floor(i), j = Math.ceil(j);
if (u > 0) for (; i <= j; ++i) {
for (k = 1, p = pows(i); k < base; ++k) {
t = p * k;
for (k = 1; k < base; ++k) {
t = pows(i, k);
if (t < u) continue;
if (t > v) break;
z.push(t);
}
} else for (; i <= j; ++i) {
for (k = base - 1, p = pows(i); k >= 1; --k) {
t = p * k;
for (k = base - 1; k >= 1; --k) {
t = pows(i, k);
if (t < u) continue;
if (t > v) break;
z.push(t);
}
}
if (z.length * 2 < n) z = ticks(u, v, n);
} else {
z = ticks(i, j, Math.min(j - i, n)).map(pows);
z = ticks(i, j, Math.min(j - i, n)).map(i => pows(i, 1));
}

return r ? z.reverse() : z;
};

scale.tickFormat = function(count, specifier) {
scale.tickFormat = (count, specifier) => {
if (count == null) count = 10;
if (specifier == null) specifier = base === 10 ? ".0e" : ",";
if (typeof specifier !== "function") {
if (!(base % 1) && (specifier = formatSpecifier(specifier)).precision == null) specifier.trim = true;
specifier = format(specifier);
}
if (count === Infinity) return specifier;
var k = Math.max(1, base * count / scale.ticks().length); // TODO fast estimate?
return function(d) {
var i = d / pows(Math.round(logs(d)));
const k = Math.max(1, base * count / scale.ticks().length); // TODO fast estimate?
return d => {
let i = d / pows(Math.round(logs(d)), 1);
if (i * base < base - 0.5) i *= base;
return i <= k ? specifier(d) : "";
};
};

scale.nice = function() {
scale.nice = () => {
return domain(nice(domain(), {
floor: function(x) { return pows(Math.floor(logs(x))); },
ceil: function(x) { return pows(Math.ceil(logs(x))); }
floor: x => pows(Math.floor(logs(x)), 1),
ceil: x => pows(Math.ceil(logs(x)), 1)
}));
};

return scale;
}

export default function log() {
var scale = loggish(transformer()).domain([1, 10]);

scale.copy = function() {
return copy(scale, log()).base(scale.base());
};

const scale = loggish(transformer()).domain([1, 10]);
scale.copy = () => copy(scale, log()).base(scale.base());
initRange.apply(scale, arguments);

return scale;
}
7 changes: 7 additions & 0 deletions test/log-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ it("log.domain(…) preserves specified domain exactly, with no floating point e
assert.deepStrictEqual(x.domain(), [0.1, 1000]);
});

it("log.ticks(…) returns exact ticks, with no floating point error", () => {
assert.deepStrictEqual(scaleLog().domain([0.15, 0.68]).ticks(), [0.2, 0.3, 0.4, 0.5, 0.6]);
assert.deepStrictEqual(scaleLog().domain([0.68, 0.15]).ticks(), [0.6, 0.5, 0.4, 0.3, 0.2]);
assert.deepStrictEqual(scaleLog().domain([-0.15, -0.68]).ticks(), [-0.2, -0.3, -0.4, -0.5, -0.6]);
assert.deepStrictEqual(scaleLog().domain([-0.68, -0.15]).ticks(), [-0.6, -0.5, -0.4, -0.3, -0.2]);
});

it("log.range(…) does not coerce values to numbers", () => {
const x = scaleLog().range(["0", "2"]);
assert.strictEqual(typeof x.range()[0], "string");
Expand Down

0 comments on commit 9f745d0

Please sign in to comment.