Something went wrong

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

Render Plans - Lona Docs Log in

Render Plans

Render plans separate what to render from how to render it. They are pure computations that take sheet state as input and produce a per-row plan describing what changed and what update strategy to use.

Overview

When a sheet column needs rendering, the pipeline is:

SheetHandle → RenderPlan.makeForRows() → RenderPlan[] → Binder (DOM)

The render plan is the decision layer. It computes:

  • Update type — whether a cell needs a full rebuild, content update, or just a style change
  • Render mode — normal, stacked, or spanned
  • Time state — whether the column is in the past, current, or future
  • Span mode — whether the cell is hidden (content rendered by fixed children)
  • Stacking — sub-column info for cells that fit multiple time periods in one column

The binder then applies these decisions to the DOM.

Usage

import { RenderPlan } from "@tento-lona/platform";

const plans = RenderPlan.makeForRows(
  context,
  rows,
  previousState,
  resizeOnly,
  subcolumnCache,
);

for (const plan of plans) {
  switch (plan.updateType) {
    case "full-rebuild":
      // Create new cell content from scratch
      break;
    case "content":
      // Rebind existing cell with new data
      break;
    case "style-only":
      // Update height, span mode, time state CSS only
      break;
    case "none":
      // Skip this cell entirely
      break;
  }
}

Update Types

TypeWhenCost
full-rebuildNew cell, type changed, or structure changedHigh — creates DOM
contentSame cell, new dataMedium — rebinds plugins
style-onlyResize, or cell is hidden/spannedLow — CSS only
noneNothing changedZero

Render Modes

Each row gets a render mode based on how its timescale compares to the sheet's timescale:

ModeConditionExample
normalRow timescale ≤ sheet timescaleDay row on day sheet
stackedRow timescale < sheet timescale (with stacking layout)Day row on month sheet
spannedRow timescale > sheet timescale (spannable)Year row on day sheet

Spanned rows have skipContent: true — their content is rendered by fixed children rather than per-column cells.

Mode resolution matrix

The mode depends on the timescale relationship AND the row descriptor's spannable and stackable flags:

TimescaleSpannableStackable + layoutMode
row < sheetanyyesstacked
row < sheetanynonormal
row = sheetanyanynormal
row > sheetyesanyspanned
row > sheetnoanynormal
customanyanynormal
  • Stacking is checked first — if the row timescale is finer than the sheet and layout has stacking info, stacking wins over spanning.
  • Spanning requires spannable: true on the descriptor. Without it, a month row on a day sheet renders normal (one cell per column).
  • Custom timescale always renders normal.

Previous State

The render plan is a pure function. To determine what changed, it compares the current row state against a PreviousState map that the caller tracks:

interface PreviousState {
  type: string;       // row type last rendered
  isStacked: boolean; // was stacked last time
  cellCount: number;  // stacking cell count
  isNew: boolean;     // true if cell slot was just created
}

When no previous state exists for a row (first render), the plan produces full-rebuild. When previous state matches current state, it produces content. When the type or structure changed, it produces full-rebuild with a diff indicating what changed.

Time State

Every plan includes a TimeState for the column:

interface TimeState {
  inThePast: boolean;
  isCurrent: boolean;
  isFuture: boolean;
}

This is used by the binder to apply visual styling (dimmed past columns, highlighted current column).

Context

The render plan context provides the sheet layout, column info, and viewport dimensions:

interface Context {
  sheet: LayoutProvider;   // timeScale + layout (row heights, stacking)
  column: Column;          // the column being rendered
  now: DateTime<Utc>;      // current time (for time state)
  visibleWidthPx: number;  // viewport width (for static span detection)
  resolvedWidthPx: number; // column width (for span size calculation)
  mapper: ColumnMapper;    // column offset resolution
}

Testing

Render plans are tested with JSON fixtures:

{
  "name": "new row produces full-rebuild",
  "input": {
    "sheetTimeScale": "day",
    "column": { "date": "2026-03-15" },
    "now": "2026-03-15T12:00:00Z",
    "rows": [{ "id": "r_...", "type": "number", "timeScale": "day" }],
    "previousState": {},
    "resizeOnly": false
  },
  "expected": [
    { "updateType": "full-rebuild", "mode": "normal" }
  ]
}

Sheet-referencing fixtures test against real SDK descriptors:

{
  "sheet": "timescale-variety",
  "input": {
    "sheetTimeScale": "day",
    "previousState": "from-rows",
    "resizeOnly": false
  },
  "expected": {
    "all": { "updateType": "content" }
  }
}

See tento/tento-lona-js/tests/fixtures/render-plan-*.fixture.json and tento/tento-lona-js/tests/sheets/*.sheet.json for the full test suite.