v1.0.0

<bmx-org-chart>

A hierarchy drawn as a diagram, with the curved connectors people know.

8 properties · 2 events · 4 methods · 9 parts

Example

The list is flat, with parent references — which is what a SQL result, an HR export or a Razor model already is. A nested 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.
Tab into it once, then use the arrows. and walk the people in reading order; opens a closed branch and, on an open one, steps into it; closes an open branch and, on anything else, goes up to the manager; * 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 tree rather than a diagram with sixteen tab stops in it.
The connectors are decoration and the nesting is the structure. There is no role for “boxes joined by curves”, and nobody can follow a line with their ears — so the SVG layer is 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.
Width is declared and height is measured. 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> &mdash; 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>&darr;</kbd> and <kbd>&uarr;</kbd> walk the people
    in reading order; <kbd>&rarr;</kbd> opens a closed branch and, on an open one, steps into it;
    <kbd>&larr;</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
    &ldquo;boxes joined by curves&rdquo;, and nobody can follow a line with their ears &mdash; 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 &mdash; they are chosen by the direction of travel rather than fixed to
    the bottom and top edges &mdash; 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 &mdash; 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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
bmxSelect BmxOrgChartSelectDetail A node was chosen - clicked, or Enter or Space on it.
bmxToggle BmxOrgChartToggleDetail A branch was opened or closed.

Methods

MethodSignatureDescription
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

SlotDescription
empty what to draw when there are no nodes at all.

CSS shadow parts

PartDescription
canvas
empty
node-count
node-detail
node-handle
node-label
tree
wire
wires

CSS custom properties

PropertyDescription
--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.