Repository: nabil6391/graphview

Issue:
graphView nodeWidget not clickable on bottom boundary region
If we try to interact with graphview leaf node widget on bottom boundary areas, it behaves abnormally and remains unclickable.


Discussion: Any code to reproduce?
I have the same problem, and I found if I adjust Level Separation smaller, it will be normal
Here is code to reproduce:

```
import 'package:flutter/material.dart';
import 'package:graphview/GraphView.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return const Scaffold(body: SafeArea(child: GraphWidget()));
  }
}

class GraphWidget extends StatefulWidget {
  const GraphWidget({super.key});

  @override
  State<GraphWidget> createState() => _GraphWidgetState();
}

class _GraphWidgetState extends State<GraphWidget> {
  final Graph graph = Graph()..isTree = true;
  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();

  @override
  void initState() {
    super.initState();

    final node1 = Node.Id(1);
    final node2 = Node.Id(2);
    final node3 = Node.Id(3);

    graph.addNode(node1);
    graph.addNode(node2);
    graph.addNode(node3);

    graph.addEdge(node1, node2);
    graph.addEdge(node1, node3, paint: Paint()..color = Colors.red);
  }

  @override
  Widget build(BuildContext context) {
    builder
      ..siblingSeparation = (50)
      ..levelSeparation = (50)
      ..subtreeSeparation = (80)
      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);

    return Expanded(
      child: InteractiveViewer(
          constrained: false,
          scaleEnabled: false,
          boundaryMargin: const EdgeInsets.all(100),
          minScale: 0.01,
          maxScale: 5.6,
          child: GraphView(
            builder: (Node node) {
              // I can decide what widget should be shown here based on the id
              var a = node.key!.value as int;
              return rectangleWidget(a);
            },
            graph: graph,
            algorithm:
                BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)),
          )),
    );
  }

  Widget rectangleWidget(int a) {
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      child: GestureDetector(
        onTap: () {
          // print('clicked');
          final n = graph.getNodeUsingId(a);
          print(n.position);
        },
        child: Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(4),
              color: Colors.blue[100],
            ),
            child: Text('Node ${a}')),
      ),
    );
  }
}
```

And here is an image from inspector:
<img width="298" alt="image" src="https://github.com/nabil6391/graphview/assets/9356589/0cc12bf0-586d-4a68-b2d8-ea8e950bbe40">

It's because when algorithm is ran, it's called with `shiftX` and `shiftY` to 10 which causes the whole graph to move by 10 pixels and therefore exiting the viewport on bottom right. ([GraphView.dart L196](https://github.com/nabil6391/graphview/blob/master/lib/GraphView.dart#L196))

Is there any reason to do that @nabil6391, looks like it was made to have some kind of padding but this seems wrong to me.

We should either be able to configure the coordinate shifting ourselves or the size should be calculated accordingly (+20 on x and y to get a consistent box) ([BuchheimWalkerAlgorithm.dart L112](https://github.com/nabil6391/graphview/blob/master/lib/tree/BuchheimWalkerAlgorithm.dart#L112))

As a temporary solution, you can extend your preferred algorithm and suppress the shifting : 

```dart
class Algorithm extends BuchheimWalkerAlgorithm {
  Algorithm(super.configuration, super.renderer);

  @override
  Size run(Graph? graph, double shiftX, double shiftY) => super.run(graph, 0, 0);
}
```
Thats a good find and honestly I am not sure why we do the shift, can you help create a PR with the fix and then I can take a look
That should be an easy fix but I'm way too busy right now to take time for it, I'm sorry.

But if anyone come accross this issue, he can easily contribute as I've provided all the necessary.

Ideally, shiftX and shiftY would become a parameter defaulting to Offset.zero

Fix patch:
diff --git a/example/lib/example.dart b/example/lib/example.dart
index 93f9219..d4204e5 100644
--- a/example/lib/example.dart
+++ b/example/lib/example.dart
@@ -1,7 +1,7 @@
 import 'package:example/layer_graphview.dart';
 import 'package:flutter/material.dart';
 
-import 'directed_graphview.dart';
+import 'force_directed_graphview.dart';
 import 'tree_graphview.dart';
 
 void main() {
diff --git a/example/lib/directed_graphview.dart b/example/lib/force_directed_graphview.dart
similarity index 98%
rename from example/lib/directed_graphview.dart
rename to example/lib/force_directed_graphview.dart
index e14c16f..73bfcb0 100644
--- a/example/lib/directed_graphview.dart
+++ b/example/lib/force_directed_graphview.dart
@@ -20,7 +20,7 @@ class _GraphClusterViewPageState extends State<GraphClusterViewPage> {
                   constrained: false,
                   boundaryMargin: EdgeInsets.all(8),
                   minScale: 0.001,
-                  maxScale: 100,
+                  maxScale: 10000,
                   child: GraphView(
                       graph: graph,
                       algorithm: builder,
diff --git a/example/lib/graph_cluster_animated.dart b/example/lib/graph_cluster_animated.dart
index 13b5c81..7bb73e4 100644
--- a/example/lib/graph_cluster_animated.dart
+++ b/example/lib/graph_cluster_animated.dart
@@ -1,12 +1,10 @@
-import 'dart:async';
 import 'dart:math';
-
 import 'package:flutter/material.dart';
 import 'package:graphview/GraphView.dart';
 
 class GraphScreen extends StatefulWidget {
-  Graph graph;
-  Algorithm algorithm;
+  final Graph graph;
+  final Algorithm algorithm;
   final Paint? paint;
 
   GraphScreen(this.graph, this.algorithm, this.paint);
@@ -16,14 +14,15 @@ class GraphScreen extends StatefulWidget {
 }
 
 class _GraphScreenState extends State<GraphScreen> {
-  bool animated = true;
-  Random r = Random();
+  GraphViewController _controller = GraphViewController();
+  final Random r = Random();
+  int nextNodeId = 1;
 
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(
-        title: Text('Graph Screen'),
+        title: Text('Graph View'),
         actions: [
           IconButton(
             icon: Icon(Icons.add),
@@ -41,43 +40,81 @@ class _GraphScreenState extends State<GraphScreen> {
             icon: Icon(Icons.animation),
             onPressed: () async {
               setState(() {
-                animated = !animated;
               });
             },
           )
         ],
       ),
-      body: InteractiveViewer(
-          constrained: false,
-          boundaryMargin: EdgeInsets.all(100),
-          minScale: 0.0001,
-          maxScale: 10.6,
-          child: GraphView(
-            graph: widget.graph,
-            algorithm: widget.algorithm,
-            animated: animated,
-            builder: (Node node) {
-              // I can decide what widget should be shown here based on the id
-              var a = node.key!.value as String;
-              return rectangWidget(a);
-            },
-          )),
+
+      body: Column(
+        children: [
+          // Navigation controls
+          Wrap(
+            children: [
+              ElevatedButton(
+                onPressed: () => _navigateToRandomNode(),
+                child: Text('Go to Node $nextNodeId'),
+              ),
+              ElevatedButton(
+                onPressed: () => _controller.resetView(),
+                child: Text('Reset View'),
+              ),
+              ElevatedButton(
+                onPressed: () => _controller.zoomToFit(),
+                child: Text("Zoom to fit"),
+              ),
+            ],
+          ),
+          Expanded(
+            child: GraphView.builder(
+              controller: _controller,
+              graph: widget.graph,
+              algorithm: widget.algorithm,
+              paint: widget.paint,
+              builder: (Node node) {
+                var a = node.key?.value;
+                return rectangleWidget(a);
+              },
+            ),
+          ),
+        ],
+      ),
     );
   }
 
-  Widget rectangWidget(String? i) {
-    return Container(
-        padding: EdgeInsets.all(16),
+  Widget rectangleWidget(nodeText) {
+    return InkWell(
+      onTap: () => print('clicked $nodeText'),
+      child: Container(
+        padding: EdgeInsets.all(10),
         decoration: BoxDecoration(
-          borderRadius: BorderRadius.circular(4),
-          boxShadow: [
-            BoxShadow(color: Colors.blue, spreadRadius: 1),
-          ],
+          color: Colors.red,
+          border: Border.all(color: Colors.white, width: 1),
+          borderRadius: BorderRadius.circular(30),
+        ),
+        child: Center(
+          child: Text(
+            '$nodeText',
+            style: TextStyle(fontSize: 10),
+          ),
         ),
-        child: Center(child: Text('Node $i')));
+      ),
+    );
   }
 
-  Future<void> update() async {
-    setState(() {});
+  void _navigateToRandomNode() {
+    if (widget.graph.nodes.isEmpty) return;
+
+    final randomNode = widget.graph.nodes.firstWhere(
+          (node) => node.key != null && node.key!.value == nextNodeId,
+      orElse: () => widget.graph.nodes.first,
+    );
+    final nodeId = randomNode.key!;
+    _controller.animateToNode(nodeId);
+
+    setState(() {
+      nextNodeId = r.nextInt(widget.graph.nodes.length) + 1;
+    });
   }
-}
+
+}
\ No newline at end of file
diff --git a/example/lib/layer_graphview.dart b/example/lib/layer_graphview.dart
index 20d11c6..658f946 100644
--- a/example/lib/layer_graphview.dart
+++ b/example/lib/layer_graphview.dart
@@ -1,5 +1,4 @@
 import 'dart:math';
-
 import 'package:flutter/material.dart';
 import 'package:graphview/GraphView.dart';
 
@@ -9,6 +8,10 @@ class LayeredGraphViewPage extends StatefulWidget {
 }
 
 class _LayeredGraphViewPageState extends State<LayeredGraphViewPage> {
+  GraphViewController _controller = GraphViewController();
+  final Random r = Random();
+  int nextNodeId = 0;
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
@@ -52,7 +55,7 @@ class _LayeredGraphViewPageState extends State<LayeredGraphViewPage> {
                   ),
                 ),
                 Container(
-                  width: 100,
+                  width: 120,
                   child: Column(
                     children: [
                       Text('Alignment'),
@@ -82,63 +85,83 @@ class _LayeredGraphViewPageState extends State<LayeredGraphViewPage> {
                     setState(() {});
                   },
                   child: Text('Add'),
-                )
+                ),
+                ElevatedButton(
+                  onPressed: () => _navigateToRandomNode(),
+                  child: Text('Go to Node $nextNodeId'),
+                ),
+                ElevatedButton(
+                  onPressed: () => _controller.resetView(),
+                  child: Text('Reset View'),
+                ),
+                ElevatedButton(
+                  onPressed: () => _controller.zoomToFit(),
+                  child: Text("Zoom to fit"),
+                ),
               ],
             ),
             Expanded(
-              child: InteractiveViewer(
-                  constrained: false,
-                  boundaryMargin: EdgeInsets.all(100),
-                  minScale: 0.0001,
-                  maxScale: 10.6,
-                  child: GraphView(
-                    graph: graph,
-                    algorithm: SugiyamaAlgorithm(builder),
-                    paint: Paint()
-                      ..color = Colors.green
-                      ..strokeWidth = 1
-                      ..style = PaintingStyle.stroke,
-                    builder: (Node node) {
-                      // I can decide what widget should be shown here based on the id
-                      var a = node.key!.value as int?;
-                      return rectangleWidget(a);
-                    },
-                  )),
+              child: GraphView.builder(
+                controller: _controller,
+                graph: graph,
+                algorithm: SugiyamaAlgorithm(builder),
+                paint: Paint()
+                  ..color = Colors.green
+                  ..strokeWidth = 1
+                  ..style = PaintingStyle.stroke,
+                builder: (Node node) {
+                  var a = node.key!.value as int?;
+                  return rectangleWidget(a);
+                },
+              ),
             ),
           ],
         ));
   }
 
-  Random r = Random();
-
   Widget rectangleWidget(int? a) {
     return Container(
         padding: EdgeInsets.all(16),
         decoration: BoxDecoration(
-          borderRadius: BorderRadius.circular(4),
+          shape: BoxShape.circle,
           boxShadow: [
             BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),
           ],
         ),
-        child: Text('Node ${a}'));
+        child: Text('${a}'));
   }
 
   final Graph graph = Graph();
-
   SugiyamaConfiguration builder = SugiyamaConfiguration()
     ..bendPointShape = CurvedBendPointShape(curveLength: 20);
 
+  void _navigateToRandomNode() {
+    if (graph.nodes.isEmpty) return;
+
+    final randomNode = graph.nodes.firstWhere(
+          (node) => node.key != null && node.key!.value == nextNodeId,
+      orElse: () => graph.nodes.first,
+    );
+    final nodeId = randomNode.key!;
+    _controller.animateToNode(nodeId);
+
+    setState(() {
+      nextNodeId = r.nextInt(graph.nodes.length) + 1;
+    });
+  }
+
   @override
   void initState() {
     super.initState();
+    final node0 = Node.Id(0);
     final node1 = Node.Id(1);
     final node2 = Node.Id(2);
     final node3 = Node.Id(3);
     final node4 = Node.Id(4);
     final node5 = Node.Id(5);
     final node6 = Node.Id(6);
-    final node8 = Node.Id(7);
-    final node7 = Node.Id(8);
+    final node7 = Node.Id(7);
+    final node8 = Node.Id(8);
     final node9 = Node.Id(9);
     final node10 = Node.Id(10);
     final node11 = Node.Id(11);
@@ -152,49 +175,38 @@ class _LayeredGraphViewPageState extends State<LayeredGraphViewPage> {
     final node19 = Node.Id(19);
     final node20 = Node.Id(20);
     final node21 = Node.Id(21);
-    final node22 = Node.Id(22);
-    final node23 = Node.Id(23);
 
-    graph.addEdge(node1, node13, paint: Paint()..color = Colors.red);
-    graph.addEdge(node1, node21);
-    graph.addEdge(node1, node4);
-    graph.addEdge(node1, node3);
-    graph.addEdge(node2, node3);
-    graph.addEdge(node2, node20);
-    graph.addEdge(node3, node4);
-    graph.addEdge(node3, node5);
-    graph.addEdge(node3, node23);
-    graph.addEdge(node4, node6);
-    graph.addEdge(node5, node7);
-    graph.addEdge(node6, node8);
-    graph.addEdge(node6, node16);
-    graph.addEdge(node6, node23);
-    graph.addEdge(node7, node9);
-    graph.addEdge(node8, node10);
-    graph.addEdge(node8, node11);
-    graph.addEdge(node9, node12);
-    graph.addEdge(node10, node13);
-    graph.addEdge(node10, node14);
-    graph.addEdge(node10, node15);
-    graph.addEdge(node11, node15);
-    graph.addEdge(node11, node16);
-    graph.addEdge(node12, node20);
-    graph.addEdge(node13, node17);
-    graph.addEdge(node14, node17);
-    graph.addEdge(node14, node18);
-    graph.addEdge(node16, node18);
-    graph.addEdge(node16, node19);
-    graph.addEdge(node16, node20);
-    graph.addEdge(node18, node21);
-    graph.addEdge(node19, node22);
-    graph.addEdge(node21, node23);
-    graph.addEdge(node22, node23);
-    graph.addEdge(node1, node22);
-    graph.addEdge(node7, node8);
+    // Adding edges based on parent-child relationships
+    graph.addEdge(node8, node0);
+    graph.addEdge(node2, node11);
+    graph.addEdge(node11, node3);
+    graph.addEdge(node12, node4);
+    graph.addEdge(node4, node9);
+    graph.addEdge(node18, node5);
+    graph.addEdge(node9, node6);
+    graph.addEdge(node15, node6);
+    graph.addEdge(node17, node6);
+    graph.addEdge(node3, node7);
+    graph.addEdge(node17, node7);
+    graph.addEdge(node20, node7);
+    graph.addEdge(node21, node7);
+    graph.addEdge(node0, node16);
+    graph.addEdge(node21, node10);
+    graph.addEdge(node16, node10);
+    graph.addEdge(node21, node12);
+    graph.addEdge(node4, node13);
+    graph.addEdge(node12, node13);
+    graph.addEdge(node1, node14);
+    graph.addEdge(node8, node14);
+    graph.addEdge(node9, node18);
+    graph.addEdge(node19, node17);
 
     builder
       ..nodeSeparation = (15)
       ..levelSeparation = (15)
       ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM;
+
+    // Set initial random node for navigation
+    nextNodeId = r.nextInt(22); // 0-21 nodes exist
   }
-}
+}
\ No newline at end of file
diff --git a/example/lib/layer_graphview_json.dart b/example/lib/layer_graphview_json.dart
index e368933..82958e2 100644
--- a/example/lib/layer_graphview_json.dart
+++ b/example/lib/layer_graphview_json.dart
@@ -1,3 +1,5 @@
+import 'dart:math';
+
 import 'package:flutter/material.dart';
 import 'package:graphview/GraphView.dart';
 
