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

English

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.

English

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 pontosCaminhoO que o gráfico guarda
Já estão na página, gerados ou buscados pelo seu próprio códigoEntregue-osTodos
Um histórico fechado, pequeno o bastante para mandar inteiroUm arquivoTodos, buscados uma vez
Um histórico longo demais para mandar, ou que muita gente lêTiles de um servidorSó o que a vista na tela precisa
Chegando agora, conforme acontecemUm feed ao vivoOs 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.

English

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

CampoO que éTipo
to tempo: nanossegundos desde 1970-01-01 00:00 UTCum inteiro de 64 bits com sinal
vo valor: um inteiro com um número fixo de casas decimaisum inteiro de 64 bits com sinal
wo peso: 1 para uma leitura simplesum 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:

DePara 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 CSVo 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.

English

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:

CampoO que guarda
first_t, first_vo tempo e o valor do primeiro ponto
last_t, last_vo tempo e o valor do último ponto
min, maxo menor e o maior valor
countquantos pontos
weighta 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ívelLargura do bucket, base 1 s
01 s
14 s
216 s
44 min 16 s
818 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

OndeComoPadrão
NavegadorcreateChart(canvas, { baseNs, levels })1 s, 9 níveis
.NETnew Series(exponent, baseNs, levels)1 s, 9 níveis
nonius-server--base-ns N --levels L1 ms, 12 níveis
RustPyramid::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.

English

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.

English

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:

DeComo
Um servidorGET /v1/series/{id}/png?… (Servir)
.NETseries.StillPng(new Still(…)) (Escolha o caminho)
Cnonius_still_png(series, &still, &png)
Rustnonius_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.NETImagem do servidor
Tematheme: "light"Theme.Lighttheme=light
FusotimeZone: "Europe/Lisbon"um TimeZoneInfotz=Europe/Lisbon
LacunasetSeriesGap(0, ns), ns um BigIntGapgap=
Título, unidadetitle, unit do createChartTitle, Unittitle=, unit=

English

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.