Nonius

Séries temporais densas, exatas até o nanossegundo em qualquer zoom.

Milhões de pontos num navegador, num programa de desktop ou nas imagens de um servidor, cada um no seu lugar por mais que você aproxime. Um único núcleo em Rust resume a série no servidor e a desenha na tela.

No crates.io e no npm; os pacotes .NET chegam em breve ao NuGet.

Um gráfico com um dia de pontos se aproxima quarenta bilhões de vezes, até dois microssegundos; os rótulos de tempo vão das horas do dia aos nanossegundos, e os pontos de uma rajada, a poucas centenas de nanossegundos uns dos outros, aparecem separados
Um dia de pontos, um por segundo, ampliado até dois microssegundos de uma rajada dentro dele. Desenhado pelo pacote do navegador, quadro a quadro.

Por que o Nonius

Exato no zoom profundo

Um gráfico que guarda o tempo como float de 32 bits, como os gráficos em GPU costumam fazer, não distingue dois instantes de 2025 separados por menos de uns dois minutos. O Nonius guarda o tempo em nanossegundos de 64 bits até descontar o início da janela visível; só os pequenos deslocamentos que sobram viram floats.

Um núcleo, do servidor ao pixel

Os tiles e as imagens do servidor e o quadro do navegador saem do mesmo código Rust, compilado para WebAssembly na página. Uma página que monta a série a partir de tiles desenha o mesmo que uma página com a série inteira, e o PNG do servidor é o quadro do navegador.

Formato próprio, lido no lugar

Uma série viaja como NSB1: colunas little-endian alinhadas a 64 bytes, o layout dos buffers do Arrow. O navegador copia os bytes uma vez para a memória do gráfico e os lê ali mesmo: sem JSON, sem decodificador. Um feed ao vivo são quadros numerados num WebSocket, e um quadro perdido é recuperado.

Uma linha por quatro pontos a 250 nanossegundos uns dos outros, o eixo do tempo rotulado em nanossegundos depois da meia-noite
Quatro pontos a 250 nanossegundos uns dos outros, no primeiro instante de 2025, desenhados a partir de um arquivo, na CPU.

Como funciona

Um ponto vai dos seus dados até a tela em cinco passos, e nenhum deles é uma conversão:

  1. O ponto: um tempo em nanossegundos, um valor e um peso, três inteiros de 64 bits, para que nada seja arredondado.
  2. NSB1: os pontos em colunas, cada uma começando num múltiplo de 64 bytes, lidas onde estão.
  3. A pirâmide: resumos em cada nível de zoom, e uma visualização lê o nível que cabe nos seus pixels.
  4. Tiles por HTTP: pedaços fixos em endereços que nunca mudam, guardados em cache por um ano.
  5. O feed ao vivo: quadros numerados, para que um quadro perdido seja notado e recuperado.

Como um ponto viaja, com os bytes de um tile de verdade.

Como se encaixa

O mesmo núcleo em Rust roda no seu servidor, na sua página e em programas nativos. Entre o servidor e a página passam só três coisas: tiles, um feed ao vivo e imagens.

Seu servidor

o núcleo do Nonius
  • guarda a série como uma pirâmide de resumos
  • a corta em tiles
  • desenha imagens na CPU

Tilescolunas NSB1 por HTTP

Feed ao vivoquadros numerados num WebSocket

ImagensPNG, para o que quer que mostre uma

Sua página

o núcleo do Nonius, em WebAssembly
  • busca os tiles de que a visualização precisa
  • desenha com WebGPU, WebGL2 ou um canvas 2D
  • acompanha o feed ao vivo num worker

Programas nativos

o núcleo do Nonius, linkado
Rust
os crates, desenhando com wgpu ou na CPU
C
uma biblioteca e seu header, para qualquer linguagem que chame C
.NET
um controle de desktop e um componente Blazor
Um só núcleo dos dois lados da rede: o tile cortado no servidor é exatamente o que a página desenha, e o PNG do servidor é o quadro do navegador. Como um ponto viaja.

Como usar

Um ponto é um tempo em nanossegundos, um valor e um peso. O valor é um inteiro com um número fixo de casas decimais, então 101.25 continua 101.25 dos seus dados até o quadro.

Os guias levam uma tarefa do começo ao fim: escolher entre um arquivo, tiles e um feed ao vivo, colocar seus dados, escolher a pirâmide, servir, e desenhar no navegador, no .NET, a partir do C ou como imagem.

Seus dados: um CSV, sem código

nonius-server csv prices.csv prices.nsb

Vem com o cargo install nonius-server. Ele descobre as casas decimais, lê tempos em ISO 8601 e o ponto e vírgula e a vírgula decimal de uma planilha, e para na primeira linha que não consegue ler, dizendo qual. Depois, nonius-server prices.nsb serve o arquivo.

De um programa, o mesmo arquivo sai de cada pacote, byte a byte: o write_series do Rust, o Nsb.Encode do .NET, o nonius_nsb_write do C e o encodeNsb no JavaScript, no navegador ou no Node.

Rust: de um arquivo de série a uma imagem, sem GPU

cargo add nonius-format nonius-core
cargo add nonius-render --no-default-features

