Can we persist the location while using tree layout

We have use case to persist the location of the node when node is being dragged. For the we are using new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), .
But nodes are not appearing in given positions if we are using TreeLayout where as it is working with GridLayout . Is there any way to achieve this for TreeLayout as well ?

With GridLayout

With TreeLayout

Normally when an automatic layout is performed it will position all of the nodes that it lays out and it may route all of the links that it lays out.

Do you want dragged nodes to remain where the user left them even when a layout is performed to position undragged nodes? The problem is that such a policy will cause the resulting layout to look bad (not tree-like) and may result in nodes that overlap each other. If you want the layout to completely ignore any manually-dragged nodes, one possibility is to implement something like:

$(go.Diagram, . . .,
  {
    "SelectionMoved": e => e.subject.each(n => {
        if (n instanceof go.Node) n.isLayoutPositioned = false;
      }),
    . . .

If you want such nodes to be ignored by layouts after reloading a model, add a Binding to your node template(s):

    new go.Binding("isLayoutPositioned").makeTwoWay(),

Thanks for the reply walter. We are trying similar thing for links as well using below binding…
new go.Binding("points").makeTwoWay(),
But in model it is getting saved as instance of list and point classes instead of plain array.

{
    "__gohashid": 1347,
    "_isFrozen": true,
    "s": [
        {
            "x": -150.767822265625,
            "y": -197.48992919921875,
            "_isFrozen": true
        },
        {
            "x": -140.767822265625,
            "y": -197.48992919921875,
            "_isFrozen": true
        },
        {
            "x": -140,
            "y": -197.48992919921875,
            "_isFrozen": true
        },
        {
            "x": -140,
            "y": -197.48992919921875,
            "_isFrozen": true
        },
        {
            "x": -70.50250244140625,
            "y": -197.48992919921875,
            "_isFrozen": true
        },
        {
            "x": -70.50250244140625,
            "y": -402.5628662109375,
            "_isFrozen": true
        },
        {
            "x": 117.06150817871094,
            "y": -402.5628662109375,
            "_isFrozen": true
        },
        {
            "x": 127.06150817871094,
            "y": -402.5628662109375,
            "_isFrozen": true
        }
    ],
    "Ja": 0,
    "Sa": null,
    "Gg": null
}

How we have to write the binding to save just the array of values as

"points":[-120,-343.78240737915036,-120,-333.78240737915036,-120,-297.6087963104248,-70,-297.6087963104248,-70,-261.43518524169923,-70,-251.43518524169923]

It is Model.toJson that converts a List of Points to an Array of numbers, not a Binding.backConverter function. So the overhead of such a conversion is not incurred continuously when dragging a node causes all of its connected links to be rerouted continuously.

So I guess you are not calling Model.toJson, is that correct?

Hi @walter , I have two questions now…

  1. IS THIS APPROACH FINE
    I am not calling Model.toJson, I am creating the diagram.model using below snippet
diagram.model = $(GraphLinksModel,
		{
			nodeDataArray: model.nodes, // 'model' is json object used to preload my json or to save json after update as it works by reference
			linkDataArray: model.edges,
			...options,
		}
	);

So whenever there is an update in diagram.model my “model” json is also getting updated without any “.toJson()” so i wrote converter function in binding like this and it is working.

new go.Binding("points").makeTwoWay(e=>{let it = e.iterator;
let res = [];
  while (it.next()) {
      let ps = go.Point.stringify(it.value);
      var psa = ps.split(' ');
      res = res.concat(psa);
  }
return res})
  1. Why ModelChanged event is getting triggered in this scenario
    when I load the diagram for the first time, with points two way binding on edge and points array present in my model json along with isLayoutPositioned as false… Model_Changed event is getting triggered for points value change and event.oldValue and event.newValue are same. It ends up triggering a transaction complete model changed event with incrementaldata as below.
{
    "class": "GraphLinksModel",
    "incremental": 1,
    "nodeCategoryProperty": "templateId",
    "linkCategoryProperty": "templateId",
    "linkFromPortIdProperty": "fromPort",
    "linkToPortIdProperty": "toPort",
    "modelData": {},
    "modifiedLinkData": [
        {
            "from_port": "49de92bc535c20100b0cddeeff7b12f9",
            "points": [
                "-200.130859375",
                "268.26153564453125",
                "-200.130859375",
                "258.26153564453125",
                "-200.130859375",
                "257.412353515625",
                "-161.5633544921875",
                "257.412353515625",
                "-161.5633544921875",
                "256.56317138671875",
                "-161.5633544921875",
                "246.56317138671875"
            ],
            "isLayoutPositioned": false,
            "from": "0734148b-6638-48c8-8ed2-860c4216248f",
            "to": "8ca73f32-5f49-4400-a4a2-1166a14aea0f",
            "edge_id": "539bb9bb532310100b0cddeeff7b1207",
            "templateId": "c98b79bb532310100b0cddeeff7b1200",
            "fromPort": "49de92bc535c20100b0cddeeff7b12f9",
            "interactiveGraphObjects": [
                {
                    "name": "addNodeButton",
                    "ariaLabel": "Opens action picker to add a new node to the diagram",
                    "tabClickHandler": null
                },
                {
                    "name": null,
                    "ariaLabel": "Add a node",
                    "tabClickHandler": null
                }
            ],
            "recenterHandler": null
        }
    ]
}

I think there’s a bit of confusion regarding the terminology. Model.toJson converts the in-memory data held by a model into a text string that is JSON-formatted. So if there has been a transaction that has modified the diagram, it is likely that there was a modification of the model, so you might want to save the model to your database.

Question 1:
I guess your Binding’s back-conversion-function is generating an Array of strings. But the Binding’s target-conversion-function is just the default identity function, so the question is what the Link.points property setter will accept. Yes, it does normally accept an Array of numbers, where the numbers are alternating x and y values for the Points. But you are passing in an Array of strings of formatted numbers, which is different. I suppose it happens to work because JavaScript will often automatically convert a string to a number when a numerical operation is performed, but I am concerned that there might be a bug there. I recommend that you instead make sure you put numbers into the Array, not strings.

Question 2:
I’m curious what kind of ChangedEvent that is that you are getting in your Model Changed listener. Could you please show us all of the properties of that ChangedEvent?

Thanks @walter for the reply.
For Question 2 : It’s wrong implementation from our side and it stands fixed.

For question 1 - I used below logic for points binding

new go.Binding('points', 'points', data => {
			const pBinding = new go.List();

			for (let i = 0; i < data.length; i = i + 2) 
				pBinding.add(new go.Point(data[i], data[i + 1]));

			return pBinding;
		}).makeTwoWay(e => {
			const it = e.iterator;
			let res = [];

			while (it.next()) {
				res = res.concat(it.value.x);
				res = res.concat(it.value.y);
			}

			return res;
		})

Here I’ve one more question that… because of this twoway binding, points value is getting populated (while initial layout) against all links in model and ModelChanged is getting triggered because of that… We have to do some custom logic on that very particular ModelChanged event (Some part of event object mentioned below).

Can we rely on changedEvent.object.wa == 'Initial Layout' to identify the ModelChanged event that got triggered because of two way bindings that got applied on initial layout ?

Yes, checking for the “Initial Layout” transaction name should work to identify the ModelChanged event you’re looking for. However, to make sure your code remains compatible with future versions you never want to rely on minified properties like changedEvent.object.wa. In your case this is a synonym for changedEvent.object.name, which you would want to use instead.

Referencing this might give you an error if the ModelChanged event doesn’t come from a transaction, so when accessing this property you’ll want to first make sure that changedEvent.object is not null, and is a Transaction.

Thanks @ian . We used following condition changedEvent.object && changedEvent.object instanceof go.Transaction && changedEvent.object.name && changedEvent.object.name == 'Initial Layout'