Data Model


Introduction

The core model abstraction in the jsPlumb Toolkit is that of a Graph, or more properly, a Directed Graph, as discussed here:

http://en.wikipedia.org/wiki/Directed_graph

A Graph is a collection of Nodes, Groups, Edges and Ports.

  • Nodes map to entities in your data model.
  • Ports are points on your Nodes that are the endpoint of some relationship with another Node, or with a Port on another Node.
  • Edges are relationships between Nodes, Groups or Ports.
  • Groups are collections of Nodes.
Example - Database Visualizer

As an example of how these data types map to an application - in the Database Visualizer application that ships with the Toolkit - Nodes, Edges and Ports are mapped as follows:

  • Nodes are tables in a database schema
  • Ports are columns on a table
  • Edges are relationships between columns on two tables
Example - Flowchart Builder

In the Flowchart Builder application, Nodes, Edges and Ports are mapped as follows:

  • Nodes are objects in the flowchart such as questions or actions.
  • Ports are used to model the yes/no connection points from a Question. Action Nodes do not have Ports as they have only one output and this is assigned to the Node itself.
  • Edges are the control flow of the chart.

Neither of these applications make use of the concept of Groups, which are new in version 1.1.0. We're currently evaluating a few different options for a demonstration application that uses Groups.

Every application using the Toolkit will have its own mapping of concepts to Nodes, Groups, Edges and Ports. Some applications may not even use Ports at all, since every Node can be an Edge target and/or source. Ports just give you the ability to model more complex data structures.

Paths

In addition to Nodes, Ports and Edges, the Toolkit has the concept of a Path: an in-order list of Nodes/Ports and Edges that represent the path from one Node or Port to some other Node or Port. These are a very useful way of querying, and operating on, your model. Paths are discussed in a separate page.

TOP


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 Database Visualizer application:

{
  "id":"book",
  "name":"Book",
  "type":"table",
  "columns":[
    { "id":"id", "datatype":"integer", "primaryKey":true },
    { "id":"isbn", "datatype":"varchar" },
    { "id":"title", "datatype":"varchar" }
  ]
}

The Toolkit has its own internal representation of Nodes, Edges and Ports, but your original data is always stored under the data member - this is the Node from above:

{
  "id":"456",
  "data":{
    "id":"book",
    "name":"Book",
    "type":"table",
    "columns":[
      { "id":"id", "datatype":"integer", "primaryKey":true },
      { "id":"isbn", "datatype":"varchar" },
      { "id":"title", "datatype":"varchar" }
    ]
  }
}
Port Data

In the Node data given above there is a columns array - each entry in this array maps to a Port on the Node representing that table. The IDs of the Ports here are the id members from each column in the array.

TOP


Node, Group, Edge and Port IDs

Every Node is required to have a unique ID. The Toolkit attempts to derive this automatically from your data, by looking for an id member - this is why in the above example the Toolkit's ID is the same as the ID from the original data. Well, it's not exactly the same - the Toolkit converts all IDs into Strings internally.

Should you wish to implement a different strategy, though, you can just supply your own idFunction to the method you use to get an instance of the Toolkit:

var toolkit = jsPlumbToolkit.newInstance({
  idFunction:function(data) {
    return SomeCustomComputing(data);
  }
});

Again note this is optional - you do not need to supply this function, but if you do not then the Toolkit will expect an id member in your data. Remember, though, the Toolkit converts the ID you supply into a String for internal use.

Group Ids

IDs for Groups are derived using whatever method the Toolkit 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 ID Optional

Edges are not required to have an ID, but you might find it useful to supply one. If you do not supply an ID, the Toolkit will assign one automatically.

Port and Edge ID functions

You can also supply separate functions to use for extracting the ID for a Port or Edge from some backing data:

var toolkit = jsPlumbToolkit.newInstance({
  idFunction:function(data) {
    return SomeCustomComputing(data);
  },
  portIdFunction:function(data) {
    return PortComputations(data);
  },
  edgeIdFunction:function(data) {
    return EdgeComputations(data);
  }
});

