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:
2026-07-24 15:57:59 +00:00
parent f36600006c
commit 3347cc342c
14 changed files with 2835 additions and 2 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
*.log
.DS_Store
dist/*.map
+422
View File
@@ -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.*
+158 -2
View File
@@ -1,3 +1,159 @@
# drawio-custom-shapes
# draw.io Custom Shapes Example Plugin
Analysis and example plugin for building custom draw.io shapes with JavaScript logic
A fully buildable example plugin demonstrating how to create custom draw.io shapes with JavaScript logic.
## What This Is
A draw.io plugin that registers custom shapes with **programmatic rendering** — shapes that use conditionals, loops, computed geometry, and style-reactive behavior. Unlike XML stencils (which are purely declarative), these shapes have full JavaScript power.
## Quick Start
```bash
# Install dependencies
npm install
# Development build (with source maps)
npm run build:dev
# Production build (minified)
npm run build
# Watch mode (rebuild on changes)
npm run watch
```
Output: `dist/drawio-custom-shapes.js`
## Installation in draw.io
### Desktop App (Electron)
1. Build the plugin: `npm run build`
2. Open draw.io desktop
3. **Extras > Plugins**
4. Enable external plugins if prompted
5. Click **"Select File..."** → navigate to `dist/drawio-custom-shapes.js`
6. Restart draw.io
7. Find "Custom Shapes Example" palette in the sidebar
### Self-Hosted (Docker)
1. Build the plugin: `npm run build`
2. Copy `dist/drawio-custom-shapes.js` to your draw.io plugins directory
3. Set environment: `DRAWIO_PLUGINS_ALLOW_CUSTOM=true`
4. Add to configuration:
```json
{
"plugins": ["/plugins/drawio-custom-shapes.js"]
}
```
5. Restart the container
### Web (URL parameter)
If you host the plugin file at a URL:
```
https://your-drawio.com/?p=YOUR_PLUGIN_KEY
```
## Project Structure
```
├── src/
│ ├── index.js # Plugin entry point (Draw.loadPlugin wrapper)
│ ├── shapes/
│ │ ├── StatusIndicator.js # Conditional rendering based on status
│ │ ├── ProgressBar.js # Computed geometry (percentage-based)
│ │ ├── DataFlowArrow.js # Dynamic arrow with animation markers
│ │ ├── HexagonCluster.js # Loop-based hexagonal grid
│ │ └── MetricGauge.js # Radial gauge with computed arcs
│ └── palette.js # Sidebar palette registration
├── dist/ # Build output
├── webpack.config.js # Build configuration
├── package.json
├── ANALYSIS.md # Full architecture analysis
└── README.md
```
## Shape Catalog
### 1. Status Indicator (`customShapes.statusIndicator`)
A shape that changes color and icon based on a `status` style parameter.
- Styles: `status=ok|warning|error|unknown`
- Demonstrates: conditional rendering, style reading
### 2. Progress Bar (`customShapes.progressBar`)
A horizontal bar that fills based on a percentage value.
- Styles: `progress=0..100`, `barColor=#4CAF50`
- Demonstrates: computed geometry, value-based rendering
### 3. Data Flow Arrow (`customShapes.dataFlowArrow`)
A directional arrow with animated flow indicators.
- Styles: `direction=right|left|both`, `flowSpeed=fast|medium|slow`
- Demonstrates: dynamic path generation, pattern rendering
### 4. Hexagon Cluster (`customShapes.hexCluster`)
A grid of hexagons that adapts to the cell size.
- Styles: `hexCount=7`, `hexFill=#E3F2FD`
- Demonstrates: loop-based drawing, mathematical geometry
### 5. Metric Gauge (`customShapes.metricGauge`)
A radial gauge (speedometer-style) showing a value within a range.
- Styles: `value=75`, `minVal=0`, `maxVal=100`, `zones=green,yellow,red`
- Demonstrates: arc computation, gradient zones, text rendering
## Customizing
### Adding a New Shape
1. Create a new file in `src/shapes/YourShape.js`:
```javascript
import { registerShape, getValue } from '../utils';
export function YourShapeName() {
mxShape.call(this);
}
mxUtils.extend(YourShapeName, mxShape);
YourShapeName.prototype.paintVertexShape = function(c, x, y, w, h) {
c.translate(x, y);
// Read style parameters
var myParam = getValue(this.style, 'myParam', 'default');
// Draw with full JS logic
c.begin();
c.moveTo(0, 0);
c.lineTo(w, h);
c.stroke();
};
// Register the shape
registerShape('customShapes.yourShape', YourShapeName);
```
2. Import it in `src/index.js`
3. Add a palette entry in `src/palette.js`
4. Rebuild: `npm run build`
### Style Parameters
Shapes read parameters from the cell's style string:
```
shape=customShapes.yourShape;myParam=value;fillColor=#fff;
```
Access in code:
```javascript
var val = mxUtils.getValue(this.style, 'myParam', 'default');
```
## Architecture Notes
See [ANALYSIS.md](./ANALYSIS.md) for the full technical analysis of how draw.io's shape systems work, including the stencil registry, cell renderer, and plugin loading mechanisms.
## License
MIT
+1
View File
File diff suppressed because one or more lines are too long
+1437
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "drawio-custom-shapes",
"version": "1.0.0",
"description": "Example draw.io plugin with custom JavaScript shapes",
"main": "dist/drawio-custom-shapes.js",
"scripts": {
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"watch": "webpack --mode development --watch",
"clean": "rm -rf dist"
},
"keywords": ["drawio", "draw.io", "plugin", "shapes", "mxgraph"],
"license": "MIT",
"devDependencies": {
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* draw.io Custom Shapes Example Plugin
*
* This is the main entry point. It:
* 1. Defines and registers custom shape classes (immediately)
* 2. Wraps sidebar palette setup in Draw.loadPlugin (deferred until UI ready)
*
* Global dependencies (provided by draw.io at runtime):
* - mxShape, mxUtils, mxCellRenderer, mxConstants (mxGraph library)
* - Draw.loadPlugin (draw.io plugin API)
*/
// Import shape definitions — each registers itself via mxCellRenderer.registerShape()
import './shapes/StatusIndicator';
import './shapes/ProgressBar';
import './shapes/DataFlowArrow';
import './shapes/HexagonCluster';
import './shapes/MetricGauge';
// Import and execute palette registration
import { registerPalette } from './palette';
// Register the plugin with draw.io
Draw.loadPlugin(function(ui) {
registerPalette(ui);
console.log('[CustomShapes] Plugin loaded successfully — 5 shapes registered');
});
+116
View File
@@ -0,0 +1,116 @@
/**
* Palette Registration
*
* Creates a sidebar palette section with all custom shapes available
* for drag-and-drop onto the canvas.
*/
export function registerPalette(ui) {
var sb = ui.sidebar;
// Define palette entries — each creates a draggable template
var fns = [
// Status Indicator variants
sb.createVertexTemplateEntry(
'shape=customShapes.statusIndicator;status=ok;showLabel=1;fillColor=none;strokeColor=none;',
60, 40, '', 'Status: OK',
null, null, 'status indicator ok green check'
),
sb.createVertexTemplateEntry(
'shape=customShapes.statusIndicator;status=warning;showLabel=1;fillColor=none;strokeColor=none;',
60, 40, '', 'Status: Warning',
null, null, 'status indicator warning orange'
),
sb.createVertexTemplateEntry(
'shape=customShapes.statusIndicator;status=error;showLabel=1;fillColor=none;strokeColor=none;',
60, 40, '', 'Status: Error',
null, null, 'status indicator error red'
),
sb.createVertexTemplateEntry(
'shape=customShapes.statusIndicator;status=unknown;showLabel=1;fillColor=none;strokeColor=none;',
60, 40, '', 'Status: Unknown',
null, null, 'status indicator unknown grey'
),
// Wide pill variant
sb.createVertexTemplateEntry(
'shape=customShapes.statusIndicator;status=ok;showLabel=1;fillColor=none;strokeColor=none;',
120, 30, '', 'Status Pill: OK',
null, null, 'status pill indicator wide'
),
// Progress Bar variants
sb.createVertexTemplateEntry(
'shape=customShapes.progressBar;progress=75;barColor=#4CAF50;showPercent=1;fillColor=none;strokeColor=none;',
200, 30, '', 'Progress Bar (75%)',
null, null, 'progress bar percentage loading'
),
sb.createVertexTemplateEntry(
'shape=customShapes.progressBar;progress=30;barColor=#FF9800;showPercent=1;fillColor=none;strokeColor=none;',
200, 30, '', 'Progress Bar (30%)',
null, null, 'progress bar low warning'
),
sb.createVertexTemplateEntry(
'shape=customShapes.progressBar;progress=90;barColor=#4CAF50;showPercent=1;animated=1;fillColor=none;strokeColor=none;',
200, 30, '', 'Progress Bar Animated (90%)',
null, null, 'progress bar animated stripes'
),
// Data Flow Arrow variants
sb.createVertexTemplateEntry(
'shape=customShapes.dataFlowArrow;direction=right;flowSpeed=medium;flowColor=#1976D2;fillColor=none;strokeColor=none;',
180, 40, '', 'Data Flow → Right',
null, null, 'data flow arrow right direction'
),
sb.createVertexTemplateEntry(
'shape=customShapes.dataFlowArrow;direction=left;flowSpeed=medium;flowColor=#1976D2;fillColor=none;strokeColor=none;',
180, 40, '', 'Data Flow ← Left',
null, null, 'data flow arrow left direction'
),
sb.createVertexTemplateEntry(
'shape=customShapes.dataFlowArrow;direction=both;flowSpeed=fast;flowColor=#7B1FA2;fillColor=none;strokeColor=none;',
180, 40, '', 'Data Flow ↔ Bidirectional',
null, null, 'data flow arrow bidirectional both'
),
// Hexagon Cluster variants
sb.createVertexTemplateEntry(
'shape=customShapes.hexCluster;hexCount=3;hexFill=#E3F2FD;hexStroke=#1565C0;fillColor=none;strokeColor=none;',
120, 120, '', 'Hex Cluster (3x)',
null, null, 'hexagon cluster grid honeycomb'
),
sb.createVertexTemplateEntry(
'shape=customShapes.hexCluster;hexCount=5;hexFill=#FFF3E0;hexStroke=#E65100;fillColor=none;strokeColor=none;',
200, 150, '', 'Hex Cluster (5x)',
null, null, 'hexagon cluster large grid'
),
sb.createVertexTemplateEntry(
'shape=customShapes.hexCluster;hexCount=4;hexFill=#E8F5E9;hexStroke=#2E7D32;filled=0,2,5;fillColor=none;strokeColor=none;',
160, 140, '', 'Hex Cluster (partial fill)',
null, null, 'hexagon cluster partial highlight'
),
// Metric Gauge variants
sb.createVertexTemplateEntry(
'shape=customShapes.metricGauge;value=75;minVal=0;maxVal=100;label=CPU;fillColor=none;strokeColor=none;',
140, 100, '', 'Metric Gauge (75/100)',
null, null, 'gauge meter speedometer metric'
),
sb.createVertexTemplateEntry(
'shape=customShapes.metricGauge;value=45;minVal=0;maxVal=100;zones=#4CAF50,#FF9800,#F44336;label=Memory;fillColor=none;strokeColor=none;',
140, 100, '', 'Metric Gauge (45/100)',
null, null, 'gauge meter memory usage'
),
sb.createVertexTemplateEntry(
'shape=customShapes.metricGauge;value=92;minVal=0;maxVal=100;zones=#4CAF50,#FF9800,#F44336;label=Disk;fillColor=none;strokeColor=none;',
140, 100, '', 'Metric Gauge Critical (92/100)',
null, null, 'gauge meter disk critical high'
),
sb.createVertexTemplateEntry(
'shape=customShapes.metricGauge;value=3.5;minVal=0;maxVal=5;zones=#F44336,#FF9800,#4CAF50;label=Rating;fillColor=none;strokeColor=none;',
140, 100, '', 'Metric Gauge (3.5/5 Rating)',
null, null, 'gauge rating stars score'
)
];
sb.addPaletteFunctions('customShapesExample', 'Custom Shapes Example', false, fns);
}
+116
View File
@@ -0,0 +1,116 @@
/**
* DataFlowArrow Shape
*
* A directional arrow with flow indicator dots/dashes.
* Demonstrates: dynamic path generation, direction-aware rendering.
*
* Style parameters:
* - direction: 'right' | 'left' | 'both' (default: 'right')
* - flowSpeed: 'fast' | 'medium' | 'slow' (default: 'medium')
* - flowColor: hex color (default: '#1976D2')
* - thickness: number (default: '3')
*/
function DataFlowArrowShape() {
mxShape.call(this);
}
mxUtils.extend(DataFlowArrowShape, mxShape);
DataFlowArrowShape.prototype.cst = {
SHAPE: 'customShapes.dataFlowArrow'
};
DataFlowArrowShape.prototype.customProperties = [
{
name: 'direction',
dispName: 'Direction',
type: 'enum',
defVal: 'right',
enumList: [
{ val: 'right', dispName: 'Right' },
{ val: 'left', dispName: 'Left' },
{ val: 'both', dispName: 'Bidirectional' }
]
},
{
name: 'flowSpeed',
dispName: 'Flow Speed',
type: 'enum',
defVal: 'medium',
enumList: [
{ val: 'fast', dispName: 'Fast' },
{ val: 'medium', dispName: 'Medium' },
{ val: 'slow', dispName: 'Slow' }
]
}
];
DataFlowArrowShape.prototype.paintVertexShape = function(c, x, y, w, h) {
c.translate(x, y);
var direction = mxUtils.getValue(this.style, 'direction', 'right');
var flowSpeed = mxUtils.getValue(this.style, 'flowSpeed', 'medium');
var flowColor = mxUtils.getValue(this.style, 'flowColor', '#1976D2');
var thickness = parseInt(mxUtils.getValue(this.style, 'thickness', '3'));
var midY = h / 2;
var arrowSize = Math.min(h / 2, 12);
var bodyStart = (direction === 'left' || direction === 'both') ? arrowSize : 0;
var bodyEnd = (direction === 'right' || direction === 'both') ? w - arrowSize : w;
// Draw main arrow body
c.setStrokeColor(flowColor);
c.setStrokeWidth(thickness);
c.setFillColor(flowColor);
// Arrow body line
c.begin();
c.moveTo(bodyStart, midY);
c.lineTo(bodyEnd, midY);
c.stroke();
// Right arrowhead
if (direction === 'right' || direction === 'both') {
c.begin();
c.moveTo(w - arrowSize, midY - arrowSize);
c.lineTo(w, midY);
c.lineTo(w - arrowSize, midY + arrowSize);
c.close();
c.fillAndStroke();
}
// Left arrowhead
if (direction === 'left' || direction === 'both') {
c.begin();
c.moveTo(arrowSize, midY - arrowSize);
c.lineTo(0, midY);
c.lineTo(arrowSize, midY + arrowSize);
c.close();
c.fillAndStroke();
}
// Flow indicator dots along the arrow body
var speeds = { fast: 8, medium: 14, slow: 22 };
var spacing = speeds[flowSpeed] || speeds.medium;
var dotRadius = Math.max(2, thickness * 0.8);
c.setFillColor('#FFFFFF');
c.setStrokeWidth(0);
for (var i = bodyStart + spacing; i < bodyEnd - spacing / 2; i += spacing) {
c.ellipse(i - dotRadius, midY - dotRadius, dotRadius * 2, dotRadius * 2);
c.fill();
}
// Outer glow/pipe effect
c.setStrokeColor(flowColor);
c.setAlpha(0.15);
c.setStrokeWidth(thickness * 3);
c.begin();
c.moveTo(bodyStart, midY);
c.lineTo(bodyEnd, midY);
c.stroke();
};
mxCellRenderer.registerShape(DataFlowArrowShape.prototype.cst.SHAPE, DataFlowArrowShape);
+135
View File
@@ -0,0 +1,135 @@
/**
* HexagonCluster Shape
*
* A grid of hexagons that adapts to the cell dimensions.
* Demonstrates: loop-based drawing, mathematical geometry, adaptive layout.
*
* Style parameters:
* - hexCount: number of hexagons across (default: '3')
* - hexFill: hex color for hexagon fill (default: '#E3F2FD')
* - hexStroke: hex color for hexagon stroke (default: '#1565C0')
* - filled: which hexagons are filled (comma-separated indices, default: all)
*/
function HexagonClusterShape() {
mxShape.call(this);
}
mxUtils.extend(HexagonClusterShape, mxShape);
HexagonClusterShape.prototype.cst = {
SHAPE: 'customShapes.hexCluster'
};
HexagonClusterShape.prototype.customProperties = [
{
name: 'hexCount',
dispName: 'Hex Count (across)',
type: 'int',
min: 1,
max: 10,
defVal: 3
},
{
name: 'hexFill',
dispName: 'Hex Fill Color',
type: 'color',
defVal: '#E3F2FD'
},
{
name: 'hexStroke',
dispName: 'Hex Stroke Color',
type: 'color',
defVal: '#1565C0'
}
];
/**
* Draw a single hexagon centered at (cx, cy) with given radius
*/
HexagonClusterShape.prototype.drawHexagon = function(c, cx, cy, radius) {
c.begin();
for (var i = 0; i < 6; i++) {
var angle = (Math.PI / 3) * i - Math.PI / 6; // flat-top orientation
var px = cx + radius * Math.cos(angle);
var py = cy + radius * Math.sin(angle);
if (i === 0) {
c.moveTo(px, py);
} else {
c.lineTo(px, py);
}
}
c.close();
};
HexagonClusterShape.prototype.paintVertexShape = function(c, x, y, w, h) {
c.translate(x, y);
var hexCount = parseInt(mxUtils.getValue(this.style, 'hexCount', '3'));
var hexFill = mxUtils.getValue(this.style, 'hexFill', '#E3F2FD');
var hexStroke = mxUtils.getValue(this.style, 'hexStroke', '#1565C0');
var filledStr = mxUtils.getValue(this.style, 'filled', '');
// Clamp
hexCount = Math.max(1, Math.min(10, hexCount));
// Calculate hex size based on available width
var padding = 4;
var availW = w - padding * 2;
var availH = h - padding * 2;
// For flat-top hexagons: width = sqrt(3) * radius, height = 2 * radius
var hexWidth = availW / hexCount;
var radius = hexWidth / Math.sqrt(3);
// How many rows fit
var rowHeight = radius * 1.5;
var rows = Math.max(1, Math.floor((availH - radius * 0.5) / rowHeight));
// Parse filled indices (which hexagons get highlight color)
var filledSet = {};
if (filledStr) {
filledStr.split(',').forEach(function(idx) {
filledSet[parseInt(idx.trim())] = true;
});
}
var index = 0;
c.setStrokeColor(hexStroke);
c.setStrokeWidth(1.5);
for (var row = 0; row < rows; row++) {
var colsInRow = (row % 2 === 0) ? hexCount : hexCount - 1;
var offsetX = (row % 2 === 0) ? 0 : hexWidth / 2;
for (var col = 0; col < colsInRow; col++) {
var cx = padding + offsetX + hexWidth / 2 + col * hexWidth;
var cy = padding + radius + row * rowHeight;
// Skip if out of bounds
if (cy + radius > h) continue;
// Determine fill: highlighted or default
var isHighlighted = filledStr === '' || filledSet[index];
c.setFillColor(isHighlighted ? hexFill : '#FAFAFA');
c.setAlpha(isHighlighted ? 1.0 : 0.5);
this.drawHexagon(c, cx, cy, radius * 0.9); // slight gap between hexagons
c.fillAndStroke();
c.setAlpha(1.0);
index++;
}
}
// Border around entire cluster
c.setStrokeColor(hexStroke);
c.setStrokeWidth(1);
c.setAlpha(0.3);
c.setDashed(true);
c.rect(0, 0, w, h);
c.stroke();
};
mxCellRenderer.registerShape(HexagonClusterShape.prototype.cst.SHAPE, HexagonClusterShape);
+179
View File
@@ -0,0 +1,179 @@
/**
* MetricGauge Shape
*
* A radial gauge (speedometer-style) showing a value within a range.
* Demonstrates: arc computation, trigonometry, zone-based coloring, text rendering.
*
* Style parameters:
* - value: current value (default: '75')
* - minVal: minimum value (default: '0')
* - maxVal: maximum value (default: '100')
* - zones: comma-separated zone colors from low to high (default: '#4CAF50,#FF9800,#F44336')
* - label: text label below the value (default: '')
* - arcWidth: width of the gauge arc (default: '8')
*/
function MetricGaugeShape() {
mxShape.call(this);
}
mxUtils.extend(MetricGaugeShape, mxShape);
MetricGaugeShape.prototype.cst = {
SHAPE: 'customShapes.metricGauge'
};
MetricGaugeShape.prototype.customProperties = [
{
name: 'value',
dispName: 'Value',
type: 'float',
defVal: 75
},
{
name: 'minVal',
dispName: 'Min Value',
type: 'float',
defVal: 0
},
{
name: 'maxVal',
dispName: 'Max Value',
type: 'float',
defVal: 100
},
{
name: 'zones',
dispName: 'Zone Colors (comma-sep)',
type: 'string',
defVal: '#4CAF50,#FF9800,#F44336'
},
{
name: 'label',
dispName: 'Label',
type: 'string',
defVal: ''
}
];
MetricGaugeShape.prototype.paintVertexShape = function(c, x, y, w, h) {
c.translate(x, y);
var value = parseFloat(mxUtils.getValue(this.style, 'value', '75'));
var minVal = parseFloat(mxUtils.getValue(this.style, 'minVal', '0'));
var maxVal = parseFloat(mxUtils.getValue(this.style, 'maxVal', '100'));
var zonesStr = mxUtils.getValue(this.style, 'zones', '#4CAF50,#FF9800,#F44336');
var label = mxUtils.getValue(this.style, 'label', '');
var arcWidth = parseInt(mxUtils.getValue(this.style, 'arcWidth', '8'));
var zones = zonesStr.split(',');
// Gauge geometry — 180° arc (semicircle)
var cx = w / 2;
var cy = h * 0.7; // center slightly below middle to leave room for text
var radius = Math.min(w / 2, h * 0.65) - arcWidth;
var startAngle = Math.PI; // left (180°)
var endAngle = 0; // right (0°)
var totalArc = Math.PI; // 180 degrees
// Draw zone arcs (background)
var zoneCount = zones.length;
var zoneArc = totalArc / zoneCount;
c.setStrokeWidth(arcWidth);
c.setLineCap('round');
for (var i = 0; i < zoneCount; i++) {
c.setStrokeColor(zones[i]);
c.setAlpha(0.3); // Background zones are translucent
var segStart = startAngle - i * zoneArc;
var segEnd = segStart - zoneArc;
// Draw arc segment using small line segments (canvas API approximation)
var steps = 20;
c.begin();
for (var s = 0; s <= steps; s++) {
var angle = segStart + (segEnd - segStart) * (s / steps);
var px = cx + radius * Math.cos(angle);
var py = cy - radius * Math.sin(angle);
if (s === 0) {
c.moveTo(px, py);
} else {
c.lineTo(px, py);
}
}
c.stroke();
}
// Draw active value arc (the "filled" portion)
var normalizedValue = Math.max(0, Math.min(1, (value - minVal) / (maxVal - minVal)));
var valueAngle = startAngle - totalArc * normalizedValue;
// Determine which zone the value falls in
var zoneIndex = Math.min(zoneCount - 1, Math.floor(normalizedValue * zoneCount));
var activeColor = zones[zoneIndex];
c.setStrokeColor(activeColor);
c.setAlpha(1.0);
c.setStrokeWidth(arcWidth);
var activeSteps = Math.max(2, Math.floor(40 * normalizedValue));
c.begin();
for (var s = 0; s <= activeSteps; s++) {
var angle = startAngle + (valueAngle - startAngle) * (s / activeSteps);
var px = cx + radius * Math.cos(angle);
var py = cy - radius * Math.sin(angle);
if (s === 0) {
c.moveTo(px, py);
} else {
c.lineTo(px, py);
}
}
c.stroke();
// Draw needle
c.setStrokeColor('#333333');
c.setStrokeWidth(2);
c.setAlpha(1.0);
var needleLength = radius * 0.85;
var needleX = cx + needleLength * Math.cos(valueAngle);
var needleY = cy - needleLength * Math.sin(valueAngle);
c.begin();
c.moveTo(cx, cy);
c.lineTo(needleX, needleY);
c.stroke();
// Needle center dot
c.setFillColor('#333333');
c.ellipse(cx - 4, cy - 4, 8, 8);
c.fill();
// Value text
c.setFontColor('#333333');
c.setFontSize(Math.min(radius * 0.4, 24));
c.setFontStyle(1); // bold
c.text(cx, cy + radius * 0.25, 0, 0, String(Math.round(value * 10) / 10),
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
// Label text
if (label) {
c.setFontColor('#666666');
c.setFontSize(Math.min(radius * 0.2, 12));
c.setFontStyle(0); // normal
c.text(cx, cy + radius * 0.45, 0, 0, label,
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
}
// Min/Max labels
c.setFontColor('#999999');
c.setFontSize(Math.min(radius * 0.15, 10));
c.text(cx - radius, cy + 12, 0, 0, String(minVal),
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_TOP, 0, null, 0, 0, 0);
c.text(cx + radius, cy + 12, 0, 0, String(maxVal),
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_TOP, 0, null, 0, 0, 0);
};
mxCellRenderer.registerShape(MetricGaugeShape.prototype.cst.SHAPE, MetricGaugeShape);
+107
View File
@@ -0,0 +1,107 @@
/**
* ProgressBar Shape
*
* A horizontal progress bar that fills based on a percentage value.
* Demonstrates: computed geometry, value-based rendering, gradient.
*
* Style parameters:
* - progress: 0-100 (default: 50)
* - barColor: hex color (default: '#4CAF50')
* - showPercent: '1' | '0' (default: '1')
* - animated: '1' | '0' — adds visual markers (default: '0')
*/
function ProgressBarShape() {
mxShape.call(this);
}
mxUtils.extend(ProgressBarShape, mxShape);
ProgressBarShape.prototype.cst = {
SHAPE: 'customShapes.progressBar'
};
ProgressBarShape.prototype.customProperties = [
{
name: 'progress',
dispName: 'Progress (%)',
type: 'int',
min: 0,
max: 100,
defVal: 50
},
{
name: 'barColor',
dispName: 'Bar Color',
type: 'color',
defVal: '#4CAF50'
}
];
ProgressBarShape.prototype.paintVertexShape = function(c, x, y, w, h) {
c.translate(x, y);
var progress = parseInt(mxUtils.getValue(this.style, 'progress', '50'));
var barColor = mxUtils.getValue(this.style, 'barColor', '#4CAF50');
var showPercent = mxUtils.getValue(this.style, 'showPercent', '1');
var animated = mxUtils.getValue(this.style, 'animated', '0');
// Clamp progress
progress = Math.max(0, Math.min(100, progress));
var borderRadius = Math.min(h / 2, 8);
var padding = 2;
// Background track
c.setFillColor('#E0E0E0');
c.setStrokeColor('#BDBDBD');
c.setStrokeWidth(1);
c.roundrect(0, 0, w, h, borderRadius, borderRadius);
c.fillAndStroke();
// Progress fill
var fillWidth = ((w - padding * 2) * progress) / 100;
if (fillWidth > 0) {
c.setStrokeWidth(0);
c.setFillColor(barColor);
// Color shifts based on progress level
if (progress < 30) {
c.setFillColor('#F44336'); // Red for low
} else if (progress < 70) {
c.setFillColor(barColor); // Default color for mid
} else {
c.setFillColor('#2E7D32'); // Dark green for high
}
// Draw filled portion with rounded left edge
c.roundrect(padding, padding, fillWidth, h - padding * 2,
borderRadius - 1, borderRadius - 1);
c.fill();
// Animated stripe markers
if (animated === '1' && fillWidth > 10) {
c.setStrokeColor('rgba(255,255,255,0.3)');
c.setStrokeWidth(2);
var stripeSpacing = 12;
for (var i = stripeSpacing; i < fillWidth - 4; i += stripeSpacing) {
c.begin();
c.moveTo(padding + i, padding);
c.lineTo(padding + i - 4, h - padding);
c.stroke();
}
}
}
// Percentage text
if (showPercent === '1') {
c.setFontColor(progress > 50 ? '#FFFFFF' : '#333333');
c.setFontSize(Math.min(h * 0.6, 14));
c.setFontStyle(1); // bold
c.text(w / 2, h / 2, 0, 0, progress + '%',
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
}
};
mxCellRenderer.registerShape(ProgressBarShape.prototype.cst.SHAPE, ProgressBarShape);
+99
View File
@@ -0,0 +1,99 @@
/**
* StatusIndicator Shape
*
* A shape that visually indicates status through color and icon.
* Demonstrates: conditional rendering, style-parameter reading.
*
* Style parameters:
* - status: 'ok' | 'warning' | 'error' | 'unknown' (default: 'unknown')
* - showLabel: '1' | '0' (default: '1')
*/
function StatusIndicatorShape() {
mxShape.call(this);
}
mxUtils.extend(StatusIndicatorShape, mxShape);
StatusIndicatorShape.prototype.cst = {
SHAPE: 'customShapes.statusIndicator'
};
StatusIndicatorShape.prototype.customProperties = [
{
name: 'status',
dispName: 'Status',
type: 'enum',
defVal: 'unknown',
enumList: [
{ val: 'ok', dispName: 'OK' },
{ val: 'warning', dispName: 'Warning' },
{ val: 'error', dispName: 'Error' },
{ val: 'unknown', dispName: 'Unknown' }
]
}
];
StatusIndicatorShape.prototype.paintVertexShape = function(c, x, y, w, h) {
c.translate(x, y);
var status = mxUtils.getValue(this.style, 'status', 'unknown');
var showLabel = mxUtils.getValue(this.style, 'showLabel', '1');
// Status-dependent colors
var colors = {
ok: { fill: '#4CAF50', stroke: '#2E7D32', icon: '✓' },
warning: { fill: '#FF9800', stroke: '#E65100', icon: '⚠' },
error: { fill: '#F44336', stroke: '#B71C1C', icon: '✗' },
unknown: { fill: '#9E9E9E', stroke: '#424242', icon: '?' }
};
var cfg = colors[status] || colors.unknown;
// Outer rounded rectangle (the indicator body)
c.setFillColor(cfg.fill);
c.setStrokeColor(cfg.stroke);
c.setStrokeWidth(2);
// Draw shape — circle if square-ish, pill if wide
if (w > h * 1.5) {
// Pill shape
var radius = h / 2;
c.begin();
c.moveTo(radius, 0);
c.lineTo(w - radius, 0);
c.arcTo(radius, radius, 0, 0, 1, w - radius, h);
c.lineTo(radius, h);
c.arcTo(radius, radius, 0, 0, 1, radius, 0);
c.close();
c.fillAndStroke();
} else {
// Circle
var size = Math.min(w, h);
var cx = w / 2;
var cy = h / 2;
c.ellipse(cx - size / 2, cy - size / 2, size, size);
c.fillAndStroke();
}
// Inner icon/symbol
if (showLabel === '1') {
c.setFontColor('#FFFFFF');
c.setFontSize(Math.min(w, h) * 0.4);
c.setFontStyle(1); // bold
c.text(w / 2, h / 2, 0, 0, cfg.icon,
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
}
// Pulse ring for 'error' status (visual emphasis)
if (status === 'error') {
c.setStrokeColor(cfg.fill);
c.setStrokeWidth(1);
c.setAlpha(0.3);
var padding = 4;
c.ellipse(-padding, -padding, w + padding * 2, h + padding * 2);
c.stroke();
}
};
mxCellRenderer.registerShape(StatusIndicatorShape.prototype.cst.SHAPE, StatusIndicatorShape);
+16
View File
@@ -0,0 +1,16 @@
const path = require('path');
module.exports = (env, argv) => ({
entry: './src/index.js',
output: {
filename: 'drawio-custom-shapes.js',
path: path.resolve(__dirname, 'dist'),
// No library wrapper — draw.io plugins are plain scripts
// that call Draw.loadPlugin() as a side effect
iife: true
},
devtool: argv.mode === 'development' ? 'source-map' : false,
optimization: {
minimize: argv.mode === 'production'
}
});