/*
Daniel Davis, 2010, nzarchitecture.com
Draws a directed graph and dynamically relaxes the graph.
Nodes in the graph can also be dragged around.
*/
HashMap graph;
PFont fontA;
int stageSize = 900; //size of the stage
int boxHeight = 10; //height of box in pixels
int border = 5; //border around box in pixels
float speed = 5; //speed to move nodes together, 1 = slow, 100 = fast.
float bubble = 80; //distance nodes will try to stay apart
float curveSize = 50; //size of the curve coming out of node, 0 = no curve.
color backgroundColor = color(227, 224, 213);
color lineColor = color(83, 100, 105);
color boxColor = color(185, 200, 201);
color hoverColor = color(214, 186, 204);
color textColor = color(83, 100, 105);
String fileName = "graph.txt"; //name of file with connection data, expecting it in this format:
//a, b, b, b
//a, b, b, b
//a
//where each row represents one node in the graph
//a is the name of the node
//b is the name of the nodes that are outputs for the node
//there can be any number of outputs as long as they are seperated by commas.
//if there are no b's, then the node only has inputs
//inputs do not need to be stated, they are implied from the outputs.
void setup()
{
size(stageSize, stageSize);
background(backgroundColor);
char[] letters = new char[]{'a'};
fontA = createFont("Helvetica", 16, true, letters);
//create blank graph
graph = new HashMap();
String[] lines = loadStrings(fileName);
//add the nodes into the graph
for(int i =0; i <lines.length; i++)
{
String[] pieces = split(lines[i], ',');
float startX = random(width);
float startY = random(height);
String name = trim(pieces[0]);
graph.put(name, new node(startX, startY, name));
}
//add the connections
for(int i =0; i <lines.length; i++)
{
String[] pieces = split(lines[i], ',');
String name = trim(pieces[0]);
for(int j=1; j<pieces.length; j++)
{
String output = trim(pieces[j]);
if(output != "" && output != null)
{
if(graph.containsKey(name) && graph.containsKey(output))
{
((node)graph.get(name)).addOutput(output);
((node)graph.get(output)).addInput(name);
} else {
println("Output/name does not match");
}
}
}
}
}
void draw()
{
background(backgroundColor);
Iterator i = graph.entrySet().iterator(); // Get an iterator
while (i.hasNext()) {
Map.Entry me = (Map.Entry)i.next();
((node)me.getValue()).update();
((node)me.getValue()).draw();
}
}
void mousePressed()
{
Iterator i = graph.entrySet().iterator();
boolean found = false;
while (i.hasNext() && !found) {
Map.Entry me = (Map.Entry)i.next();
found = ((node)me.getValue()).pressed();
}
}
void mouseReleased()
{
Iterator i = graph.entrySet().iterator();
while (i.hasNext()) {
Map.Entry me = (Map.Entry)i.next();
((node)me.getValue()).released();
}
}
/*
A node has:
an xy position on the screen
a string name, which is also its key in the graph hash table
a collection of neighbours: inputs and outputs
The node will try to move towards the centroid of its neighbours
in a form of dynamic relaxation.
If the node is dragged, it will follow the mouse.
Any output is drawn coming form the right side of the node
Any input is drawn coming to the left side of the node
*/
class node {
private float x, y, width, height, inputX, inputY, outputX, outputY, textX, textY, clickedX, clickedY = 0;
private String text = "";
private ArrayList inputs;
private ArrayList outputs;
private boolean move = false;
// Contructor
//ix = inital x position
//iy = inital y position
//iText = name of this node
node(float ix, float iy, String iText) {
x = iy;
y = ix;
text = iText;
inputs = new ArrayList();
outputs = new ArrayList();
//need to draw the font before we can know and set the width
textFont(fontA);
text(text, x, y);
width = textWidth(text) + 2 * border;
height = -1 * (boxHeight + 2 * border);
setXY();
}
public void addInput(String input)
{
inputs.add(input);
}
public void addOutput(String output)
{
outputs.add(output);
}
//updates the xy position of the node.
//if the node is currently being dragged, xy is from the mouse.
//otherwise,
//loops through all the inputs and outputs
//finds the vector towards the centroids of the inputs and outputs
//this vector is based on the line the joins the two nodes together, rather than taking their centers.
//moves node towards centroid
//
//then updates the xy values of the node.
public void update() {
if(move)
{
x = mouseX + clickedX;
y = mouseY + clickedY;
} else {
float newX = 0;
float newY = 0;
int count = 1;
if(inputs != null)
{
for(int i =0; i < inputs.size(); i++)
{
float[] newXY = moveVector((String)inputs.get(i), false);
newX += newXY[0];
newY += newXY[1];
count++;
}
}
if(outputs != null)
{
for(int i =0; i < outputs.size(); i++)
{
float[] newXY = moveVector((String)outputs.get(i), true);
newX += newXY[0];
newY += newXY[1];
count++;
}
}
//keep nodes from eachother
Iterator i = graph.entrySet().iterator(); // Get an iterator
while (i.hasNext()) {
Map.Entry me = (Map.Entry)i.next();
node nextNode = (node)me.getValue();
float diffX = nextNode.getX() - x;
float diffY = nextNode.getY() - y;
float distance = sqrt(sq(diffX) + sq(diffY));
if(distance > 0 && distance < bubble * 2)
{
newX -= speed * 10* (diffX * bubble / sq(distance));
newY -= speed * 10 * (diffY * bubble / sq(distance));
}
}
//average vector
newX /= count;
newY /= count;
//move towards centroid:
x += (newX*speed / 100);
y += (newY*speed / 100);
}
setXY();
}
public void draw() {
drawBox();
drawText();
drawCurves();
}
//callled when the mouse is pressed
//checks to see if the mouse is currently over the node
//if it is, sets move to true and returns true,
//else returns false
public boolean pressed()
{
if (over()) {
move = true;
clickedX = x-mouseX;
clickedY = y - mouseY;
return true;
} else {
move = false;
}
return false;
}
//called when the mouse is released
//stops any nodes moving
public void released()
{
move = false;
}
/*PRIVATE*/
//finds the vector between the nodeId, and the passedId.
//Note that the vector is taken between the output point and the input point
//Size of vector is adjusted so that it pulls the nodes together when they are
//far away and pushes them apart when they are close together
private float[] moveVector(String nodeId, boolean nodeIsOutput)
{
node n = (node)graph.get(nodeId);
float nodeX = 0;
float nodeY = 0;
float myX = 0;
float myY = 0;
if(nodeIsOutput)
{
myX = outputX;
myY = outputY;
nodeX = n.getInX();
nodeY = n.getInY();
} else {
myX = inputX;
myY = inputY;
nodeX = n.getOutX();
nodeY = n.getOutY();
}
float diffX = nodeX - myX;
float diffY = nodeY - myY;
//note that we use the distance between nodes and the log function.
//This means that when nodes get within the bubble distance, we reverse the direction
//of the vector, so that the nodes start replelling rather than attracting.
float distance = sqrt(sq(diffX) + sq(diffY));
float direction = log(distance / bubble);
float[] r = new float[]{diffX * direction, diffY* direction};
return r;
}
private void drawBox()
{
if(over())
{
fill(hoverColor);
} else {
fill(boxColor);
}
stroke(lineColor);
rect(round(x), round(y), width, height);
//rect(x, y, width, height);
}
private void drawText()
{
fill(textColor);
textFont(fontA);
text(text, textX, textY);
}
//draws output curves
//Only need to draw the output curves, because the other nodes that are drawing
//their output curves, will draw the inputs for us.
private void drawCurves()
{
noFill();
stroke(lineColor);
if(outputs != null)
{
for(int i = 0; i < outputs.size(); i++)
{
node inNode = (node)graph.get(outputs.get(i));
float inX = inNode.getInX();
float inY = inNode.getInY();
bezier(outputX, outputY, outputX + curveSize, outputY, inX - curveSize, inY, inX, inY);
}
}
}
// Test to see if mouse is over node
private boolean over()
{
if(mouseX > x && mouseX < x+width)
{
if(mouseY < y && mouseY > y + height)
{
return true;
}
}
return false;
}
//set all the xy values from the current xy value
private void setXY()
{
if(x < 0) x = 0;
if(x + width > stageSize) x = stageSize - width;
if(y + height < 0) y = - height;
if(y > stageSize) y = stageSize;
textX = x+border;
textY = y-border;
inputX = x;
inputY = y + height / 2;
outputX = x+width;
outputY = y + height / 2;
}
/*GETS*/
public float getX()
{
return x;
}
public float getY()
{
return y;
}
public float getInX()
{
return inputX;
}
public float getInY()
{
return inputY;
}
public float getOutX()
{
return outputX;
}
public float getOutY()
{
return outputY;
}
}
Draws a network based on a directed graph. Inputs go into the left, outputs come from the right.
To change what the graph displays, edit the graph.txt file.
The file is in the format:
a, b, b, b
a, b, b, b
Each row is a new node on the graph. A is the name of that node. B is the name of a node that is an output for the node.