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 - The isometric cubes are rendered onto the node-canvas surface
- 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.
| 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.






