I cooked up a scheme where the nodes are absolute positioned div elements that can be styled with CSS, and the edges are drawn with Protovis. I pass an object that defines the edge properties to a Javascript function that uses JQuery to find the exact positions of the nodes, and then I use Protovis to draw the edges.
Example:
HTML:
<div id="workflowContainer">
<!--
-- Draw Simple divs to represent workflow nodes, and connect them with Protovis.
--
-- Nodes are positioned absolutely.
-- Node positions can be static and manually determined,
-- or dynamic and determined by server-side or client-side
-- code. This example uses hard coded node positions.
-->
<div id="workflowChart" >
<!-- Clickable node -->
<a href=""><div id="startFlow" style="top 0; left: 440px;">Start</div></a>
<!-- Foo branch -->
<!-- Unclickable node -->
<div id="foo1Flow" style="top: 100px; left: 200px;">Foo 1</div>
<a href=""><div id="foo2Flow" style="top: 175px; left: 100px;">Foo 2</div></a>
<div id="fooChoice1Flow" style="top: 300px; left: 0px;">Foo Choice 1</div>
<div id="fooChoice2Flow" class="inactive" style="top: 300px; left: 165px;">Foo Choice 2</div>
<div id="fooChoice3Flow" class="inactive" style="top: 300px; left: 360px;">Foo Choice 3</div>
<div id="fooOptionFlow" style="top: 400px; left: 50px;">Foo Option</div>
<a href=""><div id="fooCombineFlow" style="top: 500px; left: 200px;">Foo Combine</div></a>
<a href=""><div id="fooSplit1Flow" style="top: 575px; left: 25px;">Foo Split 1</div></a>
<a href=""><div id="fooSplit2Flow" style="top: 575px; left: 250px;">Foo Split 2</div></a>
<!-- bar branch -->
<div id="barFlow" style="top: 100px; left: 700px;">Bar</div>
<a href=""><div id="bar1Flow" class="inactive" style="top: 200px; left: 550px;">Bar 1</div></a>
<a href=""><div id="bar2Flow" class="inactive" style="top: 200px; left: 825px;">Bar 2</div></a>
</div>
</div>
CSS:
/* Contains both nodes and edges. */
#workflowChartContainer {
position: relative;
width: 1000px;
}
/* This is where the edges will be drawn by protovis. */
#workflowChartContainer span {
position: absolute;
top: 0;
left: 0;
background: transparent;
z-index: 1000; /* SVG needs to be drawn on top of existing layout. */
}
#workflowChart {
position: relative;
top: 0;
left: 0;
height: 700px;
width: 1000px;
}
#workflowChart div {
border-color: #5b9bea;
background-color: #b9cde5;
position: absolute;
margin: 0;
padding: 4px;
border: 2px solid #5b9bea;
background: #b9cde5;
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
color: #000;
z-index: 10000; /* Needs to be drawn on top of SVG to be clickable. */
}
#workflowChart a {
cursor: pointer;
}
#workflowChart a div {
border-color: #f89c51;
background: #fcd5b5;
}
#workflowChart div.inactive {
border-color: #ccc;
background-color: #eee;
color: #ccc;
}
#workflowChart div:hover {
border-color: #700000;
}
Javascript:
/* Initialize workflow screen. */
var initWorkflow = function() {
// List HTML nodes to connect.
//
// The edges are hardcoded in this example,
// but could easily be made dynamic.
var edges = [
{
source: 'startFlow',
target: 'foo1Flow'
},
{
source: 'foo1Flow',
target: 'foo2Flow'
},
{
source: 'foo2Flow',
target: 'fooChoice1Flow'
},
{
source: 'foo2Flow',
target: 'fooChoice2Flow'
},
{
source: 'foo2Flow',
target: 'fooChoice3Flow'
},
{
source: 'fooChoice1Flow',
target: 'fooOptionFlow'
},
{
source: 'fooChoice2Flow',
target: 'fooOptionFlow'
},
{
source: 'fooOptionFlow',
target: 'fooCombineFlow'
},
{
source: 'fooChoice3Flow',
target: 'fooCombineFlow'
},
{
source: 'fooCombineFlow',
target: 'fooSplit1Flow'
},
{
source: 'fooCombineFlow',
target: 'fooSplit2Flow'
},
{
source: 'startFlow',
target: 'barFlow'
},
{
source: 'barFlow',
target: 'bar1Flow'
},
{
source: 'barFlow',
target: 'bar2Flow'
},
];
// Us JQUery to set height and width equal to background div.
var workflow = $('#workflowChart'),
h = workflow.height(),
w = workflow.width();
// Create Protovis Panel used to render SVG.
var vis = new pv.Panel()
.width(w)
.height(h)
.antialias(false);
// Attach Panel to dom
vis.$dom = workflow[0];
// Render connectors
drawEdges(vis, edges);
var test = vis.render();
};
/* Draw edges specified in input array. */
var drawEdges = function(vis, edges) {
// Direction indicators,
var directions = [];
$.each(edges, function(idx, item){
// Color of edges
var color = '#000';
// Arrow radius
var r = 5;
// Use JQuery to get source and destination elements
var source = $('#' + item.source);
var target = $('#' + item.target);
if (!(source.length && target.length)) {
// One of the nodes is not present in the DOM; skip it.
return;
}
var data = edgeCoords(source, target);
if (item.sourceLOffset) {
data[0].left += item.sourceLOffset;
}
if (item.targetLOffset) {
data[1].left += item.targetLOffset;
}
if (source.hasClass('inactive') || target.hasClass('inactive')) {
// If target is disabled, change the edge color.
color = '#ccc';
}
// Use Protovis to draw edge line.
vis.add(pv.Line)
.data(data)
.left(function(d) {return d.left;})
.top(function(d) {
if (d.type === 'target') {
return d.top - (r * 2);
}
return d.top;
})
.interpolate('linear')
.segmented(false)
.strokeStyle(color)
.lineWidth(2);
// Here you may want to calculate an angle
// to twist the direction arrows to make the graph
// prettier. I've left out the code to keep thing simple.
var a = 0;
// Add direction indicators to array.
var d = data[1];
directions.push({
left: d.left,
top: d.top - (r * 2),
angle: a,
color: color
});
});
// Use Protovis to draw all direction indicators
//
// Here you may want to check and make
// sure you're only drawing a single indicator
// at each position, to avoid drawing multiple
// indicators for targets that have multiple sources.
// I've left out the code for simplicity.
vis.add(pv.Dot)
.data(directions)
.left(function (d) {return d.left;})
.top(function (d) {return d.top;})
.radius(r)
.angle(function (d) {return d.angle;})
.shape("triangle")
.strokeStyle(function (d) {return d.color;})
.fillStyle(function (d) {return d.color;});
};
/* Returns the bottom-middle offset for a dom element. */
var bottomMiddle = function(node) {
var coords = node.position();
coords.top += node.outerHeight();
coords.left += node.width() / 2;
return coords;
};
/* Returns the top-middle offset for a dom element. */
var topMiddle = function(node) {
var coords = node.position();
coords.left += node.width() / 2;
return coords;
};
/* Return start/end coordinates for an edge. */
var edgeCoords = function(source, target) {
var coords = [bottomMiddle(source), topMiddle(target)];
coords[0].type = 'source';
coords[1].type = 'target';
return coords;
};
