mooncharts

Lightweight native SVG charting library for MoonBit — bar, line, pie/donut and scatter charts.

svg
chart
visualization
plot
dataviz
graphics
moon add Xpeng/mooncharts@0.8.0
Download zip
Author
Version
0.8.0
License
Apache-2.0
Last updated
last month
Downloads
37
README

#mooncharts

CI

Lightweight, dependency-free SVG charting for MoonBit. Turn typed data into standalone SVG documents you can drop into a web page, a report, or a file — bar, line, area, pie/donut and scatter charts, plus multi-series line and grouped bar charts, out of the box.

Because MoonBit compiles to JS, Wasm and native, the same chart code can run in the browser or generate static SVG on the backend.

These images are real SVG output from the library. See examples/interactive.html for a live, in-browser demo.

#Features

  • Twenty-four chart types: bar (vertical / horizontal / grouped / stacked), line, multi-series line, area, stacked area, pie, donut, rose, scatter, bubble, radar, histogram, box plot, heatmap, candlestick, waterfall, treemap, funnel, gauge, gantt, sparkline
  • Nicely rounded ticks on both axes, gridlines, zero-baseline handling for negative values (Heckbert "nice numbers")
  • Symmetric ±error bars on bar, line and scatter charts (errors?)
  • Smooth Catmull-Rom curves on line and area charts (smooth=true)
  • Axis titles on the XY charts (x_title? / y_title?)
  • Reusable statistics helpers: mean, median, quartiles; SI number formatting (format_si: 1.2k, 3.4M)
  • Squarified treemap layout, area-true rose/bubble sizing — real algorithms, unit-tested
  • Micro-benchmarked: a typical chart renders in 12–25 µs (see Performance)
  • Edge-condition tested: empty, single-point, flat and mixed-sign inputs
  • A runnable cookbook — every recipe is a moon test case
  • Light / dark themes and custom color palettes
  • Reusable SVG primitives — compose your own shapes
  • Typed, defaulted configuration (no stringly-typed option bags)
  • Zero dependencies beyond moonbitlang/core
  • Snapshot-tested, with a runnable browser gallery

#Install

moon add Xpeng/mooncharts

Then import it in your package's moon.pkg:

import { "Xpeng/mooncharts", }

#Quick start

///|
test {
let svg = @mooncharts.bar_chart(
[("Q1", 12.0), ("Q2", 19.0), ("Q3", 8.0), ("Q4", 15.0)],
title="Quarterly Revenue",
)
// `svg` is a complete, standalone SVG document string.
inspect(svg.has_prefix("<svg"), content="true")
inspect(svg.contains("Quarterly Revenue"), content="true")
}

Write the returned string to a .svg file, or embed it directly in an HTML page.

#Chart types

FunctionData shapeNotes
bar_chartArray[(String, Double)]vertical bars with value + category labels
line_chartArray[(Double, Double)]XY line with point markers
area_chartArray[(Double, Double)]line with the region beneath it filled
pie_chartArray[(String, Double)]pie, or donut via donut=0.0..1.0
scatter_chartArray[(Double, Double)]XY scatter, configurable point radius
line_chart_multiArray[(String, Array[(Double, Double)])]several named line series + legend
bar_chart_groupedArray[String], Array[(String, Array[Double])]grouped bars per category + legend
bar_chart_stackedArray[String], Array[(String, Array[Double])]stacked bars per category + legend
bar_chart_horizontalArray[(String, Double)]sideways bars with left-hand category labels
radar_chartArray[String], Array[(String, Array[Double])]spider chart, one filled polygon per series
histogramArray[Double]raw samples binned into bins? equal-width bars
box_plotArray[(String, Array[Double])]quartile boxes, 1.5×IQR whiskers, outlier dots
area_chart_stackedArray[Double], Array[(String, Array[Double])]accumulated area layers + legend
heatmapArray[String], Array[String], Array[Array[Double]]color-scaled matrix cells with value labels
candlestick_chartArray[(String, Double, Double, Double, Double)]OHLC candles, rising/falling colors
waterfall_chartArray[(String, Double)]signed deltas accumulate, connectors + total bar
bubble_chartArray[(Double, Double, Double)]XY dots with area-true size scaling

bar_chart, line_chart and scatter_chart also accept errors? : Array[Double] to draw symmetric ±error bars (the value axis widens to fit them).

Full signatures for every function live in docs/api.md, and runnable recipes for every chart in cookbook.mbt.md.

