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

Introduction

Nonius draws dense time series, from a server to the pixel, exact to the nanosecond at any zoom. Each package’s page tells you how to use that package; these guides tell you how they fit together for a task — from the data you have to a chart in a page, a desktop window or an image.

The product page says what Nonius is for and lists its packages; each package’s own page, on crates.io, npm or NuGet, is its reference.

Português

Choose a path

Every Nonius chart draws the same thing — a series of points in time order — but the points can reach it four ways. Which one fits depends on two questions: where are the points now, and do they stop? (A word new to you — a tile, a summary — is in the glossary.)

Your pointsPathWhat the chart holds
Already in the page, made or fetched by your own codePush themAll of them
A finished history, small enough to send wholeA fileAll of them, fetched once
A history too long to send, or one many people readTiles from a serverOnly what the view on screen needs
Arriving now, as they happenA live feedThe newest, as they come

And when there is no browser at all — a report sent by e-mail, a desktop program, a server answering with a picture — the chart is drawn as an image.

“Small enough to send whole” is a matter of arithmetic. A point is 24 bytes (a time, a value and a weight, 8 bytes each), so ten million points are a 240 MB file, downloaded before the first frame and held in the browser’s memory. A day of readings once a second is 86,400 points, 2 MB: a file is the simple choice. Years of readings once a millisecond are hundreds of gigabytes: only tiles reach them.

Push them

Your page already has the points — computed, read from your own API, decoded from your own format. Hand them to the chart in time order, as many at a time as you like. The browser package’s example:

const chart = await createChart(canvas);
chart.setScale(2);
chart.push(t, v, w);

t, v and w are columns of integers; Your data says how a reading becomes them, and why setScale(2).

A file

A series file holds a finished history in Nonius’s own format, NSB1. The chart fetches it whole and reads its bytes where they land, with no decoding step, so a file of millions of points is drawn as soon as it has arrived.

const chart = await createChart(canvas);
await loadNsb(chart, await fetch(url));

The file is written once, from wherever the data is: a CSV, a Rust or .NET program, Node. Your data shows each.

Tiles from a server

When the history is too long to send, a server keeps it and sends tiles: small pieces cut for the view on screen, each about its pixels’ worth of bytes whatever the length of the history. Zoomed out on ten years, the chart reads summaries; zoomed into one second, the points of that second. The chart fetches tiles as a person pans and zooms.

import { attachTiles, createChart, fetchSeries, interact } from "nonius";

// A series on a Nonius server, drawn from its tiles: the chart holds only
// what its views need and fetches the rest as a person moves, however long
// the history. `url` is the series' address, `…/v1/series/{id}`.
export async function fromServer(canvas, url) {
  // Asked for first, so the answer comes while the WebAssembly downloads.
  const meta = fetchSeries(url);
  const chart = await createChart(canvas);
  // Opens the chart on the whole series.
  const tiles = await attachTiles(chart, url, { meta });
  const interaction = interact(chart, canvas, { tiles });
  return { chart, tiles, interaction };
}

That is a whole module. fetchSeries is not awaited on purpose: the chart takes the promise, and the answer arrives while the WebAssembly downloads. interact is given the tiles, so that each view a person moves to fetches the tiles it needs. url is the series’ address on the server, http://host:port/v1/series/{id}. The server can be nonius-server, from a series file, or your own, speaking the Nonius protocol. Serving shows how to run one.

A live feed

Points that are happening now come over a WebSocket, as numbered frames sent by the Rust crate nonius-live. The chart follows the newest points; a frame the network loses is noticed by its number and asked for again.

const chart = await createChart(canvas);
// A feed's values are integers and carry no scale: say how many decimal
// places they have (2 here: 10_050 is 100.50).
chart.setScale(2);
const stream = streamInto(chart, url);

An image

The same chart, drawn as a PNG on the CPU: no browser, no GPU. A server answers GET /v1/series/{id}/png?from=…&to=…&width=…&height=…; in .NET, a series draws itself:

// Light, for a box of 1200 × 400 CSS pixels at a pixel ratio of 2:
// the size is in device pixels, the ratio in thousandths.
byte[] image = series.StillPng(new Still(first, last + 1, 2400, 800, Theme.Light, RatioPermille: 2000));
File.WriteAllBytes(png, image);

