Data model
The core data model in VisuallyJs is that of a graph, as discussed here on Wikipedia.
A graph is a collection of nodes, groups, edges and ports.
- Nodes map to entities in your data model.
- Groups are collections of nodes/groups. They map to entities in your data model.
- Ports are logical and/or physical locations on nodes/groups that are the terminus for an edge. Not all apps use ports in their data model. The simplest example of the usage of ports is perhaps a database schema: you'd represent your tables as nodes, and then the columns on each table would be represented as ports. Another good analogy is ports in TCP/IP.
- Edges are relationships between nodes, groups or ports.
In the VisuallyJs documentation you'll see references to Vertex and vertices - this is a node, port or group. We often use "vertex" to refer to the source and target terminus for an edge throughout the documentation as it's easier than typing "Node/Group/Port"...and also, conceptually, its correct!
What do I need the model for?
A brief list of the sorts of things you can do with the model:
- add/remove/update nodes, groups and edges
- undo/redo operations
- impose constraints on connectivity between objects
- specify object factories for the creation of new objects
- import/export the dataset
- execute multiple operations within a transaction
- select specific contents via filtering
- check for the existence of and retrieve paths between vertices
Changes to the model are automatically reflected in the UI.
Accessing the model
You'll need to access the model from inside your code in order to use the programmatic model API. Expand the section below for instructions on how to do that.
How to access the model
Data Format
The various parts of your data model can be represented as any valid Javascript type. As an example, here is the backing data for the Book table in the Schema Builder starter app:
{
"id":"book",
"name":"Book",
"type":"table",
"columns":[
{ "id":"123", "name":"id", "datatype":"integer", "primaryKey":true },
{ "id":"456", "name":"isbn", "datatype":"varchar" },
{ "id":"789", "name":"title", "datatype":"varchar" }
]
}
Object IDs
Every vertex is required to have a unique ID. VisuallyJs attempts to derive this automatically from your data, by looking for an id member, which should be a string.
For the vast majority of applications this setup will work fine. Should you wish to implement a different strategy, though, you can supply your own idFunction in your model options:
import { newInstance, ObjectData } from "@visuallyjs/browser-ui"
const modelOptions = {
idFunction: (data:ObjectData):string {
return SomeCustomComputing(data);
}
})
}
const model = newInstance(modelOptions)
const surface = model.render({})
Remember to pass back the ID as a string. This method will be used to attempt to derive an ID for any model object.
Again note this is optional - you do not need to supply this function, but if you do not then VisuallyJs will expect an id member in your data.
Group IDs
IDs for groups are derived using whatever method VisuallyJs is using to derive IDs for nodes - either by looking for an id value in your data, or by using a supplied idFunction. group IDs must be unique across all groups and nodes in the dataset: you cannot have a group that has the same ID as some node.
Edge IDs
You are not required to supply an ID for every edge, and if you do not, VisuallyJs will assign one automatically.
Port IDs
Port IDs are required to be unique on the node/group on which the port exists, but may be the same as the ID of a port on some other node (and in fact this is quite common).
Referencing ports by ID
When adding an edge to a VisuallyJs instance, you can reference a port on some node using, by default, dotted notation. In the data given above there were three ports. We could connect one of them to a column on another table like this:
model.connect({
source:"book.id",
target:"book_author.book_id"
})
book_author is another table in the schema from the SchemaBuilderDemoLink title="Schema Builder"/> starter app.
Custom port ID Separator
If you find that using a period as the separator in a port ID does not work for your data model, you can override what VisuallyJs will use by doing this:
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
portSeparator: "#"
}
const model = newInstance(modelOptions)
const surface = model.render({})
Object type
Every object has an associated type. This is an important concept in VisuallyJs, as it is the basic means by which the data model is bound to any renderers, via views.
The type of a model object is, by default, mapped by the object's type in its backing data. It is used by the view in the UI to determine the appearance and behaviour of the object.
To change the type of some object after it has been initially created, use the setType method on a model instance. For a discussion, see this page
As with ID, you can provide your own function to retrieve the type for some object, should you need to.
import { newInstance, ObjectData } from "@visuallyjs/browser-ui"
const modelOptions = {
typeFunction: (data:ObjectData):string => {
return SomeOtherComputing(data);
}
}
const model = newInstance(modelOptions)
const surface = model.render({})
Constraining connectivity
Connectivity can be controlled at runtime by interceptors - callbacks that can be used to cancel some proposed activity, and that are bound on an instance of the VisuallyJs model by supplying a specific function in the model constructor options.
beforeConnect
A function to run before an edge with the given data can be established between the given source and target. Returning false from this method aborts the connection. Note that this method fires regardless of the source of the new edge, meaning it will be called when loading data programmatically.
Method signature
beforeConnect(source: Vertex, target: Vertex): any
Parameters
- source The source vertex for the new edge
- target The target vertex for the new edge
Return value
false- aborts the connection- all other values are ignored and will allow the edge to be established
Example
Here, we reject connections from any vertex to itself.
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
beforeConnect: (source: Vertex, target: Vertex) => {
return (source !== target)
}
}
const model = newInstance(modelOptions)
const surface = model.render({})
beforeMoveConnection
A function to run before an edge of the given type is relocated from its current source or target to a new source or target. Returning false from this method will abort the move.
Method signature
beforeMoveConnection(source: Vertex, target: Vertex, edge: Edge): any
Parameters
- source Candidate source. May be the edge's current source, or may be a new source.
- target Candidate target. May be the edge's current target, or may be a new target.
- edge The edge that is being moved.
The parameters source and target reflect the source and target of the edge if the move were to be accepted. So if, for example, your user drags a connection by its target and drops it elsewhere, target will be the drop target, not the edge's current target, but source will be the edge's current source. You can access the current source/target via the source and target properties of edge.
Return value
false- aborts the move- all other values are ignored and will allow the move to occur
Example
Here, we reject moving any edge that has fixed:true in it backing data:
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
beforeMoveConnection: (source: Vertex, target: Vertex, edge:Edge) => {
return (edge.data.fixed !== true)
}
}
const model = newInstance(modelOptions)
const surface = model.render({})
beforeStartConnect
A function to run before an edge of the given type is dragged from the given source (ie. before the mouse starts moving). This interceptor is slightly different to the others in that it's not just a yes/no question: as with the other interceptors, returning false from this method will reject the action, that is in this case it will not allow a connection drag to begin. But you can also return an object from this method, and when you do that, the connection start is allowed, and the object you returned becomes the payload for the new edge.
Method signature
beforeStartConnect(source: Vertex, type: string): any
Parameters
- source The vertex that is the source for the new edge
- type The computed type for this new edge.
Return value
false- aborts the connectionObject- An object returned from this method will be used as the initial payload for the new edge- all other values are ignored and will allow the connection start to continue
Example - reject a connection start
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
beforeStartConnect: (source: Vertex, type:string) => {
return type !== 'not-connectable'
}
}
const model = newInstance(modelOptions)
const surface = model.render({})
Example - provide an initial payload
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
beforeStartConnect: (source: Vertex, type:string) => {
return {
type,
message:`initial payload for vertex ${source.id}`
}
}
}
const model = newInstance(modelOptions)
const surface = model.render({})
beforeDetach
A function to run before the given edge is detached from the given source vertex. If this method returns false, the detach will be aborted.
Method signature
beforeDetach(source: Vertex, target: Vertex, edge: Edge, isDiscard?: boolean): any
Parameters
- source The source vertex for the edge that is to be detached.
- target The candidate target for the edge - may be null, if the edge is being discarded
- edge The edge that is being detached.
- isDiscard True if the edge is not now connected to a target.
Return value
falseReturning false will abort the edge detach- all other values are ignored and will allow the detach to occur
Example
Here, we reject the detach if the target is null, ie. the user is trying to discard the edge, not relocate it.
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
beforeDetach: (source: Vertex, target: Vertex, edge: Edge, isDiscard?: boolean) => {
return target != null
}
}
const model = newInstance(modelOptions)
const surface = model.render({})
beforeStartDetach
Method signature
beforeStartDetach(source: Vertex, edge: Edge): any
A function to run before the given edge is detached from the given source vertex. If this method returns false, the detach will be aborted. The difference between this and beforeDetach is that this method is fired as soon as a user tries to detach an edge from an anchor in the UI, whereas beforeDetach allows a user to detach the edge in the UI.
Parameters
- source The source vertex for the edge that the user has started to detach
- edge The edge that the user has started to detach
Return value
falseReturning false will abort the edge detach- all other values are ignored and will allow the user to begin the detach
Example
Here, we reject the detach if the source vertex has doNotDetachEdges:true in its backing data.
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
beforeDetach: (source: Vertex, edge: Edge) => {
return source.data.doNotDetachEdges !== true
}
}
const model = newInstance(modelOptions)
const surface = model.render({})
Multiple interceptors
You can provide multiple interceptors - in this example we provide a beforeStartConnect and beforeDetach interceptor to the model. The beforeStartConnect interceptor prevents the user from dragging connections from any vertex whose ID is not an even number. The beforeDetach interceptor reattaches detached connections whose source ID is not an event number
import { newInstance } from "@visuallyjs/browser-ui"
const modelOptions = {
beforeStartConnect: (source, type) => {
// only allow connections from nodes whose
// ID is an even number
return parseInt(source.id, 10) % 2 === 0
},
beforeDetach: (source, target, edge, isDiscard) => {
// only allow connections to be detached whose
// source ID is an even number
return parseInt(edge.source.id, 10) % 2 === 0
}
}
const model = newInstance(modelOptions)
const surface = model.render({})