I am looking for a javascript library or a non-standard solution where I can drag and drop components and maintain a connection between them. For example, connect one node to another and freely move nodes where I want).

By maintaining relationships, I mean that the various components must support their relative relationships (as a block diagram).

After drawing the links, I need to get JSON data about their relative relationships.

Below is an example of what I'm talking about:

enter image description here

In the picture above, as you can see, I have different nodes that are interconnected. How can I achieve these goals with a library or an individual solution?

The image above is from the strom-react-diagrmas reaction library. I tried it, but it uses SVG and lacks many of the settings that I want.

I also tried rete.js , but I could not customize it to suit my needs (customize forms, etc.)

enter image description here

I am also thinking of creating a solution from scratch, the only problem I encounter is how can I connect two or more div elements on a canvas, supporting their interconnection?

Notice why am I doing this?

  1. My goal is that I want to create a visual editor in which a non-technical person could design a stream, and then I want to export JSON in order to store it in my database accordingly.

  2. When I load the canvas of this stream again, I can visualize the relationship flow again, together with the associated nodes, based on the JSON data that I have.

Can you suggest something to me if you come across such a situation?

Any help from you guys is very much appreciated.

  • And in such questions, translations, you can add your answer?) - Stepan Kasyanenko
  • association: stackoverflow.com/q/54771893/7394871 - Alexandr_TT
  • @StepanKasyanenko Yes, of course! Answers are welcome and very necessary in all questions. More knowledge is better. - Nicolas Chabanovsky

2 answers 2

You can use the GOJS library.

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

An example from the official site .

 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> 

  • Thanks for the example. really great library. (+) But you do not want to post a similar answer in the topic on enSO .. There after all, the author or a ready-made library or your solution was looking for two possible answers. - Alexandr_TT
  • @Alexandr_TT I thought these are old questions you translate. And he yesterday, it turns out. Yes, you can answer there. Thank! - Stepan Kasyanenko
  • @Alexandr_TT what do you say? - Stepan Kasyanenko

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

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

In HTML, you have all your elements inside the #wrap element.
Under the div elements is the svg element of the same size as # wrap .
Divas are positioned absolutely with the top and left attributes in percent.

Svg canvas has viewBox = "0 0 100 100" and preserveAspectRatio = "none" in order to adapt the drawing to the size # wrap

Connecting lines are paths drawn in svg with fill: none and vector-effect: non-scaling-stroke; to achieve a uniform thickness lines with increasing or decreasing the size of the canvas.

At the end you can save an array of points for the data.

I hope this can give you an idea of ​​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() // расчёт x и y координат для разъемов числом от 0 до 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){ // соединяет последние 2 точки в массиве 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${ax},${ay} C50,${ay} 50 ${by} ${bx} ${by}`; 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> 

Related answer: Drawing curved lines with SVG arrows from one div to another div

  • Very interesting, it is necessary to experiment with it. - LFC