Hand-drawn, sketchy React charts and flowcharts.
D3 does the math. Rough.js does the drawing. A Vibe engine dials in the aesthetic.
GoldenChart cleanly separates where things go from how they look:
- Calculation layer (
d3-scale,d3-shape,d3-hierarchy) computes coordinates, path strings, and layouts. It never touches the DOM. - Rendering layer (
roughjs) turns those coordinates into hand-drawn SVG paths. - The Vibe engine translates a semantic string like
messy_sketchinto concrete Rough.js parameters (roughness,bowing,hachureAngle,strokeWidth, …).
npm install goldenchart roughjs d3-scale d3-shape d3-hierarchyreact / react-dom (v18+) are peer dependencies.
import { BarChart } from 'goldenchart';
export function Sales() {
return (
<BarChart
width={480}
height={300}
vibe="chaotic_notebook"
data={[
{ label: 'Q1', value: 12 },
{ label: 'Q2', value: 19 },
{ label: 'Q3', value: 7 },
{ label: 'Q4', value: 24 },
]}
/>
);
}Every chart is built from reusable primitives, so you can draw arbitrary diagrams.
Hand any D3-computed path string to <RoughPath>:
import { Surface, RoughPath, RoughRectangle } from 'goldenchart';
import { linePath } from 'goldenchart';
<Surface width={400} height={200} vibe={{ preset: 'clean_blueprint', roughness: 1.2 }}>
<RoughRectangle x={20} y={20} width={120} height={60} fill="#fde68a" />
<RoughPath d={linePath([{ x: 0, y: 100 }, { x: 200, y: 40 }, { x: 400, y: 120 }], 'basis')} fill={null} />
</Surface>Beyond lines and rectangles, the calculation layer ships path builders for richer
shapes — regularPolygonPath, starPath, arcStrokePath, wedgePath,
ellipsePath — plus connectorPath for arrows (shaft + arrowhead + label, with
straight/curved/orthogonal routing). Hand any of their path strings to <RoughPath>.
The MCP server exposes the same shapes as compose_surface scene kinds
(polygon, regular-polygon, star, arc, wedge, ellipse, arrowhead, arrow).
A VibeConfig is either a preset name or a preset plus targeted overrides:
<BarChart vibe="messy_sketch" ... />
<BarChart vibe={{ preset: 'clean_blueprint', roughness: 2, stroke: '#0f766e' }} ... />Built-in presets: messy_sketch, clean_blueprint, chaotic_notebook, pencil, marker,
ink, crayon, davinci_journal, blueprint_dark, chalkboard, neon, comic_book,
terminal, watercolor, newsprint, whiteboard, typewriter, midnight, art_deco, manga,
highlighter, kraft, synthwave, botanical, risograph, sticky_note, amber_crt. Add
animate: { drawOn: true } for a hand-drawn reveal (honors prefers-reduced-motion).
Matte vibes can carry a faint paper-grain speckle behind the data via
texture: 'paper' | 'paper-subtle' | 'none' (several paper-like presets enable
'paper' by default). See docs/API.md#vibe-engine.
The same chart, six different vibes:
Each preset ships with a matching open-source font. Rendering is environment-aware:
-
Headless/server rendering (
goldenchart/server, the MCP server, PNG export) embeds the vibe's font automatically as@font-facein the SVG, so that output is self-contained and renders identically with no installed/network fonts. -
Browser rendering emits the
font-familyand relies on the page's webfonts, keeping the main bundle small (~35 KB gzipped). To make a browser-rendered SVG self-contained, import fromgoldenchart/fontsand inject the CSS:import { fontFaceFor } from 'goldenchart/fonts'; // inject fontFaceFor('pencil') into a <style> in your document for self-contained output
Rasterizers that load fonts explicitly (e.g. resvg) can use FONT_TTF_BASE64 from
goldenchart/fonts. See src/assets/fonts/ATTRIBUTION.md for sources and licences.
Layer a brand on top of any vibe to recolour a chart to your own identity while
keeping the hand-drawn feel. The vibe controls how a chart is drawn; the brand
sets identity — palette, primary / ink / page colours, font, and a
corner logo. Every field is optional, and an explicit vibe override still wins
over the brand.
<BarChart
vibe="pencil"
brand={{
palette: ['#ff6b35', '#f7b801', '#7a9e7e', '#ef476f', '#118ab2'],
primary: '#ff6b35',
ink: '#3a2317',
page: '#fff8ef',
logo: { src: logoUrl, position: 'bottom-right', width: 88 },
}}
data={data}
/>;Full field reference and precedence rules: docs/API.md#branding.
- Charts:
BarChart(single/grouped/stacked),LineChart,AreaChart(+ stacked),ScatterPlot,PieChart(+ donut),SankeyChart,TreemapChart,HeatmapChart,RadarChart - Diagrams:
Flowchart,MindMap,OrgChart,ArchitectureDiagram,SequenceDiagram,ERDiagram,Timeline, plus the low-levelDiagram - Auto-charting:
AutoChart/visualize(pick a chart from the data) - Chart furniture:
Axis,Grid,Legend,Annotations(reference lines/bands, callouts) - Primitives:
RoughPath,RoughLine,RoughRectangle,RoughCircle,RoughText - Container:
Surface(Tailwind wrapper +VibeProvider)
Every chart and diagram shares a BaseChartProps base (width, height, vibe,
margin, title, description, ariaLabel, bare). Data charts also accept
dataTable to emit a visually-hidden table for screen readers; diagrams render a
role="img" surface with title/description.
See docs/API.md for the full per-component prop reference.
Hand visualize (or its component form AutoChart) a row-oriented dataset and it
profiles the fields, picks a chart, and renders it. Steer it with an intent
(trend, compare, composition, distribution, correlation, flow, hierarchy):
import { AutoChart } from 'goldenchart';
<AutoChart
width={480}
height={300}
intent="trend"
data={[
{ month: 'Jan', revenue: 12 },
{ month: 'Feb', revenue: 19 },
{ month: 'Mar', revenue: 7 },
]}
/>;Or just describe what you want — visualize / AutoChart also accept a
plain-English query that picks the chart type, maps fields to roles, and selects
a vibe from the sentence. Any explicit prop you pass still wins.
import { visualize } from 'goldenchart';
// "revenue by month as a line in pencil"
visualize(data, { query: 'revenue by month as a line in pencil', width: 480, height: 300 });See the natural-language query reference for the full grammar (chart types, intents, field roles, vibes).
Beyond charts, GoldenChart lays out and sketches node-link diagrams. Flowchart,
MindMap, OrgChart, and ArchitectureDiagram take nodes / edges and a layout
engine; SequenceDiagram (actors / messages), ERDiagram (entities /
relationships), and Timeline (events) are purpose-built.
import { Flowchart } from 'goldenchart';
<Flowchart
width={420}
height={260}
direction="TB"
nodes={[
{ id: 'a', label: 'Start' },
{ id: 'b', label: 'Work', shape: 'diamond' },
{ id: 'c', label: 'Done' },
]}
edges={[
{ from: 'a', to: 'b' },
{ from: 'b', to: 'c', label: 'ok' },
]}
/>;Flowchart supports four layout directions (TB/BT/LR/RL), rect/ellipse/diamond
node shapes, edge labels, arrowheads, curved/orthogonal routing, and general DAG layout
(merges, multiple roots).
You can also parse Mermaid source into a diagram spec with
parseMermaid (flowchart, sequence, and mindmap syntaxes) and render it with renderDiagram:
import { parseMermaid, renderDiagram } from 'goldenchart';
const spec = parseMermaid('graph TD; A-->B; B-->C;');
renderDiagram(spec, { width: 400, height: 240, vibe: 'pencil' });Interactivity is a progressive enhancement behind the goldenchart/interactive
subpath — the static goldenchart entry stays untouched and font-free. Wrap any
chart in <InteractiveChart> for sketched, vibe-matched tooltips, hover/selection,
an interactive legend, semantic zoom/pan, brushing, animated data transitions, and
linked crossfilter:
import { BarChart } from 'goldenchart';
import { InteractiveChart } from 'goldenchart/interactive';
<InteractiveChart tooltip selectable>
<BarChart width={480} height={300} vibe="pencil" data={data} />
</InteractiveChart>;Charts emit an inert data-gc-* mark contract that the client layer reads, so the
server/MCP SVG is identical with or without it. interactiveEmbed(svg) (and the
MCP export_interactive_html tool) wrap a static SVG into a self-contained
hover-interactive HTML file. See docs/INTERACTIVITY.md.
The sketchy look never gets in the way of reading the chart:
-
Legible labels — text gets a page-colour halo (
paint-order: stroke) so labels stay sharp on dark or textured vibes and even when they sit on top of a hachure fill. -
Clean fills — hachure is clipped to each shape, so the fill never bleeds past the edge while the sketch outline stays loose and hand-drawn.
-
Intentional reveal — the optional
drawOnanimation sketches the outline first, then settles the fill in, instead of dashing the hatching. Open the before and after SVGs in a browser to compare.
src/
├── types/ # VibeConfig, base props, geometry, chart data shapes
├── vibe/ # presets + resolver (semantic string -> Rough.js options) + React context
├── core/ # D3 calculation layer — scales, shapes, ticks, arc, hierarchy, dag, sankey,
│ # treemap, polar, color scales, text metrics, stack, palette (no DOM)
├── render/ # shared Rough.js generator (DOM-free)
├── primitives/ # RoughPath / RoughLine / RoughRectangle / RoughCircle / RoughText
└── components/ # Surface, every chart, Axis, Grid, Legend, Annotations
npm run playground # interactive Vite demo of every chart + vibeIt has two views: the interactive Playground (live vibe / brand / natural-language controls) and a Gallery — a static tour of every vibe, every chart type, the paper-grain texture tiers, and brand kits.
An MCP server in mcp/ exposes GoldenChart as tools at every level
(vibe, calculation, primitives, charts, orchestration/export), so an agent can
render charts and flowcharts as SVG. See mcp/README.md.
npm run build # bundle with tsup (ESM + CJS + types)
npm run typecheck # tsc --noEmit
npm test # vitestThe README images live in assets/ and are generated from the library itself —
rebuild them with npm run build then cd mcp && npm run assets.
MIT




