3

I'm looking for a javascript library or custom solution where I can freely drag drop components and maintain the relationship between them (like which node is connected to what & move the nodes freely wherever I want)

By maintaining the relationship I mean the different components should maintain their interconnection flow(like a flowchart). After drawing them I need to get the JSON data of their relationship.

Below is a sample glimpse of what I'm talking about

Strom React Diagrams Which I developed

In the above picture as you can see I have different nodes which are interconnected. How can I achieve these things by library or custom solution?

The above image is from the strom-react-diagrmas react library. I have tried this but it uses SVG and lacks a lot of customization that I want.

I've also tried rete.js but unable to customize it as per my need(customizing shapes etc).

Rete js

I'm also thinking of building a solution from scratch, the only problem I'm facing is how do I join two or multiple divs on canvas maintaining its relationship?

Note why am I doing this?

  1. My goal behind doing this is I want to create a visual editor where a non-technical person can design the flow and then I want to export the JSON to store it accordingly in my database.
  2. When I load the canvas of this flow again I should be able to render the interconnection flow along with connected nodes again based on the JSON data that I will have.

Can you suggest me something if you have come across such a situation? Any help from you guys is really appreciated.

Harsh Makadia
  • 3,263
  • 5
  • 30
  • 42

4 Answers4

5

I would have loved to know more about the layout you have in mind.

This is a demo where you can click the grey dots. When 2 dots are clicked a connection between the 2 dots is drawn on the svg canvas.

In the HTML you have all your elements inside a #wrap element. Underneath the divs there is an svg element, the same size as the #wrap. The divs are positioned absolute with top and left attributes in percentages. The svg canvas has a viewBox="0 0 100 100" and preserveAspectRatio="none" in order to adapt the drawing to the size of the #wrap. The connectors are paths drawn on the svg with fill:none and vector-effect: non-scaling-stroke; for an uniform stroke on a stretched or squished canvas.

In the end you can save the points array for the data.

I hope this can give you an idea about what you need to do.

const SVG_NS = 'http://www.w3.org/2000/svg';
let mainBox = wrap.getBoundingClientRect();

let dots = Array.from(document.querySelectorAll(".dot"))

let points = [];
let count = 0;

dots.forEach(d=>{
  d.addEventListener("click",(e)=>{
    
    let bcr = d.getBoundingClientRect();
    mainBox = wrap.getBoundingClientRect()
    // calculate the x and y coordinates for the connectors as a number from 0 to 100 
    let x = map(bcr.left - mainBox.left + bcr.width/2, mainBox.left, mainBox.left + mainBox.width, 0, 100);
    let y = map(bcr.top - mainBox.top + bcr.height/2, mainBox.top, mainBox.top + mainBox.height, 0, 100);
    
    points.push({x,y})
    if(count % 2 == 1){
      // connects the last 2 dots in the array
      drawConnector(points[points.length-1],points[points.length-2])
    }    
    count++;
  })
})

function map(n, a, b, _a, _b) {
  let d = b - a;
  let _d = _b - _a;
  let u = _d / d;
  return _a + n * u;
}


function drawConnector(a,b){
  let path = document.createElementNS(SVG_NS, 'path');
  let d = `M${a.x},${a.y} C50,${a.y} 50 ${b.y} ${b.x} ${b.y}`;
  path.setAttributeNS(null,"d",d);
  svg.appendChild(path)
}
* {
  box-sizing: border-box;
}
.box {
  width: 20%;
  height: 100px;
  border: 1px solid #bbb;
  border-radius: 10px;
  position: absolute;
  background: #efefef;
}

#wrap {
  position: absolute;
  margin:auto;
  top:0;bottom:0;left:0;right:0;
  width: 60%;
  height: 350px;
  border: 1px solid;
  min-width: 350px;
}

svg {
  position: absolute;
  width: 100%;
  height: 100%;
  background: rgba(0, 100, 250, 0.25);
}

.dot {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px solid #999;
  background: #d9d9d9;
  position: relative;
  left: calc(100% - 10px);
}

.dot:hover {
  border-color: tomato;
}

