Functional Weave
Code in TypeScript

charts.scale@1.1.0

README.md

3,731 bytes · view raw

# charts.scale

A linear scale maps a data value onto a position: `linearScale([0, 100], [0, 500], 25, false)` is `125`, a quarter of the way along a 500 pixel axis. `invertLinear` goes the other way, from a pointer position back to the value under it, and `niceDomain` widens a data extent such as `[0.13, 9.7]` to `[0, 10]` so the axis starts and ends on a tick.

This is a group: three functions that only make sense together, in one package, each in its own file. Install only what you call with `require charts.scale ^1.0.0 only=linearScale`; `invertLinear` brings `linearScale` with it, because it reuses its arithmetic.

Scales are geometry, not money, so they work in floating point. Every result is rounded to 6 decimal places inside the function (half up, as `floor(x * 1e6 + 0.5) / 1e6`), which is far below a pixel and makes TypeScript, Python and Rust agree to the digit.

A domain or range is a two-element list, `[from, to]`. Either may run backwards: a y axis usually maps `[0, max]` onto `[height, 0]`. A domain whose two ends are equal has no width to map, and `linearScale` raises rather than divide by zero; `invertLinear` raises for a range with equal ends for the same reason.

Without `clamp`, a value outside the domain maps outside the range, which is what an axis drawing a point past its end wants. With it, the result stops at the range's ends.

`niceDomain` picks a tick step of 1, 2 or 5 times a power of ten, aiming for about `count` ticks across the domain, then moves each end out to a multiple of that step, repeating until the step stops changing. It returns the step with the domain, so the axis can draw ticks on exactly those multiples. A domain whose ends are equal is returned unchanged with a step of 0.

## New in 1.1.0: log, time, band and point scales

The three functions above are unchanged from 1.0.0, rounding included (half
up, `floor(x * 1e6 + 0.5)`), so upgrading moves no existing point. The four new
ones round to 6 decimal places with `math.round-float` (half away from zero on
the exact double), which is what every newer charts capability uses.

**`logScale`** maps equal ratios to equal lengths: on `[1, 1000]` the value 10
is a third of the way along. The base of the logarithm cancels, so there is no
base to pass. Both domain ends and the value must be greater than zero. The
logarithm is `math.ln`, built from `+ - * /` only, because `Math.log`,
`math.log` and `f64::ln` may differ in their last bit, which is enough to
split two languages at a rounding boundary.

**`timeScale`** is a linear scale over calendar days: the position of an ISO
date is its day count from the domain's first date (by `dates.days-between`)
over the domain's length in days. There is no `Date` object and no time zone,
so a clock change or a machine in another zone cannot move a point by an hour
or a day. Dates only; a scale over times of day is out of scope.

**`bandScale`** is d3.scaleBand without pixel rounding. The range is divided
into `n - paddingInner + 2 x paddingOuter` steps; each band is one step less
`paddingInner` of it; `align` shares the leftover outer space (0 all at the
end, 1 all at the start, 0.5 centred). It returns the band's `start` (always
its lower coordinate), `center` and `width`. A reversed range
reverses the order of the
categories, not the direction of a band. Repeated categories are an error:
d3 silently merges them, which draws two bars on top of each other.

**`pointScale`** is d3.scalePoint: a band scale with `paddingInner` 1, so the
bands have no width and each category is a point; `padding` is in steps at
each end.

Sources: Mike Bostock, d3-scale (github.com/d3/d3-scale), `band.js`,
`log.js` and `time.js`, whose layout rules these follow.