Skip to content

evil-robot/stax

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

stax

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.

License: MIT


DTI Scoring Weights by Dimension


Why this exists

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.


Install

From GitHub (available now):

npm install github:evil-robot/stax

From npm (coming soon):

npm install @artistsandrobots/stax

Native dependency note

stax uses node-canvas, which requires native build tools.

macOS:

brew install pkg-config cairo pango libpng jpeg giflib librsvg

Linux / Railway / Docker:

apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev

Vercel / Lambda: node-canvas ships prebuilt binaries for the most common runtimes — no manual install needed in most cases.


Quick start

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.


Use cases

Next.js API route

// 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' },
  });
}

Express endpoint

app.post('/chart', (req, res) => {
  const png = renderChart(req.body);
  res.set('Content-Type', 'image/png').send(png);
});

Embed in markdown (blog posts, emails)

const png = renderChart(spec);
const dataUri = `data:image/png;base64,${png.toString('base64')}`;
const markdown = `![${spec.title}](${dataUri})`;

Upload to S3

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',
}));

Real output

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?

Behavioral sequence before treatment decisions

Key statistics — leukemia patient community

Ovarian cancer awareness gap: what behavioral data reveals before diagnosis

Ovarian cancer awareness gap

Key statistics — ovarian cancer


API reference

renderChart(spec: ChartSpec): Buffer

ChartSpec

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.

Notes

  • 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.
  • unit is appended without a space — if you want 42 hrs, pass unit: " hrs" (leading space included).

Design tokens

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
  ],
};

accentColor — one line, full gradient

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" }); // violet

Indigo accent

theme: "dark"

renderChart({ ...spec, theme: "dark" });
renderChart({ ...spec, theme: "dark", accentColor: "#8B5CF6" });

Dark violet

Custom palette

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] });

How it works

obelisk.js is a browser-only library — it expects window, document, and a real DOM canvas. stax bridges the gap:

  1. A JSDOM environment is created and patched so that document.createElement('canvas') returns a node-canvas surface instead of a DOM element
  2. obelisk.js is evaluated inside this environment via eval() on the bundled obelisk.min.js
  3. The isometric cubes are rendered onto the node-canvas surface
  4. A second full-size canvas composes the two-column layout: chart, legend card, title, markers, and brand strip
  5. canvas.toBuffer('image/png') returns the final PNG as a Node.js Buffer
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 compatibility

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

Used by

SuperTruth — healthcare intelligence platform. The charts throughout supertruth.ai/blog are made with stax: generated in Node.js, embedded directly in posts as PNG.


About

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.

About

Generate isometric 3D stacked-cube bar charts in Node.js. Returns a PNG Buffer. No browser, no Puppeteer, no headless Chrome. Works in Next.js, Express, Lambda, Railway, and anywhere Node runs.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors