mym-organizator
v1.0.2
Published
Organize your company or your community with mym-organizator
Maintainers
Readme
orgchart-js
A small, modular organization chart designer that runs in any browser.
You drop one <div> on a page, include a couple of script tags, and your users get a full designer: drag-and-drop hierarchy, node types, personnel, permissions, and a plugin system you can extend.
Built on top of D3 v7. No framework. No build step required.
Table of contents
- What you get
- Quick start (60 seconds)
- Installation
- Your first chart
- Configuration options
- Working with node types
- Saving data (localStorage and backend)
- Plugins: what they are
- Using the Control Station plugin
- Writing your own plugin
- Engine API (advanced)
- Styling and theming
- Browser support
- License
What you get
When you load the library on a page and call new OrgChart.App('#app').mount(), the user sees this layout:
┌─────────────────────────────────────────────────────────────┐
│ Topbar [search] [filter] [types] [permissions] [save] │
├──────────┬─────────────────────────────────┬───────────────┤
│ │ │ │
│ Tree │ Canvas (zoom, pan, drag) │ Details │
│ (left) │ │ (right) │
│ │ │ │
└──────────┴─────────────────────────────────┴───────────────┘Out of the box:
- Create a root node with a friendly onboarding modal.
- Add, edit, delete, duplicate, and move nodes (drag-and-drop reparenting).
- Define your own node types (name, color, icon, allowed children).
- Add personnel and assign per-node permissions.
- Collapse/expand branches, search by name or code, filter by type.
- Auto-save to
localStorage. Optional sync with a backend. - Export/import the entire org as a single JSON file.
Optional add-ons (plugins):
ControlStation— adds a "Control Point" node type and a biometric device pool. Useful for visitor-tracking systems where physical checkpoints need device assignments.
Quick start (60 seconds)
Create a folder, drop in one HTML file, double-click it. That's the whole thing.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>My Org Chart</title>
<!-- 1. The library's stylesheet -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/orgchart-js@latest/src/css/orgchart.css"
/>
<style>
html,
body {
height: 100%;
margin: 0;
}
#app {
height: 100vh;
}
</style>
</head>
<body>
<!-- 2. One empty container -->
<div id="app"></div>
<!-- 3. d3 (the only dependency) -->
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<!-- 4. The library: engine first, then app -->
<script src="https://cdn.jsdelivr.net/npm/orgchart-js@latest/src/js/orgchart.engine.js"></script>
<script src="https://cdn.jsdelivr.net/npm/orgchart-js@latest/src/js/orgchart.app.js"></script>
<!-- 5. Start it -->
<script>
new OrgChart.App("#app").mount();
</script>
</body>
</html>Open the file in a browser. The onboarding modal asks for the name of your top-level organization. Type something ("ACME Corp"), click Set up and start, and you have a working org chart. Right-click any node to add children, drag any node onto another to reparent.
That's it. No build tools, no npm install, no framework.
Installation
You have three ways to use the library. Pick whichever matches your project.
Option 1: CDN (simplest)
Use the snippet above. No download, no install. Just point to jsdelivr (or unpkg). Best for prototypes, simple sites, and demos.
Option 2: Copy files
Download the five source files and put them in your project:
your-project/
lib/orgchart/
orgchart.engine.js
orgchart.app.js
orgchart-control-station.js (optional)
orgchart.css
orgchart-control-station.css (optional)Reference them with relative paths in your HTML.
Option 3: npm (for bundler-based projects)
npm install orgchart-js d3Note:
d3is apeerDependency, so you install it separately. This avoids version conflicts when your project already uses d3.
Then in your code:
import OrgChart from "orgchart-js";
import "orgchart-js/css";
new OrgChart.App("#app").mount();Your first chart
Let's build something a bit more realistic. Suppose you have a small company. You want to lay out departments and people.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="lib/orgchart/orgchart.css" />
<style>
html,
body {
height: 100%;
margin: 0;
}
#app {
height: 100vh;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="lib/orgchart/orgchart.engine.js"></script>
<script src="lib/orgchart/orgchart.app.js"></script>
<script>
new OrgChart.App("#app", {
title: "ACME Inc.",
subtitle: "Org Chart",
storageKey: "acme-org", // unique key per chart, lets you have many
}).mount();
</script>
</body>
</html>When you open this:
- The onboarding modal asks for your top-level name. Type "ACME Inc." and confirm.
- The chart shows one node (the root).
- Click the + button at the bottom-right, or right-click the root, choose Add child. Pick a type ("Department"), give it a name ("Engineering"), confirm.
- Click your new node. The right panel shows its details. Add personnel, assign permissions.
- Drag any node onto another to move it. Drag onto empty space to cancel.
Everything is auto-saved to localStorage under the key acme-org. Reload the page; your work is still there.
Configuration options
When you create the app, you can pass an options object. All fields are optional.
new OrgChart.App('#app', {
// Visual
title: 'My Organization', // top-left title
subtitle: 'Org Designer', // small text under the title
logo: '⌗', // single character in the brand box
// Storage
storageKey: 'my-org', // localStorage key, default: 'orgchart-data'
autoSave: true, // save automatically on every change
// Defaults for first-time users (before any data exists)
defaults: {
nodeTypes: [ /* see "Working with node types" */ ],
permissions: [
{ id: 'read', label: 'Read' },
{ id: 'write', label: 'Write' },
{ id: 'delete', label: 'Delete' }
]
},
// Pre-load data (overrides localStorage if provided)
initialData: { version: 3, nodeTypes: [...], data: {...} },
// Optional backend
loadUrl: '/api/org/load', // GET: returns the payload JSON
saveUrl: '/api/org/save', // POST: receives the payload JSON
antiForgeryToken: '...', // for ASP.NET CSRF
// Plugins
plugins: [
OrgChart.Plugins.ControlStation()
]
}).mount();After calling .mount(), the chart takes over the container element. To remove it later, call app.destroy().
Working with node types
A "node type" is a category of node — like Department, Team, Position. Each type has a name, an icon, a color, and rules about what can go inside it.
The library ships with three default types:
| ID | Label | Allowed children |
| --------- | -------- | ------------------------------ |
| root | Root | Anything (system type, locked) |
| unit | Top Unit | Anything |
| subunit | Sub Unit | Anything |
You can:
- Rename them — open the Types modal in the toolbar, edit any row.
- Add new types — click "Add new type" in the Types modal.
- Restrict nesting — for each type, pick which other types are allowed inside (using the chips). An empty selection means "leaf" (no children).
- Recolor / change icon — color picker and an icon field (emoji or short text).
System types like root are protected: you cannot delete them, you cannot delete their hardcoded structural rules.
You can also define types up front in defaults.nodeTypes:
defaults: {
nodeTypes: [
{
id: "root",
label: "Company",
color: "#4f46e5",
icon: "🏢",
allowedChildren: null,
isRoot: true,
system: true,
},
{
id: "dept",
label: "Department",
color: "#0ea5e9",
icon: "📂",
allowedChildren: null,
},
{
id: "team",
label: "Team",
color: "#10b981",
icon: "👥",
allowedChildren: ["person"],
},
{
id: "person",
label: "Person",
color: "#f59e0b",
icon: "👤",
allowedChildren: [] /* leaf */,
},
];
}Field reference:
id— unique string. Used internally and in the saved JSON.label— what users see.color,icon— visuals.allowedChildren— one of:null→ any type can go inside.[]→ leaf, nothing can go inside.['team', 'person']→ only these types allowed.
isRoot—truefor the top-level type. Only one type should have this.system—truemeans users cannot delete this type from the Types modal.
Saving data (localStorage and backend)
By default the library saves to localStorage after every change. The full payload looks like this:
{
"version": 3,
"nodeTypes": [ /* type definitions */ ],
"permissions": [ {"id":"read","label":"Read"}, ... ],
"data": {
"id": "node_abc",
"type": "root",
"name": "ACME Inc.",
"code": "ROOT",
"meta": { "note": "...", "deviceIds": ["dev1","dev2"] },
"staff": [ {"id":"...","name":"Alice","role":"CEO"} ],
"permissions": ["read","write"],
"children": [ /* nested nodes */ ]
},
"extensions": {
"control-station": { "devices": [...] }
}
}To export the data, the user clicks the export button in the toolbar — a JSON file is downloaded. To import, they click the import button and pick a JSON file.
Connecting a backend
Provide loadUrl and saveUrl in the options. The library uses fetch under the hood.
new OrgChart.App("#app", {
loadUrl: "/api/org/load", // GET → must return the payload JSON
saveUrl: "/api/org/save", // POST ← receives the payload JSON
antiForgeryToken: "xyz", // optional, sent as RequestVerificationToken header
}).mount();Behavior:
- On startup, the library calls
GET loadUrl. If it returns a payload, that is used. If it returns nothing or fails, falls back tolocalStorage, then to the onboarding modal. - After every change, the library calls
POST saveUrlwith the payload as JSON. Saves are debounced (about half a second) so rapid edits don't spam the server. localStorageis always used as a local cache, even when a server is connected. If the network fails, the user doesn't lose their work.
ASP.NET example
public class OrgController : Controller
{
[HttpGet]
public IActionResult Load() => Json(_store.GetCurrent());
[HttpPost, ValidateAntiForgeryToken]
public IActionResult Save([FromBody] OrgPayload payload)
{
_store.Save(payload);
return Ok();
}
}
public class OrgPayload
{
public int Version { get; set; }
public List<OrgNodeType> NodeTypes { get; set; }
public List<OrgPermission> Permissions { get; set; }
public OrgNode Data { get; set; }
public Dictionary<string, JsonElement> Extensions { get; set; }
}Plugins: what they are
The core library handles general-purpose org charts: types, nodes, personnel, permissions, hierarchy. Some features are useful only in specific projects — for example, "assign a biometric device to a checkpoint" only makes sense in a visitor-tracking system.
Instead of bloating the core with every imaginable feature, the library lets you attach plugins. A plugin is a small object that adds:
- New node types
- New toolbar buttons
- New modals
- New sections in the details panel
- New entries in the stats chip
- New badges in the tree
- New listeners for app events
- Its own data, stored in a namespaced part of the payload
You attach a plugin by passing it to the plugins option:
new OrgChart.App("#app", {
plugins: [OrgChart.Plugins.ControlStation()],
}).mount();Without plugins, you get the clean core. With plugins, the library grows feature by feature — but only the features you ask for.
Using the Control Station plugin
This is the example plugin that ships with the library. It adds:
- A
control_pointnode type (red icon, leaf, system type — cannot be deleted). - A Devices button in the toolbar that opens a CRUD modal for managing a pool of biometric devices.
- A section in the details panel that appears only for Control Point nodes. Each node can hold one or more devices — the panel lists every assigned device with a × to remove it, plus a dropdown to add another. A single device can only be assigned to one node at a time.
- A line in the stats chip showing "X/Y devices assigned" (counted across every node).
- A small badge in the tree (
📍 N) on every node that has devices.
To use it, load the plugin's JS and CSS, and pass it to the plugins option:
<link rel="stylesheet" href="lib/orgchart/orgchart.css" />
<link rel="stylesheet" href="lib/orgchart/orgchart-control-station.css" />
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="lib/orgchart/orgchart.engine.js"></script>
<script src="lib/orgchart/orgchart.app.js"></script>
<script src="lib/orgchart/orgchart-control-station.js"></script>
<script>
new OrgChart.App("#app", {
plugins: [
OrgChart.Plugins.ControlStation({
// optional — your own initial device pool
defaultDevices: [
{
id: "d1",
name: "ZK F18 #1",
brand: "ZKTeco",
model: "F18",
serial: "SN001",
},
{
id: "d2",
name: "BioStation A2 #1",
brand: "Suprema",
model: "BioStation A2",
serial: "SN002",
},
],
}),
],
}).mount();
</script>You can customize the type itself if "Control Point" is too domain-specific:
OrgChart.Plugins.ControlStation({
typeId: "checkpoint",
typeLabel: "Checkpoint",
typeColor: "#0ea5e9",
typeIcon: "🚪",
});The devices live in the payload under extensions["control-station"].devices, so exporting/importing the chart also moves the device pool with it.
Writing your own plugin
A plugin is a plain object with one method, install(api). The library calls install once, before the chart mounts. Inside install, you call API methods to register your additions.
Here is a minimal plugin that adds a node type and a toolbar button:
window.OrgChart = window.OrgChart || {};
window.OrgChart.Plugins = window.OrgChart.Plugins || {};
window.OrgChart.Plugins.MyPlugin = function (opts) {
return {
name: "my-plugin",
install: function (api) {
// 1. Add a node type
api.registerNodeType({
id: "project",
label: "Project",
color: "#a855f7",
icon: "🚀",
allowedChildren: null,
});
// 2. Add a toolbar button
api.addToolbarButton({
label: "Projects",
icon: "🚀",
title: "Show project overview",
onClick: function () {
api.toast("You clicked the Projects button.", "ok");
},
});
// 3. React to events
api.on("boot", function () {
console.log("Chart is ready");
});
api.on("node:add", function (e) {
console.log("Added node", e.node.name, "under", e.parentId);
});
},
};
};Use it the same way you use any other plugin:
new OrgChart.App("#app", {
plugins: [OrgChart.Plugins.MyPlugin()],
}).mount();The plugin API
Methods you can call from install(api):
| Method | What it does |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| api.registerNodeType(typeDef) | Adds a node type to the type list. |
| api.registerPermission(permDef) | Adds a permission to the permission list. |
| api.addToolbarButton({label, icon, title, onClick}) | Adds a button to the top-right of the toolbar. |
| api.addDetailSection({when, render}) | Adds a section to the right panel, shown only when when(node) returns true. render(node, api) must return a DOM element. |
| api.addStatsItem({compute}) | Adds an item to the stats chip. compute(state, api) returns {label, value}. |
| api.addTreeBadge({when, render}) | Adds a small badge to tree rows where when(node) returns true. |
| api.addModal({id, render}) | Adds a modal. Open it with api.openModal(id), close it with api.closeModal(id). |
| api.getEngine() | Returns the underlying OrgChart.Engine instance for direct chart manipulation. |
| api.getState() | Returns a deep copy of the current full payload. |
| api.getExtensionState(name) / api.setExtensionState(name, data) | Read/write your plugin's own data in the payload's extensions namespace. |
| api.refresh() | Re-renders the tree, the details panel, and the stats chip. Call this after you change something. |
| api.save() | Triggers an immediate save (also automatic). |
| api.toast(message, kind) | Shows a toast. kind is 'ok', 'err', or omitted. |
| api.el(tag, attrs, children) | Small DOM helper, so you don't have to use document.createElement everywhere. |
| api.on(event, callback) / api.emit(event, data) | Event bus. |
Events you can listen to
| Event | Payload | When it fires |
| ---------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| boot | { app } | After the chart is fully mounted. |
| state:load | { payload } | After data is loaded (from localStorage or loadUrl). Read payload.extensions[yourName] here. |
| state:save | { payload } | Just before data is saved. Mutate payload.extensions[yourName] to add your plugin's data. (Or just use setExtensionState which does this for you.) |
| node:select | { node } | When the user selects a node. |
| node:add | { node, parentId } | After a new node is added. |
| node:remove | { nodeId } | After a node is deleted. |
| node:move | { nodeId, oldParentId, newParentId } | After a node is dragged to a new parent. |
| node:change | {} | After any structural change. |
| node:duplicate-clean | { clone } | When a node is being duplicated. You can mutate clone to strip out plugin-specific data. |
A complete plugin
Here is a fuller example that maintains its own data and renders a custom section:
window.OrgChart.Plugins.Notes = function () {
let api = null;
return {
name: "notes",
install: function (publicApi) {
api = publicApi;
// Show a "Notes" section on every node
api.addDetailSection({
when: function () {
return true;
},
render: function (node) {
const wrap = api.el("div", { class: "my-notes-section" });
wrap.appendChild(api.el("div", { class: "oc-sect" }, ["Notes"]));
const list = api.el("div");
const myData = api.getExtensionState("notes") || { byNode: {} };
const myNotes = myData.byNode[node.id] || [];
myNotes.forEach(function (n) {
list.appendChild(api.el("div", {}, ["• " + n]));
});
const input = api.el("input", {
type: "text",
placeholder: "Add a note...",
});
input.addEventListener("keydown", function (e) {
if (e.key === "Enter" && input.value.trim()) {
myData.byNode[node.id] = myData.byNode[node.id] || [];
myData.byNode[node.id].push(input.value.trim());
api.setExtensionState("notes", myData);
api.refresh();
}
});
wrap.appendChild(list);
wrap.appendChild(input);
return wrap;
},
});
// Add to stats
api.addStatsItem({
compute: function () {
const d = api.getExtensionState("notes") || { byNode: {} };
const count = Object.values(d.byNode).reduce(function (s, arr) {
return s + arr.length;
}, 0);
return { label: "notes", value: count };
},
});
},
};
};That's a complete, useful plugin in about 30 lines. It saves itself in the payload, survives reload, and follows the export/import data with the rest of the chart.
Engine API (advanced)
If you only need the D3 chart without the full app shell (toolbar, panels, modals), you can use OrgChart.Engine directly. This is useful if you have your own UI and just want the drawing surface.
const chart = new OrgChart.Engine("#myDiv", {
nodeTypes: [
{ id: "root", label: "Root", color: "#4f46e5", icon: "🏢" },
{ id: "unit", label: "Unit", color: "#0ea5e9", icon: "📂" },
],
data: {
type: "root",
name: "ACME",
children: [
{ type: "unit", name: "Engineering" },
{ type: "unit", name: "Sales" },
],
},
onSelect: function (node) {
console.log("selected", node);
},
onChange: function () {
console.log("something changed");
},
});
chart.addNode(parentId, { type: "unit", name: "Marketing" });
chart.updateNode(id, { name: "New name" });
chart.removeNode(id);
chart.moveNode(id, newParentId);
chart.addStaff(nodeId, { name: "Alice", role: "Manager" });
chart.removeStaff(nodeId, staffId);
chart.search("engineering");
chart.setTypeFilter(["unit"]);
chart.expand(id);
chart.collapse(id);
chart.toggle(id);
chart.expandAll();
chart.collapseAll();
chart.fit();
chart.zoomIn();
chart.zoomOut();
chart.focusNode(id, 1);
chart.getNode(id);
chart.getParent(id);
chart.getData();
chart.stats();
chart.toTreeList();
chart.destroy();The Engine knows nothing about node types beyond their visual properties (color, icon, allowed children). It does not save data — that is the App's job. It just draws and dispatches events.
Styling and theming
All CSS variables are scoped to .oc-app, so you can theme the chart without affecting the rest of your page:
.oc-app {
--oc-primary: #0ea5e9; /* primary color */
--oc-primary-soft: #e0f2fe;
--oc-danger: #ef4444;
--oc-bg: #ffffff;
--oc-panel: #f9fafb;
--oc-border: #e5e7eb;
--oc-text: #111827;
--oc-muted: #6b7280;
}You can override any variable, or write custom rules using the class names below.
Class name prefixes
To avoid conflicts with your existing CSS, every class in the library uses one of three prefixes:
oc-*— the App UI (toolbar, panels, modals, buttons).os-*— the Engine SVG (chart nodes, links, drag states).cs-*— the Control Station plugin (devices, badges).
So if you write .btn in your own styles, it does not affect the library's .oc-btn.
Targeting specific things
A few useful selectors:
/* Make the chart background fully transparent */
.oc-app .oc-canvas {
background: none;
}
/* Bigger node text */
.oc-app .os-name {
font-size: 14px;
font-weight: 700;
}
/* Tighter side panel */
.oc-app .oc-side-right {
width: 280px;
}Browser support
- Chrome / Edge (recent)
- Firefox (recent)
- Safari (recent)
- Mobile browsers — usable but the designer is built for keyboard + mouse. For read-only viewing on mobile, expect to do some CSS tuning.
The library uses fetch, localStorage, ResizeObserver, and modern DOM APIs. No transpilation or polyfills shipped. If you need to support older browsers, transpile in your build.
License
MIT. See LICENSE.
Built on D3 v7.
