Poor performance with numbers of node and links

I encountered an performance issue when I using GoJS to render my flowsheet.

For example, we have 50 nodes and each node has 10 inports and 10 outports. And there are more than 250 links among the ports of these nodes.
And I use the “AvoidNode” effect for the links.

Currently, if I update the content of the nodes and it will take a long time to refreshing the flowsheet, and it looks like the GoJS will re-draw all the canvas.

And I try to remove “AvoidNode” effect for the links, and it becomes more responsive and the performance is acceptable. But the links cross the node and looks messy.

Does anyone know how to improve this?

That is on the list of optimizations that we need to do.

Part of the problem is that it appears that array access is a lot slower in JavaScript than in other languages.

In this case, I have 50 nodes and 250 links with “AvoidNode” effect.

If I update one of the nodes, and the flowsheet will be re-draw after more than 10 seconds. But if I remove all the links and the it will only take 1-2 seconds to update the canvas.

So I guess GoJS will recalculate links once any update to the whole flowsheet.

I don’t know whether GoJS has the capability of only udpating the nodes? If the recalculation of links isn’t triggered, that will improve the performance.

Your reply is appreciated. Smile

Today we were looking into potential optimizations for this case. It may take some time.

Do you have a screenshot that shows what kind of connectivity you have? That might be helpful.

Thanks for your quick response.

I paseted the code and screenshot, hope that will be helpful. Note: the code is written in typescript which is smiliar to Javascript.

The node template like below:

<span =“Apple-tab-span” style=“white-space:pre”> var $go = go.GraphObject.make;
<span =“Apple-tab-span” style=“white-space:pre”> this.diagram.nodeTemplateMap.add(“MainNode”,
$go(go.Node, go.Panel.Auto,
{ locationSpot: go.Spot.Center },
new go.Binding(“location”, “loc”, function (loc) {
return new go.Point.parse(loc) ;
}),

            $go(go.Shape, "RoundedRectangle",
                {
                    name: "SHAPE",
                    parameter1: 20,
                    fill: whiteblue,
                    spot1: go.Spot.TopLeft,
                    spot2: go.Spot.BottomRight,  
                    strokeWidth: 2

                }),

            $go(go.Panel, go.Panel.Table,
               { margin: new go.Margin(30, 0) },

                $go(go.TextBlock,
                    { row: 0, column: 0, columnSpan: 3, font: "bold 60pt sans-serif", text: "text" },
                    new go.Binding("text", "name")
                    ),
                    $go(go.Panel, go.Panel.Auto, {
                        name: "limitpanel",
                        row: 1,
                        column: 0,
                        columnSpan: 3,
                        margin: 50,  //to have more space between svg and ports
                    }, {
                        click: (e, obj) => { this.onHoverOverRect(e, obj) }
                    }),
            $go(go.Panel, go.Panel.Horizontal,
                
        $go(go.Panel, go.Panel.Auto, {  
                        name:"drilldown", 
                        margin: 2,
                        click: clickHandler,
                        row: 0,
                        column: 0,
                        background: "lightgray",
                        desiredSize: new go.Size(40, 40) },
                  $go(go.Picture, { 
                     
                        alignment: go.Spot.TopLeft,
                        alignmentFocus: go.Spot.TopLeft,
                      source: this.applicationController.baseAppPath + "/Content/Images/Drilldown16.png"})
                      ),

         $go(go.Panel, go.Panel.Auto, { 
                        name:"viewUnitData",   
                        margin: 5,
                        click: clickHandler2,
                        row: 0,
                        column: 1,
                        background: "lightgray",
                        desiredSize: new go.Size(40, 40) },
                  $go(go.Picture, {                         
                        alignment: go.Spot.TopLeft,
                        alignmentFocus: go.Spot.TopLeft,
                      source: this.applicationController.baseAppPath + "/Content/Images/ViewUnitData.png"})
                      )),

                $go(go.Panel,
                    {
                        name: "portf",
                        row: 2,
                        column: 0,
                        alignment: go.Spot.Left,
                    }),

                $go(go.Panel,
                    {
                        name: "svgpanel",
                        row: 2,
                        column: 1,
                   //     alignment: go.Spot.Center,
                        //columnSpan:3,
                        margin: 50 //to have more space between svg and ports
                    }),

                $go(go.Panel,
                    {
                        name: "portp",
                        row: 2,
                        column: 2,
                        //columnSpan:100,
                        alignment: go.Spot.Right,
                    }),

                $go(go.TextBlock,
                    { row: 3, column: 0, columnSpan: 3, font: "48pt sans-serif" },
                    new go.Binding("text", "name")
                    )
                ),
                {
                    toolTip: $go(go.Adornment, go.Panel.Auto,
                                $go(go.Shape, { fill: "whitesmoke", width: 190, height: 40 }),
                                $go(go.TextBlock, { margin: 4, font: "12pt sans-serif" }, new go.Binding("text", "name"))
                                )

                },
                {
                    selectionAdornmentTemplate:
                        $go(go.Adornment, go.Panel.Spot,
                            $go(go.Panel, go.Panel.Auto,
                                // this Adornment has a rectangular blue Shape around the selected node
                                $go(go.Shape, "RoundedRectangle", {
                                    fill: null, parameter1: 20, spot1: go.Spot.TopLeft,
                                    spot2: go.Spot.BottomRight, stroke: "dodgerblue", strokeWidth: 12 }),
                                $go(go.Placeholder)
                                )
                            ),
                },
                {
                    mouseEnter: (e, obj, prev) =>
                    { // change  background brush
                        var shape = obj.part.findObject("SHAPE");
                        if (shape)
                            shape.fill = bluegrad;
                    },
                    mouseLeave: (e, obj, next) =>
                    { // restore to original brush
                        var shape = obj.part.findObject("SHAPE");
                        if (shape)
                            shape.fill = whiteblue;
                    }
                }
            )
        );