path {
  fill: none;
  stroke: black;
  vector-effect: non-scaling-stroke;
  stroke-width: 1px;
  stroke: #555;
}
<div id="wrap">
<svg id="svg" viewBox="0 0 100 100" preserveAspectRatio="none"></svg>  
  
<div class="box" id="a" style="top: 10%; left: 10%;">
  <div class="dot" style="top:20px" ></div>
  <div class="dot" style="top:40px" ></div>
</div>
<div class="box" id="b" style="top: 60%; left: 10%;">
  <div class="dot" style="top:20px" ></div>
  <div class="dot" style="top:40px" ></div>
</div>
<div class="box"  id="c" style="top: 30%; left: 65%; ">
  <div class="dot" style="top:20px; left:-10px" ></div>
  <div class="dot" style="top:40px; left:-10px" ></div>  
</div>
  
</div>
enxaneta
  • 31,608
  • 5
  • 29
  • 42
4

Rete.js can be customized via custom Vue.js components.

Example

Visual part of the framework is represented by one of the plugins for rendering: vue or stage0. I'm prefer Vue.js so I have developed the plugin based on it.

Create custom socket and node


var CustomSocket = {
  template: `<div class="socket"
    :class="[type, socket.name, used()?'used':''] | kebab"
    :title="socket.name+'\\n'+socket.hint"></div>`,
  props: ['type', 'socket', 'used']
}


var CustomNode = {
  template,
  mixins: [VueRenderPlugin.mixin],
  methods:{
    used(io){
      return io.connections.length;
    }
  },
  components: {
    Socket: /*VueRenderPlugin.Socket*/CustomSocket
  }
}


class NumComponent extends Rete.Component {

    constructor(){
        super("Number");
        this.data.component = CustomNode;
    }
...

Template:

