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.
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 points | Path | What the chart holds |
|---|---|---|
| Already in the page, made or fetched by your own code | Push them | All of them |
| A finished history, small enough to send whole | A file | All of them, fetched once |
| A history too long to send, or one many people read | Tiles from a server | Only what the view on screen needs |
| Arriving now, as they happen | A live feed | The 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.
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
| Field | What it is | Type |
|---|---|---|
t | the time: nanoseconds since 1970-01-01 00:00 UTC | a signed 64-bit integer |
v | the value: an integer with a fixed number of decimal places | a signed 64-bit integer |
w | the weight: 1 for a plain reading | an 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:
| From | To 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 CSV | nonius-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.
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:
| Field | What it keeps |
|---|---|
first_t, first_v | the first point’s time and value |
last_t, last_v | the last point’s time and value |
min, max | the least and the greatest value |
count | how many points |
weight | the 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.
| Level | Bucket width, base 1 s |
|---|---|
| 0 | 1 s |
| 1 | 4 s |
| 2 | 16 s |
| 4 | 4 min 16 s |
| 8 | 18 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
| Where | How | Default |
|---|---|---|
| Browser | createChart(canvas, { baseNs, levels }) | 1 s, 9 levels |
| .NET | new Series(exponent, baseNs, levels) | 1 s, 9 levels |
nonius-server | --base-ns N --levels L | 1 ms, 12 levels |
| Rust | Pyramid::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.
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.
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:
| From | How |
|---|---|
| A server | GET /v1/series/{id}/png?… (Serving) |
| .NET | series.StillPng(new Still(…)) (Choose a path) |
| C | nonius_still_png(series, &still, &png) |
| Rust | nonius_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 | .NET | Server image | |
|---|---|---|---|
| Theme | theme: "light" | Theme.Light | theme=light |
| Time zone | timeZone: "Europe/Lisbon" | a TimeZoneInfo | tz=Europe/Lisbon |
| Gap | setSeriesGap(0, ns), ns a BigInt | Gap | gap= |
| Title, unit | createChart’s title, unit | Title, Unit | title=, unit= |
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.