and in Rust, nonius-render does. The image is the frame a browser draws, pixel for pixel on the 2D canvas, so a report and a page agree. Drawing shows each.

Not sure?

Start with a file. It is one fetch and one call, and the file it needs is what a server serves later: moving to tiles changes how the chart is fed, not the data.

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.

Português

The pyramid

A year of readings once a second is 31.5 million points. A screen is about 2,000 pixels wide. Reading all 31.5 million points to draw 2,000 columns, sixty times a second as a person drags, is too slow — and sending them over a network, far too slow. The pyramid is how Nonius draws that year from a few thousand numbers, and still draws exactly the line all the points would.

A summary: the bucket

A bucket summarises the points in a stretch of time in eight numbers:

FieldWhat it keeps
first_t, first_vthe first point’s time and value
last_t, last_vthe last point’s time and value
min, maxthe least and the greatest value
counthow many points
weightthe sum of their weights

Two buckets side by side merge into one: the first’s first, the second’s last, the least of the two minimums, the greatest of the two maximums, the counts and weights added. So a bucket of a minute can be made from four of fifteen seconds without going back to the points.

Levels

The pyramid is levels of buckets. Level 0’s buckets are the base wide — say one second. Each level above is four times as wide: 4 s, 16 s, 64 s, and so on. Buckets are aligned to 1970, so every client and server cuts them in the same places. A stretch with no points has no bucket.

LevelBucket width, base 1 s
01 s
14 s
216 s
44 min 16 s
818 h 12 min

What a view reads

A view is a time range on a number of pixel columns. Each column spans a stretch of time; the chart picks the coarsest level whose buckets fit at least 16 to a column, and merges them into one summary per column. When even level 0 is too coarse — fewer than 16 base widths a column, deep in a zoom — it reads the raw points instead.

So a frame reads somewhere between 16 and 64 buckets a column, a few thousand numbers for the whole screen, whatever the length of the history. And the line is exact: a column drawn from its least to its greatest value, joined to the next from its last value to the next’s first, is the same, pixel for pixel, as the line drawn from every point. The pyramid changes how much is read, never what is drawn.

Choosing the base and the levels

Two numbers, set where the pyramid is built:

  • The base: about the spacing of your points where they are densest. One reading a second, a base of one second. Points in bursts — a thousand a second for ten seconds an hour — take the spacing inside a burst, a millisecond. A base much finer than your points costs memory (64 bytes a bucket); much coarser, and a deep zoom reads more raw points a column than it needs.

  • The levels: enough for your widest view. A view reads at most 64 buckets a column while the top level is fine enough; past it, the top level’s buckets pile up. Take enough levels that

    base × 4^(levels − 1) ≥ widest span ÷ (64 × plot width)

    with the plot’s width in device pixels (CSS pixels × the pixel ratio). Take at least 2, even when the rule gives 1: a server keeps the top level always, so a single level 0 of one point a bucket is kept in full (What it costs). Three days of readings every 10 seconds, base 10 s, cost 88 bytes a point as a pyramid file with 1 level and 40 with 2. More levels than needed cost little — 44 bytes with 3, 46 with 9 — so when in doubt, take more.

Worked through: points a second apart, a year shown on 2,000 pixels. The widest span is 31,536,000 s; divided by 64 × 2,000 it is about 246 s. With a base of 1 s, the top level must be at least 246 s wide: 4⁴ = 256 s is level 4, so 5 levels.

Neither number is a promise you cannot change: a series file holds the points, not a pyramid, which is built from them when the chart or the server loads the file. (A server’s pyramid file keeps one built, and is built again to change it.)

Where they are set

WhereHowDefault
BrowsercreateChart(canvas, { baseNs, levels })1 s, 9 levels
.NETnew Series(exponent, baseNs, levels)1 s, 9 levels
nonius-server--base-ns N --levels L1 ms, 12 levels
RustPyramid::new(base, levels)—

