Interpolators
Non-numeric range
When the range of a continuous scale is not an interval between real numbers, but an array of values of a non-numeric type (such as color codes), the scale must use a function to compute intermediate values between two given values. Such a function is called an interpolator.
The default interpolator is d3.interpolate(a,b)
.
Actually, d3.interpolate(a,b)
automatically detects the type of the values, by looking at the type of value b
,
and picks the appropriate interpolator for that type.
It recognizes color codes, numbers, strings containing numbers, JavaScript date objects, arrays and objects.
Interval [a,b]
represents the range of the scale.
When d3.interpolate(a,b)
is used on a d3 scale (like in the example below),
arguments a
and b
are not explicitly given.
The range of the scale is automatically passed to arguments a
and b
of the interpolator.
The arguments a
and b
are only explicitly used when the
interpolator is used as a "stand-alone" function (see later).
The default interpolator can be changed by using myScale.interpolate(interpolator)
.
The next example calls the default interpolator in three different ways.
Scale d3.scaleLinear
automatically picks
.interpolate(d3.interpolate)
which automatically picks .interpolate(d3.interpolateRgb)
for color types.
let colorScale;
colorScale = d3.scaleLinear([0, 20], ["Tomato", "Turquoise"]);
// is equivalent to:
colorScale = d3.scaleLinear([0, 20], ["Tomato", "Turquoise"])
.interpolate(d3.interpolate);
// is equivalent to:
colorScale = d3.scaleLinear([0, 20], ["Tomato", "Turquoise"])
.interpolate(d3.interpolateRgb);
Result:
Next example changes the default interpolator
to d3.interpolateHclLong
:
const colorScale = d3.scaleLinear([0, 20], ["Tomato", "Turquoise"])
.interpolate(d3.interpolateHclLong);
d3.select("#container")
.selectAll("span")
.data([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20])
.join("span")
.style("background", d => colorScale(d))
Result:
PS. Compare the scale above with the visual representation of the previous color scale that uses default d3.interpolateRgb
.
In the next example the range is an interval of JavaScript date objects.
By default, d3.interpolate
picks
d3.interpolateDate
for JavaScript date types.
const days = d3.scaleLinear().range([new Date("2009-01-01"), new Date("2009-12-31")]);
console.log(days(0.5)); // logs: Thu Jul 02 2009
Note that if the domain of a scale is not specified, it defaults to [0, 1]
.
The same thing holds for the range.
Note that interpolators maintain the linearity of the linear scale. Interpolators on other kinds of continuous scales also maintain the mapping function of the scale.
Round ranges
In fact, not only continuous scales with non-numeric ranges, but all scales, including the ones with numeric ranges,
use an interpolator to compute intermediate values between two given values.
When d3.interpolate
recognizes a numeric range, it picks the
default d3.interpolateNumber
interpolator for number types:
let numbers;
numbers = d3.scaleLinear().range([20, 60]);
console.log(numbers(0.5)); // logs: 40
// is equivalent to:
numbers = d3.scaleLinear().range([20, 60]).interpolate(d3.interpolate);
console.log(numbers(0.5)); // logs: 40
// is equivalent to:
numbers = d3.scaleLinear().range([20, 60]).interpolate(d3.interpolateNumber);
console.log(numbers(0.5)); // logs: 40
For continuous scales with ranges with number values, the default interpolator d3.interpolateNumber
can be changed to
d3.interpolateRound
.
This rounds the returned values to integers.
let numbers;
numbers = d3.scaleLinear().range([0, 3]).interpolate(d3.interpolateRound);
console.log(numbers(0.6)); // logs: 2
console.log(numbers(0.7)); // logs: 2
// or a shorter version:
numbers = d3.scaleLinear().rangeRound([0, 3]);
console.log(numbers(0.6)); // logs: 2
console.log(numbers(0.7)); // logs: 2
d3.interpolateRound
maps from a continuous input domain to a discrete output range (integers are discrete).
An example with continuous input and discrete output (integers connected to "grades"):
const students = [
{ name: "Joe", score: 35},
{ name: "Abby", score: 86},
{ name: "Casey", score: 44},
{ name: "Max", score: 62}
];
const grades = d3.scaleLinear()
.rangeRound([0, 5]);
d3.select("#container")
.selectAll("span")
.data(students)
.join("span")
.text(d => ` ${d.name} scored a ${["F", "E", "D", "C", "B", "A"][grades(d.score / 100)]}.`); // ["F", "E", "D", "C", "B", "A"][2] === "D"
Result:
BTW: The same thing could have been achieved by using
d3.scaleQuantize
instead of d3.scaleLinear
(later more about quantize scales).
String and object ranges
By default, d3.interpolate
uses d3.interpolateString
on string valued ranges. The string interpolator finds numbers embedded in the strings and interpolate them.
See "Stand alone" interpolators for an example.
For array or object valued ranges, d3.interpolate
uses
d3.interpolateArray
and
d3.interpolateObject
respectively.
If the array or object contains string values, d3.interpolateString
will be used to interpolate,
and if the array or object contains nested arrays of objects, d3.interpolateArray
or d3.interpolateObject
will be used recursively.
Eventually d3.interpolateNumber
will be used to interpolate all embedded numbers or embedded values that can be coerced to numbers.
const myScale = d3.scaleLinear()
.range([[0, "0.5 mile", [12]], [10, "28 miles", [36]]]);
console.log(myScale(0.5)); // logs: [ 5, "14.25 miles", [24] ]
const myScale = d3.scaleLinear()
.range([
{ time: 0, distance: "0.5 mile", details: { cost: 12 } },
{ time: 10, distance: "28 miles", details: { cost: 36 } }
]);
console.log(myScale(0.5)); // logs: { time: 5, distance: "14.25 miles", details: { cost: 24 } }
If object b
in range([a, b])
contains enumerable properties that are not present in object a
,
then these properties are copied to the resulting interpolated object, without being changed.
This provides a way to only interpolate some properties and leave the others unchanged:
const myScale = d3.scaleLinear()
.range([
{ a: 0, c: 12 },
{ a: "10", b: "5 do not change", c: 36 }
]);
console.log(myScale(0.5)); // logs: { b: "5 do not change", a: "5", c: 24 }
The returned interpolated object is not a deep clone and is for every interpolation the same object:
const myScale = d3.scaleLinear()
.range([
{ prop1: 0, prop2: "0 cats", prop3: { k: 0 } },
{ prop1: 10, prop2: "10 dogs", prop3: { k: 10 } }
]);
const a = myScale(0.5);
const b = myScale(0.7);
console.log(a.prop3 === b.prop3); // logs: true // (not a deep clone)
console.log(a.prop3); // logs: { k: 7 }
console.log(a === b); // logs: true
console.log(a); // logs: { prop1: 7, prop2: "7 dogs", prop3: { k: 7 } }