If you omit either of these, the Toolkit uses whatever it is using to derive Node IDs (either the idFunction, if provided, or the default mechanism of looking for an id member).

Port ID Uniqueness

Port IDs are required to be unique on the Node on which the Port exists, but may be the same as the ID of a Port on some other Node.

Referencing Ports by ID

When adding an Edge to a Toolkit 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:

toolkit.connect({
  source:"book.id", 
  target:"book_author.book_id"
});

book_author is another table in the schema from the Database Visualizer application.

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 the Toolkit will use by doing this:

var tk = jsPlumbToolkit.newInstance({
  portSeparator:"#"
});

TOP


Extracting Port Data from Nodes

When you load data into an instance of the Toolkit and you have Nodes that themselves have Ports (consider the Database Visualizer demo app - a table is a Node, and the columns on that table are Ports), the data describing your Ports will typically be held within the Node data. For instance, in the Database Visualizer, a Node looks like this:

{
  "id":"book",
  "name":"Book",
  "type":"table",
  "columns":[
    { "id":"name", "type":"varchar" },
    { "id":"id", "type":"integer" }
  ]
}

The entries in the columns array are the Ports for this Node. In order to get the Toolkit to recognise these Ports when using the load function, you should define a portExtractor in the jsPlumbToolkit.getInstance function:

