Skip to content

symmetric diverging scales by domain extension #563

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

Merged
merged 1 commit into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions src/scales/diverging.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -64,37 +65,66 @@ 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) {
return ScaleDivergingPow(key, channels, {...options, exponent: 0.5});
}

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;
}
};
}
25 changes: 18 additions & 7 deletions test/scales/scales-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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"}});
Expand Down