mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-07-01 12:23:44 +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:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:touchable/touchable.dart';
|
import 'package:touchable/touchable.dart';
|
||||||
@ -31,6 +34,19 @@ class _MyExampleWidgetState extends State<MyExampleWidget> {
|
|||||||
graph = Graph();
|
graph = Graph();
|
||||||
graph.nodes = [a, b, c, d, e];
|
graph.nodes = [a, b, c, d, e];
|
||||||
graph.edges = edges;
|
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
|
@override
|
||||||
@ -54,7 +70,6 @@ class MyPainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
print("Paint");
|
|
||||||
var myCanvas = TouchyCanvas(context, canvas);
|
var myCanvas = TouchyCanvas(context, canvas);
|
||||||
|
|
||||||
// Draw all the edges
|
// Draw all the edges
|
||||||
@ -75,7 +90,7 @@ class MyPainter extends CustomPainter {
|
|||||||
for (var node in graph.nodes) {
|
for (var node in graph.nodes) {
|
||||||
myCanvas.drawCircle(
|
myCanvas.drawCircle(
|
||||||
Offset(node.x, node.y),
|
Offset(node.x, node.y),
|
||||||
30,
|
20,
|
||||||
Paint()..color = Colors.orange,
|
Paint()..color = Colors.orange,
|
||||||
onPanStart: (tapdetail) {
|
onPanStart: (tapdetail) {
|
||||||
node.pressed = true;
|
node.pressed = true;
|
||||||
@ -124,6 +139,9 @@ class Node {
|
|||||||
double y;
|
double y;
|
||||||
bool pressed = false;
|
bool pressed = false;
|
||||||
|
|
||||||
|
double forceX = 0.0;
|
||||||
|
double forceY = 0.0;
|
||||||
|
|
||||||
Node(this.label, this.x, this.y);
|
Node(this.label, this.x, this.y);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -141,9 +159,55 @@ class Graph extends ChangeNotifier {
|
|||||||
List<Node> nodes = [];
|
List<Node> nodes = [];
|
||||||
List<Edge> edges = [];
|
List<Edge> edges = [];
|
||||||
|
|
||||||
|
Map<String, List<int>> _neighbours = {};
|
||||||
|
Map<String, int> _nodeIndexes;
|
||||||
|
|
||||||
void notify() {
|
void notify() {
|
||||||
notifyListeners();
|
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());
|
void main() => runApp(MyApp());
|
||||||
@ -167,11 +231,8 @@ class MyWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Sample Code'),
|
|
||||||
),
|
|
||||||
body: Container(
|
body: Container(
|
||||||
height: 500,
|
height: 700,
|
||||||
width: 500,
|
width: 500,
|
||||||
child: MyExampleWidget(),
|
child: MyExampleWidget(),
|
||||||
),
|
),
|
||||||
@ -180,3 +241,109 @@ class MyWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Possibly use directed_graph library?
|
// 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