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
    +
  • +
+
+
+ +
+
+
+
+
+
+
+ Legend +
    +
  • Disabled items
  • +
  • Selected items
  • +
  • Possible path
  • +
+
+
+
+
+
+
+
+
+
+
+ + + 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

+ +
+ +
+
+
Draw me a path
+
+
+
+ draw a path +
+
Show how to use bpmn-visualization to draw a path.
+ +
+
+
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