How to combined different layout and automatic alignment layout?

Hello, I’m Alan.

first, thank you for nice diagram library.
i want to hear guys some idea.

first, see under layout image and then, is it possible layout? or give me something about layout idea?

i try to many method but each method got problem.
so, could you tell me right way or idea?

i tried to graph object with group layout(grid layout) but, grid group layout is default alignment is top left only. so, i found forum and i read one topic says grid layout default concept is alignment top left. so, i finding another way.

after then, i tried to make custom layout with api reference. small thing is possible but it’s very complex. it’s my limitation.

could you tell me, custom diagram idea like uploaded diagram image?
it’s able or not able?

Thanks
Alan

If some nodes and links were added, how should the layout position those new nodes? Or if some nodes are deleted?

Could you please show the model data that you want the custom layout to work on to produce your screenshot? Is the grouping information provided in the model? How does one know whether the tree should grow downward or upwards?

Hello, walter.

thank you your reply.

  1. my diagram main concept is center position red node is grow up horizontal direction left to right.

for example, red node#1 position is left up, and red node#2 position is left down. and red node #3 is second column up. and red node #4 is second column down position.

initial view image :

node added view image :

and each red node has property up and down position data in model. each red node should have children node. children node layout is like tree layout. tree layout up, down postion is used tree layout angle or direction. also, some children should group or not group.

i don’t care about add or remove node, after finish draw diagram.

  1. i added modal data and sample code like under.
    it’s just sample test code. not perfect.

screenshot1 (as-is) :

screenshot2 (to-be) :

code :