var toolkit = jsPlumbToolkit.getInstance({
  ...
  portExtractor:function(node) { return node.columns; }
};

Every Node that is loaded is passed through this function, whose return value must be an array of Port objects. These Port objects are then registered against the given Node.

TOP


Node, Group, Edge and Port Type

You can associate a type with each of your Nodes, Groups, Edges and Ports. This is an important concept in the Toolkit, as it is the basic means by which the data model is bound to any renderers, via Views.

As with Node/Edge/Port ID, the type of a Node, Group, Edge or Port can be derived using a default function - which looks for a type member in the backing data, or you can supply a typeFunction to the newInstance method:

var toolkit = jsPlumbToolkit.newInstance({
  idFunction:function(data) {
    return SomeCustomComputing(data);
  },
  typeFunction:function(data) {
    return SomeOtherComputing(data);
  },
  edgeTypeFunction:function(data) {
    return EdgeTypeComputing(data);
  },
  portTypeFunction:function(data) {
    return PortTypeComputing(data);
  }
});

As with ID, omitting either the Port or Edge type functions will cause the Toolkit to use whatever function it has determined it should use for deriving Node types. Also, as with ID, the Toolkit will derive Group types via the same means it is using to derive Node types.

The type of some Node, Group, Port or Edge is used by the render model in a Surface, to determine the appearance and behaviour of the object. For a discussion of render models, see here.

TOP


Data Factories

Node Factory

When you need to add a new Node programatically, you do so on the Toolkit instance using the addNode function:

addNode : function(data) {
  ...
}

It is expected that the current idFunction and typeFunction will be able to determine appropriate values from the data you provide. An example might be something like the following:

toolkitInstance.addNode({
  id:"place",
  name:"Place",
  columns:[
    { id:"id", name:"Id", primaryKey:true, datatype:"integer" },
    { id:"name", name:"Name", datatype:"varchar" },
    { id:"lat", name:"Latitude", datatype:"float" },
    { id:"lng", name:"Longitude", datatype:"float" }
  ]
})

If, however, you are using a drag and drop mechanism to add new Nodes to the UI (see the page on Rendering), the Toolkit needs some way of getting the data for a new Node. By default, the Toolkit will create a JS object with an id member set to a new UUID, but you can provide a NodeFactory in order to gain control over the creation of the data.

A NodeFactory is a function with this signature:

function myNodeFactory (type, data, callback, originalEvent, isNative);
  • type is the type of Node to create.
  • data is the data appropriate to the given Node type; in the case of drag and drop it has been generated elsewhere (again see the page on Rendering).
  • callback is a function that your code must call once it has the data for the new Node prepared - having a callback rather than returning a value enables you to optionally prepare the data asynchronously, perhaps via a round trip to the server. If you do not call this function, addition of the new Node is aborted (which you might want to do, sometimes, of course).
  • originalEvent This is the browser event related to the drop. If isNative is set, you might wish to access the dataTransfer member of this event when deciding how to populate the data object.
  • isNative Set to true if the drop event was "native", for example a file dragged into the browser from the user's desktop or file system.
Direct Callback

Here's an example of a direct callback:

var toolkit = jsPlumbToolkit.getInstance({
    nodeFactory:function(type, data, callback, evt, native) {
        callback({
            someKey:"aValue",
            someArray:[ 1, 2, 3 ]
        });
    }
};
Ajax Callback

You might do this instead if you have jQuery in your page and you want to get your new Node from the server:

var toolkit = jsPlumbToolkit.getInstance({
    nodeFactory:function(type, data, callback, evt, native) {
        $.ajax({
            url:"/get/new/" + type,
            success:callback
        });
    }
};
Native Drop

You might do this if it's a native drop:

var toolkit = jsPlumbToolkit.getInstance({
    nodeFactory:function(type, data, callback, evt, isNative) {
        if (isNative) {
          data.name = evt.dataTransfer.files[0].name;
          data.size = evt.dataTransfer.files[0].size;
          data.type = evt.dataTransfer.files[0].type;
        }
        callback(data);
    }
};
Calling the NodeFactory directly

You can call the NodeFactory directly using the addFactoryNode method:

toolkit.addFactoryNode("someType");

This will result in a call to the NodeFactory with the type someType.

It is possible to also provide some seed data for the new Node:

toolkit.addFactoryNode("someType", { foo:"bar" });

And also you can provide a callback which will be run after the NodeFactory has finished adding the new Node:

toolkit.addFactoryNode("someType", { foo:"bar" }, function(node) {
    // node is your new Node.
});

Group Factory

A Group Factory works in exactly the same way as a Node Factory. Here's the setup of the Toolkit in the groups demonstration that ships with the Toolkit:

var toolkit = jsPlumbToolkit.newInstance({
    groupFactory:function(type, data, callback) {
        data.title = "Group " + (toolkit.getGroupCount() + 1);
        callback(data);
    },
    nodeFactory:function(type, data, callback) {
        data.name = (toolkit.getNodeCount() + 1);
        callback(data);
    }
});

In this app we simply assign a label that has the Group/Node index in it.

Port Factory

If your data model is sufficiently complex, you will be making use of Ports. In the same way that the Toolkit needs to be able to assemble a JS object for each new Node created through user interaction, it needs a way to assemble a JS object for a new Port.

The Toolkit offers two methods for adding new Ports:

addPort = function(node, data) { ... }

and

addNewPort = function(node, type, portData) { ... }

The first of these - addPort - takes some existing data as argument, from which it will derive an ID and type (from the current idFunction and typeFunction) and then add the Port to the given Node.

The second function takes only the type of the Port to add, which it hands off to the Port Factory in order to get a data object for the new Port. As with Nodes, if no PortFactory is defined then a simple JS object containing an id parameter is created.

Here's the PortFactory from the Database Visualizer example application - remember, in this application, each column in a table is mapped to a port:

function(node, type, data, callback) {
  var column = { 
    id:data.columnName, 
    name:data.columnName[0].toUpperCase() + data.columnName.slice(1),  
    datatype:"varchar",
    type:"column"
  };
  // add to node's data. we have to do this manually. the Toolkit does not know our internal
  // data structure.
  node.data.columns.push(column);
  // handoff the new column.
  callback(column);   
}

Note the signature is slightly different for a PortFactory: it is given the Node to which the Port should be added, and it is not given a browser event or the isNative flag.

Where does data come from in the code snippet above? In the Database Visualizer app, we use event delegation to listen to clicks on a certain button, then popup a dialog requesting input from the user. If the user enters data and presses OK, we inform the Toolkit via the addNewPort method:

// add new column to table
jsPlumb.on(document, "tap", ".new-column i", function() {
  var info = renderer.getObjectInfo(this); // getObjectInfo is a helper method that retrieves the node or port associated with some element in the DOM.

  jsPlumbToolkit.Dialogs.show({
    id:"dlgName",
    title:"Enter new column name:",
    onOK:function(data) {
    // if the user supplied a column name, tell the toolkit to add a new port. This will result in a callback to the portFactory defined above.
    if (data.name) {
      if (data.name.length < 3)       
        alert("Column names must be at least 3 characters!")
      else
        toolkit.addNewPort(info.id, "column", { id:data.name.toLowerCase(), columnName:data.name });
      }
    }
  });
});

The dialog code shown in this snippet is discussed here.

addNewPort takes four values:

  • The Node to add the Port to (either an ID or a Node itself)
  • The type of Port to add
  • A JS object containing data for the new Port.

Edge Factory

The Edge Factory is the function that the Toolkit will call whenever a new Connection is established between two Nodes/Ports, via user mouse action. It follows the same pattern as Nodes and Ports - if there is no EdgeFactory defined then a default JS object will be used. The EdgeFactory is only ever called when the user creates a new Edge with the mouse. It is not called if you make a programmatic call tojsPlumbToolkitInstance.addEdge.

None of the examples that ship with the Toolkit use the EdgeFactory, but the Database Visualizer contains this stub:

edgeFactory : function(params, data, callback) {
  // you must hit the callback if you provide the edgeFactory.
  callback(data);
  // unless you want to return false, to abandon the edge
  //return false;
}

Here, we could manipulate the contents of data before we hit the callback if we wanted to.

TOP


Constraining Connectivity

When you instantiate a Toolkit, there are various interceptor functions that you can provide, to constrain the connectivity in your application. An example (from the Database Visualizer app):

var toolkit = jsPlumbToolkit.newInstance({
  ...
  beforeConnect:function(source, target, edgeData) {
    return source !== target && source.getNode() !== target.getNode();
  }
});

beforeConnect is called when the user drags and drops a new connection. The DatabaseVisualizer connects Ports; this interceptor checks that the source and target Ports are not the same, and then that the source and target Ports are not on the same Node.

The full set of interceptors you can provide is:

beforeConnect(source, target, edgeData)

This interceptor is called when the user drags and drops a new connection, as well as when connect is called on a Toolkit instance.

beforeConnect is given the source (a Node or Port) and target (a Node or Port) of a newly dragged Edge, as well as, optionally, data associated with the Edge. Data may be associated with an Edge via the beforeStartConnect interceptor.

Returning anything other than true from this method discards the new Edge.

beforeStartConnect(source, edgeType)

This is called as the user begins to drag a new Edge, as well as programmatically when the user calls connect on a Toolkit instance. This interceptor behaves slightly differently from the others: returning false, as with the others, will abort the action (in this case, it will abort the new connection drag). Returning true will allow the connection to drag to continue. But you can also return some value other than true, in which case dragging will continue and your return value will become the new Edge's backing data. If you do return some data from this method, it will be passed in to the beforeConnect interceptor as the edgeData parameter.

beforeMoveConnection(source, target, edge)

This interceptor is called prior to any existing edge being moved (either programmatically or via the mouse). It is passed the source and target of the existing edge, as well as the edge itself, and if it returns anything other than boolean true, the edge move is aborted. If not supplied, the default behaviour of this function is to honor the allowLoopback, allowNodeLoopback and maxConnections parameters on any Node or Port definitions you supplied to this Toolkit via the model.

beforeDetach(source, target, edge)

This interceptor can be used to override connection detachment from the mouse. The function is given (source, target, edge) and is expected to return true to confirm that the detach should occur.

beforeStartDetach(source, target, edge)

This interceptor can also be used to override connection detachment from the mouse, but it is distinct from beforeDetach in that this function is called as soon as the user begins to drag. The function is given (source, target, edge) and is expected to return true to confirm the detach should occur. Any other return value will abort the detach.