@@ -80,10 +82,18 @@ class _LayerGraphPageFromJsonState extends State<LayerGraphPageFromJson> {
       {
         "from": "161904647",
         "to": "598554018"
+      },
+      {
+        "from": "469600377",
+        "to": "254022114"
       }
     ]
   };
 
+  GraphViewController _controller = GraphViewController();
+  final Random r = Random();
+  int nextNodeId = 0;
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
@@ -148,32 +158,56 @@ class _LayerGraphPageFromJsonState extends State<LayerGraphPageFromJson> {
                     ],
                   ),
                 ),
+                ElevatedButton(
+                  onPressed: () => _navigateToRandomNode(),
+                  child: Text('Go to Node $nextNodeId'),
+                ),
+                ElevatedButton(
+                  onPressed: () => _controller.resetView(),
+                  child: Text('Reset View'),
+                ),
+                ElevatedButton(
+                  onPressed: () => _controller.zoomToFit(),
+                  child: Text("Zoom to fit"),
+                ),
               ],
             ),
             Expanded(
-              child: InteractiveViewer(
-                  constrained: false,
-                  boundaryMargin: EdgeInsets.all(100),
-                  minScale: 0.01,
-                  maxScale: 5.6,
-                  child: GraphView(
-                    graph: graph,
-                    algorithm: SugiyamaAlgorithm(builder),
-                    paint: Paint()
-                      ..color = Colors.green
-                      ..strokeWidth = 1
-                      ..style = PaintingStyle.stroke,
-                    builder: (Node node) {
-                      // I can decide what widget should be shown here based on the id
-                      var a = node.key!.value;
-                      return rectangleWidget(a, node);
-                    },
-                  )),
+              child: GraphView.builder(
+                controller: _controller,
+                graph: graph,
+                algorithm: SugiyamaAlgorithm(builder),
+                paint: Paint()
+                  ..color = Colors.green
+                  ..strokeWidth = 1
+                  ..style = PaintingStyle.stroke,
+                builder: (Node node) {
+                  // I can decide what widget should be shown here based on the id
+                  var a = node.key!.value;
+                  return rectangleWidget(a, node);
+                },
+              ),
             ),
           ],
         ));
   }
 
