Files
jarvis-at-skic 3347cc342c feat: draw.io custom shapes analysis + buildable example plugin
- ANALYSIS.md: Full architecture analysis of draw.io shape systems
  (XML stencils vs JS shapes, plugin loading, desktop/web support)
- Example plugin with 5 custom shapes demonstrating JS logic:
  - StatusIndicator: conditional rendering based on status param
  - ProgressBar: computed geometry (percentage-based fill)
  - DataFlowArrow: dynamic arrows with direction/speed params
  - HexagonCluster: loop-based hexagonal grid generation
  - MetricGauge: radial gauge with arc computation + zones
- Webpack build pipeline (npm run build / build:dev / watch)
- Pre-built dist/drawio-custom-shapes.js ready for installation
- Full README with installation instructions for desktop + web
2026-07-24 15:57:59 +00:00

15 KiB

draw.io Custom Shapes with JavaScript Logic — Architecture Analysis

Executive Summary

Can you build custom shapes with JS logic and import them as a library into draw.io?

YES — via the Plugin system. draw.io supports fully programmable shapes written in JavaScript that can be loaded as plugins in both the desktop (Electron) app and self-hosted/web instances. These plugins can define shapes with conditional rendering, computed geometry, dynamic behavior, and expose them in the sidebar palette for drag-and-drop use.

The standard "library import" (File > Open Library) only supports declarative XML — but plugins give you more power, not less.


Table of Contents

  1. Shape Systems Overview
  2. XML Stencils (Declarative)
  3. JavaScript Shape Classes (Programmatic)
  4. The Plugin Architecture
  5. Loading Plugins in Desktop App
  6. Loading Plugins in Web/Self-Hosted
  7. Shape Registration Flow
  8. Canvas API Reference
  9. Limitations and Considerations
  10. Comparison Matrix

Shape Systems Overview

draw.io has two completely separate shape definition systems:

System Format JS Logic? Import Method
XML Stencils .xml stencil files ❌ No (declarative only) mxStencilRegistry / Open Library
JS Shape Classes .js files ✅ Full programmatic Plugin system / mxCellRenderer.registerShape()

When a cell is rendered, the resolution order is:

Cell style "shape=NAME" →
  1. mxCellRenderer.defaultShapes[NAME]  → JS Shape class
  2. mxStencilRegistry.stencils[NAME]    → XML Stencil instance
  3. Dynamic loading (if enabled)        → Load from library registry

XML Stencils (Declarative)

Located in: src/main/webapp/stencils/*.xml

XML stencils define shapes using a drawing vocabulary:

<shapes name="mxgraph.rack.Oracle">
  <shape name="Server" w="19.5" h="121" aspect="variable">
    <connections>
      <constraint x="0.5" y="0" perimeter="0" name="top"/>
      <constraint x="0.5" y="1" perimeter="0" name="bottom"/>
    </connections>
    <background>
      <rect x="0" y="0" w="19.5" h="121"/>
    </background>
    <foreground>
      <fillstroke/>
      <rect x="2" y="5" w="15.5" h="20"/>
      <stroke/>
    </foreground>
  </shape>
</shapes>

Capabilities

  • Drawing primitives: move, line, quad, curve, arc, rect, roundrect, ellipse, close
  • Styling: strokecolor, fillcolor, fontcolor, alpha, strokewidth, dashed
  • State: save / restore
  • Sub-shapes: <include-shape name="...">

Limitations

  • No loops, conditionals, or computed geometry
  • No JavaScript execution (except limited allowEval for text/image values)
  • Purely static vector drawing instructions

JavaScript Shape Classes (Programmatic)

Located in: src/main/webapp/shapes/*.js (~47 files)

Examples: mxBootstrap.js, mxKubernetes.js, mxElectrical.js, mxNetworks.js, mxRack.js

JS shapes extend mxShape and have full programmatic power:

function MyShape(bounds, fill, stroke, strokewidth) {
    mxShape.call(this);
    this.bounds = bounds;
    this.fill = fill;
    this.stroke = stroke;
    this.strokewidth = (strokewidth != null) ? strokewidth : 1;
}

mxUtils.extend(MyShape, mxShape);

MyShape.prototype.paintVertexShape = function(c, x, y, w, h) {
    c.translate(x, y);

    // Full JS: conditionals, loops, math, style reading
    var mode = mxUtils.getValue(this.style, 'mode', 'default');
    var segments = parseInt(mxUtils.getValue(this.style, 'segments', '4'));

    if (mode === 'alert') {
        c.setFillColor('#ff4444');
    }

    // Computed geometry
    for (var i = 0; i < segments; i++) {
        var segH = h / segments;
        c.rect(0, i * segH, w, segH - 2);
        c.fillAndStroke();
    }
};

// Register globally
mxCellRenderer.registerShape('mxgraph.myLib.myShape', MyShape);

Capabilities

  • Conditional rendering based on style properties
  • Loops and computed geometry
  • Math and dynamic calculations
  • Reading per-instance style parameters via mxUtils.getValue(this.style, ...)
  • Full canvas API access
  • Custom connection points
  • Override getConstraints() for dynamic connection point generation

The Plugin Architecture

Plugin Structure

Every draw.io plugin follows this pattern:

// Shape definitions (execute immediately at load time)
function CustomShape() { /* ... */ }
mxUtils.extend(CustomShape, mxShape);
CustomShape.prototype.paintVertexShape = function(c, x, y, w, h) { /* ... */ };
mxCellRenderer.registerShape('mxgraph.custom.shapeName', CustomShape);

