javascriptgojs

Creating a family tree - How to add line for child?


I need to create a family tree and I'm using GoJS for this.

I have a father and mother, but also 2 sisters. One of my sister is the child of my father but isn't the child of my mother.

I need to create connections from the children to the parents and connections from the parent to the child.

I used basis code from template organization chart from site https://gojs.net/latest/samples/orgChartEditor.html

My current output is the following:

enter image description here

But actually, I need to change the output to this: enter image description here

How can I modify the code to achieve this result?

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

  myDiagram =
    new go.Diagram("myDiagramDiv", // must be the ID or reference to div
      {
        allowCopy: true,
        allowDelete: true,
        //initialAutoScale: go.Diagram.Uniform,
        maxSelectionCount: 1, // users can select only one part at a time
        validCycle: go.Diagram.CycleDestinationTree, // make sure users can only create trees
        "clickCreatingTool.archetypeNodeData": { // allow double-click in background to create a new node
          name: "(new person)",
          title: "",
          comments: ""
        },
        "clickCreatingTool.insertPart": function(loc) { // method override must be function, not =>
          const node = go.ClickCreatingTool.prototype.insertPart.call(this, loc);
          if (node !== null) {
            this.diagram.select(node);
            this.diagram.commandHandler.scrollToPart(node);
            this.diagram.commandHandler.editTextBlock(node.findObject("NAMETB"));
          }
          return node;
        },
        layout: $(go.TreeLayout, {
          treeStyle: go.TreeLayout.StyleLastParents,
          arrangement: go.TreeLayout.ArrangementHorizontal,
          // properties for most of the tree:
          angle: 90,
          layerSpacing: 35,
          // properties for the "last parents":
          alternateAngle: 90,
          alternateLayerSpacing: 35,
          alternateAlignment: go.TreeLayout.AlignmentBus,
          alternateNodeSpacing: 20
        }),
        "undoManager.isEnabled": true // enable undo & redo
      });

  // when the document is modified, add a "*" to the title and enable the "Save" button
  myDiagram.addDiagramListener("Modified", e => {
    const button = document.getElementById("SaveButton");
    if (button) button.disabled = !myDiagram.isModified;
    const idx = document.title.indexOf("*");
    if (myDiagram.isModified) {
      if (idx < 0) document.title += "*";
    } else {
      if (idx >= 0) document.title = document.title.slice(0, idx);
    }
  });


  //const levelColors = ["#AC193D", "#2672EC", "#8C0095", "#5133AB","#008299", "#D24726", "#008A00", "#094AB2"];
  const levelColors = ["#FFFFFF"];

  // override TreeLayout.commitNodes to also modify the background brush based on the tree depth level
  myDiagram.layout.commitNodes = function() { // method override must be function, not =>
    go.TreeLayout.prototype.commitNodes.call(this); // do the standard behavior
    // then go through all of the vertexes and set their corresponding node's Shape.fill
    // to a brush dependent on the TreeVertex.level value
    myDiagram.layout.network.vertexes.each(v => {
      if (v.node) {
        const level = v.level % (levelColors.length);
        const color = levelColors[level];
        const shape = v.node.findObject("SHAPE");
        if (shape) shape.stroke = $(go.Brush, "Linear", {
          0: color,
          1: go.Brush.lightenBy(color, 0.05),
          start: go.Spot.Left,
          end: go.Spot.Right
        });
      }
    });
  };
  //console.log(myDiagram.layout);
  // this is used to determine feedback during drags
  function mayWorkFor(node1, node2) {
    if (!(node1 instanceof go.Node)) return false; // must be a Node
    if (node1 === node2) return false; // cannot work for yourself
    if (node2.isInTreeOf(node1)) return false; // cannot work for someone who works for you
    return true;
  }

  // This function provides a common style for most of the TextBlocks.
  // Some of these values may be overridden in a particular TextBlock.
  function textStyle() {
    return {
      font: "8pt  Segoe UI,sans-serif",
      stroke: "white"
    };
  }

  // This converter is used by the Picture.
  function findHeadShot(pic) {
    if (!pic) return "images/HSnopic.png"; // There are only 16 images on the server
    return "images/HS" + pic;
  }

  // define the Node template
  myDiagram.nodeTemplate =
    $(go.Node, "Spot", {
        selectionObjectName: "BODY",
        mouseEnter: (e, node) => node.findObject("BUTTON").opacity = node.findObject("BUTTONX").opacity = node.findObject("BUTTONC").opacity = 1,
        mouseLeave: (e, node) => node.findObject("BUTTON").opacity = node.findObject("BUTTONX").opacity = node.findObject("BUTTONC").opacity = 0,
        // handle dragging a Node onto a Node to (maybe) change the reporting relationship
        mouseDragEnter: (e, node, prev) => {
          const diagram = node.diagram;
          const selnode = diagram.selection.first();
          if (!mayWorkFor(selnode, node)) return;
          const shape = node.findObject("SHAPE");
          if (shape) {
            shape._prevFill = shape.fill; // remember the original brush
            shape.fill = "darkred";
          }
        },
        mouseDragLeave: (e, node, next) => {
          const shape = node.findObject("SHAPE");
          if (shape && shape._prevFill) {
            shape.fill = shape._prevFill; // restore the original brush
          }
        },
        mouseDrop: (e, node) => {
          const diagram = node.diagram;
          const selnode = diagram.selection.first(); // assume just one Node in selection
          if (mayWorkFor(selnode, node)) {
            // find any existing link into the selected node
            const link = selnode.findTreeParentLink();
            if (link !== null) { // reconnect any existing link
              link.fromNode = node;
            } else { // else create a new link
              diagram.toolManager.linkingTool.insertLink(node, node.port, selnode, selnode.port);
            }
          }
        }
      },
      // for sorting, have the Node.text be the data.name
      new go.Binding("text", "name"),
      // bind the Part.layerName to control the Node's layer depending on whether it isSelected
      new go.Binding("layerName", "isSelected", sel => sel ? "Foreground" : "").ofObject(),
      $(go.Panel, "Auto", {
          name: "BODY"
        },
        // define the node's outer shape
        $(go.Shape, "Rectangle", {
          name: "SHAPE",
          fill: "#39F",
          stroke: 'white',
          strokeWidth: 1.5,
          portId: ""
        }),
        $(go.Panel, "Horizontal",
          $(go.Picture, {
              name: "Picture",
              desiredSize: new go.Size(70, 70),
              margin: 1.5,
              source: "images/HSnopic.png" // the default image
            },
            new go.Binding("source", "pic", findHeadShot)),
          // define the panel where the text will appear
          $(go.Panel, "Table", {
              minSize: new go.Size(180, NaN),
              maxSize: new go.Size(200, NaN),
              margin: new go.Margin(6, 10, 0, 6),
              defaultAlignment: go.Spot.Left
            },
            $(go.RowColumnDefinition, {
              column: 2,
              width: 4
            }),
            $(go.TextBlock, textStyle(), // the name
              {
                name: "NAMETB",
                row: 1,
                column: 0,
                columnSpan: 4,
                font: "10pt Segoe UI,sans-serif",
                editable: true,
                isMultiline: false,
                minSize: new go.Size(60, 16)
              },
              new go.Binding("text", "name").makeTwoWay()),
            $(go.TextBlock, "", textStyle(), {
              row: 2,
              column: 0
            }),
            $(go.TextBlock, textStyle(), {
                row: 2,
                column: 0,
                columnSpan: 4,
                editable: true,
                isMultiline: false,
                minSize: new go.Size(50, 14),
                margin: new go.Margin(0, 0, 0, 0)
              },
              new go.Binding("text", "title").makeTwoWay()),
            $(go.TextBlock, textStyle(), {
                row: 4,
                column: 0
              },
              new go.Binding("text", "key", v => "")),
            $(go.TextBlock, textStyle(), // the comments
              {
                row: 4,
                column: 0,
                columnSpan: 5,
                font: "8pt",
                wrap: go.TextBlock.WrapFit,
                editable: true, // by default newlines are allowed
                minSize: new go.Size(100, 14)
              },
              new go.Binding("text", "comments").makeTwoWay())
          ) // end Table Panel
        ) // end Horizontal Panel
      ), // end Auto Panel

      $("Button",
        $(go.Shape, "PlusLine", {
          width: 12,
          height: 12
        }), {
          name: "BUTTON",
          alignment: go.Spot.Right,
          opacity: 1, // initially not visible
          click: (e, button) => addEmployee(button.part)
        },
        // button is visible either when node is selected or on mouse-over
        new go.Binding("opacity", "isSelected", s => s ? 1 : 0).ofObject()
      ),

      // new Button for create children
      $("Button",
        $(go.Shape, "LineUp", {
          width: 12,
          height: 12
        }), {
          name: "BUTTONC",
          alignment: go.Spot.Left,
          opacity: 1, // initially not visible
          click: (e, button) => addEmployeeChild(button.part)
        },
        new go.Binding("opacity", "isSelected", s => s ? 1 : 0).ofObject()
        // button is visible either when node is selected or on mouse-over
      ),


      new go.Binding("isTreeExpanded").makeTwoWay(),
      $("TreeExpanderButton", {
          name: "BUTTONX",
          alignment: go.Spot.Bottom,
          opacity: 0, // initially not visible
          "_treeExpandedFigure": "TriangleUp",
          "_treeCollapsedFigure": "TriangleDown"
        },
        // button is visible either when node is selected or on mouse-over
        new go.Binding("opacity", "isSelected", s => s ? 1 : 0).ofObject()
      )
    ); // end Node, a Spot Panel

  function addEmployee(node) {
    if (!node) return;
    const thisemp = node.data;
    myDiagram.startTransaction("add employee");
    const newemp = {
      name: "(new person)",
      title: "(title)",
      comments: "",
      parent: thisemp.key
    };
    myDiagram.model.addNodeData(newemp);
    const newnode = myDiagram.findNodeForData(newemp);
    if (newnode) newnode.location = node.location;
    myDiagram.commitTransaction("add employee");
    myDiagram.commandHandler.scrollToPart(newnode);
  }

  // New Function to children link
  function addEmployeeChild(node) {
    if (!node) return;
    const thisemp = node.data;
    myDiagram.startTransaction("add employee");
    const newemp = {
      name: "(new person)",
      title: "(title)",
      comments: "",
      child: thisemp.key
    };
    myDiagram.model.addNodeData(newemp);
    const newnode = myDiagram.findNodeForData(newemp);
    if (newnode) newnode.location = node.location;
    myDiagram.commitTransaction("add employee");
    myDiagram.commandHandler.scrollToPart(newnode);
  }


  // the context menu allows users to make a position vacant,
  // remove a role and reassign the subtree, or remove a department
  myDiagram.nodeTemplate.contextMenu =
    $("ContextMenu",
      $("ContextMenuButton",
        $(go.TextBlock, "Add Employee"), {
          click: (e, button) => addEmployee(button.part.adornedPart)
        }
      ),
      $("ContextMenuButton",
        $(go.TextBlock, "Add Children"), {
          click: (e, button) => addEmployeeChild(button.part.adornedPart)
        }
      ),
      $("ContextMenuButton",
        $(go.TextBlock, "Vacate Position"), {
          click: (e, button) => {
            const node = button.part.adornedPart;
            if (node !== null) {
              const thisemp = node.data;
              myDiagram.startTransaction("vacate");
              // update the key, name, picture, and comments, but leave the title
              myDiagram.model.setDataProperty(thisemp, "name", "(Vacant)");
              myDiagram.model.setDataProperty(thisemp, "pic", "");
              myDiagram.model.setDataProperty(thisemp, "comments", "");
              myDiagram.commitTransaction("vacate");
            }
          }
        }
      ),
      $("ContextMenuButton",
        $(go.TextBlock, "Remove Role"), {
          click: (e, button) => {
            // reparent the subtree to this node's boss, then remove the node
            const node = button.part.adornedPart;
            if (node !== null) {
              myDiagram.startTransaction("reparent remove");
              const chl = node.findTreeChildrenNodes();
              // iterate through the children and set their parent key to our selected node's parent key
              while (chl.next()) {
                const emp = chl.value;
                myDiagram.model.setParentKeyForNodeData(emp.data, node.findTreeParentNode().data.key);
                //myDiagram.model.setChildrenKeyForNodeData(emp.data, node.findTreeChildrenNode().data.key);
              }
              // and now remove the selected node itself
              myDiagram.model.removeNodeData(node.data);
              myDiagram.commitTransaction("reparent remove");
            }
          }
        }
      ),
      $("ContextMenuButton",
        $(go.TextBlock, "Remove Department"), {
          click: (e, button) => {
            // remove the whole subtree, including the node itself
            const node = button.part.adornedPart;
            if (node !== null) {
              myDiagram.startTransaction("remove dept");
              myDiagram.removeParts(node.findTreeParts());
              myDiagram.commitTransaction("remove dept");
            }
          }
        }
      )
    );

  // define the Link template
  myDiagram.linkTemplate =
    $(go.Link, go.Link.Orthogonal, {
        layerName: "Background",
        corner: 5
      },
      $(go.Shape, {
        strokeWidth: 1.2,
        stroke: "#FFFFFF"
      })); // the link shape

  // read in the JSON-format data from the "mySavedModel" element
  load();


  // support editing the properties of the selected person in HTML
  if (window.Inspector) myInspector = new Inspector("myInspector", myDiagram, {
    properties: {
      "key": {
        readOnly: true
      },
      "comments": {}
    }
  });

  // Setup zoom to fit button
  document.getElementById('zoomToFit').addEventListener('click', () => myDiagram.commandHandler.zoomToFit());

  document.getElementById('centerRoot').addEventListener('click', () => {
    myDiagram.scale = 1;
    myDiagram.commandHandler.scrollToPart(myDiagram.findNodeForKey(1));
  });
} // end init


