npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mym-organizator

v1.0.2

Published

Organize your company or your community with mym-organizator

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

  1. What you get
  2. Quick start (60 seconds)
  3. Installation
  4. Your first chart
  5. Configuration options
  6. Working with node types
  7. Saving data (localStorage and backend)
  8. Plugins: what they are
  9. Using the Control Station plugin
  10. Writing your own plugin
  11. Engine API (advanced)
  12. Styling and theming
  13. Browser support
  14. 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 d3

Note: d3 is a peerDependency, 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:

  1. The onboarding modal asks for your top-level name. Type "ACME Inc." and confirm.
  2. The chart shows one node (the root).
  3. 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.
  4. Click your new node. The right panel shows its details. Add personnel, assign permissions.
  5. 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.
  • isRoottrue for the top-level type. Only one type should have this.
  • systemtrue means 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 to localStorage, then to the onboarding modal.
  • After every change, the library calls POST saveUrl with the payload as JSON. Saves are debounced (about half a second) so rapid edits don't spam the server.
  • localStorage is 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_point node 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.