// UI integration (executes when draw.io is ready)
Draw.loadPlugin(function(ui) {
    // ui = EditorUi instance — full access to the application

    // Add sidebar palette
    var fns = [
        ui.sidebar.createVertexTemplateEntry(
            'shape=mxgraph.custom.shapeName;fillColor=#dae8fc;',
            120, 80, '', 'My Custom Shape', null, null, 'search keywords'
        )
    ];
    ui.sidebar.addPaletteFunctions('customPalette', 'My Custom Library', true, fns);

    // Can also: add menus, actions, event listeners, modify graph behavior
});

What Draw.loadPlugin(callback) Provides

The callback receives ui (EditorUi instance) with access to:

Object Purpose
ui.editor.graph The mxGraph instance (model, view, styles)
ui.sidebar Sidebar/palette management
ui.menus Menu system
ui.actions Action registry
ui.editor Editor instance
Global: mxShape, mxCellRenderer, mxUtils, mxConstants All mxGraph APIs

Production Example: rackF5.js

The plugins/rackF5.js file (1674 lines) is a real-world plugin that:

  • Defines ~20+ shape classes with full JS rendering
  • Uses conditionals based on style (hasEars, isFront, isDC, psNum)
  • Registers all shapes with mxCellRenderer.registerShape()
  • Adds a complete sidebar palette via Draw.loadPlugin()

Loading Plugins in Desktop App

The draw.io Electron app (desktop) has full plugin support via Extras > Plugins:

Method 1: Built-in Plugins

A dropdown of pre-packaged plugins (App.publicPlugin):

  • ex (explore), tips, svgdata, number, sql, props, text, anim, update, trees, replay, anon, webcola, tags

Method 2: External Plugins (Custom .js Files)

  1. Enable external plugins (one-time):

    • The app checks requestSync('isPluginsEnabled')
    • If disabled, it shows "pluginsDisabled" message
    • Enable via app preferences or configuration
  2. Install your plugin:

    • Extras > Plugins → "Select File..."
    • Native file dialog opens, filtered for .js files
    • Plugin is copied to App Data folder (installPlugin action)
    • Registered in mxSettings
  3. Restart draw.io — plugin loads on next startup

Loading sequence (from source):

App startup
  → mxSettings.getPlugins() returns saved plugin list
  → App.initPluginCallback() creates Draw object + queue
  → For each plugin:
    ├─ Built-in (starts with ./plugins/) → load relative path
    └─ External → requestSync('getPluginFile') → load from file:// App Data
  → Scripts loaded via <script> tags
  → Draw.loadPlugin(callback) pushes to queue
  → Once UI ready: queue callbacks invoked with EditorUi instance

Security

  • Paths containing .. are rejected
  • External plugins stored in sandboxed App Data folder
  • Non-built-in plugins blocked unless explicitly enabled

App Data locations:

  • Linux: ~/.config/draw.io/plugins/
  • Windows: %APPDATA%\draw.io\plugins\
  • macOS: ~/Library/Application Support/draw.io/plugins/

Loading Plugins in Web/Self-Hosted

URL Parameter

https://your-drawio.com/?p=pluginKey

Where pluginKey maps to a path in App.pluginRegistry.

Configuration JSON

In draw.io configuration (Extras > Configuration):

{
  "plugins": ["https://your-server.com/path/to/plugin.js"]
}

Self-Hosted Docker

Set environment variable:

DRAWIO_PLUGINS_ALLOW_CUSTOM=true

Then configure via URL params or configuration JSON.

ALLOW_CUSTOM_PLUGINS

In Init.js, ALLOW_CUSTOM_PLUGINS must be true for non-built-in plugin URLs. Self-hosted instances can set this; app.diagrams.net (cloud) restricts it.


Shape Registration Flow

┌────────────────────────────────────────────────────────────┐
│ Plugin .js file loaded (via <script> or eval)              │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  1. Shape class defined (extends mxShape)                  │
│  2. paintVertexShape() implemented with JS logic           │
│  3. mxCellRenderer.registerShape(name, Constructor)        │
│     → stored in mxCellRenderer.defaultShapes[name]         │
│                                                            │
│  4. Draw.loadPlugin(function(ui) { ... })                  │
│     → Callback queued until UI ready                       │
│     → When invoked:                                        │
│       a. Creates palette entries referencing shape names    │
│       b. ui.sidebar.addPaletteFunctions(...)               │
│       c. Shapes appear in sidebar for drag-and-drop        │
│                                                            │
├────────────────────────────────────────────────────────────┤
│ At render time:                                            │
│  Cell style: "shape=mxgraph.myLib.myShape;param=value;"    │
│  → mxCellRenderer looks up defaultShapes['mxgraph.myLib.myShape'] │
│  → Instantiates shape, calls paintVertexShape(c,x,y,w,h)  │
│  → Shape reads style params, draws conditionally           │
└────────────────────────────────────────────────────────────┘

