Custom edge styles

May 19th, 2008

An edge style is a property of an edge that computes a set of points (or a single point) whenever the edge is changed. Default edge styles are located in the mxEdgeStyle class. To write a custom edge style, a function must be added to the mxEdgeStyle class (or object in JavaScript) as follows:

mxEdgeStyle.MyStyle = function(state, source, target, points, result)
{
if (source != null && target != null)
{
var pt = new mxPoint(target.getCenterX(), source.getCenterY());

if (mxUtils.contains(source, pt.x, pt.y))
{
pt.y = source.y + source.height;
}

result.push(pt);
}
};

In the above example, a right angle is created using a point on the horizontal center of the target vertex and the vertical center of the source vertex. The code checks if that point intersects the source vertex and makes the edge straight if it does. The point is then added into the result array, which acts as the return value of the function.

The custom edge style above can be used in a specific edge as follows:

model.setStyle(edge, “edgeStyle=mxEdgeStyle.MyStyle”);

Or it can be used for all edges in the graph as follows:

var style = graph.getStylesheet().getDefaultEdgeStyle();
style[mxConstants.STYLE_EDGE] = mxEdgeStyle.MyStyle;

Events

December 3rd, 2007

mxGraph supports three types of events, namely DOM events, JavaScript events and graph events (gestures), which are higher-level events that consist of a specific sequence of DOM events.

DOM events are handled using the mxEvent class, which is a singleton with cross-browser functionality for DOM event handling. It also takes care of resolving cycles between DOM nodes and JavaScript event handlers, which can lead to memory leaks in IE6.

JavaScript events are implemented using the mxEventSource base class. It allows adding or removing handlers and firing named events. A handler in this context is a function with a specific list of arguments. There is no object representation for the actual event.

Graph events are a sequence of mousedown, mousemove, and mouseup events that are redirected to the graph event dispatch loop. A handler in this context is an object that provides a list of methods as defined by the mxGraph class.

mxObjectCodec

September 7th, 2007

The generic mxObjectCodec implements a mapping between JavaScript objects and XML nodes that maps each field or element to an attribute or child node, and vice versa.

Atomic Values

Consider the following example:

var obj = new Object();
obj.foo = “Foo”;
obj.bar = “Bar”;

This object is encoded into an XML node using the following:

var enc = new mxCodec();
var node = enc.encode(obj);

The output of the encoding may be viewed using mxLog as follows:

mxLog.setVisible(true);
mxLog.debug(mxUtils.getPrettyXml(node));

Finally, the result of the encoding looks as follows:

<Object foo=”Foo” bar=”Bar”/>

In the above output, the foo and bar fields have been mapped to attributes with the same names, and the name of the constructor was used for the nodename.

Booleans

Since booleans are numbers in JavaScript, all boolean values are encoded into 1 for true and 0 for false. The decoder also accepts the string true and false for boolean values.

Objects

The above scheme is applied to all atomic fields, that is, to all non-object fields of an object. For object fields, a child node is created with a special attribute that contains the fieldname. This special attribute is called “as” and hence, as is a reserved word that should not be used for a fieldname.

Consider the following example where foo is an object and bar is an atomic property of foo.

var obj = {foo: {bar: “Bar”}};

This will be mapped to the following XML structure by mxObjectCodec:

<Object>
<Object bar=”Bar” as=”foo”/>
</Object>

In the above output, the inner Object node contains the as-attribute that specifies the fieldname in the enclosing object. That is, the field foo was mapped to a child node with an as-attribute that has the value foo.

Arrays

Arrays are special objects that are either associative, in which case each key, value pair is treated like a field where the key is the fieldname, or they are a sequence of atomic values and objects, which is mapped to a sequence of child nodes. For object elements, the above scheme is applied without the use of the special as-attribute for creating each child. For atomic elements, a special add-node is created with the value stored in the value-attribute.

For example, the following array contains one atomic value and one object with a field called bar. Furthermore it contains two associative entries called bar with an atomic value, and foo with an object value.

var obj = [”Bar”, {bar: “Bar”}];
obj[”bar”] = “Bar”;
obj[”foo”] = {bar: “Bar”};

This array is represented by the following XML nodes:

<Array bar=”Bar”>
<add value=”Bar”/>
<Object bar=”Bar”/>
<Object bar=”Bar” as=”foo”/>
</Array>