<script src="./release/go.js"></script>
<script id="code">
    function init(){
        var $ = go.GraphObject.make;  // for conciseness in defining templates

        // Diagram object create.
        myDiagram = $(go.Diagram, "myDiagramDiv",
            {
                initialContentAlignment: go.Spot.Center,
                contentAlignment : go.Spot.Center,
                initialDocumentSpot: go.Spot.TopCenter,
                initialViewportSpot: go.Spot.TopCenter,
                initialAutoScale: go.Diagram.Uniform
            });

        //Node Template Setting
        myDiagram.nodeTemplate =
            $(go.Node, "Auto",
                $(go.Shape,
                    "Rectangle",
                    { width: 50, height: 50, strokeWidth: 1 },
                    new go.Binding("fill", "color"),
                    new go.Binding("stroke", "strokeColor", function(s) { return s ? s : "gray"; }),
                ),
                $(go.TextBlock, { margin: 5 },
                    new go.Binding("text", "text")
                )
            );

        //Group Template Map Added.
        myDiagram.groupTemplateMap.add("WRAPPER",
            $(go.Group, "Auto",
                {
                    layout: $(go.GridLayout,
                        {
                            wrappingColumn: 2,
                            spacing: new go.Size(150, 50)
                        }
                    )
                },
                $(go.Shape, { fill: "white", stroke: "lightgray" }),
                $(go.Placeholder, { padding: 10 })
            ));

        myDiagram.groupTemplateMap.add("ROOTS",
            $(go.Group, "Auto",
                { layout: $(go.GridLayout, { wrappingColumn: 2, spacing: new go.Size(150, 50) }) },
                $(go.Shape, { fill: "white", stroke: "lightgray", stretch: go.GraphObject.Horizontal }),
                $(go.Placeholder, { padding: 10 })
            ));

        myDiagram.groupTemplateMap.add("GROUP1",
            $(go.Group, "Auto",
                { layout: $(go.TreeLayout, { nodeSpacing: 110, layerSpacing: 100, angle: 270 }) },
                $(go.Shape, { fill: "white", stroke: "lightgray" }),
                $(go.Placeholder, { padding: 10 })
            ));

        myDiagram.groupTemplateMap.add("GROUP2",
            $(go.Group, "Auto",
                { layout: $(go.TreeLayout, { nodeSpacing: 110, layerSpacing: 100, angle: 270 }) },
                $(go.Shape, { fill: "white", stroke: "lightgray" }),
                $(go.Placeholder, { padding: 10 })
            ));

        myDiagram.groupTemplateMap.add("GROUP3",
            $(go.Group, "Auto",
                { layout: $(go.TreeLayout, { nodeSpacing: 110, layerSpacing: 100, angle: 90 }) },
                $(go.Shape, { fill: "white", stroke: "lightgray" }),
                $(go.Placeholder, { padding: 10 })
            ));

        myDiagram.groupTemplateMap.add("GROUP4",
            $(go.Group, "Auto",
                { layout: $(go.TreeLayout, { nodeSpacing: 110, layerSpacing: 100, angle: 90 }) },
                $(go.Shape, { fill: "white", stroke: "lightgray" }),
                $(go.Placeholder, { padding: 10 })
            ));


        //Link Template Settings
        myDiagram.linkTemplate =
            $(go.Link,
                $(go.Shape, { strokeWidth: 15, stroke: "orange" }),
                $(go.Shape, { toArrow: "" }),
                $(go.TextBlock,
                    new go.Binding("text", "interfaceFrom"),
                    { segmentIndex: 0, segmentOffset: new go.Point(NaN, 0),
                        segmentOrientation: go.Link.OrientUpright, stroke: "white", font: "8pt serif" }),
                $(go.TextBlock,
                    new go.Binding("text", "interfaceTo"),
                    { segmentIndex: -1, segmentOffset: new go.Point(NaN, 0),
                        segmentOrientation: go.Link.OrientUpright, stroke: "white", font: "8pt serif" })
            );

        // node date model
        myDiagram.model.nodeDataArray = [
            // root group
            // { key: "WRAPPER", isGroup: true},
            { key: "UPSIDE", isGroup: true, group: "WRAPPER" },
            { key: "ROOTS", isGroup: true, group: "WRAPPER" },
            { key: "DOWNSIDE", isGroup: true, group: "WRAPPER" },

            { key: "GROUP1", isGroup: true, category: "GROUP1", group: "UPSIDE" },
            { key: "GROUP2", isGroup: true, category: "GROUP2", group: "UPSIDE" },
            { key: "GROUP3", isGroup: true, category: "GROUP3", group: "DOWNSIDE" },
            { key: "GROUP4", isGroup: true, category: "GROUP4", group: "DOWNSIDE" },

            { key: "1", text: "F", color: "lightblue", strokeColor:"cyan", group: "ROOTS" },

            { key: "2", text: "S", color: "lightblue", strokeColor:"cyan", group: "GROUP1" },
            { key: "3", text: "G", color: "lightblue", strokeColor:"red", group: "GROUP1"  },
            { key: "4", text: "H", color: "lightblue", strokeColor:"yellow", group: "GROUP1"  },
            { key: "5", text: "H", color: "lightblue", strokeColor:"cyan", group: "GROUP1"  },
            { key: "6", text: "H", color: "lightblue", strokeColor:"green", group: "GROUP1"  },

            { key: "7", text: "F", color: "lightblue", strokeColor:"cyan", group: "ROOTS" },

            { key: "8", text: "S", color: "lightblue", strokeColor:"cyan", group: "GROUP2" },
            { key: "9", text: "G", color: "lightblue", strokeColor:"red", group: "GROUP2"  },
            { key: "10", text: "H", color: "lightblue", strokeColor:"yellow", group: "GROUP2"  },
            { key: "11", text: "H", color: "lightblue", strokeColor:"cyan", group: "GROUP2"  },
            { key: "12", text: "H", color: "lightblue", strokeColor:"green", group: "GROUP2"  },

            { key: "13", text: "F", color: "lightblue", strokeColor:"cyan", group: "ROOTS" },

            { key: "14", text: "S", color: "lightblue", strokeColor:"cyan", group: "GROUP3" },
            { key: "15", text: "G", color: "lightblue", strokeColor:"red", group: "GROUP3"  },
            { key: "16", text: "H", color: "lightblue", strokeColor:"yellow", group: "GROUP3"  },
            { key: "17", text: "H", color: "lightblue", strokeColor:"cyan", group: "GROUP3"  },
            { key: "18", text: "H", color: "lightblue", strokeColor:"green", group: "GROUP3"  },

            { key: "19", text: "F", color: "lightblue", strokeColor:"cyan", group: "ROOTS" },

            { key: "20", text: "S", color: "lightblue", strokeColor:"cyan", group: "GROUP4" },
            { key: "21", text: "G", color: "lightblue", strokeColor:"red", group: "GROUP4"  },
            { key: "22", text: "H", color: "lightblue", strokeColor:"yellow", group: "GROUP4"  },
            { key: "23", text: "H", color: "lightblue", strokeColor:"cyan", group: "GROUP4"  },
            { key: "24", text: "H", color: "lightblue", strokeColor:"green", group: "GROUP4"  },

        ];

        //link data model.
        myDiagram.model.linkDataArray = [
            { from: "1", to: "2", interfaceFrom: "10Gi", interfaceTo: "127.0.0.1"},
            { from: "2", to: "3", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "3", to: "4", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "3", to: "5", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "3", to: "6", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},

            { from: "7", to: "8", interfaceFrom: "10Gi", interfaceTo: "127.0.0.1"},
            { from: "8", to: "9", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "9", to: "10", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "9", to: "11", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "9", to: "12", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},

            { from: "13", to: "14", interfaceFrom: "10Gi", interfaceTo: "127.0.0.1"},
            { from: "14", to: "15", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "15", to: "16", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "15", to: "17", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "15", to: "18", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},

            { from: "19", to: "20", interfaceFrom: "10Gi", interfaceTo: "127.0.0.1"},
            { from: "20", to: "21", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "21", to: "22", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "21", to: "23", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},
            { from: "21", to: "24", interfaceFrom: "1Gi", interfaceTo: "127.0.0.1"},

        ];

        // node column limit calculation. gridlayout column is max value 2.
        // var columnLimit = Math.ceil((myDiagram.model.nodeDataArray.length)/2);
        var columnLimit = 1 //temp value.

        //browser log.
        console.log('COUNT = ' + columnLimit);

        // wrappingColumn value reset.
        myDiagram.layout = $(go.GridLayout,
            {
                wrappingColumn: 1,
            }
        );

    }

