@bardioc/org-chart
v0.1.0
Published
Configurable, read-only React org chart. Every visual aspect is driven by a single config object.
Keywords
Readme
@bardioc/org-chart
A read-only, deeply configurable React org chart. Every visual — canvas, node, connector, connector label, chrome — is driven by one config object, so the same component covers a bare name-and-hairline diagram and a fully dressed gradient-and-avatar chart.
In dev mode the repo is an experimental lab: a live control panel that edits every config field against three sample datasets.
npm install
npm run dev # lab at http://localhost:5273
npm run build # publishable library into dist/
npm run build:lab # static build of the labThe lab keeps your configuration and any custom presets in localStorage
(zustand + persist), so a reload restores the design you were working on. The
Preset tab holds the editor: save/rename/delete custom presets, export and
import them as JSON, and paste a config in directly — either as a patch over the
defaults (what you would put in the config prop) or as the fully resolved
object. Dataset and filters deliberately start fresh on every load.
Scope
Visualisation only. There is no dragging, no inline editing, no forms. Nodes support hover, selection, collapse/expand, search dimming and pan/zoom.
Usage
import { OrgChart } from '@bardioc/org-chart';
import '@bardioc/org-chart/styles.css';
<OrgChart
data={people} // flat list with parentId, or a nested root
config={{ layout: { orientation: 'left-right' } }}
/>;The library is data-model agnostic. Point it at any object shape via accessors, and reference arbitrary fields from the node config:
<OrgChart
data={employees}
accessors={{ id: 'employeeId', parentId: 'managerId', kind: 'type' }}
config={{
node: {
fields: [
{ key: 'name', value: '{firstName} {lastName}' },
{ key: 'title', value: 'jobTitle' },
{ key: 'email', visible: false },
],
},
}}
/>Field value is either a dot path (profile.jobTitle) or a template
({firstName} {lastName}).
Filtering by kind
filter keeps only the nodes it accepts and re-parents the survivors onto the
nearest kept ancestor, so hiding every org unit collapses the chart down to a
pure reporting line instead of tearing the hierarchy apart:
const onlyPeople = useCallback((d: Person) => d.kind === 'employee', []);
<OrgChart data={people} filter={onlyPeople} />;Memoise the predicate — a new function identity re-runs the layout.
Refitting after a swap
Pass fitKey; when it changes the chart refits once the layout settles. This is
more reliable than calling fitView() after changing data, because the request
is registered during render and cannot race the measure pass:
<OrgChart data={data} fitKey={`${datasetId}|${presetId}`} />Configuration
config is a deep-partial of OrgChartConfig, merged over the defaults.
| Group | Covers |
| --- | --- |
| canvas | fill (solid/gradient), pattern (dots, grid, cross, lines), frame, vignette, fit padding, cursors |
| layout | mode (tree or radial), orientation (4), fixed vs measured sizing, sibling/subtree/level/root gaps, per-level or uniform rows, parent alignment, leaf alignment, sibling sorting, indented leaf stacking |
| layout.radial | angular window (start/end), inner radius, ring gap, centred root, node and subtree arc gaps, fill-the-window, card rotation, ring growth cap |
| node | fill, border, shadow, padding, accent bar or ring, avatar (initials/image/icon), ordered content rows, divider, descendant-count badge, collapse toggle, hover/selected/dimmed states |
| edge | path shape, corner radius, curvature, shared trunk position, stroke, dash, flow animation, gradient, width-by-subtree-size, start/end markers, labels, hover/dimmed states |
| controls | visibility, corner, direction, which buttons, zoom readout |
| minimap | visibility, corner, size, colours, interactivity |
| interaction | pan/zoom rules, fit behaviour, collapsibility, initial depth, selection, path highlighting, dimming (by ancestors or by neighbours), tooltip |
Colours are ColorSource values, resolved per node — fixed, by kind, by
depth, or read from a datum field. Defaults reference the shadcn/bardioc CSS
custom properties (var(--card), var(--chart-1), …), so the chart follows the
host theme in light and dark with no extra wiring.
node.byKind / node.byDepth (and the edge equivalents) layer scoped overrides
on top of the base config.
Presets
PRESETS exports eight ready-made configs, each carrying a category
(tree or radial) and tags so a picker can group them: default, minimal,
maximal, blueprint, roster, neon, orbit and graph. Use them directly
or as a starting point:
import { OrgChart, PRESET_BY_ID } from '@bardioc/org-chart';
<OrgChart data={people} config={PRESET_BY_ID.get('minimal')!.config} />;Custom nodes
<OrgChart
data={people}
config={{ node: { variant: 'custom' } }}
renderNode={({ node, isSelected }) => <MyCard datum={node.datum} active={isSelected} />}
/>Layout still measures the rendered element, so custom nodes of varying size do not overlap.
Imperative handle
const chart = useRef<OrgChartHandle>(null);
chart.current?.fitView();
chart.current?.centerOnNode('user-42', 1);
chart.current?.collapseAll();fitView keeps re-fitting until the measure/relayout cycle settles, so it is
safe to call immediately after changing config or data. toSVG() returns
standalone SVG markup — real edge geometry plus a box and primary label per
node; rich card styling is not reproduced.
How it works
- Layout is a tidy tree via
d3-hierarchy. Cross-axis units are pixels andseparationreturns the required centre-to-centre distance, which is what makes variable-width nodes overlap-free. Multiple roots hang off a virtual root that is dropped afterwards. - Radial mode maps depth to a ring. A node's angular footprint depends on
its radius, so
separationreturnsrequiredArc / radius; when the inner rings still cannot fit their children, the ring spacing is grown iteratively (capped bymaxRingGrowth) rather than compressing nodes into each other. A full 360° window is treated as cyclic, so the first and last node keep a gap. Note that ring spacing has to clear the card's width, because near the left and right of the circle the radial direction runs horizontally — narrow cards, orrotateNodes, are what keep a radial chart compact. - Sizing defaults to
measured: nodes report their box from a layout effect and the tree is recomputed before paint.fixedignores the measurement cache instead of clearing it, so toggling back does not re-measure from scratch. The cache is deliberately never invalidated on config change — a ResizeObserver already tracks the truth, and clearing used to race with the child's own report, stranding the layout on the fallback size. - Edges are SVG paths in a layer behind (or above) the nodes. Sibling elbows
share a trunk coordinate so they bend on exactly the same line;
forkshares one trunk per depth, giving the classic bus even when parent cards differ in height. Labels are HTML, positioned along the path polyline. - Pan/zoom is
d3-zoomapplied to a transformed wrapper. A click that ends a drag is swallowed, so panning never changes the selection.
Migration note
The sample dataset in src/lab/sample-data.ts is the Sunshine Company org from
the previous org-builder React Flow chart, re-expressed in this package's
generic flat shape. The domain types (Organization, OrganizationalUnit,
Team, User) are deliberately not baked into the library.
