Subtype links (Many to One or One to Many)

Hi,

I am trying to make a subtype link by using gojs.
image
Highlighted part denotes a subtype relationship, in gojs links go from parent to child or child to parent.
But here in my case, we’ve some intermediate labels. Link can or can not go from subtype symbol.
How to achieve the above requirement with gojs?

Note- I don’t want to use any kind of layout.

First, you could use the predefined “CircleLine” arrowhead:

  $(go.Link,
    { routing: go.Link.Orthogonal },
    $(go.Shape, { strokeWidth: 1.5 }),
    $(go.Shape,
      {
        toArrow: "CircleLine", fill: "white", scale: 1.5,
        segmentIndex: 1, segmentFraction: 1, segmentOffset: new go.Point(-2, 0)
      }),
  )

which produces:
image

But if you really want that line to be so broad relative to the circular piece, you’ll probably want to define your own arrowhead. Maybe something like:

go.Shape.defineArrowheadGeometry("CircleLine2", "F1 M12 0 V16 M12 8 b 0 360 -4 0 4 z");

which produces:
image

Fiddle with the geometry until it looks like how you want it.

I already have shapes at the start and end of the link like circles, diamonds, triangles, etc. PFB snapshot.


Therefore if I make use of ‘toArrow’ field, I won’t be able to add the ‘CircleLine’ shape.

Why not? There can be multiple arrowheads. Have you tried it?

Note how that particular arrowhead (with the circle-line combination) has its own settings for segment… properties.

Yes, we can have multiple arrowheads.
image
But see the highlighted links. It should be only one link, till ‘CircleLine’ icon. Then it should be divided into two to reach other square boxes.

What is the fromSpot of either the node/port or of each link? That’s what normally determines whether links connect at a particular point (i.e. specific Spot) or along a side (i.e. a “…Side” Spot) or at the closest intersection with the edge of the port.

fromSpot is BottomSide(E/1). toSpots are LeftSide(E/2), and RightSide(E/3)

Then the links are being routed correctly – the link points are supposed to be spread out evenly along the side.

If you didn’t mean to have them spread out, just use go.Spot.Bottom or whatever.

Ok, The solution has partially worked. After using go.Spot.Bottom, go.Spot.Left etc, links are not spreading out. But now the problem is, links are always starting from the top of the box. PFA image’s highlighted part.
image

The size of the node includes every element in it. That means it includes the TextBlocks such as “E/5”, which means the height of the node (Node.actualBounds.height) is about twice as tall as the rectangle. If you specify a fromSpot or toSpot that is go.Spot.Left or go.Spot.Right, that would be in the middle of the side of the node, which happens to be at the corner of the rectangle Shape.

You probably want to set portId to be the empty string on that Shape. This is discussed at: GoJS Ports in Nodes-- Northwoods Software

Also I notice in your screenshot that the link(s) connecting with “E/1” are connecting with an end segment that is at 45 degrees. That is because you specified the …Spot to be go.Spot.TopLeft. If that is what you want, that’s good. If it isn’t you could use a different Spot value, such as new go.Spot(0, 0.0001) to have links connect coming out or going into the left side of the port.

I removed go.Spot.TopLeft from all of the places. There are no occurrences go.Spot.TopLeft in the file. But still I see attached diagram.
image

That’s odd. What are your link template and node template?

Link Template

