gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
507 lines (488 loc) • 25.4 kB
HTML
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GoJS Using Models -- Northwoods Software</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<script src="../release/go.js"></script>
<script src="goIntro.js"></script>
</head>
<body onload="goIntro()">
<div id="container" class="container-fluid">
<div id="content">
<h1>Using Models and Templates</h1>
<p>
You can build a diagram of nodes and links programmatically.
But <b>GoJS</b> 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.
</p>
<h2 id="BuildingDiagramsWithCode">Building diagrams with code</h2>
<p>
Let us try to build two nodes and connect them with a link.
Here is one way of doing that:
</p>
<pre class="lang-js" id="twoNodesOneLinkCode">
var node1 =
$(go.Node, "Auto",
$(go.Shape,
{ figure: "RoundedRectangle",
fill: "lightblue" }),
$(go.TextBlock,
{ text: "Alpha",
margin: 5 })
)
diagram.add(node1);
var node2 =
$(go.Node, "Auto",
$(go.Shape,
{ figure: "RoundedRectangle",
fill: "pink" }),
$(go.TextBlock,
{ text: "Beta",
margin: 5 })
);
diagram.add(node2);
diagram.add(
$(go.Link,
{ fromNode: node1, toNode: node2 },
$(go.Shape)
));
</pre>
<script>goCode("twoNodesOneLinkCode", 250, 150)</script>
<p>
This produces a nice, simple diagram.
If you drag one of the nodes, you will see that the link remains connected to it.
</p>
<p>
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.
</p>
<p>
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?
</p>
<p>
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.
</p>
<p>
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.
</p>
<h2 id="UsingModelAndTemplates">Using a Model and Templates</h2>
<p>
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 <a>Part</a> that can be copied; you would have different templates for <a>Node</a>s and for <a>Link</a>s.
</p>
<p>
In fact, a <a>Diagram</a> 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 <a>Diagram.nodeTemplate</a>.
</p>
<p>
To automatically make use of templates, provide the diagram a model holding the data for each node and the data for each link.
A <a>GraphLinksModel</a> holds the collections (actually arrays) of node data and link data as the values of
<a>GraphLinksModel.nodeDataArray</a> and <a>GraphLinksModel.linkDataArray</a>.
You then set the <a>Diagram.model</a> property so that the diagram can create <a>Node</a>s for all of the node data
and <a>Link</a>s for all of the link data.
</p>
<p>
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.
</p>
<p>
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 <b>GoJS</b> 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".
</p>
<p>
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 <a>Node.key</a> property or via <code>someNode.data.key</code>.
</p>
<p>
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.
</p>
<pre class="lang-js" id="simpleModelNoTemplates">
var nodeDataArray = [
{ key: "Alpha"},
{ key: "Beta" }
];
var linkDataArray = [
{ from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
</pre>
<script>goCode("simpleModelNoTemplates", 250, 150)</script>
<p>
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.
</p>
<pre class="lang-js" id="simpleModelNoBind">
diagram.nodeTemplate = // provide custom Node appearance
$(go.Node, "Auto",
$(go.Shape,
{ figure: "RoundedRectangle",
fill: "white" }),
$(go.TextBlock,
{ text: "hello!",
margin: 5 })
);
var nodeDataArray = [
{ key: "Alpha" },
{ key: "Beta" }
];
var linkDataArray = [
{ from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
</pre>
<script>goCode("simpleModelNoBind", 250, 150)</script>
<p>
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.
</p>
<h2 id="ParameterizingNodesUsingDataBindings">Parameterizing Nodes using data binding</h2>
<p>
A data binding is a declarative statement that the value of the property of one object
should be used to set the value of a property of another object.
</p>
<p>
In this case, we want to make sure that the <a>TextBlock.text</a> property gets the
"key" value of the corresponding node data.
And we want to make sure that the <a>Shape.fill</a> property gets set to the color/brush given
by the "color" property value of the corresponding node data.
</p>
<p>
We can declare such data-bindings by creating <a>Binding</a> objects and associating them with the target <a>GraphObject</a>.
Programmatically you do this by calling <a>GraphObject.bind</a>.
But when using <b>go.GraphObject.make</b>, this happens automatically when you pass in a <a>Binding</a>.
</p>
<pre class="lang-js" id="simpleModelWithBind">
diagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape,
{ figure: "RoundedRectangle",
fill: "white" }, // default Shape.fill value
new go.Binding("fill", "color")), // binding to get fill from nodedata.color
$(go.TextBlock,
{ margin: 5 },
new go.Binding("text", "key")) // binding to get TextBlock.text from nodedata.key
);
var nodeDataArray = [
{ key: "Alpha", color: "lightblue" }, // note extra property for each node data: color
{ key: "Beta", color: "pink" }
];
var linkDataArray = [
{ from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
</pre>
<script>goCode("simpleModelWithBind", 250, 150)</script>
<p>
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.
</p>
<p>
Actually, you may notice that the <a>Link</a> is different: it has an arrowhead.
No arrowhead was included when we first built this diagram using code.
But the default <a>Diagram.linkTemplate</a> includes an arrowhead
and we did not replace the link template with a custom one in this example.
</p>
<p>
Notice that the value of <a>Shape.fill</a> 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.
</p>
<p>
At this point we can also be a bit more precise about what a template is.
A template is a <a>Part</a> that may have some data <a>Binding</a>s and that is not itself in a diagram
but may be copied to create parts that are added to a diagram.
</p>
<h3 id="TemplateDefinitions">Template Definitions</h3>
<p>
The implementations of all predefined templates are provided in <a href="../extensions/Templates.js">Templates.js</a> in the Extensions directory.
You may wish to copy and adapt these definitions when creating your own templates.
</p>
<p>
Those definitions might not be an up-to-date description
of the actual standard template implementations that are in <b>GoJS</b>.
</p>
<h2 id="KindsOfModels">Kinds of Models</h2>
<p>
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, <a>Model</a>, 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.
</p>
<h3 id="GraphLinksModel">GraphLinksModel</h3>
<p>
The kind of model you have seen above, <a>GraphLinksModel</a>, is actually the most general kind.
It supports link relationships using a separate link data object for each <a>Link</a>.
There is no inherent limitation on which <a>Nodes</a> 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 <a>Diagram.validCycle</a>.
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.
</p>
<p>
Furthermore a <a>GraphLinksModel</a> also supports identifying logically and physically different connection objects,
known as "ports", within a <a>Node</a>.
Thus an individual link may connect with a particular port rather than with the node as a whole.
The <a href="connectionPoints.html">Link Points</a> and <a href="ports.html">Ports</a> pages discuss this topic in more depth.
</p>
<p>
A <a>GraphLinksModel</a> also supports the group-membership relationship.
Any <a>Part</a> can belong to at most one <a>Group</a>; no group can be contained in itself, directly or indirectly.
You can learn more about grouping in other pages, such as <a href="groups.html">Groups</a>.
</p>
<h3 id="TreeModel">TreeModel</h3>
<p>
A simpler kind of model, the <a>TreeModel</a>, 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 <a>Link</a> is still data bound, but the link's data is the child node data.
</p>
<pre class="lang-js" id="simpleTree">
diagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape,
{ figure: "Ellipse" },
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 5 },
new go.Binding("text", "key"))
);
var nodeDataArray = [
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", parent: "Alpha", color: "yellow" }, // note the "parent" property
{ key: "Gamma", parent: "Alpha", color: "orange" },
{ key: "Delta", parent: "Alpha", color: "lightgreen" }
];
diagram.model = new go.TreeModel(nodeDataArray);
</pre>
<script>goCode("simpleTree", 250, 150)</script>
<p>
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.
</p>
<p>
Other pages such as <a href="trees.html">Trees</a> discuss tree-oriented features of <b>GoJS</b> in more detail.
</p>
<h2 id="IdentityAndReferences">Identity and References</h2>
<p>
Each <a>Node</a> is the visual representation of a specific JavaScript Object that is in the <a>Model.nodeDataArray</a>.
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:
</p>
<pre class="lang-js">
myDiagram.model.nodeDataArray = [
{ text: "something", count: 17 },
{ text: "something", count: 17 }
];
</pre>
<p>
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.
</p>
<p>
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:
</p>
<pre class="lang-js">
myDiagram.model.removeNodeData({ text: "something", count: 17 });
</pre>
<p>
Such code will never remove any node data from the <a>Model.nodeDataArray</a> nor any <a>Node</a> from any <a>Diagram</a>
because the Object that is passed to <a>Model.removeNodeData</a> is a new Object, not the <em>same Object</em> that is present in the model.
</p>
<p>
Nor can you find a node by giving it a similar node data object.
There is no such method on <a>Model</a>, although there is a <a>Model.findNodeDataForKey</a> method.
But if you really want to search for nodes that have particular properties,
you can call <a>Diagram.findNodesByExample</a>.
</p>
<pre class="lang-js">
var nodes = myDiagram.findNodesByExample({ text: "something", count: 17 });
nodes.each(function(n) { console.log(n.key); });
</pre>
<p>
For the model shown above, this will return a collection of two <a>Node</a>s.
It then iterates over that collection and prints each <a>Node.key</a>,
which in the case of the above model will be some automatically assigned key values.
</p>
<h3 id="ReferencesToNodes">References to Nodes</h3>
<p>
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 <a>Model.nodeKeyProperty</a>.)
Using keys instead of direct references to data objects makes it easier to read and write models,
especially by <a>Model.toJson</a> and <a>Model,fromJson</a>, and to debug them in memory.
Thus <a>Link</a>s are defined by data using keys, and <a>Group</a> membership is determined using keys:
</p>
<pre class="lang-js">
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"}
];
</pre>
<pre class="lang-js">
myDiagram.model.nodeDataArray = [ // for a TreeModel
{ key: "Alpha" },
{ key: "Beta", parent: "Alpha" }
];
</pre>
<h2 id="ModifyingModels">Modifying Models</h2>
<p>
If you want to add or remove nodes programmatically, you will probably want to call the
<a>Model.addNodeData</a> and <a>Model.removeNodeData</a> methods.
Use the <a>Model.findNodeDataForKey</a> method to find a particular node data object if you only have its unique key value.
You may also call <a>Model.copyNodeData</a> to make a copy of a node data object that you can then modify and pass to <a>Model.addNodeData</a>.
</p>
<p>
It does not work to simply mutate the Array that is the value of <a>Model.nodeDataArray</a>,
because the <b>GoJS</b> software will not be notified about any change to any JavaScript Array and
thus will not have a chance to add or remove <a>Node</a>s or other <a>Part</a>s as needed.
(But setting the <a>Model.nodeDataArray</a> property to refer to a different Array does of course notify the model.)
</p>
<p>
Similarly, it does not work to simply set a property of a node data object.
Any <a>Binding</a> that depends on the property will not be notified about any changes,
so it will not be able to update its target <a>GraphObject</a> property.
For example, setting the color property will not cause the <a>Shape</a> to change color.
</p>
<pre class="lang-js">
var data = myDiagram.model.findNodeDataForKey("Delta");
// This will NOT change the color of the "Delta" Node
if (data !== null) data.color = "red";
</pre>
<p>
Instead you need to call <a>Model.setDataProperty</a> to modify an object in the model.
</p>
<pre class="lang-js">
var data = myDiagram.model.findNodeDataForKey("Delta");
// This will update the color of the "Delta" Node
if (data !== null) myDiagram.model.setDataProperty(data, "color", "red");
</pre>
<p>
Calling model methods such as <a>Model.addNodeData</a> or <a>Model.setDataProperty</a> is required
when the JavaScript Array or Object is already part of the Model.
When first building the Array of Objects for the <a>Model.nodeDataArray</a>
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.
</p>
<h3 id="ExternallyModifiedData">Externally Modified Data</h3>
<p>
In some software architectures it might not be possible to insist that all data changes go through <a>Model</a> methods.
In such cases it is possible to call <a>Diagram.updateAllRelationshipsFromData</a> and
<a>Diagram.updateAllTargetBindings</a>.
</p>
<p>
However, please note that doing so will prevent the <a>UndoManager</a> from properly recording state changes.
There would be no way for the <a>UndoManager</a> to know what had been the previous values of properties.
Furthermore it makes it hard to have more than one Diagram showing the Model.
</p>
<h3 id="ImmutableData">Immutable Data</h3>
<p>
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 <a>Diagram.model</a> 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.
</p>
<p>
If you do have immutable model data, you can update the existing <a>Model</a> and thus its <a>Diagram</a>s by calling
the <a>Model.mergeNodeDataArray</a> and <a>GraphLinksModel.mergeLinkDataArray</a> methods.
This will be much more efficient than replacing the <a>Model.nodeDataArray</a> and <a>GraphLinksModel.linkDataArray</a>
Arrays each time, because it will preserve the existing Nodes and Links if possible.
</p>
<p>
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
<a>GraphLinksModel.linkKeyProperty</a> to the name of the property on the link data that
you want to use to remember the key value.
</p>
<p>
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 <a>Model.toIncrementalData</a> 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 <a href="react.html">Using GoJS with React</a>
and the <a href="https://github.com/NorthwoodsSoftware/gojs-react">gojs-react package</a>,
which provides generic Diagram components that you can use in your app using React.
</p>
<h2 id="SavingAndLoadingModels">Saving and Loading Models</h2>
<p>
<b>GoJS</b> 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.
</p>
<p>
Just call <a>Model.toJson</a> to generate a string representing your model.
Call the static method <a>Model,fromJson</a> to construct and initialize a model given a string produced by <a>Model.toJson</a>.
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.
</p>
<p>
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:
</p>
<ul>
<li>the property is enumerable and its name does not start with an underscore (you can use property names that do start with an underscore, but they won't be saved)</li>
<li>the property value is not undefined and is not a function (JSON cannot faithfully hold functions)</li>
<li>the model knows how to convert the property value to JSON format (numbers, strings, JavaScript Arrays, or plain JavaScript Objects)</li>
<li>property values that are Objects or Arrays form a tree structure -- no shared or cyclical references</li>
</ul>
<p>
<a>Model.toJson</a> and <a>Model,fromJson</a> will also handle instances of
<a>Point</a>, <a>Size</a>, <a>Rect</a>, <a>Spot</a>, <a>Margin</a>, <a>Geometry</a>, and non-pattern <a>Brush</a>es.
However we recommend that you store those objects in their string representations, using those classes' <code>parse</code> and <code>stringify</code> static functions.
</p>
<p>
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 <a>Model.modelData</a> object.
This object's properties will be written by <a>Model.toJson</a> and read by <a>Model,fromJson</a>, just as node data objects are written and read.
</p>
</div>
</div>
</body>
</html>