# Continuous non-linear scales

## Scales for continuous quantitative data

In previous chapters we covered linear scales `d3.scaleLinear`, `d3.scaleTime`, `d3.scaleUtc`, `d3.scaleSequential` and `d3.scaleDiverging`. D3 also provides some non-linear scales for continuous quantitative data. In this chapter we will explore power scales, and more specifically the square root scale, and logarithmic scales.

## Power scales

A power scale maps each range value y to a domain value x by the function y = mxk + b, where k is the exponent. The default exponent is 1, which yields a linear scale, and the exponent can be any real number, except 0. Constants m and b are defined by the given domain and range.

``````
let myScale = d3.scalePow()
.domain([0, 100])
.range([0, 16])
.exponent(2);
console.log(myScale(50));  // logs: 4 // 50 (1/2 * 100) => 4 (1/4 * 16)
``````

## Square root scales

`d3.scaleSqrt` is short for `d3.scalePow().exponent(0.5)`. A square root scale is often used where the output y is a radius of a disk, but it is the disk's area that needs to be proportional to the input value x. The area is proportional to the square of the radius (A = πr2), so conversely, the radius y needs to be proportional to the square root of the input value x.

```π y 2 = cx  // area proportional to input value
y 2 = nx
y = m√x
```
``````
const countries = [
{name: "Germany", population: 84_270_625},
{name: "France", population: 68_042_591},
{name: "Spain", population: 47_222_613},
{name: "the Netherlands", population: 17_882_900},
{name: "Sweden", population: 10_481_937},
];

const largestPopulation = d3.max(countries, d => d.population);

const areaScale = d3.scaleSqrt()
.domain([0, largestPopulation])
.range([0, 50]);

const colorScale = d3.scaleOrdinal(d3.schemeCategory10.slice(10 - countries.length)); //*

const largestDiskDiameter = areaScale(largestPopulation) * 2;
const fontSize = 10;

const svg = d3.select("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("font-family", "sans-serif")
.attr("font-size", fontSize)
.attr("text-anchor", "middle")
.attr("viewBox", `0 0 \${countries.length * largestDiskDiameter} \${largestDiskDiameter}`);

const disks = svg.selectAll("g")
.data(countries)
.join("g")
.attr("transform", (d, i) => `translate(\${i * largestDiskDiameter + largestDiskDiameter/2}, \${largestDiskDiameter/2})`)
.call(g => g.append("circle")
.style('fill', (d, i) => colorScale(d.name))
.attr("r", (d, i) => areaScale(d.population))
)
.call(g => g.append("text")
.style('fill', "var(--main-color)")
.attr('dy', fontSize/2)
.text(d => d.name)
);
``````

Result:

*) See d3 categorical schemes for `d3.schemeCategory10`. `d3.scaleOrdinal` will be explained later.

When an input value is negative, the scale first computes the output for the absolute (positive) value, and then negates the result.

Also scale `d3.scaleRadial` is available that does the same thing as `d3.scaleSqrt` for numeric ranges. Unlike `d3.scaleSqrt`, radial scales do not support interpolate.

``````
let sqrtScale = d3.scaleSqrt()
.domain([0, 100])
.range([0, 30]);

console.log(sqrtScale(25));  // logs: 15
console.log(sqrtScale(-25));  // logs: -15

.domain([0, 100])
.range([0, 30]);

console.log(sqrtScale(25));  // logs: 15
console.log(sqrtScale(-25));  // logs: -15

sqrtScale = d3.scaleSqrt([0, 100], ["red", "blue"]);
console.log(sqrtScale(25));   // logs: "rgb(128, 0, 128)"

sqrtScale = d3.scaleRadial([0, 100], ["red", "blue"]);
console.log(sqrtScale(25));   // logs: undefined
``````

## Logarithmic scales

Logarithmic scales are obviously suitable for data that are intrinsically logarithmic, like magnitude on the Richter scale as a function of amplitude. Logarithmic scales are also very suitable for wide-range data visualizations.

Suppose our data are the grains of wheat on each chessboard square in the wheat and chessboard problem (and we stop at square 15). This results in a very wide range of values. The numbers of grains on the first 4 squares are successively 1, 2, 4, 8, but on square 15 there are 16 384 grains. Visualizing this in a graph makes it impossible to distinguish the smallest values, unless the graph has the height of an apartment building.

For wide-range problems we can use a logarithmic scale. Instead of equally spacing the numbers 0, 1, 2, 3, 4..., we now equally space b0, b1, b2, b3 ..., Where b is the base of the logarithm. A logarithmic scale maps domain values x to range values y by the function y = m logb(x) + c. Note that in this function x and y are domain (input) values and range (output) values of the scale, not of the plotted function. Each individual axis in a data visualization may or may not have a logarithmic scale.

See "a closer look at the logarithmic scale" for more information on logarithmic scales.

Next are two plots of the wheat and chessboard problem. The first has a linear scaled x-axis and y-axis, the second has a logarithmic scale (base 2) on the y-axis and a linear scale on the x-axis. The logarithmic scale spans several orders of magnitude, yet it is still possible to read the smallest values on the graph.