let linkTemplate = $(
      go.Link, // the whole link panel
      {
        // routing: go.Link.AvoidsNodes,
        routing: go.Link.Orthogonal,
        reshapable: true,
        resegmentable: true,
        adjusting: go.Link.Stretch,
        layerName: 'Background',
        selectionChanged: link => {
          handleLinkClick(link);
        },
        doubleClick: (e, node) => {
          handleDoubleClick(node?.part?.data);
        },
      },
      new go.Binding('visible', 'displayOptions', displayOptions => {
        return displayOptions.includes(
          constants.HIDE_SHOW_RELATIONSHIP_DISPLAY,
        );
      }).ofModel(),
      // new go.Binding("visible", "viewMode", (viewMode, tb) => {
      //   if (viewMode === constants.VIEW_MODE_PHYSICAL) {
      //     return tb.data.HideInPhysical === "true" ? false : true;
      //   } else {
      //     return tb.data.HideInLogical === "true" ? false : true;
      //   }
      // }).ofModel(),
      // @ts-ignore
      new go.Binding('fromSpot', value => {
        if (value.Type === 'Subtype') {
          return value.fromSpot.replace('Side', '')
        } else {
          return go.Spot.BottomCenter;
        }
      }, go.Spot.parse),
      // @ts-ignore
      new go.Binding('toSpot', value => {
        if (value.Type === 'Subtype') {
          return value.toSpot.replace('Side', '')
        } else {
          return go.Spot.TopCenter;
        }
      }, go.Spot.parse),
      $(
        go.Shape, // the link shape
        // @ts-ignore
        new go.Binding('stroke', value => {
          return value.fillOptions.linecolor === null ||
            value.fillOptions.linecolor === undefined ||
            value.fillOptions.linecolor === ''
            ? '#9c9b9c'
            : value.fillOptions.linecolor;
        }),
        // @ts-ignore
        new go.Binding('strokeWidth', val => {
          return val.fillOptions.linewidth;
        }),
        // @ts-ignore
        new go.Binding('strokeDashArray', value => {
          return value.isDashed === 'true' || value.Type === '7'
            ? [15, 3]
            : [0, 0];
        }),
      ),
      $(
        go.Shape, // the "to" end arrowhead
        new go.Binding('toArrow', 'toArrowShape'),
        // @ts-ignore
        new go.Binding('fill', value => {
          return value.toArrowColor === null ||
            value.toArrowColor === undefined ||
            value.toArrowColor === '' ||
            value.toArrowColor === '#000000'
            ? '#FFFFFF'
            : value.fillOptions?.linecolor;
        }),
      ),
      $(
        go.Shape, // the "to" end arrowhead
        new go.Binding('fromArrow', 'fromArrowShape'),
        // @ts-ignore
        new go.Binding('fill', value => {
          return value.fromArrowColor === null ||
            value.fromArrowColor === undefined ||
            value.fromArrowColor === '' ||
            value.fromArrowColor === '#000000'
            ? '#FFFFFF'
            : value.fillOptions?.linecolor;
        }),
      ),
      $(
        go.TextBlock, // the "from" label
        { segmentOffset: new go.Point(0, -10) },
        {
          contextMenu: $(
            'ContextMenu',
            $(
              // @ts-ignore
              'ContextMenuButton',
              {
                'ButtonBorder.fill': 'white',
                _buttonFillOver: 'skyblue',
              },
              $(go.TextBlock, 'Properties'),
              { click: (e, node) => handleGetProperties(node?.part?.data) },
            ),
          ),
        },
        // @ts-ignore
        new go.Binding('font', val => {
          return erDiagramUtils.getFontStyle(val.fontOptions);
        }),
        // @ts-ignore
        new go.Binding('stroke', val => {
          return val.fontOptions.color === null ||
            val.fontOptions.color === undefined
            ? 'black'
            : val.fontOptions.color;
        }),
        // new go.Binding("visible", "hiddenLabels").ofModel(),
        new go.Binding('text', 'viewMode', (val, tb) => {
          return val === constants.VIEW_MODE_PHYSICAL
            ? tb.part.data.PhysicalName
            : tb.part.data.Name;
        }).ofModel(),
        new go.Binding('visible', 'displayOptions', displayOptions => {
          return displayOptions.includes(constants.DISPLAY_RELATIONSHIP_NAME);
        }).ofModel(),
      ),
      $(
        go.TextBlock,
        'to',
        {
          segmentIndex: -1,
          segmentOffset: new go.Point(NaN, NaN),
          segmentOrientation: go.Link.OrientUpright,
        },
        // @ts-ignore
        new go.Binding('visible', 'displayOptions', displayOptions => {
          return displayOptions.includes(constants.DISPLAY_CARDINALITY);
        }).ofModel(),
        // @ts-ignore
        new go.Binding('text', val => {
          return val.Cardinality;
        }),
        // @ts-ignore
        new go.Binding('stroke', val => {
          return val.fontOptions.color === null ||
            val.fontOptions.color === undefined
            ? 'black'
            : val.fontOptions.color;
        }),
      ),
      {
        // define a tooltip for each node that displays the color as text
        toolTip: $(
          'ToolTip',
          $(
            go.TextBlock,
            { margin: 4 },
            { alignment: go.Spot.Top, stroke: 'blue' },
            // @ts-ignore
            new go.Binding('text', 'viewMode', (val, tb) => {
              let physicalTooltip =
                tb.part.data.PhysicalName +
                ' (' +
                tb.part.data.Parent_PhysicalName +
                ' to ' +
                tb.part.data.Child_PhysicalName +
                ')';
              let logicalTooltip =
                tb.part.data.Name +
                ' (' +
                tb.part.data.Parent_Name +
                ' to ' +
                tb.part.data.Child_Name +
                ')';
              return val === constants.VIEW_MODE_PHYSICAL
                ? physicalTooltip +
                    '\n' +
                    'Parent:' +
                    tb.part.data.Parent_PhysicalName +
                    '\n' +
                    'Child: ' +
                    tb.part.data.Child_PhysicalName +
                    '\n' +
                    'Auto Routing: Off'
                : logicalTooltip +
                    '\n' +
                    'Parent:' +
                    tb.part.data.Parent_Name +
                    '\n' +
                    'Child: ' +
                    tb.part.data.Child_Name +
                    '\n' +
                    'Auto Routing: Off';
            }).ofModel(),
          ),
        ), // end of Adornment
      },
    );

