Sign in

    moon_vegalite

    Spec-driven Vega-Lite renderer: Vega-Lite JSON in, backend-neutral scenegraph and SVG out.

    vega-lite
    visualization
    chart
    svg
    declarative
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    5 hours ago
    Downloads
    4

    #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, 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

    Repositories: GitHub · GitLink.

    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

    AreaSupported
    Marksbar, line, point, circle, square, area, rule, tick, rect, text
    Channelsx, 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)
    Field typesquantitative, ordinal, nominal, temporal
    Aggregatescount, sum, mean, average, min, max, median, distinct, … (vega aggregate ops)
    Transformsfilter (expression or predicate), calculate (expression subset), aggregate, bin, timeUnit, fold; field bin, timeUnit, aggregate
    Time unitsyear, quarter, month, week, date, day, hours, minutes, seconds, milliseconds and combinations (UTC; "local" time is UTC)
    Scaleslinear, log, sqrt, pow, time, utc, band, point, ordinal (domain, range, zero, nice, reverse, clamp, base, exponent)
    Stackingdefault stacking for bar and area; stack: zero / center / normalize / null
    Sortingsort on discrete x / y: ascending, descending, a value list, -x / -y, {op, field, order}
    Guidesaxes (grid, ticks, labels with flush / overlap removal, title), symbol legends, chart title
    Layoutsingle view, autosize: pad, width / height as numbers or {step}
    Configaccepted 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:

    LayerReferenceWhere
    number formatting, ticks, nice, time formats, scalesd3 / vega-format / vega-scale as bundled in vega 5.33.1fixtures/{numfmt,ticks,formatcases,timecases,scalecases}
    expressions, data transformsvega-expression, vega-transformsfixtures/{exprcases,transformcases}
    scenegraph bounds, SVGvega-scenegraphfixtures/scenecases
    end to end: 60 official Vega-Lite examples, edge cases, MoonFrame specsvega-lite 5.23.0 compile + vega scenegraph and SVGfixtures/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 any committed generated file differs.

    util/pow10_table.mbt holds 10^k for integer k in [-330, 310]. It is generated as Number("1e" + k) (the correctly rounded value) and not with Math.pow(10, k): V8 is not consistent about Math.pow, which differs by one ulp for a few exponents between Linux, macOS and Windows builds of Node 24 (and for about 70 exponents between Node 22 and 24). js_pow10 therefore matches V8 on most builds rather than on one particular build. Compared with the Windows build that produced the previous table, only 10^23 changed, and the full test-suite, including the end-to-end comparison with the official output, passes unchanged.

    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.

    #Layout

    PackageContent
    util, stats, format, time, valueJavaScript-compatible numbers (V8 libm), d3 ticks, d3-format, time intervals / formats, dynamic values
    scalecontinuous, band, point, ordinal and sequential scales, colour schemes
    expr, transformthe expression subset, aggregate / bin / stack / timeunit / fold / filter / formula
    scene, svgscenegraph, mark bounds, path generation, SVG renderer
    specstrict reader of Vega-Lite JSON (unsupported features raise errors with paths)
    vlthe compiler: data pipeline, scales, axes, legends, layout
    cmd/vl2svgcommand line tool (native)

    #Development history

    Most of the work before 24 October 2026 was done locally, without committing step by step. On 24 October 2026 the existing code was organised into commits by module (low-level numeric and time libraries, scales, expressions and transforms, scenegraph and SVG, specification reader, compiler stages, tests, CI). The history on GitHub was rewritten once at that time, to fold a few follow-up CI fixes into one commit. Since then every change is made on its own branch for a GitHub issue and merged through a pull request that closes the issue.

    #License

    Apache-2.0. Parts of the code are ports of vega, vega-lite and d3 (BSD-3-Clause / ISC); see the file headers.

    Compiled

    The result of compiling a specification: see @vl.Compiled.

    RenderError

    pub suberror RenderError {
    RenderError(String)
    }

    An error raised for malformed JSON, unsupported Vega-Lite features (the message names the JSON path) or rendering failures.

    RenderError::message

    fn RenderError::message(self : RenderError) -> String

    compile

    Compile a Vega-Lite specification (JSON text) into a scenegraph.

    render_svg

    fn render_svg(spec : String, scale? : Double) -> String raise RenderError

    Render a Vega-Lite specification (JSON text) as an SVG document. scale multiplies the pixel size of the image (the viewBox is kept).

    scene_json

    The scenegraph of a compiled specification as JSON: a view object (size, origin, background) and the scene tree with the bounds of every mark and item. This is the format the test-suite compares with vega's scenegraph.

    svg_of

    fn svg_of(c :
    Compiled
    , scale? : Double) -> String raise RenderError

    Render an already compiled specification as SVG.

    version

    fn version() -> String

    Library version, kept in sync with moon.mod.

    Source Files