Seus dados
Todo pacote recebe a mesma coisa: pontos { t, v, w }, em ordem de
tempo. Uma leitura de sensor, uma negociação e uma amostra de CPU
entram assim, e o Nonius nunca sabe qual é qual. Este capítulo mostra
como suas leituras viram pontos, e como pontos viram um arquivo de
série.
Um ponto
| Campo | O que é | Tipo |
|---|---|---|
t | o tempo: nanossegundos desde 1970-01-01 00:00 UTC | um inteiro de 64 bits com sinal |
v | o valor: um inteiro com um número fixo de casas decimais | um inteiro de 64 bits com sinal |
w | o peso: 1 para uma leitura simples | um inteiro de 64 bits sem sinal |
Os três são inteiros, e esse é o sentido do formato: nada passa por um número de ponto flutuante a caminho da tela, então nada é arredondado.
Tempo
Um tempo é uma contagem de nanossegundos desde a época Unix, em UTC — nunca um horário local. Uma contagem de 64 bits alcança uns 292 anos para cada lado de 1970 e guarda cada nanossegundo: uma vista de poucos nanossegundos em 2026 ainda desenha cada ponto onde ele está. Esse é o “exato em qualquer zoom” do Nonius.
Converter é uma multiplicação:
| De | Para nanossegundos |
|---|---|
| Segundos Unix | × 1.000.000.000 |
Date.parse do JavaScript (milissegundos) | × 1.000.000, como BigInt |
DateTime do .NET (UTC) | (time - DateTime.UnixEpoch).Ticks * 100 |
| Um texto ISO 8601, num CSV | o nonius-server csv lê, com Z ou um deslocamento como -03:00, e até nove casas decimais de segundo |
Os pontos vêm em ordem de tempo. Dois pontos podem ter o mesmo tempo; um ponto anterior ao que veio antes é recusado, com a linha onde aconteceu.
Valores
Um valor é um inteiro, e a série diz quantas casas decimais ele tem: a
sua escala (também chamada de expoente). Com duas casas, 21,37 °C é
2137; com três, uma tensão de 4,217 é 4217. A escala é um número só
para a série inteira, escolhido uma vez: o máximo de casas decimais que
seus valores têm.
Por que não um número de ponto flutuante? Porque nele 0.1 + 0.2 não é
0.3, e um gráfico que resume milhões de pontos levaria esses erros até
os rótulos. Um inteiro é exato, e a escala só põe a vírgula de volta
quando um rótulo é desenhado.
Um inteiro de 64 bits guarda até uns 9,2 × 10¹⁸: com duas casas decimais, valores até uns 92 quatrilhões. Escolher mais casas do que seus dados têm só custa alcance.
O peso
w é quanto um ponto conta. Para uma leitura, é 1. Para uma
negociação, pode ser o volume. Os resumos do gráfico somam os pesos,
então o resumo de um minuto sabe quanto aconteceu nele, não só a máxima
e a mínima. Quando fica de fora (no encodeNsb do JavaScript, num CSV
sem coluna de peso), todo peso é 1.
Lacunas
A linha liga cada ponto ao seguinte, por mais distantes que estejam. Um
sensor que manda dados em rajadas, ou que ficou desligado por um dia, é
desenhado como uma linha contínua, a menos que você diga onde ela
quebra: chart.setSeriesGap(0, 60_000_000_000n) no navegador (o id da
série, 0 para a primeira, e depois nanossegundos como BigInt), gap=
na imagem de um servidor, Gap no .NET. A linha quebra sempre que um
ponto vem mais que esses nanossegundos depois do anterior.
O gráfico não adivinha a lacuna: você conhece o ritmo dos seus dados, e um palpite quebraria a linha em lugares diferentes em zooms diferentes. Algumas vezes o espaçamento normal é um bom começo: para uma leitura a cada 10 segundos, um minuto mantém ligadas uma ou duas leituras perdidas e ainda quebra numa interrupção.
Fusos horários
Os dados estão sempre em UTC. Um fuso muda só os rótulos: diga qual
(timeZone: "Europe/Lisbon" no navegador, tz= na imagem de um
servidor, um TimeZoneInfo no .NET) e o eixo, a mira e o tooltip leem
o relógio local, dos dois lados de cada entrada e saída do horário de
verão.
Escrever um arquivo de série
Um arquivo de série — formato NSB1 — guarda as três colunas, a escala
e uma soma de verificação. Todo pacote escreve os mesmos bytes para os
mesmos pontos, pelo mesmo código Rust, então um arquivo escrito no .NET
é lido pelo navegador e servido pelo nonius-server sem nenhuma etapa
no meio.
De um CSV, sem código
O nonius-server transforma um CSV num arquivo de série. Cada linha é
um tempo e um valor, com ou sem cabeçalho:
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
A primeira linha toma a primeira coluna como tempo e a segunda como
valor. A segunda dá nome às colunas da exportação de uma planilha, com
; entre os campos e vírgula decimal. Nada é arredondado: o valor é
guardado com o máximo de casas decimais que algum valor do arquivo tem.
As opções (--time-unit, --decimals e as outras) estão na
página do nonius-server.
Do 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)?)?;
}
O write_series está no nonius-core, com a feature std. O arquivo
é aberto para leitura e escrita: o escritor relê o que escreveu para
calcular o hash.
Do .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) dá os mesmos bytes como byte[]. Os dois estão
no pacote NuGet Nonius.
Do JavaScript
No Node (20.16 ou mais novo) ou no navegador, o encodeNsb do pacote
npm 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));
O valor é lido do texto de propósito: Math.round(21.4 * 100) acerta
aqui, mas um float não guarda todo decimal, e o texto sempre guarda.
Próximo
Com um arquivo de série, escolha o caminho até o gráfico. Se ele for servido em tiles, a pirâmide é como o servidor o resume.