Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Português

Your data

Every package takes the same thing: points { t, v, w }, in time order. A sensor reading, a trade and a CPU sample all enter this way, and Nonius never knows which. This chapter is how your readings become points, and how points become a series file.

A point

FieldWhat it isType
tthe time: nanoseconds since 1970-01-01 00:00 UTCa signed 64-bit integer
vthe value: an integer with a fixed number of decimal placesa signed 64-bit integer
wthe weight: 1 for a plain readingan unsigned 64-bit integer

All three are integers, and that is the point of the format: nothing passes through a floating-point number on the way to the screen, so nothing is rounded.

Time

A time is a count of nanoseconds since the Unix epoch, in UTC — never a local time. A 64-bit count reaches about 292 years either side of 1970, and keeps every nanosecond: a view of a few nanoseconds in 2026 still draws each point where it is. That is the “exact at any zoom” of Nonius.

Converting is a multiplication:

FromTo nanoseconds
Unix seconds× 1,000,000,000
JavaScript Date.parse (milliseconds)× 1,000,000, as a BigInt
.NET DateTime (UTC)(time - DateTime.UnixEpoch).Ticks * 100
An ISO 8601 text, in a CSVnonius-server csv reads it, with Z or an offset such as -03:00, up to nine decimal places of seconds

Points come in time order. Two points may share a time; a point earlier than the one before it is refused, with the row where it happened.

Values

A value is an integer, and the series says how many decimal places it has: its scale (also called its exponent). At two places, 21.37 °C is 2137; at three, a voltage of 4.217 is 4217. The scale is one number for the whole series, chosen once: the most decimal places your values have.

Why not a floating-point number? Because 0.1 + 0.2 is not 0.3 in one, and a chart that summarises millions of points would carry those errors into its labels. An integer is exact, and the scale puts the decimal point back only when a label is drawn.

A 64-bit integer holds about 9.2 × 10¹⁸: at two decimal places, values up to about 92 million billion. Choosing more places than your data has costs nothing but range.

The weight

w is how much a point counts. For a reading, it is 1. For a trade, it can be its volume. The chart’s summaries add the weights up, so a summary of a minute knows how much happened in it, not only its high and low. Left out (in JavaScript’s encodeNsb, in a CSV without a weight column), every weight is 1.

Gaps

The line joins each point to the next, however far apart they are. A sensor that reports in bursts, or one that was switched off for a day, is drawn as one continuous line unless you say where the line breaks: chart.setSeriesGap(0, 60_000_000_000n) in the browser (the series’ id, 0 for the first, then nanoseconds as a BigInt), gap= on a server’s image, Gap in .NET. The line breaks wherever a point comes more than that many nanoseconds after the one before.

The chart does not guess the gap: you know your data’s rhythm, and a guess would break the line in different places at different zooms. A few times the usual spacing is a good start: for a reading every 10 seconds, a minute keeps a lost reading or two joined and still breaks at an outage.

Time zones

The data is always in UTC. A time zone changes only the labels: name one (timeZone: "Europe/Lisbon" in the browser, tz= on a server’s image, a TimeZoneInfo in .NET) and the axis, the crosshair and the tooltip read the local wall clock, on both sides of each change to or from daylight saving time.

Writing a series file

A series file — format NSB1 — holds the three columns, the scale and a checksum. Every package writes the same bytes for the same points, through the same Rust code, so a file written in .NET is read by the browser and served by nonius-server without a step between.

From a CSV, with no code

nonius-server turns a CSV into a series file. A line is a time and a value, a header first or not:

time,price
1788220800000000000,101.25
1788220800000001500,99.5
nonius-server csv prices.csv prices.nsb
nonius-server csv sheet.csv day.nsb --delimiter ';' --time time --value price --weight volume

The first line takes the first column as the time and the second as the value. The second names the columns of a spreadsheet’s export, with ; between fields and a decimal comma. Nothing is rounded: a value is kept with the most decimal places any value in the file has. The options (--time-unit, --decimals and the rest) are on nonius-server’s page.

From Rust

#![allow(unused)]
fn main() {
// A reading of 21.37 °C at two decimal places is 2137; w is 1 for a
// plain reading.
let points = [Point { t: 1_735_689_600_000_000_000, v: 2137, w: 1 }];
let file = File::options().read(true).write(true).create(true).truncate(true).open("day.nsb")?;
write_series(file, &points, Scale::new(2)?)?;
}

write_series is in nonius-core, with its std feature. The file is opened readable as well as writable: the writer reads what it wrote back, to hash it.

From .NET

(DateTime Time, decimal Celsius)[] readings =
[
    (new DateTime(2026, 3, 1, 0, 0, 0, DateTimeKind.Utc), 21.37m),
    (new DateTime(2026, 3, 1, 0, 0, 1, 500, DateTimeKind.Utc), 21.4m),
    (new DateTime(2026, 3, 1, 0, 0, 3, DateTimeKind.Utc), -0.05m),
];
// A time is nanoseconds since 1970 (UTC), and a tick is 100 of
// them. A value is an integer of the decimal places kept, here 2:
// 21.4 is 2140. w is 1 for a plain reading.
var points = readings
    .Select(r => new Point((r.Time - DateTime.UnixEpoch).Ticks * 100, (long)decimal.Round(r.Celsius * 100m), 1))
    .ToArray();
using var file = File.Create(path);
Nsb.Write(file, points, 2);

Nsb.Encode(points, 2) gives the same bytes as a byte[]. Both are in the NuGet package Nonius.

From JavaScript

In Node (20.16 or later) or in the browser, encodeNsb from the npm package nonius:

const readings = [
  { time: "2026-03-01T00:00:00Z", celsius: "21.37" },
  { time: "2026-03-01T00:00:01.5Z", celsius: "21.4" },
  { time: "2026-03-01T00:00:03Z", celsius: "-0.05" },
];

// A time is nanoseconds since 1970 (UTC), a BigInt. `Date` counts
// milliseconds, so a finer time needs its own parsing.
const nanoseconds = (iso) => BigInt(Date.parse(iso)) * 1_000_000n;

// A value is an integer of the decimal places you keep, here 2, read
// from its text and never through a float: "21.4" is 2140.
function hundredths(text) {
  const [whole, fraction = ""] = text.split(".");
  if (fraction.length > 2) throw new Error(`${text} has more than 2 decimal places`);
  const sign = whole.startsWith("-") ? -1n : 1n;
  return sign * (BigInt(whole.replace("-", "")) * 100n + BigInt(fraction.padEnd(2, "0")));
}

const t = BigInt64Array.from(readings, (r) => nanoseconds(r.time));
const v = BigInt64Array.from(readings, (r) => hundredths(r.celsius));
writeFileSync("readings.nsb", await encodeNsb({ t, v }, 2));

The value is read from its text on purpose: Math.round(21.4 * 100) is right here, but a float cannot hold every decimal, and the text always can.

Next

With a series file, choose a path to the chart. If it will be served in tiles, the pyramid is how the server summarises it.