Functional Weave
Code in Python

charts.ticks@1.0.0

README.md

3,230 bytes · view raw

# charts.ticks

Where the ticks on an axis go. `niceTicks(0, 1, 10)` is `[0, 0.1, ..., 1]`,
`timeTicks("2026-01-01", "2026-12-31", "month", 3)` is the four quarter
starts, and `tickStep` and `timeTickInterval` say what spacing to use for
about `count` ticks. This is a group: the four functions are the tick half of
an axis, used with `charts.scale` and `charts.format`.

## Number ticks

`tickStep` is d3-array's rule: divide the extent by `count`, take the power of
ten at or below it, and round the leftover factor to 1, 2, 5 or 10 on a log
scale (the cut-offs are √2, √10 and √50). `niceTicks` returns every multiple
of that step between the two ends inclusive, in the direction from `start` to
`stop`; the ends themselves are ticks only if they are multiples. Equal ends
give `[start]` (and a step of 0). If one tick is asked for and no multiple
falls inside, it tries again for two, as d3 does.

The arithmetic is chosen so that the three languages print the same ticks:

- The power of ten is found by multiplying whole numbers, never with `log10`,
  whose last bit is platform-dependent.
- A fractional step is kept as a whole-number divisor: a step of 0.2 is
  "divide by 5", so the tick is `i / 5`, one correctly rounded division.
  Adding 0.1 three times gives 0.30000000000000004; here the third tenth is
  exactly the double 0.3. `tickStep` returns `factor / 10^k` the same way.
- Tick indexes are rounded with `math.round-float` and then corrected by
  comparison, so a product that lands a hair either side of a whole number
  cannot add or lose a tick.

## Calendar ticks

`timeTicks` returns the calendar boundaries inside a date range, inclusive:

| interval  | a tick on                              | `step` counts                          |
|-----------|----------------------------------------|----------------------------------------|
| `day`     | days with (day of month - 1) % step = 0 | restarts each month, as d3's timeDay.every |
| `week`    | Mondays (ISO weeks)                    | whole weeks since Monday 1970-01-05     |
| `month`   | the 1st, (month - 1) % step = 0        | month 3 is January, April, July, October |
| `quarter` | 1 January, April, July, October        | quarter of the year % step = 0          |
| `year`    | 1 January, year % step = 0             | every fifth year is 2005, 2010, ...     |

Steps are anchored to the calendar, not to the first date, so an axis that
pans by a day keeps the same ticks instead of shifting them all. Dates are
counted as whole days (via `dates.add-days`), never through a `Date` object,
so time zones and clock changes cannot move a tick. More than 10,000 ticks is
an error, since it is always a wrong interval.

`timeTickInterval` picks the interval for about `count` ticks the way d3's
time scale does: the ideal spacing is the span in days over `count`, and it
takes whichever neighbouring rung of the ladder day, 2 days, week, month (30
days), quarter (90 days), year (365 days) is nearer by ratio. Beyond a year it
uses whole years in a `tickStep` of 1, 2 or 5 x 10^k.

Sources: Mike Bostock, d3-array `ticks.js` (tickIncrement, ticks) and
d3-scale `time.js` (tickIntervals), github.com/d3; ISO 8601 for weeks starting
on Monday.