#Performance

Rendering is a pure string computation — no DOM, no canvas — so it is fast. Measured with moon bench, MoonBit's built-in benchmark runner, on this library's own calls:

Benchmarkwasm-gcjs
pie_chart, 6 slices17.7 µs11.9 µs
bar_chart, 6 bars24.3 µs15.8 µs
line_chart, 100 points94.9 µs62.3 µs
heatmap, 10×10211 µs138 µs
line_chart, 1000 points1.12 ms0.55 ms

Mean over moon bench's repeated runs on one developer machine — reproduce with moon bench / moon bench --target js. The live demo also displays the render time of every chart you build.

All chart functions share optional title?, width?, height? and theme? parameters.

#Theming

Every chart accepts an optional theme?. Use the built-in light (default) or dark theme, or swap the series palette:

///|
test {
let dark = @mooncharts.bar_chart(
[("Q1", 12.0), ("Q2", 19.0)],
theme=@mooncharts.Theme::dark(),
)
inspect(dark.contains("#1e1e2e"), content="true") // dark background

let custom = @mooncharts.Theme::light().with_palette(["#ff5733", "#33c1ff"])
let svg = @mooncharts.bar_chart([("A", 1.0), ("B", 2.0)], theme=custom)
inspect(svg.contains("#ff5733"), content="true")
}

Run the bundled example to generate an HTML page showing every chart type:

moon run cmd/main > gallery.html # then open gallery.html in your browser

#Live interactive demo

examples/interactive.html runs mooncharts in the browser: the library is compiled to JavaScript and re-renders charts live as you switch chart types, drag data sliders, or toggle the dark theme. The web/ package exposes a render function to JS via link.js.exports; rebuild the bundle with:

moon build --target js --release cp _build/js/release/build/web/web.js examples/mooncharts.js

Then open examples/interactive.html in a browser.

#Project write-up

docs/devlog.md is a development log covering the architecture, the design decisions (SVG primitives, nice-number ticks, theming, multi-series), what MoonBit was like to use, and the AI-assisted workflow behind the project.

#License

#
Theme

pub(all) struct Theme {
palette : Array[String]
background : String
grid : String
axis : String
text : String
title : String
}

Visual theme shared by every chart. background of "" means transparent (no background rectangle is drawn).

#
Theme::dark

fn Theme::dark() -> Theme

A dark theme: dark background, light text and a brighter palette.

#
Theme::light

fn Theme::light() -> Theme

The default light theme: transparent background with dark text.

#
Theme::with_palette

fn Theme::with_palette(self : Theme, palette : Array[String]) -> Theme

Return a copy of this theme using a different series palette.

#
area_chart

