Introdução
O Nonius desenha séries temporais densas, do servidor ao pixel, exato até o nanossegundo em qualquer zoom. A página de cada pacote diz como usar aquele pacote; estes guias dizem como eles se encaixam para uma tarefa — dos dados que você tem até um gráfico numa página, numa janela de desktop ou numa imagem.
A página do produto diz para que serve o Nonius e lista os seus pacotes; a página de cada pacote, no crates.io, no npm ou no NuGet, é a sua referência.
Escolha o caminho
Todo gráfico do Nonius desenha a mesma coisa — uma série de pontos em ordem de tempo —, mas os pontos podem chegar até ele de quatro jeitos. Qual serve depende de duas perguntas: onde estão os pontos agora, e eles param de chegar? (Uma palavra nova — um tile, um resumo — está no glossário.)
| Seus pontos | Caminho | O que o gráfico guarda |
|---|---|---|
| Já estão na página, gerados ou buscados pelo seu próprio código | Entregue-os | Todos |
| Um histórico fechado, pequeno o bastante para mandar inteiro | Um arquivo | Todos, buscados uma vez |
| Um histórico longo demais para mandar, ou que muita gente lê | Tiles de um servidor | Só o que a vista na tela precisa |
| Chegando agora, conforme acontecem | Um feed ao vivo | Os mais novos, conforme chegam |
E quando não há navegador nenhum — um relatório mandado por e-mail, um programa de desktop, um servidor que responde com uma figura —, o gráfico é desenhado como imagem.
“Pequeno o bastante para mandar inteiro” é questão de conta. Um ponto tem 24 bytes (um tempo, um valor e um peso, 8 bytes cada), então dez milhões de pontos são um arquivo de 240 MB, baixado antes do primeiro quadro e guardado na memória do navegador. Um dia de leituras uma por segundo são 86.400 pontos, 2 MB: o arquivo é a escolha simples. Anos de leituras uma por milissegundo são centenas de gigabytes: só tiles chegam lá.
Entregue-os
Sua página já tem os pontos — calculados, lidos da sua própria API, decodificados do seu próprio formato. Entregue-os ao gráfico em ordem de tempo, quantos quiser de cada vez. O exemplo do pacote do navegador:
const chart = await createChart(canvas);
chart.setScale(2);
chart.push(t, v, w);
t, v e w são colunas de inteiros; Seus dados diz
como uma leitura vira esses inteiros, e por que setScale(2).
Um arquivo
Um arquivo de série guarda um histórico fechado no formato do próprio
Nonius, o NSB1. O gráfico busca o arquivo inteiro e lê seus bytes onde
eles chegam, sem etapa de decodificação, então um arquivo de milhões de
pontos é desenhado assim que termina de chegar.
const chart = await createChart(canvas);
await loadNsb(chart, await fetch(url));
O arquivo é escrito uma vez, de onde os dados estiverem: um CSV, um programa em Rust ou .NET, o Node. Seus dados mostra cada um.
Tiles de um servidor
Quando o histórico é longo demais para mandar, um servidor o guarda e manda tiles: pedaços pequenos cortados para a vista na tela, cada um com mais ou menos os bytes dos seus pixels, seja qual for o tamanho do histórico. Com o zoom aberto em dez anos, o gráfico lê resumos; com o zoom num segundo, os pontos daquele segundo. O gráfico busca tiles conforme a pessoa arrasta e dá zoom.
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 };
}
Esse é um módulo inteiro. O fetchSeries fica sem await de
propósito: o gráfico recebe a promise, e a resposta chega enquanto o
WebAssembly baixa. O interact recebe os tiles, para que cada vista
para onde a pessoa se move busque os tiles de que precisa. url é o
endereço da série no servidor, http://host:porta/v1/series/{id}. O servidor pode ser o
nonius-server, a partir de um arquivo de série, ou o seu, falando o
protocolo do Nonius.
Servir mostra como rodar um.
Um feed ao vivo
Pontos que estão acontecendo agora chegam por um WebSocket, como
quadros numerados enviados pelo crate Rust
nonius-live. O gráfico
acompanha os pontos mais novos; um quadro que a rede perde é notado
pelo número e pedido de novo.
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);
Uma imagem
O mesmo gráfico, desenhado como PNG na CPU: sem navegador, sem GPU. Um
servidor responde a GET /v1/series/{id}/png?from=…&to=…&width=…&height=…;
no .NET, a série se desenha:
// 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);
e no Rust, o nonius-render faz isso. A imagem é o quadro que o
navegador desenha, pixel por pixel no canvas 2D, então um relatório e
uma página concordam. Desenhar mostra cada um.
Na dúvida?
Comece por um arquivo. É uma busca e uma chamada, e o arquivo que ele pede é o que um servidor serve depois: passar para tiles muda como o gráfico é alimentado, não os dados.
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.
A pirâmide
Um ano de leituras uma por segundo são 31,5 milhões de pontos. Uma tela tem uns 2.000 pixels de largura. Ler os 31,5 milhões de pontos para desenhar 2.000 colunas, sessenta vezes por segundo enquanto a pessoa arrasta, é lento demais — e mandá-los pela rede, mais ainda. A pirâmide é como o Nonius desenha esse ano a partir de alguns milhares de números, e ainda desenha exatamente a linha que todos os pontos desenhariam.
Um resumo: o bucket
Um bucket resume os pontos de um trecho de tempo em oito números:
| Campo | O que guarda |
|---|---|
first_t, first_v | o tempo e o valor do primeiro ponto |
last_t, last_v | o tempo e o valor do último ponto |
min, max | o menor e o maior valor |
count | quantos pontos |
weight | a soma dos pesos |
Dois buckets vizinhos se juntam num só: o primeiro do primeiro, o último do segundo, o menor dos dois mínimos, o maior dos dois máximos, as contagens e os pesos somados. Assim um bucket de um minuto sai de quatro de quinze segundos, sem voltar aos pontos.
Níveis
A pirâmide é feita de níveis de buckets. Os buckets do nível 0 têm a largura da base — digamos, um segundo. Cada nível acima é quatro vezes mais largo: 4 s, 16 s, 64 s, e assim por diante. Os buckets são alinhados a 1970, então todo cliente e todo servidor os cortam nos mesmos lugares. Um trecho sem pontos não tem bucket.
| Nível | Largura do bucket, base 1 s |
|---|---|
| 0 | 1 s |
| 1 | 4 s |
| 2 | 16 s |
| 4 | 4 min 16 s |
| 8 | 18 h 12 min |
O que uma vista lê
Uma vista é um intervalo de tempo sobre um número de colunas de pixels. Cada coluna cobre um trecho de tempo; o gráfico escolhe o nível mais grosso cujos buckets cabem pelo menos 16 numa coluna, e os junta num resumo por coluna. Quando nem o nível 0 serve — menos de 16 larguras de base por coluna, no fundo de um zoom —, ele lê os pontos crus.
Então um quadro lê entre 16 e 64 buckets por coluna, alguns milhares de números para a tela inteira, seja qual for o tamanho do histórico. E a linha é exata: uma coluna desenhada do menor ao maior valor, ligada à seguinte do seu último valor ao primeiro da próxima, é a mesma, pixel por pixel, que a linha desenhada com todos os pontos. A pirâmide muda quanto se lê, nunca o que se desenha.
Escolher a base e os níveis
Dois números, definidos onde a pirâmide é construída:
-
A base: mais ou menos o espaçamento dos seus pontos onde eles são mais densos. Uma leitura por segundo, base de um segundo. Pontos em rajadas — mil por segundo durante dez segundos a cada hora — usam o espaçamento dentro da rajada, um milissegundo. Uma base muito mais fina que os pontos custa memória (64 bytes por bucket); muito mais grossa, e um zoom profundo lê mais pontos crus por coluna do que precisa.
-
Os níveis: o suficiente para a sua vista mais larga. Uma vista lê no máximo 64 buckets por coluna enquanto o nível do topo for fino o bastante; depois disso, os buckets do topo se acumulam. Use níveis suficientes para que
base × 4^(níveis − 1) ≥ intervalo mais largo ÷ (64 × largura do gráfico)com a largura do gráfico em pixels do dispositivo (pixels CSS × a razão de pixels). Use pelo menos 2, mesmo quando a regra dá 1: o servidor sempre mantém o nível do topo, então um nível 0 sozinho, com um ponto por bucket, fica inteiro (O que custa). Três dias de leituras a cada 10 segundos, base de 10 s, custam 88 bytes por ponto num arquivo de pirâmide com 1 nível e 40 com 2. Níveis a mais custam pouco — 44 bytes com 3, 46 com 9 —, então na dúvida use mais.
Exemplo: pontos a um segundo de distância, um ano mostrado em 2.000 pixels. O intervalo mais largo é 31.536.000 s; dividido por 64 × 2.000, dá uns 246 s. Com base de 1 s, o nível do topo precisa ter pelo menos 246 s: 4⁴ = 256 s é o nível 4, então 5 níveis.
Nenhum dos dois números é uma promessa que você não pode mudar: um arquivo de série guarda os pontos, não uma pirâmide, que é construída a partir deles quando o gráfico ou o servidor carrega o arquivo. (O arquivo de pirâmide de um servidor guarda uma já construída, e é construído de novo para mudá-la.)
Onde eles são definidos
| Onde | Como | Padrão |
|---|---|---|
| Navegador | createChart(canvas, { baseNs, levels }) | 1 s, 9 níveis |
| .NET | new Series(exponent, baseNs, levels) | 1 s, 9 níveis |
nonius-server | --base-ns N --levels L | 1 ms, 12 níveis |
| Rust | Pyramid::new(base, levels) | — |
Os padrões cobrem anos de pontos a um segundo de distância. A base de
um milissegundo do nonius-server serve para dados rápidos; para um
sensor que lê a cada poucos segundos, passe --base-ns. Um gráfico
alimentado por tiles usa a pirâmide do servidor, dos seus metadados:
os ajustes do próprio gráfico são para pontos que você entrega.
No Rust, construir uma pirâmide são duas chamadas:
#![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())?;
}
O que custa
Um bucket tem 64 bytes. O nível 0 de uma base de um segundo sobre um
ano de pontos a cada segundo tem 31,5 milhões deles — tantos quanto os
pontos, com quase três vezes o tamanho. Esse nível não rende nada: um
bucket de um ponto é o ponto. Por isso o nonius-server libera um
nível cujos buckets têm em média menos de 4 pontos (--sparse 4, o
padrão) — qualquer nível menos o do topo, que ele sempre mantém — e
responde uma vista nessa profundidade com os próprios pontos. Cada
nível acima tem um quarto dos buckets do de baixo, então o resto da
pirâmide junto custa mais ou menos um terço do primeiro nível mantido.
No exemplo de Servir, isso dá: 24 bytes por ponto para os próprios pontos, 16 para o nível 1 (um bucket de 64 bytes a cada 4 pontos), e uns 5 para os níveis acima — 45 bytes por ponto.
Servir mostra um servidor construindo uma pirâmide e dizendo o que manteve.
Servir
Um servidor guarda uma série e responde a pedidos pequenos sobre ela: a
sua descrição, tiles para um gráfico no navegador e imagens. O
protocolo é HTTP GET simples, então qualquer cliente, cache ou proxy o
entende. O nonius-server é o servidor de referência, um programa que
você instala pelo Cargo:
cargo install nonius-server
Este capítulo serve as leituras de Seus dados: um ponto por milissegundo durante 100 segundos, num CSV.
De um CSV a um servidor no ar
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
A primeira linha escreve o arquivo de série. A segunda constrói um arquivo de pirâmide: a série e a sua pirâmide, guardadas como os tiles do protocolo, então um tile é lido direto dele. A terceira confere os hashes e constrói a pirâmide de novo para comparar. Cada uma diz o que fez:
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
Os pontos estão a um milissegundo de distância, então a base é um milissegundo. O nível 0 teria um ponto por bucket, e foi liberado (veja O que custa): três níveis ficam.
Um servidor também pode servir o arquivo de série simples, construindo a pirâmide na memória ao iniciar. Um arquivo de pirâmide é construído uma vez e depois mapeado: o servidor inicia na hora, seja qual for o tamanho, e só as partes que um pedido lê vão para a memória.
nonius-server readings.pyr.nsb --id readings --addr 127.0.0.1:8080 &
O & o deixa rodando em segundo plano, para os comandos abaixo. O id
de uma série é o nome do arquivo, a menos que --id dê outro; vários
arquivos servem várias séries.
Perguntar a ele
A descrição da série, os seus metadados:
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"}}
Os inteiros de 64 bits são strings, porque números JSON perdem precisão
depois de 2⁵³. O attachTiles do navegador lê isso primeiro, depois
pede tiles: /v1/series/{id}/columns/{level}/{k}/{i} para resumos e
/v1/series/{id}/raw/{i} para pontos. Os endereços e o conteúdo deles
são as seções 4 e 5 do
protocolo;
o gráfico os calcula sozinho, e você nunca escreve um à mão.
Uma imagem dos 100 segundos inteiros, clara, com título e unidade:
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 e to são o tempo mostrado, [from, to): o to não entra,
então a série inteira termina no tempo do último ponto mais um, ou
depois. width e height são os pixels do próprio PNG; ratio=2 desenha
rótulos e linhas para uma tela de dois pixels do dispositivo por pixel
CSS, então essa imagem preenche uma caixa de 800 × 300 numa tela assim.
A unidade °C vai codificada com porcentagem, como o URLSearchParams
do navegador escreve. O tema é escuro, a menos que venha
theme=light. Todo parâmetro está na página do
nonius-server; um que o
servidor não conhece é ignorado, então um erro de digitação
(timezone= em vez de tz=) volta em silêncio ao padrão.
Cache
Um tile ou uma imagem nunca muda: o mesmo endereço sempre responde os
mesmos bytes. O servidor diz isso — Cache-Control: public, max-age=31536000, immutable — e dá a cada resposta um ETag, um hash
dos seus bytes. Um cliente que já a tem pergunta com a etiqueta e
recebe 304 Not Modified, sem corpo:
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"
Isso imprime 304. Então um navegador, um proxy ou uma CDN podem
guardar tiles e imagens pelo tempo que quiserem. Os metadados são a
exceção: dizem no-cache, porque uma série que cresce muda o seu
último ponto.
Uma página em outra origem
Uma página servida de um endereço só lê um servidor em outro se o
servidor permitir. O nonius-server permite qualquer página: toda
resposta leva Access-Control-Allow-Origin: *, expõe o seu ETag e os
cabeçalhos da imagem aos scripts (Access-Control-Expose-Headers), e
Cross-Origin-Resource-Policy: cross-origin, então uma página que se
isola para a memória compartilhada do feed ao vivo ainda consegue lê-lo.
Uma imagem também diz onde está o seu gráfico: Nonius-Plot: x y width height, o retângulo do gráfico nos pixels da imagem, e Nonius-Range: lo hi, os valores nas suas linhas de baixo e de cima. Uma página que
mostra o PNG transforma um clique num tempo e num valor com eles, e
pede ao /pick do servidor o ponto embaixo dele.
Em produção
O nonius-server é a referência do protocolo e um apoio de testes, não
um servidor de produção: sem TLS, sem limite de conexões, uma thread
por conexão. Diante de usuários, coloque-o atrás de um proxy reverso
que cuide do TLS e limite as conexões; como toda resposta, menos os
metadados, é imutável, o proxy pode guardar todas em cache.
Para um servidor seu, dois caminhos:
- O protocolo dentro do seu programa Rust, pela biblioteca do crate:
#![allow(unused)]
fn main() {
let served = Served::new("day", (t, v, w), 2, Options::default())?;
}
served.answer(&request) responde a um pedido sem socket, e serve
o coloca num TcpListener; a página do
crate mostra os dois.
- O protocolo em qualquer linguagem. Ele foi escrito para ser implementado sem o código do Nonius: o protocolo dá cada endereço, byte e regra, e as imagens são desenhadas pelo pacote .NET como pelo Rust.
Desenhar
Um núcleo Rust transforma uma vista no que um quadro desenha, em todo lugar onde o Nonius roda. No navegador, ele é compilado para WebAssembly e desenha com WebGPU, com WebGL2 onde falta WebGPU, e na CPU num canvas 2D quando o navegador não dá nenhum dos dois. No .NET e no C, é uma biblioteca nativa. Como imagem, desenha na CPU num PNG. Todos desenham o mesmo quadro: os mesmos eixos, rótulos, mira e legenda, desenhados pelo próprio gráfico e nunca sobrepostos a ele como HTML.
Então a escolha abaixo é só onde o seu programa vive.
No navegador
O pacote npm nonius desenha num <canvas>. Duas regras valem em toda
página: a caixa dá o tamanho ao canvas, nunca os pixels do próprio
canvas; e a página é servida por HTTP, já que um módulo e o seu
WebAssembly não carregam de 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>
Essa é uma página inteira. O import map diz ao navegador onde está o
"nonius"; com um bundler (Vite, webpack), o bundler faz isso. O módulo
que ela carrega cria o gráfico, entrega os pontos por qualquer
caminho — o módulo dos tiles ali está inteiro — e
deixa a pessoa explorar com o interact: arrastar para mover, roda do
mouse ou dois dedos para o zoom, passar o cursor para a mira. Um
gráfico alimentado por tiles entrega os tiles ao interact,
interact(chart, canvas, { tiles }), para que mover busque o que a
nova vista precisa.
As opções do gráfico vão para o createChart, e cada uma tem um método
para mudá-la depois:
createChart(canvas, { theme: "light", timeZone: "America/Sao_Paulo", title: "Estufa 2", unit: "°C" }),
e depois setTheme, setTimeZone(chart, name), setTitle e
setUnit. Toda opção, tipada e documentada, está no index.d.ts do
pacote.
React
O gráfico é criado de forma assíncrona (ele espera o seu WebAssembly) e tem memória fora da do JavaScript, então um componente o cria num efeito e o libera na limpeza do efeito — também quando a limpeza vem antes, como o modo de desenvolvimento do React faz de propósito:
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
O mesmo, em onMounted e onBeforeUnmount, com uma regra própria do
Vue: guarde o gráfico numa variável simples, não num 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;
Os dois componentes estão inteiros nos exemplos de React e Vue do pacote, rodando com o Vite e só o plug-in do próprio framework: o WebAssembly não precisa de plug-in nenhum.
No .NET
O pacote NuGet Nonius traz o núcleo nativo para Windows x64 e Linux
x64. Sobre ele, dois controles.
Avalonia, para uma janela de desktop (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, para uma página (Nonius.Blazor), que usa o
pacote do navegador por baixo:
<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" />
Tempos e valores passam entre o .NET e a página como strings decimais, nunca como números JavaScript, que arredondariam um tempo em nanossegundos de hoje para 256 ns.
A partir do C
O nonius-capi é o núcleo atrás de uma interface C: um cabeçalho, o
nonius.h, e uma biblioteca para ligar. Os pacotes .NET são feitos
sobre ele, e qualquer outra linguagem que chama C — o ctypes do
Python, o cgo do Go, o FFM do Java — chega ao Nonius do mesmo jeito. Um
arquivo de série desenhado como 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;
}
Toda chamada devolve um status; nonius_status_message o diz em
palavras. O programa inteiro, e como compilá-lo com MSVC ou gcc, estão
na página do nonius-capi.
Como imagem
Uma still é um quadro desenhado na CPU, sem janela e sem GPU, codificado como PNG: para um relatório, um e-mail, a resposta de um servidor. Quatro caminhos para a mesma imagem:
| De | Como |
|---|---|
| Um servidor | GET /v1/series/{id}/png?… (Servir) |
| .NET | series.StillPng(new Still(…)) (Escolha o caminho) |
| C | nonius_still_png(series, &still, &png) |
| Rust | nonius_render::still_png |
No Rust, uma still diz tudo o que um quadro precisa:
#![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 e height são os pixels da própria imagem; ratio é a razão
de pixels da tela a que ela se destina, que escala os rótulos e as
linhas, não a imagem.
O mesmo gráfico em todo lugar
Cada opção tem o mesmo sentido em todo pacote, e o mesmo padrão: escuro, UTC, todo ponto ligado, sem cabeçalho.
| Navegador | .NET | Imagem do servidor | |
|---|---|---|---|
| Tema | theme: "light" | Theme.Light | theme=light |
| Fuso | timeZone: "Europe/Lisbon" | um TimeZoneInfo | tz=Europe/Lisbon |
| Lacuna | setSeriesGap(0, ns), ns um BigInt | Gap | gap= |
| Título, unidade | title, unit do createChart | Title, Unit | title=, unit= |
Glossário
As palavras que estes guias usam, cada uma explicada para quem a vê pela primeira vez. Uma palavra em itálico tem a sua própria entrada. Os termos ficam em inglês quando é assim que aparecem no código.
Backend
A parte do navegador com que o gráfico desenha: WebGPU onde o navegador tem, WebGL2 onde não tem, e um canvas 2D desenhado na CPU quando não tem nenhum dos dois. O gráfico escolhe o melhor sozinho; todo backend desenha o mesmo quadro.
Base
A largura de tempo dos buckets mais finos da pirâmide, o seu nível 0 — um segundo por padrão. Escolha mais ou menos o espaçamento dos seus pontos: veja a pirâmide.
Bucket
Um resumo dos pontos de um trecho de tempo em oito números: o tempo e o valor do primeiro e do último ponto, o menor e o maior valor, quantos pontos, e a soma dos pesos. Dois buckets vizinhos se juntam num só sem voltar aos pontos.
ABI em C
A Application Binary Interface: como um programa compilado chama uma
biblioteca — que funções existem, e como os argumentos ficam na
memória. O nonius-capi dá ao Nonius uma em C, então qualquer
linguagem que chama C pode usá-lo. O número de versão muda quando esse
arranjo muda.
Coluna
Uma coluna de pixels do gráfico. Uma vista distribui o seu intervalo de tempo pelas colunas, e o gráfico lê um resumo por coluna.
CORS
Cross-Origin Resource Sharing: a regra pela qual o navegador deixa
uma página ler um servidor em outro endereço. O servidor diz quem pode,
em cabeçalhos; o nonius-server permite qualquer página.
Crate
Um pacote Rust, publicado no crates.io. O Nonius são sete deles, do
nonius-format (os bytes) ao nonius-server (o programa).
Pixel do dispositivo e razão de pixels
Os pixels da própria tela. Uma página é diagramada em pixels CSS; numa tela nítida, um pixel CSS são dois ou três pixels do dispositivo, a sua razão de pixels. O gráfico desenha em pixels do dispositivo, então as linhas ficam nítidas.
ETag
Uma etiqueta curta que o servidor dá a uma resposta — o
nonius-server usa um hash dos bytes. Um cliente que já tem a resposta
pergunta de novo com a etiqueta e recebe 304 Not Modified, sem corpo,
se nada mudou.
Expoente
Veja escala.
Quadro
Dois sentidos. Na tela, uma imagem que o gráfico desenha — sessenta por segundo enquanto a pessoa arrasta. Num feed ao vivo, uma mensagem numerada que traz pontos novos.
Lacuna
O maior tempo entre dois pontos que a linha ainda liga. Além dele, a linha quebra. Por padrão todo ponto é ligado; você define a lacuna pelo que sabe dos seus dados.
Nível
Uma camada da pirâmide. Os buckets do nível 0 têm a largura da base; cada nível acima é quatro vezes mais largo.
Feed ao vivo
Pontos enviados conforme acontecem, por um WebSocket, em quadros numerados. Um quadro que a rede perde é notado pelo número que falta e pedido de novo.
Metadados
A descrição de uma série, como um servidor a responde em JSON: o intervalo de tempo, a escala, a pirâmide, o último ponto. O gráfico os lê antes de qualquer tile.
NSB1
O formato de arquivo de série do Nonius: as três colunas dos pontos
(tempo, valor, peso), a escala e uma soma de verificação, num
contêiner cujos primeiros bytes são NONIUSB1. É lido onde cai na
memória, sem etapa de decodificação. O arquivo costuma terminar em
.nsb.
Pick
Achar o ponto embaixo do cursor: o gráfico faz isso para a mira, e um
servidor responde /pick para uma página que mostra a sua imagem.
Ponto
A única coisa que todo pacote recebe: t, v e w, três inteiros —
um tempo em nanossegundos desde 1970 (UTC), um valor, um peso.
Protocolo
As regras para perguntar a um servidor sobre uma série por HTTP: que
endereços existem e que bytes cada um responde. Escrito para ser
implementado sem o código do Nonius; o nonius-server é a referência.
Pirâmide
Uma série resumida em várias larguras de tempo, cada nível quatro vezes mais largo que o de baixo, para que uma vista de qualquer tamanho leia alguns milhares de buckets em vez de todos os pontos — e desenhe exatamente a mesma linha.
Arquivo de pirâmide
Uma série e a sua pirâmide construída num arquivo só, guardadas nas colunas do próprio protocolo. Um servidor o mapeia e inicia na hora, seja qual for o tamanho.
Pontos crus
Os próprios pontos, não resumos. Uma vista funda o bastante num zoom os lê.
Escala
Quantas casas decimais os valores de uma série têm. Com escala 2, o
inteiro 2137 quer dizer 21,37. Também chamada de expoente.
Série
Os pontos de uma grandeza ao longo do tempo, em ordem de tempo: um sensor, um instrumento, uma métrica.
Still
Um quadro desenhado na CPU como imagem PNG, sem janela e sem GPU.
Tile
Um pedaço pequeno de uma série cortado para uma vista, para que o gráfico busque só o que a tela mostra. Um tile de colunas guarda resumos de um nível; um tile cru guarda os pontos crus de um trecho de tempo.
Fuso horário
A regra de um lugar para o seu horário local, com o nome do banco de
dados IANA, como Europe/Lisbon. O Nonius guarda todo tempo em UTC; um
fuso muda só os rótulos.
WebAssembly
Um formato compacto de programa que o navegador roda quase na velocidade nativa. O núcleo Rust do Nonius é compilado para ele, então o navegador roda o mesmo código que o servidor. Muitas vezes escrito wasm.
WebGL2 e WebGPU
As duas formas do navegador desenhar na placa de vídeo. O WebGPU é o mais novo; o WebGL2 está em todo lugar. Veja backend.
WebSocket
Uma conexão que a página mantém aberta com um servidor, para que o servidor mande dados no momento em que os tem. Um feed ao vivo roda sobre um.
Peso
O w de um ponto: quanto ele conta — 1 para uma leitura simples, o
volume de uma negociação. Os resumos somam os pesos.
Canvas 2D
A superfície de desenho simples do navegador, desenhada pela CPU. O último backend do gráfico, usado quando o navegador não tem WebGPU nem WebGL2.