Node Template

let columnAttributeTemplate = $(
      go.Node,
      'Vertical',
      {
        selectionAdorned: true,
        resizable: true,
        layoutConditions: go.Part.LayoutStandard & ~go.Part.LayoutNodeSized,
        resizeObjectName: 'ITEMARRAYPANEL',
        doubleClick: (e, node) => {
          handleDoubleClick(node?.part?.data);
        },
      },
      /*new go.Binding('visible', 'viewMode', (viewMode, tb) => {
        if (viewMode === constants.VIEW_MODE_PHYSICAL) {
          return tb.data.HideInPhysical !== 'true';
        } else {
          return tb.data.HideInLogical !== 'true';
        }
      }).ofModel(),*/
      new go.Binding('location', 'Coordinates', go.Point.parse),
      $(
        go.TextBlock,
        {
          margin: 3,
          alignment: go.Spot.Left,
        },
        {
          contextMenu: $(
            'ContextMenu',
            $(
              // @ts-ignore
              'ContextMenuButton',
              {
                'ButtonBorder.fill': 'white',
                _buttonFillOver: 'skyblue',
              },
              $(go.TextBlock, 'Properties'),
              { click: (e, node) => handleGetProperties(node?.part?.data) },
            ),
          ),
        },
        // @ts-ignore
        new go.Binding('stroke', val => {
          return val.fontOptions.color;
        }),
        new go.Binding('text', '', (val, tb) => {
          return val.viewMode === 'physical'
            ? tb.part.data.PhysicalName
            : tb.part.data.Name;
        }).ofModel(),
      ),
      $(
        go.Panel,
        'Auto',
        $(
          go.Shape,
          {
            minSize: new go.Size(100, 20),
            strokeWidth: 2,
            portId: '',
            // spot1: go.Spot.TopLeft,
            // spot2: go.Spot.BottomRight,
            fromSpot: go.Spot.Right,  // port properties go on the port!
            toSpot: go.Spot.Left
          },
          // @ts-ignore
          new go.Binding('stroke', value => {
            return value.fillOptions.outlineColor !== undefined
              ? value.fillOptions.outlineColor
              : '#000000';
          }),
          // @ts-ignore
          new go.Binding('strokeDashArray', value => {
            return value.icon === 'View' ? [2, 4] : [0, 0];
          }),
          // @ts-ignore
          new go.Binding('fill', val => {
            return $(go.Brush, 'Linear', {
              0: val.fillOptions.startingColor,
              1: val.fillOptions.endingColor,
            });
          }),
          {
            // define a tooltip for each node that displays the color as text
            toolTip: $(
              'ToolTip',
              $(
                go.TextBlock,
                { margin: 4, stroke: 'blue' },
                // @ts-ignore
                new go.Binding('text', value => {
                  return (
                    value.Name +
                    '\nAuto Sizing: On' +
                    '\nDefinition: ' +
                    value.Definition
                  );
                }),
              ),
            ), // end of Adornment
          },
          //new go.Binding("width", "width"),
          new go.Binding('figure', 'figure'),
        ),
        $(
          go.Panel,
          'Table',
          new go.Binding('itemArray', 'Attribute'),
          // @ts-ignore
          {
            name: 'ITEMARRAYPANEL',
            // margin: 3,
            defaultAlignment: go.Spot.Left,
            // itemTemplate: itemTempl,  // map was defined above
            itemTemplateMap: acItemTemplateMap,
            // alignment: go.Spot.Left
          },
          // $(go.RowColumnDefinition,
          //   { separatorStroke: "#C71585", row: 1 })
        ),
      ),
    );

I think your bindings have problems.

new go.Binding('fromSpot', value => {
        if (value.Type === 'Subtype') {
          return value.fromSpot.replace('Side', '')
        } else {
          return go.Spot.BottomCenter;
        }
      }, go.Spot.parse)

The main problem is that the second argument to the Binding constructor must be the name of a property, or else the empty string to mean to use the whole object. Binding | GoJS API

If you use the go-debug.js library, you would catch these and other errors more quickly.

Thank you, Walter. the issue has been resolved.