Generate isometric 3D stacked-cube bar charts in Node.js. Returns a PNG Buffer. No browser, no Puppeteer, no headless Chrome, no external API.
Stax is the only server-side isometric chart library for Node.js. It runs anywhere Node runs — Next.js API routes, Express endpoints, Railway workers, AWS Lambda, Vercel serverless functions, cron jobs, build scripts — and produces pixel-perfect isometric bar charts as PNG images you can write to disk, upload to S3, embed in markdown, or stream from an HTTP endpoint.
Built on obelisk.js and node-canvas. TypeScript-first. Zero browser dependency. If you've ever tried to generate an isometric chart on a server and hit a wall, this is what you were looking for.
An Artists & Robots project by Jason Alan Snyder.
I'm the co-founder of Artists & Robots and SuperTruth. For the SuperTruth blog, I wanted beautiful isometric stacked-cube charts — the kind that make data feel dimensional — that I could generate in Node.js and embed directly in posts, emails, and images without spinning up a browser.
I couldn't find a good, simple solution. Everything was browser-only, a Chrome extension, or a React component. Getting obelisk.js — the only serious isometric canvas library — running inside Node.js with node-canvas and JSDOM took a few days of real pain. I built stax so nobody else has to go through that. The charts on supertruth.ai/blog are the live proof it works.
From GitHub (available now):
npm install github:evil-robot/staxFrom npm (coming soon):
npm install @artistsandrobots/staxstax uses node-canvas, which requires native build tools.
macOS:
brew install pkg-config cairo pango libpng jpeg giflib librsvgLinux / Railway / Docker:
apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-devVercel / Lambda: node-canvas ships prebuilt binaries for the most common runtimes — no manual install needed in most cases.
import { renderChart } from '@artistsandrobots/stax';
import { writeFileSync } from 'fs';
const png = renderChart({
title: 'Analysis Time: Before vs After AI',
labels: ['Ingestion', 'Scoring', 'QA Review', 'Reporting', 'Delivery'],
values: [42, 18, 31, 25, 9],
unit: ' hrs',
});
writeFileSync('chart.png', png);png is a Buffer. Write it to disk, upload to S3, embed in markdown, stream it from an API route — whatever you need.
// app/api/chart/route.ts
import { renderChart } from '@artistsandrobots/stax';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const spec = await req.json();
const png = renderChart(spec);
return new NextResponse(png, {
headers: { 'Content-Type': 'image/png' },
});
}app.post('/chart', (req, res) => {
const png = renderChart(req.body);
res.set('Content-Type', 'image/png').send(png);
});const png = renderChart(spec);
const dataUri = `data:image/png;base64,${png.toString('base64')}`;
const markdown = ``;import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const png = renderChart(spec);
await s3.send(new PutObjectCommand({
Bucket: 'my-bucket',
Key: 'charts/output.png',
Body: png,
ContentType: 'image/png',
}));These charts appear on supertruth.ai/blog and were made with stax — generated in Node.js and embedded directly in each post.
Leukemia patient community: what behavioral signals precede treatment decisions?
Ovarian cancer awareness gap: what behavioral data reveals before diagnosis
renderChart(spec: ChartSpec): Buffer| Field | Type | Default | Description |
|---|---|---|---|
title |
string |
required | Chart title. Wraps to two lines at 60 characters. |
labels |
string[] |
required | Label for each bar. Maximum 12 bars (A–L). |
values |
number[] |
required | Numeric value for each bar. |
unit |
string |
"" |
Appended to each value in the legend. Pass "%", " hrs", "x", etc. |
descriptions |
string[] |
— | Optional subtext shown under each label in the legend card. |
width |
number |
960 |
Canvas width in px. |
height |
number |
540 |
Canvas height in px. |
brandText |
string |
stax attribution | Text in the bottom brand strip. Pass "" to hide it entirely. |
brandLogo |
string |
— | Absolute path to a logo image file. Drawn left of brandText. |
accentColor |
string |
— | Single hex color. Auto-generates a 4-shade gradient and tints the markers. Overrides palette. |
theme |
"light" | "dark" |
"light" |
"dark" flips to a slate-900 background with adjusted card and text colors. |
palette |
(string | number)[] |
teal gradient | Cube colors light → dark. Accepts "#14B8A6" or 0x14B8A6. |
- Bar heights are relative: the tallest bar always reaches
MAX_STACKS(10 cubes) and others scale proportionally. - Values ≤ 0 render as a single-cube minimum bar — they never disappear entirely.
- Labels longer than the legend column are truncated with an ellipsis.
unitis appended without a space — if you want42 hrs, passunit: " hrs"(leading space included).
Every dimension, color, and typographic value lives in src/tokens.ts. The full token system:
// Cube geometry (obelisk iso units)
export const CUBE = {
W: 40, // cube face width
H: 18, // single stack unit height
GAP: 22, // gap between bar columns
};
// Canvas layout
export const CHART = {
WIDTH: 960,
HEIGHT: 540,
MAX_STACKS: 10, // tallest bar = this many cubes
CHART_FRAC: 0.55, // fraction of body width for the chart area
BRAND_BAND: 46, // brand strip height at bottom
};
// Default cube palette (top-face hex integers)
export const PALETTE = {
cubes: [
0x14B8A6, // teal-500 — base
0x0D9488, // teal-600
0x0F766E, // teal-700
0x115E59, // teal-800 — top
],
};Pass a single hex color and stax auto-generates a 4-shade light-to-dark gradient and tints the markers to match.
renderChart({ ...spec, accentColor: "#6366F1" }); // indigo
renderChart({ ...spec, accentColor: "#F43F5E" }); // rose
renderChart({ ...spec, accentColor: "#8B5CF6" }); // violetrenderChart({ ...spec, theme: "dark" });
renderChart({ ...spec, theme: "dark", accentColor: "#8B5CF6" });For precise control, pass an array of colors light → dark. Accepts hex strings or integers — both work.
// hex strings
renderChart({ ...spec, palette: ["#FCD34D", "#FBBF24", "#F59E0B", "#D97706"] });
// hex integers (also fine)
renderChart({ ...spec, palette: [0x6366F1, 0x4F46E5, 0x4338CA, 0x3730A3] });obelisk.js is a browser-only library — it expects window, document, and a real DOM canvas. stax bridges the gap:
- A JSDOM environment is created and patched so that
document.createElement('canvas')returns a node-canvas surface instead of a DOM element - obelisk.js is evaluated inside this environment via
eval()on the bundledobelisk.min.js, with the browser globals it expects installed only for the duration of that synchronous evaluation - The isometric cubes are rendered onto the node-canvas surface — again with the globals scoped to the synchronous render, then restored
- A second full-size canvas composes the two-column layout: chart, legend card, title, markers, and brand strip
canvas.toBuffer('image/png')returns the final PNG as a Node.jsBuffer
ChartSpec
→ JSDOM shim
→ obelisk.js (iso cube renderer)
→ node-canvas (Cairo-backed 2D context)
→ Buffer (PNG)
The JSDOM instance and obelisk.js evaluation are lazy and cached — the first call takes ~50ms to initialize, subsequent calls render in ~10ms.
Some server-side SDKs refuse to run in a "browser-like environment": the Anthropic SDK, the Neon serverless driver, and others check for a global window object and throw if they find one. stax never leaks one. window, document, Image, and HTMLCanvasElement are installed on globalThis only while a synchronous obelisk operation is running and restored in a finally before renderChart returns. Rendering never awaits, so no other code on the event loop can observe them. You can render charts and construct these SDK clients in the same process, in any order.
This guarantee is enforced by a regression test (npm test) that runs in CI on every push.
| Platform | Status | Notes |
|---|---|---|
| Local Node.js | Works | Install native deps via Homebrew |
| Railway | Works | Add apt-get build deps to Dockerfile or Nixpacks config |
| Vercel | Works | Prebuilt node-canvas binaries for Node 18/20 |
| AWS Lambda | Works | Use a Lambda layer with node-canvas prebuilt binary |
| Docker (Alpine) | Works | Use node:lts-bookworm base, add apt deps |
| Cloudflare Workers | Not supported | No Node.js native module support |
SuperTruth — healthcare intelligence platform. The charts throughout supertruth.ai/blog are made with stax: generated in Node.js, embedded directly in posts as PNG.
Artists & Robots is a human-first AI studio building intelligent platforms for media, healthcare, and commerce.
SuperTruth is the truth layer underneath healthcare intelligence — consent-verified, integrity-scored patient data that powers better decisions.
Built by Jason Alan Snyder, co-founder of both.
MIT License. Copyright (c) 2026 Jason Alan Snyder / Artists & Robots.