<span =“Apple-tab-span” style=“white-space:pre”>
And the ports of nodes are added dynamically, because the ports for each node are different. In the node template, I first define two section (portf and portp), and they will be filled when rendering each node.

<span =“Apple-tab-span” style=“white-space:pre”> addports(node)
{
<span =“Apple-tab-span” style=“white-space:pre”> // Find the node
<span =“Apple-tab-span” style=“white-space:pre”> var node = this.diagram.findNodeForKey(nodeId);
<span =“Apple-tab-span” style=“white-space:pre”>
var fPortPanel = $go(go.Panel, go.Panel.Vertical,
{ margin: 20, name: “fpanel” }
);

    var pPortPanel = $go(go.Panel, go.Panel.Vertical,
        { margin: 20, name: "ppanel" }
    );

    Dojo.Utils.forEach(node.ports, (port) =>
    {
        var portPanel = this.createPortsForPort(port);

        if (port.isFeed)
            fPortPanel.add(portPanel);
        else
            pPortPanel.add(portPanel);
    });

    node.findObject("portf").add(fPortPanel);
    node.findObject("portp").add(pPortPanel);
}

createPorts(port): any
{
    var portPanel =
        $go(go.Panel, go.Panel.Auto,
            {
                stretch: go.GraphObject.Horizontal,
                defaultStretch: go.GraphObject.Horizontal
                //defaultStretch is for panel elements (port)
            },
            $go(go.Shape,
                {
                    "figure": "Roundedrectangle", 
                    height: 50,
                   // width: 100,
                    portId: port.stream.id,
                    "fill": "transparent",
                    fromSpot: go.Spot.RightSide,                       
                    stroke: fillColor,

<span =“Apple-tab-span” style=“white-space:pre”> toSpot: go.Spot.LeftSide
}),
$go(go.TextBlock,
{
alignment: go.Spot.Left,
alignmentFocus: go.Spot.Left,
textAlign: “left”,
“text”: port.name,
font: “12pt sans-serif”,
margin: 5
})
);
return portPanel;
}
<span =“Apple-tab-span” style=“white-space:pre”>
The link template like below:

<span =“Apple-tab-span” style=“white-space:pre”> var $go = go.GraphObject.make;
<span =“Apple-tab-span” style=“white-space:pre”> this.diagram.linkTemplate =
$go(go.Link, // the whole link panel
{
routing: go.Link.AvoidsNodes
},
$go(go.Shape, // the link path shape
{
name:“LinkShape”,
isPanelMain: true,
stroke: “black”, strokeWidth: 1
}),
$go(go.Shape, // the arrowhead
{
toArrow: “standard”,
stroke: null, fill: “black”
}),
$go(go.Panel, go.Panel.Auto,
{
visible: false,
name: “LABEL”,
segmentIndex: 0
},
$go(go.Shape, // the link shape
{
figure: ‘RoundedRectangle’,
fill: “black”,
stroke: null
})
)
);

And sometime I need to update the nodes like below:

updatePort()
{
Dojo.Utils.forEach(nodes, (node) =>{
var node = this.diagram.findNodeForKey(node.id);

        Dojo.Utils.forEach(node.ports, (port) =>
        {
                if (port.hasProblem)
                {
                    var node = this.diagram.findNodeForKey(node.id);
                    if (port.isInport)
                    {
                        var fpanel = node.findObject("fpanel");
                        var panel = fpanel.findObject(port.name); 
                        panel.findObject('portlengthtext').text = port.problem.text;
                    }
                    else
                    {
                        var ppanel = node.findObject("ppanel");
                        var panel = ppanel.findObject(port.name); 
                        panel.findObject('portlengthtext').text = port.problem.text;
                    }
                }
            })
    });
}<span ="Apple-tab-span" style="white-space:pre">			</span>

Overview:

Single Node:

OK, thanks for the screenshot.

My first impression was that you will have a lot of overlapping line segments. Then it occurred to me that even if the line segments were separated to reduce overlaps, it still would be very hard for someone to figure out what connects with what.

Setting Link.corner might help people tell which way links turn.

But it would be most helpful to provide more interactivity on links and especially on ports. Take a look at the FriendWheel sample, as the mouse passes over links or selects nodes.

Oh, it also would help a lot to bind/set the from/toEndSegmentLength on each port object to different values for all ports on one side of a node. This makes it easy to distinguish links connecting with different ports.

And using differently colored links might help too, although I do not know what would make the most sense for your app.