- 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
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
- Shape Systems Overview
- XML Stencils (Declarative)
- JavaScript Shape Classes (Programmatic)
- The Plugin Architecture
- Loading Plugins in Desktop App
- Loading Plugins in Web/Self-Hosted
- Shape Registration Flow
- Canvas API Reference
- Limitations and Considerations
- 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
allowEvalfor 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)
-
Enable external plugins (one-time):
- The app checks
requestSync('isPluginsEnabled') - If disabled, it shows "pluginsDisabled" message
- Enable via app preferences or configuration
- The app checks
-
Install your plugin:
- Extras > Plugins → "Select File..."
- Native file dialog opens, filtered for
.jsfiles - Plugin is copied to App Data folder (
installPluginaction) - Registered in
mxSettings
-
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
.drawiofile 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:
- Write a plugin (
.jsfile) that defines shape classes and registers them - Load it via Extras > Plugins (desktop) or configuration JSON (web/self-hosted)
- 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.