  <div class="node" :class="[selected(), node.name] | kebab">
  <div class="title">{{node.name}}</div>
  <div class="content">
    <div class="col" v-if="node.controls.size&gt;0 || node.inputs.size&gt;0">
      <div class="input" v-for="input in inputs()" :key="input.key" style="text-align: left">
        <Socket v-socket:input="input" type="input" :socket="input.socket" :used="() => input.connections.length"></Socket>
        <div class="input-title" v-show="!input.showControl()">{{input.name}}</div>
        <div class="input-control" v-show="input.showControl()" v-control="input.control"></div>
     </div>
     <div class="control" v-for="control in controls()" v-control="control"></div>
    </div>
    <div class="col">
      <div class="output" v-for="output in outputs()" :key="output.key">
        <div class="output-title">{{output.name}}</div>
        <Socket v-socket:output="output" type="output" :socket="output.socket" :used="() => output.connections.length"></Socket>
      </div>
    </div>
  </div> 
</div>

As a result, you can customize nodes, connections and background without restrictions

Ni55aN
  • 196
  • 1
  • 12
3

You can use a GOJS.

This is a great solution for a commercial project. It is flexible in settings and makes it quite easy to do amazing things.

Example on official website.

function init() {
  if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
  var $ = go.GraphObject.make; // for conciseness in defining templates

  myDiagram =
    $(go.Diagram, "myDiagramDiv", {
      validCycle: go.Diagram.CycleNotDirected, // don't allow loops
      // For this sample, automatically show the state of the diagram's model on the page
      "undoManager.isEnabled": true
    });

  // This template is a Panel that is used to represent each item in a Panel.itemArray.
  // The Panel is data bound to the item object.
  var fieldTemplate =
    $(go.Panel, "TableRow", // this Panel is a row in the containing Table
      new go.Binding("portId", "name"), // this Panel is a "port"
      {
        background: "transparent", // so this port's background can be picked by the mouse
        fromSpot: go.Spot.Right, // links only go from the right side to the left side
        toSpot: go.Spot.Left,
        // allow drawing links from or to this port:
        fromLinkable: true,
        toLinkable: true
      },
      $(go.Shape, {
          width: 12,
          height: 12,
          column: 0,
          strokeWidth: 2,
          margin: 4,
          // but disallow drawing links from or to this shape:
          fromLinkable: false,
          toLinkable: false
        },
        new go.Binding("figure", "figure"),
        new go.Binding("fill", "color")),
      $(go.TextBlock, {
          margin: new go.Margin(0, 5),
          column: 1,
          font: "bold 13px sans-serif",
          alignment: go.Spot.Left,
          // and disallow drawing links from or to this text:
          fromLinkable: false,
          toLinkable: false
        },
        new go.Binding("text", "name")),
      $(go.TextBlock, {
          margin: new go.Margin(0, 5),
          column: 2,
          font: "13px sans-serif",
          alignment: go.Spot.Left
        },
        new go.Binding("text", "info"))
    );

  // This template represents a whole "record".
  myDiagram.nodeTemplate =
    $(go.Node, "Auto", {
        copyable: false,
        deletable: false
      },
      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      // this rectangular shape surrounds the content of the node
      $(go.Shape, {
        fill: "#EEEEEE"
      }),
      // the content consists of a header and a list of items
      $(go.Panel, "Vertical",
        // this is the header for the whole node
        $(go.Panel, "Auto", {
            stretch: go.GraphObject.Horizontal
          }, // as wide as the whole node
          $(go.Shape, {
            fill: "#1570A6",
            stroke: null
          }),
          $(go.TextBlock, {
              alignment: go.Spot.Center,
              margin: 3,
              stroke: "white",
              textAlign: "center",
              font: "bold 12pt sans-serif"
            },
            new go.Binding("text", "key"))),
        // this Panel holds a Panel for each item object in the itemArray;
        // each item Panel is defined by the itemTemplate to be a TableRow in this Table
        $(go.Panel, "Table", {
            padding: 2,
            minSize: new go.Size(100, 10),
            defaultStretch: go.GraphObject.Horizontal,
            itemTemplate: fieldTemplate
          },
          new go.Binding("itemArray", "fields")
        ) // end Table Panel of items
      ) // end Vertical Panel
    ); // end Node

  myDiagram.linkTemplate =
    $(go.Link, {
        relinkableFrom: true,
        relinkableTo: true, // let user reconnect links
        toShortLength: 4,
        fromShortLength: 2
      },
      $(go.Shape, {
        strokeWidth: 1.5
      }),
      $(go.Shape, {
        toArrow: "Standard",
        stroke: null
      })
    );

  myDiagram.model =
    $(go.GraphLinksModel, {
      copiesArrays: true,
      copiesArrayObjects: true,
      linkFromPortIdProperty: "fromPort",
      linkToPortIdProperty: "toPort",
      nodeDataArray: [{
          key: "Record1",
          fields: [{
              name: "field1",
              info: "",
              color: "#F7B84B",
              figure: "Ellipse"
            },
            {
              name: "field2",
              info: "the second one",
              color: "#F25022",
              figure: "Ellipse"
            },
            {
              name: "fieldThree",
              info: "3rd",
              color: "#00BCF2"
            }
          ],
          loc: "0 0"
        },
        {
          key: "Record2",
          fields: [{
              name: "fieldA",
              info: "",
              color: "#FFB900",
              figure: "Diamond"
            },
            {
              name: "fieldB",
              info: "",
              color: "#F25022",
              figure: "Rectangle"
            },
            {
              name: "fieldC",
              info: "",
              color: "#7FBA00",
              figure: "Diamond"
            },
            {
              name: "fieldD",
              info: "fourth",
              color: "#00BCF2",
              figure: "Rectangle"
            }
          ],
          loc: "280 0"
        }
      ],
      linkDataArray: [{
          from: "Record1",
          fromPort: "field1",
          to: "Record2",
          toPort: "fieldA"
        },
        {
          from: "Record1",
          fromPort: "field2",
          to: "Record2",
          toPort: "fieldD"
        },
        {
          from: "Record1",
          fromPort: "fieldThree",
          to: "Record2",
          toPort: "fieldB"
        }
      ]
    });
}

init();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gojs/2.0.3/go.js"></script>
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:300px"></div>
</div>
Stepan Kasyanenko
  • 3,176
  • 1
  • 15
  • 23
1

Hello guys I have decided to start my own flowchart open project with ReactJs, but if you need, you can adapt it to pure javascript, please feel free to contribute.

enter image description here https://github.com/lmoraobando/lmDiagram

Leonardo Mora
  • 349
  • 3
  • 11