A lean process flowchart generator built with based on CSS Grid and React
data = {
firstStep: "b1fe-auth",
workflows: {
"b1fe-auth": { id: "b1fe-auth", name: "Authorize", type: AUTHORIZE, children: ["b1fe"] },
"b1fe": { id: "b1fe", name: "Decision", type: DECISION, children: ["3902", "e5d2"] },
"3902": { id: "3902", name: "Human", type: TRANSLATION, children: [ "2910"] },
"e5d2": { id: "e5d2", name: "MachineMachine", type: TRANSLATION, children: ["3bb4"] },
"2910": { id: "2910", name: "Edit", type: POST_TRANSLATION, children: ["3bb4"] },
"3bb4": { id: "3bb4", name: "Review", type: REVIEW, children: ["51fa"] },
"51fa": { id: "51fa", name: "Published", type: PUBLISH, children: [] }
}
}becomes
The visualization component (WorkflowVis) takes as inputs
matrix- A N x M matrix where N is the number of columns and M is the number of rows. Each element of the matrix specifies the i of building block to render.workflowVisData- A datastructure that represents the graph of all the workflow steps with information on how they are connected and the distance between the node and the root of the graph (assumed to be a directed acyclic graph (DAG)).
matrix is created from workflowVisData. (WorkflowVisContainer) creates matrix as follows:
-
Initialize a N x M
matrix. N is longest path in the DAG (workflowStepOrderis pre-calculated for each node in the graph which provides the path of the node from the root node). M is calculated from the largest frequency ofworkflowStepOrder.matrixis initialized withempty. -
Populate
matrixwith nodes (BFS of on theworkflowVisData). Each time we loop, a node is placed into the matrix and its children are added to thetoExplorequeue to be placed into the matrix in the future. Additionally, two hash maps are created during this process:nodeIdToCoordmaps nodeId to its encodedcolNum, rowNumstring representing the node's position in the matrix.nodeToParentCoordsmaps nodeId of a node to an array of the coordinates (encodedcolNum, rowNumstring) of all the node's parents in the matrix.
-
Populate
matrixwith connectors using the hash map generated from the previous step.
You can add a new node to the flowchart by clicking the plus button. WorkflowVisContainer passes a function down to the visualization that gets called when a plus sign is clicked.
There are three major considerations to placing the nodes into the matrix:
The order in which we explore the nodes is critical to the correctness of the visualization. Because our algorithm places a node into the matrix every time we loop, it's making a myopic and irrevocable decision that has consequences down the line that would affect the quality of the visualization. Our goal is to minimize the number of intersecting lines in our visualization. the following techniques are employed to achieve this goal:
The toExplore queue is maintained as a priority queue (minHeap), with the priority being
priority = workflowStepOrder + childOrderwhere
workflowStepOrderis the number that indicates the distance of the current node from the root node of the visualization andchilrenOrderis a number between0and1which indicates the order in which we will explore the children of a node.
The smaller the priority, the earlier we place the node into the matrix.
How do we determine childOrder? Given a children array. There are two cases:
-
When the node is not a decision step, it has exactly one child. so the
childOrderis0. -
When the node is a decision step (fork step), it can have multiple children. Each child has an attribute called
primary, which indicates if it's the master branch and as such, should be place at the very top of the fork (there can only be oneprimarybranch per fork). When there are multiple children. This is the case when the node is a decision step. ThechildOrderisi / children.lengthwhereiis the index of the child node in a sortedchildrenarray. To sort thechildrenarray:- We are given an array of children. We only perform the sort when there are 2 or more children and only one of the children can be primary.
Important Assumptions: The graph begins with a single source node and ends with a single sink node. All branches merge back into the primary branch, so by induction, all branches share a single sink node.
- We create paths for each node in the
childrenarray. Apathis defined as a sequence of nodes to reach the sink node in the graph beginning with the child node. We set the path containing the primary child node to becurrPrimaryPath. - We keep track of the other child nodes which we want to sort in an array called
nodesToSort. We keep track of the sorted child nodes insortedNodes. We initialize:nodesToSortcontains all child nodes except fo the the primary child node.sortedNodescontains only the primary child node.
- We start looking at the nodes (i.e., descendants) in the
currPrimaryPathfrom left to right. For every descendant incurrPrimaryPath, we determine if any of the paths corresponding to child nodes innodesToSortshares that descendant and if so, where in the path it is encountered.- If multiple paths contain the descendant, the winner is the one which we encounter the descendant earliest in the path. Once we find the winner from the
nodesToSort, we add that child node to the end ofsortedNodes, remove that child node fromnodesToSort, and make that child node's path the newcurrPrimaryPath. - If no paths contains the descendant, we move on to the next descendant until we reach the end of
currPrimaryPath. At that point, we may choose any child nodenodesToSortas the winner.
- If multiple paths contain the descendant, the winner is the one which we encounter the descendant earliest in the path. Once we find the winner from the
- Then, we recursively update
currPrimaryPath, add next child nodes tosortedNodes, and remove child nodes fromnodesToSortuntil we have nothing left innodesToSort.
Aside: Can we use Dijkstra's?
- Yes, O(N * M)
- Suppose we build the graph backward beginning from the sink node where all the branches eventually merge into and creates a tree in which the sink node is the root and all our leaves are nodes in our
childrenarray. Then we look at the descendants of the primary path from left to right. For each descendant, we traverse the tree starting from the descendant until we reach the leaf nodes and label the leaf nodes a number designating the number of hops from the descendant node. Then we get back the subset of leaf nodes branching from that descendant sorted by their distance from the descendant node (in ascending order) and append it to our result. After that, we look at the next descendant of the primary path until we reach the sink node.
- We are given an array of children. We only perform the sort when there are 2 or more children and only one of the children can be primary.
- Naive: The column number of the node is
workflowStepOrder * 2.
The row number of the node is determined as follows:
- Naive:
rowNumis the first unoccupied. Each node is placed into the next empty spot in the column array. - Better: If no parent,
rowNumis the first unoccupied. If has parent,rowNumis the parent'srowNum - Even Better: If no parent, rowNum is the first unoccupied. If has parent, rowNum is parent rowNum but if that is occupied, then we shift col 2 places to the right.
- The flowchart has to be a DAG
- The longest path cannot be greater than 1000 (limitation of the grid column template specified in the css)
This project was bootstrapped with Create React App.
In the project directory, you can run:
npm start runs the app in the development mode.
Open http://localhost:3000 to view it in the browser.
npm test launches the test runner in the interactive watch mode.
See the section about running tests for more information.
- Render connectors for step merge from 2 or more rows away
- Sort workflow steps with the same WorkflowStepOrder - encode that in priority
- Support for when a decision step has the same distance from the root node as another node - need to shift things to the right




