UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

387 lines (360 loc) 18 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/> <meta name="description" content="A data mapping diagram to show and edit the relationships between items in two different trees."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title>GoJS Tree Mapper</title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary"> <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2"> <div class="md:pl-4"> <a class="text-white hover:text-white no-underline hover:no-underline font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../"> <h1 class="my-0 p-1 ">GoJS</h1> </a> </div> <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path> <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20"> <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0"> <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li> </ul> </div> </div> <hr class="border-b border-gray-600 opacity-50 my-0 py-0" /> </nav> <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="../release/go.js"></script> <div id="allSampleContent" class="p-4 w-full"> <script id="code"> // Use a TreeNode so that when a node is not visible because a parent is collapsed, // connected links seem to be connected with the lowest visible parent node. class TreeNode extends go.Node { findVisibleNode() { // redirect links to lowest visible "ancestor" in the tree var n = this; while (n !== null && !n.isVisible()) { n = n.findTreeParentNode(); } return n; } } // end TreeNode // Control how Mapping links are routed: // - "Normal": normal routing with fixed fromEndSegmentLength & toEndSegmentLength // - "ToGroup": so that the link routes stop at the edge of the group, // rather than going all the way to the connected nodes // - "ToNode": so that they go all the way to the connected nodes // but only bend at the edge of the group var ROUTINGSTYLE = "ToGroup"; // If you want the regular routing where the Link.[from/to]EndSegmentLength controls // the length of the horizontal segment adjacent to the port, don't use this class. // Replace MappingLink with a go.Link in the "Mapping" link template. class MappingLink extends go.Link { getLinkPoint(node, port, spot, from, ortho, othernode, otherport) { if (ROUTINGSTYLE !== "ToGroup") { return super.getLinkPoint(node, port, spot, from, ortho, othernode, otherport); } else { var r = port.getDocumentBounds(); var group = node.containingGroup; var b = (group !== null) ? group.actualBounds : node.actualBounds; var op = othernode.getDocumentPoint(go.Spot.Center); var x = (op.x > r.centerX) ? b.right : b.left; return new go.Point(x, r.centerY); } } computePoints() { var result = super.computePoints(); if (result && ROUTINGSTYLE === "ToNode") { var fn = this.fromNode; var tn = this.toNode; if (fn && tn) { var fg = fn.containingGroup; var fb = fg ? fg.actualBounds : fn.actualBounds; var fpt = this.getPoint(0); var tg = tn.containingGroup; var tb = tg ? tg.actualBounds : tn.actualBounds; var tpt = this.getPoint(this.pointsCount-1); this.setPoint(1, new go.Point((fpt.x < tpt.x) ? fb.right : fb.left, fpt.y)); this.setPoint(this.pointsCount-2, new go.Point((fpt.x < tpt.x) ? tb.left : tb.right, tpt.y)); } } return result; } } // end MappingLink function init() { // Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make // For details, see https://gojs.net/latest/intro/buildingObjects.html const $ = go.GraphObject.make; // for conciseness in defining templates function handleTreeCollapseExpand(e) { e.subject.each(n => { n.findExternalTreeLinksConnected().each(l => l.invalidateRoute()); }); } myDiagram = new go.Diagram("myDiagramDiv", { "commandHandler.copiesTree": true, "commandHandler.deletesTree": true, "TreeCollapsed": handleTreeCollapseExpand, "TreeExpanded": handleTreeCollapseExpand, // newly drawn links always map a node in one tree to a node in another tree "linkingTool.archetypeLinkData": { category: "Mapping" }, "linkingTool.linkValidation": checkLink, "relinkingTool.linkValidation": checkLink, "undoManager.isEnabled": true, "ModelChanged": e => { if (e.isTransactionFinished) { // show the model data in the page's TextArea document.getElementById("mySavedModel").textContent = e.model.toJson(); } } }); // All links must go from a node inside the "Left Side" Group to a node inside the "Right Side" Group. function checkLink(fn, fp, tn, tp, link) { // make sure the nodes are inside different Groups if (fn.containingGroup === null || fn.containingGroup.data.key !== -1) return false; if (tn.containingGroup === null || tn.containingGroup.data.key !== -2) return false; //// optional limit to a single mapping link per node //if (fn.linksConnected.any(l => l.category === "Mapping")) return false; //if (tn.linksConnected.any(l => l.category === "Mapping")) return false; return true; } // Each node in a tree is defined using the default nodeTemplate. myDiagram.nodeTemplate = $(TreeNode, { movable: false, copyable: false, deletable: false }, // user cannot move an individual node // no Adornment: instead change panel background color by binding to Node.isSelected { selectionAdorned: false, background: "white", mouseEnter: (e, node) => node.background = "aquamarine", mouseLeave: (e, node) => node.background = node.isSelected ? "skyblue" : "white" }, new go.Binding("background", "isSelected", s => s ? "skyblue" : "white").ofObject(), // whether the user can start drawing a link from or to this node depends on which group it's in new go.Binding("fromLinkable", "group", k => k === -1), new go.Binding("toLinkable", "group", k => k === -2), $("TreeExpanderButton", // support expanding/collapsing subtrees { width: 14, height: 14, "ButtonIcon.stroke": "white", "ButtonIcon.strokeWidth": 2, "ButtonBorder.fill": "goldenrod", "ButtonBorder.stroke": null, "ButtonBorder.figure": "Rectangle", "_buttonFillOver": "darkgoldenrod", "_buttonStrokeOver": null, "_buttonFillPressed": null }), $(go.Panel, "Horizontal", { position: new go.Point(16, 0) }, //// optional icon for each tree node //$(go.Picture, // { width: 14, height: 14, // margin: new go.Margin(0, 4, 0, 0), // imageStretch: go.GraphObject.Uniform, // source: "images/defaultIcon.png" }, // new go.Binding("source", "src")), $(go.TextBlock, new go.Binding("text", "key", s => "item " + s)) ) // end Horizontal Panel ); // end Node // These are the links connecting tree nodes within each group. myDiagram.linkTemplate = $(go.Link); // without lines myDiagram.linkTemplate = // with lines $(go.Link, { selectable: false, routing: go.Link.Orthogonal, fromEndSegmentLength: 4, toEndSegmentLength: 4, fromSpot: new go.Spot(0.001, 1, 7, 0), toSpot: go.Spot.Left }, $(go.Shape, { stroke: "lightgray" })); // These are the blue links connecting a tree node on the left side with one on the right side. myDiagram.linkTemplateMap.add("Mapping", $(MappingLink, { isTreeLink: false, isLayoutPositioned: false, layerName: "Foreground" }, { fromSpot: go.Spot.Right, toSpot: go.Spot.Left }, { relinkableFrom: true, relinkableTo: true }, $(go.Shape, { stroke: "blue", strokeWidth: 2 }) )); myDiagram.groupTemplate = $(go.Group, "Auto", { deletable: false, layout: makeGroupLayout() }, new go.Binding("position", "xy", go.Point.parse).makeTwoWay(go.Point.stringify), new go.Binding("layout", "width", makeGroupLayout), $(go.Shape, { fill: "white", stroke: "lightgray" }), $(go.Panel, "Vertical", { defaultAlignment: go.Spot.Left }, $(go.TextBlock, { font: "bold 14pt sans-serif", margin: new go.Margin(5, 5, 0, 5) }, new go.Binding("text")), $(go.Placeholder, { padding: 5 }) ) ); function makeGroupLayout() { return $(go.TreeLayout, // taken from samples/treeView.html { alignment: go.TreeLayout.AlignmentStart, angle: 0, compaction: go.TreeLayout.CompactionNone, layerSpacing: 16, layerSpacingParentOverlap: 1, nodeIndentPastParent: 1.0, nodeSpacing: 0, setsPortSpot: false, setsChildPortSpot: false, // after the tree layout, change the width of each node so that all // of the nodes have widths such that the collection has a given width commitNodes: function() { // method override must be function, not => go.TreeLayout.prototype.commitNodes.call(this); if (ROUTINGSTYLE === "ToGroup") { updateNodeWidths(this.group, this.group.data.width || 100); } } }); } // Create some random trees in each group var nodeDataArray = [ { isGroup: true, key: -1, text: "Left Side", xy: "0 0", width: 150 }, { isGroup: true, key: -2, text: "Right Side", xy: "300 0", width: 150 } ]; var linkDataArray = [ { from: 6, to: 1012, category: "Mapping" }, { from: 4, to: 1006, category: "Mapping" }, { from: 9, to: 1004, category: "Mapping" }, { from: 1, to: 1009, category: "Mapping" }, { from: 14, to: 1010, category: "Mapping" } ]; // initialize tree on left side var root = { key: 0, group: -1 }; nodeDataArray.push(root); for (var i = 0; i < 11;) { i = makeTree(3, i, 17, nodeDataArray, linkDataArray, root, -1, root.key); } // initialize tree on right side root = { key: 1000, group: -2 }; nodeDataArray.push(root); for (var i = 0; i < 15;) { i = makeTree(3, i, 15, nodeDataArray, linkDataArray, root, -2, root.key); } myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); } // help create a random tree structure function makeTree(level, count, max, nodeDataArray, linkDataArray, parentdata, groupkey, rootkey) { var numchildren = Math.floor(Math.random() * 10); for (var i = 0; i < numchildren; i++) { if (count >= max) return count; count++; var childdata = { key: rootkey + count, group: groupkey }; nodeDataArray.push(childdata); linkDataArray.push({ from: parentdata.key, to: childdata.key }); if (level > 0 && Math.random() > 0.5) { count = makeTree(level - 1, count, max, nodeDataArray, linkDataArray, childdata, groupkey, rootkey); } } return count; } window.addEventListener('DOMContentLoaded', init); function updateNodeWidths(group, width) { if (isNaN(width)) { group.memberParts.each(n => { if (n instanceof go.Node) n.width = NaN; // back to natural width }); } else { var minx = Infinity; // figure out minimum group width group.memberParts.each(n => { if (n instanceof go.Node) { minx = Math.min(minx, n.actualBounds.x); } }); if (minx === Infinity) return; var right = minx + width; group.memberParts.each(n => { if (n instanceof go.Node) n.width = Math.max(0, right - n.actualBounds.x); }); } } // this function is only needed when changing the value of ROUTINGSTYLE dynamically function changeStyle() { // find user-chosen style name var stylename = "ToGroup"; var radio = document.getElementsByName("MyRoutingStyle"); for (var i = 0; i < radio.length; i++) { if (radio[i].checked) { stylename = radio[i].value; break; } } if (stylename !== ROUTINGSTYLE) { myDiagram.commit(diag => { ROUTINGSTYLE = stylename; diag.findTopLevelGroups().each(g => updateNodeWidths(g, NaN)); diag.layoutDiagram(true); // force layouts to happen again diag.links.each(l => l.invalidateRoute()); }); } } </script> <div id="sample"> <div id="myDiagramDiv" style="border: 1px solid black; width: 700px; height: 350px"></div> <p> This sample is like the <a href="records.html">Mapping Fields of Records</a> sample, but it has a collapsible tree of nodes on each side, rather than a simple list of items. The implementation of the trees comes from the <a href="treeView.html">Tree View</a> sample. </p> <p> Draw new links by dragging from any field (i.e. any tree node). Reconnect a selected link by dragging its diamond-shaped handle. A minor enhancement to this diagram supports operator nodes that transform data from various fields on the left to provide values for fields on the right. </p> <p> This sample supports three different routing styles:<br> <input type="radio" name="MyRoutingStyle" onclick="changeStyle()" value="Normal" /> "Normal"<br> <input type="radio" name="MyRoutingStyle" onclick="changeStyle()" value="ToGroup" checked="checked"/> "ToGroup", where the links stop at the border of the group<br> <input type="radio" name="MyRoutingStyle" onclick="changeStyle()" value="ToNode" /> "ToNode", where the links bend at the border of the group but go all the way to the node, as normal<br> </p> <p> There is a variation of this sample where the tree on the right is mirrored, so that the links naturally connect closer to the nodes in the tree. </p> <p>The model data, automatically updated after each change or undo or redo:</p> <textarea id="mySavedModel" style="width:100%;height:300px"></textarea> </div> </div> <!-- * * * * * * * * * * * * * --> <!-- End of GoJS sample code --> </div> </body> <!-- This script is part of the gojs.net website, and is not needed to run the sample --> <script src="../assets/js/goSamples.js"></script> </html>