A step by step dive into the Kap Lab Diagrammer component
Introduction
This documentation present how to integrate diagramming functionality in your AS3 application:
Adding a SVG library in a flex application
Adding layout to a diagram
Binding data to a diagram
Sprites composition in a diagram
We use Flex for this tutorial, but because the Diagrammer is a pure AS3 component, it can also be integrated into Flash AS3 applications.
Keep in mind that the samples used in this documentation are neither meant to be useful nor in phase with software development quality standards. For the sake of simplicity and to keep focus on the original goal which is showing how to integrate the Diagrammer, we tried to reduce the amount of code artifact. So we put all the code we can into a single mxml file, and created as little class as possible, using generic data container such as ArrayCollections. In real world software, you should use a MVC framework, create specific class to hold your data, and so on.
You can download a complete solution with all the code from this documentation if you don't want to type.
Adding a SVG library in a flex application
In this chapter we will show how to write a very simple application and with a few lines of code add a diagramming functionality.
First we need to create the sprite library that will be used in the application.
Diagrammer uses SVG to define sprites. Using SVG helps easily integrate complex shapes into Diagrammer. It also gives the opportunity to export a diagram in SVG for printing.
Diagrammer supports a subset of SVG 1.1. Most of the features needed for sprite definitions are supported :
To define sprites, Diagrammer uses extensions to standard SVG. They are defined in the namespace "http://schemas.kapit.fr/svg/2007/". You must add to the <svg> tag in your library the following attribute : xmlns:k="http://schemas.kapit.fr/svg/2007/" .
Each sprite must be defined as a group (<g> tag), with the followings attributes :
k:spriteid defines the identifier of the sprite. It is used in Diagrammer as a unique identifier for a specific shape.
k:groupid defines the group of the sprite. Diagrammer provides a method to retrieve all sprite of a given group. This functionality can use by components (drop down menu, panels) that enable user to select any sprite from a given group.
Using css styling, we will define how can the user interact with each sprite.
To enable input links to a part of a sprite, we must add the style action-accept:link to this part (it can be a group or any basic drawing instruction).
To enable output links for a part of a sprite, we must add the style action-click:link to this part.
To enable annotation for a part of a sprite, we must add the action-accept:annotation to this part. We usually put this in the group that defines the sprite.
As a basic rule, it is better to add a dedicated transparent path to define the link anchors, but depending on the sprite we can use part of sprite.
In this sample we will use a two sprite library. We define two basic shapes, a rectangle and a rounded rectangle. Using a vector based image editor that support SVG export (such as Inkscape), we can define more complex sprite. Be sure to remove all non standard tags from the SVG library before using it in Diagrammer.
We will use a SVG file generated by Inkspace to create a simple rectangle with a linear gradient and a rounded rectangle :
We defined basic and bg style. Basic style will be applied to a sprite to enable annotations. Bg style will be applied to anchor part.
The system style annotation and link-annotation are overloaded to fit our taste.
Then for each sprite we modify the group to set spriteid and groupid.
Now the library is ready. We can open it with any SVG editor to check that it still renders as intended (but we shouldn't export it again to avoid adding unwanted tags).
Here is the SVG file (without the <?xml header) :
Enter a name for your Project ("DiagrammerTutorial" for instance). Select Web Application and none for application server type. Press Finish to accept all default options
From the folder where you downloaded it, drag the Diagrammer.swc file into the libs folder in the flex navigator.
Type the following code in the DiagrammerTutorial.mxml file :
Basic Integration Tutorial
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:diagctl="com.kapit.diagram.controls.*"
xmlns:diagview="com.kapit.diagram.view.*"
layout="absolute" preinitialize="init();">
<mx:XML xmlns="" id="svglib">
Paste your svg library in this area...
</mx:XML>
<mx:Script>
<![CDATA[
import com.kapit.diagram.library.SVGAssetLibrary;
public function init():void
{
var lib:SVGAssetLibrary=new SVGAssetLibrary(svglib);
}
public function initDiagram():void
{
diagram.multipanel=false;
diagram.selectionenabled=true;
diagram.keyboardenabled=true;
diagram.dragenabled=true;
diagram.dropenabled=true;
}
]]>
</mx:Script>
<mx:ApplicationControlBar width="100%" height="50" horizontalAlign="right">
<diagctl:SVGAssetLibraryGroupButton width="150" groupid="Basic" cornerRadius="10" paddingLeft="8" paddingBottom="0" paddingTop="0" useHandCursor="true" toolTip="Drag & drop" labelPlacement="right" textAlign="left"/>
</mx:ApplicationControlBar >
<mx:HBox left="10" top="60" bottom="10" right="10" horizontalGap="10">
<diagview:DiagramView width="100%" height="100%" id="diagram" creationComplete="initDiagram();">
</diagview:DiagramView>
</mx:HBox>
</mx:Application>
We removed the SVG part to show that you don't have to write lots of code. The svg library can be found on the previous section of this documentation.
Save the file, compile the project and run the application :
Drag shapes from the upper right menu to the main view. Clicking on an shape shows a upper right graphical menu that enables you to create new objects linked to the selected one.
Including the svg library in the source is not mandatory. For example you can use an URLLoader to load the library from a svg file somewhere on the network.
In the init fonction we simply load the library from the xml. This must be done before the DiagramView object is created.
SVG Including in XML Tag sample
import com.kapit.diagram.library.SVGAssetLibrary;
public function init():void
{
var lib:SVGAssetLibrary=new SVGAssetLibrary(svglib);
}
The application layout is simple. We use an ApplicationControlBar to hold a SVGAssetLibraryGroupButton, and an HBox to hold a DiagramView.
The initDiagram function sets some diagram's options.
initDiagram Function Options Setting Tutorial
public function initDiagram():void
{
diagram.multipanel=false;
diagram.selectionenabled=true;
diagram.keyboardenabled=true;
diagram.dragenabled=true;
diagram.dropenabled=true;
}
Multipanel is set to false so that the diagram doesn't support multipanel view. This option allows the diagram to be divided in verticals panels and lanes.
SelectionEnabled is set to true, so that objects can be selected on the diagram.
KeyboardEnabled is set to true, so that you can scroll with keyboard, enter text, etc.
DragEnabled enables dragging of object in the diagram.
DropEnabled enables dropping of objects in the diagram.
In this part we will add layout functionality to our diagramming application. Diagrammer layouts are powerful algorithms that automatically place sprites and links of a diagram
Now your application supports the radial layout. The Diagrammer has many more built-in layouts such as hierarchical, radial, balloon, organic layouts.
Let's add the animated organic layout to your application.
Add a new button to the Application control bar that calls the doAnimatedLayout function when clicked.
Add the doAnimatedLayout function at the end of the script part
Animated Layout Integration Code
public function doAnimatedLayout():void
{
proxy.exportGraph(Constants.ANIMATED_FORCEDIRECTED_LAYOUT);
}
]]>
Compile and launch the application. Create linked objects, click Animated Layout button, and drag one object. You will see all the other objects following your object in a nice animation.
In this part we will add data binding between the diagram view and application data. This enables developers to propose alternative views of sprites and links of the diagram. Using flex bindings any change in property of sprites and links in the diagram are automatically reported in alternative views.
Data binding from diagram objects to application data uses proxies. To enable binding, a developer must create sprite proxy classes implementing ISpriteProxy interface and link proxy classes implementing ILinkProxy interface. Then he must create a configuration file that links spriteid, links and proxies. Then he must create a DiagramModel with the configuration file and define it as the model of the DiagramView.
When this plumbery is done, everytime a sprite or a link is created, deleted or changed, the corresponding method of the corresponding proxy is called. It is the responsibility of the developer to implements application data manipulation so that application data reflects the states of the diagram.
In this tutorial we will use only one proxy for sprites and one for links. For real world application, you can have specific proxy for some sprites, or have several sprites sharing the same proxy.
From the Flex Navigator src folder, select New / ActionScript Class. Name it MyObjectProxy, and add ISpriteProxy as an interface. This way all methods will be automaticaly created by Flex builder for you.
Add the following code:
Object Proxy Integration Tutorial
package
{
import com.kapit.diagram.IDiagramElement;
import com.kapit.diagram.layers.DiagramLane;
import com.kapit.diagram.model.ISpriteProxy;
import com.kapit.diagram.view.DiagramSprite;
import com.kapit.diagram.view.DiagramView;
import mx.collections.ArrayCollection;
import mx.utils.UIDUtil;
public class MyObjectProxy implements ISpriteProxy
{
publicstaticvar _objects:ArrayCollection = null;
protectedvar _view:DiagramView;
public function MyObjectProxy(view:DiagramView)
{
_view = view;
}
protected function getElementIndex(el:IDiagramElement):int
{
if (el.dataobjectid && _objects)
{
for(var i:int=0; i < _objects.length; i++)
{
var obj:Object = _objects.getItemAt(i);
if (obj.uid == el.dataobjectid)
{
return i;
}
}
}
return -1;
}
public function createDataObject(el:IDiagramElement):String
{
var type:String = el.getTagName();
var spriteid:String = DiagramSprite(el).spriteid;
var name:String = el.did;
var uid:String = UIDUtil.createUID();
var obj:MyObject = new MyObject();
obj.type = type;
obj.spriteid = spriteid;
obj.did = el.did;
obj.uid = uid;
obj.name = "";
if (_objects)
_objects.addItem(obj);
return uid;
}
public function removeDataObject(el:IDiagramElement):void
{
var index:int = getElementIndex(el);
if (index != -1)
_objects.removeItemAt(index);
}
public function allowLinkAction(el:IDiagramElement):Boolean
{
returntrue;
}
public function propertyModified(el:IDiagramElement, propname:String, propvalue:Object, shapeid:String):void
{
var index:int = getElementIndex(el);
var obj:MyObject = null;
if (index != -1)
{
obj = _objects.getItemAt(index) as MyObject;
}
if ("text" == propname)
{
if (obj)
{
obj.name=String(propvalue);
}
}
}
public function preAcceptLinkSource(spriteid:String, sourcespriteid:String, el:IDiagramElement):Boolean
{
returntrue;
}
public function preAcceptLinkTarget(spriteid:String, targetspriteid:String, el:IDiagramElement):Boolean
{
returntrue;
}
public function dataObjectPropertyModified(uid:String, propname:String, propvalue:Object):void
{
}
public function acceptLinkTarget(el:IDiagramElement, target:IDiagramElement):Boolean
{
returntrue;
}
public function dataObjectRemoved(uid:String):void
{
}
public function dataObjectLoaded(el:IDiagramElement):void
{
}
public function acceptLinkSource(el:IDiagramElement, source:IDiagramElement):Boolean
{
returntrue;
}
public function laneChanged(el:IDiagramElement, lane:DiagramLane):void
{
}
public function acceptPropertyModification(el:IDiagramElement, propname:String, propvalue:Object, shapeid:String):Boolean
{
returntrue;
}
public function acceptRemoveObject(el:IDiagramElement):Boolean
{
returntrue;
}
}
}
From the Flex Navigator src folder, select New / ActionScript Class. Name it MyLinkProxy, and add ILinkProxy as an interface. This way all methods will be automatically created by Flex builder for you.
Add the following code:
Link Proxy Integration Tutorial
package
{
import com.kapit.diagram.IDiagramElement;
import com.kapit.diagram.model.ILinkProxy;
import com.kapit.diagram.view.DiagramLink;
import com.kapit.diagram.view.DiagramView;
import mx.collections.ArrayCollection;
import mx.utils.UIDUtil;
public class MyLinkProxy implements ILinkProxy
{
publicstaticvar _links:ArrayCollection = null;
protectedvar _view:DiagramView;
public function MyLinkProxy(view:DiagramView)
{
_view = view;
}
public function createDataObject(el:IDiagramElement):String
{
var type:String = el.getTagName();
var sourceid:String = DiagramLink(el).sourceobject.dataobjectid;
var targetid:String = DiagramLink(el).targetobject.dataobjectid;
var uid:String = UIDUtil.createUID();
var obj:Object = newObject();
obj.start = sourceid;
obj.end = targetid;
obj.uid = uid;
if (_links)
_links.addItem(obj);
return uid;
}
public function scopeChanged(link:DiagramLink, oldscope:String):void
{
}
public function removeDataObject(el:IDiagramElement):void
{
if (el.dataobjectid && _links)
{
for(var i:int=0; i < _links.length; i++)
if (_links.getItemAt(i).uid == el.dataobjectid)
{
_links.removeItemAt(i);
break;
}
}
}
public function propertyModified(el:IDiagramElement, propname:String, propvalue:Object, shapeid:String):void
{
}
public function dataObjectPropertyModified(uid:String, propname:String, propvalue:Object):void
{
}
public function dataObjectRemoved(uid:String):void
{
}
public function dataObjectLoaded(el:IDiagramElement):void
{
}
public function acceptPropertyModification(el:IDiagramElement, propname:String, propvalue:Object, shapeid:String):Boolean
{
returntrue;
}
public function acceptRemoveObject(el:IDiagramElement):Boolean
{
returntrue;
}
}
}
Add imports and application data definition. In this tutorial we will use ArrayCollection as an example, as we don't want to multiply class definitions.
Imports and Application Definition Code
privatevar proxy:KDLProxy;
import com.kapit.diagram.model.DiagramModel;
import MyObjectProxy;
import MyLinkProxy;
import mx.collections.ArrayCollection;
[Bindable]
publicvar myObjects:ArrayCollection = new ArrayCollection();
[Bindable]
publicvar myLinks:ArrayCollection = new ArrayCollection();
public function init():void
In the initDiagram function, set the proxy ArrayCollection and set the diagram model.
Setting ArrayCollection Proxy and Diagram Model Sample
diagram.dropenabled=true;
MyObjectProxy._objects = myObjects;
MyLinkProxy._links = myLinks;
var model:DiagramModel=new DiagramModel(diagramMappings);
diagram.model=model;
proxy = new KDLProxy(diagram);
Data bindings are now activated. You can test it by debugging your application and setting breakpoint in MyObjectProxy and MyLinkProxy methods. You will see that the proxy object is created when the first diagram object is created, and that createDataObject function is called each time an object is created.
The code you wrote add an Object in the myOBjects and myLinks ArrayCollections and keep them synchronized with the diagram sprites and links.
Now we will add some code to use these ArrayCollections.
Build and run the application. Add sprites and links to the diagram, you will see them appear on the corresponding grid. Add annotation to objects and see them reflected in the name column.
Now we have two DataGrids connected to the Diagram. But when you select a sprite or a link, nothing is selected on the grids. And when you select a row on a grid nothing get selected on the Diagram. Let's fix this.
Register to the selection changed of the diagram in the initDiagram function
Selection Changed Registration Code
diagram.model=model;
diagram.addEventListener(DiagramEvent.SELECTION_CHANGED,handleDiagramSelectionChanged);
proxy = new KDLProxy(diagram);
Add the function to handle selection on the diagram and select the corresponding row on the correct grid.
Diagram Selection and DataGrid Row Correspondency Code
private function handleDiagramSelectionChanged(e:Event):void
{
var arr:Array=diagram.getSelectedObjects();
var uid:String=(arr&&arr.length==1)?DiagramObject(arr[0]).dataobjectid:null;
if(uid)
{
var found:Boolean=false;
for(var i:int=0;i<myObjects.length;i++)
{
if (myObjects.getItemAt(i).uid == uid)
{
objectsGrid.selectedIndex = i;
found = true;
break;
}
}
if (!found)
{
for(i=0;i< myLinks.length;i++)
{
if (myLinks.getItemAt(i).uid == uid)
{
linksGrid.selectedIndex = i;
found = true;
break;
}
}
}
}
}
Now we have one way selection synchronization : when an object is selected on the diagram the corresponding row get selected on the grid. We need to add the other way.
Register a function on the itemClick event for the two DataGrid :
private function handleObjectSelected(event:ListEvent):void
{
var uid:String = myObjects.getItemAt(event.rowIndex).uid;
var dob:DiagramObject = DiagramObject(diagram.getElementByDataObjectId(uid));
diagram.deselectAll();
diagram.selectObject(dob);
}
private function handleLinkSelected(event:ListEvent):void
{
var uid:String = myLinks.getItemAt(event.rowIndex).uid;
var dob:DiagramObject = DiagramObject(diagram.getElementByDataObjectId(uid));
diagram.deselectAll();
diagram.selectObject(dob);
}
And that's it. Compile and run the application, create sprites and links, click on a diagram object and grid row and see as all is kept in sync.
Now the last improvement we can add is the ability to change graph object property from user input outside of the diagram view.
private function handleEditEnd(event:DataGridEvent):void
{
var edit:TextInput = TextInput(event.currentTarget.itemEditorInstance);
var name:String = edit.text;
var uid:String = myObjects.getItemAt(event.rowIndex).uid;
var dob:DiagramObject = DiagramObject(diagram.getElementByDataObjectId(uid));
if (dob.annotation)
dob.annotation.text = name;
else
diagram.createAnnotation(dob, name);
}
In the handleEditEnd function we retrieve the new value and set it as an annotation for the corresponding sprite in the diagrammer. If the sprite doesn't already has an annotation we create it.
You now have a bi-directionnal data bindings between the diagram and application data.
In this part we will add sprite composition capability to our application.
Sprite composition is a mecanism of visually combining several sprites into a single one, just like in a group. But there are differences between grouping and composition :
one of the sprites (the master or composite) contains all the composition elements. He can be move and resize, and the composition elements are moved and resized accordingly.
Not all sprites can become composite or composition elements. This is defined by the SVG style "action-accept:composition" for a composition element and "action-accept:composite" for a composite (or master element).
A composite has reserved areas (svg elements) where composition elements can be put. There is only one composition element per area. So there is a maximum number of elements in a composition, and the maximum depends on the composite sprite definition.
Composition is triggered when the user drags a sprite (the composition element) into another sprite (the composite).
Composition is considered as a change in data model, so acceptance of composition is enforced through methods of the sprite proxies, and occurrence of composition lead to sprite proxies methods calls.
We will create a composite at the application start.
First add some imports
Basic Import Code
import com.kapit.diagram.view.DiagramSprite
Change the initDiagram function:
Composite Creation Code
public function initDiagram():void
{
...
proxy = new KDLProxy(diagram);
proxy.importGraph();
var s2:DiagramSprite = diagram.createSprite("rounded-rectangle", null, true, false);
var s1:DiagramSprite = diagram.createSprite("rectangle", null, true, false);
diagram.createComposition(s2, s1, false);
}
We first create a rounded-rectangle sprite, which can become a composite, then a rectangle, which can become a composition element.
Then we create a composition with the rounded-rectangle as a master, and the rectangle as an element.
At this point if you run your application you will see the composition sprite. You can move it, resize it and you will see how the composition element are moved and resized.
Modify MyObjectProxy.as file to handle composition messages.
Composition Handler on The Object Proxy Code
protected function getElementSpriteId(el:IDiagramElement):String
{
if (el is DiagramSprite)
{
var s:DiagramSprite = el as DiagramSprite;
return s.spriteid;
}
returnnull;
}
public function createDataObject(el:IDiagramElement):String
{
var type:String = el.getTagName();
var spriteid:String = DiagramSprite(el).spriteid;
var name:String = el.did;
var uid:String = UIDUtil.createUID();
_objects.length
var obj:MyObject = new MyObject();
obj.type = type;
obj.spriteid = spriteid;
obj.did = el.did;
obj.uid = uid;
obj.name = "";
obj.compositions = 0;
obj.master = "";
if (_objects)
_objects.addItem(obj);
return uid;
}
public function removeDataObject(el:IDiagramElement):void
{
var index:int = getElementIndex(el);
if (index != -1)
_objects.removeItemAt(index);
}
public function allowLinkAction(el:IDiagramElement):Boolean
{
returntrue;
}
public function propertyModified(el:IDiagramElement, propname:String, propvalue:Object, shapeid:String):void
{
var index:int = getElementIndex(el);
var obj:MyObject = null;
if (index != -1)
{
obj = _objects.getItemAt(index) as MyObject;
}
var s:DiagramSprite = null;
if (el is DiagramSprite)
s = el as DiagramSprite;
if ("text" == propname)
{
if (obj)
{
obj.name=String(propvalue);
}
}
if ("compositionmasterleave" == propname)
{
if (s && "rectangle-composed" == shapeid)
{
s.spriteid = "rectangle";
s.dragenabled = true;
s.selectable = true;
if (obj)
{
obj.spriteid = s.spriteid;
obj.master="";
}
}
}
if ("compositionmaster" == propname)
{
if (s && "rectangle" == shapeid)
{
s.spriteid = "rectangle-composed";
s.dragenabled = true;
s.selectable = true;
if (obj)
{
obj.master=DiagramSprite(propvalue).did;
obj.spriteid = s.spriteid;
}
}
}
if ("compositionelement" == propname)
{
if (obj)
{
obj.compositions=obj.compositions + 1;
}
}
if ("compositionelementremove" == propname)
{
if (obj)
{
obj.compositions=obj.compositions - 1;
}
}
}
The property compositionmaster is changed when a sprite enter a composition, and compositionmasterleave when he leaves a composition.
In our application when a sprite enter a compositon we :
Make it selectable and movable.
Change its sprite.
We he leaves a composition, an element sprite is set back to original sprite.
The property compositionelement is changed when a composite receive a new composition element, and compositionelementremove when the element leaves (or is deleted).
In our application we increase and decrease the number of element of the composite when an element is added or removed.
Change the MyObjectProxy class to handle accept message:
Message Acceptance Handler Through Proxies Integration Code
public function acceptPropertyModification(el:IDiagramElement,propname:String,propvalue:Object,shapeid:String):Boolean
{
var index:int = getElementIndex(el);
var obj:MyObject = null;
if (index != -1)
{
obj = _objects.getItemAt(index) as MyObject;
}
if ("text" == propname)
{
returntrue;
}
if ("compositionmasterleave" == propname)
{
returntrue;
}
if ("compositionmaster" == propname)
{
return ((el as DiagramSprite).masterobject == null || (el as DiagramSprite).masterobject == propvalue);
}
if ("compositionelement" == propname)
{
// No more than one element in a composition
if (obj)
{
return ((obj.compositions<1) || (obj.compositions == 1 && (propvalue as DiagramSprite).masterobject == el));
}
}
if ("compositionelementremove" == propname)
{
returntrue;
}
returntrue;
}
public function acceptRemoveObject(el:IDiagramElement):Boolean
{
var s:DiagramSprite = el as DiagramSprite;
if (s && s.masterobject)
returnfalse;
returntrue;
}
Returning false in these function forbid the user to do the action he attempted.
In the compositionmaster case, we accept that a sprite enter a composition only if it wasn't in a composition before or with the same master (in the case the element is moved in another area inside the composite).
In the compositionelement case, we accept that a sprite enter a composition only if it the composition is empty or with the same master (in the case the element is moved in another area inside the composite). This way we limit the number of element of a composition to 1, although we can have several area in a composite.
In the acceptRemoveObject, we don't accept to remve a sprite inside a composition (with a non null masterobject).
As you can see, with acceptance functions you have great flexibility to enforce your business rules.