The Array node name is the name of the constructor. The additional as-attribute in the last child contains the key of the associative entry, whereas the second last child is part of the array sequence and does not have an as-attribute.

References

Objects may be represented as child nodes or attributes with ID values, which are used to lookup the object in a table within mxCodec. The isReference function is in charge of deciding if a specific field should be encoded as a reference or not. Its default implementation returns true if the fieldname is in idrefs, an array of strings that is used to configure the mxObjectCodec.

Using this approach, the mapping does not guarantee that the referenced object itself exists in the document. The fields that are encoded as references must be carefully chosen to make sure all referenced objects exist in the document, or may be resolved by some other means if necessary.

For example, in the case of the graph model all cells are stored in a tree whose root is referenced by the model’s root field. A tree is a structure that is well suited for an XML representation, however, the additional edges in the graph model have a reference to a source and target cell, which are also contained in the tree. To handle this case, the source and target cell of an edge are treated as references, whereas the children are treated as objects. Since all cells are contained in the tree and no edge references a source or target outside the tree, this setup makes sure all referenced objects are contained in the document.

In the case of a tree structure we must further avoid infinite recursion by ignoring the parent reference of each child. This is done by returning true in isExcluded, whose default implementation uses the array of excluded fieldnames passed to the mxObjectCodec constructor.

References are only used for cells in mxGraph. For defining other referencable object types, the codec must be able to work out the ID of an object. This is done by implementing the reference function in mxCodec. For decoding a reference, the XML node with the respective id-attribute is fetched from the document, decoded, and stored in a lookup table for later reference. For looking up external objects, the lookup function in mxCodec may be implemented.

Expressions

For decoding JavaScript expressions, the add-node may be used with a text content that contains the JavaScript expression. For example, the following creates a field called foo in the enclosing object and assigns it the value of mxConstants.ALIGN_LEFT.

<Object>
<add as=”foo”>mxConstants.ALIGN_LEFT</add>
</Object>

The resulting object has a field called foo with the value “left”. Its XML representation looks as follows:

<Object foo=”left”/>

This means the expression is evaluated at decoding time and the result of the evaluation is stored in the respective field. Valid expressions are all JavaScript expressions, including function definitions, which are mapped to functions on the resulting object.

Subclassing in mxGraph

September 1st, 2007

In JavaScript, there are various ways of mapping the Object Oriented paradigm to language constructs. mxGraph uses a particular scheme throughout the project, with the following implicit rules:

  1. Do not change built-in prototypes
  2. Do not limit the power of JavaScript

There are two types of “classes” in mxGraph: classes and singletons (where only one instance exists). Singletons are mapped to global objects where the variable name equals the classname. For example mxConstants is an object with all the constants defined as object fields. Normal classes are mapped to a constructor function and a prototype which defines the instance fields and methods. For example, mxEditor is a function and mxEditor.prototype is the prototype for the object that the mxEditor function creates. The mx prefix is a convention that is used for all classes in the mxGraph package to avoid conflicts with other objects in the global namespace.

For subclassing, the superclass must provide a constructor that is either parameterless or handles an invocation with no arguments. Furthermore, the special constructor field must be redefined after extending the prototype. For example, the superclass of mxEditor is mxEventSource. This is represented in JavaScript by first “inheriting” all fields and methods from the superclass by assigning the prototype to an instance of the superclass, eg. mxEditor.prototype = new mxEventSource() and redefining the constructor field using mxEditor.prototype.constructor = mxEditor. The latter rule is applied so that the type of an object can be retrieved via the name of it’s constructor using mxUtils.getFunctionName(obj.constructor).

Constructor

For subclassing in mxGraph, the same scheme should be applied. For example, for subclassing the mxGraph class, first a constructor must be defined for the new class. The constructor calls the super constructor with any arguments that it may have using the call function on the mxGraph function object, passing along explitely each argument:

function MyGraph(container)
{
mxGraph.call(this, container);
}

The prototype of MyGraph inherits from mxGraph as follows. As usual, the constructor is redefined after extending the superclass:

MyGraph.prototype = new mxGraph();
MyGraph.prototype.constructor = MyGraph;

You may want to define the codec associated for the class after the above code. This code will be executed at class loading time and makes sure the same codec is used to encode instances of mxGraph and MyGraph.

var codec = mxCodecRegistry.getCodec(mxGraph);
codec.template = new MyGraph();
mxCodecRegistry.register(codec);

Functions

In the prototype for MyGraph, functions of mxGraph can then be extended as follows.

MyGraph.prototype.isSelectable = function(cell)
{
var selectable = mxGraph.prototype.isSelectable.apply(this, arguments);

var geo = this.model.getGeometry(cell);
return selectable && (geo == null || !geo.relative);
}

The supercall in the first line is optional. It is done using the apply function on the isSelectable function object of the mxGraph prototype, using the special this and arguments variables as parameters. Calls to the superclass function are only possible if the function is not replaced in the superclass as follows, which is another way of “subclassing” in JavaScript.

mxGraph.prototype.isSelectable = function(cell)
{
var geo = this.model.getGeometry(cell);
return selectable &&
(geo == null ||
!geo.relative);
}

The above scheme is useful if a function definition needs to be replaced completely.

In order to add new functions and fields to the subclass, the following code is used. The example below adds a new function to return the XML representation of the graph model:

MyGraph.prototype.getXml = function()
{
var enc = new mxCodec();
return enc.encode(this.getModel());
}

Fields
Likewise, a new field is declared and defined as follows:

MyGraph.prototype.myField = ‘Hello, World!’;

Note that the value assigned to myField is created only once, that is, all instances of MyGraph share the same value. If you require instance-specific values, then the field must be defined in the constructor instead. For example:

function MyGraph(container)
{
mxGraph.call(this, container);
this.myField = new Array();
}

Finally, a new instance of MyGraph is created using the following code, where container is a DOM node that acts as a container for the graph view:

var graph = new MyGraph(container);

Creating New Connections

July 16th, 2007

When creating new connections in mxGraph, the mxConnectionHandler is typically used by setting graph.setConnectable(true). This handler uses mxTerminalMarker to find the source and target cell for the new connection and creates a new edge using the connect hook function. The new edge is created using the createEdge hook function which in turn uses the handler’s factoryMethod or creates a new default edge.

The handler uses a “highlight-paradigm” for indicating if a cell is being used as a source or target terminal, as seen in MS Visio and other products. In order to allow both, moving and connecting cells at the same time, the mxConstants.ACTIVE_REGION is used in the mxConnectionHandler to determine the hotspot of a cell, that is, the region of the cell which is used to trigger a new connection. The constant is a value between 0 and 1 that specifies the amount of the width and height around the center to be used for the hotspot of a cell and its default value is 0.5. In addition, the MIN_ACTIVE_REGION constant defines the minimum number of pixels for the width and height of the hotspot.

This solution, while standards compliant, may be somewhat confusing because there is no visual indicator for the hotspot and the highlight is seen to switch on and off while the mouse is being moved in and out. Furthermore, this paradigm does not allow to create different connections depending on the highlighted hotspot as there is only one hotspot per cell and it normally does not allow cells to be moved and connected at the same time as there is no clear indication of the connectable area of the cell.

To come across these issues, the mxConnectionHandler in mxGraph 0.9.12.0 and later has an additional createIcons hook with a default implementation that allows to create one icon to be used to trigger new connections. If this icon is specified, then new connections can only be created if the image is clicked while the cell is being highlighted. The createIcons hook may be overridden to create more than one mxImageShape for creating new connections, but the default implementation supports one image and is used as follows:

In order to display the “connect image” whenever the mouse is over the cell, an ACTIVE_REGION of 1 should be used:

mxConstants.ACTIVE_REGION = 1;

In order to avoid confusion with the highlighting, the highlight color should not be used with a connect image:

mxConstants.HIGHLIGHT_COLOR = null;

To install the image, the connectImage field of the mxConnectionHandler must be assigned a new mxImage instance:

mxConnectionHandler.prototype.connectImage = new mxImage(’images/green-dot.gif’, 14, 14);

This will use the green-dot.gif with a width and height of 14 pixels as the image to trigger new connections. In createIcons the icon field of the handler will be set in order to remember the icon that has been clicked for creating the new connection. This field will be available under selectedIcon in the connect method, which may be overridden to take the icon that triggered the new connection into account. This is useful if more than one icon may be used to create a connection.

Boolean Decoding Problem

June 12th, 2007

In release 0.9.11.0 and later, the keywords “true” and “false” in XML are no longer converted into boolean values when decoded. The reason being that in JavaScript, the boolean values true and false are equal to the numeric values 1 and 0, respectively.

This means, converting a value of “true” or “false” to a numeric value of 1 or 0 may lead to undesired results if the values are intended to be strings, whereas the conversion of 1 or 0 into booleans does not require any conversion in JavaScript, other than from string to numeric values, which is considered less harmfull.

This problem does not exist in strongly typed languages such as Java and C#, as the type of the decoded values may be deduced from the class definitions. (In the case of a dictionary or hashtable the conversion is “best effort”.)

In order to upgrade from earlier releases to 0.9.11.0 and later, there are a number of places where this change must be taken into account.

The most obvious change is for all XML files, namely config files and diagram files, where all occurrences of “true” and “false” must be changed to “1″ or “0″, respectively. (Only occurrences of boolean values must be changed, the string value true and false must not be changed.)

Here is an example:

<mxEditor layoutDiagram=”true” …

must be changed to:

<mxEditor layoutDiagram=”1″ …

The same holds for diagram files, eg:

<mxCell vertex=”true” …

must be changed to:

<mxCell vertex=”1″ …

Consequently, when changing all occurrences of the string true and false to booleans in the object model, the stylesheet definition changes from containing strings to actual booleans for switches such as STYLE_SHADOW, STYLE_ROUNDED etc.

This means that, in code, the quotes around all style-definitions to “true” or “false” must be removed. In other words: All programmatical definitions of boolean styles must be assigned boolean values. Here is an example:

style[mxConstants.STYLE_ROUNDED] = “true”;

must be changed to:

style[mxConstants.STYLE_ROUNDED] = true;

Keep in mind that the config files may also contain code! Finally, the cell styles of the form stylename[;key=value] must also reflect this change. That is, the keywords true and false must be replaced with 1 and 0, respectively. Here is an example:

connector;rounded=true

must be changed to:

connector;rounded=1

Simple Drag and Drop

May 15th, 2007

With the new mxUtils.makeDraggable function it is possible to make any DOM node draggable. This can be useful if you do not want to use the built-in toolbar or if you want to provide additional shapes to be inserted into the graph.

To use the function, you need a DOM node which acts as the source of the drag, the target graph and a function that executes the insert. An optional DOM node may be given for the preview while dragging. If no preview is given, then the source DOM node is cloned and used for the preview.

Below is a typical function used for inserting a new vertex into a graph. The graph, event and cell under the mousepointer is passed to the function at invocation time, that is, when the element is dropped on the graph. The cell may be ignored or used to insert the vertex as a child or neighbour, depending on the requirements.

var funct = function(graph, evt, cell)
{
var pt = graph.getPointForEvent(evt);
var parent = graph.getDefaultParent();
var model = graph.getModel();
var v1 = null;

try
{
model.beginUpdate();
v1 = model.addVertex(parent, null, ‘Hello, World!’, pt.x, pt.y, 120, 120);
}
finally
{
model.endUpdate();
}
graph.setSelectionCell(v1);
}

For the other arguments, you can use any DOM node. In the example below, an image is used as the source of the drag and a slightly larger clone of the image is used as the preview for the drag operation:

var img = document.createElement(’img’);
img.setAttribute(’src’, ‘editors/images/rectangle.gif’);
img.style.position = ‘absolute’;
img.style.left = ‘0px’;
img.style.top = ‘0px’;
img.style.width = ‘16px’;
img.style.height = ‘16px’;

var dragImage = img.cloneNode(true);
dragImage.style.width = ‘32px’;
dragImage.style.height = ‘32px’;

The last step is to invoke mxUtils.makeDraggable with these arguments and insert the img into the DOM:

mxUtils.makeDraggable(img, graph, funct, dragImage);
document.body.appendChild(img);

Improved Startup Time

April 25th, 2007

For improved startup time of the client it is recommend to compress the mxClient.js file using gzip in production environments. On *nix systems, the gzip command may be used to compress the file. Since there have been reports that the compressed library is not always downloaded properly we also recommend to keep the uncompressed file for a fallback in case the compressed file cannot be loaded.

Use the following commands to create a copy of the mxClient.js file, compress it, and move the copy back to the old filename:

cp mxClient.js mxClient.js.tmp
gzip mxClient.js
mv mxClient.js.tmp mxClient.js

In the HTML files, the reference to the mxClient library must be changed as follows. The second script tag contains the fallback in case the compressed library was not loaded into the browser correctly:

<script type=”text/javascript”
src=”../src/js/mxClient.js.gz”></script>
<script type=”text/javascript”>
if (typeof(mxClient) == ‘undefined’)
{
if (navigator.appName.toUpperCase() ==
‘MICROSOFT INTERNET EXPLORER’)
{
document.write(’<script src=”../src/js/mxClient.js”></’+’script>’);
}
else
{
var script = document.createElement(’script’);

script.setAttribute(’type’, ‘text/javascript’);
script.setAttribute(’src’, ‘../src/js/mxClient.js’);

var head = document.getElementsByTagName(’head’)[0];
head.appendChild(script);
}
}
</script>

You may have to change the location of the mxClient library to match the mxBasePath directive. Note that the compressed library may only be loaded from a webserver, that is, the client must be started from an http:// location.

Multiplicities and Validation

April 5th, 2007

To control the possible connections in mxGraph, the getEdgeValidationError function is used. The default implementation of this function uses the mxGraph.multiplicities field, which is an array of mxMultiplicity. Using this class allows to establish simple multiplicities, which are enforced by the graph.

The mxMultiplicity uses mxCell.is to determine for which terminals it applies. The default implementation of the is-function in mxCell works with DOM nodes (XML nodes) and checks if the given type parameter matches the nodeName of the node (case insensitive). Optionally, an attributename and value can be specified which are also checked.

The getEdgeValidationError is called whenever the connectivity of an edge changes. It returns an empty string or an error message if the edge is invalid or null if the edge is valid. If the returned string is not empty then it is displayed as an error message.

The mxMultiplicity allows to specify the multiplicity between a terminal and its possible neighbors. For example, if any rectangle may only be connected to, say, a maximum of two circles you can add the following rule to mxGraph.multiplicities:

graph.multiplicities.push(new mxMultiplicity(
true, ‘rectangle’, null, null, 0, 2, [’circle’],
‘Only 2 targets allowed’,
‘Only shape targets allowed’));

This will display the first error message whenever a rectangle is connected to more than two circles and the second error message if a rectangle is connected to anything but a circle.

For certain multiplicities, such as a minimum of 1 connection, which cannot be enforced at cell creation time (unless the cell is created together with the connection), mxGraph offers the validate method which checks all multiplicities for all cells and displays the respective error messages in an overlay icon on the cells.

If a cell is collapsed and contains validation errors, a respective warning icon is attached to the collapsed cell.

Command History and Diagram Sharing

March 26th, 2007

The graph is stored in an object representation in memory in all browsers. The state of the graph elements is stored in mxCells, the interface to access the graph is the mxGraphModel. SVG and VML is only used to render the graph based on these objects, but it is not used for storing the graph data or representing undo or state information.

Command History

When changing the graph model, an mxUndoableChange object is created at the start of the transaction (when model.beginUpdate is called). All atomic changes are then added to this object until the last model.endUpdate call, at which point the mxUndoableEdit is dispatched in an event, and added to the history inside mxUndoManager. This is done by an event listener in mxEditor.installUndoHandler.

Each atomic change of the model is represented by an object (eg. mxRootChange, mxChildChange, mxTerminalChange etc) which contains the complete undo information. The mxUndoManager also listens to the mxGraphView and stores it’s changes to the current root as insignificant undoable changes, so that drilling (step into, step up) is undone.

This means when you execute an atomic change on the model, then change the current root on the view and click undo, the change of the root will be undone together with the change of the model so that the display represents the state at which the model was changed. However, these changes are not transmitted for sharing as they do not represent a state change.

Diagram Sharing

The diagram sharing is a mechanism where each atomic change of the model is encoded into XML using mxCodec and then transmitted to the server by the mxSession object. On the server, the XML data is dispatched to each listener on the same diagram (except the sender), and the XML is decoded back into atomic changes on the client side, which are then executed on the model and stored in the command history.

The mxSession.significantRemoteChanges specifies how these changes are treated with respect to undo: The default value (true) will undo the last change regardless of whether it was a remote or a local change. If the switch is false, then an undo will go back until the last local change, silently undoing all remote changes up to that point. Note that these changes will be added as new remote changes to the history of the other clients.

The changes are executed on the client while any interactive changes are in progress and are animated in the examle application so that the user gets a better idea of what has changed. There is no higher-level locking on these changes and the architecture is setup so that no changes are lost.