You can build a diagram of nodes and links programmatically. But GoJS offers a way to build diagrams in a more declarative manner. You only provide the node and link data (i.e. the model) necessary for the diagram and instances of parts (i.e. the templates) that are automatically copied into the diagram. Those templates may be parameterized by properties of the node and link data.
Let us try to build two nodes and connect them with a link. Here is one way of doing that:
const node1 =
new go.Node("Auto")
.add(
new go.Shape("RoundedRectangle", { fill: "lightblue" }),
new go.TextBlock("Alpha", { margin: 5 })
);
diagram.add(node1);
const node2 =
new go.Node("Auto")
.add(
new go.Shape("RoundedRectangle", { fill: "pink" }),
new go.TextBlock("Beta", { margin: 5 })
);
diagram.add(node2);
diagram.add(
new go.Link({ fromNode: node1, toNode: node2 })
.add(
new go.Shape()
));
This produces a nice, simple diagram. If you drag one of the nodes, you will see that the link remains connected to it.
Although this way of building a diagram will work, it will not scale up well when creating large diagrams. Normally you will want a varying number of nodes each of which is very similar to the others. It would be better to share the construction of the node but parameterize a few things where the values should vary.
One possibility would be put the code to build a Node into a function that returned a fully constructed Node, including all of the Panels and other GraphObjects in its visual tree. You would probably want to parameterize the function in order to provide the desired strings and colors and figures and image URLs. However such an approach is very ad-hoc: it would be difficult for the system to know how to automatically call such functions in order to create new nodes or new links on demand. Furthermore as your application data changes dynamically, how would you use such functions to update properties of existing objects within existing nodes and links, without inefficiently re-creating everything? And if you wanted anything/everything to update automatically as your application data changes, how would the system know what to do?
This diagram-building code is also more cumbersome than it needs to be to manage references to nodes so that you can link them up. This is similar to the earlier problem when building a node's visual tree in code of having to use temporary named variables and referring to them when needed.
What we are looking for is the separation of the appearance, definition, and construction of all of the nodes from the application data needed to describe the unique aspects of each particular node.
One way of achieving the separation of node appearance from node data is to use a data model and node templates. A model is basically just a collection of data that holds the essential information for each node and each link. A template is basically just a Part that can be copied; you would have different templates for Nodes and for Links.
In fact, a Diagram already has very simple default templates for Nodes and Links. If you want to customize the appearance of the nodes in your diagram, you can replace the default node template by setting Diagram.nodeTemplate.
To automatically make use of templates, provide the diagram a model holding the data for each node and the data for each link. A GraphLinksModel holds the collections (actually arrays) of node data and link data as the values of GraphLinksModel.nodeDataArray and GraphLinksModel.linkDataArray. You then set the Diagram.model property so that the diagram can create Nodes for all of the node data and Links for all of the link data.
Models interpret and maintain references between the data. Each node data is expected to have a unique key value so that references to node data can be resolved reliably. Models also manage dynamically adding and removing data.
The node data and the link data in models can be any JavaScript object. You get to decide what properties those objects have -- add as many as you need for your app. Since this is JavaScript, you can even add properties dynamically. There are several properties that GoJS models assume exist on the data, such as "key" (on node data) and "category" and "from" and "to" (the latter two on link data). However you can tell the model to use different property names by setting the model properties whose names end in "...Property".
A node data object normally has its node's unique key value in the "key" property.
Currently node data keys must be strings or numbers.
You can get the key for a Node either via the Node.key property or via someNode.data.key
.
Let us create a diagram providing the minimal amount of necessary information. The particular node data has been put into an array of JavaScript objects. We declare the link relationships in a separate array of link data objects. Each link data holds references to the node data by using their keys. Normally the references are the values of the "from" and "to" properties.
const nodeDataArray = [
{ key: "Alpha"},
{ key: "Beta" }
];
const linkDataArray = [
{ from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
This results in two nodes and a link, but the nodes do not appear the way we want. So we define the node template to be a generalization of the particular node constructions that we did above.
diagram.nodeTemplate = // provide custom Node appearance
new go.Node("Auto")
.add(
new go.Shape("RoundedRectangle", { fill: "white" }),
new go.TextBlock("hello!", { margin: 5 })
);
const nodeDataArray = [
{ key: "Alpha" },
{ key: "Beta" }
];
const linkDataArray = [
{ from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
Now the graph looks better, but the nodes have not been parameterized -- they are all identical! We can achieve that parameterization by using data binding.
A data binding is a declaration that the value of the property of one object should be used to set the value of a property of another object.
In this case, we want to make sure that the TextBlock.text property gets the "key" value of the corresponding node data. And we want to make sure that the Shape.fill property gets set to the color/brush given by the "color" property value of the corresponding node data.
We can declare such data-bindings by creating Binding objects and associating them with the target GraphObject. Programmatically you do this by calling GraphObject.bind.
diagram.nodeTemplate =
new go.Node("Auto")
.add(
new go.Shape("RoundedRectangle", { fill: "white" }) // default Shape.fill value
.bind("fill", "color"), // binding to get fill from nodedata.color
new go.TextBlock({ margin: 5 })
.bind("text") // binding to get TextBlock.text from nodedata.text
);
const nodeDataArray = [
// note extra properties for each node data: text and color
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "pink" }
];
const linkDataArray = [
{ from: 1, to: 2 }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
Now we have the same diagram result as before, but it is implemented in much more general manner. You can easily add more node and link data to build bigger diagrams. And you can easily change the appearance of all of the nodes without modifying the data.
Actually, you may notice that the Link is different: it has an arrowhead. No arrowhead was included when we first built this diagram using code. But the default Diagram.linkTemplate includes an arrowhead and we did not replace the link template with a custom one in this example.
Notice that the value of Shape.fill in the template above gets a value twice. First it is set to "white". Then the binding sets it to whatever value the node data's "color" property has. It may be useful to be able to specify an initial value that remains in case the node data does not have a "color" property or if there is an error getting that value.
At this point we can also be a bit more precise about what a template is. A template is a Part that may have some data Bindings and that is not itself in a diagram but may be copied to create parts that are added to a diagram.
The implementations of all predefined templates are provided in Templates.js in the Extensions directory. You may wish to copy and adapt these definitions when creating your own templates.
A model is a way of interpreting a collection of data objects as an abstract graph with various kinds of relationships determined by data properties and the assumptions that the model makes. The simplest kind of model, Model, can only hold "parts" without any relationships between them -- no links or groups. But that model class acts as the base class for other kinds of models.
The kind of model you have seen above, GraphLinksModel, is actually the most general kind. It supports link relationships using a separate link data object for each Link. There is no inherent limitation on which Nodes a Link may connect, so reflexive and duplicate links are allowed. Links might also result in cycles in the graph. However you may prevent the user from drawing such links by setting various properties, such as Diagram.validCycle. And if you want to have a link appear to connect with a link rather than with a node, this is possible by having special nodes, known as "label nodes", that belong to links and are arranged along the path of a link in the same manner as text labels are arranged on a link.
Furthermore a GraphLinksModel also supports identifying logically and physically different connection objects, known as "ports", within a Node. Thus an individual link may connect with a particular port rather than with the node as a whole. The Link Points and Ports pages discuss this topic in more depth.
A GraphLinksModel also supports the group-membership relationship. Any Part can belong to at most one Group; no group can be contained in itself, directly or indirectly. You can learn more about grouping in other pages, such as Groups.
A simpler kind of model, the TreeModel, only supports link relationships that form a tree-structured graph. There is no separate link data, so there is no "linkDataArray". The parent-child relationship inherent in trees is determined by an extra property on the child node data which refers to the parent node by its key. If that property, whose name defaults to "parent", is undefined, then that data's corresponding node is a tree root. Each Link is still data bound, but the link's data is the child node data.
diagram.nodeTemplate =
new go.Node("Auto")
.add(
new go.Shape("Ellipse")
.bind("fill", "color"),
new go.TextBlock({ margin: 5 })
.bind("text")
);
const nodeDataArray = [
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", parent: 1, color: "yellow" }, // note the "parent" property
{ key: 3, text: "Gamma", parent: 1, color: "orange" },
{ key: 4, text: "Delta", parent: 1, color: "lightgreen" }
];
diagram.model = new go.TreeModel(nodeDataArray);
Many of the tree-oriented samples make use of a TreeModel instead of a GraphLinksModel. But just because your graph is tree-structured does not mean you have to use a TreeModel. You may find that your data is organized with a separate "table" defining the link relationships, so that using a GraphLinksModel is most natural. Or you may want to use other features that TreeModel does not support.
Other pages such as Trees discuss tree-oriented features of GoJS in more detail.
Each Node is the visual representation of a specific JavaScript Object that is in the Model.nodeDataArray. If there are two objects in the model, they will result in two nodes, even if the properties of both objects are exactly the same. For example:
myDiagram.model.nodeDataArray = [
{ text: "something", count: 17 },
{ text: "something", count: 17 }
];
This will cause there to be two separate nodes that happen to have the same property values. In fact each of those JavaScript Objects will get a different "key" value, so that references to nodes will always be able to be distinguished.
This illustrates how the identity of each node is determined by the Object in memory that is the node's data. You cannot delete a node by removing an object that is similar to one that is in the model. Consider this statement:
myDiagram.model.removeNodeData({ text: "something", count: 17 });
Such code will never remove any node data from the Model.nodeDataArray nor any Node from any Diagram because the Object that is passed to Model.removeNodeData is a new Object, not the same Object that is present in the model.
Nor can you find a node by giving it a similar node data object. There is no such method on Model, although there is a Model.findNodeDataForKey method. But if you really want to search for nodes that have particular properties, you can call Diagram.findNodesByExample.
const nodes = myDiagram.findNodesByExample({ text: "something", count: 17 });
nodes.each(n => console.log(n.key));
For the model shown above, this will return a collection of two Nodes. It then iterates over that collection and prints each Node.key, which in the case of the above model will be some automatically assigned key values.
Although the identity of a node is the node's data object in the model, references to nodes are not "pointers" to those objects. Instead, references are always by the "key" of the node data. (The property need not be named "key" -- see Model.nodeKeyProperty.) Using keys instead of direct references to data objects makes it easier to read and write models, especially by Model.toJson and Model.fromJson, and to debug them in memory. Thus Links are defined by data using keys, and Group membership is determined by data using keys:
myDiagram.model.nodeDataArray = [ // for a GraphLinksModel
{ key: "Alpha" },
{ key: "Beta", group: "Gamma" },
{ key: "Gamma", isGroup: true }
];
myDiagram.model.linkDataArray = [ // for a GraphLinksModel
{ from: "Alpha", to: "Beta"}
];
myDiagram.model.nodeDataArray = [ // for a TreeModel
{ key: "Alpha" },
{ key: "Beta", parent: "Alpha" }
];
If you want to add or remove nodes programmatically, you will probably want to call the Model.addNodeData and Model.removeNodeData methods. Use the Model.findNodeDataForKey method to find a particular node data object if you only have its unique key value. You may also call Model.copyNodeData to make a copy of a node data object that you can then modify and pass to Model.addNodeData.
It does not work to simply mutate the Array that is the value of Model.nodeDataArray, because the GoJS software will not be notified about any change to any JavaScript Array and thus will not have a chance to add or remove Nodes or other Parts as needed. (But setting the Model.nodeDataArray property to refer to a different Array does of course notify the model.)
Similarly, it does not work to simply set a property of a node data object. Any Binding that depends on the property will not be notified about any changes, so it will not be able to update its target GraphObject property. For example, setting the color property will not cause the Shape to change color.
const data = myDiagram.model.findNodeDataForKey("Delta");
// This will NOT change the color of the "Delta" Node
if (data !== null) data.color = "red";
Instead you need to call Model.setDataProperty to modify an object in the model.
const data = myDiagram.model.findNodeDataForKey("Delta");
// This will update the color of the "Delta" Node
if (data !== null) myDiagram.model.setDataProperty(data, "color", "red");
Calling model methods such as Model.addNodeData or Model.setDataProperty is required when the JavaScript Array or Object is already part of the Model. When first building the Array of Objects for the Model.nodeDataArray or when initializing a JavaScript Object as a new node data object, such calls are not necessary. But once the data is part of the Model, calling the model's methods to effect changes is necessary.
In some software architectures it might not be possible to insist that all data changes go through Model methods. In such cases it is possible to call Diagram.updateAllRelationshipsFromData and Diagram.updateAllTargetBindings.
However, please note that doing so will prevent the UndoManager from properly recording state changes. There would be no way for the UndoManager to know what had been the previous values of properties. Furthermore it makes it hard to have more than one Diagram showing the Model.
In some software architectures it is customary to have "models" consist of immutable (unmodifiable) data. However, as the GoJS diagram is modified, its model data will be modified, so you cannot use that immutable data in the model. You could make a copy of all of the immutable data and then replace the Diagram.model whenever the data has changed outside of the diagram/model. But that would cause old Nodes and Links to be re-created, and that would be unworkably expensive in time and space when the model is large.
If you do have immutable model data, you can update the existing Model and thus its Diagrams by calling the Model.mergeNodeDataArray and GraphLinksModel.mergeLinkDataArray methods. This will be much more efficient than replacing the Model.nodeDataArray and GraphLinksModel.linkDataArray Arrays each time, because it will preserve the existing Nodes and Links if possible.
Note that this scheme depends on maintaining the "key"s for all of the node data and for all of the link data. That happens automatically for all nodes, but for GraphLinksModels, it means setting GraphLinksModel.linkKeyProperty to the name of the property on the link data that you want to use to remember the key value.
After each diagram transaction some of the model data may have changed. But you cannot share references to that modified data with the rest of the software that is expecting immutable data. Instead you can call Model.toIncrementalData which will provide copies of the modified data. That data can then be used to update the rest of the app's state. Read more about this at Using GoJS with React and the gojs-react package, which provides generic Diagram components that you can use in your app using React.
GoJS does not require you to save models in any particular medium or format. But because this is JavaScript and JSON is the most popular data-interchange format, we do make it easy to write and read models as text in JSON format.
Just call Model.toJson to generate a string representing your model. Call the static function Model.fromJson to construct and initialize a model given a string produced by Model.toJson. Many of the samples demonstrate this -- search for JavaScript functions named "save" and "load". Most of those functions write and read a TextArea on the page itself, so that you can see and modify the JSON text and then load it to get a new diagram. But please be cautious when editing because JSON syntax is very strict, and any syntax errors will cause those "load" functions to fail.
JSON formatted text has strict limits on the kinds of data that you can represent without additional assumptions. To save and load any data properties that you set on your node data (or link data), they need to meet the following requirements:
Model.toJson and Model.fromJson will also handle instances of
Point, Size, Rect, Spot, Margin,
Geometry, and non-pattern Brushes.
However we recommend that you store those objects in their string representations,
using those classes' parse
and stringify
static functions.
Because you are using JavaScript, it is trivial for you to add data properties to your node data. This allows you to associate whatever information you need with each node. But if you need to associate some information with the model, which will be present even if there is no node data at all, you can add properties to the Model.modelData object. This object's properties will be written by Model.toJson and read by Model.fromJson, just as node data objects are written and read.