diff --git a/examples/dot/.gitignore b/examples/dot/.gitignore
new file mode 100644
index 0000000000..69c575d17f
--- /dev/null
+++ b/examples/dot/.gitignore
@@ -0,0 +1,3 @@
+build/
+dist/
+node_modules/
diff --git a/examples/dot/README.md b/examples/dot/README.md
new file mode 100644
index 0000000000..2a003e0424
--- /dev/null
+++ b/examples/dot/README.md
@@ -0,0 +1,22 @@
+# JointJS List Demo
+
+## Setup
+
+Use Yarn to run this demo.
+
+You need to build *JointJS* first. Navigate to the root folder and run:
+```bash
+yarn install
+yarn run build
+```
+
+Navigate to this directory, then run:
+```bash
+yarn start
+```
+
+## License
+
+The *JointJS* library is licensed under the [Mozilla Public License 2.0](https://github.com/clientIO/joint/blob/master/LICENSE).
+
+Copyright © 2013-2024 client IO
diff --git a/examples/dot/css/dot.css b/examples/dot/css/dot.css
new file mode 100644
index 0000000000..400324d859
--- /dev/null
+++ b/examples/dot/css/dot.css
@@ -0,0 +1,8 @@
+#paper-container {
+ position: absolute;
+ right: 0;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ overflow: scroll;
+ }
diff --git a/examples/dot/index.html b/examples/dot/index.html
new file mode 100644
index 0000000000..5cdc5ba6e8
--- /dev/null
+++ b/examples/dot/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Fault Tree Analysis | JointJS
+
+
+
+
+
+
diff --git a/examples/dot/package.json b/examples/dot/package.json
new file mode 100644
index 0000000000..363e9e4af1
--- /dev/null
+++ b/examples/dot/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@joint/demo-graphviz-dot-layout",
+ "version": "4.0.1",
+ "main": "src/index.js",
+ "homepage": "https://jointjs.com",
+ "author": {
+ "name": "client IO",
+ "url": "https://client.io"
+ },
+ "license": "MPL-2.0",
+ "private": true,
+ "installConfig": {
+ "hoistingLimits": "workspaces"
+ },
+ "scripts": {
+ "start": "webpack-dev-server",
+ "tsc": "tsc"
+ },
+ "dependencies": {
+ "@hpcc-js/wasm": "^2.15.3",
+ "@joint/core": "workspace:^"
+ },
+ "devDependencies": {
+ "css-loader": "3.5.3",
+ "style-loader": "1.2.1",
+ "webpack": "^5.61.0",
+ "webpack-cli": "^4.8.0",
+ "webpack-dev-server": "^4.2.1"
+ },
+ "volta": {
+ "node": "16.18.1",
+ "npm": "8.19.2",
+ "yarn": "3.4.1"
+ }
+}
diff --git a/examples/dot/src/index.js b/examples/dot/src/index.js
new file mode 100644
index 0000000000..fe90f93bb8
--- /dev/null
+++ b/examples/dot/src/index.js
@@ -0,0 +1,314 @@
+import { dia, elementTools, shapes as defaultShapes, util, V, g } from '@joint/core';
+import { Graphviz } from "@hpcc-js/wasm/graphviz";
+
+ import '../css/dot.css'
+
+const graph = new dia.Graph({}, { cellNamespace: defaultShapes });
+
+const paper = new dia.Paper({
+ width: '100%',
+ height: '100%',
+ model: graph,
+ // defaultConnectionPoint: { name: 'boundary', args: { offset: 20 }},
+ // defaultConnector: {
+ // name: 'straight',
+ // args: { cornerType: 'line', cornerRadius: 10 },
+ // },
+ // defaultRouter: { name: 'orthogonal' },
+ defaultConnector: { name: 'curve' },
+ defaultConnectionPoint: { name: 'anchor' },
+ async: true,
+ // interactive: false,
+ frozen: true,
+ cellViewNamespace: defaultShapes,
+ background: { color: '#131e29' }
+});
+
+document.getElementById('paper-container').appendChild(paper.el);
+
+const diagram = {
+ 'objects': [
+ {
+ 'id': 'pm',
+ 'data': {
+ 'nodes': [
+ {
+ 'node_id': 0,
+ },
+ {
+ 'node_id': 6,
+ },
+ {
+ 'node_id': 8,
+ },
+ {
+ 'node_id': 5,
+ },
+ {
+ 'node_id': 3,
+ },
+ {
+ 'node_id': 9,
+ },
+ {
+ 'node_id': 1,
+ },
+ {
+ 'node_id': 4,
+ },
+ {
+ 'node_id': 7,
+ },
+ {
+ 'node_id': 11,
+ },
+ {
+ 'node_id': 10,
+ },
+ {
+ 'node_id': 2,
+ },
+ {
+ 'node_id': 12,
+ }
+ ],
+ 'edges': [
+ {
+ 'node_id_from': 5,
+ 'node_id_to': 1,
+ },
+ {
+ 'node_id_from': 5,
+ 'node_id_to': 3,
+ },
+ {
+ 'node_id_from': 5,
+ 'node_id_to': 5,
+ },
+ {
+ 'node_id_from': 5,
+ 'node_id_to': 9,
+ },
+ {
+ 'node_id_from': 9,
+ 'node_id_to': 3,
+ },
+ {
+ 'node_id_from': 9,
+ 'node_id_to': 4,
+ },
+ {
+ 'node_id_from': 10,
+ 'node_id_to': 2,
+ },
+ {
+ 'node_id_from': 8,
+ 'node_id_to': 5,
+ },
+ {
+ 'node_id_from': 8,
+ 'node_id_to': 8,
+ },
+ {
+ 'node_id_from': 1,
+ 'node_id_to': 2,
+ },
+ {
+ 'node_id_from': 3,
+ 'node_id_to': 4,
+ },
+ {
+ 'node_id_from': 3,
+ 'node_id_to': 9,
+ },
+ {
+ 'node_id_from': 6,
+ 'node_id_to': 8,
+ },
+ {
+ 'node_id_from': 11,
+ 'node_id_to': 12,
+ },
+ {
+ 'node_id_from': 11,
+ 'node_id_to': 7,
+ },
+ {
+ 'node_id_from': 11,
+ 'node_id_to': 10,
+ },
+ {
+ 'node_id_from': 0,
+ 'node_id_to': 6,
+ },
+ {
+ 'node_id_from': 2,
+ 'node_id_to': 12,
+ },
+ {
+ 'node_id_from': 7,
+ 'node_id_to': 10,
+ },
+ {
+ 'node_id_from': 7,
+ 'node_id_to': 11,
+ },
+ {
+ 'node_id_from': 4,
+ 'node_id_to': 7,
+ },
+ {
+ 'node_id_from': 4,
+ 'node_id_to': 11,
+ }
+ ]
+ }
+ }
+ ]
+};
+
+const cells = [];
+diagram.objects[0].data.nodes.forEach((node) => {
+ cells.push({
+ id: `${node.node_id}`,
+ type: 'standard.Ellipse',
+ size: { width: 32, height: 32 },
+ // position: { x: Math.random() * 800, y: Math.random() * 800 },
+ attrs: {
+ // label: { text: node.node_name },
+ label: { text: `${node.node_id}` },
+ body: { fill: '#3498db', stroke: '#2980b9' }
+ },
+ z: 2
+ });
+});
+
+diagram.objects[0].data.edges.forEach((edge) => {
+ cells.push({
+ type: 'standard.Link',
+ source: { id: `${edge.node_id_from}` },
+ target: { id: `${edge.node_id_to}` },
+ z: -1,
+
+ attrs: {
+ line: {
+ stroke: 'white',
+ targetMarker: {
+ 'type': 'path',
+ 'd': 'M 6 -3 -2 0 6 3 z'
+ }
+ }
+ }
+ });
+});
+
+const variants = ['0', '6', '8', '5', '9', '4', '11'];
+
+graph.fromJSON({ cells: cells });
+
+Graphviz.load().then(graphviz => {
+ const links = graph.getLinks().map(link => {
+ const source = link.source().id.replace(/-/g, '_');
+ const target = link.target().id.replace(/-/g, '_');
+ console.log(source, target);
+ return `${source} -> ${target};`;
+ });
+
+ const elements = graph.getElements().map(element => element.id.replace(/-/g, '_'));
+ const elementAttributes = '' && elements.map(v => `${v} [width=2 height=2];`).join(' ');
+ const dot = `digraph G { rankdir=LR; node [fixedsize=true width=0.5 height=0.5] edge [arrowhead=none] ${links.join(' ')} { rank=same; ${variants.join(', ')} } }`;
+ console.log(dot)
+
+ // const dot = `digraph G { rankdir=TB; ordering="in"; ${variants.map(v => `${v} [group=1];`).join(' ')} ${links.join(' ')} }`;
+
+ const result = JSON.parse(graphviz.layout(dot, 'json', 'dot', { yInvert: true }));
+
+ result.objects.forEach(obj => {
+ const id = obj.name.replace(/_/g, '-');
+ const element = graph.getCell(id);
+ if (!element) {
+ return;
+ }
+ if (variants.includes(id)) {
+ element.attr('body/stroke', '#e74c3c');
+ }
+ const [x,y] = obj.pos.split(',').map(Number);
+ const { width, height } = element.size();
+ element.position(x - width / 2, y - height / 2);
+ });
+
+ graph.getLinks().forEach((link, index) => {
+
+
+ const rawPoints = result.edges[index]._draw_[1].points.map(([x,y]) => { return { x, y } });
+
+ let points = rawPoints.slice();
+
+ function showPoints(points) {
+ points.forEach((point, index) => {
+ V('circle').attr({ r: 3, fill: 'red', cx: point.x, cy: point.y }).appendTo(paper.viewport);
+ });
+ }
+
+ // points = points.filter((_, index) => index === 0 || ((index + 1) % 3 === 0));
+
+ // points = points.filter((_, index) => index === 0);
+
+ // showPoints(points);
+
+ // x1,c1, c2a, x2, c2b, c3, x3
+ // 0 1 2 3 0 1 2 3
+ // find first point, the last point and points in between
+ // const first = points[0];
+ // const last = points[points.length - 1];
+ // const middle = points.slice(1, points.length - 1);
+
+ // showPoints([first]);
+
+ const [first, second] = rawPoints;
+ const [beforeLast, last] = rawPoints.slice(-2);
+
+ const sourceCenter = link.getSourceCell().getBBox().center();
+ const targetCenter = link.getTargetCell().getBBox().center();
+
+ const sourceVector = new g.Point(second).difference(first);
+ const targetVector = new g.Point(beforeLast).difference(last);
+
+ const { x: dx1, y: dy1 } = new g.Point(first).difference(link.getSourceCell().getBBox().center());
+ const { x: dx2, y: dy2 } = new g.Point(last).difference(link.getTargetCell().getBBox().center());
+
+ link.prop({
+ source: { anchor: { name: 'modelCenter', args: { dx: dx1, dy: dy1 }}},
+ target: { anchor: { name: 'modelCenter', args: { dx: dx2, dy: dy2 }}}
+ });
+
+ link.connector('curve', {
+ sourceTangent: sourceVector,
+ targetTangent: targetVector,
+ });
+
+ // showPoints([beforeLast, last]);
+ // showPoints([rawPoints[0], rawPoints[1]]);
+
+ const vertices = rawPoints.filter((_, index) => ((index + 1) % 3 === 0));
+ if (link.hasLoop()) {
+ // showPoints(rawPoints);
+ link.vertices([rawPoints[3]])
+ }
+ // link.set('vertices', vertices.slice(0, -1));
+ // console.log(vertices.slice(0, -1))
+ });
+
+ // graph.getLinks()[1].attr('line/stroke', '#e74c3c');
+
+ // console.log(result);
+
+ paper.transformToFitContent({
+ padding: 15,
+ contentArea: graph.getBBox(),
+ horizontalAlign: 'middle',
+ });
+
+ paper.unfreeze();
+
+});
diff --git a/examples/dot/webpack.config.js b/examples/dot/webpack.config.js
new file mode 100644
index 0000000000..20663a5d3b
--- /dev/null
+++ b/examples/dot/webpack.config.js
@@ -0,0 +1,29 @@
+const path = require('path');
+
+module.exports = {
+ resolve: {
+ extensions: ['.js']
+ },
+ entry: './src/index.js',
+ output: {
+ filename: 'bundle.js',
+ path: path.resolve(__dirname, 'dist'),
+ publicPath: '/dist/'
+ },
+ mode: 'development',
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ sideEffects: true,
+ use: ['style-loader', 'css-loader'],
+ }
+ ]
+ },
+ devServer: {
+ static: {
+ directory: __dirname,
+ },
+ compress: true
+ },
+};