JS Plumb in React
Flowcharting & drawing lines on html using jsPlumb
Time for more & more AI & AR web apps has come, & with advance in technology, clients & customers require the web service or app to provide more information, by showing some animations, & dynamic drawings over the subject such as lines connecting the various parts of the AI subject, which add a lot more interactivity & comprehension of user with the service, also it boosts the AI appeal & quality of the service.
A similar scenario can be thought of a simple AI project, where a user uploads an invoice in format of image/pdf , it gets processed on backend, the response of the document contains the JSON for different characters recognized from the document uploaded, which then has to be shown on either side of the invoice document image on the screen in list format.
Also the recognized characters from the uploaded document have to be shown highlighted on the screen over the image of the document.
On clicking any of the listings, a connector line should appear connecting the list item to its corresponding highlighted characters.
That appears to be a Rocket science. Isn’t it ? Let's check out.
How JsPlumb works ?
JSplumb is a flowcharting library which is used for showing real flowcharts on html page. It has draggable elements, moveable connectors, different types of connectors. More can be read on
https://docs.jsplumbtoolkit.com/toolkit/current/index.html
To show connection between 2 points, which can be 2 html elements like <div> or <rect>, both the pairing elements need to be same. viz - <div> to <div> or <rect> to <rect>
Secondly, both elements should have unique ids & have jsPlumb endpoints added right after the component gets loaded onto html
Such as,
Source element - <rect id=”name_source”></rect>
Target element - <rect id=”name_target”></rect>
Now endpoints should be made by this jsPlumb method for all source & target elements
jsPlumbInstance.addEndpoint(<ELEMENT ID>,<PROPERTIES SET JSON FOR SOURCE OR TARGET ENDPOINT TYPE>, {
anchor: "<POSITION OF THE ENDPOINT ON THE ELEMENT LEFT/RIGHT>",
uuid: “<ELEMENT ID>”,
});
jsPlumbInstance.addEndpoint(“name_source”, sourceEndpoint, {
anchor: "RightMiddle",
uuid: “name_source”,
});
jsPlumbInstance.addEndpoint(“name_target”, targetEndpoint, {
anchor: "LeftMiddle",
uuid: “name_target”,
});
The ‘sourceEndpoint’ & ‘targetEndpoint’ used in the snippets above are also a set of key value pairs provided by jsPlumb, that will be discussed later.
We are 90% done here, we have elements to be connected, we have their ids, we have setup their endpoints, now we can connect these elements using this method
jsPlumbInstance.connect({
uuids: [‘<SOURCE ELEMENT ID>’, ‘<TARGET ELEMENT ID>’]
});
We need to pass the respective ids for the elements we want to connect
jsPlumbInstance.connect({
uuids: [‘name_source’, ‘name_target’]
});
We’ll get connected elements like this
Setup
JS Plumb is not a node library, hence it cannot be npm installed into the project.
Inside our React js project, go to /public/index.html
& use these script tags in the head section
<link rel="stylesheet" href="./css/jsplumbtoolkit-defaults.css" />
<link rel="stylesheet" href="./css/main.css" />
<link rel="stylesheet" href="./css/jsplumbtoolkit-demo.css" />
<link rel="stylesheet" href="./demo.css" />
Next, use these tags in body section
<script src="./js/jsplumb.js"></script>
<script src="./js/demo-list.js"></script>
Now we’re ready to use the JSplumb instance inside our project
Before that, we need to know what kind of response we need for the working of jsPlumb
For each node , whether starting or ending, we need some coordinates to show the elements on, so the response needs to have atleast this set of data for each node
{
"source_key": "name_source",
"target_key": "name_target",
"value": " <OCR value obtained from response>",
"boundingCoordinates": [
{
"x1": 0.0856633,
"y1": 0.24884406,
"x2": 0.21594289,
"y2": 0.26523748,
"x3": 0.21594289,
"y3": 0.27742749,
"x4": 0.0856633,
"y4": 0.26229507
}
]
}
On the basis of which, multiple elements for source can be rendered on either side of document image with listing pairs such as
{
sourceElementsSet.map(item => {
return (
<svg id='svgElement0'>
<g id='boundingbox_group_0'>
<rect x= {item.boundingCoordinates[0].x1*100+'%'}
y = {item.boundingCoordinates[0].y1*100+'%'}
width={(item.boundingCoordinates[0].x2*100 - item.boundingCoordinates[0].x1*100)+'%'}
height={(item.boundingCoordinates[0].y4*100 - item.boundingCoordinates[0].y1*100)+'%'}
id={item.key}
class="annotation-symbols-bounding-box"></rect>
</g>
</svg>
)
})
}
Where sourceElementsSet is our array of objects with prescribed set of data as shown above this code. We can render target elements in similar manner using say targetElementsSet , which will contain target elements coordinates to be shown on the document image.
First lets import following items
import $ from "jquery";
import '../../css/font.css';
import '../../css/jsplumbtoolkit-defaults.css';
import '../../css/main.css';
import '../../css/jsplumbtoolkit-demo.css';
import '../../css/demo.css';
Then , using this function inside componentDidMount such as , also we need to pass instance of the class to this function as ‘self’ so that it can access the class instance ‘this’ inside function to use class methods.
For example, this.setState({myVar}); will be used as self.setState({myVar});
componentDidMount() {
this.setJsPlumbCanvas(this); // passing ‘this’ to access this keyword
}
setJsPlumbCanvas = (self) => {
window.jsPlumb.ready(function () {
var offsetCalculators = {
"RECT": function (el, parentOffset) {
var x = el[0].getBoundingClientRect().x,
y = el[0].getBoundingClientRect().y + (el[0].getBoundingClientRect().height / 2);
return {
left: x,
top: y
};
}
};
var sizeCalculators = {
"RECT": function (el) {
var w = 0,
h = 0;
return [w, h];
}
};
var originalOffset = window.jsPlumbInstance.prototype.getOffset;
var originalSize = window.jsPlumbInstance.prototype.getSize;
window.jsPlumbInstance.prototype.getOffset = function (el) {
var tn = el.tagName.toUpperCase();
if (offsetCalculators[tn]) {
return offsetCalculators[tn]($(el), $(el).parent().offset());
} else
return $(el).offset();
};
window.jsPlumbInstance.prototype.getSize = function (el) {
var tn = el.tagName.toUpperCase();
if (sizeCalculators[tn]) {
return sizeCalculators[tn]($(el));
} else
return [$(el).outerWidth(), $(el).outerHeight()];
};
instance = (window.jsp = window.jsPlumb.getInstance({
DragOptions: {
cursor: "pointer",
zIndex: 2000
},
ConnectionOverlays: [
[
"Label",
{
location: 0.1,
id: "label",
cssClass: "aLabel",
},
],
],
Container: "canvas",
}));
instance.registerConnectionType("basic", basicType);
//Checked
for (let i = 0; i < self.state.sourceKeys.length; i++) {
for (let j = 0; j < self.state.sourceKeys[i].length; j++) {
//For Left Node end points self.addEndPoint(self.state.sourceKeys[i][j].key,sourceEndpoint,"RightMiddle") ;
}}
for (let k = 0; k < self.state.targetKeys.length; k++) {
for (let l = 0; l < self.state.targetKeys[k].length; l++) {
//For Right Node end points self.addEndPoint(self.state.targetKeys[k][l].key,targetEndPoint,"LeftMiddle") ;
}
}
});
}
Making an onClick event on the items we rendered above,
<rect x= {item.boundingCoordinates[0].x1*100+'%'}
onClick = {() => this.handleConnectionSetup(item)}
y = {item.boundingCoordinates[0].y1*100+'%'}
handleLeftClick = (item) => {
let {source_key,target_key} = item;
if (connection) {
instance.deleteConnection(connection);
}
if (rightKey) {
connection = instance.connect({
uuids: [source_key, target_key],
});
}
}
Thats all about setting up flowcharting in react app using jsPlumb, Thanks for reading.