diff --git a/README.md b/README.md
index a6da32d6..94d4bfb5 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,7 @@ especially when using `bpmn-visualization` prior version `0.27.0`.
- [Hacktoberfest themes](demo/hacktoberfest-custom-themes/README.md) - special Hacktoberfest diagram with Hacktoberfest colors
- [Monitoring of all process instances demo](demo/monitoring-all-process-instances/README.md) - show how to use `bpmn-visualization` to render the monitoring of all process instances for a defined process
- [Demo for ICPM 2022](demo/icpm-2022/README.md) - show a Process Mining scenario (_Conformance_, _Compliance_ and _Happy path_)
+- [Draw a path](demo/draw-path/README.md) - show how to draw a path
## Tutorials
diff --git a/demo/draw-path/README.md b/demo/draw-path/README.md
new file mode 100644
index 00000000..15666ea3
--- /dev/null
+++ b/demo/draw-path/README.md
@@ -0,0 +1,5 @@
+# Draw me a path
+
+Javascript example to demonstrate how to use `bpmn-visualization` to draw a path.
+- [__⏩ live environment__](https://cdn.statically.io/gh/process-analytics/bpmn-visualization-examples/master/demo/draw-path/index.html)
+- to run locally, open the [index.html](index.html) directly in a Web Browser
diff --git a/demo/draw-path/css/legend.css b/demo/draw-path/css/legend.css
new file mode 100644
index 00000000..da3aa88f
--- /dev/null
+++ b/demo/draw-path/css/legend.css
@@ -0,0 +1,41 @@
+fieldset {
+ border: 1px solid#ddd;
+ padding: 5px 12px;
+}
+
+legend {
+ font-size: 18px;
+ padding: 0 10px;
+ margin: auto;
+}
+
+.legend li:first-child::before {
+ background-color: var(--color-disableAll);
+}
+.legend li:nth-child(2)::before {
+ background-color: var(--color-highlight);
+}
+.legend li:nth-child(3)::before {
+ background-color: var(--color-possibleNext);
+}
+
+.legend li::before {
+ content: "";
+ display: inline-block;
+ vertical-align: middle;
+ margin-inline-end: .5rem;
+ border-width: 2px;
+ border-style: solid;
+ width: 1rem;
+ height: 1rem;
+ border-radius: 3px;
+ border-color: #000;
+}
+
+.legend li {
+ line-height: 1rem;
+}
+.legend {
+ list-style: none;
+ font-size: 0.8rem;
+}
diff --git a/demo/draw-path/css/path.css b/demo/draw-path/css/path.css
new file mode 100644
index 00000000..ea82e4fa
--- /dev/null
+++ b/demo/draw-path/css/path.css
@@ -0,0 +1,83 @@
+:root {
+ --color-disableAll: Silver;
+ --color-highlight: LightSeaGreen;
+ --color-possibleNext: MediumVioletRed;
+}
+
+#use-case-panel {
+ padding-bottom: 0;
+}
+
+.bpmn-type-event:hover, .bpmn-type-gateway:hover, .bpmn-type-activity:hover, .bpmn-type-flow:hover {
+ cursor: pointer;
+}
+
+.disablePointer.bpmn-type-event:hover, .disablePointer.bpmn-type-gateway:hover, .disablePointer.bpmn-type-activity:hover, .disablePointer.bpmn-type-flow:hover {
+ cursor: default;
+}
+
+/* ------------------------------------------------ DISABLE EVERYTHING ------------------------------------------------ */
+/* SHAPE & EDGE */
+.disableAll.bpmn-type-activity > *,
+.disableAll.bpmn-type-event > *,
+.disableAll.bpmn-type-gateway > *,
+.disableAll.bpmn-type-flow > * {
+ stroke: var(--color-disableAll);
+}
+
+/* ICON */
+.disableAll.bpmn-type-gateway > path:nth-child(2), .disableAll.bpmn-type-flow > path:nth-child(3) {
+ fill: var(--color-disableAll);
+}
+
+/* LABEL */
+.disableAll.bpmn-type-activity > g > foreignObject > div > div > div,
+.disableAll.bpmn-type-event > g > foreignObject > div > div > div,
+.disableAll.bpmn-type-gateway > g > foreignObject > div > div > div,
+.disableAll.bpmn-type-flow > g > foreignObject > div > div > div {
+ color: var(--color-disableAll) !important;
+}
+
+/* ------------------------------------------------ HIGHLIGHT ------------------------------------------------ */
+/* SHAPE */
+.highlight.bpmn-type-activity > rect:first-child,
+.highlight.bpmn-type-event > ellipse:first-child,
+.highlight.bpmn-type-gateway > path:first-child,
+.highlight.bpmn-type-flow > * {
+ stroke: var(--color-highlight);
+ filter: drop-shadow(0 0 0.75rem var(--color-highlight));
+}
+
+/* ICON */
+.highlight.bpmn-type-gateway > :not(:first-child) {
+ stroke: var(--color-highlight);
+}
+
+.highlight.bpmn-type-gateway > path:nth-child(2), .highlight.bpmn-type-flow > path:nth-child(3) {
+ fill: var(--color-highlight);
+}
+
+/* ------------------------------------------------ POSSIBLE NEXT ------------------------------------------------ */
+/* SHAPE & EDGE */
+.possibleNext.bpmn-type-activity > *,
+.possibleNext.bpmn-type-event > *,
+.possibleNext.bpmn-type-gateway > *,
+.possibleNext.bpmn-type-flow > * {
+ stroke: var(--color-possibleNext) !important;
+}
+
+/* ICON */
+.possibleNext.bpmn-type-gateway > path:nth-child(2), .possibleNext.bpmn-type-flow > path:nth-child(3) {
+ fill: var(--color-possibleNext) !important;
+}
+
+/* LABEL */
+.possibleNext.bpmn-type-activity > g > foreignObject > div > div > div,
+.possibleNext.bpmn-type-event > g > foreignObject > div > div > div,
+.possibleNext.bpmn-type-gateway > g > foreignObject > div > div > div,
+.possibleNext.bpmn-type-flow > g > foreignObject > div > div > div {
+ color: var(--color-possibleNext) !important;
+}
+
+
+
diff --git a/demo/draw-path/css/step.css b/demo/draw-path/css/step.css
new file mode 100644
index 00000000..74ed2116
--- /dev/null
+++ b/demo/draw-path/css/step.css
@@ -0,0 +1,33 @@
+/* To override the kind of the children of a .step of SPECTRE.CSS */
+.step .step-item.active ~ .step-item div {
+ color: #bcc3ce;
+}
+.step .step-item div {
+ color: #5755d9;
+ display: inline-block;
+ padding: 20px 10px 0;
+ text-decoration: none;
+}
+
+.step .step-item.active ~ .step-item div::before {
+ background: #dadee4;
+}
+
+.step .step-item.active div::before {
+ background: #fff;
+ border: .1rem solid #5755d9;
+}
+.step .step-item div::before {
+ background: #5755d9;
+ border: .1rem solid #fff;
+ border-radius: 50%;
+ content: "";
+ display: block;
+ height: .6rem;
+ left: 50%;
+ position: absolute;
+ top: .2rem;
+ transform: translateX(-50%);
+ width: .6rem;
+ z-index: 1;
+}
\ No newline at end of file
diff --git a/demo/draw-path/index.html b/demo/draw-path/index.html
new file mode 100644
index 00000000..36180c65
--- /dev/null
+++ b/demo/draw-path/index.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+ bpmn-visualization - Draw me a path
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Draw me a path
+
Visualize the path between two consecutive shapes.
+
+
+
+
+
+
+
Progress
+
+ -
+
Click on a shape
+
+ -
+
+
Click on a next shape
+
+
+ -
+
Reset
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/draw-path/js/index.js b/demo/draw-path/js/index.js
new file mode 100644
index 00000000..42abcb3b
--- /dev/null
+++ b/demo/draw-path/js/index.js
@@ -0,0 +1,8 @@
+// Initialize UseCase
+const useCase = new PathUseCase(getHardwareRetailerDiagram);
+
+document.addEventListener('DOMContentLoaded', function () {
+ // Waiting for the displayed page before to load diagram & display data
+ useCase.display();
+})
+
diff --git a/demo/draw-path/js/path-use-case.js b/demo/draw-path/js/path-use-case.js
new file mode 100644
index 00000000..50e5be8e
--- /dev/null
+++ b/demo/draw-path/js/path-use-case.js
@@ -0,0 +1,208 @@
+class PathUseCase extends UseCase {
+
+ _state;
+
+ _bpmnElementIds;
+
+ _steps;
+
+ constructor(getDiagram) {
+ super('path', getDiagram, true);
+
+ this._state = {
+ firstSelectedShape: undefined,
+ secondSelectedShape: undefined,
+ };
+
+ this._steps = new Steps();
+ }
+
+ display() {
+ super.display();
+
+ const shapes = this._getShapes();
+ const allEdges = this._getAllEdges();
+ this._bpmnElementIds = [...shapes, ...allEdges].map(shapeOrEdge => shapeOrEdge.bpmnSemantic.id);
+ const endEventIds = shapes.filter(shape => shape.bpmnSemantic.kind === bpmnvisu.ShapeBpmnElementKind.EVENT_END).map(endEvent => endEvent.bpmnSemantic.id);
+
+ this._configureShapeHandlers(shapes, endEventIds);
+ this._configureEdgeHandlers(allEdges, endEventIds);
+
+ document.getElementById('btn-reset').onclick = () => {
+ this._reset();
+ this._disablePointerOn(endEventIds);
+ };
+ }
+
+ _getShapes() {
+ return this._bpmnVisualization.bpmnElementsRegistry.getElementsByKinds(
+ Object.values(bpmnvisu.ShapeBpmnElementKind).filter(kind =>
+ kind !== bpmnvisu.ShapeBpmnElementKind.LANE &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.POOL &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.GROUP &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.TEXT_ANNOTATION &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.GLOBAL_TASK &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.GLOBAL_TASK_BUSINESS_RULE &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.GLOBAL_TASK_MANUAL &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.GLOBAL_TASK_SCRIPT &&
+ kind !== bpmnvisu.ShapeBpmnElementKind.GLOBAL_TASK_USER)
+ ) ;
+ }
+
+ _getAllEdges() {
+ return this._bpmnVisualization.bpmnElementsRegistry.getElementsByKinds(Object.values(bpmnvisu.FlowKind));
+ }
+
+ _configureShapeHandlers(allShapes, endEventIds) {
+ allShapes.forEach(item => {
+ const currentId = item.bpmnSemantic.id;
+
+ item.htmlElement.onclick = () => {
+ if (!this._isEndEvent(item) && this._state.firstSelectedShape && this._state.secondSelectedShape) {
+ this._reset();
+ }
+
+ if (!this._isEndEvent(item) && !this._state.firstSelectedShape) {
+ this._disableAllShapesAndEdgesExcept([currentId]);
+ this._highlight(currentId);
+ this._state.firstSelectedShape = currentId;
+ this._steps.goToStep2();
+ } else if (this._state.firstSelectedShape) {
+ this._doActionBeforeSecondShapeSelection(currentId, (filteredPath) => {
+ this._highlight([filteredPath.edgeId, filteredPath.targetId]);
+ this._activatePointerOn(this._bpmnElementIds.filter(id => !endEventIds.includes(id)));
+ this._state.secondSelectedShape = currentId;
+ this._steps.goToStep3();
+ });
+ }
+ };
+ item.htmlElement.onmouseenter = () => {
+ if (!this._isEndEvent(item) && (!this._state.firstSelectedShape || (this._state.firstSelectedShape && this._state.secondSelectedShape))) {
+ this._displayPossibleNextElements(currentId);
+ } else {
+ this._doActionBeforeSecondShapeSelection(currentId, (filteredPath) => this._displayPossibleNextPath(filteredPath));
+ }
+ };
+ item.htmlElement.onmouseleave = () => {
+ if (!this._isEndEvent(item) && (!this._state.firstSelectedShape || (this._state.firstSelectedShape && this._state.secondSelectedShape))) {
+ this._nonDisplayPossibleNextElements(currentId);
+ } else {
+ this._doActionBeforeSecondShapeSelection(currentId, (filteredPath) => this._nonDisplayPossibleNextPath(filteredPath));
+ }
+ };
+ });
+ this._disablePointerOn(endEventIds);
+ }
+
+ _isEndEvent(item) {
+ return item.bpmnSemantic.kind === bpmnvisu.ShapeBpmnElementKind.EVENT_END;
+ }
+
+ _configureEdgeHandlers(allEdges, endEventIds) {
+ allEdges.forEach(item => {
+ const currentId = item.bpmnSemantic.id;
+
+ item.htmlElement.onclick = () => {
+ if (this._state.firstSelectedShape && this._state.secondSelectedShape) {
+ this._reset();
+ }
+
+ this._doActionOnEdge(currentId, (filteredPath) => {
+ if (!this._state.firstSelectedShape) {
+ this._disableAllShapesAndEdgesExcept([filteredPath.sourceId]);
+ this._highlight(filteredPath.sourceId);
+ this._state.firstSelectedShape = filteredPath.sourceId;
+ }
+ this._highlight([filteredPath.edgeId, filteredPath.targetId]);
+ this._activatePointerOn(this._bpmnElementIds.filter(id => !endEventIds.includes(id)));
+ this._state.secondSelectedShape = filteredPath.targetId;
+ this._steps.goToStep3();
+ });
+ };
+ item.htmlElement.onmouseenter = () => {
+ this._doActionOnEdge(currentId, (filteredPath) => this._displayPossibleNextPath(filteredPath));
+ };
+ item.htmlElement.onmouseleave = () => {
+ this._doActionOnEdge(currentId, (filteredPath) => this._nonDisplayPossibleNextPath(filteredPath));
+ };
+ });
+ }
+
+ _doActionBeforeSecondShapeSelection(possibleSecondShapeId, action) {
+ if (this._state.firstSelectedShape && !this._state.secondSelectedShape) {
+ const filteredPaths = paths.filter(path => path.sourceId === this._state.firstSelectedShape && path.targetId === possibleSecondShapeId);
+ if (filteredPaths.length > 0) {
+ action(filteredPaths[0]);
+ }
+ }
+ }
+
+ _doActionOnEdge(edgeId, action) {
+ if (!this._state.secondSelectedShape || (this._state.secondSelectedShape && this._state.firstSelectedShape)) {
+ const filteredPaths = paths.filter(path => ((!this._state.secondSelectedShape && this._state.firstSelectedShape) ? path.sourceId === this._state.firstSelectedShape : true) && path.edgeId === edgeId);
+ if (filteredPaths.length > 0) {
+ action(filteredPaths[0]);
+ }
+ }
+ }
+
+ _reset() {
+ this._bpmnVisualization.bpmnElementsRegistry.removeCssClasses(this._bpmnElementIds, ['disableAll', 'possibleNext', 'highlight', 'disablePointer']);
+ this._state.firstSelectedShape = undefined;
+ this._state.secondSelectedShape = undefined;
+ this._steps.reset();
+ }
+
+ _displayPossibleNextPath(path) {
+ const ids = [path.edgeId, path.targetId];
+ (!this._state.firstSelectedShape || (this._state.secondSelectedShape && this._state.firstSelectedShape)) ? ids.push(path.sourceId) : this._activatePointerOn(ids);
+ this._displayPossibleNextElements(ids);
+ }
+
+ _displayPossibleNextElements(ids) {
+ this._bpmnVisualization.bpmnElementsRegistry.addCssClasses(ids, 'possibleNext');
+ }
+
+ _nonDisplayPossibleNextPath(path) {
+ const ids = [path.edgeId, path.targetId];
+ (!this._state.firstSelectedShape || (this._state.secondSelectedShape && this._state.firstSelectedShape)) ? ids.push(path.sourceId) : this._disablePointerOn(ids);
+ this._nonDisplayPossibleNextElements(ids);
+ }
+
+ _nonDisplayPossibleNextElements(ids) {
+ this._bpmnVisualization.bpmnElementsRegistry.removeCssClasses(ids, 'possibleNext');
+ }
+
+ /**
+ * @param ids can be an array or a string
+ * @private
+ */
+ _highlight(ids) {
+ this._bpmnVisualization.bpmnElementsRegistry.removeCssClasses(ids, ['disableAll', 'possibleNext']);
+ this._bpmnVisualization.bpmnElementsRegistry.addCssClasses(ids, ['highlight', 'disablePointer']);
+ }
+
+ /**
+ * @param ids must be an array
+ * @private
+ */
+ _disableAllShapesAndEdgesExcept(ids) {
+ this._bpmnVisualization.bpmnElementsRegistry.addCssClasses(this._bpmnElementIds.filter(shapeOrEdge => !ids.includes(shapeOrEdge)), ['disableAll', 'disablePointer']);
+ }
+
+ /**
+ * @param ids can be an array or a string
+ * @private
+ */
+ _disablePointerOn(ids) {
+ this._bpmnVisualization.bpmnElementsRegistry.addCssClasses(ids, 'disablePointer');
+ }
+
+ /**
+ * @param ids can be an array or a string
+ * @private
+ */
+ _activatePointerOn(ids) {
+ this._bpmnVisualization.bpmnElementsRegistry.removeCssClasses(ids, 'disablePointer');
+ }
+}
diff --git a/demo/draw-path/js/paths.js b/demo/draw-path/js/paths.js
new file mode 100644
index 00000000..2603d771
--- /dev/null
+++ b/demo/draw-path/js/paths.js
@@ -0,0 +1,44 @@
+class Path {
+ _sourceId ;
+ _edgeId;
+ _targetId;
+
+ constructor(sourceId, edgeId, targetId) {
+ this._sourceId = sourceId;
+ this._edgeId = edgeId;
+ this._targetId = targetId;
+ }
+
+ get sourceId() {
+ return this._sourceId;
+ }
+
+ get edgeId() {
+ return this._edgeId;
+ }
+
+ get targetId() {
+ return this._targetId;
+ }
+}
+
+const paths = [
+ new Path('start_event', "sequence_flow_1", "parallel_gateway_1"),
+ new Path('parallel_gateway_1', "sequence_flow_2", "task_1"),
+ new Path('parallel_gateway_1', "sequence_flow_18", "task_2"),
+ new Path('task_1', "sequence_flow_3", "exclusive_gateway_1"),
+ new Path('task_2', "sequence_flow_15", "parallel_gateway_2"),
+ new Path('exclusive_gateway_1', "sequence_flow_4", "task_3"),
+ new Path('exclusive_gateway_1', "sequence_flow_5", "task_5"),
+ new Path('exclusive_gateway_2', "sequence_flow_14", "parallel_gateway_2"),
+ new Path('parallel_gateway_2', "sequence_flow_16", "task_8"),
+ new Path('task_8', "sequence_flow_17", "end_event"),
+ new Path('task_3', "sequence_flow_12", "task_4"),
+ new Path('task_4', "sequence_flow_13", "exclusive_gateway_2"),
+ new Path('task_5', "sequence_flow_6", "inclusive_gateway_1"),
+ new Path('inclusive_gateway_1', "sequence_flow_7", "task_7"),
+ new Path('inclusive_gateway_1', "sequence_flow_8", "task_6"),
+ new Path('inclusive_gateway_2', "sequence_flow_11", "exclusive_gateway_2"),
+ new Path('task_7', "sequence_flow_10", "inclusive_gateway_2"),
+ new Path('task_6', "sequence_flow_9", "inclusive_gateway_2")
+];
diff --git a/demo/draw-path/js/steps.js b/demo/draw-path/js/steps.js
new file mode 100644
index 00000000..a160fb88
--- /dev/null
+++ b/demo/draw-path/js/steps.js
@@ -0,0 +1,22 @@
+class Steps {
+
+ reset() {
+ this._goToStep(1);
+ }
+
+ goToStep2(){
+ this._goToStep(2);
+ }
+
+ goToStep3(){
+ this._goToStep(3);
+ }
+
+ _goToStep(index) {
+ const stepItems = document.getElementsByClassName("step-item");
+ for (let stepItem of stepItems) {
+ stepItem.classList.remove('active');
+ }
+ document.getElementById(`step${index}`).classList.add('active');
+ }
+}
\ No newline at end of file
diff --git a/demo/static/js/use-case.js b/demo/static/js/use-case.js
index 4fc9d8e5..ceeead34 100644
--- a/demo/static/js/use-case.js
+++ b/demo/static/js/use-case.js
@@ -80,8 +80,11 @@ class UseCase {
}
// Display corresponding HTML element
- document.getElementById(`${this.#type}-${subId}`).classList.remove('d-hide');
- console.info('%s displayed', `${this.#type}-${subId}`);
+ const element = document.getElementById(`${this.#type}-${subId}`);
+ if(element) {
+ element.classList.remove('d-hide');
+ console.info('%s displayed', `${this.#type}-${subId}`);
+ }
}
}
diff --git a/examples/index.html b/examples/index.html
index f5f2549c..51578c3d 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -313,6 +313,22 @@ Demos
+
+
diff --git a/examples/static/img/preview/demo/draw-path.png b/examples/static/img/preview/demo/draw-path.png
new file mode 100644
index 00000000..6b7191db
Binary files /dev/null and b/examples/static/img/preview/demo/draw-path.png differ