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
This commit is contained in:
+422
@@ -0,0 +1,422 @@
|
||||
# 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.*
|
||||
Reference in New Issue
Block a user