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 world layer: 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

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

PropTypeDefaultDescription
bindanyTwo-way bound value. .set() fires onchange; plain assignment is silent. two-way selected node id (‘any’: string
dataarrayOrgItem[] — the flat adjacency list
orientationstring''” top-down (default)
nodewidthnumber180card width in px
nodeheightnumber70card height in px
hspacingnumber24gap between siblings (px)
vspacingnumber50gap between levels (px)
compactbooleanfalsestack a node’s children vertically when they are all leaves
heightnumber480viewport height (px); width is always fluid
controlsbooleantrueshow the zoom / fit control cluster
searchbooleantrueshow the quick-search box
collapsiblebooleantrueallow collapsing a branch from its card
avatarsbooleantruerender the avatar images
legendbooleanfalseshow a status legend (needs statuslabels)
statuslabelsobject{ ‘#90EE90’: ‘Active’, ‘#D3D3D3’: ‘Inactive’ }
minzoomnumber0.2lower zoom bound
maxzoomnumber2.5upper zoom bound
zoomnumber0initial zoom (0 = auto-fit on mount)
fitbooleantrueauto-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 click
  • oncollapse — (id, collapsed) on expand/collapse
  • onzoom — (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 mount
  • reset()
  • 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.