Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Data Visualization - Lona Docs Log in

Data Visualization

Lona-js provides a pure-data chart model with no DOM dependencies. Frontend consumers — HTML5 canvas, SVG, PDF renderers, native apps — use these models to render charts from synced datasets.

Overview

Data rows connect to external sources (Garmin, weather, system metrics) and sync values as date-indexed cells. The SDK extracts those cells into typed point arrays, computes regression curves, and exposes everything through a single query API:

Cell data → DataIndex → DataFrame → Your renderer

The SDK handles:

  • Adapter execution — converting raw JSON cells to [x, y] points
  • Regression — Gaussian kernel smoothing with confidence bands
  • Imputation — filling gaps in sparse time series
  • Axis scaling — coordinate transforms between data and canvas space

Your renderer handles:

  • Drawing points, lines, and filled regions
  • Canvas/SVG/PDF output
  • Interaction (hover, drag, zoom)

Accessing data

DataIndex

The primary API for chart data. Registered as a row index factory, it populates automatically when cell data arrives.

import { DataIndex } from "@lona-js";

// Register the factory when creating the client
const client = new LonaSdk.Client({
  rowIndexFactories: [DataIndex.factory()],
  // ...
});

Access the index through a data row's variant:

const row = sheet.rows.get(rowId);
const dataIndex = (row.variant as DataVariant).dataIndex;

if (!dataIndex) return; // No preset or no data yet

Querying series

Each data row has one or more named series. A series holds extracted [x, y] points and optional regression data:

const series = dataIndex.getSeries("default");

series.key;        // "default"
series.type;       // "regression" | "default"
series.label;      // display name
series.points;     // sorted [x, y][] — raw data points
series.regression; // KernelSmoothResult | null
series.yRange;     // { min, max, center } | null

For multi-series presets (e.g., garmin:weight has "weight" + "bodyFat"):

for (const [key, series] of dataIndex.getAllSeries()) {
  console.log(`${key}: ${series.points.length} points`);
}

Range queries

Get points within a specific x-range (efficient binary search):

const points = dataIndex.getPointsInRange("default", xStart, xEnd);

DataFrame — column-scoped view

For rendering a specific column (e.g., one week), getDataFrame returns a complete snapshot with the x-range already computed:

import { NaiveDate, TimezoneRegion } from "@tento-chrono";

const range = new NaiveDate.Range(
  NaiveDate.fromYmd1(2026, 3, 10).exp(),
  NaiveDate.fromYmd1(2026, 3, 17).exp(),
);
const tz = TimezoneRegion.UTC;

const frame = dataIndex.getDataFrame(range, tz);

A DataFrame contains:

FieldTypeDescription
xRange{start, end}X-axis bounds for the column
yAxis{min, max, center}Y-axis configuration from preset
seriesMap<string, DataFrameSeries>Per-series data
rendererSpecsRendererSpec[]What visual layers to draw

Each DataFrameSeries has:

FieldTypeDescription
points[number, number][]Raw data points in x-range
regressionCurveRegressionPoint[]Smoothed curve points
yRange{min, max, center}Series-specific y-axis override

Rendering with a DataFrame

The DataFrame is renderer-agnostic. Here's how a consumer maps it to a drawing surface:

HTML5 Canvas example

function renderChart(
  ctx: CanvasRenderingContext2D,
  frame: DataIndex.DataFrame,
  width: number,
  height: number,
) {
  const { xRange, yAxis } = frame;
  if (!yAxis) return;

  const toCanvasX = (x: number) =>
    ((x - xRange.start) / (xRange.end - xRange.start)) * width;
  const toCanvasY = (y: number) =>
    height - ((y - yAxis.min) / (yAxis.max - yAxis.min)) * height;

  for (const [key, series] of frame.series) {
    // Draw raw data points
    ctx.fillStyle = "#fe887c";
    for (const [x, y] of series.points) {
      ctx.beginPath();
      ctx.arc(toCanvasX(x), toCanvasY(y), 3, 0, Math.PI * 2);
      ctx.fill();
    }

    // Draw regression curve
    if (series.regressionCurve.length > 1) {
      ctx.strokeStyle = "#fe887c";
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      for (let i = 0; i < series.regressionCurve.length; i++) {
        const pt = series.regressionCurve[i];
        const cx = toCanvasX(pt.x);
        const cy = toCanvasY(pt.y);
        if (i === 0) ctx.moveTo(cx, cy);
        else ctx.lineTo(cx, cy);
      }
      ctx.stroke();

      // Draw confidence band
      ctx.fillStyle = "rgba(245, 158, 11, 0.1)";
      ctx.beginPath();
      for (const pt of series.regressionCurve) {
        ctx.lineTo(toCanvasX(pt.x), toCanvasY(pt.y + pt.confidence));
      }
      for (let i = series.regressionCurve.length - 1; i >= 0; i--) {
        const pt = series.regressionCurve[i];
        ctx.lineTo(toCanvasX(pt.x), toCanvasY(pt.y - pt.confidence));
      }
      ctx.closePath();
      ctx.fill();
    }
  }
}

