A spec-driven Vega-Lite renderer written in MoonBit.
A Vega-Lite JSON specification goes in; a backend-neutral scenegraph and an SVG string come out.
It runs on the wasm-gc, js and native backends and has no dependency outside the MoonBit core library.
let svg = @moon_vegalite.render_svg(spec_json) // SVG document
let c = @moon_vegalite.compile(spec_json) // scenegraph + view metadata
let scene = @moon_vegalite.scene_json(c) // the scenegraph as JSON
moon build --target native cmd/vl2svg
vl2svg spec.vl.json chart.svg # SVG file (stdout without the second argument)
vl2svg --scene spec.vl.json # scenegraph JSON, with the bounds of every item
See the gallery: the SVG from this library next to the official vega output.
Positioning and differences from other charting libraries
It follows Vega-Lite, not a new grammar. The goal is the same picture the reference implementation draws
(vega-lite 5.23.0 + vega 5.33.1), not a similar one: axis ticks, label formats, legends, layout and mark bounds are ported
from vega-lite / vega, including their JavaScript number and text-measurement quirks.
Scenegraph first. Like Vega, the library produces a tree of marks and items with computed bounds. SVG is one renderer
of that tree; other backends can consume it. Most MoonBit charting code draws directly; here the intermediate form is public.
Specifications in, pictures out. Charts are data (JSON), so they can come from other tools. The test-suite feeds specs
generated by MoonFrame (DataFrame → Vega-Lite) to the renderer.
No browser, no JavaScript runtime. Rendering is a pure function, usable in a native CLI, in WebAssembly, or in JS.
Honest about scope. It is a subset renderer. Anything outside the subset fails with an error that names the JSON path
(encoding.color.condition 暂不支持) instead of drawing something different.
Support
Area
Supported
Marks
bar, line, point, circle, square, area, rule, tick, rect, text
Channels
x, y, x2, y2, color (nominal/ordinal fields with the category scheme, quantitative fields with a blues ramp and gradient legend, constant values), size, opacity, shape, text (size, opacity and shape on point, circle, square; quantitative fields give continuous scales, shape is nominal)
filter (expression or predicate), calculate (expression subset), aggregate, bin, timeUnit, fold; field bin, timeUnit, aggregate
Time units
year, quarter, month, week, date, day, hours, minutes, seconds, milliseconds and combinations (UTC; “local” time is UTC)
Scales
linear, log, sqrt, pow, time, utc, band, point, ordinal (domain, range, zero, nice, reverse, clamp, base, exponent)
Stacking
default stacking for bar and area; stack: zero / center / normalize / null
Sorting
sort on discrete x / y: ascending, descending, a value list, -x / -y, {op, field, order}
Guides
axes (grid, ticks, labels with flush / overlap removal, title), symbol legends, chart title
Layout
single view, autosize: pad, width / height as numbers or {step}
Config
accepted keys are recorded; a subset (view, scale.zero, …) is honoured
Not supported — each raises an error that names the JSON path: params / selection, tooltip, projection, layer,
facet, concat (hconcat / vconcat), repeat, data.url (embed data.values instead), condition, and the full
Vega expression language (only the subset used by calculate / filter is implemented). Gradient legends for
quantitative colour is limited to the default blues ramp (no scale options on non-positional channels).
Anything the compiler would otherwise ignore is rejected with the JSON path: unknown mark properties, config keys other than scale.zero, axis / scale / legend options that are not implemented, sort on other channels, and so on (vl/validate.mbt).
Accuracy against the official implementation
Every expected value in the tests comes from the official packages, never from this repository:
Layer
Reference
Where
number formatting, ticks, nice, time formats, scales
d3 / vega-format / vega-scale as bundled in vega 5.33.1
end to end: 60 official Vega-Lite examples, edge cases, MoonFrame specs
vega-lite 5.23.0 compile + vega scenegraph and SVG
fixtures/vlexamples
For the end-to-end cases the scenegraph (every mark, item property and bounds, numbers within a relative 1e-9) and the
SVG text (exactly, ignoring only the accessibility role / aria-label attributes) must equal the official output.
The cases cover the 60 official examples listed in examples/official.txt, the edge cases in examples/edge_*.vl.json, the channel specs in examples/channel_*.vl.json
(empty data, all-null column, single-value domain, log scale with non-positive values), and six charts generated by MoonFrame
(examples/moonframe). Invalid JSON and unsupported features are covered by vl/errors_test.mbt.
moon test passes on wasm-gc, js and native.
Fixture provenance and regeneration
Fixtures are MoonBit files generated by scripts/gen_fixtures.mjs with the versions pinned in scripts/package-lock.json
(vega-lite 5.23.0, vega 5.33.1, vega-datasets 3.2.1) on Node.js 24 (some expected values depend on V8’s Math functions, so the Node major version matters). The process time zone is fixed to UTC.
cd scripts
npm ci
npm run fixtures # downloads the example specs of the vega-lite tag, regenerates ../fixtures
npm run gallery # regenerates ../gallery (needs: moon build --target native cmd/vl2svg)
CI regenerates the fixtures and fails if the committed files differ. The one exception is util/pow10_table.mbt (V8’s Math.pow(10, k)): regenerated on the Linux runner it differs from the committed file, which was generated on Windows, so it is not compared there (the cause was not investigated further).
examples/moonframe/*.vl.json were produced by ihb2032/MoonFrame 0.7.0 (@chart.format_vega_lite). MoonFrame 0.7.0 does
not currently build with the latest MoonBit toolchain (its dependencies NyaCSV and quickcheck still ship moon.mod.json, and
three NyaCSV tests use a removed Buffer API), so the specs were generated in a scratch project after migrating those
manifests and removing the three tests locally. The generated JSON is committed unchanged.
moon_vegalite
A spec-driven Vega-Lite renderer written in MoonBit. A Vega-Lite JSON specification goes in; a backend-neutral scenegraph and an SVG string come out. It runs on the
wasm-gc,jsandnativebackends and has no dependency outside the MoonBit core library.Repositories: GitHub · GitLink.
See the gallery: the SVG from this library next to the official vega output.
Positioning and differences from other charting libraries
DataFrame→ Vega-Lite) to the renderer.encoding.color.condition 暂不支持) instead of drawing something different.Support
bar,line,point,circle,square,area,rule,tick,rect,textx,y,x2,y2,color(nominal/ordinal fields with the category scheme, quantitative fields with a blues ramp and gradient legend, constant values),size,opacity,shape,text(size, opacity and shape onpoint,circle,square; quantitative fields give continuous scales, shape is nominal)quantitative,ordinal,nominal,temporalcount,sum,mean,average,min,max,median,distinct, … (vega aggregate ops)filter(expression or predicate),calculate(expression subset),aggregate,bin,timeUnit,fold; fieldbin,timeUnit,aggregatelinear,log,sqrt,pow,time,utc,band,point,ordinal(domain,range,zero,nice,reverse,clamp,base,exponent)barandarea;stack: zero / center / normalize / nullsorton discrete x / y:ascending,descending, a value list,-x/-y,{op, field, order}autosize: pad,width/heightas numbers or{step}view,scale.zero, …) is honouredNot supported — each raises an error that names the JSON path:
params/selection,tooltip,projection,layer,facet,concat(hconcat/vconcat),repeat,data.url(embeddata.valuesinstead),condition, and the full Vega expression language (only the subset used bycalculate/filteris implemented). Gradient legends for quantitative colour is limited to the default blues ramp (noscaleoptions on non-positional channels). Anything the compiler would otherwise ignore is rejected with the JSON path: unknown mark properties,configkeys other thanscale.zero, axis / scale / legend options that are not implemented,sorton other channels, and so on (vl/validate.mbt).Accuracy against the official implementation
Every expected value in the tests comes from the official packages, never from this repository:
nice, time formats, scalesfixtures/{numfmt,ticks,formatcases,timecases,scalecases}fixtures/{exprcases,transformcases}fixtures/scenecasesfixtures/vlexamplesFor the end-to-end cases the scenegraph (every mark, item property and bounds, numbers within a relative
1e-9) and the SVG text (exactly, ignoring only the accessibilityrole/aria-labelattributes) must equal the official output. The cases cover the 60 official examples listed inexamples/official.txt, the edge cases inexamples/edge_*.vl.json, the channel specs inexamples/channel_*.vl.json(empty data, all-null column, single-value domain, log scale with non-positive values), and six charts generated by MoonFrame (examples/moonframe). Invalid JSON and unsupported features are covered byvl/errors_test.mbt.moon testpasses onwasm-gc,jsandnative.Fixture provenance and regeneration
Fixtures are MoonBit files generated by
scripts/gen_fixtures.mjswith the versions pinned inscripts/package-lock.json(vega-lite 5.23.0, vega 5.33.1, vega-datasets 3.2.1) on Node.js 24 (some expected values depend on V8’sMathfunctions, so the Node major version matters). The process time zone is fixed to UTC.CI regenerates the fixtures and fails if the committed files differ. The one exception is
util/pow10_table.mbt(V8’sMath.pow(10, k)): regenerated on the Linux runner it differs from the committed file, which was generated on Windows, so it is not compared there (the cause was not investigated further).examples/moonframe/*.vl.jsonwere produced byihb2032/MoonFrame0.7.0 (@chart.format_vega_lite). MoonFrame 0.7.0 does not currently build with the latest MoonBit toolchain (its dependenciesNyaCSVandquickcheckstill shipmoon.mod.json, and threeNyaCSVtests use a removedBufferAPI), so the specs were generated in a scratch project after migrating those manifests and removing the three tests locally. The generated JSON is committed unchanged.Layout
util,stats,format,time,valuescaleexpr,transformscene,svgspecvlcmd/vl2svgLicense
Apache-2.0. Parts of the code are ports of vega, vega-lite and d3 (BSD-3-Clause / ISC); see the file headers.