The defaults cover years of points a second apart. nonius-server’s millisecond base suits fast data; for a sensor that reads every few seconds, pass --base-ns. A chart fed by tiles takes the server’s pyramid, from its metadata: its own settings are for points you push.

In Rust, building a pyramid is two calls:

#![allow(unused)]
fn main() {
// Summaries from one millisecond up, eight levels.
let mut pyramid = Pyramid::new(Width::new(1_000_000)?, 8)?;
pyramid.extend_sorted(series.iter())?;
}

What it costs

A bucket is 64 bytes. Level 0 of a one-second base over a year of one-second points has 31.5 million of them — as many as the points, at almost three times their size. That level earns nothing: a bucket of one point is the point. nonius-server therefore releases a level whose buckets hold fewer than 4 points on average (--sparse 4, the default) — any level but the top one, which it always keeps — and answers a view at that depth from the points themselves. Each level above has a quarter of the buckets of the one below, so the rest of the pyramid together costs about a third of its first kept level.

In Serving’s example that is: 24 bytes a point for the points themselves, 16 for level 1 (a 64-byte bucket for every 4 points), and about 5 for the levels above it — 45 bytes a point.

Serving shows a server building a pyramid and reporting what it kept.

Português

Serving

A server holds a series and answers small requests about it: its description, tiles for a chart in a browser, and images. The protocol is plain HTTP GET, so any client, cache and proxy understands it. nonius-server is its reference server, a program you install with Cargo:

cargo install nonius-server

This chapter serves the readings of Your data: one point a millisecond for 100 seconds, in a CSV.

From a CSV to a running server

nonius-server csv readings.csv readings.nsb
nonius-server build readings.nsb readings.pyr.nsb --base-ns 1000000 --levels 4
nonius-server verify readings.pyr.nsb

The first line writes the series file. The second builds a pyramid file: the series and its pyramid, stored as the protocol’s tiles are, so a tile is read straight from it. The third checks its hashes and builds the pyramid again to compare. Each says what it did:

readings.nsb: 100000 points from readings.csv; 2 decimal places, the most any value has; in time order
readings.pyr.nsb: 100000 points, 3 levels kept from level 1, 32810 buckets; 4.5 MB, 45.0 B a point

The points are a millisecond apart, so the base is a millisecond. Level 0 would hold one point a bucket, and was released (see What it costs): three levels are kept.

A server can serve the plain series file too, building the pyramid in memory as it starts. A pyramid file is built once and then mapped: the server starts at once, whatever its size, and only the parts a request reads are brought into memory.

nonius-server readings.pyr.nsb --id readings --addr 127.0.0.1:8080 &

The & leaves it running in the background, for the commands below. A series’ id is its file’s name unless --id names it; several files serve several series.

Asking it

The series’ description, its metadata:

curl -s http://127.0.0.1:8080/v1/series/readings
{"protocol":1,"series":"readings","base_ns":"1000000","levels":4,"ratio":4,"tile_columns":2048,"raw_tile_ns":"4096000000","max_k":4096,"v_exponent":2,"rows":"100000","first_t":"1760000000000000000","last_t":"1760000099999000004","last":{"t":"1760000099999000004","v":"10099","w":"1"}}

The 64-bit integers are strings, because JSON numbers lose precision past 2⁵³. A browser’s attachTiles reads this first, then asks for tiles: /v1/series/{id}/columns/{level}/{k}/{i} for summaries and /v1/series/{id}/raw/{i} for points. Their addresses and contents are the protocol’s sections 4 and 5; a chart computes them itself, and you never write one by hand.

An image of the whole 100 seconds, light, with a title and a unit:

image='http://127.0.0.1:8080/v1/series/readings/png?from=1760000000000000000&to=1760000100000000000&width=1600&height=600&ratio=2&theme=light&title=Readings&unit=%C2%B0C'
curl -s -o readings.png "$image"

from and to are the time shown, [from, to): to is not included, so the whole series ends at its last point’s time plus one, or later. width and height are the PNG’s own pixels; ratio=2 draws labels and lines for a screen of two device pixels a CSS pixel, so this image fills a box of 800 × 300 on such a screen. The unit °C is percent-encoded, as a browser’s URLSearchParams writes it. The theme is dark unless theme=light. Every parameter is on nonius-server’s page; one the server does not know is ignored, so a misspelling (timezone= for tz=) quietly falls back to the default.

