Skip to content

Server-Side Rendering

Ripl charts can render without a browser. The @ripl/node package configures Ripl's platform factory for Node.js (animation timing, text measurement, default state) and re-exports everything from @ripl/core and @ripl/terminal, so a single import gets you a working headless pipeline.

NOTE

For the platform details (which factory bindings @ripl/node installs), see the Node runtime docs.

Setup

bash
npm install @ripl/node @ripl/charts

Import @ripl/node at the top of your entry point. The import itself configures the runtime; it is the Node equivalent of importing @ripl/web in the browser:

ts
import '@ripl/node';

Without a DOM, there is no Canvas or SVG backend; the built-in headless rendering target is the terminal context, which rasterizes the scenegraph into braille characters with ANSI truecolor.

Rendering a Chart Headlessly

Create a terminal context from the stdout-backed output adapter and pass it to any chart factory:

ts
import '@ripl/node';

import {
    createContext,
    createTerminalOutput,
} from '@ripl/node';

import {
    createBarChart,
} from '@ripl/charts';

const output = createTerminalOutput(); // backed by process.stdout
const context = createContext(output, {
    logicalWidth: 800,
    logicalHeight: 480,
});

const chart = createBarChart(context, {
    data: [
        {
            month: 'Jan',
            sales: 120,
        },
        {
            month: 'Feb',
            sales: 200,
        },
        {
            month: 'Mar',
            sales: 150,
        },
    ],
    key: 'month',
    series: [
        {
            id: 'sales',
            label: 'Sales',
            value: 'sales',
        },
    ],
    animation: false,
});

Two options matter in headless environments:

  • animation: false: renders the final frame immediately. Animations do run headlessly (@ripl/node shims requestAnimationFrame onto setTimeout), but for a one-shot render or a CI snapshot you want the settled state, not a tween.
  • logicalWidth / logicalHeight: author the chart in CSS-pixel coordinates; the terminal context scales and letterboxes that space into the character grid.

Interactive features (tooltips, hover highlights, crosshair) are inert in the terminal, which has no pointer events.

Exporting

Every context snapshots its output via export(); charts forward it with chart.export():

ts
const snapshot = chart.export();

// The rendered chart as plain braille text: log it, write it to a file,
// or embed it in CLI output.
const text = snapshot.toString();

// Raw pixel data (ImageData) rasterized from the braille grid; pass it to
// any PNG encoder to produce an image file.
const image = await snapshot.toImage();

toString() is the natural fit for CLI tools and logs. toImage() returns environment-agnostic ImageData (width, height, and an RGBA byte array); Node has no built-in PNG encoder, so pair it with one of your choosing to write image files. (toURL() relies on browser object URLs and is not useful in Node.)

Awaiting a Render

render() is async and resolves when the render pass (including any transitions) completes. When you need to export deterministically, for example in a test or a build step, disable autoRender and await an explicit render:

ts
const chart = createBarChart(context, {
    data,
    key: 'month',
    series,
    animation: false,
    autoRender: false,
});

await chart.render();

const text = chart.export().toString();

What About Headless Canvas or SVG?

@ripl/node deliberately ships no DOM emulation: its factory stubs return inert elements, so the Canvas (@ripl/canvas) and SVG (@ripl/svg) contexts do not run under Node. If you need raster or vector output server-side, render to the terminal context and encode toImage() yourself, or run the browser contexts in an environment that provides a real DOM and canvas implementation.