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

423 lines
15 KiB
Markdown

# 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](#shape-systems-overview)
2. [XML Stencils (Declarative)](#xml-stencils-declarative)
3. [JavaScript Shape Classes (Programmatic)](#javascript-shape-classes-programmatic)
4. [The Plugin Architecture](#the-plugin-architecture)
5. [Loading Plugins in Desktop App](#loading-plugins-in-desktop-app)
6. [Loading Plugins in Web/Self-Hosted](#loading-plugins-in-webself-hosted)
7. [Shape Registration Flow](#shape-registration-flow)
8. [Canvas API Reference](#canvas-api-reference)
9. [Limitations and Considerations](#limitations-and-considerations)
10. [Comparison Matrix](#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:
```xml
<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**:
```javascript
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:
```javascript
// 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):
```json
{
"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
```javascript
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.*