UNPKG

@yworks/react-yfiles-process-mining

Version:

yFiles React Process Mining Component - A powerful and versatile React component based on the yFiles library, allows you to seamlessly incorporate dynamic and interactive process mining visualizations into your applications.

1,541 lines (1,514 loc) 60 kB
// src/ProcessMiningProvider.tsx import { createContext, useContext, useMemo } from "react"; import { exportImageAndSave, exportSvgAndSave, printDiagram, useGraphComponent, withGraphComponentProvider } from "@yworks/react-yfiles-core"; import { HierarchicalLayout, HierarchicalLayoutData, HierarchicalLayoutEdgeDescriptor, HierarchicalLayoutRoutingStyle, RoutingStyleDescriptor, Command, MutableRectangle, Insets } from "@yfiles/yfiles"; // src/core/process-graph-extraction.ts import { IEnumerable, LayoutExecutor } from "@yfiles/yfiles"; // src/styles/HeatData.ts var HeatData = class { values; minTime; maxTime; constructor(elements = 64, minTime = 0, maxTime = 1) { this.values = new Float32Array(elements); this.minTime = minTime; this.maxTime = maxTime; } /** * Returns the heat value at a specific timestamp. */ getValue(timestamp) { if (timestamp < this.minTime || timestamp >= this.maxTime) { return 0; } else { const ratio = this.calculateRatio(timestamp); const index = Math.floor(ratio); const fraction = ratio - index; if (index + 1 < this.values.length) { return this.values[index] * (1 - fraction) + this.values[index + 1] * fraction; } else { return this.values[index]; } } } /** * Adds the given value for a given time span. This is used internally to set up the heat data. */ addValues(from, to, value) { const fromRatio = this.calculateRatio(from); const fromIndex = Math.floor(fromRatio); const fromFraction = fromRatio - fromIndex; const toRatio = this.calculateRatio(to); const toIndex = Math.floor(toRatio); const toFraction = toRatio - toIndex; this.values[fromIndex] += (1 - fromFraction) * value; for (let i = fromIndex + 1; i < toIndex; i++) { this.values[i] += value; } this.values[toIndex] += value * toFraction; } /** * Calculates the ratio of the given time in relation to the time span * covered by this {@link HeatData}. */ calculateRatio(time) { return (time - this.minTime) / (this.maxTime - this.minTime) * this.values.length; } /** * Returns the maximum heat value over the whole time span that is covered by this {@link HeatData}. */ getMaxValue() { return this.values.reduce((maxValue, value) => Math.max(maxValue, value), 0); } }; // src/core/process-graph-extraction.ts LayoutExecutor.ensure(); function getProcessStepData(step) { return step.tag; } function getTransitionData(transition) { return transition.tag; } function createProcessStepData(activity) { return { label: activity, heat: new HeatData(128, 0, 30), capacity: 1 }; } function createTransitionData(sourceActivity, targetActivity) { return { sourceLabel: sourceActivity, targetLabel: targetActivity, heat: new HeatData(128, 0, 30), capacity: 1 }; } function extractGraph(eventLog, graphComponent) { const graph = graphComponent.graph; graph.clear(); const activity2node = /* @__PURE__ */ new Map(); const activities2edge = /* @__PURE__ */ new Map(); IEnumerable.from(eventLog).groupBy( (event) => event.caseId, (caseId, events) => events?.toArray() ?? [] ).forEach((events) => { let lastEvent; events.sort((event1, event2) => event1.timestamp - event2.timestamp).forEach((event) => { const activity = event.activity; let node = activity2node.get(activity); if (!node) { node = graph.createNode({ // labels: [event.activity], tag: createProcessStepData(activity) }); activity2node.set(activity, node); } const lastActivity = lastEvent?.activity; let edge = activities2edge.get(lastActivity + activity); if (lastEvent && !edge) { const lastNode = activity2node.get(lastActivity); edge = graph.createEdge({ source: lastNode, target: node, tag: createTransitionData(lastActivity, activity) }); activities2edge.set(lastActivity + activity, edge); } lastEvent = event; }); }); graphComponent.fitGraphBounds(); } // src/animation/AnimationController.ts import { Animator } from "@yfiles/yfiles"; var AnimationController = class { /** * Creates a new Animation controller. * @param graphComponent the graph component to which the animation belongs */ constructor(graphComponent) { this.graphComponent = graphComponent; this.running = false; this.animator = new Animator({ canvasComponent: graphComponent, allowUserInteraction: true, autoInvalidation: true }); } animator; running; /** * Starts the animation. * @param progressCallback a callback function to report the progress back * @param maxTime the maximum time that the animation can take */ async runAnimation(progressCallback, maxTime) { if (!this.running) { await this.animator.animate(progressCallback, maxTime); this.running = false; } } /** * Starts the animation. * @param progressCallback a callback function to report the progress back * @param duration the maximum time that the animation can take */ async startAnimation(progressCallback, duration) { if (this.animator) { this.animator.stop(); this.animator.paused = false; } this.running = false; await this.runAnimation(progressCallback, duration); } /** * Stops the animation. */ stopAnimation() { if (this.animator) { this.animator.stop(); this.animator.paused = false; } } }; // src/core/process-visualization.ts import { IEnumerable as IEnumerable2 } from "@yfiles/yfiles"; function prepareProcessVisualization(graph, eventLog, transitionEventStyling, transitionEventVisualSupport) { const eventsByActivities = Object.fromEntries( IEnumerable2.from(eventLog).groupBy( (event) => event.activity, (activity, events) => [activity, events?.toList()] ) ); graph.nodes.forEach((processStep) => { const processStepData = getProcessStepData(processStep); const events = eventsByActivities[processStepData.label]; events?.forEach((event) => { processStepData.heat.addValues( event.timestamp, event.timestamp + (event.duration ?? 0), event.cost ?? 1 ); }); processStepData.capacity = processStepData.heat.getMaxValue(); }); graph.edges.forEach((transition) => { const transitionData = getTransitionData(transition); let allEvents; if (transition.isSelfLoop) { allEvents = eventsByActivities[transitionData.sourceLabel]; allEvents.sort((event1, event2) => event1.timestamp - event2.timestamp); allEvents = allEvents.map((event, index) => ({ source: index % 2 === 0, event })); } else { const sourceEvents = eventsByActivities[transitionData.sourceLabel].map((event) => ({ source: true, event })); const targetEvents = eventsByActivities[transitionData.targetLabel].map((event) => ({ source: false, event })); allEvents = sourceEvents.concat(targetEvents); } allEvents.groupBy( (event) => event.event.caseId, (caseId, events) => events?.toArray() ?? [] ).filter((events) => events.length > 1).map( (events) => events.sort((event1, event2) => event1.event.timestamp - event2.event.timestamp) ).forEach((events) => { for (let i = 0; i < events.length - 1; i++) { if (events[i].source && !events[i + 1].source) { const event = events[i].event; const nextEvent = events[i + 1].event; transitionData.heat.addValues(event.timestamp, nextEvent.timestamp, 1); const { hue, size } = transitionEventStyling(event, nextEvent); transitionEventVisualSupport.addItem( events[0].event.caseId, transition, false, event.timestamp, nextEvent.timestamp, size, hue / 360 ); } } }); transitionData.capacity = transitionData.heat.getMaxValue(); }); const maxTime = eventLog.sort((event1, event2) => event1.timestamp - event2.timestamp).at(-1)?.timestamp ?? 0; return maxTime + 1; } // src/ProcessMiningProvider.tsx import { jsx } from "react/jsx-runtime"; var ProcessMiningContext = createContext(null); function useProcessMiningContextInternal() { return useContext(ProcessMiningContext); } function useProcessMiningContext() { const context = useContext(ProcessMiningContext); if (context === null) { throw new Error( "This method can only be used inside an Process Mining component or ProcessMiningProvider." ); } return context; } var ProcessMiningProvider = withGraphComponentProvider( ({ children }) => { const graphComponent = useGraphComponent(); if (!graphComponent) { return children; } const processMining = useMemo(() => { return createProcessMiningModel(graphComponent); }, []); return /* @__PURE__ */ jsx(ProcessMiningContext.Provider, { value: processMining, children }); } ); var defaultMargins = { top: 5, right: 5, left: 5, bottom: 5 }; var defaultLayoutOptions = { direction: "top-to-bottom", minimumLayerDistance: 20, nodeToNodeDistance: 10, nodeToEdgeDistance: 10, maximumDuration: Number.POSITIVE_INFINITY, edgeGrouping: false }; function createProcessMiningModel(graphComponent) { let onRenderedCallback = null; const setRenderedCallback = (cb) => { onRenderedCallback = cb; }; const onRendered = () => { onRenderedCallback?.(); onRenderedCallback = null; }; const animationController = new AnimationController(graphComponent); function zoomTo(items) { if (items.length === 0) { return; } const targetBounds = new MutableRectangle(); const graph = graphComponent.graph; items.forEach((item) => { if ("sourceLabel" in item && "targetLabel" in item) { const source = getNode(item.sourceLabel, graph); const target = getNode(item.targetLabel, graph); const edge = graph.getEdge(source, target); if (edge) { targetBounds.add(edge.sourceNode.layout); targetBounds.add(edge.targetNode.layout); } } else { const node = getNode(item.label, graph); targetBounds.add(node.layout); } }); graphComponent.focus(); void graphComponent.zoomToAnimated(targetBounds.toRect().getEnlarged(200)); } let _showTransitionEvents; let _eventLog; let _transitionEventStyling; let _transitionEventVisualSupport; return { graphComponent, set showTransitionEvents(value) { _showTransitionEvents = value; }, set eventLog(value) { _eventLog = value; }, set transitionEventStyling(value) { _transitionEventStyling = value; }, set transitionEventVisualSupport(value) { _transitionEventVisualSupport = value; }, async startAnimation(progressCallback, startTimestamp, endTimestamp, duration) { await animationController.startAnimation( (progress) => progressCallback(startTimestamp + progress * (endTimestamp - startTimestamp)), duration ); }, stopAnimation() { animationController.stopAnimation(); }, async applyLayout(layoutOptions = defaultLayoutOptions, morphLayout = false) { if (_showTransitionEvents && _eventLog) { _transitionEventVisualSupport.hideVisual(graphComponent); _transitionEventVisualSupport.clearItems(); } const layout = new HierarchicalLayout({ layoutOrientation: layoutOptions.direction, minimumLayerDistance: layoutOptions.minimumLayerDistance, nodeDistance: layoutOptions.nodeToNodeDistance, nodeToEdgeDistance: layoutOptions.nodeToEdgeDistance, stopDuration: layoutOptions.maximumDuration, automaticEdgeGrouping: layoutOptions.edgeGrouping, defaultEdgeDescriptor: new HierarchicalLayoutEdgeDescriptor({ routingStyleDescriptor: new RoutingStyleDescriptor( HierarchicalLayoutRoutingStyle.CURVED, false ) }) }); const layoutData = new HierarchicalLayoutData({ nodeMargins: new Insets(5) }); await graphComponent.applyLayoutAnimated({ layout, animationDuration: morphLayout ? "1s" : "0s", layoutData, targetBoundsPadding: 50 }); if (_showTransitionEvents && _eventLog) { _transitionEventVisualSupport.showVisual(graphComponent); } if (_eventLog) { prepareProcessVisualization( graphComponent.graph, _eventLog, _transitionEventStyling ?? (() => ({ size: 7, hue: 200 })), _transitionEventVisualSupport ); } }, zoomToItem(item) { zoomTo([item]); }, zoomTo, zoomIn() { graphComponent.executeCommand(Command.INCREASE_ZOOM, null); }, zoomOut() { graphComponent.executeCommand(Command.DECREASE_ZOOM, null); }, zoomToOriginal() { graphComponent.executeCommand(Command.ZOOM, 1); }, fitContent() { graphComponent.executeCommand(Command.FIT_GRAPH_BOUNDS, null); }, async exportToSvg(exportSettings) { const settings = exportSettings ?? { zoom: graphComponent.zoom, scale: graphComponent.zoom, margins: defaultMargins, inlineImages: true }; await exportSvgAndSave(settings, graphComponent, setRenderedCallback); }, async exportToPng(exportSettings) { const settings = exportSettings ?? { zoom: graphComponent.zoom, scale: 1, margins: defaultMargins }; await exportImageAndSave(settings, graphComponent, setRenderedCallback); }, async print(printSettings) { const settings = printSettings ?? { zoom: graphComponent.zoom, scale: 1, margins: defaultMargins }; await printDiagram(settings, graphComponent); }, refresh() { graphComponent.invalidate(); }, getSearchHits: () => [], // will be replaced during initialization onRendered }; } function getNode(label, graph) { return graph.nodes.find((node) => getProcessStepData(node).label === label); } // src/styles/RenderHeatGauge.tsx import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime"; function RenderHeatGauge({ heat, size }) { const { cx, cy, r, thickness } = getCircleLayout(size); const currentHeat = heat ?? 0.8; const perimeter = r * 2 * Math.PI; const length = perimeter * (1 - currentHeat); const color = getIntensityColor(currentHeat); return /* @__PURE__ */ jsx2(Fragment, { children: /* @__PURE__ */ jsx2("svg", { width: size, height: size, children: /* @__PURE__ */ jsxs("g", { style: { transform: "rotate(-90deg)", transformOrigin: "center" }, children: [ /* @__PURE__ */ jsx2( "circle", { cx, cy, r, stroke: "#6e6e6e", fill: "none", strokeWidth: thickness + 4 } ), /* @__PURE__ */ jsx2( "circle", { cx, cy, r, fill: "none", stroke: "#dcdcdc", strokeWidth: thickness } ), /* @__PURE__ */ jsx2( "circle", { cx, cy, r, fill: "none", stroke: color, strokeWidth: thickness, strokeDashoffset: length, strokeDasharray: perimeter } ) ] }) }) }); } function getCircleLayout(height) { const thickness = Math.max(height / 4, 10); const r = height / 2 - thickness / 2 - height / 20; const cx = height / 2; const cy = height / 2; return { thickness, r, cx, cy }; } function getIntensityColor(value) { return `rgb(${16 + value * 239 | 0}, ${(1 - value) * 239 | 16}, 16)`; } // src/styles/RenderProcessStep.tsx import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime"; function RenderProcessStep({ dataItem, hovered, focused, selected, timestamp }) { const heat = Math.min(1, (dataItem.heat?.getValue(timestamp) ?? 0) / (dataItem.capacity ?? 100)); const currentHeat = Math.floor((heat ?? 0) * 30) / 30; const gaugeSize = 43; return /* @__PURE__ */ jsx3(Fragment2, { children: /* @__PURE__ */ jsxs2( "div", { className: `yfiles-react-render-process-step ${getHighlightClasses( selected, hovered, focused )}`, style: { width: "100%", height: "100%" }, children: [ /* @__PURE__ */ jsx3("div", { className: "yfiles-react-render-process-step-gauge", children: /* @__PURE__ */ jsx3(RenderHeatGauge, { size: gaugeSize, heat: currentHeat }) }), /* @__PURE__ */ jsx3("div", { className: "yfiles-react-render-process-step-text", children: dataItem.label }) ] } ) }); } function getHighlightClasses(selected, hovered, focused) { const highlights = ["yfiles-react-node-highlight"]; if (focused) { highlights.push("yfiles-react-node-highlight--focused"); } if (hovered) { highlights.push("yfiles-react-node-highlight--hovered"); } if (selected) { highlights.push("yfiles-react-node-highlight--selected"); } return highlights.join(" "); } // src/ProcessMining.tsx import { useEffect, useLayoutEffect, useMemo as useMemo2, useState } from "react"; import { checkLicense, checkStylesheetLoaded, ContextMenu, LicenseError, Popup, ReactNodeRendering, Tooltip, useGraphSearch, useReactNodeRendering, withGraphComponent } from "@yworks/react-yfiles-core"; import { INode as INode5 } from "@yfiles/yfiles"; // src/core/input.ts import { GraphItemTypes as GraphItemTypes2, GraphViewerInputMode as GraphViewerInputMode2, INode as INode3 } from "@yfiles/yfiles"; // src/core/SingleSelectionHelper.ts import { EventRecognizers, GraphItemTypes, Command as Command2, IModelItem } from "@yfiles/yfiles"; var commandBindings = []; var oldMultiSelectionRecognizer = null; function enableSingleSelection(graphComponent) { const mode = graphComponent.inputMode; oldMultiSelectionRecognizer = mode.multiSelectionRecognizer; mode.marqueeSelectionInputMode.enabled = false; mode.multiSelectionRecognizer = EventRecognizers.NEVER; mode.availableCommands.remove(Command2.TOGGLE_ITEM_SELECTION); mode.availableCommands.remove(Command2.SELECT_ALL); mode.navigationInputMode.availableCommands.remove(Command2.EXTEND_SELECTION_LEFT); mode.navigationInputMode.availableCommands.remove(Command2.EXTEND_SELECTION_UP); mode.navigationInputMode.availableCommands.remove(Command2.EXTEND_SELECTION_DOWN); mode.navigationInputMode.availableCommands.remove(Command2.EXTEND_SELECTION_RIGHT); const dummyExecutedHandler = (_) => { }; commandBindings.push( mode.keyboardInputMode.addCommandBinding(Command2.EXTEND_SELECTION_LEFT, dummyExecutedHandler) ); commandBindings.push( mode.keyboardInputMode.addCommandBinding(Command2.EXTEND_SELECTION_UP, dummyExecutedHandler) ); commandBindings.push( mode.keyboardInputMode.addCommandBinding(Command2.EXTEND_SELECTION_DOWN, dummyExecutedHandler) ); commandBindings.push( mode.keyboardInputMode.addCommandBinding(Command2.EXTEND_SELECTION_RIGHT, dummyExecutedHandler) ); commandBindings.push( mode.keyboardInputMode.addCommandBinding( Command2.TOGGLE_ITEM_SELECTION, (evt) => toggleItemSelectionExecuted(graphComponent, evt.parameter), (evt) => toggleItemSelectionCanExecute(graphComponent, evt.parameter) ) ); graphComponent.selection.clear(); } function toggleItemSelectionCanExecute(graphComponent, parameter) { const modelItem = parameter instanceof IModelItem ? parameter : graphComponent.currentItem; return modelItem !== null; } function toggleItemSelectionExecuted(graphComponent, parameter) { const modelItem = parameter instanceof IModelItem ? parameter : graphComponent.currentItem; const inputMode = graphComponent.inputMode; if (modelItem === null || !graphComponent.graph.contains(modelItem) || !GraphItemTypes.itemIsOfTypes(inputMode.selectableItems, modelItem) || !inputMode.graphSelection) { return; } const isSelected = inputMode.graphSelection.includes(modelItem); if (isSelected) { inputMode.graphSelection.clear(); } else { inputMode.graphSelection.clear(); inputMode.setSelected(modelItem, true); } } // src/core/configure-smart-click-navigation.ts import { IEdge, INode as INode2 } from "@yfiles/yfiles"; function enableSmartClickNavigation(graphInputMode, graphComponent, mode = "zoom-to-viewport-center") { graphInputMode.addEventListener( "item-left-clicked", async (event) => { if (!event.handled) { const item = event.item; const location = getFocusPoint(item, graphComponent); if (location) { if (mode === "zoom-to-mouse-location") { const offset = event.location.subtract(graphComponent.viewport.center); await graphComponent.zoomToAnimated(graphComponent.zoom, location.subtract(offset)); } else { await graphComponent.zoomToAnimated(graphComponent.zoom, location); } } } } ); } function getFocusPoint(item, graphComponent) { if (item instanceof INode2) { return item.layout.center; } else if (item instanceof IEdge) { const targetNodeCenter = item.targetNode.layout.center; const sourceNodeCenter = item.sourceNode.layout.center; const viewport = graphComponent.viewport; if (viewport.contains(targetNodeCenter) && viewport.contains(sourceNodeCenter)) { return null; } else { if (viewport.center.subtract(targetNodeCenter).vectorLength < viewport.center.subtract(sourceNodeCenter).vectorLength) { return sourceNodeCenter; } else { return targetNodeCenter; } } } return null; } // src/core/input.ts function initializeInputMode(graphComponent, processMining, smartClickNavigation = false) { const graphViewerInputMode = new GraphViewerInputMode2({ clickableItems: GraphItemTypes2.NODE | GraphItemTypes2.PORT | GraphItemTypes2.EDGE, selectableItems: GraphItemTypes2.NODE, marqueeSelectableItems: GraphItemTypes2.NONE, toolTipItems: GraphItemTypes2.NONE, contextMenuItems: GraphItemTypes2.NODE | GraphItemTypes2.EDGE, focusableItems: GraphItemTypes2.NODE, clickHitTestOrder: [GraphItemTypes2.PORT, GraphItemTypes2.NODE, GraphItemTypes2.EDGE] }); initializeHighlights(graphComponent); graphComponent.inputMode = graphViewerInputMode; if (smartClickNavigation) { enableSmartClickNavigation(graphViewerInputMode, graphComponent); } enableSingleSelection(graphComponent); } function initializeHover(onHover, graphComponent) { const inputMode = graphComponent.inputMode; inputMode.itemHoverInputMode.hoverItems = GraphItemTypes2.NODE; const hoverItemChangedListener = (evt, _) => { const manager = graphComponent.highlightIndicatorManager; if (evt.oldItem) { manager.items?.remove(evt.oldItem); } if (evt.item) { manager.items?.add(evt.item); } if (onHover) { onHover(evt.item?.tag, evt.oldItem?.tag); } }; inputMode.itemHoverInputMode.addEventListener("hovered-item-changed", hoverItemChangedListener); return hoverItemChangedListener; } function initializeTransitionEventsClick(onTransitionEventsClick, graphComponent, transitionEventVisualSupport) { const inputMode = graphComponent.inputMode; const transitionEventsClickedListener = (evt, _) => { if (onTransitionEventsClick) { const clickedTransitionEntries = transitionEventVisualSupport.getEntriesAtLocation( evt.location ); onTransitionEventsClick(clickedTransitionEntries.map((entry) => entry.caseId)); } }; inputMode.addEventListener("item-clicked", transitionEventsClickedListener); inputMode.addEventListener("canvas-clicked", transitionEventsClickedListener); return transitionEventsClickedListener; } function initializeFocus(onFocus, graphComponent) { let currentItemChangedListener = () => { }; if (onFocus) { currentItemChangedListener = () => { const currentItem = graphComponent.currentItem; if (currentItem instanceof INode3) { onFocus(getProcessStepData(currentItem)); } else { onFocus(null); } }; } graphComponent.addEventListener("current-item-changed", currentItemChangedListener); return currentItemChangedListener; } function initializeSelection(onSelect, graphComponent) { let itemSelectionChangedListener = () => { }; if (onSelect) { itemSelectionChangedListener = () => { const selectedItems = graphComponent.selection.nodes.map((node) => getProcessStepData(node)).toArray(); onSelect(selectedItems); }; } graphComponent.selection.addEventListener("item-added", itemSelectionChangedListener); graphComponent.selection.addEventListener("item-removed", itemSelectionChangedListener); return itemSelectionChangedListener; } function initializeHighlights(graphComponent) { graphComponent.graph.decorator.nodes.selectionRenderer.hide(); graphComponent.graph.decorator.nodes.focusRenderer.hide(); } // src/styles/process-mining-styles.ts import { PolylineEdgeStyle, Rect, Size } from "@yfiles/yfiles"; import { convertToPolylineEdgeStyle, ReactComponentHtmlNodeStyle } from "@yworks/react-yfiles-core"; function initializeNodeDefaultStyle(graphComponent, setNodeInfos, nodeSize) { const graph = graphComponent.graph; graph.nodeDefaults.style = new ReactComponentHtmlNodeStyle( RenderProcessStep, setNodeInfos ); if (nodeSize) { graph.nodeDefaults.size = new Size(nodeSize.width, nodeSize.height); } graphComponent.highlightIndicatorManager.enabled = false; } function initializeEdgeDefaultStyle(graphComponent) { graphComponent.graph.edgeDefaults.style = new PolylineEdgeStyle({ stroke: "2px #33a", targetArrow: "#33a triangle", smoothingLength: 100 }); } function updateNodeSizes(graphComponent, itemSize) { graphComponent.graph.nodes.forEach((node) => { if (itemSize) { graphComponent.graph.setNodeLayout(node, Rect.fromCenter(node.layout.center, itemSize)); } else { const data = getProcessStepData(node); if (data.width && data.height) { graphComponent.graph.setNodeLayout( node, Rect.fromCenter(node.layout.center, [data.width, data.height]) ); } } }); } function updateNodeStyles(graphComponent, setNodeInfos, renderProcessStep) { const renderStep = renderProcessStep ?? RenderProcessStep; graphComponent.graph.nodes.forEach((node) => { graphComponent.graph.setStyle( node, new ReactComponentHtmlNodeStyle( renderStep, setNodeInfos ) ); }); } function updateEdgeStyles(graphComponent, transitionStyles) { if (transitionStyles) { graphComponent.graph.edges.forEach((edge) => { const styleProperties = transitionStyles( getProcessStepData(edge.sourceNode), getProcessStepData(edge.targetNode) ); graphComponent.graph.setStyle( edge, styleProperties ? convertToPolylineEdgeStyle(styleProperties) : graphComponent.graph.edgeDefaults.style ); }); } } // src/core/heatmap.ts import { HtmlCanvasVisual, IVisualCreator } from "@yfiles/yfiles"; var heatScale = 0.5; var HeatmapBackground = class extends HtmlCanvasVisual { getHeat; backBufferCanvas = null; backBufferContext = null; constructor(getHeat) { super(); this.getHeat = getHeat || (() => 1); } /** * Renders the heat map on a canvas. */ render(renderContext, ctx) { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); const { width, height } = ctx.canvas; let canvas = this.backBufferCanvas; let backBufferContext; if (!canvas || canvas.width !== width || canvas.height !== height) { canvas = document.createElement("canvas"); canvas.setAttribute("width", String(width)); canvas.setAttribute("height", String(height)); backBufferContext = canvas.getContext("2d"); this.backBufferCanvas = canvas; this.backBufferContext = backBufferContext; } else { backBufferContext = this.backBufferContext; backBufferContext.clearRect(0, 0, width, height); } const scale = renderContext.zoom * heatScale; backBufferContext.setTransform( renderContext.canvasComponent.devicePixelRatio, 0, 0, renderContext.canvasComponent.devicePixelRatio, 0, 0 ); let lastFillStyleHeat = -1; for (const node of renderContext.canvasComponent.graph.nodes) { const topLeft = renderContext.worldToViewCoordinates(node.layout.topLeft); const bottomRight = renderContext.worldToViewCoordinates(node.layout.bottomRight); const heat = this.getHeat(node); if (heat > 0) { if (heat !== lastFillStyleHeat) { backBufferContext.fillStyle = `rgba(255,255,255, ${heat})`; lastFillStyleHeat = heat; } backBufferContext.beginPath(); backBufferContext.rect( topLeft.x - 20, topLeft.y - 20, bottomRight.x - topLeft.x + 40, bottomRight.y - topLeft.y + 40 ); backBufferContext.fill(); } } let lastStrokeStyleHeat = -1; for (const edge of renderContext.canvasComponent.graph.edges) { const heat = this.getHeat(edge); if (heat > 0) { if (heat !== lastStrokeStyleHeat) { backBufferContext.strokeStyle = `rgba(255,255,255, ${heat})`; backBufferContext.lineWidth = heat * 100 * scale; backBufferContext.lineCap = "square"; lastStrokeStyleHeat = heat; } const path = edge.style.renderer.getPathGeometry(edge, edge.style).getPath().flatten(1); backBufferContext.beginPath(); const cursor = path.createCursor(); if (cursor.moveNext()) { const point = renderContext.worldToViewCoordinates(cursor.currentEndPoint); backBufferContext.moveTo(point.x, point.y); while (cursor.moveNext()) { const point2 = renderContext.worldToViewCoordinates(cursor.currentEndPoint); backBufferContext.lineTo(point2.x, point2.y); } backBufferContext.stroke(); } } } ctx.filter = "url(#yfiles-react-heatmap)"; ctx.drawImage(canvas, 0, 0); ctx.restore(); } }; var installedDivElement = null; function addHeatmap(graphComponent, getHeat) { if (!graphComponent.htmlElement.parentElement) { throw new Error("Cannot add heatmap for unmounted component"); } const heatmapParent = graphComponent.htmlElement.parentElement; if (!installedDivElement || !heatmapParent.contains(installedDivElement)) { installedDivElement = document.createElement("div"); installedDivElement.setAttribute( "style", "height: 0px; width: 0px;position:absolute; top:0, left: 0; visibility: hidden" ); installedDivElement.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" width="0" height="0"> <defs> <filter id="yfiles-react-heatmap" x="0" y="0" width="100%" height="100%"> <!-- Blur the image - change blurriness via stdDeviation between 10 and maybe 25 - lower values may perform better --> <feGaussianBlur stdDeviation="16" edgeMode="none"/> <!-- Take the alpha value --> <feColorMatrix type="matrix" values="0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0" /> <!-- Map it to a "heat" rainbow colors --> <feComponentTransfer> <feFuncR type="table" tableValues="0 0 0 0 1 1"></feFuncR> <feFuncG type="table" tableValues="0 0 1 1 1 0"></feFuncG> <feFuncB type="table" tableValues="0.5 1 0 0 0"></feFuncB> <!-- specify maximum opacity for the overlay here --> <!-- less opaque: <feFuncA type="table" tableValues="0 0.1 0.4 0.6 0.7"></feFuncA> --> <feFuncA type="table" tableValues="0 0.6 0.7 0.8 0.9"></feFuncA> </feComponentTransfer> </filter> </defs> </svg> `; heatmapParent.appendChild(installedDivElement); } return graphComponent.renderTree.backgroundGroup.renderTree.createElement( graphComponent.renderTree.backgroundGroup, IVisualCreator.create({ createVisual() { return new HeatmapBackground(getHeat); }, updateVisual(context, oldVisual) { return oldVisual; } }) ); } // src/styles/TransitionEventVisual.ts import { Arrow, ArrowType, BaseClass, Color, IBoundsProvider, IObjectRenderer, IHitTestable, IVisibilityTestable, IVisualCreator as IVisualCreator2, PathType, PolylineEdgeStyle as PolylineEdgeStyle2, SimpleEdge, WebGLVisual } from "@yfiles/yfiles"; // src/styles/webgl-utils.ts var WebGLBufferData = class { constructor(entryCount, pointerType, attributeName, elementSize, dataType) { this.entryCount = entryCount; this.pointerType = pointerType; this.attributeName = attributeName; this.elementSize = elementSize; this.DataType = dataType; } dirty = true; buffer = null; DataType; data = null; attributeLocation = -1; init(gl, program) { this.dirty = true; this.buffer = gl.createBuffer(); this.data = new this.DataType(this.elementSize * this.entryCount); this.attributeLocation = gl.getAttribLocation(program, this.attributeName); } updateData() { this.dirty = true; } enableRendering(renderContext, gl) { gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); if (this.dirty) { gl.bufferData(gl.ARRAY_BUFFER, this.data, gl.STATIC_DRAW); this.dirty = false; } gl.enableVertexAttribArray(this.attributeLocation); gl.vertexAttribPointer(this.attributeLocation, this.elementSize, this.pointerType, false, 0, 0); } disableRendering(renderContext, gl) { gl.disableVertexAttribArray(this.attributeLocation); } dispose(gl, program) { gl.deleteBuffer(this.buffer); gl.deleteProgram(program); this.data = null; this.entryCount = 0; this.attributeLocation = -1; } }; var WebGLProgramInfo = class { constructor(entryCount) { this.entryCount = entryCount; this.buffers = []; } buffers; createFloatBuffer(attributeName, entrySize = 1) { const bufferData = new WebGLBufferData( this.entryCount, WebGLRenderingContext.prototype.FLOAT, attributeName, entrySize, Float32Array ); this.buffers.push(bufferData); return bufferData; } init(gl, program) { for (const buffer of this.buffers) { buffer.init(gl, program); } } enableRendering(renderContext, gl) { for (const buffer of this.buffers) { buffer.enableRendering(renderContext, gl); } } disableRendering(renderContext, gl) { for (const buffer of this.buffers) { buffer.disableRendering(renderContext, gl); } } dispose(gl, program) { for (const buffer of this.buffers) { buffer.dispose(gl, program); } } }; // src/styles/TransitionEventVisual.ts function createTransitionEventVisualSupport(graphComponent) { return new TransitionEventVisualSupport(graphComponent); } var dummyEdgeStyle = new PolylineEdgeStyle2({ sourceArrow: new Arrow(ArrowType.NONE), targetArrow: new Arrow(ArrowType.NONE) }); var TransitionEventVisualSupport = class { transitionEventVisual; transitionEventObject = null; constructor(graphComponent) { this.transitionEventVisual = new TransitionEventVisual(); graphComponent.addEventListener( "viewport-changed", () => this.transitionEventVisual.dirty = true ); } /** * Returns all entries that are located at the given location at the current time */ getEntriesAtLocation(location) { const currentTime = this.transitionEventVisual.time; const currentVisibleEntries = this.transitionEventVisual.entries.filter( (entry) => entry.startTime < currentTime && entry.endTime > currentTime ); const locationX = location.x; const locationY = location.y; return currentVisibleEntries.filter((entry) => { const timeRatio = (currentTime - entry.startTime) / (entry.endTime - entry.startTime); const x = entry.x0 + (entry.x1 - entry.x0) * timeRatio; const y = entry.y0 + (entry.y1 - entry.y0) * timeRatio; return locationX > x - entry.size && locationX < x + entry.size && locationY > y - entry.size && locationY < y + entry.size; }); } /** * Installs a transition event visual in the given canvas component. */ showVisual(canvas) { if (!this.transitionEventObject) { this.transitionEventObject = canvas.renderTree.highlightGroup.renderTree.createElement( canvas.renderTree.highlightGroup, this.transitionEventVisual, new TransitionEventCanvasObjectDescriptor() ); } } /** * Installs a transition event visual in the given canvas component. */ hideVisual(canvas) { if (this.transitionEventObject) { canvas.renderTree.remove(this.transitionEventObject); this.transitionEventObject = null; } } /** * Updates the time in the transition event visual. * This is used to update the visuals over time. */ updateTime(time) { this.transitionEventVisual.time = time; } /** * Adds an event item to the transition event visual for the given edge and a specified timespan. * @param caseId the case id of the item * @param path the edge that the item should follow * @param reverse the direction in which the item should move * @param startTime the time when the item starts traversing the edge * @param endTime the time when the item stops traversing the edge * @param size the diameter of the item's circle * @param color a color value */ addItem(caseId, path, reverse, startTime, endTime, size, color) { this.transitionEventVisual.addItem(caseId, path, reverse, startTime, endTime, size, color); } /** * Removes all event items from the transition event visual. */ clearItems() { this.transitionEventVisual.clearItems(); } }; function getGeneralPath(path) { const dummyEdge = new SimpleEdge({ sourcePort: path.sourcePort, targetPort: path.targetPort, style: dummyEdgeStyle, bends: path.bends }); return dummyEdge.style.renderer.getPathGeometry(dummyEdge, dummyEdge.style).getPath(); } var TransitionEventProgramInfo = class extends WebGLProgramInfo { toPositionData; fromPositionData; indexData; sizeData; startTimeData; endTimeData; colorData; samplerUniformLocation = null; timeUniformLocation = null; texture = null; constructor(entryCount) { super(entryCount); this.toPositionData = this.createFloatBuffer("a_to", 2); this.fromPositionData = this.createFloatBuffer("a_from", 2); this.indexData = this.createFloatBuffer("a_index", 1); this.sizeData = this.createFloatBuffer("a_size", 1); this.startTimeData = this.createFloatBuffer("a_startTime", 1); this.endTimeData = this.createFloatBuffer("a_endTime", 1); this.colorData = this.createFloatBuffer("a_color", 1); } init(gl, program) { super.init(gl, program); this.initRainbowTexture(gl); this.samplerUniformLocation = gl.getUniformLocation(program, "u_Sampler"); this.timeUniformLocation = gl.getUniformLocation(program, "time"); } initRainbowTexture(gl) { const texture = this.texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); const width = 256; const height = 1; const values = []; for (let i = 0; i < width; i++) { const c = Color.fromHSLA(i / 256, 0.8, 0.5, 1); values.push(c.r & 255, c.g & 255, c.b & 255, 255); } gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(values) ); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); return texture; } enableRendering(renderContext, gl) { super.enableRendering(renderContext, gl); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.uniform1i(this.samplerUniformLocation, 0); } render(renderContext, gl, time) { if (this.entryCount > 0) { this.enableRendering(renderContext, gl); gl.uniform1f(this.timeUniformLocation, time); gl.drawArrays(gl.TRIANGLES, 0, this.entryCount); this.disableRendering(renderContext, gl); } } disableRendering(renderContext, gl) { super.disableRendering(renderContext, gl); } dispose(gl, program) { super.dispose(gl, program); gl.deleteTexture(this.texture); } }; var fragmentShader = ` #ifdef GL_OES_standard_derivatives #extension GL_OES_standard_derivatives : enable #endif precision mediump float; varying float v_colorIndex; uniform sampler2D u_Sampler; varying vec2 v_coord; void main() { float r = 0.0, delta = 0.0, alpha = 1.0; r = dot(v_coord, v_coord); #ifdef GL_OES_standard_derivatives delta = fwidth(r); alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r); gl_FragColor = texture2D(u_Sampler, vec2(v_colorIndex, 0)) * alpha; #endif #ifndef GL_OES_standard_derivatives if ( dot(v_coord, v_coord) < 1.0){ gl_FragColor = texture2D(u_Sampler, vec2(v_colorIndex, 0)); } else { gl_FragColor = vec4( 0., 0., 0., 0. ); } #endif }`; var vertexShader = ` uniform float time; attribute vec2 a_from; attribute vec2 a_to; attribute float a_startTime; attribute float a_endTime; attribute float a_index; attribute float a_size; attribute float a_color; varying vec2 v_coord; varying float v_colorIndex; void main() { if (time >= a_startTime && time < a_endTime){ int index = int(a_index); v_colorIndex = a_color; float timeRatio = (time - a_startTime) / (a_endTime - a_startTime); vec2 pos = mix(a_from, a_to, timeRatio); v_coord = vec2(0,0); float w = a_size; float h = a_size; if (index == 2 || index == 3 || index == 4){ pos.x -= w; v_coord.x = 1.; } else { pos.x += w; v_coord.x = -1.; } if (index == 0 || index == 5 || index == 4){ pos.y -= h; v_coord.y = -1.; } else { pos.y += h; v_coord.y = 1.; } gl_Position = vec4((u_yf_worldToWebGL * vec3(pos, 1)).xy, 0., 1.); } else { gl_Position = vec4(-10.,-10.,-10.,1); } }`; var TransitionEventVisual = class extends WebGLVisual { entryCount; entries; dirty; $time; timeDirty; constructor() { super(); this.timeDirty = true; this.$time = 0; this.entries = []; this.entryCount = 0; this.dirty = false; } set time(value) { this.$time = value; this.timeDirty = true; } get time() { return this.$time; } clearItems() { this.dirty = true; this.entries.length = 0; this.entryCount = 0; } addItem(caseId, path, reverse, startTime = 0, endTime = 1, size = 10, color = 0.5) { this.dirty = true; const entries = this.entries; function appendSegment(x0, y0, x1, y1) { if (reverse) { const dx = x0 - x1; const dy = y0 - y1; const length = Math.sqrt(dx * dx + dy * dy); const segmentStartTime = startTime + (endTime - startTime) * (runningTotal / totalLength); runningTotal -= length; const segmentEndTime = startTime + (endTime - startTime) * (runningTotal / totalLength); entries.push({ caseId, color, startTime: segmentEndTime, endTime: segmentStartTime, size, x0: x1, y0: y1, x1: x0, y1: y0 }); } else { const dx = x1 - x0; const dy = y1 - y0; const length = Math.sqrt(dx * dx + dy * dy); const segmentStartTime = startTime + (endTime - startTime) * (runningTotal / totalLength); runningTotal += length; const segmentEndTime = startTime + (endTime - startTime) * (runningTotal / totalLength); entries.push({ caseId, color, startTime: segmentStartTime, endTime: segmentEndTime, size, x0, y0, x1, y1 }); } } const generalPath = getGeneralPath(path); const totalLength = generalPath.getLength(); const pathCursor = generalPath.createCursor(); const coords = [0, 0, 0, 0, 0, 0]; let runningTotal = reverse ? totalLength : 0; let lastMoveX = 0; let lastMoveY = 0; let lastX = 0; let lastY = 0; while (pathCursor.moveNext()) { switch (pathCursor.getCurrent(coords)) { case PathType.LINE_TO: appendSegment(lastX, lastY, lastX = coords[0], lastY = coords[1]); break; case PathType.CLOSE: appendSegment(lastX, lastY, lastX = lastMoveX, lastY = lastMoveY); break; case PathType.CUBIC_TO: appendSegment(lastX, lastY, lastX = coords[4], lastY = coords[5]); break; case PathType.MOVE_TO: lastX = lastMoveX = coords[0]; lastY = lastMoveY = coords[1]; break; case PathType.QUAD_TO: appendSegment(lastX, lastY, lastX = coords[2], lastY = coords[3]); break; } } } expungeOldEntries() { const currentTime = this.time; const entries = this.entries; for (let i = 0; i < entries.length; i++) { if (entries[i].endTime < currentTime) { entries.splice(i, 1); i--; } } } /** * Paints onto the context using WebGL item styles. */ render(renderContext, gl) { gl.getExtension("GL_OES_standard_derivatives"); gl.getExtension("OES_standard_derivatives"); const program = renderContext.webGLSupport.useProgram( vertexShader, fragmentShader ); if (!program.info || this.dirty) { const entryCount = this.entryCount = this.entries.length; const vertexCount = entryCount * 6; if (program.info) { program.info.dispose(gl, program); renderContext.webGLSupport.deleteProgram(program); } program.info = new TransitionEventProgramInfo(vertexCount); program.info.init(gl, program); this.updateData(program.info); } program.info.render(renderContext, gl, this.time); } updateData(programInfo) { const toPosition = programInfo.toPositionData.data; const fromPosition = programInfo.fromPositionData.data; const start = programInfo.startTimeData.data; const end = programInfo.endTimeData.data; const colorData = programInfo.colorData.data; const itemSize = programInfo.sizeData.data; const vertexIndex = programInfo.indexData.data; this.entries.forEach((n, index) => { const offset = index * 12; const { x0, y0, x1, y1, size, startTime, endTime, color } = n; for (let i = 0; i < 12; i += 2) { fromPosition[offset + i] = x0; fromPosition[offset + i + 1] = y0; toPosition[offset + i] = x1; toPosition[offset + i + 1] = y1; } let coordinatesOffset = index * 6; for (let i = 0; i < 6; i++) { start[coordinatesOffset] = startTime; end[coordinatesOffset] = endTime; itemSize[coordinatesOffset] = size; vertexIndex[coordinatesOffset] = i; colorData[coordinatesOffset++] = color; } }); this.dirty = false; this.timeDirty = false; } get needsRepaint() { return this.entryCount > 0 && this.timeDirty || this.dirty && this.entries.length > 0; } }; var TransitionEventCanvasObjectDescriptor = class extends BaseClass(IObjectRenderer, IVisualCreator2) { transitionEventVisual = null; getBoundsProvider(forUserObject) { return IBoundsProvider.UNBOUNDED; } getHitTestable(forUserObject) { return IHitTestable.NEVER; } getVisibilityTestable(forUserObject) { return IVisibilityTestable.ALWAYS; } getVisualCreator(forUserObject) { this.transitionEventVisual = forUserObject; return this; } isDirty(context, canvasObject) { return canvasObject.dirty || canvasObject.tag.needsRepaint; } createVisual(context) { return this.transitionEventVisual; } updateVisual(context, oldVisual) { return this.transitionEventVisual; } }; // src/ProcessMining.tsx import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime"; var licenseErrorCodeSample = `import {ProcessMining, registerLicense} from '@yworks/react-yfiles-process-mining' import '@yworks/react-yfiles-process-mining/dist/index.css' import yFilesLicense from './license.json' function App() { registerLicense(yFilesLicense) const eventLog = [ { caseId: 0, activity: 'Start', timestamp: 8.383561495922297, duration: 0.0006804154279300233 }, { caseId: 0, activity: 'Step A', timestamp: 8.928697652413142, duration: 0.10316578562597925 }, { caseId: 0, activity: 'Step B', timestamp: 9.576999594529966, duration: 0.041202953341980784 }, { caseId: 0, activity: 'End', timestamp: 10.163338704362792, duration: 0.2746326125522593 } ] return <ProcessMining eventLog={eventLog}></ProcessMining> }`; function ProcessMining(props) { if (!checkLicense()) { return /* @__PURE__ */ jsx4( LicenseError, { componentName: "yFiles React Process Mining Component", codeSample: licenseErrorCodeSample } ); } const isWrapped = useProcessMiningContextInternal(); if (isWrapped) { return /* @__PURE__ */ jsx4(ProcessMiningCore, { ...props, children: props.children }); }