Common Features
This section describes the common features of all data binding classes.
Throughout the graph builder API, collections of business data objects are called "data" and single data objects are called "data items".
Binding the Business Data to the Graph Builders
So-called source objects bind a collection of data items to the graph builder. There are different factory methods to create source objects, e.g., createNodesSource<TDataItem>, and createEdgesSource<TDataItem>. You can create multiple nodes and edges sources, where each source may represent differently structured data collections and data items. This is especially useful when you have different types of nodes or when the business data originates from multiple different sources. All sources contribute to the same graph, and nodes from different sources may be connected by edges.
const graphBuilder = new GraphBuilder()
const redNodesSource = graphBuilder.createNodesSource(
redNodes, // data collection for red nodes
(data) => data.id // id provider
)
const blueNodesSource = graphBuilder.createNodesSource(
blueNodes, // data collection for blue nodes
(data) => data.id // id provider
)
const edgesSource = graphBuilder.createEdgesSource(
edges, // data collection for edges
(data) => data.sourceNode, // provider for the source node id
(data) => data.targetNode // provider for the target node id
)
const graphBuilder = new GraphBuilder()
const redNodesSource = graphBuilder.createNodesSource(
redNodes, // data collection for red nodes
(data) => data.id! // id provider
)
const blueNodesSource = graphBuilder.createNodesSource(
blueNodes, // data collection for blue nodes
(data) => data.id! // id provider
)
const edgesSource = graphBuilder.createEdgesSource(
edges, // data collection for edges
(data) => data.sourceNode, // provider for the source node id
(data) => data.targetNode // provider for the target node id
)
The data binding module for yFiles for HTML accepts different types of data collections. Data
collections may be JavaScript arrays
, JavaScript objects
,
iterables,
or instances of an EcmaScript 2015 Map
. Since IEnumerable<T> implements the iterable
protocol, all yFiles for HTML collections are accepted as well.
Each data item may be identified by a unique ID. The default
ID of a data item depends on the type of the data collection. If the collection is an instance of Map
or a
JavaScript object
, the default ID is the associated key in the Map or object. Otherwise, the item itself is the
default ID.
Each source type has an idProvider property (for example,
idProvider) that can provide a different ID for a data item. The key in a Map
or object
or the index in an array
or iterable
is passed to the idProvider as the second canonicalId
parameter.
Configuring the Visual Appearance of Graph Items
Each source object has a creator property, e.g., nodeCreator. While the source objects are responsible for building the graph structure, the creator objects manage the visual appearance of single items.
The appearance of a node is controlled by the NodeCreator<TDataItem>'s defaults and its providers, e.g., layoutProvider. The defaults determine the default properties that are shared among all nodes created by this NodeCreator<TDataItem>. The defaults cascade with the GraphBuilder’s graph, meaning that all properties that are not explicitly set on the NodeCreator’s defaults are inherited from the graph’s defaults. The providers allow for binding specific properties of a data item to properties of the node, such as its location or tag.
Similarly to nodes, edges are created by an EdgeCreator<TDataItem>.
Both node and edge creators allow for defining one or multiple labels that depend on the node or edge data items. Label creation is explained in more detail below.
Both the sources and the creators have a generic type that is bound to the data item type. The data items are passed to the various provider functions. The following tables give an overview of the available properties:
Property | Description |
---|---|
Configuring a NodeCreator shows how to configure a NodeCreator<TDataItem> to use a specific node style that is not shared between the created nodes and set several style properties depending on the node data item:
nodesSource.nodeCreator.defaults.shareStyleInstance = false
nodesSource.nodeCreator.defaults.style = new ShapeNodeStyle({
stroke: 'darkOrange',
fill: 'lightYellow',
shape: 'round-rectangle'
})
nodesSource.nodeCreator.styleBindings.addBinding('stroke', (employee) =>
employee.position.includes('Chief') ? 'darkRed' : 'darkOrange'
)
nodesSource.nodeCreator.styleBindings.addBinding('shape', (employee) =>
employee.freelancer ? 'hexagon' : 'roundRectangle'
)
In addition to the properties shared by all creator types, there are further properties available on NodeCreator<TDataItem>, EdgeCreator<TDataItem> and LabelCreator<TDataItem>:
Property | Description |
---|---|
x and y on the layout provided by the defaults or the layout
provider. | |
Property | Description |
---|---|
Property | Description |
---|---|
Note that NodeCreator<TDataItem>, EdgeCreator<TDataItem>, and LabelCreator<TDataItem> can be used independently of any graph builder. This might come in handy if the business data structure does not fit any of the graph builders' requirements.
Adding labels
Labels can be added using the node and edge creators. When the number of labels per node or edge is known beforehand and the labels do not have their own data objects, createLabelBinding will create a LabelCreator<TDataItem> with the node’s or edge’s data item. Otherwise, createLabelsSource<TLabelDataItem> creates a LabelsSource<TDataItem> that allows for creating a dynamic amount of labels per node or edge with their own data objects.
createLabelBinding returns a LabelCreator<TDataItem> whose textProvider is preconfigured with the text provider passed as parameter to createLabelBinding.
nodesSource.nodeCreator.createLabelBinding((employee) => employee.name)
The LabelCreator<TDataItem> can be configured further to adjust the styling and position according to the given data item. Note that with createLabelBinding the data item is of the same type as the item of the owning node or edge.
const labelCreator = nodesSource.nodeCreator.createLabelBinding(
(employee) => employee.name
)
labelCreator.styleProvider = (employee) =>
employee.department == 'IT' ? itLabelStyle : logisticsLabelStyle
const labelCreator: LabelCreator<Employee> =
nodesSource.nodeCreator.createLabelBinding((employee) => employee.name)
labelCreator.styleProvider = (employee) =>
employee.department == 'IT' ? itLabelStyle : logisticsLabelStyle
const creator1 = nodesSource.nodeCreator.createLabelBinding(
(employee) => employee.name
)
creator1.defaults.layoutParameter = InteriorNodeLabelModel.TOP
const creator2 = nodesSource.nodeCreator.createLabelBinding(
(employee) => employee.department
)
creator2.defaults.layoutParameter = InteriorNodeLabelModel.BOTTOM
createLabelsSource<TLabelDataItem> returns a LabelsSource<TDataItem> whose labelCreator has to be configured further. Note that the data provider which is passed as parameter is expected to return an enumerable of data items where each item represents one label.
const labelsSource = nodesSource.nodeCreator.createLabelsSource(
(employee) => employee.labels
)
labelsSource.labelCreator.styleProvider = (labelObject) =>
labelObject.style == 'Orange' ? orangeLabelStyle : defaultLabelStyle
labelsSource.labelCreator.textProvider = (labelObject) => labelObject.text
const labelsSource: LabelsSource<LabelObject> =
nodesSource.nodeCreator.createLabelsSource(
(employee) => employee.labels
)
labelsSource.labelCreator.styleProvider = (labelObject) =>
labelObject.style == 'Orange' ? orangeLabelStyle : defaultLabelStyle
labelsSource.labelCreator.textProvider = (labelObject) => labelObject.text
Labels can also be used to provide icons instead of text, using an IconLabelStyle and configuring the LabelCreator<TDataItem> accordingly:
const labelCreator = nodesSource.nodeCreator.createLabelBinding()
// to avoid empty labels to be created return null for missing image URL
labelCreator.textProvider = (employee) =>
employee.imageUrl != null ? ' ' : null
labelCreator.styleProvider = (employee) =>
new IconLabelStyle({
href: employee.imageUrl,
iconSize: new Size(16, 16),
iconPlacement: ExteriorNodeLabelModel.LEFT
})
Creating And Updating Graphs
Each graph builder manages an IGraph instance that can be passed as a constructor parameter. If no graph is passed, the graph builder creates a new Graph. Creating the initial graph and updating it with changed data isn’t automatic; you must trigger it explicitly.
- buildGraph populates the managed graph instance with items generated from the bound data. It preserves existing items and does not clear the graph.
- updateGraph updates the graph to reflect changes in the bound data by adding new elements and removing obsolete ones. You can set updated data collections using the various setData<TDataItem> methods. This is only required if the instances of the data collections have changed.
Note that neither method triggers an automatic layout. If a layout is required, you must apply it in a subsequent step.
// Build a graph initially
graphBuilder.buildGraph()
// Apply a layout in a subsequent step
graph.applyLayout(new OrganicLayout())
// ...
// Set the new data collections:
graphBuilder.setData(redNodesSource, newRedNodes)
graphBuilder.setData(blueNodesSource, newBlueNodes)
graphBuilder.setData(edgesSource, newEdges)
// Update a graph after the business data has changed
graphBuilder.updateGraph()
// Apply a layout in a subsequent step
graph.applyLayout(new OrganicLayout())
The updateGraph methods only perform structural updates on the graph by default.
If the data for existing nodes might change and require updating (e.g., style or labels),
you must update the graph item explicitly. The NodeCreator<TDataItem> raises an node-updated
event for each node that has been kept in the graph and might require updating.
Developers can use the various update
methods provided by NodeCreator<TDataItem> to use the binding
mechanism for node and edge creation to update various properties on existing nodes.
For edges, EdgeCreator<TDataItem> provides similar methods that can be called upon the
edge-updated event.
nodeCreator.addEventListener('node-updated', (evt) => {
// evaluate the providers and apply respective bindings
nodeCreator.updateLayout(evt.graph, evt.item, evt.dataItem)
nodeCreator.updateStyle(evt.graph, evt.item, evt.dataItem)
nodeCreator.updateTag(evt.graph, evt.item, evt.dataItem)
nodeCreator.updateLabels(evt.graph, evt.item, evt.dataItem)
})
edgeCreator.addEventListener('edge-updated', (evt) => {
// evaluate the providers and apply respective bindings
edgeCreator.updateBends(evt.graph, evt.item, evt.dataItem)
edgeCreator.updateStyle(evt.graph, evt.item, evt.dataItem)
edgeCreator.updateTag(evt.graph, evt.item, evt.dataItem)
edgeCreator.updateLabels(evt.graph, evt.item, evt.dataItem)
})
graphBuilder.updateGraph()
Only add update methods for properties that might change. Properties that are known not to change or are not affected by the data do not need their update method called.
The same applies to labels: if the text, style, or other properties of an existing label might change,
developers must add update
methods to eachLabelCreator<TDataItem> instance.
const labelCreator1 = nodeCreator.createLabelBinding((item) => item.name)
labelCreator1.addEventListener('label-updated', (evt) => {
// update everything which might change
labelCreator1.updateStyle(evt.graph, evt.item, evt.dataItem)
labelCreator1.updateText(evt.graph, evt.item, evt.dataItem)
labelCreator1.updateTag(evt.graph, evt.item, evt.dataItem)
labelCreator1.updateLayoutParameter(evt.graph, evt.item, evt.dataItem)
labelCreator1.updatePreferredSize(evt.graph, evt.item, evt.dataItem)
})
const labelCreator2 = nodeCreator.createLabelBinding(
(item) => item.position
)
labelCreator2.addEventListener('label-updated', (evt) => {
// update only properties which might change
labelCreator2.updateText(evt.graph, evt.item, evt.dataItem)
})
Update only the properties that might change.
Grouping
All graph builders fully support the creation of group nodes from data. Group nodes can appear anywhere regular nodes can. This especially means that group nodes can be recursively nested, and edges can connect to group nodes.
The isGroupPredicate determines whether a node is created as a group node. Each graph
builder provides a group source factory method that conveniently creates a source object for only group nodes
(e.g. createGroupNodesSource<TDataItem>). These sources have the isGroupPredicate
set to a function that always returns true
and have defaults that cascade with the
groupNodeDefaults.
You can set a custom isGroupPredicate when the NodesSource<TDataItem> should create a mix of group and normal nodes. Note, however, that all nodes share the same defaults of their NodesSource<TDataItem>.
Similar to the IGraph API, the relationship between a node and its group is determined by the parentIdProvider property, which determines the ID of a node’s parent group node.
const nodesData = getNodesData()
const groupNodesData = getGroupNodesData()
const graphBuilder = new GraphBuilder()
// create a nodes source for the regular nodes
const nodesSource = graphBuilder.createNodesSource(
nodesData,
(item) => item.id
)
// set a parent provider that associates a node with its parent group
nodesSource.parentIdProvider = (nodeDataItem) => nodeDataItem.parentId
// create a nodes source for the group nodes
const groupNodesSource = graphBuilder.createGroupNodesSource(
groupNodesData,
(groupDataItem) => groupDataItem.id
)
// groups may be nested within each other -> set a parent provider that
// associates a node with its parent group
groupNodesSource.parentIdProvider = (groupDataItem) =>
groupDataItem.parentId