How to re-calculate Link Points after changing link template?

Hi, we are changing the diagram Link template from:

    this.diagram.linkTemplate =
      $(Link,
        {
          ...
          routing: Link.Orthogonal,
          curve: Link.JumpOver,
          corner: 10
        },
        new Binding('points').makeTwoWay(),

to

    this.diagram.linkTemplate =
      $(Link,
        {
          ...
          routing: Link.Normal,
          curve: Link.Bezier,
          adjusting: Link.Stretch
        },
        new Binding('points').makeTwoWay(),

then we are seeking a way to migrate old linkDataArray to fit new link template. However, when we load old data on new link template, the result is not what we want, as you can see screenshots below:

Before changing LinkTemplate:

After, as you can see the labels changed their position too and looked very strange

What we want to achieve is: (below is just removed new Binding(‘points’).makeTwoWay() )

I guess this is because the points value in old LinkDataArray was fit for Orthognal, but not for normal. What’s the best way to solve this situation? Btw, we are using Angular.

Do you need to keep the new go.Binding('points')?

Yes, I think so. This is how we save the links position data. or is there any other better solution?

If you like the routing that you got without the saved points, why bother saving them?

Or, more precisely: if the user is not manually modifying the routes of any links, you don’t need to save the routes by using the binding on “points”.

In our case, users need manually adjust links position as we have some users have drawn very complicated diagram already. the screenshots above are just a simple example. As we decide to change the routing, users will adjust links position later anyway cuz Bezier doesn’t have Jumpover or Jumpgap. And labels is another reason user will manually adjust link.

The third screenshot above is ideally what we want — when we load old linkdataArray on new routing, points should be re-calculated to fit new routing. then by a bit manual adjusting, save the data. Journey ends. And if we can identify the old link data is drawn by Orthognal then we conditionly do some workaround to convert it. That’ll be even better.

At the time that you load the model, you should know which template(s) that you are using, so you can decide whether to keep the data.points Array or not.

By the way, the reason you get the appearances for the link paths that you do in your second screenshot, is because the links think that there is some not-“None” value for the fromSpot and the toSpot. I cannot tell whether you have set or bound those properties in your link template(s) or in your node template(s).

  1. My question is: when we load the model by our data:

    then
this.diagram.model = new GraphLinksModel(nodeDataArray, linkDataArray);

How do we know this data is generated by which LinkTemplate?

  1. We don’t set or bind fromSpot and the toSpot in our link template and node template. Are default value of those properties None? I tried to add following set to link or node template but the result is still shown as screenshot 2.
          fromSpot: Spot.None,
          toSpot: Spot.None,
  1. I don’t understand what you are saying. Templates do not generate data. Templates are copied to make the Nodes and Links in the Diagram, and they are bound to the data provided in the model.

I was just saying that at the time that you set Diagram.model, you should know what templates you are using, and thus whether or not you want to make use of the data.points Array.

  1. OK. The other common way in which links get their fromSpot and/or toSpot set is by a layout. For example TreeLayout and LayeredDigraphLayout set them on the Links that they route, unless you tell them not to. But I have no idea of what layout you are using – I had been assuming you were not using one. And I didn’t see any node template nor do I see any location data property on the node data objects in the model.
  1. Sorry about my description that made you confused :( What I was trying to say is, once we find a way to migrate old linkArrary data to perfactly fit new link template, then we wish:
  • existing users load their old linkArrary data -> apply migration method -> display on new link template properly

  • new users load new linkArray data -> no need to apply migration method as they starts diagram by new link template

I wonder if we can trigger migration method or not by analysing points array in linkArrary data. That’s my assumption, not sure it’s correct. And Thx for your patience : )

  1. Here is our node template set and more detail for linkdata and node data, yes we don’t use any layout.
    const nodeTemplate = [
      Node, 'Spot',
      {
        locationSpot: Spot.Center,
        isShadowed: true,
        shadowOffset: new Point(0, 3),
        shadowColor: 'rgba(0,0,0,0.2)',
        layerName: LAYER_FOREGROUND, 
        selectionAdorned: false,

        selectionChanged: (node: Node) => {
          this.summaryData = SummaryUtils.showSummaryForNode(node.data);
          this.portUtils.clearAllPorts([node]);
        }
      },

      new Binding('location', 'loc', Point.parse).makeTwoWay(Point.stringify),
...

I still don’t understand what you really want to do with any saved routes. That’s OK, we can worry about that later. For now I’ll try to help you get the routing in the third screenshot, which is what you want by default, yes?

Maybe the problem is that you have set Link.adjusting. Try not setting that, as well as making sure you don’t set or bind fromSpot or toSpot or Link.points anywhere. In fact, don’t set routing properties on the Link at all, Just set curve: go.Link.Bezier.

And don’t use any ports either – just use the default port for the whole Node. I don’t know what data.portConfigs are, but are you sure you want to use them?

I followed your guide and removed Link.adjusting. And make sure there is no place to set or bind fromSpot or toSpot or Link.points

Things looked one step closer to the success.

As you can see gif below, after the loading, links display porperly for a sec then back to abnormal. Then I click each node, related links revert back to normal. Looks like links are re-computed after clicking node. The initial form and to points position are inherited by Orthogonal to cause abnormal I guess?

I can’t remove ports here as they will be portentially used for linking by ports later

Do you set or use anything with “initial” in the name?
What version are you using?

this is what we did for initial:

  /**
   * Initializes the main GoJS diagram
   */
  private initDiagram(): void {

    this.linksPanels = new GoJSLinks();

    this.diagram = DiagramUtils.getDiagram(this.linksPanels);

    const forelayer: Layer = this.diagram.findLayer(LAYER_FOREGROUND);
    this.diagram.addLayerAfter($(Layer, { name: LAYER_POPUP_LIST }), forelayer);
  }

we use gojs 2.0.9 in Angular

How are the ports defined?

Here are related code about how we define ports:

-- Main Component TS:
    /**
     * Uses portConfigs to customise each node's port configuration
     */
    const portPositioners: IPortPositioner[] = PortUtils.getPortPositioners();
    if (portPositioners) {
      portPositioners.forEach(cnf => {
        // order of panel is important, as it determines the z-index
        nodeTemplate.push(
          this.getPortCollection(cnf, port)
        );
      });
    }


  /**
   * Returns the shape that is used for port dots, using the
   * IPortPositioner for positioning coords.
   */
  private getPortCollection(cnf: IPortPositioner, port: GoJSPort): Panel {
    const x: number = this.utils.getPortPos(true, cnf, 0);
    const y: number = this.utils.getPortPos(true, cnf, 1);
    const offx: number = this.utils.getPortPos(true, cnf, 2);
    const offy: number = this.utils.getPortPos(true, cnf, 3);

    return port.getPort(cnf.name, new Spot(x, y, offx, offy));
  }

-- gojs-port.ts

  getPort(portUIStateName: string, spot: Spot): Panel {
    return $(Panel, Panel.Auto,
      {
        name: PORT_PANEL_NAME_PREFIX + portUIStateName,
        alignment: spot,
        mouseEnter: (e, panel: Panel) => {
          this.showLargeLinkButton(panel, portUIStateName, true);
        }
      },

      // only shows port if it is configured to have ports
      new Binding('visible', 'portConfigs', (portConfigs: IPortConfig[], panel: Panel) => {
          return portConfigs ? portConfigs.some(cnf => cnf.name === portUIStateName) : false;
        }
      ),

      this.getPortPanel(portUIStateName, spot)
    );
  }

private getPortPanel(portUIStateName: string, spot: Spot): Panel {
    return $(Panel, Panel.Auto,
      {
        name: PORT_PANEL_INNER_NAME_PREFIX + portUIStateName
      },

      // hides the port when largeLinkButtonVisible is visible
      new Binding('visible', 'uiStates', (uiStates: IUIStates, shape: Shape) => {
          const nodeState = uiStates.nodes[shape.part.data.key];

          const portState = nodeState.ports[portUIStateName];
          return nodeState.portsShowAll || portState.isPortActive;
        }
      ).ofModel(),

      // small port dot
      $(Shape, 'Circle',
        {
          name: portUIStateName,
          fill: COLORS.NODE_SELECT,
          stroke: COLORS.WHITE,
          strokeWidth: 1,
          desiredSize: new Size(PORT_WIDTH, PORT_WIDTH),
          portId: portUIStateName, // declare this object to be a 'port'
          fromLinkable: true,
          toLinkable: true, // declare whether the user may draw links to/from here
          cursor: 'not-allowed'
        },
        new Binding('fromLinkable', 'portConfigs', (portConfigs: IPortConfig[]) => {
            // if specified direction, uses that value, otherwise defaults to true
            const cnf: IPortConfig = portConfigs.find(o => o.name === portUIStateName);
            return cnf ? cnf.direction === EPortRule.FROM || cnf.direction === EPortRule.BIDIRECTIONAL : true;
          }
        ),
        new Binding('toLinkable', 'portConfigs', (portConfigs: IPortConfig[]) => {
            // if specified direction, uses that value, otherwise defaults to true
            const cnf: IPortConfig = portConfigs.find(o => o.name === portUIStateName);
            return cnf
              ? cnf.direction === EPortRule.TO ||
                  cnf.direction === EPortRule.BIDIRECTIONAL
              : true;
          }
        ),

        // changes the colour of the port when popup is visible
        new Binding('fill', 'uiStates', (uiStates: IUIStates, shape: Shape) => {
            const nodeState: INodeUIState = uiStates.nodes[shape.part.data.key];
            const portState: IPortUIState = nodeState.ports[portUIStateName];

            return portState.isPortActive ? COLORS.DARK_GREY : COLORS.NODE_SELECT;
          }
        ).ofModel()
      )
    );
  }

--port-utls.ts

  /**
   * Returns an array of possible port configs for a node
   */
  static getPortPositioners(): IPortPositioner[] {
    return [
      {
        name: PORT_POS.TOP_MIDDLE,
        position: [0.5, 0, 0, PORT_OFFSET_DIRECTION.MINUS] as (string | number)[]
      }, {
        name: PORT_POS.TOP_LEFT,
        position: [0, 0, PORT_OFFSET_DIRECTION.MINUS, PORT_OFFSET_DIRECTION.MINUS] as (string | number)[]
      }, {
        name: PORT_POS.TOP_RIGHT,
        position: [1, 0, PORT_OFFSET_DIRECTION.PLUS, PORT_OFFSET_DIRECTION.MINUS] as (string | number)[]
      }, {
        name: PORT_POS.LEFT_MIDDLE,
        position: [0, 0.5, PORT_OFFSET_DIRECTION.MINUS, 0] as (string | number)[]
      }, {
        name: PORT_POS.RIGHT_MIDDLE,
        position: [1, 0.5, PORT_OFFSET_DIRECTION.PLUS, 0] as (string | number)[]
      }, {
        name: PORT_POS.BOTTOM_MIDDLE,
        position: [0.5, 1, 0, PORT_OFFSET_DIRECTION.PLUS] as (string | number)[]
      }, {
        name: PORT_POS.BOTTOM_LEFT,
        position: [0, 1, PORT_OFFSET_DIRECTION.MINUS, PORT_OFFSET_DIRECTION.PLUS] as (string | number)[]
      }, {
        name: PORT_POS.BOTTOM_RIGHT,
        position: [1, 1, PORT_OFFSET_DIRECTION.PLUS, PORT_OFFSET_DIRECTION.PLUS] as (string | number)[]
      }
    ];
  }