Data Mapping diagram

Hi,

I want to implement a data mapping diagram as shown below. Want to know whether its possible to implement with GoJS, if so any sample code is appreciated
image

https://gojs.net/extras/treeMapperSidesPorts.html

Of course you can have whatever nodes you want between the groups.
Here are some examples: GoJS Data Binding -- Northwoods Software

Thanks for your response. I need to use combination of Tree Mapper and Record Mapper.

Left Hand side show the Trees and Records on right hand side. Lines should be drawn from Tree node to record attributes.

Any suggestions on how to implement this?

I would start from a copy of the Tree Mapper sample. Rename the default node template and the default link template to be “TreeNode” or “TN” and “TreeLink” or “TL”, respectively. The data for the trees will each need category: "TN" or category: "TL".

For the right side of the diagram, did you really want to use the single Node with a bunch of fields, or did you want separate Nodes for each field? The former is what the Record Mapper sample uses; the latter is more like what the Tree Mapper sample uses – i.e. a Group with a bunch of member Nodes.

Here’s a meld of the code for TreeMapper, a group with a single column GridLayout, and multiport input-output nodes, along with a Palette and an Overview.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Mapper</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div style="display: flex; flex-direction: column; margin: 0 2px 0 0">
      <div id="myPaletteDiv" style="flex-grow: 1; width: 150px; background-color: floralwhite; border: solid 1px black"></div>
      <div id="myOverviewDiv" style="margin: 2px 0 0 0; width: 150px; height: 100px; background-color: whitesmoke; border: solid 1px black"></div>
    </div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
  </div>
  <div>
    <button id="myLoadButton">Load</button>
    <button id="mySaveButton">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:400px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script>
// 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
    let n = this;
    while (n !== null && !n.isVisible()) {
      n = n.findTreeParentNode();
    }
    return n;
  }
}  // end TreeNode


// 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) {
    const group = node.containingGroup;
    const special = (group !== null && group.category === "Tree");
    if (!special) return super.getLinkPoint(node, port, spot, from, ortho, othernode, otherport);
    const r = port.getDocumentBounds();
    const b = special ? group.actualBounds : node.actualBounds;
    const op = othernode.getDocumentPoint(go.Spot.Center);
    const x = (op.x > r.centerX) ? b.right : b.left;
    return new go.Point(x, r.centerY);
  }
}  // end MappingLink


const $ = go.GraphObject.make;  // for conciseness in defining templates

const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      "commandHandler.copiesTree": true,
      "commandHandler.deletesTree": true,
      "TreeCollapsed": handleTreeCollapseExpand,
      "TreeExpanded": handleTreeCollapseExpand,
      "linkingTool.linkValidation": checkLink,
      "relinkingTool.linkValidation": checkLink,
      "undoManager.isEnabled": true
    });

function handleTreeCollapseExpand(e) {
  e.subject.each(n => {
    n.findExternalTreeLinksConnected().each(l => l.invalidateRoute());
  });
}

// No link connects two nodes in the same group
function checkLink(fn, fp, tn, tp, link) {
  return fn.containingGroup === null || tn.containingGroup === null;
}


// Each node in a tree is defined using the default nodeTemplate.
myDiagram.nodeTemplateMap.add("TreeNode",
  $(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.ImageStretch.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 a tree group.
myDiagram.linkTemplateMap.add("TreeLink",  // 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" })
  ));

myDiagram.groupTemplateMap.add("Tree",
  $(go.Group, "Auto",
    {
      deletable: false,
      layout: $(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);
            updateNodeWidths(this.group, this.group.data.width || 100);
          }
        })
    },
    new go.Binding("position", "xy", go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.Shape, { fill: "whitesmoke", stroke: "gray" }),
    $(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 })
    )
  ));


// Template for nodes in the list group
myDiagram.nodeTemplateMap.add("ListNode",
  $(go.Node,
    { 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),
    $(go.TextBlock,
      new go.Binding("text"))
  ));

myDiagram.groupTemplateMap.add("List",
  $(go.Group, "Auto",
    {
      deletable: false,
      layout: $(go.GridLayout,  // taken from samples/treeView.html
        {
          wrappingColumn: 1,
          cellSize: new go.Size(1, 1),
          spacing: new go.Size(0, 0)
        })
    },
    new go.Binding("position", "xy", go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.Shape, { fill: "whitesmoke", stroke: "gray" }),
    $(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 })
    )
  ));


// These are the regular nodes for the middle of the diagram.

function portStyle() {
  return [
    new go.Binding("portId", "portId"),
    $(go.Shape,
      {
        fill: "gray", stroke: null, strokeWidth: 0,
        width: 10, height: 10
      }),
    $(go.TextBlock,
      { fromLinkable: false, toLinkable: false, editable: true },
      new go.Binding("text", "", d => d.text || d.portId)
        .makeTwoWay((v, d) => d.text = v))
  ];
}