</script>

Could you check my code and image, after then tell me some idea or right way.
i need your help.

Thanks
Alan

Thanks for the additional information.

First, I notice that your “initial…” Diagram property settings almost all conflict with each other. Setting Diagram.initialAutoScale might be sufficient.

Second, I notice that you have so many Group templates, some of which appear to be the same.

Third, I think you do need a separate template for the “root” Group, and it needs a custom Layout that positions its nodes (which happen to be red) appropriately based on the widths of the connected groups. But it is not clear to me how you decide where you want to position each red node – why are “N” and “…” where they are in your screenshot?

Hmmm, it actually would be better to have the custom Layout be Diagram.layout, which would position all of the Groups that have trees in them and position all of the red “root” nodes. The “root” Group holding the red nodes would have Group.layout set to null, thereby letting that group’s container be responsible for doing the layout of the red nodes.

<!DOCTYPE html>
<html>
<head>
<title>Minimal GoJS Sample</title>
<!-- Copyright 1998-2019 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script id="code">
  function GroupedRootsLayout() {
    go.Layout.call(this);
    this.groupSpacing = new go.Size(20, 20);
    this.rootSpacing = new go.Size(50, 50);
  }
  go.Diagram.inherit(GroupedRootsLayout, go.Layout);

  GroupedRootsLayout.prototype.doLayout = function(coll) {
    var lay = this;
    // find all Parts to be laid out, filter out those that shouldn't be laid out
    coll = this.collectParts(coll);
    var uproots = new go.List();
    var maxuprootheights = 0;
    var downroots = new go.List();
    var maxdownrootheights = 0;
    var upgroups = new go.List();
    var maxupheights = 0;
    var totupwidth = 0;
    var downgroups = new go.List();
    var maxdownheights = 0;
    var totdownwidth = 0;
    coll.each(function(p) {
      if (p instanceof go.Group) {
        if (p.data.angle === 270) {
          upgroups.add(p);
          maxupheights = Math.max(maxupheights, p.actualBounds.height);
          totupwidth += p.actualBounds.width + (totupwidth > 0 ? lay.groupSpacing.width : 0);
        } else if (p.data.angle === 90) {
          downgroups.add(p);
          maxdownheights = Math.max(maxdownheights, p.actualBounds.height);
          totdownwidth += p.actualBounds.width + (totdownwidth > 0 ? lay.groupSpacing.width : 0);
        }
      } else if (p instanceof go.Node) {
        var child = p.findTreeChildrenNodes().first();
        if (child !== null) {
          var group = child.containingGroup;
          if (group !== null) {
            if (group.data.angle === 270) {
              uproots.add(p);
              maxuprootheights = Math.max(maxuprootheights, child.actualBounds.height);
            } else if (group.data.angle === 90) {
              downroots.add(p);
              maxdownrootheights = Math.max(maxdownrootheights, child.actualBounds.height);
            }
          }
        }
      }
    });
    this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
    var x = this.arrangementOrigin.x;
    var y = this.arrangementOrigin.y;
    // position all up groups
    upgroups.each(function(g) {
      g.moveTo(x, y + maxupheights - g.actualBounds.height);
      x += g.actualBounds.width + lay.groupSpacing.width;
    });
    // position all down groups
    x = this.arrangementOrigin.x;
    y += maxupheights + lay.groupSpacing.height +
         lay.rootSpacing.height/2 + maxuprootheights + lay.rootSpacing.height + maxdownrootheights + lay.rootSpacing.height/2 +
         lay.groupSpacing.height;
    downgroups.each(function(g) {
      g.moveTo(x, y);
      x += g.actualBounds.width + lay.groupSpacing.width;
    });
    // position all up root nodes
    x = this.arrangementOrigin.x;
    y = this.arrangementOrigin.y + maxupheights + lay.groupSpacing.height + lay.rootSpacing.height/2 + maxuprootheights/2;
    uproots.each(function(n) {
      var child = n.findTreeChildrenNodes().first();
      n.location = new go.Point(child ? child.location.x : x, y);
    });
    // position all down root nodes
    y += maxuprootheights/2 + lay.rootSpacing.height + maxdownrootheights/2;
    downroots.each(function(n) {
      var child = n.findTreeChildrenNodes().first();
      n.location = new go.Point(child ? child.location.x : x, y);
    });
  }

  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
          {
            initialAutoScale: go.Diagram.Uniform,
            layout: new GroupedRootsLayout()
          });

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        { width: 40, height: 40, locationSpot: go.Spot.Center },
        $(go.Shape, "Circle",
          { fill: "lightblue", portId: "" },
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          new go.Binding("text", "key"))
      );

    myDiagram.groupTemplate =
      $(go.Group, "Auto",
        new go.Binding("layout", "angle", function(a) {
          return $(go.TreeLayout, { angle: a });
        }),
        $(go.Shape, { fill: "lightgray" }),
        $(go.Placeholder, { padding: 10 })
      );

    myDiagram.groupTemplateMap.add("Root",
      $(go.Group, "Auto",
        { layout: null },
        $(go.Shape, { fill: "lightgray" }),
        $(go.Placeholder, { padding: 25 })
      ));

    myDiagram.model = new go.GraphLinksModel(
    [
      { key: "root", isGroup: true, category: "Root" },
      { key: 10, group: "root", color: "red" },
      { key: -10, isGroup: true, angle: 90 },
      { key: 11, group: -10 },
      { key: 12, group: -10 },
      { key: 13, group: -10 },
      { key: 14, group: -10 },
      { key: 20, group: "root", color: "red" },
      { key: -20, isGroup: true, angle: 90 },
      { key: 21, group: -20 },
      { key: 22, group: -20 },
      { key: 23, group: -20 },
      { key: 30, group: "root", color: "red" },
      { key: -30, isGroup: true, angle: 270 },
      { key: 31, group: -30 },
      { key: 32, group: -30 },
      { key: 33, group: -30 },
      { key: 34, group: -30 },
      { key: 35, group: -30 },
      { key: 40, group: "root", color: "red" },
      { key: -40, isGroup: true, angle: 90 },
      { key: 41, group: -40 },
      { key: 42, group: -40 },
      { key: 43, group: -40 },
      { key: 44, group: -40 }
    ],
    [
      { from: 10, to: 11 },
      { from: 11, to: 12 },
      { from: 11, to: 13 },
      { from: 11, to: 14 },
      { from: 20, to: 21 },
      { from: 21, to: 22 },
      { from: 21, to: 23 },
      { from: 30, to: 31 },
      { from: 31, to: 32 },
      { from: 31, to: 33 },
      { from: 31, to: 34 },
      { from: 31, to: 35 },
      { from: 40, to: 41 },
      { from: 41, to: 42 },
      { from: 41, to: 43 },
      { from: 41, to: 44 },
    ]);
  }
