svgraph is a pure Duso module for generating scalable vector graphics (SVG) charts. Create beautiful, publication-quality charts directly from Duso scripts without external dependencies. Charts render to standalone SVG files that work in any browser or editor.
sg = require("svgraph")
// Generate a line chart
svg = sg.render({
type = "line",
title = "Temperature Over Week",
axis = ["Days", "Temperature (°F)"],
data = [65, 68, 72, 70, 75, 78, 76]
})
save("chart.svg", svg)
All charts are created with a single function: render(spec). The function takes a specification object and returns SVG markup as a string.
The spec parameter is an object with the following fields:
| Field | Required | Type | Description |
|---|---|---|---|
type |
Yes | string | Chart type: "line", "bar", "hbar", "scatter", "bubble", "area", "multi", or "donut" |
data |
Yes | array | Chart data (format depends on type) |
title |
No | string | Chart title displayed at top |
axis |
No | array | Axis labels [x_label, y_label] |
labels |
No | array | X-axis tick labels, parallel to data. Thinned automatically so they don’t overprint; a short array labels only the points it covers |
width / height |
No | number | Canvas size. Default 1200 × 800 |
margin |
No | object | {top, right, bottom, left}; any subset |
background |
No | string | Backing rect color, or "none" to omit it |
ink |
No | string | Axes, ticks and text |
series |
No | string or array | One color, or a palette cycled across series |
font_family |
No | string | |
font_size |
No | number | Label size; the title scales from it |
line_width / dot_size |
No | number | |
ticks |
No | number | Approximate y-axis tick count. Default 5 |
zero |
No | boolean | Force the value axis to include 0. Defaults true for bar, hbar and area |
bare |
No | boolean | Drop all chrome — no title, axes, ticks or margins. For sparklines |
Anywhere label/value pairs are accepted, objects work too, so callers with structured data don’t have to flatten it by hand:
sg.render({type = "bar", data = [{label = "Q1", value = 100}, {label = "Q2", value = 150}]})
Scatter takes {x =, y =} and bubble {x =, y =, size =} the same way.
Plot values as a continuous line with markers at each point.
Data Format: Array of numbers
Use Cases: Time series, trends, continuous measurements
sg = require("svgraph")
line_chart = sg.render({
type = "line",
title = "Stock Price",
axis = ["Week", "Price ($)"],
data = [100, 105, 102, 110, 115, 120, 118]
})
save("stock.svg", line_chart)
Tips:
Display categorical data as vertical bars.
Data Format: Alternating label-value pairs: [label1, value1, label2, value2, ...]
Use Cases: Category comparisons, sales by quarter, discrete measurements
bar_chart = sg.render({
type = "bar",
title = "Sales by Quarter",
axis = ["Quarter", "Sales ($)"],
data = ["Q1", 100000, "Q2", 150000, "Q3", 120000, "Q4", 180000]
})
save("sales.svg", bar_chart)
Features:
Display categorical data as horizontal bars (useful for many categories or long labels).
Data Format: Alternating label-value pairs: [label1, value1, label2, value2, ...]
Use Cases: Project progress, survey results, rankings
hbar_chart = sg.render({
type = "hbar",
title = "Project Progress",
axis = ["Project", "% Complete"],
data = ["Frontend", 85, "Backend", 75, "Database", 90, "Testing", 60, "Docs", 45]
})
save("progress.svg", hbar_chart)
Display individual (x, y) points to reveal correlations and distributions.
Data Format: Alternating x-y pairs: [x1, y1, x2, y2, x3, y3, ...]
Use Cases: Correlation analysis, outlier detection, bivariate relationships
scatter = sg.render({
type = "scatter",
title = "Study Hours vs Grade",
axis = ["Hours Studied", "Grade (%)"],
data = [2, 55, 3, 60, 5, 75, 6, 85, 7, 82, 8, 90, 10, 95]
})
save("correlation.svg", scatter)
Features:
Like a line chart, but with the area under the line filled in.
Data Format: Array of numbers (same as line chart)
Use Cases: Cumulative values, stacked measurements, total amount over time
area_chart = sg.render({
type = "area",
title = "Cumulative Revenue",
axis = ["Month", "Total Revenue ($)"],
data = [10000, 25000, 18000, 35000, 42000]
})
save("revenue.svg", area_chart)
Features:
Scatter plot where bubble size represents a third dimension.
Data Format: Triplets of x, y, size: [x1, y1, size1, x2, y2, size2, ...]
Use Cases: Market analysis (price vs rating vs volume), portfolio analysis, multidimensional comparisons
bubble = sg.render({
type = "bubble",
title = "Product Market Analysis",
axis = ["Price ($)", "Customer Rating"],
data = [
10, 3.5, 100, // Product A: $10, 3.5 stars, 100 units
25, 4.2, 250, // Product B: $25, 4.2 stars, 250 units
15, 3.8, 180, // Product C: $15, 3.8 stars, 180 units
40, 4.7, 320 // Product D: $40, 4.7 stars, 320 units
]
})
save("products.svg", bubble)
Features:
Plot multiple series on the same chart for comparison.
Data Format: Array of arrays (one per series): [[series1_val1, series1_val2, ...], [series2_val1, ...], ...]
Use Cases: Multi-stock comparison, resource usage over time, multiple metrics
multi = sg.render({
type = "multi",
title = "Stock Price Comparison",
axis = ["Week", "Price ($)"],
data = [
[100, 105, 102, 110, 115, 120, 118], // Company A
[50, 52, 51, 55, 58, 62, 60], // Company B
[200, 198, 205, 210, 208, 215, 220] // Company C
]
})
save("stocks.svg", multi)
Requirements:
Color Scheme:
Pie-like chart showing proportions of a whole.
Data Format: Alternating label-value pairs: [label1, value1, label2, value2, ...]
Use Cases: Market share, budget allocation, traffic sources, composition analysis
donut = sg.render({
type = "donut",
title = "Market Share by Competitor",
data = [
"Company A", 35,
"Company B", 28,
"Company C", 22,
"Company D", 10,
"Others", 5
]
})
save("market.svg", donut)
The ring is drawn as a fraction of the shorter canvas side, so a wide
canvas gives a small ring in a lot of empty space rather than a wide one.
donut_size is that fraction, as an outer diameter:
donut = sg.render({
type = "donut",
data = ["Desktop", 107, "Mobile", 69, "Tablet", 13],
width = 1000,
height = 800,
donut_size = 0.6, // default 0.375
donut_label_gutter = 0.22, // default 0.117
donut_gap = 0 // default 3 degrees
})
donut_gap is the separation between slices, in degrees; 0 gives a
continuous ring, with adjacent slices sharing their arc endpoints inner and
outer. Slices are separated by angle only — every arc is struck about the one
chart centre, so the ring is a true annulus and the gaps are radial. Note the
gap is an angle, so it widens in pixels as donut_size grows: a ring that
looks lightly segmented at the default size can read as heavily split once
enlarged.
Labels sit outside their slice on a leader line, never inside it. Text drawn on a slice has to contrast with that slice’s own color, which differs per series and again between light and dark themes — there is no single ink that works against all of them.
Nothing here measures text, because there are no font metrics. Where a leader
label looks like it will overrun its gutter, textLength is set so the
renderer fits the glyphs to the space instead of letting them run off the
canvas. The check that decides this is an estimate, so it errs toward
applying textLength when it wasn’t strictly needed — a slightly condensed
label rather than a clipped one.
Every slice and bar carries a <title> child, so browsers show a tooltip
naming the item, its value and — for donuts — its share. Pair it with CSS on
the sg- classes for highlight-on-hover, no script involved:
.sg-slice:hover { filter: brightness(1.12); }
Light the hovered mark rather than dimming its neighbours: dimming reads as the whole chart reacting to the pointer, which is distracting to move across.
Labels sit donut_label_gutter × width in from each edge and are anchored
outward, so text wider than that gutter runs off the canvas and is
clipped. Names alone fit the default; names carrying values —
"Desktop 107" — need it raised. Between them, donut_size and
donut_label_gutter split the canvas: the ring wants the middle, the
labels want the edges, and pushing either too far collides with the other.
Features:
svgraph works seamlessly with generated and transformed data:
sg = require("svgraph")
// Generate Fibonacci sequence
fib = []
a = 1
b = 1
for i = 1, 10 do
push(fib, a)
temp = a + b
a = b
b = temp
end
// Chart the generated data
chart = sg.render({
type = "line",
title = "Fibonacci Sequence",
axis = ["Position", "Value"],
data = fib
})
save("fibonacci.svg", chart)
Transform structured data into chart format:
sg = require("svgraph")
// Structured data
sales_data = [
{month = "Jan", revenue = 50000},
{month = "Feb", revenue = 65000},
{month = "Mar", revenue = 58000},
{month = "Apr", revenue = 75000},
{month = "May", revenue = 82000}
]
// Transform to flat array for bar chart
flat_data = []
for data_point in sales_data do
push(flat_data, data_point.month)
push(flat_data, data_point.revenue)
end
chart = sg.render({
type = "bar",
title = "Monthly Revenue",
axis = ["Month", "Revenue ($)"],
data = flat_data
})
save("revenue.svg", chart)
Generate multiple charts in a loop:
sg = require("svgraph")
periods = [
{name = "Q1", values = [10, 15, 12]},
{name = "Q2", values = [18, 22, 19]},
{name = "Q3", values = [25, 28, 24]},
{name = "Q4", values = [32, 35, 30]}
]
for period in periods do
chart = sg.render({
type = "line",
title = "Performance: " + period.name,
axis = ["Week", "Score"],
data = period.values
})
save("period_" + lower(period.name) + ".svg", chart)
print("Created: period_" + lower(period.name) + ".svg")
end
svgraph handles edge cases gracefully:
sg = require("svgraph")
// Single data point
single = sg.render({
type = "line",
title = "Single Value",
data = [42]
})
// Constant values (flat line)
flat = sg.render({
type = "line",
title = "Constant",
data = [50, 50, 50, 50, 50]
})
// Large numbers (auto-scales axes)
large = sg.render({
type = "bar",
title = "Large Numbers",
data = ["A", 1000000, "B", 2500000, "C", 1800000]
})
// Very small decimal numbers
small = sg.render({
type = "scatter",
title = "Precision",
axis = ["X", "Y"],
data = [0.001, 0.002, 0.003, 0.001, 0.005, 0.003]
})
All edge cases render correctly without special handling needed.
The render() function returns valid SVG markup. Save it directly to a file:
sg = require("svgraph")
svg = sg.render({type = "line", data = [1, 2, 3, 4, 5]})
save("chart.svg", svg)
SVG files are:
Default: 1200 × 800, overridable with width and height.
Only the ratio matters for display. The SVG carries a viewBox and no
width/height attributes, so it scales to whatever box it’s given —
which means the canvas should match the shape of the container it lands
in. Declaring a height in CSS as well as here is what produces
letterboxing; pick the shape once, on the canvas, and let CSS set width
with height: auto.
Every element carries a class, so a page can restyle a whole chart in CSS without touching the markup. Colors are emitted as presentation attributes, which lose to any CSS rule, so a stylesheet always wins:
| Class | Element |
|---|---|
sg |
the root <svg> |
sg-bg |
backing rect |
sg-title |
title text |
sg-axis |
axis lines |
sg-tick / sg-tick-label |
tick marks and their text |
sg-axis-label |
axis names |
sg-series / sg-series-N |
lines; N is the series index |
sg-area |
area fill |
sg-point |
data point markers |
sg-bar |
bars |
sg-slice |
donut segments |
sg-label / sg-leader / sg-leader-dot |
donut labels and pointers |
.sg-series { stroke: var(--primary); }
.sg-area { fill: var(--primary); }
.sg-axis, .sg-tick { stroke: var(--border); }
.sg-tick-label { fill: var(--text-muted); }
This is the way to theme a chart on a page that has light and dark modes:
render with background = "none" so the card behind it shows through,
and let the tokens do the rest.
Color options (ink, series) accept currentColor. They do not
accept var(--x) — CSS variables don’t resolve inside SVG presentation
attributes. Use the classes above for that.
The module validates data and throws clear errors:
try
// Missing data field
sg.render({type = "line"})
catch (e)
print("Error: " + e) // "Error: Need: line|bar|..."
end
try
// Wrong data format for bar chart
sg.render({type = "bar", data = [1, 2, 3]})
catch (e)
print("Error: " + e) // "Error: Need label,value pairs"
end
try
// Bubble chart needs triplets
sg.render({type = "bubble", data = [1, 2, 3, 4]})
catch (e)
print("Error: " + e) // "Error: Need x,y,size triplets"
end
svgraph generates charts efficiently:
See the contrib/svgraph/examples/ directory for complete, runnable examples:
Run any example:
duso contrib/svgraph/examples/basic.du
Use svgraph in Claude-powered workflows to automatically visualize AI analysis:
claude = require("claude")
sg = require("svgraph")
// Claude analyzes data and returns numeric results
response = claude.prompt("Analyze these sales numbers...")
data = parse_json(response)
// Visualize the results
chart = sg.render({
type = "bar",
title = "Analysis Results",
data = data
})
save("analysis.svg", chart)
print("Visualization saved to analysis.svg")
Q: Can I customize colors and fonts?
A: The module uses fixed styling for simplicity. Modify contrib/svgraph/svgraph.du to customize the style() function.
Q: How many data points can I chart? A: Thousands of points work fine. For millions of points, consider aggregation.
Q: Can I embed the SVG in HTML?
A: Yes! Either embed the SVG directly or reference it: <img src="chart.svg"> or <embed src="chart.svg">
Q: What if my data has missing values? A: Pass valid numbers only. Use Claude or Duso functions to preprocess data and handle missing values before charting.
Q: Can I animate SVG charts?
A: SVG supports CSS animations. Edit the output SVG to add <style> tags with keyframes.