Canvas API Reference

The c parameter in paintVertexShape(c, x, y, w, h) is an mxAbstractCanvas2D instance:

Drawing

Method Description
c.begin() Start a new path
c.moveTo(x, y) Move to point
c.lineTo(x, y) Line to point
c.quadTo(x1, y1, x2, y2) Quadratic curve
c.curveTo(x1, y1, x2, y2, x3, y3) Cubic curve
c.arcTo(rx, ry, angle, largeArc, sweep, x, y) Arc
c.close() Close path
c.rect(x, y, w, h) Rectangle
c.roundrect(x, y, w, h, dx, dy) Rounded rectangle
c.ellipse(x, y, w, h) Ellipse

Rendering

Method Description
c.fill() Fill current path
c.stroke() Stroke current path
c.fillAndStroke() Fill and stroke

Style

Method Description
c.setFillColor(color) Set fill color
c.setStrokeColor(color) Set stroke color
c.setStrokeWidth(width) Set stroke width
c.setFontColor(color) Set text color
c.setFontSize(size) Set font size
c.setFontFamily(family) Set font family
c.setAlpha(alpha) Set opacity (0-1)
c.setDashed(dashed) Toggle dashing
c.setDashPattern(pattern) Set dash pattern
c.setLineCap(cap) Line cap style
c.setLineJoin(join) Line join style
c.setShadow(shadow) Toggle shadow
c.setGradient(c1, c2, x, y, w, h, dir, a1, a2) Set gradient fill

Transform & State

Method Description
c.translate(x, y) Translate origin
c.rotate(theta, flipH, flipV, cx, cy) Rotate
c.scale(s) Scale
c.save() Save state
c.restore() Restore state

Text & Images

Method Description
c.text(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir) Draw text
c.image(x, y, w, h, src, aspect, flipH, flipV) Draw image

Reading Style Values

var value = mxUtils.getValue(this.style, 'paramName', 'defaultValue');

Limitations and Considerations

What Plugins CAN Do

  • ✅ Define shapes with full JavaScript logic (conditionals, loops, computed geometry)
  • ✅ Read per-instance style parameters for dynamic rendering
  • ✅ Add sidebar palettes for drag-and-drop
  • ✅ Add menus, actions, keyboard shortcuts
  • ✅ Listen to graph events (selection, edit, etc.)
  • ✅ Modify export behavior
  • ✅ Override core graph methods
  • ✅ Access and manipulate the full graph model

What Plugins CANNOT Do

  • ❌ Be loaded via "File > Open Library" (that's XML-only)
  • ❌ Run in sandboxed/restricted mode on app.diagrams.net (cloud)
  • ❌ Persist custom shape data in the .drawio file beyond style strings
  • ❌ Add new file format support easily
  • ❌ Work across draw.io versions without potential API breakage (no stable plugin API contract)

Security Notes

  • Plugins execute with full page privileges
  • External plugins require explicit user opt-in
  • The desktop app sandboxes plugin storage to App Data
  • Self-hosted instances control plugin trust via ALLOW_CUSTOM_PLUGINS

Comparison Matrix

Feature XML Library XML Stencil JS Plugin
Conditional rendering ❌ ❌ ✅
Computed geometry ❌ ❌ ✅
Loop-based drawing ❌ ❌ ✅
Style-reactive ❌ Limited ✅
Sidebar palette ✅ ✅ ✅
Drag-and-drop ✅ ✅ ✅
File > Open Library ✅ ⚠️ (via registry) ❌
Desktop app support ✅ ✅ ✅
Web app support ✅ ✅ ⚠️ (config needed)
User install difficulty Easy Moderate Moderate
Custom connection points ❌ (fixed) ✅ (XML) ✅ (dynamic)
Event handling ❌ ❌ ✅
Menu integration ❌ ❌ ✅

Conclusion

For building custom shapes with JavaScript logic that behave as a reusable library in draw.io:

  1. Write a plugin (.js file) that defines shape classes and registers them
  2. Load it via Extras > Plugins (desktop) or configuration JSON (web/self-hosted)
  3. Shapes appear in the sidebar palette, fully interactive with JS-powered rendering

The plugin system is the officially supported, production-proven mechanism — used internally by draw.io for shapes like F5 rack equipment, and by the ~47 shape library files bundled with the application.


Analysis performed on draw.io source (https://github.com/jgraph/drawio) — commit at time of analysis.