@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
JavaScript
// 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 });
}