diff --git a/src/scales/diverging.js b/src/scales/diverging.js index 9d871f02b2..081868f394 100644 --- a/src/scales/diverging.js +++ b/src/scales/diverging.js @@ -51,10 +51,11 @@ function ScaleD(key, scale, transform, channels, { // Normalize the interpolator for symmetric difference around the pivot. if (symmetric) { - const mindelta = Math.abs(transform(min) - transform(pivot)); - const maxdelta = Math.abs(transform(max) - transform(pivot)); - if (mindelta < maxdelta) interpolate = truncateLower(interpolate, mindelta / maxdelta); - else if (mindelta > maxdelta) interpolate = truncateUpper(interpolate, maxdelta / mindelta); + const mid = transform.apply(pivot); + const mindelta = mid - transform.apply(min); + const maxdelta = transform.apply(max) - mid; + if (mindelta < maxdelta) min = transform.invert(mid - maxdelta); + else if (mindelta > maxdelta) max = transform.invert(mid + mindelta); } scale.domain([min, pivot, max]).unknown(unknown).interpolator(interpolate); @@ -64,7 +65,7 @@ function ScaleD(key, scale, transform, channels, { } export function ScaleDiverging(key, channels, options) { - return ScaleD(key, scaleDiverging(), x => x, channels, options); + return ScaleD(key, scaleDiverging(), transformIdentity, channels, options); } export function ScaleDivergingSqrt(key, channels, options) { @@ -72,29 +73,58 @@ export function ScaleDivergingSqrt(key, channels, options) { } export function ScaleDivergingPow(key, channels, {exponent = 1, ...options}) { - return ScaleD(key, scaleDivergingPow().exponent(exponent), transformPow(exponent), channels, {...options, type: "diverging-pow"}); + return ScaleD(key, scaleDivergingPow().exponent(exponent = +exponent), transformPow(exponent), channels, {...options, type: "diverging-pow"}); } export function ScaleDivergingLog(key, channels, {base = 10, pivot = 1, domain = inferDomain(channels, pivot < 0 ? negative : positive), ...options}) { - return ScaleD(key, scaleDivergingLog().base(base), Math.log, channels, {domain, pivot, ...options}); + return ScaleD(key, scaleDivergingLog().base(base = +base), transformLog, channels, {domain, pivot, ...options}); } export function ScaleDivergingSymlog(key, channels, {constant = 1, ...options}) { - return ScaleD(key, scaleDivergingSymlog().constant(constant), transformSymlog(constant), channels, options); + return ScaleD(key, scaleDivergingSymlog().constant(constant = +constant), transformSymlog(constant), channels, options); } -function truncateLower(interpolate, k) { - return t => interpolate(t < 0.5 ? t * k + (1 - k) / 2 : t); -} +const transformIdentity = { + apply(x) { + return x; + }, + invert(x) { + return x; + } +}; -function truncateUpper(interpolate, k) { - return t => interpolate(t > 0.5 ? t * k + (1 - k) / 2 : t); -} +const transformLog = { + apply: Math.log, + invert: Math.exp +}; + +const transformSqrt = { + apply(x) { + return Math.sign(x) * Math.sqrt(Math.abs(x)); + }, + invert(x) { + return Math.sign(x) * (x * x); + } +}; function transformPow(exponent) { - return exponent === 0.5 ? Math.sqrt : x => Math.sign(x) * Math.pow(Math.abs(x), exponent); + return exponent === 0.5 ? transformSqrt : { + apply(x) { + return Math.sign(x) * Math.pow(Math.abs(x), exponent); + }, + invert(x) { + return Math.sign(x) * Math.pow(Math.abs(x), 1 / exponent); + } + }; } function transformSymlog(constant) { - return x => Math.sign(x) * Math.log1p(Math.abs(x / constant)); + return { + apply(x) { + return Math.sign(x) * Math.log1p(Math.abs(x / constant)); + }, + invert(x) { + return Math.sign(x) * Math.expm1(Math.abs(x)) * constant; + } + }; } diff --git a/test/scales/scales-test.js b/test/scales/scales-test.js index 8b102b54be..21fb116eef 100644 --- a/test/scales/scales-test.js +++ b/test/scales/scales-test.js @@ -393,18 +393,14 @@ it("plot(…).scale('color') can return an asymmetric diverging scale", async () it("plot(…).scale('color') can return a symmetric diverging scale", async () => { const gistemp = await d3.csv("data/gistemp.csv", d3.autoType); const plot = Plot.dot(gistemp, {x: "Date", stroke: "Anomaly"}).plot({color: {type: "diverging"}}); - const {interpolate, ...color} = plot.scale("color"); - assert.deepStrictEqual(color, { + assert.deepStrictEqual(plot.scale("color"), { type: "diverging", - domain: [-0.78, 1.35], + domain: [-1.35, 1.35], + interpolate: d3.interpolateRdBu, pivot: 0, clamp: false, label: "Anomaly" }); - const k = 0.78 / 1.35; - for (const t of d3.ticks(0, 1, 100)) { - assert.strictEqual(interpolate(t), d3.interpolateRdBu(t < 0.5 ? t * k + (1 - k) / 2: t)); - } }); it("plot(…).scale('color') can return a diverging scale with an explicit range", async () => { @@ -455,6 +451,21 @@ it("plot(…).scale('color') can return a transformed diverging scale", async () }); }); +it("plot(…).scale('color') can return a transformed symmetric diverging scale", async () => { + const transform = d => d * 100; + const gistemp = await d3.csv("data/gistemp.csv", d3.autoType); + const plot = Plot.dot(gistemp, {x: "Date", stroke: "Anomaly"}).plot({color: {type: "diverging", transform}}); + assert.deepStrictEqual(plot.scale("color"), { + type: "diverging", + domain: [-135, 135], + pivot: 0, + transform, + interpolate: d3.interpolateRdBu, + clamp: false, + label: "Anomaly" + }); +}); + it("plot(…).scale('color') can return an asymmetric diverging pow scale with an explicit scheme", async () => { const gistemp = await d3.csv("data/gistemp.csv", d3.autoType); const plot = Plot.dot(gistemp, {x: "Date", stroke: "Anomaly"}).plot({color: {type: "diverging-sqrt", symmetric: false, scheme: "piyg"}});