// the node template
// includes a panel on each side with an itemArray of panels containing ports
myDiagram.nodeTemplate =
  $(go.Node, "Table",
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),

    // the body
    $(go.Shape, "Rectangle",
      {
        row: 0, rowSpan: 2, column: 0, columnSpan: 3,
        stretch: go.GraphObject.Fill,
        fill: "lightgray",
        minSize: new go.Size(40, 40),
        //margin: new go.Margin(0, 5)
      }),

    $(go.TextBlock,
      {
        row: 0, column: 0, columnSpan: 2, margin: new go.Margin(2, 8),
        textAlign: "center", font: "bold 14px sans-serif",
        stroke: "#484848", editable: true
      },
      new go.Binding("text", "name").makeTwoWay()),

    // the Panel holding the left port elements, which are themselves Panels,
    // created for each item in the itemArray, bound to data.ins
    $(go.Panel, "Vertical",
      new go.Binding("itemArray", "ins"),
      {
        row: 1, column: 0,
        margin: new go.Margin(2, 2),
        alignment: go.Spot.Left,
        defaultAlignment: go.Spot.Left,
        itemTemplate:
          $(go.Panel, "Horizontal",
            {
              fromSpot: go.Spot.Left, toSpot: go.Spot.Left,
              toLinkable: true
            },
            portStyle()
          )  // end itemTemplate
      }
    ),  // end Vertical Panel

    // the Panel holding the right port elements, which are themselves Panels,
    // created for each item in the itemArray, bound to data.outs
    $(go.Panel, "Vertical",
      new go.Binding("itemArray", "outs"),
      {
        row: 1, column: 1,
        margin: new go.Margin(2, 0),
        alignment: go.Spot.Right,
        defaultAlignment: go.Spot.Right,
        itemTemplate:
          $(go.Panel, "Horizontal",
            {
              fromSpot: go.Spot.Right, toSpot: go.Spot.Right,
              fromLinkable: true, cursor: "pointer",
              isOpposite: true
            },
            portStyle()
          )  // end itemTemplate
      }
    )  // end Vertical Panel
  );  // end Node

// These are the links connecting nodes that are not in the same group.
myDiagram.linkTemplate =
  $(MappingLink,
    { isTreeLink: false, isLayoutPositioned: false, layerName: "Foreground" },
    { fromSpot: go.Spot.Right, toSpot: go.Spot.Left },
    { relinkableFrom: true, relinkableTo: true },
    $(go.Shape, { strokeWidth: 1.5 })
  );


// Create an initial model
const nodeDataArray = [
  { isGroup: true, category: "Tree", key: -1, text: "Tree", xy: "0 0", width: 150 },
  { isGroup: true, category: "List", key: -2, text: "List", xy: "500 0", width: 150 },
  {"key":2001, "name":"Unit One", "loc":"280 20",
   "ins":[ {"portId":"in0"} ],
   "outs":[ {"portId":"out0"},{"portId":"out1"} ] },
];
const linkDataArray = [
  { from: 6, to: 2001, toPort: "in0" },
  { from: 2001, to: 1000, fromPort: "out1" },
  { from: 4, to: 1006 },
  { from: 9, to: 1004 },
  { from: 1, to: 1009 },
  { from: 14, to: 1010 }
];

// initialize a random tree on left side
const root1 = { category: "TreeNode", key: 0, group: -1 };
nodeDataArray.push(root1);
for (let i = 0; i < 11;) {
  i = makeTree(3, i, 17, nodeDataArray, linkDataArray, root1, -1, root1.key);
}

// initialize list on right side
for (let i = 0; i < 12; i++) {
  nodeDataArray.push({ category: "ListNode", key: 1000 + i, group: -2, text: "item " + i });
}

myDiagram.model = new go.GraphLinksModel({
  "copiesArrays": true,
  "copiesArrayObjects": true,
  "linkFromPortIdProperty": "fromPort",
  "linkToPortIdProperty": "toPort",
  nodeDataArray,
  linkDataArray
});

// help create a random tree structure
function makeTree(level, count, max, nodeDataArray, linkDataArray, parentdata, groupkey, rootkey) {
  const numchildren = Math.floor(Math.random() * 10);
  for (let i = 0; i < numchildren; i++) {
    if (count >= max) return count;
    count++;
    const childdata = { category: "TreeNode", key: rootkey + count, group: groupkey };
    nodeDataArray.push(childdata);
    linkDataArray.push({ category: "TreeLink", 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;
}

function updateNodeWidths(group, width) {
  if (isNaN(width)) {
    group.memberParts.each(n => {
      if (n instanceof go.Node) n.width = NaN;  // back to natural width
    });
  } else {
    let 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;
    let right = minx + width;
    group.memberParts.each(n => {
      if (n instanceof go.Node) n.width = Math.max(0, right - n.actualBounds.x);
    });
  }
}

// initialize Palette
myPalette =
  new go.Palette("myPaletteDiv",
    {
      nodeTemplateMap: myDiagram.nodeTemplateMap,
      model: new go.GraphLinksModel([
        {"key":1, "name":"Unit One", "loc":"101 204",
        "ins":[ {"portId":"in0"}, {"portId":"in1"} ],
        "outs":[ {"portId":"out0"},{"portId":"out1"} ] },
        {"key":2, "name":"Unit Two", "loc":"320 152",
        "ins":[ {"portId":"in0"},{"portId":"in1"},{"portId":"in2"} ],
        "outs":[ {"portId":"out"} ] },
        {"key":3, "name":"Unit Three", "loc":"384 319",
        "ins":[ {"portId":"in0"},{"portId":"in1"},{"portId":"in2"} ],
        "outs":[ {"portId":"out0"},{"portId":"out1"} ] },
        {"key":4, "name":"Unit Four", "loc":"138 351",
        "ins":[ {"portId":"in0"} ],
        "outs":[ {"portId":"out0"},{"portId":"out1"},{"portId":"out2"} ] }
      ])
    });

// initialize Overview
myOverview =
  new go.Overview("myOverviewDiv",
    {
      observed: myDiagram,
      contentAlignment: go.Spot.Center
    });


// save a model to and load a model from Json text, displayed below the Diagram
function save() {
  const str = myDiagram.model.toJson();
  document.getElementById("mySavedModel").value = str;
}
document.getElementById("mySaveButton").addEventListener("click", save);

function load() {
  const str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
}
document.getElementById("myLoadButton").addEventListener("click", load);
  </script>
</body>
</html>