Caching

A tile or an image never changes: the same address always answers the same bytes. The server says so — Cache-Control: public, max-age=31536000, immutable — and gives each answer an ETag, a hash of its bytes. A client that already has it asks with the tag and gets 304 Not Modified, no body:

etag=$(curl -s -o /dev/null -w '%header{etag}' "$image")
curl -s -o /dev/null -w '%{http_code}\n' -H "If-None-Match: $etag" "$image"

That prints 304. So a browser, a proxy or a CDN can keep tiles and images for as long as it likes. The metadata is the exception: it says no-cache, because a series that grows changes its last point.

A page on another origin

A page served from one address reads a server at another only if the server allows it. nonius-server allows any page: every answer carries Access-Control-Allow-Origin: *, exposes its ETag and its image headers to scripts (Access-Control-Expose-Headers), and Cross-Origin-Resource-Policy: cross-origin, so a page that isolates itself for the live feed’s shared memory can still read it.

An image also says where its plot is: Nonius-Plot: x y width height, the plot’s rectangle in the image’s pixels, and Nonius-Range: lo hi, the values on its bottom and top rows. A page showing the PNG turns a click into a time and a value with them, and asks the server’s /pick for the point under it.

In production

nonius-server is the protocol’s reference and a test fixture, not a production server: no TLS, no limit on connections, one thread a connection. In front of users, put it behind a reverse proxy that terminates TLS and limits connections; since every answer but the metadata is immutable, the proxy can cache them all.

For a server of your own, two ways:

  • The protocol inside your Rust program, from the crate’s library:
#![allow(unused)]
fn main() {
let served = Served::new("day", (t, v, w), 2, Options::default())?;
}

served.answer(&request) answers a request with no socket, and serve puts it on a TcpListener; the crate’s page shows both.

  • The protocol in any language. It is written to be implemented without Nonius’s code: the protocol gives every address, byte and rule, and the images are drawn by the .NET package as by Rust.

Português

Drawing

One Rust core turns a view into what a frame draws, everywhere Nonius runs. In a browser it is compiled to WebAssembly and draws with WebGPU, with WebGL2 where WebGPU is missing, and on the CPU into a 2D canvas when the browser gives neither. In .NET and C it is a native library. As an image, it draws on the CPU into a PNG. Each draws the same frame: the same axes, labels, crosshair and legend, drawn by the chart itself and never laid over it as HTML.

So the choice below is only where your program lives.

In a browser

The npm package nonius draws into a <canvas>. Two rules hold in every page: the box sizes the canvas, never the canvas’s own pixels; and the page is served over HTTP, since a module and its WebAssembly do not load from file://.

<!-- Names the package for the browser, as a bundler would. -->
<script type="importmap">{ "imports": { "nonius": "./node_modules/nonius/index.js" } }</script>
<style>
  /* The box gives the canvas its size; the chart draws at that size in
     device pixels and follows it. */
  .plot { position: relative; height: 400px; }
  .plot canvas { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }
</style>
<div class="plot"><canvas></canvas></div>
<script type="module">
  import { fromPoints } from "./points.js";
  await fromPoints(document.querySelector("canvas"));
</script>

That is a whole page. The import map tells the browser where "nonius" is; with a bundler (Vite, webpack), the bundler does that instead. The module it loads makes the chart, gives it points by any path — the tiles’ module there is a whole one — and lets a person explore it with interact: drag to pan, the wheel or two fingers to zoom, hover for the crosshair. A chart fed by tiles hands interact its tiles, interact(chart, canvas, { tiles }), so that moving fetches what the new view needs.

The chart’s options go to createChart, and each has a method to change it later: createChart(canvas, { theme: "light", timeZone: "America/Sao_Paulo", title: "Estufa 2", unit: "°C" }), then setTheme, setTimeZone(chart, name), setTitle and setUnit. Every option, typed and documented, is in the package’s index.d.ts.

React

A chart is made asynchronously (it waits for its WebAssembly) and owns memory outside JavaScript’s, so a component makes it in an effect and frees it in the effect’s clean-up — also when the clean-up comes first, as React’s development mode does on purpose:

