ui: switch Graph to bucket grid layouting

This change introduces bucket grid layouting, which sorts the nodes into buckets based on how they are connected. Each
bucket is then layouted individually before they are arranged next to each other. Edges go from left to right, except
for inheritance edges that go from bottom to top.
This commit is contained in:
Eberhard Graether
2015-09-24 03:12:42 +02:00
parent c18bd6a8bc
commit 5825fe77ab
15 changed files with 575 additions and 56 deletions
@@ -0,0 +1,352 @@
#include "component/controller/helper/BucketGrid.h"
#include "component/controller/helper/DummyEdge.h"
#include "component/controller/helper/DummyNode.h"
#include "component/view/GraphViewStyle.h"
Bucket::Bucket()
: i(0)
, j(0)
, m_width(0)
, m_height(0)
{
}
Bucket::Bucket(int i, int j)
: i(i)
, j(j)
, m_width(0)
, m_height(0)
{
}
int Bucket::getWidth() const
{
return m_width;
}
int Bucket::getHeight() const
{
return m_height;
}
bool Bucket::hasNode(DummyNode* node) const
{
for (DummyNode* n : m_nodes)
{
if (node == n)
{
return true;
}
}
return false;
}
void Bucket::addNode(DummyNode* node)
{
m_nodes.push_back(node);
m_width = (node->size.x > m_width ? node->size.x : m_width);
m_height += node->size.y + GraphViewStyle::toGridGap(10);
}
void Bucket::preLayout(Vec2i viewSize)
{
int cols = m_height / viewSize.y + 1;
int x = 0;
int y = 0;
int height = m_height / cols;
int width = 0;
m_height = 0;
for (DummyNode* node : m_nodes)
{
node->position.x = x;
node->position.y = y;
y += node->size.y + GraphViewStyle::toGridGap(10);
width = (node->size.x > width ? node->size.x : width);
m_height = (y > m_height ? y : m_height);
if (y > height)
{
y = 0;
x += width + GraphViewStyle::toGridGap(25);
width = 0;
}
}
m_width = x + width;
}
void Bucket::layout(int x, int y, int width, int height)
{
int cx = GraphViewStyle::toGridOffset(x + (width - m_width) / 2);
int cy = GraphViewStyle::toGridOffset(y + (height - m_height) / 2);
for (DummyNode* node : m_nodes)
{
node->position.x = node->position.x + cx;
node->position.y = node->position.y + cy;
}
}
void BucketGrid::layout(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges, Vec2i viewSize)
{
BucketGrid grid(viewSize);
grid.createBuckets(nodes, edges);
grid.layoutBuckets();
}
BucketGrid::BucketGrid(Vec2i viewSize)
: m_viewSize(viewSize)
, m_i1(0)
, m_j1(0)
, m_i2(0)
, m_j2(0)
{
m_buckets[0][0] = Bucket(0, 0);
}
void BucketGrid::createBuckets(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges)
{
if (!nodes.size())
{
return;
}
bool activeNodeAdded = false;
for (DummyNode& node : nodes)
{
if (node.hasActiveSubNode())
{
addNode(&node);
activeNodeAdded = true;
}
}
if (!activeNodeAdded)
{
addNode(&nodes[0]);
}
std::vector<const DummyEdge*> remainingEdges;
for (const DummyEdge& edge : edges)
{
remainingEdges.push_back(&edge);
}
size_t i = 0;
while (remainingEdges.size())
{
const DummyEdge* edge = remainingEdges[i];
DummyNode* owner = findTopMostDummyNodeRecursive(nodes, edge->ownerId);
DummyNode* target = findTopMostDummyNodeRecursive(nodes, edge->targetId);
bool removeEdge = false;
if (!owner || !target)
{
removeEdge = true;
}
else
{
bool horizontal = edge->data ? !edge->data->isType(Edge::EDGE_INHERITANCE) : true;
removeEdge = addNode(owner, target, horizontal);
}
if (removeEdge)
{
remainingEdges.erase(remainingEdges.begin() + i);
}
else
{
i++;
}
if (i == remainingEdges.size())
{
i = 0;
}
}
}
void BucketGrid::layoutBuckets()
{
std::map<int, int> widths;
std::map<int, int> heights;
for (int j = m_j1; j <= m_j2; j++)
{
for (int i = m_i1; i <= m_i2; i++)
{
Bucket* bucket = &m_buckets[j][i];
bucket->preLayout(m_viewSize);
std::map<int, int>::iterator wt = widths.find(i);
if (wt == widths.end() || wt->second < bucket->getWidth())
{
widths[i] = bucket->getWidth();
}
std::map<int, int>::iterator ht = heights.find(j);
if (ht == heights.end() || ht->second < bucket->getHeight())
{
heights[j] = bucket->getHeight();
}
}
}
int y = 0;
for (int j = m_j1; j <= m_j2; j++)
{
int x = 0;
for (int i = m_i1; i <= m_i2; i++)
{
Bucket* bucket = &m_buckets[j][i];
bucket->layout(x, y, widths[i], heights[j]);
x += widths[i] + GraphViewStyle::toGridGap(85);
}
y += heights[j] + GraphViewStyle::toGridGap(45);
}
}
DummyNode* BucketGrid::findTopMostDummyNodeRecursive(std::vector<DummyNode>& nodes, Id tokenId, DummyNode* top)
{
for (DummyNode& node : nodes)
{
DummyNode* t = (top ? top : &node);
if (node.visible && node.tokenId == tokenId)
{
return t;
}
DummyNode* result = findTopMostDummyNodeRecursive(node.subNodes, tokenId, t);
if (result != nullptr)
{
return result;
}
}
return nullptr;
}
void BucketGrid::addNode(DummyNode* node)
{
Bucket* bucket = getBucket(0, 0);
bucket->addNode(node);
}
bool BucketGrid::addNode(DummyNode* owner, DummyNode* target, bool horizontal)
{
Bucket* ownerBucket = getBucket(owner);
Bucket* targetBucket = getBucket(target);
if (!ownerBucket && !targetBucket)
{
return false;
}
else if (ownerBucket && targetBucket)
{
return true;
}
if (ownerBucket)
{
int i = horizontal ? ownerBucket->i + 1 : ownerBucket->i;
int j = horizontal ? ownerBucket->j : ownerBucket->j - 1;
Bucket* bucket = getBucket(i, j);
bucket->addNode(target);
}
else
{
int i = horizontal ? targetBucket->i - 1 : targetBucket->i;
int j = horizontal ? targetBucket->j : targetBucket->j + 1;
Bucket* bucket = getBucket(i, j);
bucket->addNode(owner);
}
return true;
}
Bucket* BucketGrid::getBucket(int i, int j)
{
bool newColumn = false;
bool newRow = false;
if (i == m_i1 - 1)
{
m_i1 = i;
newColumn = true;
}
else if (i == m_i2 + 1)
{
m_i2 = i;
newColumn = true;
}
if (newColumn)
{
for (int cj = m_j1; cj <= m_j2; cj++)
{
m_buckets[cj][i] = Bucket(i, cj);
}
}
if (j == m_j1 - 1)
{
m_j1 = j;
newRow = true;
}
else if (j == m_j2 + 1)
{
m_j2 = j;
newRow = true;
}
if (newRow)
{
for (int ci = m_i1; ci <= m_i2; ci++)
{
m_buckets[j][ci] = Bucket(ci, j);
}
}
if (i >= m_i1 && i <= m_i2 && j >= m_j1 && j <= m_j2)
{
return &m_buckets[j][i];
}
return nullptr;
}
Bucket* BucketGrid::getBucket(DummyNode* node)
{
for (int j = m_j1; j <= m_j2; j++)
{
for (int i = m_i1; i <= m_i2; i++)
{
Bucket* bucket = &m_buckets[j][i];
if (bucket->hasNode(node))
{
return bucket;
}
}
}
return nullptr;
}
@@ -0,0 +1,66 @@
#ifndef BUCKET_GRID_H
#define BUCKET_GRID_H
#include <map>
#include "utility/math/Vector2.h"
#include "utility/types.h"
struct DummyEdge;
struct DummyNode;
class Bucket
{
public:
Bucket();
Bucket(int i, int j);
int getWidth() const;
int getHeight() const;
bool hasNode(DummyNode* node) const;
void addNode(DummyNode* node);
void preLayout(Vec2i viewSize);
void layout(int x, int y, int width, int height);
int i;
int j;
private:
int m_width;
int m_height;
std::vector<DummyNode*> m_nodes;
};
class BucketGrid
{
public:
static void layout(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges, Vec2i viewSize);
private:
BucketGrid(Vec2i viewSize);
void createBuckets(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges);
void layoutBuckets();
DummyNode* findTopMostDummyNodeRecursive(std::vector<DummyNode>& nodes, Id tokenId, DummyNode* top = nullptr);
void addNode(DummyNode* node);
bool addNode(DummyNode* owner, DummyNode* target, bool horizontal);
Bucket* getBucket(int i, int j);
Bucket* getBucket(DummyNode* node);
Vec2i m_viewSize;
std::map<int, std::map<int, Bucket>> m_buckets;
int m_i1;
int m_j1;
int m_i2;
int m_j2;
};
#endif // BUCKET_GRID_H
@@ -0,0 +1,261 @@
#include "component/controller/helper/GraphLayouter.h"
#include <cmath>
#include <map>
#include <queue>
#include "component/controller/helper/BucketGrid.h"
#include "component/controller/helper/DummyEdge.h"
#include "component/controller/helper/DummyNode.h"
#include "data/graph/Node.h"
// for prototyping, remove when done
#include "Eigen/Dense"
#include "Eigen/Eigenvalues"
#include "unsupported/Eigen/MatrixFunctions"
bool compareEigenvaluePairs(const std::pair<int, double>& p0, const std::pair<int, double>& p1)
{
return p0.second > p1.second;
}
void GraphLayouter::layoutSimpleRaster(std::vector<DummyNode>& nodes)
{
int x = 0;
int y = 0;
int offset = 150;
int w = ceil(sqrt(nodes.size()));
for (unsigned int i = 0; i < nodes.size(); i++)
{
if (i > 0 && i % w == 0)
{
y += offset;
x = 0;
}
nodes[i].position = Vec2i(x, y);
x += offset;
}
}
void GraphLayouter::layoutSimpleRing(std::vector<DummyNode>& nodes)
{
if (nodes.size() >= 1)
{
nodes[0].position = Vec2i(0, 0);
if (nodes.size() > 1)
{
float offset = 200.0f;
for (unsigned int i = 1; i < nodes.size(); i++)
{
float rad = 2.0f * 3.14159265359f / float(nodes.size() - 1) * i - 1;
int x = offset * std::cos(rad);
int y = offset * std::sin(rad);
nodes[i].position = Vec2i(x, y);
}
}
}
}
void GraphLayouter::layoutBucket(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges, Vec2i viewSize)
{
BucketGrid::layout(nodes, edges, viewSize);
}
void GraphLayouter::layoutSpectralPrototype(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges)
{
if (nodes.size() < 2)
{
LOG_INFO("Not enough nodes for layouting");
return;
}
MatrixDynamicBase<int> laplacian = buildLaplacianMatrix(nodes, edges);
// If the laplacian matrix has a zero value in it's diagonal, that means there are unconnected nodes and the spectral
// layouting fails for some reason. In this case we switch to raster layout.
for (unsigned int i = 0; i < laplacian.getColumnsCount(); i++)
{
if (laplacian.getValue(i, i) == 0)
{
layoutSimpleRaster(nodes);
return;
}
}
Eigen::MatrixXd degreeMatrix = Eigen::MatrixXd::Zero(laplacian.getColumnsCount(), laplacian.getRowsCount());
Eigen::MatrixXd eigenMatrix = Eigen::MatrixXd::Zero(laplacian.getColumnsCount(), laplacian.getRowsCount());
for(unsigned int x = 0; x < laplacian.getColumnsCount(); x++)
{
for(unsigned int y = 0; y < laplacian.getRowsCount(); y++)
{
eigenMatrix(x, y) = laplacian.getValue(x, y);
if(x == y)
{
degreeMatrix(x, y) = laplacian.getValue(x, y);
}
}
}
degreeMatrix = degreeMatrix.inverse();
Eigen::MatrixPower<Eigen::MatrixXd> dPow(degreeMatrix);
degreeMatrix = dPow(0.5);
eigenMatrix = degreeMatrix * eigenMatrix * degreeMatrix;
eigenMatrix.normalize();
Eigen::EigenSolver<Eigen::MatrixXd> solver(eigenMatrix);
std::vector<std::vector<double>> eigenVectors;
for(unsigned int i = 0; i < solver.eigenvectors().cols(); i++)
{
eigenVectors.push_back(std::vector<double>());
for(unsigned int j = 0; j < solver.eigenvectors().rows(); j++)
{
eigenVectors[i].push_back(solver.eigenvectors()(i*solver.eigenvectors().rows() + j).real());
}
}
std::vector<std::pair<int, double>> eigenValues;
for(unsigned int i = 0; i < solver.eigenvalues().size(); i++)
{
eigenValues.push_back(std::pair<int, double>(i, solver.eigenvalues()(i).real()));
}
std::sort(eigenValues.begin(), eigenValues.end(), compareEigenvaluePairs);
if(eigenVectors.size() > 0 && eigenVectors[0].size() >= 3)
{
unsigned int xIdx = eigenValues[eigenValues.size()-2].first;
unsigned int yIdx = eigenValues[eigenValues.size()-3].first;
/*double xEigenValue = std::sqrt(solver.eigenvalues()(xIdx).real());
double yEigenValue = std::sqrt(solver.eigenvalues()(yIdx).real());*/
for(unsigned int i = 0; i < nodes.size(); i++)
{
float xPos = eigenVectors[xIdx][i];
float yPos = eigenVectors[yIdx][i];
Vec2f newPos(xPos, yPos);
newPos.normalize();
newPos *= 600.0f;
nodes[i].position.x = newPos.x;
nodes[i].position.y = newPos.y;
}
}
}
MatrixDynamicBase<int> GraphLayouter::buildLaplacianMatrix(
const std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges)
{
MatrixDynamicBase<int> matrix(nodes.size(), nodes.size());
std::map<Id, DummyNode> nodesMap;
std::queue<DummyNode> remainingNodes;
for(unsigned int i = 0; i < nodes.size(); i++)
{
remainingNodes.push(nodes[i]);
}
while(remainingNodes.size() > 0)
{
if(remainingNodes.front().subNodes.size() > 0)
{
for(unsigned int i = 0; i < remainingNodes.front().subNodes.size(); i++)
{
remainingNodes.push(remainingNodes.front().subNodes[i]);
}
}
nodesMap[remainingNodes.front().tokenId] = remainingNodes.front();
remainingNodes.pop();
}
std::map<std::pair<Id, Id>, int> weightsMap;
for(unsigned int i = 0; i < edges.size(); i++)
{
const DummyEdge& edge = edges[i];
DummyNode ownerNode = nodesMap[edge.ownerId];
DummyNode targetNode = nodesMap[edge.targetId];
if(ownerNode.topLevelAncestorId != targetNode.topLevelAncestorId)
{
int weightIncrement = edge.getWeight();
Id ownerId = ownerNode.topLevelAncestorId;
Id targetId = targetNode.topLevelAncestorId;
std::pair<Id, Id> key(ownerId, targetId);
std::pair<Id, Id> inverseKey(targetId, ownerId);
std::pair<Id, Id> keyOwner(ownerId, ownerId);
std::pair<Id, Id> keyTarget(targetId, targetId);
std::map<std::pair<Id, Id>, int>::iterator it = weightsMap.find(key);
if(it == weightsMap.end())
{
weightsMap[key] = 0;
}
weightsMap[key] += weightIncrement;
it = weightsMap.find(inverseKey);
if(it == weightsMap.end())
{
weightsMap[inverseKey] = 0;
}
weightsMap[inverseKey] += weightIncrement;
it = weightsMap.find(keyOwner);
if(it == weightsMap.end())
{
weightsMap[keyOwner] = 0;
}
weightsMap[keyOwner] += weightIncrement;
it = weightsMap.find(keyTarget);
if(it == weightsMap.end())
{
weightsMap[keyTarget] = 0;
}
weightsMap[keyTarget] += weightIncrement;
}
}
for(unsigned int x = 0; x < nodes.size(); x++)
{
for(unsigned int y = x; y < nodes.size(); y++)
{
unsigned int xNodeId = nodes[x].tokenId;
unsigned int yNodeId = nodes[y].tokenId;
std::pair<Id, Id> key(xNodeId, yNodeId);
if(x == y)
{
matrix.setValue(x, y, weightsMap[key]);
}
else
{
matrix.setValue(x, y, -weightsMap[key]);
matrix.setValue(y, x, -weightsMap[key]);
}
}
}
return matrix;
}
@@ -0,0 +1,28 @@
#ifndef GRAPH_LAYOUTER_H
#define GRAPH_LAYOUTER_H
#include <vector>
#include "utility/math/MatrixDynamicBase.h"
#include "utility/math/Vector2.h"
#include "utility/types.h"
struct DummyEdge;
struct DummyNode;
class GraphLayouter
{
public:
static void layoutSimpleRaster(std::vector<DummyNode>& nodes);
static void layoutSimpleRing(std::vector<DummyNode>& nodes);
static void layoutBucket(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges, Vec2i viewSize);
static void layoutSpectralPrototype(std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges);
private:
static MatrixDynamicBase<int> buildLaplacianMatrix(
const std::vector<DummyNode>& nodes, const std::vector<DummyEdge>& edges);
};
#endif // GRAPH_LAYOUTER_H