fn area_chart(data : Array[(Double, Double)], smooth? : Bool, x_title? : String, y_title? : String, title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render an area chart for a series of (x, y) points as a standalone SVG document string. The region between the line and the baseline is filled with a translucent color; smooth curves the top edge; x_title / y_title add axis captions. title, width, height and theme are optional.

#
area_chart_stacked

fn area_chart_stacked(xs : Array[Double], series : Array[(String, Array[Double])], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a stacked area chart as a standalone SVG document string: series values at shared xs positions are accumulated so each layer sits on top of the one below, with a legend. series is a list of (name, values) where values[i] pairs with xs[i]. title, width, height and theme are optional.

#
bar_chart

fn bar_chart(data : Array[(String, Double)], errors? : Array[Double], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a vertical bar chart as a standalone SVG document string.

data is a list of (label, value) pairs; negative values draw downward from the zero baseline. errors[i] (optional) draws a symmetric ±error bar on bar i. title, width, height and theme are optional.

#
bar_chart_grouped

fn bar_chart_grouped(categories : Array[String], series : Array[(String, Array[Double])], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a grouped bar chart: for each category, one bar per series, drawn side by side, with a legend. categories names the groups along the x-axis; series is a list of (name, values) pairs where values[i] is the value for category i. title, width, height and theme are optional.

#
bar_chart_horizontal

fn bar_chart_horizontal(data : Array[(String, Double)], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a horizontal bar chart as a standalone SVG document string: one bar per (label, value) pair growing rightward, with category labels on the left, value labels at the bar tips, and vertical gridlines. Values are expected to be non-negative. title, width, height and theme are optional.

#
bar_chart_stacked

fn bar_chart_stacked(categories : Array[String], series : Array[(String, Array[Double])], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a stacked bar chart: for each category the series values are stacked into one bar, with a legend. categories names the bars; series is a list of (name, values) where values[i] is the value for category i. title, width, height and theme are optional.

#
box_plot

fn box_plot(data : Array[(String, Array[Double])], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a box plot as a standalone SVG document string: one box per (label, samples) pair. Boxes span the interquartile range with a median line; whiskers extend to the furthest samples within 1.5×IQR of the box, and samples beyond the whiskers are drawn as outlier dots. title, width, height and theme are optional.

#
bubble_chart

fn bubble_chart(data : Array[(Double, Double, Double)], max_radius? : Double, title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a bubble chart as a standalone SVG document string. Each entry of data is (x, y, size); bubble area is proportional to size, with the largest bubble drawn at max_radius pixels. title, width, height and theme are optional.

#
candlestick_chart

fn candlestick_chart(data : Array[(String, Double, Double, Double, Double)], up_color? : String, down_color? : String, title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a candlestick chart as a standalone SVG document string. Each entry of data is (label, open, high, low, close); the thin wick spans low..high and the body spans open..close. Rising candles (close >= open) use up_color, falling ones down_color. title, width, height and theme are optional.

#
container

fn container(tag : String, attrs : Array[(String, String)], inner : String) -> String

A container element wrapping inner markup, e.g. <g ...>...</g>.

#
document

fn document(width : Double, height : Double, body : String) -> String

Wrap body in a root <svg> element with the given pixel size and the standard namespace, producing a standalone, browser-renderable document.

#
elem

fn elem(tag : String, attrs : Array[(String, String)]) -> String

A self-closing SVG element such as <rect x="0" ... />.

#
escape

fn escape(s : String) -> String

Escape the XML special characters in text or attribute content so that the resulting SVG string stays well-formed.

#
format_si

fn format_si(x : Double) -> String

Format a number with an SI suffix: 1_200 -> "1.2k", 3_400_000 -> "3.4M", 0.0012 -> "1.2m". Values below 1000 (and above 1/1000) keep their plain num form. Handy for compact axis labels and data labels.

Example

test {
inspect(@mooncharts.format_si(1200.0), content="1.2k")
inspect(@mooncharts.format_si(3400000.0), content="3.4M")
inspect(@mooncharts.format_si(-52000.0), content="-52k")
inspect(@mooncharts.format_si(42.0), content="42")
}

#
funnel_chart

fn funnel_chart(data : Array[(String, Double)], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a funnel chart as a standalone SVG document string. Each (label, value) becomes a horizontal band whose width is proportional to its value; consecutive bands are joined into trapezoids so the funnel tapers from the largest stage down. A conversion percentage relative to the first stage is printed on each band. title, width, height and theme are optional.

#
gantt_chart

fn gantt_chart(tasks : Array[(String, Double, Double)], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a Gantt chart as a standalone SVG document string. Each task is (name, start, end) on an arbitrary numeric time scale (days, weeks, …); bars share a rounded time axis with vertical gridlines. title, width, height and theme are optional.

#
gauge_chart

fn gauge_chart(value : Double, min? : Double, max? : Double, bands? : Array[Double], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a radial gauge as a standalone SVG document string. The needle points to value on a 180° arc spanning min..max; the arc is split into colored zones by bands (each a fraction in 0..1 of the sweep, cycling the palette). title, width, height and theme are optional.

#
heatmap

fn heatmap(rows : Array[String], cols : Array[String], values : Array[Array[Double]], title? : String, width? : Double, height? : Double, low_color? : String, high_color? : String, theme? : Theme) -> String

Render a heatmap as a standalone SVG document string. rows and cols label the grid and values[r][c] is the cell value; cell colors interpolate between low_color and high_color across the value range, and each cell prints its value with a contrast-aware text color. title, width, height, the two scale colors and theme are optional.

#
histogram

fn histogram(values : Array[Double], bins? : Int, title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a histogram of raw values as a standalone SVG document string: samples are counted into bins equal-width bins over a nicely rounded range and drawn as near-flush bars. bins, title, width, height and theme are optional.

#
line_chart

fn line_chart(data : Array[(Double, Double)], errors? : Array[Double], smooth? : Bool, x_title? : String, y_title? : String, title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a line chart connecting a series of (x, y) points as a standalone SVG document string. The value axis auto-fits the data with nicely rounded gridlines; when all y-values are positive the baseline is anchored at zero. errors[i] (optional) draws a symmetric ±error bar on point i; smooth replaces straight segments with a Catmull-Rom curve; x_title / y_title add axis captions. title, width, height and theme are optional.

#
line_chart_multi

fn line_chart_multi(series : Array[(String, Array[(Double, Double)])], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render several named line series on shared axes with a legend, as a standalone SVG document string. series is a list of (name, points) pairs where each points is a list of (x, y). title, width, height and theme are optional.

#
mean

fn mean(values : Array[Double]) -> Double

Arithmetic mean of values; 0 for an empty list.

Example

test {
inspect(@mooncharts.mean([1.0, 2.0, 3.0, 4.0]), content="2.5")
}

#
median

fn median(values : Array[Double]) -> Double

Median of values; 0 for an empty list.

Example

test {
inspect(@mooncharts.median([3.0, 1.0, 2.0]), content="2")
inspect(@mooncharts.median([4.0, 1.0, 3.0, 2.0]), content="2.5")
}

#
num

fn num(x : Double) -> String

Format a Double as a compact SVG coordinate: rounded to at most two decimals, with trailing zeros and a redundant ".0" dropped (10.0 -> "10", 10.5 -> "10.5", 43.929 -> "43.93"). Manual formatting avoids the float artifacts that plain to_string can produce.

#
pie_chart

fn pie_chart(data : Array[(String, Double)], title? : String, width? : Double, height? : Double, donut? : Double, theme? : Theme) -> String

Render a pie chart (or a donut when donut is set to an inner-radius ratio in 0.0..1.0) as a standalone SVG document string. data is a list of (label, value) pairs; non-positive values are skipped in the ring but still shown in the legend. title, width and height are optional.

#
quartiles

fn quartiles(values : Array[Double]) -> (Double, Double, Double)

(q1, median, q3) quartiles of values using the median-split (Tukey) method: the lower/upper halves exclude the middle element when the count is odd. Returns (0, 0, 0) for an empty list.

Example

test {
let (q1, m, q3) = @mooncharts.quartiles([
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,
])
inspect(q1, content="2.5")
inspect(m, content="5")
inspect(q3, content="7.5")
}

#
radar_chart

fn radar_chart(axes : Array[String], series : Array[(String, Array[Double])], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a radar (spider) chart: each entry of axes becomes a spoke and each series in series is drawn as a closed polygon over the spokes. series is a list of (name, values) where values[i] is the value on axis i. title, width, height and theme are optional.

#
rose_chart

fn rose_chart(data : Array[(String, Double)], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a Nightingale rose chart as a standalone SVG document string. Each (label, value) gets an equal-angle sector whose radius is proportional to sqrt(value) (so sector area tracks the value). Concentric guide rings and a legend are drawn. title, width, height and theme are optional.

#
scatter_chart

fn scatter_chart(data : Array[(Double, Double)], errors? : Array[Double], x_title? : String, y_title? : String, title? : String, width? : Double, height? : Double, radius? : Double, theme? : Theme) -> String

Render a scatter plot of (x, y) points as a standalone SVG document string. The y value axis gets nicely rounded gridlines; the x domain is padded slightly so points never sit on the frame. title, width, height, the point radius and theme are optional.

#
sparkline

fn sparkline(values : Array[Double], width? : Double, height? : Double, dot? : Bool, theme? : Theme) -> String

Render a compact sparkline as a standalone SVG document string: a small polyline through values with an optional end-point dot. No axes, labels or padding beyond a thin margin, so it embeds inline. width, height, dot and theme are optional.

#
text

fn text(x : Double, y : Double, content : String, attrs : Array[(String, String)]) -> String

A <text> element anchored at (x, y) with escaped text content.

#
treemap

fn treemap(data : Array[(String, Double)], title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a treemap as a standalone SVG document string: each (label, value) becomes a cell whose area is proportional to its value, laid out with the squarified algorithm (cells stay close to square). Non-positive values are skipped. title, width, height and theme are optional.

#
waterfall_chart

fn waterfall_chart(data : Array[(String, Double)], show_total? : Bool, total_label? : String, up_color? : String, down_color? : String, title? : String, width? : Double, height? : Double, theme? : Theme) -> String

Render a waterfall chart as a standalone SVG document string. Each (label, delta) bar floats at the running total, colored up_color for gains and down_color for losses, with dashed connectors between steps. With show_total a final bar for the cumulative result is appended under total_label. title, width, height and theme are optional.