Graph Rendering: Implement a simple Force Directed Layout

Based on the paper "Simple Algorithms for Network Visualization: A
Tutorial" by Michael J. McGuffin.
This commit is contained in:
Vishesh Handa
2020-08-03 00:13:07 +02:00
parent 120e516ff9
commit e09ea4313b

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:touchable/touchable.dart';
@ -31,6 +34,19 @@ class _MyExampleWidgetState extends State<MyExampleWidget> {
graph = Graph();
graph.nodes = [a, b, c, d, e];
graph.edges = edges;
graph.assignRandomPositions(400, 650);
const interval = Duration(milliseconds: 25);
bool shouldStop = false;
var timer = Timer.periodic(interval, (Timer t) {
if (shouldStop) {
return;
}
shouldStop = updateGraphPositions(graph);
});
Timer(const Duration(seconds: 5), () => timer.cancel());
}
@override
@ -54,7 +70,6 @@ class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print("Paint");
var myCanvas = TouchyCanvas(context, canvas);
// Draw all the edges
@ -75,7 +90,7 @@ class MyPainter extends CustomPainter {
for (var node in graph.nodes) {
myCanvas.drawCircle(
Offset(node.x, node.y),
30,
20,
Paint()..color = Colors.orange,
onPanStart: (tapdetail) {
node.pressed = true;
@ -124,6 +139,9 @@ class Node {
double y;
bool pressed = false;
double forceX = 0.0;
double forceY = 0.0;
Node(this.label, this.x, this.y);
@override
@ -141,9 +159,55 @@ class Graph extends ChangeNotifier {
List<Node> nodes = [];
List<Edge> edges = [];
Map<String, List<int>> _neighbours = {};
Map<String, int> _nodeIndexes;
void notify() {
notifyListeners();
}
List<int> computeNeighbours(Node n) {
if (_nodeIndexes == null) {
_nodeIndexes = <String, int>{};
for (var i = 0; i < this.nodes.length; i++) {
var node = this.nodes[i];
_nodeIndexes[node.label] = i;
}
}
var _nodes = _neighbours[n.label];
if (_nodes != null) {
return _nodes;
}
var nodes = <int>{};
for (var edge in edges) {
if (edge.a.label == n.label) {
nodes.add(_nodeIndexes[edge.b.label]);
continue;
}
if (edge.b.label == n.label) {
nodes.add(_nodeIndexes[edge.a.label]);
continue;
}
}
_nodes = nodes.toList();
_neighbours[n.label] = _nodes;
return _nodes;
}
void assignRandomPositions(int maxX, int maxY) {
var random = Random(DateTime.now().millisecondsSinceEpoch);
for (var node in nodes) {
node.x = random.nextInt(maxX).toDouble();
node.y = random.nextInt(maxY).toDouble();
}
notifyListeners();
}
}
void main() => runApp(MyApp());
@ -167,11 +231,8 @@ class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample Code'),
),
body: Container(
height: 500,
height: 700,
width: 500,
child: MyExampleWidget(),
),
@ -180,3 +241,109 @@ class MyWidget extends StatelessWidget {
}
// FIXME: Possibly use directed_graph library?
//
// Basic Force Directed Layout
//
const l = 150.0; // sping rest length
const k_r = 10000.0; // repulsive force constant
const k_s = 20; // spring constant
const delta_t = 0.005; // time step
const MAX_DISPLACEMENT_SQUARED = 16;
const min_movement = 1.0;
bool updateGraphPositions(Graph g) {
var numNodes = g.nodes.length;
// Initialize net forces
for (var i = 0; i < numNodes; i++) {
g.nodes[i].forceX = 0;
g.nodes[i].forceY = 0;
}
for (var i1 = 0; i1 < numNodes - 1; i1++) {
var node1 = g.nodes[i1];
for (var i2 = i1 + 1; i2 < numNodes; i2++) {
var node2 = g.nodes[i2];
var dx = node2.x - node1.x;
var dy = node2.y - node1.y;
if (dx != 0 || dy != 0) {
var distSq = (dx * dx) + (dy * dy);
var distance = sqrt(distSq);
var force = k_r / distSq;
var fx = force * dx / distance;
var fy = force * dy / distance;
node1.forceX -= fx;
node1.forceY -= fy;
node2.forceX += fx;
node2.forceY += fy;
}
}
}
// Spring forces between adjacent pairs
for (var i1 = 0; i1 < numNodes; i1++) {
var node1 = g.nodes[i1];
var node1Neighbours = g.computeNeighbours(node1);
for (var j = 0; j < node1Neighbours.length; j++) {
var i2 = node1Neighbours[j];
var node2 = g.nodes[i2];
if (i1 < i2) {
var dx = node2.x - node1.x;
var dy = node2.y - node1.y;
if (dx != 0 || dy != 0) {
var distSq = (dx * dx) + (dy * dy);
var distance = sqrt(distSq);
var force = k_s * (distance - l);
var fx = force * dx / distance;
var fy = force * dy / distance;
node1.forceX += fx;
node1.forceY += fy;
node2.forceX -= fx;
node2.forceY -= fy;
}
}
}
}
// Update positions
var allBelowThreshold = true;
for (var i = 0; i < numNodes; i++) {
var node = g.nodes[i];
var dx = delta_t * node.forceX;
var dy = delta_t * node.forceY;
var dispSq = (dx * dx) + (dy * dy);
if (dispSq > MAX_DISPLACEMENT_SQUARED) {
var s = sqrt(MAX_DISPLACEMENT_SQUARED / dispSq);
dx *= s;
dy *= s;
}
print('${node.label} $dx $dy');
node.x += dx;
node.y += dy;
if (dx.abs() > min_movement || dy.abs() > min_movement) {
allBelowThreshold = false;
}
}
print('------------------');
g.notify();
return allBelowThreshold;
}