mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-07-01 04:07:53 +08:00
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:
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user