A linear plot (linear scale on the y-axis, linear scale on the x-axis) of f(n) = 2 n.

A log–linear plot (logarithmic scale on the y-axis, linear scale on the x-axis) of f(n) = 2 n. The base of the logarithm is 2.

The domain of a logarithmic scale must not include or cross zero, since logb(x) → − when x → 0 for b > 1 and logb(x) → when x → 0 for b < 1. The behavior of the scale is undefined if you pass a negative value to a log scale with a positive domain or vice versa. If the domain spans negative numbers, the absolute value is taken.

The base can be specified by a `base()` method:

``````
const myScale = d3.scaleLog()
.base(2)
.domain([2**-10, 2**20]) // !! A log scale domain must be strictly-positive or strictly-negative, the domain must not include or cross zero.
.range([ height, 0 ])
.nice();
``````

If `base()` is not present, the default base 10 will be used. Next example uses base 10:

``````
const objects = [
{ name: "Milky Way Galaxy", size: 1e18 }, // sizes in km
{ name: "Nearest Star", size: 1e13 },
{ name: "The Solar System", size: 1e9 },
{ name: "The Sun", size: 1e6 },
{ name: "The Earth", size: 1e3 },
{ name: "A Mountain", size: 75 },
{ name: "A Human", size: 1e-3 },
{ name: "A Cell", size: 1e-8 },
{ name: "An Atom", size: 1e-12 },
{ name: "A Proton", size: 1e-15 }
];

const htmlContainer = d3.select("#container");

const margin = {top: 10, right: 10, bottom: 30, left: 60},
svgWidth = 300,
svgHeight = 400,
width = svgWidth - margin.left - margin.right,
height = svgHeight - margin.top - margin.bottom;

const svgContainer = htmlContainer
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform",
`translate(\${margin.left},\${margin.top})`);

const scaleUniverse = d3.scaleLog()
.domain([1e-15, 1e20])
.range([ height, 0 ])
.nice();

const vertAxis = svgContainer.append("g")
.attr("transform", "translate(0,0)")
.call(d3.axisLeft(scaleUniverse).ticks(10, "~e"))

const marks = svgContainer.append("g")
.selectAll('g')
.data(objects)
.join('g')
.attr("transform", d => `translate(0, \${scaleUniverse(d.size)})`)
.call(g => g.append("circle")
.attr('r', 4)
.attr("fill", "steelblue")
)
.call(g => g.append("text")
.style('fill', "gray")
.attr("font-size", 12)
.attr("dominant-baseline", "middle")
.attr('dx', 10)
.text(d => d.name)
);
``````

Result:

Adapted from Observable notebook: Continuous scales.

## Symlog scales

`d3.scaleSymLog` provides a modified logarithmic transformation, a bi-symmetric log transformation, particularly suitable for representing wide-range data that has both positive and negative components. Unlike a log scale, a `scaleSymLog` domain can include zero. A single constant (`symlog.constant()`) (defaults to 1) is provided to tune the transformation's behavior around the "region near zero".

The next example uses `d3.scaleSymLog` for data much denser around zero than further away from zero (the days around "now" are much denser in data points than the long-gone past and far future):

``````
const days = [
// l: label ; v: value
{ l: "The Big Bang", v: -13.8e9 * 365.24 },
{ l: "Dinosaur extinction", v: -65e6 * 365.24 },
{ l: "The founding of Rome", v: -(800 + 2019) * 365.24 },
{ l: "Last year", v: -365 },
{ l: "Yesterday", v: -1 },
{ l: "Now", v: +0 },
{ l: "Tomorrow", v: +1 },
{ l: "Next year", v: +365 },
{ l: "2100", v: +365.24 * 91 },
{ l: "Asimov’s Foundation", v: +12000 * 365.24 },
{ l: "Sun dies", v: 6e9 * 365 }
];

const htmlContainer = d3.select("#container");

const margin = {top: 10, right: 10, bottom: 30, left: 60},
svgWidth = 300,
svgHeight = 600,
width = svgWidth - margin.left - margin.right,
height = svgHeight - margin.top - margin.bottom;

const svgContainer = htmlContainer
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform",
`translate(\${margin.left},\${margin.top})`);

const scale = d3.scaleSymlog()
//.domain(d3.extent(days, d => d.v))
.domain([d3.max(days, d => d.v), d3.min(days, d => d.v)])
.constant(0.1)
.range([ height, 0 ]);

const mark = svgContainer.append("g")
.selectAll('g')
.data(days)
.join('g')
.attr("transform", d => `translate(0, \${scale(d.v)})`)
.call(g => g.append("circle")
.attr('r', 4)
.attr("fill", d => (d.l === "Now" ? "red" : "steelblue"))
)
.call(g => g.append("text")
.style('fill', "gray")
.attr("font-size", 12)
.attr("dominant-baseline", "middle")
.attr('dx', 10 )
.text(d => d.l )
);

const vertLine = svgContainer.append("line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", scale.range())
.attr("y2", scale.range())
.attr("stroke", "gray");
``````

Result:

Example adapted from Observable notebook: Continuous scales.

BTW: The slider is an addition to the code presented above. View the source code to see how the slider is implemented.