createChart(element).then((made) => {
  if (disposed) {
    made.free();
    return;
  }
  chart = made;
  chart.setScale(decimals);
  chart.push(points.t, points.v, points.w);
return () => {
  disposed = true;
  interaction?.stop();
  chart?.free();
};

Vue

The same, in onMounted and onBeforeUnmount, with one Vue-specific rule: keep the chart in a plain variable, not a ref.

// Plain variables, not refs: Vue would wrap the chart in a proxy, and the
// chart's methods must be called on the chart itself.
let chart: Chart | undefined;
let interaction: ReturnType<typeof interact> | undefined;

Both components are whole in the package’s React and Vue examples, run with Vite and its framework’s own plug-in alone: the WebAssembly needs no plug-in of its own.

In .NET

The NuGet package Nonius holds the native core for Windows x64 and Linux x64. On it, two controls.

Avalonia, for a desktop window (Nonius.Avalonia):

var series = Series.FromNsb(File.ReadAllBytes(nsb));
var chart = new NoniusChart { Series = series };
chart.PointPicked += (_, point) => Console.WriteLine(point);
window.Content = chart;

Blazor WebAssembly, for a page (Nonius.Blazor), which runs the browser package underneath:

<NoniusChart @ref="chart" Nsb="day-4096.nsb" Start="First" End="Last" Theme="theme" Gap="gap"
             TimeZone="zone" Title="@title" Unit="@unit"
             ViewChanged="Changed" style="display: block; width: 800px; height: 400px" />

Times and values cross between .NET and the page as decimal strings, never as JavaScript numbers, which would round a nanosecond time of today to 256 ns.

From C

nonius-capi is the core behind a C interface: a header, nonius.h, and a library to link. The .NET packages are built on it, and any other language that can call C — Python’s ctypes, Go’s cgo, Java’s FFM — can reach Nonius the same way. A series file drawn as a PNG:

NoniusSeries *series = NULL;
/* Summaries from one second up, nine levels. */
int32_t status = nonius_series_from_nsb(bytes, len, 1000000000, 9, &series);
if (status != NONIUS_OK) {
    fprintf(stderr, "%s\n", nonius_status_message(status));
    return 1;
}

Every call returns a status; nonius_status_message says it in words. The whole program, and how to compile it with MSVC or gcc, are on nonius-capi’s page.

As an image

A still is one frame drawn on the CPU, with no window and no GPU, encoded as a PNG: for a report, an e-mail, a server’s answer. Four ways to the same image:

FromHow
A serverGET /v1/series/{id}/png?… (Serving)
.NETseries.StillPng(new Still(…)) (Choose a path)
Cnonius_still_png(series, &still, &png)
Rustnonius_render::still_png

In Rust, a still says everything a frame needs:

#![allow(unused)]
fn main() {
let png = still_png(&series, &Still {
    start: 0,
    end: 1_000_000_000,
    width: 640,
    height: 320,
    theme: Theme::dark(),
    ratio: 1.0,
    scale: Scale::new(2)?,
    gap: None,
    zone: Zone::UTC,
    title: None,
    unit: None,
})?;
}

width and height are the image’s own pixels; ratio is the pixel ratio of the screen it is meant for, which scales the labels and lines, not the image.

The same chart everywhere

Each option has the same meaning in every package, and the same default: dark, UTC, every point joined, no header.

Browser.NETServer image
Themetheme: "light"Theme.Lighttheme=light
Time zonetimeZone: "Europe/Lisbon"a TimeZoneInfotz=Europe/Lisbon
GapsetSeriesGap(0, ns), ns a BigIntGapgap=
Title, unitcreateChart’s title, unitTitle, Unittitle=, unit=

Português

Glossary

The words these guides use, each said for someone meeting it the first time. A word in italics has its own entry.

Backend

The part of the browser a chart draws with: WebGPU where the browser has it, WebGL2 where it does not, and a 2D canvas drawn on the CPU when it has neither. The chart picks the best one by itself; every backend draws the same frame.

Base

The width of time of the pyramid’s finest buckets, its level 0 — one second by default. Choose about the spacing of your points: see the pyramid.

Bucket

A summary of the points in a stretch of time in eight numbers: the first and last point’s time and value, the least and greatest value, how many points, and the sum of their weights. Two neighbouring buckets merge into one without going back to the points.

C ABI

The Application Binary Interface: how a compiled program calls a library — which functions exist, and how their arguments sit in memory. nonius-capi gives Nonius a C one, so any language that can call C can use it. Its version number changes when that layout does.

Column

A pixel column of the chart. A view spreads its time range over its columns, and the chart reads one summary per column.

CORS

Cross-Origin Resource Sharing: the rule by which a browser lets a page read a server at another address. The server says who may, in headers; nonius-server allows any page.

Crate

A Rust package, published on crates.io. Nonius is seven of them, from nonius-format (the bytes) to nonius-server (the program).

Device pixel and pixel ratio

A screen’s own pixels. A page is laid out in CSS pixels; on a sharp screen one CSS pixel is two or three device pixels, its pixel ratio. The chart draws in device pixels, so its lines stay sharp.

ETag

A short tag a server gives an answer — nonius-server uses a hash of its bytes. A client that has the answer asks again with the tag, and gets 304 Not Modified with no body if nothing changed.

Exponent

See scale.

Frame

Two meanings. On screen, one picture the chart draws — sixty a second as a person drags. On a live feed, one numbered message carrying new points.

Gap

The longest time between two points that the line still joins. Past it, the line breaks. By default every point is joined; you set the gap from what you know of your data.

Level

One layer of the pyramid. Level 0’s buckets are the base wide; each level above is four times as wide.

Live feed

Points sent as they happen, over a WebSocket, in numbered frames. A frame the network loses is noticed by its missing number and asked for again.

Metadata

A series’ description, as a server answers it in JSON: its time range, its scale, its pyramid, its last point. A chart reads it before any tile.

NSB1

Nonius’s series file format: the points’ three columns (time, value, weight), the scale, and a checksum, in a container whose first bytes are NONIUSB1. It is read where it lands in memory, with no decoding step. A file usually ends in .nsb.

Pick

Finding the point under the pointer: the chart does it for the crosshair, and a server answers /pick for a page that shows its image.

Point

The one thing every package takes: t, v and w, three integers — a time in nanoseconds since 1970 (UTC), a value, a weight.

Protocol

The rules for asking a server about a series over HTTP: which addresses exist and which bytes each answers. Written to be implemented without Nonius’s code; nonius-server is its reference.

Pyramid

A series summarised at several widths of time, each level four times as wide as the one below, so a view of any length reads a few thousand buckets instead of every point — and draws exactly the same line.

Pyramid file

A series and its built pyramid in one file, stored as the protocol’s own columns. A server maps it and starts at once, whatever its size.

Raw points

The points themselves, not summaries. A view deep enough in a zoom reads them.

Scale

How many decimal places a series’ values have. At a scale of 2, the integer 2137 means 21.37. Also called the exponent.

Series

The points of one quantity over time, in time order: one sensor, one instrument, one metric.

Still

One frame drawn on the CPU as a PNG image, with no window and no GPU.

Tile

A small piece of a series cut for a view, so a chart fetches only what its screen shows. A column tile holds summaries of one level; a raw tile holds the raw points of a span of time.

Time zone

A place’s rule for its local time, named as in the IANA database, such as Europe/Lisbon. Nonius keeps every time in UTC; a zone changes only the labels.

WebAssembly

A compact program format browsers run at near native speed. Nonius’s Rust core is compiled to it, so the browser runs the same code as the server. Often written wasm.

WebGL2 and WebGPU

The browser’s two ways of drawing on the graphics card. WebGPU is the newer; WebGL2 is everywhere. See backend.

WebSocket

A connection a page keeps open to a server, so the server can send data the moment it has it. A live feed runs over one.

Weight

The w of a point: how much it counts — 1 for a plain reading, the volume of a trade. Summaries add the weights up.

2D canvas

The browser’s plain drawing surface, drawn by the CPU. The chart’s last backend, used when the browser has no WebGPU or WebGL2.