</script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
</body>
</html>

produces:

1 Like

Hello, walter.

Thank you reply with code.

that code is just sample code. but, your opinion is good for me.
i will try make new diagram with your opinion.

root group node is maybe added more right position.
so, i marked “N”.
if some new node added, new node position is right top or down position.
but, i think this is complete in your code.

i just question one,
almost perfect your code. but, if some root group node have not children node or not linked, can not be show in root group node in your code. it is possible to show in root group node?

code :

<script src="./release/go.js"></script>
<script id="code">
    function GroupedRootsLayout() {
        go.Layout.call(this);
        this.groupSpacing = new go.Size(20, 20);
        this.rootSpacing = new go.Size(50, 50);
    }
    go.Diagram.inherit(GroupedRootsLayout, go.Layout);

    GroupedRootsLayout.prototype.doLayout = function(coll) {
        var lay = this;
        // find all Parts to be laid out, filter out those that shouldn't be laid out
        coll = this.collectParts(coll);
        var uproots = new go.List();
        var maxuprootheights = 0;
        var downroots = new go.List();
        var maxdownrootheights = 0;
        var upgroups = new go.List();
        var maxupheights = 0;
        var totupwidth = 0;
        var downgroups = new go.List();
        var maxdownheights = 0;
        var totdownwidth = 0;
        coll.each(function(p) {
            if (p instanceof go.Group) {
                if (p.data.angle === 270) {
                    upgroups.add(p);
                    maxupheights = Math.max(maxupheights, p.actualBounds.height);
                    totupwidth += p.actualBounds.width + (totupwidth > 0 ? lay.groupSpacing.width : 0);
                } else if (p.data.angle === 90) {
                    downgroups.add(p);
                    maxdownheights = Math.max(maxdownheights, p.actualBounds.height);
                    totdownwidth += p.actualBounds.width + (totdownwidth > 0 ? lay.groupSpacing.width : 0);
                }
            } else if (p instanceof go.Node) {
                var child = p.findTreeChildrenNodes().first();
                if (child !== null) {
                    var group = child.containingGroup;
                    if (group !== null) {
                        if (group.data.angle === 270) {
                            uproots.add(p);
                            maxuprootheights = Math.max(maxuprootheights, child.actualBounds.height);
                        } else if (group.data.angle === 90) {
                            downroots.add(p);
                            maxdownrootheights = Math.max(maxdownrootheights, child.actualBounds.height);
                        }
                    }
                }
            }
        });
        this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
        var x = this.arrangementOrigin.x;
        var y = this.arrangementOrigin.y;
        // position all up groups
        upgroups.each(function(g) {
            g.moveTo(x, y + maxupheights - g.actualBounds.height);
            x += g.actualBounds.width + lay.groupSpacing.width;
        });
        // position all down groups
        x = this.arrangementOrigin.x;
        y += maxupheights + lay.groupSpacing.height +
            lay.rootSpacing.height/2 + maxuprootheights + lay.rootSpacing.height + maxdownrootheights + lay.rootSpacing.height/2 +
            lay.groupSpacing.height;
        downgroups.each(function(g) {
            g.moveTo(x, y);
            x += g.actualBounds.width + lay.groupSpacing.width;
        });
        // position all up root nodes
        x = this.arrangementOrigin.x;
        y = this.arrangementOrigin.y + maxupheights + lay.groupSpacing.height + lay.rootSpacing.height/2 + maxuprootheights/2;
        uproots.each(function(n) {
            var child = n.findTreeChildrenNodes().first();
            n.location = new go.Point(child ? child.location.x : x, y);
        });
        // position all down root nodes
        y += maxuprootheights/2 + lay.rootSpacing.height + maxdownrootheights/2;
        downroots.each(function(n) {
            var child = n.findTreeChildrenNodes().first();
            n.location = new go.Point(child ? child.location.x : x, y);
        });
    }

    function init() {
        var $ = go.GraphObject.make;

        myDiagram =
            $(go.Diagram, "myDiagramDiv",
                {
                    initialAutoScale: go.Diagram.Uniform,
                    layout: new GroupedRootsLayout()
                });

        myDiagram.nodeTemplate =
            $(go.Node, "Auto",
                { width: 40, height: 40, locationSpot: go.Spot.Center },
                $(go.Shape, "Circle",
                    { fill: "lightblue", portId: "" },
                    new go.Binding("fill", "color")),
                $(go.TextBlock,
                    new go.Binding("text", "key"))
            );

        myDiagram.groupTemplate =
            $(go.Group, "Auto",
                new go.Binding("layout", "angle", function(a) {
                    return $(go.TreeLayout, { angle: a });
                }),
                $(go.Shape, { fill: "lightgray" }),
                $(go.Placeholder, { padding: 10 })
            );

        myDiagram.groupTemplateMap.add("Root",
            $(go.Group, "Auto",
                { layout: null },
                $(go.Shape, { fill: "lightgray" }),
                $(go.Placeholder, { padding: 25 })
            ));

        myDiagram.model = new go.GraphLinksModel(
            [
                { key: "root", isGroup: true, category: "Root" },
                { key: 10, group: "root", color: "red" },
                { key: -10, isGroup: true, angle: 90 },
                { key: 11, group: -10 },
                { key: 12, group: -10 },
                { key: 13, group: -10 },
                { key: 14, group: -10 },
                { key: 20, group: "root", color: "red" },
                { key: -20, isGroup: true, angle: 90 },
                { key: 21, group: -20 },
                { key: 22, group: -20 },
                { key: 23, group: -20 },
                { key: 30, group: "root", color: "red" },
                { key: -30, isGroup: true, angle: 270 },
                { key: 31, group: -30 },
                { key: 32, group: -30 },
                { key: 33, group: -30 },
                { key: 34, group: -30 },
                { key: 35, group: -30 },
                { key: 40, group: "root", color: "red" },
                { key: -40, isGroup: true, angle: 90 },
                { key: 41, group: -40 },
                { key: 42, group: -40 },
                { key: 43, group: -40 },
                { key: 44, group: -40 },
                { key: 50, group: "root", color: "red" },
                { key: -50, isGroup: true, angle: 270 },
                { key: 51, group: -50 },
                { key: 52, group: -50 },
                { key: 53, group: -50 },
                { key: 60, group: "root", color: "red" },
                { key: -60, isGroup: true, angle: 270 },
                { key: 61, group: -60 },
                { key: 62, group: -60 },
                { key: 63, group: -60 },
                { key: 64, group: -60 },
                { key: 65, group: -60 },
                { key: 66, group: -60 },
                { key: 67, group: -60 },
                { key: 68, group: -60 },
                { key: 70, group: "root", color: "red" },
                { key: -70, isGroup: true, angle: 270 },

            ],
            [
                { from: 10, to: 11 },
                { from: 11, to: 12 },
                { from: 11, to: 13 },
                { from: 11, to: 14 },
                { from: 20, to: 21 },
                { from: 21, to: 22 },
                { from: 21, to: 23 },
                { from: 30, to: 31 },
                { from: 31, to: 32 },
                { from: 31, to: 33 },
                { from: 31, to: 34 },
                { from: 31, to: 35 },
                { from: 40, to: 41 },
                { from: 41, to: 42 },
                { from: 41, to: 43 },
                { from: 41, to: 44 },
                { from: 50, to: 51 },
                { from: 51, to: 52 },
                { from: 51, to: 53 },
                { from: 60, to: 61 },
                { from: 61, to: 62 },
                { from: 61, to: 63 },
                { from: 61, to: 64 },
                { from: 61, to: 65 },
                { from: 61, to: 66 },
                { from: 61, to: 67 },
                { from: 67, to: 68 }
            ]);
    }
</script>

Thanks
Alan

Yes there are a lot of cases that the code above might not do what you want. You need to modify the code to handle those cases.