<bmx-org-chart>
A hierarchy drawn as a diagram, with the curved connectors people know.
8 properties · 2 events · 4 methods · 9 parts
Example
children array is the prettier literal and would make every
consumer convert their data on each render, so this component takes one shape rather than both.
initial-depth opens the first generations and leaves the rest behind a counter, because a chart
that opens with eleven hundred boxes on it has answered no question anybody had.
tree rather than a diagram with sixteen tab stops in it.
aria-hidden and the boxes are a real nested tree reporting aria-level,
aria-posinset, aria-setsize and aria-expanded. A relationship drawn in a
hidden layer has to be stated somewhere that is not hidden.
orientation="horizontal" runs the generations left to right, which is what a deep, narrow
hierarchy wants: a chart eleven levels deep and three wide is unreadable drawn downwards and obvious drawn
across. The connectors' anchors follow — they are chosen by the direction of travel rather than fixed to
the bottom and top edges — and under RTL the whole thing mirrors, curves included, because an SVG path
does not flip with a logical property.
node-width keeps the boxes uniform,
which is what makes a chart tidy, and the component writes it into its own CSS so the box drawn and the box
laid out are the same box. The height is measured off the rendered nodes instead, because your font, your
padding and a job title that wraps to two lines all change it — and a layout given a height the boxes do
not have draws them overlapping. Everything about how a node looks is CSS: part="node",
part="node-label", part="node-detail" and a dozen custom properties.
Show markup
<bmx-org-chart
label="Who reports to whom"
initial-depth="2"
nodes='[
{"id":"ceo","label":"Ada Lovelace","detail":"Chief executive"},
{"id":"cto","parentId":"ceo","label":"Grace Hopper","detail":"Technology"},
{"id":"cfo","parentId":"ceo","label":"Alan Turing","detail":"Finance"},
{"id":"eng","parentId":"cto","label":"Katherine Johnson","detail":"Engineering"},
{"id":"data","parentId":"cto","label":"Dorothy Vaughan","detail":"Data platform"},
{"id":"eng1","parentId":"eng","label":"Annie Easley"},
{"id":"eng2","parentId":"eng","label":"Melba Roy Mouton"},
{"id":"fin1","parentId":"cfo","label":"Edith Clarke","detail":"Controller"}
]'
></bmx-org-chart>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
The list is <strong>flat, with parent references</strong> — which is what a SQL result, an HR export or a
Razor model already is. A nested <code>children</code> array is the prettier literal and would make every
consumer convert their data on each render, so this component takes one shape rather than both.
<code>initial-depth</code> opens the first generations and leaves the rest behind a counter, because a chart
that opens with eleven hundred boxes on it has answered no question anybody had.
</span>
</div>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
<strong>Tab into it once, then use the arrows.</strong> <kbd>↓</kbd> and <kbd>↑</kbd> walk the people
in reading order; <kbd>→</kbd> opens a closed branch and, on an open one, steps into it;
<kbd>←</kbd> closes an open branch and, on anything else, goes up to the manager; <kbd>*</kbd> opens a
whole generation; and typing a name jumps to it. That is the tree pattern every file explorer already taught
the reader, and it is why this is a <code>tree</code> rather than a diagram with sixteen tab stops in it.
</span>
</div>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
<strong>The connectors are decoration and the nesting is the structure.</strong> There is no role for
“boxes joined by curves”, and nobody can follow a line with their ears — so the SVG layer is
<code>aria-hidden</code> and the boxes are a real nested tree reporting <code>aria-level</code>,
<code>aria-posinset</code>, <code>aria-setsize</code> and <code>aria-expanded</code>. A relationship drawn in a
hidden layer has to be stated somewhere that is not hidden.
</span>
</div>
<div class="row" style="margin-block-start: 1.5rem">
<bmx-org-chart
label="The same people, sideways"
orientation="horizontal"
node-width="150"
nodes='[
{"id":"ceo","label":"Ada Lovelace","detail":"Chief executive"},
{"id":"cto","parentId":"ceo","label":"Grace Hopper"},
{"id":"cfo","parentId":"ceo","label":"Alan Turing"},
{"id":"eng","parentId":"cto","label":"Katherine Johnson"},
{"id":"data","parentId":"cto","label":"Dorothy Vaughan"}
]'
></bmx-org-chart>
</div>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
<code>orientation="horizontal"</code> runs the generations left to right, which is what a deep, narrow
hierarchy wants: a chart eleven levels deep and three wide is unreadable drawn downwards and obvious drawn
across. The connectors' anchors follow — they are chosen by the direction of travel rather than fixed to
the bottom and top edges — and under RTL the whole thing mirrors, curves included, because an SVG path
does not flip with a logical property.
</span>
</div>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
<strong>Width is declared and height is measured.</strong> <code>node-width</code> keeps the boxes uniform,
which is what makes a chart tidy, and the component writes it into its own CSS so the box drawn and the box
laid out are the same box. The height is measured off the rendered nodes instead, because your font, your
padding and a job title that wraps to two lines all change it — and a layout given a height the boxes do
not have draws them overlapping. Everything about how a node <em>looks</em> is CSS: <code>part="node"</code>,
<code>part="node-label"</code>, <code>part="node-detail"</code> and a dozen custom properties.
</span>
</div>
<bmx-org-chart
label="Who reports to whom"
nodes='[{"id":"1","label":"Ada Lovelace","detail":"Chief executive"},
{"id":"2","parentId":"1","label":"Grace Hopper"}]'
></bmx-org-chart>
THE CONNECTORS ARE THE COMPONENT, AND THEY ARE GEOMETRY
Almost nothing here is DOM. Where every box goes is Reingold and Tilford's
tidy-tree algorithm - Buchheim's linear version of it - and every connector
is a cubic between two measured anchors; all of it is src/core/orgchart.ts,
pure, with fifty unit tests, because a layout that can only be looked at
cannot be checked. What is left for this file is what only a browser settles:
how tall a node really is once a consumer has restyled it, where the focus
goes, and what the tree says about itself.
A TREE, WITH THE DIAGRAM BEHIND IT
This is the fifth component in this library whose shape was decided by what
the accessibility tree is told rather than by what the thing looks like, and
it is the clearest case of the five. On screen it is a diagram: boxes in two
dimensions joined by curves. To a screen reader a diagram is nothing at all -
there is no role for "boxes joined by curves", and a reader cannot follow a
line with their ears. What the drawing means is a hierarchy, and a
hierarchy has a role: tree, whose items nest, report their level and open
and close with the arrow keys every file explorer already taught the reader.
So the SVG layer is aria-hidden decoration and the boxes are a real nested
tree - the DOM carries the hierarchy and CSS carries the positions, with
the two answering to the same layout. A connector is never the only place
a relationship is stated, which is the lesson defect 59 charged the
calendar for: something drawn in a hidden layer has to be said somewhere
that is not hidden, and here the nesting says it.
WIDTH IS DECLARED AND HEIGHT IS MEASURED
Uniform box widths are what makes a chart tidy, so the width is a property
and the component writes it into the CSS. Height is the opposite: a job title
that wraps to two lines, a larger font, a consumer's own padding all change
it, and a layout using a height the boxes do not have draws them overlapping.
So the tallest rendered node is measured and fed back in - the discipline
bmx-toolbar established, and §6c item 5's rule about never asking
getComputedStyle for a custom property.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
initialDepth |
initial-depth |
number |
— | How many generations to open on first render. A chart that opens with eleven hundred boxes on it has answered no question anybody had. Left unset, everything is open except the nodes that said collapsed themselves. |
label |
label |
string |
— | What the tree is called. Without one it is announced as "Organisation chart". |
levelGap |
level-gap |
number |
56 |
The gap between one generation and the next, in pixels. |
nodeHeight |
node-height |
number |
72 |
The least a box may be, in pixels. A floor rather than a height: the real height is measured, because a consumer's font, padding or a job title that wraps to two lines all change it and a layout that used this number regardless would draw the boxes overlapping. |
nodeWidth |
node-width |
number |
176 |
How wide a box is, in pixels. A layout input rather than a styling one, and the distinction matters because this library does not take @Prop()s for looks. The arithmetic cannot run without a number, and it cannot read one out of a custom property - getComputedStyle hands those back unresolved, which is §6c item 5 and cost bmx-select a build cycle. So the number comes in here and the component writes it into the CSS, which keeps the box that is drawn and the box that was laid out the same box by construction. Everything about how a node looks is still CSS: see the parts below. |
nodes |
nodes |
BmxOrgNode[] | string |
[] |
The people, as a flat list. Read through src/core/markup.ts, so nodes='[{...}]' written in a Razor page, a Blazor component or plain HTML works exactly as an assignment from React does - which is defect 45's whole subject. |
orientation |
orientation |
BmxOrgOrientation |
'vertical' |
Which way the generations run. |
siblingGap |
sibling-gap |
number |
24 |
The gap between two boxes side by side, in pixels. |
Events
| Event | Detail | Description |
|---|---|---|
bmxSelect |
BmxOrgChartSelectDetail |
A node was chosen - clicked, or Enter or Space on it. |
bmxToggle |
BmxOrgChartToggleDetail |
A branch was opened or closed. |
Methods
| Method | Signature | Description |
|---|---|---|
collapseAll |
collapseAll() => Promise<void> |
Close every branch that has anything in it. |
expandAll |
expandAll() => Promise<void> |
Open every branch. |
focusNode |
focusNode(id: string) => Promise<void> |
Put the tab stop on a node, and the focus with it. |
toggle |
toggle(id: string, expanded?: boolean) => Promise<void> |
Open or close one branch by id. |
Slots
| Slot | Description |
|---|---|
empty |
what to draw when there are no nodes at all. |
CSS shadow parts
| Part | Description |
|---|---|
canvas |
|
empty |
|
node-count |
|
node-detail |
|
node-handle |
|
node-label |
|
tree |
|
wire |
|
wires |
CSS custom properties
| Property | Description |
|---|---|
--bmx-org-node-background |
The box's fill. |
--bmx-org-node-border |
Its border. |
--bmx-org-node-color |
Its text. |
--bmx-org-node-height |
The least a box may be. From node-height. |
--bmx-org-node-radius |
Its corner radius. |
--bmx-org-node-width |
How wide a box is. Written by the component from node-width, so the box drawn and the box laid out are the same box. |
--bmx-org-wire-color |
The connectors. |
--bmx-org-wire-width |
How thick they are. @part canvas - the whole drawing area. @part wires - the SVG layer of connectors. @part wire - one connector. @part tree - the role="tree" that holds the boxes. @part node - one box. Also node-branch or node-leaf, and node-<category>. @part node-label - the name inside it. @part node-detail - the second line. @part node-handle - the plus or minus that opens the branch. @part node-count - the +6 on a closed branch. @part empty - what is drawn when there is nothing to chart. |