Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

circlechart

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

demo

Install

pip install -e .            # SVG output, no dependencies
pip install -e ".[png]"     # + PNG output via resvg-py

CLI

circlechart 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 2

Python API

The 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 programmatically

Language reference

A diagram lives between @startradial and @endradial. Lines starting with ' or // are comments.

Chart options (one per line, key value)

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

Segments and groups

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. padding controls the frame's radial overhang. The group's fill/line/corner-radius/shadow style the frame; its text/font/label attributes cascade to its children. The group name is drawn curved along the outside of the frame. With frames off no frame is drawn and the group's fill cascades to its children instead.

Centre

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.

Style attributes

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

Layers

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

Tests

pip install -e ".[test]"
pytest

Design notes

  • 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/align visibly 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.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages