React-diagrams: aligning HTML and SVG with drag, drop, pan and zoom

Created on 27 Apr 2017  路  2Comments  路  Source: projectstorm/react-diagrams

Really cool project. Thanks for making this.

You've solved a general problem of integrating HTML and SVG, which I think a lot of people will face, and I was hoping hoping you could say a bit about how you did it. In particular, how did you get the links to point to a particular part of an HTML element when the HTML element moves, and how did you get pan/zoom to work (while having good performance)? I'll be reading your code in the mean time, but thought this would be generally useful to a lot of people doing interactive dataviz.

architecture question

Most helpful comment

Hey man, thats really awesome of you to say such kind words. Basically it all started with me using JointJS. JointJS is a really awesome project that allows you to create really sophisticated diagrams using a powerful engine that solves the problem entirely with SVG. I had a number of problems however:

1) The system was very heavy and required JQuery, BackBone, Custom SVG Library, a number of other little libraries and then obviously the core. I wanted something that was super lightweight and didn't rely on libraries that pollute the global space as JQuery Does.
2) The library didn't work very will with any sort of module loading system, and while there wasn't really a solution to this when JointJS was written, even though an effort was made to modernize it, at the time it was really lack luster. I also had issues getting the dependent libraries to work with things like browserify (Jquery, backbone etc..)
3) I wanted to have something that would be easier to create my own custom nodes that (very importantly) had lots of interactivity like drop downs and input fields etc.. But because of the attention to detail I wanted to apply to the design of the different nodes (as seen in my first example picture on the front page), simply embedding HTML into the SVG was going to be difficult.
4) Having HTML float on-top of a hidden SVG node was a nightmare because the HTML would need to track the SVG dimensions, and there is no real good way to do this that is performant.
5) I did not like how the links connected to the ports, there was a sort of threshold where the links would never actually point to the center of the port, but to the edges of the port.

So I thought "surely I could make a much more simpler library that solves much less of the problems that JointJS solves, but make sure that the things we DO solve solve well."

Therefore I decided to set some design goals:

1) Always use react for the view, React is HOT and React is easy to make views with.
2) Reverse the concept of HTML tracking SVG and rather have SVG track HTML, since the HTML nodes are going to be whats changing as their properties change.
3) Make sure its compatible with a modern build system.
4) Use a little external libraries as possible.

With that in mind I decided we were going to need two layers. One to render the SVG stuff and another to render the HTML stuff. I decided to use SVG for paths and HTML for everything else, since you can do everything with HTML except draw paths.

So seeing as the SVG Links would need to track the ports, I added logic that on each render cycle, we ask the HTML node for the actual DOM offset of each port. This happens here:

https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/LinkLayerWidget.ts#L45

Whenever we ask the system for an actual DOM location, we work out the "virtual" location by reversing the transforms of the canvas according to where it is positioned in the document.

This is obviously quite slow, so we implemented a system where tell the engine what it is we are actually translating before we render and we check that directive on each render cycle here:

https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/NodeWidget.ts#L26

This allows us to have complete control over the performance. We then just apply some simple maths which emulate matrix transformations to handle the transforms. For translations we just add a canvas offset to each X & Y location, for zooms, we just run a scaleX and scaleY on the whole canvas.

The last thing though, which I was quite adamant we needed to do, was make this library a 'Starting Point' and 'Engine' only, so that people could extend the platform without needing to hack the core. We did this with the use of factories which return their own React Widgets that get rendered inside a wrapping NodeWidget. The node widget (https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/NodeWidget.ts) handles the translations and scaling, so all you have to do is render a react widget that can fit inside there. We also made it so that if you pass in a PortWidget which handles adding attributes to the DOM that allow the links to find the actual locations, then you can attach links to them. Of course, you can also pass in your own CustomPort widget so long as you add this to its attribute list:

https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/PortWidget.ts#L35-L36

I hope this has been somewhat helpful and informative xD

All 2 comments

Hey man, thats really awesome of you to say such kind words. Basically it all started with me using JointJS. JointJS is a really awesome project that allows you to create really sophisticated diagrams using a powerful engine that solves the problem entirely with SVG. I had a number of problems however:

1) The system was very heavy and required JQuery, BackBone, Custom SVG Library, a number of other little libraries and then obviously the core. I wanted something that was super lightweight and didn't rely on libraries that pollute the global space as JQuery Does.
2) The library didn't work very will with any sort of module loading system, and while there wasn't really a solution to this when JointJS was written, even though an effort was made to modernize it, at the time it was really lack luster. I also had issues getting the dependent libraries to work with things like browserify (Jquery, backbone etc..)
3) I wanted to have something that would be easier to create my own custom nodes that (very importantly) had lots of interactivity like drop downs and input fields etc.. But because of the attention to detail I wanted to apply to the design of the different nodes (as seen in my first example picture on the front page), simply embedding HTML into the SVG was going to be difficult.
4) Having HTML float on-top of a hidden SVG node was a nightmare because the HTML would need to track the SVG dimensions, and there is no real good way to do this that is performant.
5) I did not like how the links connected to the ports, there was a sort of threshold where the links would never actually point to the center of the port, but to the edges of the port.

So I thought "surely I could make a much more simpler library that solves much less of the problems that JointJS solves, but make sure that the things we DO solve solve well."

Therefore I decided to set some design goals:

1) Always use react for the view, React is HOT and React is easy to make views with.
2) Reverse the concept of HTML tracking SVG and rather have SVG track HTML, since the HTML nodes are going to be whats changing as their properties change.
3) Make sure its compatible with a modern build system.
4) Use a little external libraries as possible.

With that in mind I decided we were going to need two layers. One to render the SVG stuff and another to render the HTML stuff. I decided to use SVG for paths and HTML for everything else, since you can do everything with HTML except draw paths.

So seeing as the SVG Links would need to track the ports, I added logic that on each render cycle, we ask the HTML node for the actual DOM offset of each port. This happens here:

https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/LinkLayerWidget.ts#L45

Whenever we ask the system for an actual DOM location, we work out the "virtual" location by reversing the transforms of the canvas according to where it is positioned in the document.

This is obviously quite slow, so we implemented a system where tell the engine what it is we are actually translating before we render and we check that directive on each render cycle here:

https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/NodeWidget.ts#L26

This allows us to have complete control over the performance. We then just apply some simple maths which emulate matrix transformations to handle the transforms. For translations we just add a canvas offset to each X & Y location, for zooms, we just run a scaleX and scaleY on the whole canvas.

The last thing though, which I was quite adamant we needed to do, was make this library a 'Starting Point' and 'Engine' only, so that people could extend the platform without needing to hack the core. We did this with the use of factories which return their own React Widgets that get rendered inside a wrapping NodeWidget. The node widget (https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/NodeWidget.ts) handles the translations and scaling, so all you have to do is render a react widget that can fit inside there. We also made it so that if you pass in a PortWidget which handles adding attributes to the DOM that allow the links to find the actual locations, then you can attach links to them. Of course, you can also pass in your own CustomPort widget so long as you add this to its attribute list:

https://github.com/projectstorm/react-diagrams/blob/master/src/widgets/PortWidget.ts#L35-L36

I hope this has been somewhat helpful and informative xD

Super helpful, thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

M2Costa picture M2Costa  路  3Comments

jardg picture jardg  路  3Comments

iddan picture iddan  路  3Comments

DanieLazarLDAPPS picture DanieLazarLDAPPS  路  3Comments

kmannislands picture kmannislands  路  3Comments