Leia o arquivo no lugar, guarde-o como uma pirâmide de resumos e desenhe qualquer visualização dele; aqui, a série inteira:

use nonius_core::{Loaded, Pyramid, Series, Width, Zone};
use nonius_format::{AlignedBytes, Container};
use nonius_render::{Still, Theme, still_png};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Read once into memory aligned for the columns, then read in place.
    let bytes = AlignedBytes::from_slice(&std::fs::read("day.nsb")?);
    let container = Container::parse_checked(bytes.as_slice())?;
    let series = Series::from_container(&container)?;

    // Summaries from one second up, nine levels (see nonius-core's
    // "Choosing the pyramid").
    let mut pyramid = Pyramid::new(Width::new(1_000_000_000)?, 9)?;
    pyramid.extend_sorted(series.iter())?;
    let loaded = Loaded::new(&pyramid, (series.t(), series.v(), series.w()));

    let (Some(&first), Some(&last)) = (series.t().first(), series.t().last()) else {
        return Err("the file holds no points".into());
    };
    // The image's size is in device pixels; `ratio` scales labels and lines.
    let png = still_png(&loaded, &Still {
        start: first,
        end: last + 1,
        width: 2400,
        height: 800,
        theme: Theme::light(),
        ratio: 2.0,
        scale: series.scale(),
        gap: None,
        zone: Zone::UTC,
        title: None,
        unit: None,
    })?;
    std::fs::write("day.png", png)?;
    Ok(())
}

Uma imagem diz onde está o gráfico e o que ele abrange, então um clique nela escolhe o ponto exato e uma marca desenhada por cima cai onde a linha passa: o /png do servidor responde Nonius-Plot e Nonius-Range, e o still_placement, o nonius_still_placement do C e o Series.StillPlacement do .NET dão os mesmos números.

O navegador: um gráfico que se arrasta e se amplia

npm install nonius
const chart = await createChart(canvas);

// Your points: times in nanoseconds since 1970 as BigInts, values as
// integers with a fixed number of decimal places (2 here: 10_000 is
// 100.00), and a weight each.
const { t, v, w } = randomWalk();
chart.setScale(2);
chart.push(t, v, w);

// Open on everything, the value axis fitted to it.
const [start, end] = [t[0], t[t.length - 1] + 1n];
const [lo, hi] = chart.autoscale(start, end, 0, 8);
chart.setView(start, end, lo, hi);

WebGPU quando o navegador tem, WebGL2 quando não tem, e um canvas 2D quando nenhum dos dois oferece um dispositivo. O pacote também acompanha os tiles de um servidor e recebe um feed ao vivo num worker; React e Vue o usam como qualquer outro módulo.

Um sensor que envia dados em rajadas aparece com uma reta de uma rajada à seguinte, a menos que você diga quanto tempo de pausa é uma lacuna: chart.setSeriesGap(id, ns) quebra a linha onde um ponto chega mais de ns nanossegundos depois do anterior, nos mesmos lugares em qualquer zoom.

O tempo sai em UTC até você indicar um fuso: setTimeZone(chart, "America/Sao_Paulo") mostra o eixo, a mira e o tooltip na hora local, dos dois lados de cada troca de horário de verão, com os dados de fuso do próprio navegador. As imagens do servidor aceitam tz=, e o gráfico do .NET, um TimeZoneInfo.

O título e a unidade ficam no próprio gráfico, não na página em volta: createChart(canvas, { title: "Temperatura média", unit: "°C" }) desenha uma linha de cabeçalho acima da área do gráfico, o título à esquerda e a unidade sobre os valores, então um PNG enviado por e-mail ou um gráfico de desktop em .NET diz o que mostra. Os rótulos desenham Latin-1 — todos os acentos do português, do espanhol, do francês e do alemão — e alguns sinais, como € e “ ”. As imagens do servidor aceitam title= e unit=, e o gráfico do .NET, Title e Unit.

.NET: o mesmo núcleo, em C#

dotnet add package Nonius
using var series = Series.FromNsb(File.ReadAllBytes(nsb));
if (!series.TryGetRange(out var first, out var last))
{
    throw new InvalidDataException($"{nsb} holds no points");
}
// 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);

Nonius.Avalonia é um controle de gráfico para desktop, e Nonius.Blazor um componente que traz o pacote do navegador.

Os pacotes

PacoteO que é
nonius-corePontos, séries, a pirâmide de resumos, tiles, escala automática, eixos.
nonius-formatNSB1, o contêiner colunar, escrito e lido no lugar.
nonius-renderO renderizador: na GPU com wgpu, ou na CPU sem dispositivo.
nonius-textA fonte dos rótulos dos eixos, Inter, rasterizada num atlas.
nonius-serverO servidor de referência: uma série em tiles e imagens por HTTP.
nonius-liveO cliente de um feed ao vivo: quadros, lacunas, recuperação, reconexão.
nonius-capiA interface em C, com seu header.
nonius (npm)O pacote do navegador.
Nonius, Nonius.Avalonia, Nonius.Blazor (NuGet).NET, em breve no NuGet.

Especificações

O protocolo de rede foi escrito para ser implementado sem ler o código do Nonius: um cliente foi escrito só a partir dele, e suas colunas batem com as do núcleo. As especificações estão em inglês.