fulgur-chart
A CLI that generates static SVG / PNG charts from a chart.js v4–compatible JSON spec (a side project of Fulgur).
Why
Generates deterministic charts — byte-identical output for the same input — without a browser or JavaScript. Combined with Fulgur, the resulting SVG can be embedded as a vector graphic in a PDF. Re-generating reports in CI produces no diff, making it easy to keep figures under version control.
Installation
This installs a binary named fulgur-chart.
Usage
Prepare a minimal chart.js spec (chart.json):
Generate SVG / PNG:
# SVG (default)
# PNG (--scale sets the resolution multiplier; 2 doubles the pixel dimensions)
Use - for stdin / stdout piping:
|
Key options:
--format svg|png— Output format. Inferred from the output extension (.png→ png; otherwise / stdout → svg) when omitted.--width <px>/--height <px>— Override canvas dimensions (default 800 × 450).--scale <factor>— PNG resolution multiplier (default 1.0).--font <path>— Replace the font used for measurement, SVG, and PNG (default: bundled Noto Sans JP).--out-dir <dir>— Output directory for batch generation (see below).--dsl chartjs|vegalite— Input DSL. Auto-detected when omitted: a top-levelmarkkey selects Vega-Lite; a top-leveltypekey selects chart.js.--strict— Treat unknown / unsupported keys as errors (silently ignored by default).
# Override dimensions and detect unknown keys with --strict
Batch generation
Render multiple specs at once (useful for generating report figures in CI).
Each input X.json is written to <out-dir>/X.<ext> (output is byte-identical per file).
Other subcommands
# Print the JSON Schema for an input DSL (useful for validation tooling)
# Inspect the semantic model (IR + layout) for a spec — pretty JSON
Supported chart types
- Bar chart (vertical / horizontal; horizontal via
options.indexAxis: "y") - Stacked bar chart (
options.scales.{x,y}.stacked: true) - Line chart
- Area chart (
datasets[].fill: trueon a line dataset) - Pie chart
- Doughnut chart
- Scatter plot (
{x, y}point data) - Bubble chart (
{x, y, r}point data) - Radar chart
- Mixed chart (per-dataset
type, e.g. bar + line) - Progress bar chart (QuickChart-style; horizontal fill bar with centered percentage)
- Matrix chart / heatmap (
{x, y, v}point data; cells shaded by interpolating between two colors) - Box plot chart (5-number summary:
type: "boxplot",dataas nested arrays[min, q1, median, q3, max]) - Gauge chart (QuickChart-style; semicircle with colored zones, needle, value label)
- Radial gauge chart (QuickChart-style; full circle fill-to-value with center value text)
Supported chart.js subset
Supports a data-only, static subset:
type—bar/line/pie/doughnut/scatter/bubble/radar/matrix/boxplot/progress/gauge/radialGauge(QuickChart'sprogressBaris also accepted as an alias)data.labelsdata.datasets[]—label/data(numeric array;{x,y}/{x,y,r}for scatter/bubble;{x,y,v}for matrix; nested[min,q1,median,q3,max]arrays for boxplot) /backgroundColor/borderColor/borderWidth/fill/tension/pointRadius/type(per-dataset type for mixed charts)- For
progress(aliasprogressBar),datasets[0].dataholds each bar's value; an optional second dataset'sdataoverrides the per-bar max (default 100). The percentage label is shown by default and can be hidden withoptions.plugins.datalabels.display: false. - For
gauge,datasets[0].dataholds cumulative zone thresholds,valueis the needle value, andbackgroundColoris the per-zone colors (minValuesets the lower bound). Configure withoptions.needle/options.valueLabel. The value label falls back to the rounded value (JSvalueLabel.formatteris not executed). - For
radialGauge,datasets[0].dataholds a single value drawn as a fill-to-value arc on a track ring. Configure withoptions.domain/options.trackColor/options.centerPercentage/options.roundedCorners/options.centerArea(displayText/fontSize). The center value text falls back to the rounded value (JScenterArea.textis not executed). options.indexAxisoptions.plugins.title/options.plugins.legend(position: top/bottom/left/right;legenddoes not apply togauge/radialGauge)options.plugins.datalabels(display— renders a value label at each data point)options.scales(stacked/suggestedMin/suggestedMaxand a subset of other options)options.theme(extension; see below)
Dynamic JavaScript features (callback / animation / interaction / plugin scripts)
are not supported. Unknown keys are silently ignored by default; use --strict to
detect them as errors.
Themes (options.theme)
chart.js v4 default colors and styles are used as a baseline. options.theme overrides
the appearance (this is an extension key not present in chart.js itself; omit it to use
the defaults).
palette— Array of color strings for automatic dataset / slice coloringgridColor/textColor— Grid line color / text colorbackgroundColor— Canvas background (transparent by default)fontSize— Base font size for labels (px)
Colors accept #rgb / #rrggbb / rgb() / rgba() / hsl() / hsla() / CSS color names.
Vega-Lite input (--dsl vegalite)
In addition to chart.js specs, a minimal Vega-Lite subset is accepted as input:
# Explicit
# Auto-detected (top-level "mark" key selects Vega-Lite)
Supported subset: mark (bar / line / point → scatter / arc → pie), inline
data.values, and encoding fields x / y / color / theta. The Tableau10 color
palette is applied automatically to Vega-Lite specs. Input is converted to a shared
intermediate representation, so output determinism and Fulgur integration are identical
to chart.js input.
Ruby binding
An in-repository Ruby gem (crates/bindings/ruby) wraps the same rendering core via a
Rust native extension (magnus / rb-sys). It is build-from-source only (not yet published
to RubyGems) and requires a Rust toolchain.
&&
svg = FulgurChart.build(spec_json).width(800).height(450).render(:svg)
png = FulgurChart.build(spec_json).scale(2.0).render(:png)
See crates/bindings/ruby/README.md for the full API reference.
Fulgur integration
Embed the generated SVG in HTML with <img> and render to PDF with Fulgur:
See examples/report.html for a minimal example.
Bundling the same Noto Sans JP font on the Fulgur side ensures chart text glyphs match.
Determinism
The same input spec always produces byte-identical output. Only the bundled Noto Sans JP font is used; system fonts are never loaded.
Roadmap
The following are not yet implemented (candidates for future support):
- Value labels on radar chart axes; data labels on scatter / radar
- Dual-axis mixed charts (separate left/right y-scales); mixing with horizontal / stacked bars
- Vega-Lite URL data,
transform, andaggregate(currently inlinedata.valuesonly) - Font subsetting (binary size reduction)
License
Code is dual-licensed under MIT OR Apache-2.0.
The bundled Noto Sans JP font is distributed under the SIL Open Font License 1.1 and is included as-is from the upstream notofonts / noto-cjk distribution.