Renderer specs

The rendererSpecs array describes what visual layers the preset expects. Consumers can use this to decide what to draw:

for (const spec of frame.rendererSpecs) {
  switch (spec.type) {
    case "points":
      drawPoints(spec.key, spec.color, spec.radius);
      break;
    case "regression-curve":
      drawCurve(spec.key, spec.color);
      break;
    case "regression-band":
      drawConfidenceBand(spec.key);
      break;
    case "hrule":
      drawHorizontalRule(spec.offset, spec.offsetType);
      break;
  }
}
Renderer typeDescription
pointsScatter plot dots at data points
regression-curveSmoothed trend line
regression-bandConfidence interval fill
regression-fillFill between curve and horizontal rule
regression-pointsDots at regression sample points
hruleSingle horizontal reference line
hrule-allThree horizontal lines (min, center, max)
bar-chart-with-widthBars with dynamic width
bucketsAggregated bucket bars

Regression & smoothing

Kernel smoothing

The kernelSmooth function computes a Nadaraya-Watson regression over sorted point arrays:

import { kernelSmooth } from "@lona-js";

const result = kernelSmooth(points, halfWidth, {
  confFactor: 1.96,  // 95% confidence interval
  maxPoints: 500,    // limit output density
});

for (const pt of result.curve) {
  pt.x;          // evaluation x-coordinate
  pt.y;          // smoothed value (weighted local mean)
  pt.confidence; // stdev-based confidence band width
  pt.density;    // data support (0 = no nearby data, 1 = dense)
}

The halfWidth parameter controls smoothing:

  • Small (e.g., 3 for daily data) → follows individual points closely
  • Large (e.g., 14) → smooth trend, ignores day-to-day noise

Imputation

Fill gaps in sparse data via linear interpolation before smoothing:

import { impute } from "@lona-js";

const filled = impute(sparsePoints, step);
// step = 1 for daily DSE data, step = 3600000 for hourly MSE data

Density-aware rendering

Each regression point includes a density value (0–1) indicating how much data supports that region. Use this to fade the curve in sparse areas:

const alpha = smoothstep(0.01, 0.2, pt.density);
ctx.globalAlpha = alpha;

Axis scaling

The ChartViewModel namespace provides coordinate transforms between data values and pixel coordinates:

import { ChartViewModel } from "@lona-js";

const yScaler = new ChartViewModel.LinearYScaler({
  range: { min: 0, max: 100, center: 50 },
});

// Data value → canvas Y coordinate
const canvasY = yScaler.d2c(dataValue, height, padding);

// Canvas Y coordinate → data value
const dataValue = yScaler.c2d(canvasY, height, padding);

// Generate tick marks for the Y axis
const ticks = yScaler.getTicks();

Presets

Data rows use presets to configure how their cell data maps to chart series. Presets are registered by lookup key:

PresetSeriesX-axisDescription
:~garmin:stressoverallDSEDaily stress score (0–100)
:~garmin:hrvdefaultMSEHRV readings (multiple per day)
:~garmin:sleepdefault, durationssecondsSleep levels + durations
:~garmin:weightweight, bodyFatDSEWeight (kg) + body fat (%)
:~garmin:spo2defaultDSEBlood oxygen (85–100%)
:~whoop:sleepperformance, efficiencyMSESleep quality metrics
:~whoop:recoveryrecovery, hrvMSERecovery + HRV scores

Each preset defines:

  • Adapters — functions that extract [x, y] from cell JSON
  • Y-axis range — min, max, center for scaling
  • X-axis type — DSE (days), MSE (milliseconds), or custom
  • Renderer specs — what visual layers to render

See also