A PlantUML-style text DSL for hierarchical radial charts (sunbursts with opinions), rendered to SVG natively or PNG via resvg.
You describe root components and a hierarchy of weighted child components; each component takes up an angular share of its ring proportional to its weight, nested inside its parent's span. Labels curve along their segments and can face the centre, the rim, or auto-flip so they never read upside down.
@startradial
title Acme Engineering — Team Allocation
sizing fill
anchor top
align center
segment "Product" weight=45 {
segment "Web" weight=3
segment "Mobile" weight=2 {
segment "iOS" weight=1
segment "Android" weight=1
}
}
segment "Platform" weight=35 {
segment "Infra" weight=2
segment "Data" weight=1
}
group "Support" padding=5 {
segment "QA" weight=12
segment "Docs" weight=8
}
@endradial
pip install -e . # SVG output, no dependencies
pip install -e ".[png]" # + PNG output via resvg-pycirclechart examples/demo.rchart # writes examples/demo.svg
circlechart examples/demo.rchart -o out.png # PNG (needs [png] extra)
circlechart examples/demo.rchart -o out.png --scale 2The CLI is a thin wrapper over a pure text-in/bytes-out pipeline, so the same calls drop straight into a Docker service or FaaS handler:
from circlechart import parse_text, to_svg, svg_to_png, render
svg = render(source_text) # one call: DSL -> SVG string
png = render(source_text, "png", scale=2) # DSL -> PNG bytes
chart = parse_text(source_text) # or step by step / build the
svg = to_svg(chart) # Chart/Node model programmaticallyA diagram lives between @startradial and @endradial. Lines starting with
' or // are comments.
| Option | Values (default) | Meaning |
|---|---|---|
title |
text | Title above the chart |
theme |
light (default), dark |
Base colours and palette |
size |
px (640) |
Width/height of the drawing square |
margin |
px (24) |
Outer margin |
inner-radius |
px (50) |
Hub radius |
sizing |
equal (default), fill |
equal: every ring the same thickness. fill: branches scale ring thickness so every outermost segment reaches the rim |
anchor |
top (default), bottom, left, right, or degrees |
Where the first ring starts |
align |
start (default), center |
start: first segment starts at the anchor. center: first segment is centred on the anchor |
direction |
cw (default), ccw |
Winding direction |
gap |
px (2) |
Gap between angularly-adjacent segments. It is a constant linear width at every radius — each segment is inset by gap/2 perpendicular to its radial edges, so gaps never fan out with radius or pinch to a point, and they line up across rings. A segment that fills the whole circle is left as a continuous ring (no seam). |
radial-gap |
px (2) |
Gap between rings |
background |
colour | Overrides the theme background |
frames |
on (default), off |
off disables group frames; a group's fill then cascades to its children so colour alone indicates the grouping |
segment "Name" key=value ... { ... children ... }
group "Name" padding=6 fill=#26262c { ... segments ... }
weight=N— angular share relative to siblings (default 1;45%also accepted, treated as a relative number). Sibling order in the file is the adjacency order on the ring.- A group does not occupy a ring: its children sit on the ring the group's
siblings occupy, and the group draws a background frame (an annulus
sector) behind its whole subtree. Frames never disturb the layout: segment
angles and gaps are identical with or without them — the frame widens into
the ordinary sibling gap.
paddingcontrols the frame's radial overhang. The group'sfill/line/corner-radius/shadowstyle the frame; its text/font/label attributes cascade to its children. The group name is drawn curved along the outside of the frame. Withframes offno frame is drawn and the group'sfillcascades to its children instead.
By default the hub (inside inner-radius) is empty. A center block fills it
with a flat pie of weighted wedges — centre is accepted too:
center {
segment "Build" weight=3
segment "Run" weight=2
segment "Govern" weight=1
}
Centre segments take the usual style attributes but no children or groups;
layer 0 sets their ring defaults. They honour anchor/align/direction.
Palette colours are assigned centre-first, in file order. A chart with only a
center block and no rings is simply a pie chart that fills the whole radius.
The wedges are inset by the same constant gap / 2 used everywhere else in the
chart, so the centre gaps match the ring gaps and hold their width almost all
the way in; the inset naturally tips each wedge in just shy of the exact centre,
so the gaps converge to the middle with no blank hub dot. Set gap 0 for a
classic gapless pie.
Usable on segments, on layer <n> ... lines (per-ring defaults), and at chart
level. Cascade precedence: segment > inherited from ancestors > layer >
chart > theme.
| Attribute | Values | Notes |
|---|---|---|
fill |
colour | Root segments without a fill get palette colours; descendants inherit the nearest ancestor fill, lightened by shade per generation |
shade |
0..1 (0.16) |
Per-generation lightening of inherited fills (0 = inherit exactly) |
line, line-width |
colour, px | Segment outline (theme default: background colour, width 2 — gives the clean separation look) |
text |
colour | Label colour; unset = automatic black/white for contrast |
font-family, font-size |
(13) |
Labels fit themselves automatically: names wrap at spaces (or at explicit \n breaks in the quoted name) whenever wrapping buys a larger font, shrink when needed, rotate 90° in angularly-thin segments (auto/radial facing), and hide below ~6.5px as a last resort |
bold, italic |
on/off |
|
labels |
out, in, auto (default), radial, none |
out: glyph tops toward the rim (reads upright at 12 o'clock). in: tops toward the centre (upright at 6 o'clock). auto: flips per segment so text never reads upside down, and rotates to radial when the segment is too thin for curved text. radial: force text to run along the radius. Inherited like any style |
corner-radius |
px | Rounded segment corners |
shadow |
on/off |
Drop shadow |
layer 2 thickness=1.3 labels=out font-size=11
layer <n> sets defaults for every segment on ring n (root = 1);
thickness is a relative ring-thickness weight used when sizing equal.
- examples/demo.rchart — the basic capabilities.
- examples/showcase.rchart — dark theme, shadows, layer overrides, explicit colours, groups, 6-o'clock anchor.
- examples/center.rchart — a centre pie inside the hub with rings around it.
pip install -e ".[test]"
pytest- Angles are degrees in SVG screen space (0° = 3 o'clock, clockwise); 12 o'clock is −90°.
- The pipeline is
parse_text -> Chart -> build_layout -> Layout -> render_layout -> SVG; every stage is a pure function over dataclasses, so any stage can be driven programmatically. - In strict-sunburst semantics a child ring always subdivides its parent's
angular span, so
anchor/alignvisibly affect ring 1; deeper rings are constrained by their parents. - The default palettes are pre-validated for colour-vision-deficiency-safe adjacency in both light and dark themes; slot order is part of that validation.
See docs/plantuml-proposal.md for a sketch of how this could land in PlantUML itself.
MIT — see LICENSE.
