Phart
PHART: Python Hierarchical ASCII Rendering Tool - A Pure Python library and console CLI tool that renders graphs (NetworkX, GraphML, GraphViz/DOT) as diagrams/charts using 7-bit ASCII, (or 8-bit, Unicode, ANSI-color) or other plain-text output formats (SVG, HTML, Mermaid, etc.)
Install / Use
/learn @scottvr/PhartREADME
phart v1.5.0
PHART: The Python Hierarchical ASCII Representation Tool - A Pure Python tool for graph visualization via charts and diagrams rendered in ASCII.
New!
This 1.5.0 release is bigger than any single update in the two years phart has been in development.
I'll try to document all of the new features, but alas, I tend to get too wordy so I've trashed and reverted back the old README several times already. For deep dives into new topics, I'm creating separate docs in the docs directory in the repo.
Features (some pre-date v1.5.0 but hadn't been documented yet.)
New stuff is bolded.
- Render using ASCII (7-bit) or Unicode characters
- Optional ANSI color for either charset
- Multiple node styles (square, round, diamond, custom)
- Customizable edge characters
- Support for directed and undirected graphs
- Handles cycles and complex layouts
- Bidirectional edge support
- Edge attribute support (and attribute-based coloring of edges)
- Edge label rendering from edge
labelattributes - Over ten layout strategies
- Orthogonal edge paths (all 90 degree turns, "Manhattan" style)
- Node labels using multi-column character sets (such as CJK)
- Optional width/height pagination for text output
- Optional multiline node labels in bounding boxes
- mermaid flowchart, svg, and html source output
- pagination (horizontal and vertical) witth CLI page-selector support
- partitioning (horizontal and vertical - set the screen canvas size as a contraint, and adjust the layers (rank aands ) to fit within canvas constraints
- nodes and edges support arbitrary attribiutes now, not just label, color, etc)
- those attributes can be displayed as lbels on nodes and edges
- styling, coloring, etc based on attributes is now done with a single unified, simple, and flexible syntax.
- not that you asked for it, but connectors and panel headers can now be styled too
- and the phart 0.1.4 original node styling and edge styling is now fully realized.
- what's a panel header? Check out the new docs in the docs/ directory for deep dives architecturally
- the rest I'll try to touch on in this README
- docs/architecture/style-rules-spec.md
- docs/architecture/layout-partitioning-spec.md
Some details on new v1.5.0 stuff:
Label Synthesis and Multiline BBoxes
When node labels are enabled with --labels (or --node-labels), and a node does not define label, PHART can synthesize label text from ordered attribute paths:
phart --labels --bboxes --bbox-multiline-labels \
--node-label-lines name,birt.date,deat.date \
examples/gedcom.py
Notes:
name,birt.date,deat.daterenders those three values in order (multiline in bboxes when enabled).- You can also use dotted paths directly, such as
name,birt.date,deat.date.
Text Pagination
Pagination is available for --output-format text and is useful for wide/tall renders:
phart --labels --bboxes \
--paginate-output-width 100 \
--paginate-output-height 30 \
--page-x 1 --page-y 0 \
--list-pages \
examples/gedcom.py
Notes:
--paginate-output-width autoand--paginate-output-height autorequire terminal stdout.- Pagination is ANSI-aware: escape sequences are not counted toward visible width, and page slices preserve complete ANSI sequences.
Constrained Layout Panels and Partition Metadata
Constrained layout is different from output pagination: it partitions during layout/routing, then renders panelized output with connector cues between panels.
phart --layout layered --constrained \
--target-canvas-width 80 \
--target-canvas-height 24 \
--partition-overlap 1 \
--partition-affinity-strength 1 \
--panel-headers lineage \
--connector-ref label \
--connector-compaction partition \
examples/gedcom.py
Notes:
- Constrained mode currently supports
--layout auto|bfs|hierarchical|layered. --partition-affinity-strength 0disables split-affinity heuristics. Values greater than zero bias boundaries to keep close family/group relationships together.- Constrained splitting uses affinity-aware boundary selection; if no valid optimized split is found, it falls back to deterministic greedy splitting.
- If a single node cannot fit inside the target canvas width, that node is kept intact and the panel can overflow.
--target-canvas-width autoand--target-canvas-height autorequire terminal stdout.
Programmatic export of partition metadata:
import networkx as nx
from phart import ASCIIRenderer, LayoutOptions, NodeStyle
G = nx.DiGraph()
G.add_edges_from(
[("R", "A1"), ("R", "A2"), ("R", "A3"), ("A1", "B1"), ("A2", "B2")]
)
renderer = ASCIIRenderer(
G,
options=LayoutOptions(
node_style=NodeStyle.MINIMAL,
layout_strategy="layered",
constrained=True,
target_canvas_width=12,
partition_affinity_strength=1,
connector_compaction="partition",
connector_ref_mode="label",
),
)
print(renderer.render())
plan = renderer.get_partition_plan() # PartitionPlan | None
metadata = renderer.export_partition_metadata() # dict (schema_version=1.0)
print(metadata["partition_count"])
print(metadata["cross_partition_edges"][:2])
Why export metadata?
- Build your own panel index/navigation around constrained output.
- Assert deterministic partitioning in tests/CI.
- Compare effects of
partition_affinity_strength,partition_order, and overlap settings during tuning.
Of course, this is supported via the CLI as well.
Edge Glyph Presets and Arrow Styles
You can set global edge line-art and arrowhead style without per-glyph mapping:
phart --edge-glyph-preset thick --edge-arrow-style unicode your_graph.py
Full style-rule semantics and field reference: docs/architecture/style-rules-spec.md
Node decorators can also be driven by style rules:
phart --labels \
--style-rule 'node: sex=="F" -> prefix=(,suffix=)' \
--style-rule 'node: sex=="M" -> prefix=[,suffix=]' \
examples/gedcom.py
Style rules still win for keys they set:
phart --edge-glyph-preset thick \
--style-rule 'edge: role=="link" -> line_vertical=!,arrow_down=x' \
your_graph.py
Legacy note:
- Legacy global style fields continue to work.
- Style rules are the preferred per-node/per-edge customization path and take precedence for overlapping keys.
Compatibility / breaking-notes:
- Style-rule validation is strict: unknown
setkeys and wrong target/key combinations now fail fast with explicit errors. - Edge glyph rule values must be single-cell glyphs (multi-character and wide glyphs are rejected).
--edge-arrow-style unicodeis automatically coerced to ASCII when using ASCII charset mode.
mermaid output
flowchart TD is now a supported output. Read about it here.
TL;DR:
--output-format mmdalong with, optionally--output yourfile.mmd(or you can just redirect stdout with> yourfile.mdWill generate a Mermaidflowchart TDfrom your graph.
New Layout Strategies
See LAYOUT-STRATEGIES.md in the repo for some examples of output.
I have also documented one of the scripts in the examples/ directory and shown its output here in TRIADIC-CENSUS.md
Node ordering
--node-order {layout-default,preserve,alpha,natural,numeric}
Node ordering policy: layout-default (default), preserve, alpha, natural, or numeric
--node-order-attr NODE_ORDER_ATTR
Optional node attribute name to use as the ordering key
--node-order-reverse
The result of the sorting method used by the layout strategy will be reversed
Intended usage examples:
--node-order natural--node-order-attr label --node-order alpha--node-order-attr rank --node-order numeric--node-order alpha --node-order-reverse
Also added were:
--shared-ports {any,minimize,none}
Terminal port sharing policy: any (default), minimize (prefer unused points on the same face),
or none (avoid sharing until the node has no free terminal slots)
--bidirectional-mode {coalesce,separate}
How to render reciprocal directed edges: coalesce (default) draws one shared route with arrows
at both ends; separate draws each direction independently
Regarding shared_ports_mode, which was added to LayoutOptions in styles.py and exposed in the cli as --shared-ports {any,minimize,none}:
any: legacy compact behavior. Reuse within the local face pool is allowed once that local pool is exhausted. This is the default.minimize: avoid reuse on the same face by expanding to the rest of that face before sharing a terminal slot.none: do the minimize behavior, and also rebalance endpoints across other node faces so sharing is avoided until the node has no free terminal slots left anywhere.
When testing changes or debugging expected rendered graph output, I make use of variations on:
$ phart --shared-ports none --bidirectional-mode separate ...
$ phart --shared-ports minimize --bidirectional-mode separate ...
$ phart --shared-ports any --bidirectional-mode coalesce ...
# and so on...
Often, I'll combine those with variations on --hpad/--vpad, --layer/node-sizing and --colors ..., but hopefully unless you're adding a new layout strategy or other such enhancement, you aren't having to do a lot of puzzling about output. If you are, feel
