Skip to content

Log scales ​

scale: 'log' spaces values by ratio instead of by difference: each power of 10 takes the same length, so 1 to 10 is as tall as 1000 to 10000. Use it for data spanning several orders of magnitude, where a linear axis flattens everything but the largest values, and for growth, where equal rates of growth are equal slopes.

ts
// scale: 'log' gives each power of 10 the same height, so equal growth rates are
// equal slopes and the early years of a fast-growing series stay readable next
// to its millions.
import type { MochartInputConfig } from '@mochart/core';

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Monthly Active Users (fictional)' },
  categoryAxis: { property: 'year', type: 'number', scale: 'linear', tickLabel: { format: 'd' }, valueFormat: 'd' },
  valueAxes: [{ scale: 'log', title: { text: 'Users' } }],
  series: [
    { property: 'orbit', title: 'Orbit', renderer: 'line' },
    { property: 'lantern', title: 'Lantern', renderer: 'line' },
    { property: 'harbor', title: 'Harbor', renderer: 'line' }
  ]
};

export const data = [
  { year: 2014, orbit: 120, lantern: 5000, harbor: 40000 },
  { year: 2015, orbit: 380, lantern: 6500, harbor: 52000 },
  { year: 2016, orbit: 1200, lantern: 8400, harbor: 61000 },
  { year: 2017, orbit: 3900, lantern: 11000, harbor: 68000 },
  { year: 2018, orbit: 12000, lantern: 14300, harbor: 70000 },
  { year: 2019, orbit: 40000, lantern: 18600, harbor: 66000 },
  { year: 2020, orbit: 125000, lantern: 24100, harbor: 58000 },
  { year: 2021, orbit: 400000, lantern: 31400, harbor: 47000 },
  { year: 2022, orbit: 1200000, lantern: 40800, harbor: 36000 },
  { year: 2023, orbit: 2600000, lantern: 53000, harbor: 27000 },
  { year: 2024, orbit: 4100000, lantern: 68900, harbor: 19000 },
  { year: 2025, orbit: 5300000, lantern: 89600, harbor: 13000 },
  { year: 2026, orbit: 6000000, lantern: 116500, harbor: 9000 }
];

How it works ​

  • The ticks sit at the powers of 10. When they do not all fit, the axis keeps every second, third or n-th one, for the smallest n that fits; when there are too few of them, it adds the 2 and 5 multiples as ticks, or every multiple from 2 to 9, where they fit. Inside a single power of 10, such as 50 to 80, the ticks are the multiples that lie there (50, 60, 70, 80), or linear ones on an axis with room for more than twice that many.
  • Between the ticks, the minor ticks are the multiples from 2 to 9 that are not ticks or, when the axis keeps every n-th power of 10, the powers it skips, thinned to sit at least minTickSpacing apart. Their tick marks and grid lines show by default: on a log axis minorTickMark.visible and minorGridLine.visible default to "major", which uses the value of tickMark.visible and gridLine.visible, so hiding the grid lines hides the minor grid lines too. Their labels stay off. minorTickLabel.visibletrue labels them all when every label fits, and hides their marks and grid lines along with the labels when one does not, so it suits a tall axis with few powers of 10; minorTickLabel.format alone leaves the labels off on a log axis.
  • With the default "auto" format, each tick label takes an SI prefix of its own magnitude, so 1m, 1 and 1k share one axis, where a linear axis's tick labels take one prefix for the whole axis; tooltip and label values are formatted per value on both. Beyond the prefixes, below 1e-24 or from 1e27, a value takes exponent form, 1e+30. The prefix reaches down to milli, so a ratio such as a gain of 0.1 reads 100m; where that misleads, a tickLabel.format of "~g" labels the ticks and the tooltip values as plain decimals, as the log-log example below does. A format that leaves its precision open, such as "s" or "~g", takes 3 significant digits on a log axis, with the trailing zeros trimmed, since there is no tick step to take one from.
  • minMarginFraction and maxMarginFraction are taken from the logs of the domain, so a 5% margin is 5% of the axis length on either scale.
  • Bars and areas grow from the minimum end of the axis unless base is set. A base above 0 works as it does on a linear axis: with base: 1, ratios above 1 grow up and ratios below it grow down, with a doubling and a halving the same length.
  • Values and the domain animate in logs, so a value moving from 1 to 1000 is halfway up the axis at 31.6 rather than at 500.
  • Changing scale on an axis restarts the chart without a transition.

Values at or below 0 ​

A log axis has no position for 0 or a negative value. The chart handles each kind of value differently. A tooltip shows the value itself, and a series label shows it only where the point is drawn, at the base under missingValueMode: 'base'.

  • A series value at or below 0 is left out of the axis domain and drawn as missing, the way the series missingValueMode draws a missing value: a gap, a connection across it, or a point at the base. The chart logs a console warning naming the series, since nothing on the chart says why the point is gone.
  • An error bar end at or below 0, low or high, is drawn past the minimum end of the axis and cut off at the plot edge, so an error bar crossing 0 (5 plus or minus 8) runs off the bottom of the plot. So is the lower end of a range whose other end is above 0. The clip indicator marks the edge. A range with both ends at or below 0 is missing.
  • A threshold line at or below 0 is a validation error. A threshold range with one end at or below 0 fills from the minimum end of the axis, so a range from 0 to 10 covers everything below 10. A range with no end above 0 is a validation error too.

What a log axis rejects ​

Each of these is a validation error on a log axis, because it needs a position a log axis does not have or a fixed distance in values that takes a different length at each end of the axis:

  • min, max, softMin, softMax, base and explicit ticks at or below 0. min: 0 and base: 0 are the usual leftovers from a linear config.
  • A series stack on the axis. A stack starts at 0, and a segment's height would depend on the total below it rather than its own value.
  • minOffset, maxOffset and minTickInterval other than 0. Use the margin fractions to leave space at the ends.
  • A tickStep or thresholdStep interval. interval: 100 on an axis from 1 to 100000 would put almost every tick in the top power of 10.
  • A pie chart's axes, which do not accept "log".

Log category axes ​

A category axis whose type is number takes scale: 'log' too, for log-log plots such as a frequency response:

ts
// A number category axis can be log too: with both axes log, a low-pass
// filter's roll-off above its 1 kHz cutoff is a straight line, falling by 10x
// for every tenfold rise in frequency for the first-order filter and 100x for
// the second-order one.
import type { MochartInputConfig } from '@mochart/core';

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Low-Pass Filter Response' },
  categoryAxis: {
    property: 'hz',
    type: 'number',
    scale: 'log',
    title: { text: 'Frequency (Hz)' },
    valueFormat: ',.4~g',
    valueSuffix: ' Hz'
  },
  valueAxes: [{ scale: 'log', title: { text: 'Gain (output / input)' }, tickLabel: { format: '~g' } }],
  series: [
    { property: 'firstOrder', title: 'First order', renderer: 'line', marker: { shape: null } },
    { property: 'secondOrder', title: 'Second order', renderer: 'line', marker: { shape: null } }
  ]
};

export const data = [
  { hz: 20, firstOrder: 0.9998, secondOrder: 1 },
  { hz: 25, firstOrder: 0.9997, secondOrder: 1 },
  { hz: 31.5, firstOrder: 0.9995, secondOrder: 1 },
  { hz: 40, firstOrder: 0.9992, secondOrder: 1 },
  { hz: 50, firstOrder: 0.9988, secondOrder: 1 },
  { hz: 63, firstOrder: 0.998, secondOrder: 1 },
  { hz: 80, firstOrder: 0.9968, secondOrder: 1 },
  { hz: 100, firstOrder: 0.995, secondOrder: 1 },
  { hz: 125, firstOrder: 0.9923, secondOrder: 0.9999 },
  { hz: 160, firstOrder: 0.9874, secondOrder: 0.9997 },
  { hz: 200, firstOrder: 0.9806, secondOrder: 0.9992 },
  { hz: 250, firstOrder: 0.9701, secondOrder: 0.9981 },
  { hz: 315, firstOrder: 0.9538, secondOrder: 0.9951 },
  { hz: 400, firstOrder: 0.9285, secondOrder: 0.9874 },
  { hz: 500, firstOrder: 0.8944, secondOrder: 0.9701 },
  { hz: 630, firstOrder: 0.8461, secondOrder: 0.9295 },
  { hz: 800, firstOrder: 0.7809, secondOrder: 0.8423 },
  { hz: 1000, firstOrder: 0.7071, secondOrder: 0.7071 },
  { hz: 1250, firstOrder: 0.6247, secondOrder: 0.5391 },
  { hz: 1600, firstOrder: 0.53, secondOrder: 0.3639 },
  { hz: 2000, firstOrder: 0.4472, secondOrder: 0.2425 },
  { hz: 2500, firstOrder: 0.3714, secondOrder: 0.158 },
  { hz: 3150, firstOrder: 0.3026, secondOrder: 0.1003 },
  { hz: 4000, firstOrder: 0.2425, secondOrder: 0.06238 },
  { hz: 5000, firstOrder: 0.1961, secondOrder: 0.03997 },
  { hz: 6300, firstOrder: 0.1568, secondOrder: 0.02519 },
  { hz: 8000, firstOrder: 0.124, secondOrder: 0.01562 },
  { hz: 10000, firstOrder: 0.0995, secondOrder: 0.01 },
  { hz: 12500, firstOrder: 0.07975, secondOrder: 0.0064 },
  { hz: 16000, firstOrder: 0.06238, secondOrder: 0.003906 },
  { hz: 20000, firstOrder: 0.04994, secondOrder: 0.0025 }
];
  • Category values at or below 0 are a data error, like duplicate category values: the category values place everything else, so the chart is not drawn.
  • It takes line, area and marker-only series. Bar series are a validation error, because a bar's width is a fixed distance in values.
  • categoryValueInterval must stay "auto", which on a log axis is the smallest ratio between neighbouring category values. Values a fixed ratio apart (1, 10, 100, or frequencies an octave apart) keep half a slot at each end of the axis, as evenly spaced values do on a linear axis.
  • A date axis cannot be log, since a date has no natural zero. For elapsed time on a log axis, use a number axis of days (or seconds) since an event.

Log price axes ​

A candlestick or OHLC chart of a price that multiplies reads best on a log price axis. See the candlestick recipe for how to set it on the price axis alone when there is a volume pane.

Released under the MIT License.