JavaScript Organogram
@lemonadejs/organogram · ✓ 43 contract checks · framework-agnostic · zero dependencies
.
An org chart / hierarchy diagram built from a FLAT adjacency list:
const people = [ { id: 1, name: ‘Jorge’, role: ‘CEO’, parent: 0, status: ‘#90EE90’, img: ‘/ceo.png’ }, { id: 2, name: ‘Antonio’, role: ‘Vice president’, parent: 1, status: ‘#90EE90’, img: ‘/u.jpg’ }, … ]; <${Organogram} data=”${people}” bind=”${selected}” />
parent points at another row’s id; a parent of 0 / null / unknown
is a root (a forest of several roots is supported). Everything else —
the tree, the tidy layout, the elbow connectors, the bounds — is one
reactive model derived from the props. Mutate the data in place +
data.touch() (or assign a new array) and only the layout recomputes;
pan/zoom live in their OWN state so dragging never rebuilds the tree.
THE INTERACTION, AND WHY IT IS NOT A LIBRARY OF ITS OWN:
- Pan/zoom is Google-Maps style on ONE transformed
worldlayer: translate()+scale() on a single element. Dragging the background pans; the wheel zooms ANCHORED at the cursor (the point under the pointer stays put). There is no per-frame relayout — the browser composites the transform; we never recompute node positions while panning. - Nodes are real HTML cards (avatar, name, role, status) positioned by left/top in world coordinates, so a CSS transition animates the re-layout when a branch collapses — again, no JS tween loop.
- Connectors are one
<svg>in the same world layer; orthogonal elbow paths are plain strings rebuilt only when the layout changes.
Quick-search centers the viewport on any node (expanding its ancestors first if it was collapsed away) — the “fly to” of a maps UI.
Example
import { html } from 'lemonadejs';
import Organogram from '@lemonadejs/organogram';
const avatar = (seed) => `https://picsum.photos/seed/${seed}/80/80`;
const people = [
{ id: 1, name: 'Jorge Lima', role: 'CEO', parent: 0, status: '#90EE90', img: avatar('jorge') },
{ id: 2, name: 'Antonio Reis', role: 'Vice president', parent: 1, status: '#90EE90', img: avatar('antonio') },
{ id: 3, name: 'Manoel Souza', role: 'Production manager', parent: 1, status: '#D3D3D3', img: avatar('manoel') },
{ id: 4, name: 'Ana Costa', role: 'Sales manager', parent: 2, status: '#90EE90', img: avatar('ana') },
{ id: 5, name: 'Marcos Dias', role: 'Marketing manager', parent: 2, status: '#90EE90', img: avatar('marcos') },
{ id: 6, name: 'Pedro Alves', role: 'Intern', parent: 3, status: '#90EE90', img: avatar('pedro') },
{ id: 7, name: 'Iris Moreau', role: 'Sales assistant', parent: 4, status: '#D3D3D3', img: avatar('iris') },
];
const App = (props, { state }) => {
const selected = state('');
const name = () => {
const person = people.find((p) => p.id === selected.value);
return person ? `${person.name} (${person.role})` : 'none';
};
return html`<div>
<${Organogram} data="${people}" bind="${selected}" height="${340}" controls="${false}"
legend statuslabels="${{ '#90EE90': 'Active', '#D3D3D3': 'On leave' }}" />
<p style="font-size:13px">Drag to pan, scroll to zoom, click a card to select. Selected: <b>${name}</b></p>
</div>`;
};Installation
npm install @lemonadejs/organogram
import Organogram from '@lemonadejs/organogram';
import '@lemonadejs/organogram/style.css';
The icons come from Google Material Symbols. Load the font once per page:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">
Three deployment forms, one component:
html`<${Organogram} />` // by value (no registration)
setComponents({ Organogram }); // then <Organogram /> by name anywhere
createWebComponent(Organogram); // <lm-organogram> in plain HTML/any framework
Props
Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.
| Prop | Type | Default | Description |
|---|---|---|---|
bind | any | — | Two-way bound value. .set() fires onchange; plain assignment is silent. two-way selected node id (‘any’: string |
data | array | — | OrgItem[] — the flat adjacency list |
orientation | string | '' | ” top-down (default) |
nodewidth | number | 180 | card width in px |
nodeheight | number | 70 | card height in px |
hspacing | number | 24 | gap between siblings (px) |
vspacing | number | 50 | gap between levels (px) |
compact | boolean | false | stack a node’s children vertically when they are all leaves |
height | number | 480 | viewport height (px); width is always fluid |
controls | boolean | true | show the zoom / fit control cluster |
search | boolean | true | show the quick-search box |
collapsible | boolean | true | allow collapsing a branch from its card |
avatars | boolean | true | render the avatar images |
legend | boolean | false | show a status legend (needs statuslabels) |
statuslabels | object | — | { ‘#90EE90’: ‘Active’, ‘#D3D3D3’: ‘Inactive’ } |
minzoom | number | 0.2 | lower zoom bound |
maxzoom | number | 2.5 | upper zoom bound |
zoom | number | 0 | initial zoom (0 = auto-fit on mount) |
fit | boolean | true | auto-fit the whole chart into view on mount |
Events
All event names are lowercase (the platform convention — LJS-305 warns otherwise).
onchange— (id, item) on selection (bindable)onnodeclick— (id, item) on any card clickoncollapse— (id, collapsed) on expand/collapseonzoom— (scale) after any zoom change
API
import { ref } from 'lemonadejs';
const organogram = ref();
html`<${Organogram} ref="${organogram}" />`;
// organogram.current.select(...) · organogram.current.center(...) · organogram.current.fit(...) · organogram.current.reset(...) · organogram.current.zoomIn(...) · organogram.current.zoomOut(...) · organogram.current.setZoom(...) · organogram.current.getZoom(...) · organogram.current.expand(...) · organogram.current.collapse(...) · organogram.current.toggle(...) · organogram.current.expandAll(...) · organogram.current.collapseAll(...)
select()center()fit()— auto-fit the whole chart into view on mountreset()zoomIn()zoomOut()setZoom()getZoom()expand()collapse()toggle()expandAll()collapseAll()
Styling
All classes follow the lm-organogram-* convention; visual variants are data-*
attributes on the root. Override freely — there is no styling engine to fight.
Contract
The machine-readable schema ships with the package:
import contract from '@lemonadejs/organogram/contract.json';
verify.json carries the conformance proof produced by verify(Organogram).
Looking for the v5 plugin? See the archived v5 documentation.