+  void _navigateToRandomNode() {
+    if (graph.nodes.isEmpty) return;
+
+    final randomNode = graph.nodes.firstWhere(
+          (node) => node.key != null && node.key!.value == nextNodeId,
+      orElse: () => graph.nodes.first,
+    );
+    final nodeId = randomNode.key!;
+    _controller.animateToNode(nodeId);
+
+    setState(() {
+      nextNodeId = r.nextInt(graph.nodes.length) + 1;
+    });
+  }
+
+
   Widget rectangleWidget(String? a, Node node) {
     return Container(
       color: Colors.amber,
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 87732db..28ca5ac 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -4,7 +4,7 @@ import 'package:example/tree_graphview_json.dart';
 import 'package:flutter/material.dart';
 import 'package:graphview/GraphView.dart';
 
-import 'directed_graphview.dart';
+import 'force_directed_graphview.dart';
 import 'graph_cluster_animated.dart';
 import 'layer_graphview_json.dart';
 import 'tree_graphview.dart';
@@ -16,247 +16,403 @@ void main() {
 class MyApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
-    return MaterialApp(home: Home());
+    return MaterialApp(
+      title: 'GraphView Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+        fontFamily: 'SF Pro Display',
+        visualDensity: VisualDensity.adaptivePlatformDensity,
+      ),
+      home: Home(),
+      debugShowCheckedModeBanner: false,
+    );
   }
 }
 
 class Home extends StatelessWidget {
-  const Home({
-    Key? key,
-  }) : super(key: key);
+  const Home({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return SafeArea(
-      child: Scaffold(
-        body: Center(
-          child: Column(children: [
-            SizedBox(
-              height: 20,
-            ),
-            MaterialButton(
-                color: Colors.redAccent,
-                shape: RoundedRectangleBorder(
-                  borderRadius: BorderRadius.circular(30),
-                ),
-                onPressed: () => Navigator.push(
-                      context,
-                      MaterialPageRoute(
-                        builder: (context) => TreeViewPage(),
-                      ),
-                    ),
-                child: Text(
-                  'Tree View (BuchheimWalker)',
-                  style: TextStyle(fontSize: 30),
-                )),
-            SizedBox(
-              height: 20,
+    return Scaffold(
+      body: Container(
+        decoration: BoxDecoration(
+          gradient: LinearGradient(
+            begin: Alignment.topLeft,
+            end: Alignment.bottomRight,
+            colors: [
+              Color(0xFF667eea),
+              Color(0xFF764ba2),
+            ],
+          ),
+        ),
+        child: SafeArea(
+          child: Column(
+            children: [
+              Expanded(
+                child: _buildScrollableContent(),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildScrollableContent() {
+    return SingleChildScrollView(
+      physics: BouncingScrollPhysics(),
+      padding: EdgeInsets.all(20),
+      child: Column(
+        children: [
+          _buildSection('Tree Algorithms', [
+            _buildButton(
+              'Tree View',
+              'BuchheimWalker Algorithm',
+              Icons.account_tree,
+              Colors.deepPurple,
+                  () => TreeViewPage(),
             ),
-            MaterialButton(
-                color: Colors.blueAccent,
-                shape: RoundedRectangleBorder(
-                  borderRadius: BorderRadius.circular(30),
-                ),
-                onPressed: () => Navigator.push(
-                      context,
-                      MaterialPageRoute(
-                        builder: (context) => GraphClusterViewPage(),
-                      ),
-                    ),
-                child: Text(
-                  'Graph Cluster View (FruchtermanReingold)',
-                  style: TextStyle(fontSize: 30),
-                )),
-            SizedBox(
-              height: 20,
+            _buildButton(
+              'Tree from JSON',
+              'Dynamic tree generation',
+              Icons.data_object,
+              Colors.indigo,
+                  () => TreeViewPageFromJson(),
             ),
-            MaterialButton(
-                color: Colors.greenAccent,
-                shape: RoundedRectangleBorder(
-                  borderRadius: BorderRadius.circular(30),
-                ),
-                onPressed: () => Navigator.push(
-                      context,
-                      MaterialPageRoute(
-                        builder: (context) => LayeredGraphViewPage(),
-                      ),
-                    ),
-                child: Text(
-                  "Layered View (Sugiyama)",
-                  style: TextStyle(fontSize: 30),
-                )),
-            SizedBox(
-              height: 20,
+          ]),
+
+          _buildSection('Layered Algorithms', [
+            _buildButton(
+              'Layered View',
+              'Sugiyama Algorithm',
+              Icons.layers,
+              Colors.teal,
+                  () => LayeredGraphViewPage(),
             ),
-            MaterialButton(
-                color: Colors.redAccent,
-                shape: RoundedRectangleBorder(
-                  borderRadius: BorderRadius.circular(30),
-                ),
-                onPressed: () => Navigator.push(
-                      context,
-                      MaterialPageRoute(
-                        builder: (context) => TreeViewPageFromJson(),
-                      ),
-                    ),
-                child: Text(
-                  'Tree View From Json(BuchheimWalker)',
-                  style: TextStyle(fontSize: 30),
-                )),
-            SizedBox(
-              height: 20,
+            _buildButton(
+              'Layer from JSON',
+              'JSON-based layered graphs',
+              Icons.timeline,
+              Colors.cyan,
+                  () => LayerGraphPageFromJson(),
             ),
-            MaterialButton(
-                color: Colors.redAccent,
-                shape: RoundedRectangleBorder(
-                  borderRadius: BorderRadius.circular(30),
-                ),
-                onPressed: () => Navigator.push(
-                  context,
-                  MaterialPageRoute(
-                    builder: (context) => LayerGraphPageFromJson(),
-                  ),
-                ),
-                child: Text(
-                  'Layer Graph From Json ',
-                  style: TextStyle(fontSize: 30),
-                )),
-            SizedBox(
-              height: 20,
+            _buildButton(
+              'Decision Tree',
+              'Decision-making visualization',
+              Icons.device_hub,
+              Colors.green,
+                  () => DecisionTreeScreen(),
             ),
-            MaterialButton(
-              onPressed: () {
-                var graph = new Graph();
-
-                Node node1 = Node.Id('One');
-                Node node2 = Node.Id('Two');
-                Node node3 = Node.Id('Three');
-                Node node4 = Node.Id('Four');
-                Node node5 = Node.Id('Five');
-                Node node6 = Node.Id('Six');
-                Node node7 = Node.Id('Seven');
-                Node node8 = Node.Id('Eight');
-                Node node9 = Node.Id('Nine');
-                graph.addEdge(node1, node2);
-                graph.addEdge(node1, node4);
-                graph.addEdge(node2, node3);
-                graph.addEdge(node2, node5);
-                graph.addEdge(node3, node6);
-                graph.addEdge(node4, node5);
-                graph.addEdge(node4, node7);
-                graph.addEdge(node5, node6);
-                graph.addEdge(node5, node8);
-                graph.addEdge(node6, node9);
-                graph.addEdge(node7, node8);
-                graph.addEdge(node8, node9);
-                var builder = FruchtermanReingoldAlgorithm();
+          ]),
 
-                Navigator.push(
-                  context,
-                  MaterialPageRoute(builder: (context) => GraphScreen(graph, builder, null)),
-                );
-              },
-              color: Colors.blueAccent,
-              shape: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(30),
-              ),
-              child: Text(
-                'Square Grid (FruchtermanReingold)',
-                style: TextStyle(fontSize: 30),
-              ),
+          _buildSection('Cluster Algorithms', [
+            _buildButton(
+              'Graph Cluster',
+              'FruchtermanReingold Algorithm',
+              Icons.bubble_chart,
+              Colors.orange,
+                  () => GraphClusterViewPage(),
             ),
-            SizedBox(
-              height: 20,
+            _buildCustomGraphButton(
+              'Square Grid',
+              'Structured 3x3 layout',
+              Icons.grid_3x3,
+              Colors.pink,
+              _createSquareGraph,
             ),
-            MaterialButton(
-              onPressed: () {
-                var graph = new Graph();
-
-                Node node1 = Node.Id('One');
-                Node node2 = Node.Id('Two');
-                Node node3 = Node.Id('Three');
-                Node node4 = Node.Id('Four');
-                Node node5 = Node.Id('Five');
-                Node node6 = Node.Id('Six');
-                Node node7 = Node.Id('Seven');
-                Node node8 = Node.Id('Eight');
-                Node node9 = Node.Id('Nine');
-                Node node10 = Node.Id('Ten');
+            _buildCustomGraphButton(
+              'Triangle Grid',
+              'Complex network topology',
+              Icons.change_history,
+              Colors.deepOrange,
+              _createTriangleGraph,
+            ),
+          ]),
 
-                graph.addEdge(node1, node2);
-                graph.addEdge(node1, node3);
-                graph.addEdge(node2, node4);
-                graph.addEdge(node2, node5);
-                graph.addEdge(node2, node3);
-                graph.addEdge(node3, node5);
-                graph.addEdge(node3, node6);
-                graph.addEdge(node4, node7);
-                graph.addEdge(node4, node8);
-                graph.addEdge(node4, node5);
-                graph.addEdge(node5, node8);
-                graph.addEdge(node5, node9);
-                graph.addEdge(node5, node6);
-                graph.addEdge(node9, node6);
-                graph.addEdge(node10, node6);
-                graph.addEdge(node7, node8);
-                graph.addEdge(node8, node9);
-                graph.addEdge(node9, node10);
 
-                var builder = FruchtermanReingoldAlgorithm();
+        ],
+      ),
+    );
+  }
 
-                Navigator.push(
-                  context,
-                  MaterialPageRoute(builder: (context) => GraphScreen(graph, builder, null)),
-                );
-              },
-              color: Colors.blueAccent,
-              shape: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(30),
-              ),
-              child: Text(
-                'Triangle Grid (FruchtermanReingold)',
-                style: TextStyle(fontSize: 30),
+  Widget _buildSection(String title, List<Widget> buttons) {
+    return Container(
+      margin: EdgeInsets.only(bottom: 24),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Padding(
+            padding: EdgeInsets.only(left: 4, bottom: 12),
+            child: Text(
+              title,
+              style: TextStyle(
+                fontSize: 20,
+                fontWeight: FontWeight.w600,
+                color: Colors.white,
+                letterSpacing: -0.3,
               ),
             ),
-            SizedBox(
-              height: 20,
+          ),
+          ...buttons.map((button) => Padding(
+            padding: EdgeInsets.only(bottom: 12),
+            child: button,
+          )),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildButton(
+      String title,
+      String subtitle,
+      IconData icon,
+      Color color,
+      Widget Function() pageBuilder,
+      ) {
+    return Builder(
+      builder: (context) => Container(
+        height: 80,
+        child: Material(
+          borderRadius: BorderRadius.circular(16),
+          elevation: 4,
+          shadowColor: Colors.black.withOpacity(0.1),
+          child: InkWell(
+            borderRadius: BorderRadius.circular(16),
+            onTap: () => Navigator.push(
+              context,
+              MaterialPageRoute(builder: (context) => pageBuilder()),
             ),
-            MaterialButton(
-              onPressed: () {
-                Navigator.push(
-                  context,
-                  MaterialPageRoute(builder: (context) => DecisionTreeScreen()),
-                );
-              },
-              color: Colors.greenAccent,
-              shape: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(30),
+            child: Container(
+              padding: EdgeInsets.all(16),
+              decoration: BoxDecoration(
+                borderRadius: BorderRadius.circular(16),
+                gradient: LinearGradient(
+                  begin: Alignment.centerLeft,
+                  end: Alignment.centerRight,
+                  colors: [
+                    color.withOpacity(0.1),
+                    Colors.white,
+                  ],
+                ),
+                border: Border.all(
+                  color: color.withOpacity(0.3),
+                  width: 1,
+                ),
               ),
-              child: Text(
-                'Decision Tree (Sugiyama)',
-                style: TextStyle(fontSize: 30),
+              child: Row(
+                children: [
+                  Container(
+                    width: 48,
+                    height: 48,
+                    decoration: BoxDecoration(
+                      color: color.withOpacity(0.1),
+                      borderRadius: BorderRadius.circular(12),
+                    ),
+                    child: Icon(
+                      icon,
+                      color: color,
+                      size: 24,
+                    ),
+                  ),
+                  SizedBox(width: 16),
+                  Expanded(
+                    child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Text(
+                          title,
+                          style: TextStyle(
+                            fontSize: 16,
+                            fontWeight: FontWeight.w600,
+                            color: Colors.grey[800],
+                          ),
+                        ),
+                        SizedBox(height: 2),
+                        Text(
+                          subtitle,
+                          style: TextStyle(
+                            fontSize: 13,
+                            color: Colors.grey[600],
+                            fontWeight: FontWeight.w400,
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                  Icon(
+                    Icons.arrow_forward_ios,
+                    color: Colors.grey[400],
+                    size: 16,
+                  ),
+                ],
               ),
             ),
-          ]),
+          ),
         ),
       ),
     );
   }
 
-  Widget createNode(String nodeText) {
-    return Container(
-      padding: EdgeInsets.all(10),
-      decoration: BoxDecoration(
-        color: Colors.red,
-        border: Border.all(color: Colors.white, width: 1),
-        borderRadius: BorderRadius.circular(30),
-      ),
-      child: Center(
-        child: Text(
-          nodeText,
-          style: TextStyle(fontSize: 10),
+  Widget _buildCustomGraphButton(
+      String title,
+      String subtitle,
+      IconData icon,
+      Color color,
+      Graph Function() graphBuilder,
+      ) {
+    return Builder(
+      builder: (context) => Container(
+        height: 80,
+        child: Material(
+          borderRadius: BorderRadius.circular(16),
+          elevation: 4,
+          shadowColor: Colors.black.withOpacity(0.1),
+          child: InkWell(
+            borderRadius: BorderRadius.circular(16),
+            onTap: () {
+              var graph = graphBuilder();
+              var builder = FruchtermanReingoldAlgorithm();
+              Navigator.push(
+                context,
+                MaterialPageRoute(
+                  builder: (context) => GraphScreen(graph, builder, null),
+                ),
+              );
+            },
+            child: Container(
+              padding: EdgeInsets.all(16),
+              decoration: BoxDecoration(
+                borderRadius: BorderRadius.circular(16),
+                gradient: LinearGradient(
+                  begin: Alignment.centerLeft,
+                  end: Alignment.centerRight,
+                  colors: [
+                    color.withOpacity(0.1),
+                    Colors.white,
+                  ],
+                ),
+                border: Border.all(
+                  color: color.withOpacity(0.3),
+                  width: 1,
+                ),
+              ),
+              child: Row(
+                children: [
+                  Container(
+                    width: 48,
+                    height: 48,
+                    decoration: BoxDecoration(
+                      color: color.withOpacity(0.1),
+                      borderRadius: BorderRadius.circular(12),
+                    ),
+                    child: Icon(
+                      icon,
+                      color: color,
+                      size: 24,
+                    ),
+                  ),
+                  SizedBox(width: 16),
+                  Expanded(
+                    child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Text(
+                          title,
+                          style: TextStyle(
+                            fontSize: 16,
+                            fontWeight: FontWeight.w600,
+                            color: Colors.grey[800],
+                          ),
+                        ),
+                        SizedBox(height: 2),
+                        Text(
+                          subtitle,
+                          style: TextStyle(
+                            fontSize: 13,
+                            color: Colors.grey[600],
+                            fontWeight: FontWeight.w400,
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                  Icon(
+                    Icons.arrow_forward_ios,
+                    color: Colors.grey[400],
+                    size: 16,
+                  ),
+                ],
+              ),
+            ),
+          ),
         ),
       ),
     );
   }
-}
+
+  Graph _createSquareGraph() {
+    var graph = Graph();
+    Node node1 = Node.Id('One');
+    Node node2 = Node.Id('Two');
+    Node node3 = Node.Id('Three');
+    Node node4 = Node.Id('Four');
+    Node node5 = Node.Id('Five');
+    Node node6 = Node.Id('Six');
+    Node node7 = Node.Id('Seven');
+    Node node8 = Node.Id('Eight');
+    Node node9 = Node.Id('Nine');
+
+    graph.addEdge(node1, node2);
+    graph.addEdge(node1, node4);
+    graph.addEdge(node2, node3);
+    graph.addEdge(node2, node5);
+    graph.addEdge(node3, node6);
+    graph.addEdge(node4, node5);
+    graph.addEdge(node4, node7);
+    graph.addEdge(node5, node6);
+    graph.addEdge(node5, node8);
+    graph.addEdge(node6, node9);
+    graph.addEdge(node7, node8);
+    graph.addEdge(node8, node9);
+
+    return graph;
+  }
+
+  Graph _createTriangleGraph() {
+    var graph = Graph();
+    Node node1 = Node.Id('One');
+    Node node2 = Node.Id('Two');
+    Node node3 = Node.Id('Three');
+    Node node4 = Node.Id('Four');
+    Node node5 = Node.Id('Five');
+    Node node6 = Node.Id('Six');
+    Node node7 = Node.Id('Seven');
+    Node node8 = Node.Id('Eight');
+    Node node9 = Node.Id('Nine');
+    Node node10 = Node.Id('Ten');
+
+    graph.addEdge(node1, node2);
+    graph.addEdge(node1, node3);
+    graph.addEdge(node2, node4);
+    graph.addEdge(node2, node5);
+    graph.addEdge(node2, node3);
+    graph.addEdge(node3, node5);
+    graph.addEdge(node3, node6);
+    graph.addEdge(node4, node7);
+    graph.addEdge(node4, node8);
+    graph.addEdge(node4, node5);
+    graph.addEdge(node5, node8);
+    graph.addEdge(node5, node9);
+    graph.addEdge(node5, node6);
+    graph.addEdge(node9, node6);
+    graph.addEdge(node10, node6);
+    graph.addEdge(node7, node8);
+    graph.addEdge(node8, node9);
+    graph.addEdge(node9, node10);
+
+    return graph;
+  }
+}
\ No newline at end of file
diff --git a/example/lib/tree_graphview.dart b/example/lib/tree_graphview.dart
index 42444c3..4684a1e 100644
--- a/example/lib/tree_graphview.dart
+++ b/example/lib/tree_graphview.dart
@@ -8,14 +8,22 @@ class TreeViewPage extends StatefulWidget {
   _TreeViewPageState createState() => _TreeViewPageState();
 }
 
-class _TreeViewPageState extends State<TreeViewPage> {
+class _TreeViewPageState extends State<TreeViewPage> with TickerProviderStateMixin {
+
+  GraphViewController _controller = GraphViewController();
+  final Random r = Random();
+  int nextNodeId = 1;
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-        appBar: AppBar(),
+        appBar: AppBar(
+          title: Text('Tree View'),
+        ),
         body: Column(
           mainAxisSize: MainAxisSize.max,
           children: [
+            // Configuration controls
             Wrap(
               children: [
                 Container(
@@ -71,39 +79,50 @@ class _TreeViewPageState extends State<TreeViewPage> {
                     setState(() {});
                   },
                   child: Text('Add'),
-                )
+                ),
+                ElevatedButton(
+                  onPressed: _navigateToRandomNode,
+                  child: Text('Go to Node $nextNodeId'),
+                ),
+                SizedBox(width: 8),
+                ElevatedButton(
+                  onPressed: _resetView,
+                  child: Text('Reset View'),
+                ),
+                SizedBox(width: 8,),
+                ElevatedButton(onPressed: (){
+                  _controller.zoomToFit();
+                }, child: Text("Zoom to fit"))
               ],
             ),
+
             Expanded(
-              child: InteractiveViewer(
-                  constrained: false,
-                  boundaryMargin: EdgeInsets.all(100),
-                  minScale: 0.01,
-                  maxScale: 5.6,
-                  child: GraphView(
-                    graph: graph,
-                    algorithm: BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)),
-                    paint: Paint()
-                      ..color = Colors.green
-                      ..strokeWidth = 1
-                      ..style = PaintingStyle.stroke,
-                    builder: (Node node) {
-                      // I can decide what widget should be shown here based on the id
-                      var a = node.key!.value as int?;
-                      return rectangleWidget(a);
-                    },
-                  )),
+              child: GraphView.builder(
+                controller: _controller,
+                graph: graph,
+                algorithm: BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)),
+                builder: (Node node) => InkWell(
+                  onTap: () => _controller.animateToNode(node.key?.value),
+                  child: Container(
+                    padding: EdgeInsets.all(16),
+                    decoration: BoxDecoration(
+                      color: Colors.white,
+                      borderRadius: BorderRadius.circular(4),
+                      boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)],
+                    ),
+                    child: Text('Node ${node.key?.value} \n${graph.nodes.firstWhere((n) => n.key == node.key).position}'),
+                  ),
+                ),
+              ),
             ),
           ],
         ));
   }
 
-  Random r = Random();
-
   Widget rectangleWidget(int? a) {
     return InkWell(
       onTap: () {
-        print('clicked');
+        print('clicked node $a');
       },
       child: Container(
           padding: EdgeInsets.all(16),
@@ -113,15 +132,36 @@ class _TreeViewPageState extends State<TreeViewPage> {
               BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),
             ],
           ),
-          child: Text('Node ${a}')),
+          child: Text('Node ${a} ')),
     );
   }
 
   final Graph graph = Graph()..isTree = true;
   BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();
 
+  void _navigateToRandomNode() {
+    if (graph.nodes.isEmpty) return;
+
+    final randomNode = graph.nodes.firstWhere(
+      (node) => node.key != null && node.key!.value == nextNodeId,
+      orElse: () => graph.nodes.first,
+    );
+    final nodeId = randomNode.key!;
+    _controller.animateToNode(nodeId);
+
+    setState(() {
+      nextNodeId = r.nextInt(graph.nodes.length) + 1;
+    });
+  }
+
+  void _resetView() {
+    _controller.resetView();
+  }
+
   @override
   void initState() {
+    super.initState();
+
     final node1 = Node.Id(1);
     final node2 = Node.Id(2);
     final node3 = Node.Id(3);
@@ -152,4 +192,5 @@ class _TreeViewPageState extends State<TreeViewPage> {
       ..subtreeSeparation = (150)
       ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);
   }
-}
+
+}
\ No newline at end of file
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index e4cfe51..64e6536 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 version: 1.0.0+1
 
 environment:
-  sdk: '>=2.12.0 <3.0.0'
+  sdk: '>=2.12.0 <4.0.0'
 
 dependencies:
   flutter:
diff --git a/lib/GraphView.dart b/lib/GraphView.dart
index 0e7e015..3f9701f 100644
--- a/lib/GraphView.dart
+++ b/lib/GraphView.dart
@@ -1,8 +1,8 @@
 library graphview;
 
+import 'dart:async';
 import 'dart:collection';
 import 'dart:math';
-import 'dart:async';
 import 'dart:convert';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
@@ -40,42 +40,202 @@ part 'tree/TreeEdgeRenderer.dart';
 
 typedef NodeWidgetBuilder = Widget Function(Node node);
 
+class GraphViewController {
+  _GraphViewState? _state;
+  final TransformationController? transformationController;
+
+  GraphViewController({this.transformationController});
+
+  void _attach(_GraphViewState state) => _state = state;
+
+  void _detach() => _state = null;
+
+  void animateToNode(ValueKey key) => _state?.animateToNode(key);
+
+  void animateToMatrix(Matrix4 targetMatrix) =>
+      _state?.animateToMatrix(targetMatrix);
+
+  void resetView() => _state?.resetView();
+
+  void zoomToFit() => _state?.zoomToFit();
+}
+
 class GraphView extends StatefulWidget {
   final Graph graph;
   final Algorithm algorithm;
   final Paint? paint;
   final NodeWidgetBuilder builder;
   final bool animated;
-
-  GraphView(
-      {Key? key, required this.graph, required this.algorithm, this.paint, required this.builder, this.animated = true})
-      : super(key: key);
+  final GraphViewController? controller;
+  final bool _isBuilder;
+
+  GraphView({
+    Key? key,
+    required this.graph,
+    required this.algorithm,
+    this.paint,
+    required this.builder,
+    this.animated = true,
+  })  : controller = null,
+        _isBuilder = false,
+        super(key: key);
+
+  GraphView.builder({
+    Key? key,
+    required this.graph,
+    required this.algorithm,
+    this.paint,
+    required this.builder,
+    this.controller,
+    this.animated = true,
+  })  : _isBuilder = true,
+        super(key: key);
 
   @override
   _GraphViewState createState() => _GraphViewState();
 }
 
-class _GraphViewState extends State<GraphView> {
+class _GraphViewState extends State<GraphView> with TickerProviderStateMixin {
+  TransformationController _transformationController = TransformationController();
+
+  // Separate animation controllers
+  late final AnimationController
+      _cameraAnimationController; // For camera movements
+
+  Animation<Matrix4>? _animationMove;
+  late final AnimationController _animationController;
+
+  @override
+  void initState() {
+    super.initState();
+
+    if (widget.controller?.transformationController != null) {
+      _transformationController.dispose();
+      _transformationController = widget.controller!.transformationController!;
+    }
+    // Initialize camera animation controller (for animateToNode, zoomToFit, etc.)
+    _cameraAnimationController = AnimationController(
+      vsync: this,
+      duration: const Duration(milliseconds: 800),
+    );
+    widget.controller?._attach(this);
+  }
+
+  @override
+  void dispose() {
+    widget.controller?._detach();
+    _cameraAnimationController.dispose();
+
+    if (widget.controller?.transformationController == null) {
+      _transformationController.dispose();
+    }
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
-    if (widget.algorithm is FruchtermanReingoldAlgorithm) {
-      return _GraphViewAnimated(
-        key: widget.key,
-        graph: widget.graph,
-        algorithm: widget.algorithm,
-        paint: widget.paint,
-        builder: widget.builder,
-      );
-    } else {
-      return _GraphView(
-        key: widget.key,
-        graph: widget.graph,
-        algorithm: widget.algorithm,
-        paint: widget.paint,
-        builder: widget.builder,
+    final graphView = _GraphView(
+      graph: widget.graph,
+      algorithm: widget.algorithm,
+      paint: widget.paint,
+      builder: widget.builder,
+    );
+
+    if (widget._isBuilder) {
+      return InteractiveViewer(
+        transformationController: _transformationController,
+        constrained: false,
+        boundaryMargin: EdgeInsets.all(double.infinity),
+        minScale: 0.01,
+        maxScale: 5.6,
+        child: graphView,
       );
     }
+
+    return graphView;
+  }
+
+  void animateToNode(ValueKey key) {
+    final node = widget.graph.nodes.cast<Node?>().firstWhere(
+          (n) => n?.key == key,
+          orElse: () => null,
+        );
+    if (node == null) return;
+
+    final renderBox = context.findRenderObject() as RenderBox?;
+    if (renderBox == null) return;
+
+    final viewportSize = renderBox.size;
+    final centerX = viewportSize.width / 2;
+    final centerY = viewportSize.height / 2;
+    final nodeCenter = Offset(
+      node.position.dx + node.width / 2,
+      node.position.dy + node.height / 2,
+    );
+
+    final targetMatrix = Matrix4.identity()
+      ..translate(centerX - nodeCenter.dx, centerY - nodeCenter.dy);
+
+    animateToMatrix(targetMatrix);
+  }
+
+  void resetView() => animateToMatrix(Matrix4.identity());
+
+  void zoomToFit() {
+    if (widget.graph.nodes.isEmpty) return;
+    final renderBox = context.findRenderObject() as RenderBox?;
+    if (renderBox == null) return;
+
+    final viewportSize = renderBox.size;
+    final bounds = _calculateGraphBounds();
+
+    final scale = (viewportSize.shortestSide * 0.8 / bounds.longestSide).clamp(0.01, 5.6);
+    final centerOffset = Offset(
+      viewportSize.width / 2 - bounds.center.dx * scale,
+      viewportSize.height / 2 - bounds.center.dy * scale,
+    );
+
+    final targetMatrix = Matrix4.identity()
+      ..translate(centerOffset.dx, centerOffset.dy)
+      ..scale(scale);
+
+    animateToMatrix(targetMatrix);
+  }
+
+  Rect _calculateGraphBounds() {
+    if (widget.graph.nodes.isEmpty) return Rect.zero;
+
+    final positions = widget.graph.nodes.map((n) => n.position);
+    final left = positions.map((p) => p.dx).reduce((a, b) => a < b ? a : b);
+    final top = positions.map((p) => p.dy).reduce((a, b) => a < b ? a : b);
+    final right = positions.map((p) => p.dx + 100).reduce((a, b) => a > b ? a : b);
+    final bottom = positions.map((p) => p.dy + 50).reduce((a, b) => a > b ? a : b);
+
+    return Rect.fromLTRB(left, top, right, bottom);
+  }
+
+  void animateToMatrix(Matrix4 targetMatrix) {
+    _cameraAnimationController.reset();
+    _animationMove = Matrix4Tween(
+      begin: _transformationController.value,
+      end: targetMatrix,
+    ).animate(CurvedAnimation(
+      parent: _cameraAnimationController,
+      curve: Curves.easeInOut,
+    ));
+    _animationMove!.addListener(_onAnimateMove);
+    _cameraAnimationController.forward();
+  }
+
+  void _onAnimateMove() {
+    _transformationController.value = _animationMove!.value;
+    if (!_cameraAnimationController.isAnimating) {
+      _animationMove!.removeListener(_onAnimateMove);
+      _animationMove = null;
+      _cameraAnimationController.reset();
+    }
   }
+
 }
 
 class _GraphView extends MultiChildRenderObjectWidget {
@@ -194,7 +354,7 @@ class RenderCustomLayoutBox extends RenderBox
       position++;
     }
 
-    size = algorithm.run(graph, 10, 10);
+    size = algorithm.run(graph, 0, 0);
 
     child = firstChild;
     position = 0;
diff --git a/lib/edgerenderer/ArrowEdgeRenderer.dart b/lib/edgerenderer/ArrowEdgeRenderer.dart
index 1072311..35d85d7 100644
--- a/lib/edgerenderer/ArrowEdgeRenderer.dart
+++ b/lib/edgerenderer/ArrowEdgeRenderer.dart
@@ -6,6 +6,11 @@ const double ARROW_LENGTH = 10;
 class ArrowEdgeRenderer extends EdgeRenderer {
   var trianglePath = Path();
 
+  Offset _getNodeCenter(Node node) {
+    final nodePosition = getNodePosition(node);
+    return Offset(nodePosition.dx + node.width * 0.5, nodePosition.dy + node.height * 0.5);
+  }
+
   @override
   void render(Canvas canvas, Graph graph, Paint paint) {
     var trianglePaint = Paint()
@@ -16,22 +21,16 @@ class ArrowEdgeRenderer extends EdgeRenderer {
       var source = edge.source;
       var destination = edge.destination;
 
-      var sourceOffset = source.position;
-
-      var x1 = sourceOffset.dx;
-      var y1 = sourceOffset.dy;
+      var sourceOffset = getNodePosition(source);
+      var destinationOffset = getNodePosition(destination);
 
-      var destinationOffset = destination.position;
+      var startX = sourceOffset.dx + source.width * 0.5;
+      var startY = sourceOffset.dy + source.height * 0.5;
+      var stopX = destinationOffset.dx + destination.width * 0.5;
+      var stopY = destinationOffset.dy + destination.height * 0.5;
 
-      var x2 = destinationOffset.dx;
-      var y2 = destinationOffset.dy;
-
-      var startX = x1 + source.width / 2;
-      var startY = y1 + source.height / 2;
-      var stopX = x2 + destination.width / 2;
-      var stopY = y2 + destination.height / 2;
-
-      var clippedLine = clipLine(startX, startY, stopX, stopY, destination);
+      var clippedLine = clipLineEnd(startX, startY, stopX, stopY, destinationOffset.dx,
+          destinationOffset.dy, destination.width, destination.height);
 
       Paint? edgeTrianglePaint;
       if (edge.paint != null) {
@@ -41,67 +40,141 @@ class ArrowEdgeRenderer extends EdgeRenderer {
       }
 
       var triangleCentroid = drawTriangle(
-          canvas, edgeTrianglePaint ?? trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]);
-
-      canvas.drawLine(Offset(clippedLine[0], clippedLine[1]), Offset(triangleCentroid[0], triangleCentroid[1]),
+          canvas,
+          edgeTrianglePaint ?? trianglePaint,
+          clippedLine[0],
+          clippedLine[1],
+          clippedLine[2],
+          clippedLine[3]);
+
+      canvas.drawLine(
+          Offset(clippedLine[0], clippedLine[1]),
+          triangleCentroid,
           edge.paint ?? paint);
     });
   }
 
-  List<double> drawTriangle(Canvas canvas, Paint paint, double x1, double y1, double x2, double y2) {
-    var angle = (atan2(y2 - y1, x2 - x1) + pi);
-    var x3 = (x2 + ARROW_LENGTH * cos((angle - ARROW_DEGREES)));
-    var y3 = (y2 + ARROW_LENGTH * sin((angle - ARROW_DEGREES)));
-    var x4 = (x2 + ARROW_LENGTH * cos((angle + ARROW_DEGREES)));
-    var y4 = (y2 + ARROW_LENGTH * sin((angle + ARROW_DEGREES)));
-    trianglePath.moveTo(x2, y2); // Top;
-    trianglePath.lineTo(x3, y3); // Bottom left
-    trianglePath.lineTo(x4, y4); // Bottom right
-    trianglePath.close();
+  Offset drawTriangle(
+      Canvas canvas, Paint paint, double lineStartX, double lineStartY, double arrowTipX, double arrowTipY) {
+
+    // Calculate direction from line start to arrow tip, then flip 180° to point backwards from tip
+    var lineDirection = (atan2(arrowTipY - lineStartY, arrowTipX - lineStartX) + pi);
+
+    // Calculate the two base points of the arrowhead triangle
+    var leftWingX = (arrowTipX + ARROW_LENGTH * cos((lineDirection - ARROW_DEGREES)));
+    var leftWingY = (arrowTipY + ARROW_LENGTH * sin((lineDirection - ARROW_DEGREES)));
+    var rightWingX = (arrowTipX + ARROW_LENGTH * cos((lineDirection + ARROW_DEGREES)));
+    var rightWingY = (arrowTipY + ARROW_LENGTH * sin((lineDirection + ARROW_DEGREES)));
+
+    // Draw the triangle: tip -> left wing -> right wing -> back to tip
+    trianglePath.moveTo(arrowTipX, arrowTipY);    // Arrow tip
+    trianglePath.lineTo(leftWingX, leftWingY);    // Left wing
+    trianglePath.lineTo(rightWingX, rightWingY);  // Right wing
+    trianglePath.close();                         // Back to tip
     canvas.drawPath(trianglePath, paint);
 
-    // calculate centroid of the triangle
-    var x = (x2 + x3 + x4) / 3;
-    var y = (y2 + y3 + y4) / 3;
-    var triangleCentroid = [x, y];
+    // Calculate center point of the triangle
+    var triangleCenterX = (arrowTipX + leftWingX + rightWingX) / 3;
+    var triangleCenterY = (arrowTipY + leftWingY + rightWingY) / 3;
+
     trianglePath.reset();
-    return triangleCentroid;
+    return Offset(triangleCenterX, triangleCenterY);
+  }
+
+  List<double> clipLineEnd(
+      double startX,
+      double startY,
+      double stopX,
+      double stopY,
+      double destX,
+      double destY,
+      double destWidth,
+      double destHeight) {
+    var clippedStopX = stopX;
+    var clippedStopY = stopY;
+
+    if (startX == stopX && startY == stopY) {
+      return [startX, startY, clippedStopX, clippedStopY];
+    }
+
+    var slope = (startY - stopY) / (startX - stopX);
+    final halfHeight = destHeight * 0.5;
+    final halfWidth = destWidth * 0.5;
+
+    // Check vertical edge intersections
+    if (startX != stopX) {
+      final halfSlopeWidth = slope * halfWidth;
+      if (halfSlopeWidth.abs() <= halfHeight) {
+        if (destX > startX) {
+          // Left edge intersection
+          return [startX, startY,stopX - halfWidth, stopY - halfSlopeWidth];
+        } else if (destX < startX) {
+          // Right edge intersection
+          return [startX, startY, stopX + halfWidth, stopY + halfSlopeWidth];
+        }
+      }
+    }
+
+    // Check horizontal edge intersections
+    if (startY != stopY && slope != 0) {
+      final halfSlopeHeight = halfHeight / slope;
+      if (halfSlopeHeight.abs() <= halfWidth) {
+        if (destY < startY) {
+          // Bottom edge intersection
+          clippedStopX = stopX + halfSlopeHeight;
+          clippedStopY = stopY + halfHeight;
+        } else if (destY > startY) {
+          // Top edge intersection
+          clippedStopX = stopX - halfSlopeHeight;
+          clippedStopY = stopY - halfHeight;
+        }
+      }
+    }
+
+    return [startX, startY, clippedStopX, clippedStopY];
   }
 
-  List<double> clipLine(double startX, double startY, double stopX, double stopY, Node destination) {
-    var resultLine = List.filled(4, 0.0);
-    resultLine[0] = startX;
-    resultLine[1] = startY;
+  List<double> clipLine(double startX, double startY, double stopX,
+      double stopY, Node destination) {
+    final resultLine = [startX, startY, stopX, stopY];
+
+    if (startX == stopX && startY == stopY) return resultLine;
 
     var slope = (startY - stopY) / (startX - stopX);
-    var halfHeight = destination.height / 2;
-    var halfWidth = destination.width / 2;
-    var halfSlopeWidth = slope * halfWidth;
-    var halfSlopeHeight = halfHeight / slope;
-
-    if (-halfHeight <= halfSlopeWidth && halfSlopeWidth <= halfHeight) {
-      // line intersects with ...
-      if (destination.x > startX) {
-        // left edge
-        resultLine[2] = stopX - halfWidth;
-        resultLine[3] = stopY - halfSlopeWidth;
-      } else if (destination.x < startX) {
-        // right edge
-        resultLine[2] = stopX + halfWidth;
-        resultLine[3] = stopY + halfSlopeWidth;
+    final halfHeight = destination.height * 0.5;
+    final halfWidth = destination.width * 0.5;
+
+    // Check vertical edge intersections
+    if (startX != stopX) {
+      final halfSlopeWidth = slope * halfWidth;
+      if (halfSlopeWidth.abs() <= halfHeight) {
+        if (destination.x > startX) {
+          // Left edge intersection
+          resultLine[2] = stopX - halfWidth;
+          resultLine[3] = stopY - halfSlopeWidth;
+          return resultLine;
+        } else if (destination.x < startX) {
+          // Right edge intersection
+          resultLine[2] = stopX + halfWidth;
+          resultLine[3] = stopY + halfSlopeWidth;
+          return resultLine;
+        }
       }
     }
 
-    if (-halfWidth <= halfSlopeHeight && halfSlopeHeight <= halfWidth) {
-      // line intersects with ...
-      if (destination.y < startY) {
-        // bottom edge
-        resultLine[2] = stopX + halfSlopeHeight;
-        resultLine[3] = stopY + halfHeight;
-      } else if (destination.y > startY) {
-        // top edge
-        resultLine[2] = stopX - halfSlopeHeight;
-        resultLine[3] = stopY - halfHeight;
+    // Check horizontal edge intersections
+    if (startY != stopY && slope != 0) {
+      final halfSlopeHeight = halfHeight / slope;
+      if (halfSlopeHeight.abs() <= halfWidth) {
+        if (destination.y < startY) {
+          // Bottom edge intersection
+          resultLine[2] = stopX + halfSlopeHeight;
+          resultLine[3] = stopY + halfHeight;
+        } else if (destination.y > startY) {
+          // Top edge intersection
+          resultLine[2] = stopX - halfSlopeHeight;
+          resultLine[3] = stopY - halfHeight;
+        }
       }
     }
 
diff --git a/lib/edgerenderer/EdgeRenderer.dart b/lib/edgerenderer/EdgeRenderer.dart
index eb2135a..5b2c73d 100644
--- a/lib/edgerenderer/EdgeRenderer.dart
+++ b/lib/edgerenderer/EdgeRenderer.dart
@@ -1,6 +1,8 @@
 part of graphview;
 
 abstract class EdgeRenderer {
+  Offset getNodePosition(Node node) => node.position;
+
   void render(Canvas canvas, Graph graph, Paint paint);
 }
 
diff --git a/lib/layered/SugiyamaEdgeRenderer.dart b/lib/layered/SugiyamaEdgeRenderer.dart
index 9d1c148..bbea871 100644
--- a/lib/layered/SugiyamaEdgeRenderer.dart
+++ b/lib/layered/SugiyamaEdgeRenderer.dart
@@ -5,31 +5,21 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer {
   Map<Edge, SugiyamaEdgeData> edgeData;
   BendPointShape bendPointShape;
   bool addTriangleToEdge;
+  var path = Path();
 
   SugiyamaEdgeRenderer(this.nodeData, this.edgeData, this.bendPointShape, this.addTriangleToEdge);
 
-  var path = Path();
+  bool hasBendEdges(Edge edge) => edgeData.containsKey(edge) && edgeData[edge]!.bendPoints.isNotEmpty;
 
   @override
   void render(Canvas canvas, Graph graph, Paint paint) {
     var trianglePaint = Paint()
       ..color = paint.color
       ..style = PaintingStyle.fill;
-      
 
     graph.edges.forEach((edge) {
       final source = edge.source;
-
-      var x = source.x;
-      var y = source.y;
-
-      var destination = edge.destination;
-
-      var x1 = destination.x;
-      var y1 = destination.y;
-      path.reset();
-
-      var clippedLine = <double>[];
+      final destination = edge.destination;
 
       Paint? edgeTrianglePaint;
       if (edge.paint != null) {
@@ -41,108 +31,107 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer {
       var currentPaint = edge.paint ?? paint
         ..style = PaintingStyle.stroke;
 
-      if (edgeData.containsKey(edge) && edgeData[edge]!.bendPoints.isNotEmpty) {
-        // draw bend points
-        var bendPoints = edgeData[edge]!.bendPoints;
-        final size = bendPoints.length;
+      if (hasBendEdges(edge)) {
+        _renderEdgeWithBendPoints(canvas, edge, source, destination, currentPaint, edgeTrianglePaint ?? trianglePaint);
+      } else {
+        _renderStraightEdge(canvas, edge, source, destination, currentPaint, edgeTrianglePaint ?? trianglePaint);
+      }
+    });
+  }
 
-        if (nodeData[source]!.isReversed) {
-          clippedLine = clipLine(bendPoints[2], bendPoints[3], bendPoints[0], bendPoints[1], destination);
-        } else {
-          clippedLine = clipLine(
-              bendPoints[size - 4], bendPoints[size - 3], bendPoints[size - 2], bendPoints[size - 1], destination);
-        }
+  void _renderEdgeWithBendPoints(Canvas canvas, Edge edge, Node source,
+      Node destination, Paint currentPaint, Paint trianglePaint) {
+    var bendPoints = edgeData[edge]!.bendPoints;
 
-        path.reset();
-        path.moveTo(bendPoints[0], bendPoints[1]);
+    var sourceCenter = _getNodeCenter(source);
 
-        final bendPointsWithoutDuplication = <Offset>[];
+    // Calculate the transition/offset from the original bend point to animated position
+    final transitionDx = sourceCenter.dx - bendPoints[0];
+    final transitionDy = sourceCenter.dy - bendPoints[1];
 
-        for (var i = 0; i < bendPoints.length; i += 2) {
-          final isLastPoint = i == bendPoints.length - 2;
+    path.reset();
+    path.moveTo(sourceCenter.dx, sourceCenter.dy);
 
-          final x = bendPoints[i];
-          final y = bendPoints[i + 1];
-          final x2 = isLastPoint ? -1 : bendPoints[i + 2];
-          final y2 = isLastPoint ? -1 : bendPoints[i + 3];
-          if (x == x2 && y == y2) {
-            // Skip when two consecutive points are identical
-            // because drawing a line between would be redundant in this case.
-            continue;
-          }
-          bendPointsWithoutDuplication.add(Offset(x, y));
-        }
+    final bendPointsWithoutDuplication = <Offset>[];
 
-        if (bendPointShape is MaxCurvedBendPointShape) {
-          _drawMaxCurvedBendPointsEdge(bendPointsWithoutDuplication);
-        } else if (bendPointShape is CurvedBendPointShape) {
-          final shape = bendPointShape as CurvedBendPointShape;
-          _drawCurvedBendPointsEdge(bendPointsWithoutDuplication, shape.curveLength);
-        } else {
-          _drawSharpBendPointsEdge(bendPointsWithoutDuplication);
-        }
+    for (var i = 0; i < bendPoints.length; i += 2) {
+      final isLastPoint = i == bendPoints.length - 2;
 
-        if (addTriangleToEdge) {
-          final triangleCentroid = drawTriangle(
-              canvas, edgeTrianglePaint ?? trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]);
+      // Apply the same transition to all bend points
+      final x = bendPoints[i] + transitionDx;
+      final y = bendPoints[i + 1] + transitionDy;
+      final x2 = isLastPoint ? -1 : bendPoints[i + 2] + transitionDx;
+      final y2 = isLastPoint ? -1 : bendPoints[i + 3] + transitionDy;
 
-          path.lineTo(triangleCentroid[0], triangleCentroid[1]);
-        } else {
-          final stopX = x1 + destination.width / 2;
-          final stopY = y1 + destination.height / 2;
-          path.lineTo(stopX, stopY);
-        }
-        canvas.drawPath(path, currentPaint);
-      } else {
-        final startX = x + source.width / 2;
-        final startY = y + source.height / 2;
-        final stopX = x1 + destination.width / 2;
-        final stopY = y1 + destination.height / 2;
+      if (x == x2 && y == y2) {
+        // Skip when two consecutive points are identical
+        // because drawing a line between would be redundant in this case.
+        continue;
+      }
+      bendPointsWithoutDuplication.add(Offset(x, y));
+    }
 
-        clippedLine = clipLine(startX, startY, stopX, stopY, destination);
+    if (bendPointShape is MaxCurvedBendPointShape) {
+      _drawMaxCurvedBendPointsEdge(bendPointsWithoutDuplication);
+    } else if (bendPointShape is CurvedBendPointShape) {
+      final shape = bendPointShape as CurvedBendPointShape;
+      _drawCurvedBendPointsEdge(bendPointsWithoutDuplication, shape.curveLength);
+    } else {
+      _drawSharpBendPointsEdge(bendPointsWithoutDuplication);
+    }
 
-        var destinationPoint = Offset(stopX, stopY);
+    var descOffset = getNodePosition(destination);
+    var stopX = descOffset.dx + destination.width * 0.5;
+    var stopY = descOffset.dy + destination.height * 0.5;
 
-        if (addTriangleToEdge) {
-          final triangleCentroid = drawTriangle(
-              canvas, edgeTrianglePaint ?? trianglePaint, clippedLine[0],
-              clippedLine[1], clippedLine[2], clippedLine[3]);
+    if (addTriangleToEdge) {
+      var clippedLine = <double>[];
+      final size = bendPoints.length;
+      if (nodeData[source]!.isReversed) {
+        clippedLine = clipLineEnd(bendPoints[2], bendPoints[3],  stopX, stopY, destination.x ,
+            destination.y, destination.width, destination.height);
+      } else {
+        clippedLine = clipLineEnd(bendPoints[size - 4], bendPoints[size - 3],
+            stopX, stopY, descOffset.dx,
+            descOffset.dy, destination.width, destination.height);
+      }
+      final triangleCentroid = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]);
+      path.lineTo(triangleCentroid.dx, triangleCentroid.dy);
+    } else {
+      path.lineTo(stopX, stopY);
+    }
+    canvas.drawPath(path, currentPaint);
+  }
 
-          destinationPoint = Offset(triangleCentroid[0], triangleCentroid[1]);
-        }
+  void _renderStraightEdge(Canvas canvas, Edge edge, Node source,
+      Node destination, Paint currentPaint, Paint trianglePaint) {
+    final sourceCenter = _getNodeCenter(source);
+    var destCenter = _getNodeCenter(destination);
 
-        // Draw the line
-        switch (nodeData[destination]?.lineType) {
-          case LineType.DashedLine:
-            _drawDashedLine(
-                canvas,
-                Offset(clippedLine[0], clippedLine[1]), destinationPoint,
-                currentPaint, 0.6
-            );
-            break;
-          case LineType.DottedLine:
-            // dotted line uses the same method as dashed line, but with a lineLength of 0.0
-            _drawDashedLine(
-                canvas,
-                Offset(clippedLine[0], clippedLine[1]), destinationPoint,
-                currentPaint, 0.0
-            );
-            break;
-          case LineType.SineLine:
-            _drawSineLine(
-                canvas,
-                Offset(clippedLine[0], clippedLine[1]), destinationPoint,
-                currentPaint
-            );
-            break;
-          default:
-            canvas.drawLine(
-                Offset(clippedLine[0], clippedLine[1]), destinationPoint,
-                currentPaint
-            );
-            break;
-        }
-    }});
+    if (addTriangleToEdge) {
+      final clippedLine = clipLineEnd(sourceCenter.dx, sourceCenter.dy,
+          destCenter.dx, destCenter.dy, destination.x,
+          destination.y, destination.width, destination.height);
+
+      destCenter = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]);
+    }
+
+    // Draw the line
+    switch (nodeData[destination]?.lineType) {
+      case LineType.DashedLine:
+        _drawDashedLine(canvas, sourceCenter, destCenter, currentPaint, 0.6);
+        break;
+      case LineType.DottedLine:
+        // dotted line uses the same method as dashed line, but with a lineLength of 0.0
+        _drawDashedLine(canvas, sourceCenter, destCenter, currentPaint, 0.0);
+        break;
+      case LineType.SineLine:
+        _drawSineLine(canvas, sourceCenter, destCenter, currentPaint);
+        break;
+      default:
+        canvas.drawLine(sourceCenter, destCenter, currentPaint);
+        break;
+    }
   }
 
   void _drawSharpBendPointsEdge(List<Offset> bendPoints) {
@@ -172,9 +161,7 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer {
       final arcEndPointRadians = atan2(nextNode.dy - afterNextNode.dy, nextNode.dx - afterNextNode.dx);
       final arcEndPoint = nextNode - Offset.fromDirection(arcEndPointRadians, curveLength);
 
-      if (previousNode != null &&
-          ((currentNode.dx == nextNode.dx && nextNode.dx == afterNextNode.dx) ||
-              (currentNode.dy == nextNode.dy && nextNode.dy == afterNextNode.dy))) {
+      if (previousNode != null && ((currentNode.dx == nextNode.dx && nextNode.dx == afterNextNode.dx) || (currentNode.dy == nextNode.dy && nextNode.dy == afterNextNode.dy))) {
         path.lineTo(nextNode.dx, nextNode.dy);
       } else {
         path.lineTo(arcStartPoint.dx, arcStartPoint.dy);
@@ -183,46 +170,43 @@ class SugiyamaEdgeRenderer extends ArrowEdgeRenderer {
     }
   }
 
+  void _drawDashedLine(Canvas canvas, Offset source, Offset destination,
+      Paint paint, double lineLength) {
+    var dx = destination.dx - source.dx;
+    var dy = destination.dy - source.dy;
 
-void _drawDashedLine(Canvas canvas, Offset source, Offset destination, Paint paint, double lineLength) {
-  // Calculate the distance between the source and destination points
-  var dx = destination.dx - source.dx;
-  var dy = destination.dy - source.dy;
-
-  // Calculate the Euclidean distance
-  var distance = sqrt(dx * dx + dy * dy);
-
-  var numLines = lineLength == 0.0 ? (distance / 5).ceil() : 14;
+    // Calculate the Euclidean distance
+    var distance = sqrt(dx * dx + dy * dy);
 
-  // Calculate the step size for each line
-  var stepX = dx / numLines;
-  var stepY = dy / numLines;
+    var numLines = lineLength == 0.0 ? (distance / 5).ceil() : 14;
 
-  // Set a fixed radius for the circles
-  var circleRadius = 1.0;
+    // Calculate the step size for each line
+    var stepX = dx / numLines;
+    var stepY = dy / numLines;
 
-  // Set a fixed stroke width for the circles
-  var circleStrokeWidth = 1.0;
-  var circlePaint = Paint()
-    ..color = paint.color
-    ..strokeWidth = circleStrokeWidth
-    ..style = PaintingStyle.fill; // Change to fill style
+    var circleRadius = 1.0;
 
-  // Draw the lines or dots between the two points
-  Iterable<int>.generate(numLines).forEach((i) {
-    var startX = source.dx + (i * stepX);
-    var startY = source.dy + (i * stepY);
-    if (lineLength == 0.0) {
-      // Draw a dot with a fixed radius and stroke width
-      canvas.drawCircle(Offset(startX, startY), circleRadius, circlePaint);
-    } else {
-      // Draw a dash
-      var endX = startX + (stepX * lineLength);
-      var endY = startY + (stepY * lineLength);
-      canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint);
+    var circleStrokeWidth = 1.0;
+    var circlePaint = Paint()
+      ..color = paint.color
+      ..strokeWidth = circleStrokeWidth
+      ..style = PaintingStyle.fill; // Change to fill style
+
+    // Draw the lines or dots between the two points
+    for (var i = 0; i < numLines; i++) {
+      var startX = source.dx + (i * stepX);
+      var startY = source.dy + (i * stepY);
+      if (lineLength == 0.0) {
+        // Draw a dot with a fixed radius and stroke width
+        canvas.drawCircle(Offset(startX, startY), circleRadius, circlePaint);
+      } else {
+        // Draw a dash
+        var endX = startX + (stepX * lineLength);
+        var endY = startY + (stepY * lineLength);
+        canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint);
+      }
     }
-  });
-}
+  }
 
   void _drawSineLine(Canvas canvas, Offset source, Offset destination, Paint paint) {
     paint..strokeWidth = 1.5;
@@ -252,7 +236,8 @@ void _drawDashedLine(Canvas canvas, Offset source, Offset destination, Paint pai
         num x;
         if ((dx > 0 && dy < 0) || (dx < 0 && dy > 0)) {
           x = sin(phase + phaseOffset) * segmentLength;
-        } else { // dx < 0 && dy < 0
+        } else {
+          // dx < 0 && dy < 0
           x = -sin(phase + phaseOffset) * segmentLength;
         }
 
diff --git a/lib/tree/BuchheimWalkerAlgorithm.dart b/lib/tree/BuchheimWalkerAlgorithm.dart
index 8d0d0ef..9d9289f 100644
--- a/lib/tree/BuchheimWalkerAlgorithm.dart
+++ b/lib/tree/BuchheimWalkerAlgorithm.dart
@@ -20,10 +20,27 @@ class BuchheimWalkerAlgorithm extends Algorithm {
         orientation == BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT;
   }
 
+  void _detectCycles(Graph graph) {
+    Set<Node> visiting = {};
+
+    bool hasCycle(Node node) {
+      if (visiting.contains(node)) return true;
+      visiting.add(node);
+      bool cycleFound = successorsOf(node).any(hasCycle);
+      visiting.remove(node);
+      return cycleFound;
+    }
+
+    if (graph.nodes.any(hasCycle)) {
+      throw Exception('Cyclic dependency detected - tree structure required');
+    }
+  }
+
   @override
   Size run(Graph? graph, double shiftX, double shiftY) {
     nodeData.clear();
     initData(graph);
+    _detectCycles(graph!);
     var firstNode = getFirstNode(graph!);
     firstWalk(graph, firstNode, 0, 0);
     secondWalk(graph, firstNode, 0.0);
diff --git a/lib/tree/TreeEdgeRenderer.dart b/lib/tree/TreeEdgeRenderer.dart
index b511a65..d6ae38d 100644
--- a/lib/tree/TreeEdgeRenderer.dart
+++ b/lib/tree/TreeEdgeRenderer.dart
@@ -9,7 +9,7 @@ class TreeEdgeRenderer extends EdgeRenderer {
 
   @override
   void render(Canvas canvas, Graph graph, Paint paint) {
-    var levelSeparationHalf = configuration.levelSeparation / 2;
+    var levelSeparationHalf = configuration.levelSeparation * 0.5;
 
     graph.nodes.forEach((node) {
       var children = graph.successorsOf(node);
@@ -17,51 +17,76 @@ class TreeEdgeRenderer extends EdgeRenderer {
       children.forEach((child) {
         var edge = graph.getEdgeBetween(node, child);
         var edgePaint = (edge?.paint ?? paint)..style = PaintingStyle.stroke;
+        final parentOffset = getNodePosition(node);
+        final childOffset = getNodePosition(child);
+
+        final parentCenterX = parentOffset.dx + node.width * 0.5;
+        final parentCenterY = parentOffset.dy + node.height * 0.5;
+        final childCenterX = childOffset.dx + child.width * 0.5;
+        final childCenterY = childOffset.dy + child.height * 0.5;
+
         linePath.reset();
+
         switch (configuration.orientation) {
           case BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM:
-            // position at the middle-top of the child
-            linePath.moveTo((child.x + child.width / 2), child.y);
-            // draws a line from the child's middle-top halfway up to its parent
-            linePath.lineTo(child.x + child.width / 2, child.y - levelSeparationHalf);
-            // draws a line from the previous point to the middle of the parents width
-            linePath.lineTo(node.x + node.width / 2, child.y - levelSeparationHalf);
-
-            // position at the middle of the level separation under the parent
-            linePath.moveTo(node.x + node.width / 2, child.y - levelSeparationHalf);
-            // draws a line up to the parents middle-bottom
-            linePath.lineTo(node.x + node.width / 2, node.y + node.height);
-
+            _drawLShapedPath(
+                childCenterX,
+                childOffset.dy,
+                childCenterX,
+                childOffset.dy - levelSeparationHalf,
+                parentCenterX,
+                childOffset.dy - levelSeparationHalf,
+                parentCenterX,
+                parentOffset.dy + node.height);
             break;
           case BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP:
-            linePath.moveTo(child.x + child.width / 2, child.y + child.height);
-            linePath.lineTo(child.x + child.width / 2, child.y + child.height + levelSeparationHalf);
-            linePath.lineTo(node.x + node.width / 2, child.y + child.height + levelSeparationHalf);
-
-            linePath.moveTo(node.x + node.width / 2, child.y + child.height + levelSeparationHalf);
-            linePath.lineTo(node.x + node.width / 2, node.y + node.height);
-
+            _drawLShapedPath(
+                childCenterX,
+                childOffset.dy + child.height,
+                childCenterX,
+                childOffset.dy + child.height + levelSeparationHalf,
+                parentCenterX,
+                childOffset.dy + child.height + levelSeparationHalf,
+                parentCenterX,
+                parentOffset.dy + node.height);
             break;
-          case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT:
-            linePath.moveTo(child.x, child.y + child.height / 2);
-            linePath.lineTo(child.x - levelSeparationHalf, child.y + child.height / 2);
-            linePath.lineTo(child.x - levelSeparationHalf, node.y + node.height / 2);
-
-            linePath.moveTo(child.x - levelSeparationHalf, node.y + node.height / 2);
-            linePath.lineTo(node.x + node.width, node.y + node.height / 2);
 
+          case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT:
+            _drawLShapedPath(
+                childOffset.dx,
+                childCenterY,
+                childOffset.dx - levelSeparationHalf,
+                childCenterY,
+                childOffset.dx - levelSeparationHalf,
+                parentCenterY,
+                parentOffset.dx + node.width,
+                parentCenterY);
             break;
-          case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT:
-            linePath.moveTo(child.x + child.width, child.y + child.height / 2);
-            linePath.lineTo(child.x + child.width + levelSeparationHalf, child.y + child.height / 2);
-            linePath.lineTo(child.x + child.width + levelSeparationHalf, node.y + node.height / 2);
 
-            linePath.moveTo(child.x + child.width + levelSeparationHalf, node.y + node.height / 2);
-            linePath.lineTo(node.x + node.width, node.y + node.height / 2);
+          case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT:
+            _drawLShapedPath(
+                childOffset.dx + child.width,
+                childCenterY,
+                childOffset.dx + child.width + levelSeparationHalf,
+                childCenterY,
+                childOffset.dx + child.width + levelSeparationHalf,
+                parentCenterY,
+                parentOffset.dx + node.width,
+                parentCenterY);
+            break;
         }
-
         canvas.drawPath(linePath, edgePaint);
       });
     });
   }
+
+  void _drawLShapedPath(double x1, double y1, double x2, double y2, double x3,
+      double y3, double x4, double y4) {
+    linePath
+      ..moveTo(x1, y1)
+      ..lineTo(x2, y2)
+      ..lineTo(x3, y3)
+      ..moveTo(x3, y3)
+      ..lineTo(x4, y4);
+  }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index b1ff91e..22584a8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -4,7 +4,7 @@ version: 1.2.0
 homepage: https://github.com/nabil6391/graphview
 
 environment:
-  sdk: '>=2.14.0 <3.0.0'
+  sdk: '>=2.17.0 <4.0.0'
   flutter: ">=1.17.0"
 
 dependencies:


Test patch:
diff --git a/test/buchheim_walker_algorithm_test.dart b/test/buchheim_walker_algorithm_test.dart
index bbf9835..2970fd4 100644
--- a/test/buchheim_walker_algorithm_test.dart
+++ b/test/buchheim_walker_algorithm_test.dart
@@ -64,6 +64,42 @@ void main() {
       expect(size, Size(950.0, 850.0));
     });
 
+    test('Buchheim detects cyclic dependencies', () {
+      // Create a graph with a cycle
+      final cyclicGraph = Graph();
+      final nodeA = Node.Id('A');
+      final nodeB = Node.Id('B');
+      final nodeC = Node.Id('C');
+
+      // Create cycle: A -> B -> C -> A
+      cyclicGraph.addEdge(nodeA, nodeB);
+      cyclicGraph.addEdge(nodeB, nodeC);
+      cyclicGraph.addEdge(nodeC, nodeA); // This creates the cycle
+
+      for (var i = 0; i < cyclicGraph.nodeCount(); i++) {
+        cyclicGraph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);
+      }
+
+      final configuration = BuchheimWalkerConfiguration()
+        ..siblingSeparation = (100)
+        ..levelSeparation = (150)
+        ..subtreeSeparation = (150)
+        ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);
+
+      var algorithm = BuchheimWalkerAlgorithm(
+          configuration, TreeEdgeRenderer(configuration));
+
+      // Should throw exception when cycle is detected
+      expect(
+            () => algorithm.run(cyclicGraph, 0, 0),
+        throwsA(isA<Exception>().having(
+              (e) => e.toString(),
+          'message',
+          contains('Cyclic dependency detected'),
+        )),
+      );
+    });
+
     test('Buchheim Performance for 100 nodes to be less than 2.5s', () {
 
 
diff --git a/test/controller_tests.dart b/test/controller_tests.dart
new file mode 100644
index 0000000..9fb3f45
--- /dev/null
+++ b/test/controller_tests.dart
@@ -0,0 +1,113 @@
+import 'dart:ui';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:graphview/GraphView.dart';
+
+void main() {
+  group('GraphView Controller Tests', () {
+    testWidgets('animateToNode centers the target node', (WidgetTester tester) async {
+      // Setup graph
+      final graph = Graph();
+      final targetNode = Node.Id('target');
+      targetNode.key = const ValueKey('target');
+      final otherNode = Node.Id('other');
+
+      graph.addEdge(targetNode, otherNode);
+
+      final transformationController = TransformationController();
+      final controller = GraphViewController(transformationController: transformationController);
+      final configuration = BuchheimWalkerConfiguration();
+      final algorithm = BuchheimWalkerAlgorithm(configuration, TreeEdgeRenderer(configuration));
+
+      // Build widget
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: SizedBox(
+              width: 400,
+              height: 600,
+              child: GraphView.builder(
+                graph: graph,
+                algorithm: algorithm,
+                controller: controller,
+                builder: (node) => Container(
+                  width: 100,
+                  height: 50,
+                  color: Colors.blue,
+                  child: Text(node.key?.value ?? ''),
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      await tester.pumpAndSettle();
+
+      // Get the actual position of target node after algorithm runs
+      final actualNodePosition = targetNode.position;
+      final nodeCenter = Offset(
+        actualNodePosition.dx + targetNode.width / 2,
+        actualNodePosition.dy + targetNode.height / 2,
+      );
+
+      // Get initial transformation
+      final initialMatrix = transformationController.value;
+
+      // Animate to target node
+      controller.animateToNode(const ValueKey('target'));
+
+      // Let animation complete
+      await tester.pump(const Duration(milliseconds: 100));
+      await tester.pumpAndSettle();
+
+      // Verify transformation changed
+      final finalMatrix = transformationController.value;
+      expect(finalMatrix, isNot(equals(initialMatrix)));
+
+      // With viewport size 400x600, center should be at (200, 300)
+      // Expected translation should center the node at viewport center
+      final expectedTranslationX = 200 - nodeCenter.dx; // viewport_center_x - node_center_x
+      final expectedTranslationY = 300 - nodeCenter.dy; // viewport_center_y - node_center_y
+
+      expect(finalMatrix.getTranslation().x, closeTo(expectedTranslationX, 5));
+      expect(finalMatrix.getTranslation().y, closeTo(expectedTranslationY, 5));
+    });
+
+    testWidgets('animateToNode handles non-existent node gracefully', (WidgetTester tester) async {
+      final graph = Graph();
+      final node = Node.Id('exists');
+      graph.nodes.add(node);
+
+      final transformationController = TransformationController();
+      final controller = GraphViewController(transformationController: transformationController);
+      final algorithm = BuchheimWalkerAlgorithm(
+          BuchheimWalkerConfiguration(),
+          TreeEdgeRenderer(BuchheimWalkerConfiguration())
+      );
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: GraphView.builder(
+            graph: graph,
+            algorithm: algorithm,
+            controller: controller,
+            builder: (node) => Container(),
+          ),
+        ),
+      );
+
+      await tester.pumpAndSettle();
+
+      final initialMatrix = transformationController.value;
+
+      // Try to animate to non-existent node
+      controller.animateToNode(const ValueKey('nonexistent'));
+      await tester.pumpAndSettle();
+
+      // Matrix should remain unchanged
+      final finalMatrix = transformationController.value;
+      expect(finalMatrix, equals(initialMatrix));
+    });
+  });
+}
\ No newline at end of file