// Show the diagram's model in JSON format
function save() {
  document.getElementById("mySavedModel").value = myDiagram.model.toJson();
  myDiagram.isModified = false;
}

function load() {
  myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
  go.Model.linkDataArray = [{
    to: "2",
    from: "20",
    color: "#FFF"
  }];
  // make sure new data keys are unique positive integers
  let lastkey = 1;
  myDiagram.model.makeUniqueKeyFunction = (model, data) => {
    let k = data.key || lastkey;
    while (model.findNodeDataForKey(k)) k++;
    data.key = lastkey = k;
    return k;
  };


}

window.addEventListener('DOMContentLoaded', init);
<script src="https://cdnjs.cloudflare.com/ajax/libs/gojs/2.3.16/go.js" ></script>

<div id="myDiagramDiv"><canvas></canvas></div>
<div id="myInspector" class="myInspector">

</div>

<textarea class="d-none" id="mySavedModel" style="width:100%; height:270px;" class="form-control">
  { 
  "class": "go.TreeModel",
  "nodeDataArray": [
    {"key":1, "name":"I am", "title":"", "pic":"5.jpg"},
    {"key":2, "name":"Father", "title":"", "pic":"8.jpg", "parent":1},
    {"key":3, "name":"Mather", "title":"", "pic":"9.jpg", "parent":1},
    {"key":4, "name":"Sister", "title":"", "pic":"10.jpg","child":2},
 ]
}
</textarea>


Solution

  • You are asking to make your graph something more than tree-structured, but you cannot do that with a TreeModel. You have to use a GraphLinksModel and use the GraphLinksModel.linkDataArray to define the relationships (Links) between people (Nodes).

    You won't be able to use TreeLayout for the same reason. Use LayeredDigraphLayout instead.

    You might be interested in the Genogram samples. https://gojs.net/latest/samples/genogram.html

    But you could continue using your node template. Just make sure each node data object in the model has the property values that the node template needs.