[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: ['https://www.paypal.me/nabil6391']\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n      - name: Install and set Flutter version\n        uses: subosito/flutter-action@v1.5.3\n        with:\n          channel: 'beta'\n      - name: Get packages\n        run: flutter pub get\n#      - name: Analyze\n#        run: flutter analyze\n      - name: Run tests\n        run: flutter test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nbuild/\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\nAGENTS.md\n"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: 7c6f9dd2396dfe7deb6fd11edc12c10786490083\n  channel: master\n\nproject_type: package\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 1.5.1\n- Fix Zoom To fit for hidden nodes\n- Add Fade in Support for Edges\n- Add Loopback support \n\n## 1.5.0\n\n- **MAJOR UPDATE**: Added 5 new layout algorithms\n    - BalloonLayoutAlgorithm: Radial tree layout with circular child arrangements around parents\n    - CircleLayoutAlgorithm: Arranges nodes in circular formations with edge crossing reduction\n    - RadialTreeLayoutAlgorithm: Converts tree structures to polar coordinate system\n    - TidierTreeLayoutAlgorithm: Improved tree layout with better spacing and positioning\n    - MindmapAlgorithm: Specialized layout for mindmap-style distributions\n- **NEW**: Node expand/collapse functionality with GraphViewController\n    - `collapseNode()`, `expandNode()`, `toggleNodeExpanded()` methods\n    - Hierarchical visibility control with animated transitions\n    - Initial collapsed state support via `setInitiallyCollapsedNodes()`\n- **NEW**: Advanced animation system\n    - Smooth expand/collapse animations with customizable duration\n    - Node scaling and opacity transitions during state changes\n    - `toggleAnimationDuration` parameter for fine-tuning animations\n- **NEW**: Enhanced GraphView.builder constructor\n    - `animated`: Enable/disable smooth animations (default: true)\n    - `autoZoomToFit`: Automatically zoom to fit all nodes on initialization\n    - `initialNode`: Jump to specific node on startup\n    - `panAnimationDuration`: Customizable navigation movement timing\n    - `centerGraph`: Center the graph within viewport having a fixed large size of 2000000\n    - `controller`: GraphViewController for programmatic control\n- **NEW**: Navigation and pan control features\n    - `jumpToNode()` and `animateToNode()` for programmatic navigation\n    - `zoomToFit()` for automatic viewport adjustment\n    - `resetView()` for returning to origin\n    - `forceRecalculation()` for layout updates\n- **IMPROVED** TreeEdgeRenderer with curved/straight connection options\n- **IMPROVED**: Better performance with caching for graphs\n- **IMPROVED**: Sugiyama Algorithm with postStraighten and additional strategies\n\n## 1.2.0\n\n- Resolved Overlaping for Sugiyama Algorithm (#56, #93, #87)\n- Added Enum for Coordinate Assignment in Sugiyama : DownRight, DownLeft, UpRight, UpLeft, Average(Default)\n\n## 1.1.1\n\n- Fixed bug for SugiyamaAlgorithm where horizontal placement was overlapping\n- Buchheim Algorithm Performance Improvements\n\n## 1.1.0\n\n- Massive Sugiyama Algorithm Performance Improvements! (5x times faster)\n- Encourage usage of Node.id(int) for better performance\n- Added tests to better check regressions\n\n## 1.0.0\n\n- Full Null Safety Support\n- Sugiyama Algorithm Performance Improvements\n- Sugiyama Algorithm TOP_BOTTOM Height Issue Solved (#48)\n\n## 1.0.0-nullsafety.0\n\n- Null Safety Support\n\n## 0.7.0\n\n- Added methods for builder pattern and deprecated directly setting Widget Data in nodes.\n\n## 0.6.7\n\n- Fix rect value not being set in FruchtermanReingoldAlgorithm (#27)\n\n## 0.6.6\n\n- Fix Index out of range for Sugiyama Algorithm (#20)\n\n## 0.6.5\n\n- Fix edge coloring not picked up by TreeEdgeRenderer (#15)\n- Added Orientation Support in Sugiyama Configuration (#6)\n\n## 0.6.1\n\n- Fix coloring not happening for the whole graphview\n- Fix coloring for sugiyama and tree edge render\n- Use interactive viewer correctly to make the view constrained\n\n## 0.6.0\n\n- Add coloring to individual edges. Applicable for ArrowEdgeRenderer\n- Add example for focused node for Force Directed Graph. It also showcases dynamic update\n\n## 0.5.1\n\n- Fix a bug where the paint was not applied after setstate.\n- Proper Key validation to match Nodes and Edges\n\n## 0.5.0\n\n- Minor Breaking change. We now pass edge renderers as part of Layout\n- Added Layered Graph (SugiyamaAlgorithm)\n- Added Paint Object to change color and stroke parameters of the edges easily\n- Fixed a bug where by onTap in GestureDetector and Inkwell was not working\n\n## 0.1.2\n\n- Used part of library properly. Now we can only implement single graphview\n\n## 0.1.0\n\n- Initial release."
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Nabil Mosharraf\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "GraphView\n===========\nGet it from\n[![pub package](https://img.shields.io/pub/v/graphview.svg)](https://pub.dev/packages/graphview)\n[![pub points](https://img.shields.io/pub/points/graphview/?color=2E8B57&label=pub%20points)](https://pub.dev/packages/graphview/score)\n\nFlutter GraphView is used to display data in graph structures. It can display Tree layout, Directed and Layered graph. Useful for Family Tree, Hierarchy View.\n\n![alt Example](https://media.giphy.com/media/Wsd5Uwm72UBZKXb77s/giphy.gif \"Force Directed Graph\")\n![alt Example](https://media.giphy.com/media/jQ7fdMc5HmyQRoikaK/giphy.gif \"Tree\")\n![alt Example](image/LayeredGraph.png \"Layered Graph Example\")\n\nOverview\n========\nThe library is designed to support different graph layouts and currently works excellent with small graphs. It now includes advanced features like node animations, expand/collapse functionality, and automatic camera positioning.\n\nYou can have a look at the flutter web implementation here:\nhttp://graphview.surge.sh/\n\nFeatures\n========\n- **Multiple Layout Algorithms**: Tree, Directed Graph, Layered Graph, Balloon, Circular, Radial, Tidier Tree, and Mindmap layouts\n- **Node Animations**: Smooth expand/collapse animations with customizable duration\n- **Interactive Navigation**: Jump to nodes, zoom to fit, auto-centering capabilities\n- **Node Expand/Collapse**: Hierarchical node visibility control with animated transitions\n- **Customizable Rendering**: Custom edge renderers, paint styling, and node builders\n- **Touch Interactions**: Pan, zoom, and tap handling with InteractiveViewer integration\n\nLayouts\n======\n### Tree\nUses Walker's algorithm with Buchheim's runtime improvements (`BuchheimWalkerAlgorithm` class). Supports different orientations. All you have to do is using the `BuchheimWalkerConfiguration.orientation` with either `ORIENTATION_LEFT_RIGHT`, `ORIENTATION_RIGHT_LEFT`, `ORIENTATION_TOP_BOTTOM` and\n`ORIENTATION_BOTTOM_TOP` (default). Furthermore parameters like sibling-, level-, subtree separation can be set.\n\nUseful for: Family Tree, Hierarchy View, Flutter Widget Tree\n\n### Tidier Tree\nAn improved tree layout algorithm (`TidierTreeLayoutAlgorithm` class) that provides better spacing and positioning for complex hierarchical structures. Supports all orientations and provides cleaner node arrangements.\n\n![alt Example](image/TidierTree.gif \"Tidier Tree Animation\")\n\nUseful for: Complex hierarchies, Organizational charts, Decision trees\n\n### Directed graph\nDirected graph drawing by simulating attraction/repulsion forces. For this the algorithm by Fruchterman and Reingold (`FruchtermanReingoldAlgorithm` class) was implemented.\n\nUseful for: Social network, Mind Map, Cluster, Graphs, Intercity Road Network\n\n### Layered graph\nAlgorithm from Sugiyama et al. for drawing multilayer graphs, taking advantage of the hierarchical structure of the graph (SugiyamaAlgorithm class). You can also set the parameters for node and level separation using the SugiyamaConfiguration. Supports different orientations. All you have to do is using the `SugiyamaConfiguration.orientation` with either `ORIENTATION_LEFT_RIGHT`, `ORIENTATION_RIGHT_LEFT`, `ORIENTATION_TOP_BOTTOM` and `ORIENTATION_BOTTOM_TOP` (default).\n\nUseful for: Hierarchical Graph which it can have weird edges/multiple paths\n\n### Balloon Layout\nA radial tree layout (`BalloonLayoutAlgorithm` class) that arranges child nodes in circular patterns around their parents. Creates balloon-like structures that are visually appealing for hierarchical data.\n\n![alt Example](image/BalloonLayout.gif \"Balloon Layout Animation\")\n\nUseful for: Mind maps, Radial trees, Circular hierarchies\n\n### Circular Layout\nArranges all nodes in a circle (`CircleLayoutAlgorithm` class). Includes edge crossing reduction algorithms for better readability. Supports automatic radius calculation and custom positioning.\n\n![alt Example](image/CircularLayout.gif \"Circular Layout Animation\")\n\nUseful for: Network visualization, Relationship diagrams, Cyclic structures\n\n### Radial Tree Layout\nA tree layout that converts traditional tree structures into radial/polar coordinates (`RadialTreeLayoutAlgorithm` class). Nodes are positioned based on their distance from the root and angular position.\n\n![alt Example](image/RadialTree.gif \"Radial Tree Animation\")\n\nUseful for: Radial dendrograms, Phylogenetic trees, Sunburst-style hierarchies\n\n### Mindmap Layout\nSpecialized layout for mindmap-style visualizations (`MindmapAlgorithm` class) where child nodes are distributed on left and right sides of the root node.\n\n![alt Example](image/MindmapLayout.gif \"Mindmap Layout Animation\")\n\nUseful for: Mind maps, Concept maps, Brainstorming diagrams\n\nUsage\n======\n\n### Basic Setup\nCurrently GraphView must be used together with a Zoom Engine like [InteractiveViewer](https://api.flutter.dev/flutter/widgets/InteractiveViewer-class.html). To change the zoom values just use the different attributes described in the InteractiveViewer class.\n\nTo create a graph, we need to instantiate the `Graph` class. Then we need to pass the layout and also optional the edge renderer.\n\n```dart\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) => MaterialApp(home: TreeViewPage());\n}\n\nclass TreeViewPage extends StatefulWidget {\n  const TreeViewPage({super.key});\n\n  @override\n  State<TreeViewPage> createState() => _TreeViewPageState();\n}\n\nclass _TreeViewPageState extends State<TreeViewPage> {\n  final GraphViewController controller = GraphViewController();\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Column(\n        mainAxisSize: MainAxisSize.max,\n        children: [\n          Wrap(\n            children: [\n              SizedBox(\n                width: 100,\n                child: TextFormField(\n                  initialValue: builder.siblingSeparation.toString(),\n                  decoration: InputDecoration(labelText: \"Sibling Separation\"),\n                  onChanged: (text) {\n                    builder.siblingSeparation = int.tryParse(text) ?? 100;\n                    setState(() {});\n                  },\n                ),\n              ),\n              SizedBox(\n                width: 100,\n                child: TextFormField(\n                  initialValue: builder.levelSeparation.toString(),\n                  decoration: InputDecoration(labelText: \"Level Separation\"),\n                  onChanged: (text) {\n                    builder.levelSeparation = int.tryParse(text) ?? 100;\n                    setState(() {});\n                  },\n                ),\n              ),\n              SizedBox(\n                width: 100,\n                child: TextFormField(\n                  initialValue: builder.subtreeSeparation.toString(),\n                  decoration: InputDecoration(labelText: \"Subtree separation\"),\n                  onChanged: (text) {\n                    builder.subtreeSeparation = int.tryParse(text) ?? 100;\n                    setState(() {});\n                  },\n                ),\n              ),\n              SizedBox(\n                width: 100,\n                child: TextFormField(\n                  initialValue: builder.orientation.toString(),\n                  decoration: InputDecoration(labelText: \"Orientation\"),\n                  onChanged: (text) {\n                    builder.orientation = int.tryParse(text) ?? 100;\n                    setState(() {});\n                  },\n                ),\n              ),\n              ElevatedButton(\n                onPressed: () {\n                  final node12 = Node.Id(r.nextInt(100));\n                  var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount()));\n                  debugPrint(edge.toString());\n                  graph.addEdge(edge, node12);\n                  setState(() {});\n                },\n                child: Text(\"Add\"),\n              ),\n            ],\n          ),\n          Expanded(\n            child: GraphView.builder(\n              graph: graph,\n              algorithm: BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)),\n              controller: controller,\n              animated: true,\n              autoZoomToFit: true,\n              builder: (Node node) {\n                // I can decide what widget should be shown here based on the id\n                var a = node.key?.value as int;\n                return rectangleWidget(a);\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Random r = Random();\n\n  Widget rectangleWidget(int a) {\n    return InkWell(\n      onTap: () {\n        debugPrint('clicked');\n      },\n      child: Container(\n        padding: EdgeInsets.all(16),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(4),\n          boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)],\n        ),\n        child: Text('Node $a'),\n      ),\n    );\n  }\n\n  final Graph graph = Graph()..isTree = true;\n  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();\n\n  @override\n  void initState() {\n    super.initState();\n    final node1 = Node.Id(1);\n    final node2 = Node.Id(2);\n    final node3 = Node.Id(3);\n    final node4 = Node.Id(4);\n    final node5 = Node.Id(5);\n    final node6 = Node.Id(6);\n    final node8 = Node.Id(7);\n    final node7 = Node.Id(8);\n    final node9 = Node.Id(9);\n    final node10 = Node.Id(10);\n    final node11 = Node.Id(11);\n    final node12 = Node.Id(12);\n\n    graph.addEdge(node1, node2);\n    graph.addEdge(node1, node3, paint: Paint()..color = Colors.red);\n    graph.addEdge(node1, node4, paint: Paint()..color = Colors.blue);\n    graph.addEdge(node2, node5);\n    graph.addEdge(node2, node6);\n    graph.addEdge(node6, node7, paint: Paint()..color = Colors.red);\n    graph.addEdge(node6, node8, paint: Paint()..color = Colors.red);\n    graph.addEdge(node4, node9);\n    graph.addEdge(node4, node10, paint: Paint()..color = Colors.black);\n    graph.addEdge(node4, node11, paint: Paint()..color = Colors.red);\n    graph.addEdge(node11, node12);\n\n    builder\n      ..siblingSeparation = (100)\n      ..levelSeparation = (150)\n      ..subtreeSeparation = (150)\n      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n  }\n}\n```\n\n### Advanced Features\n\n#### GraphView.builder\nThe enhanced `GraphView.builder` constructor provides additional capabilities:\n\n```dart\nGraphView.builder(\n  graph: graph,\n  algorithm: BuchheimWalkerAlgorithm(config, TreeEdgeRenderer(config)),\n  controller: controller,\n  animated: true,                    // Enable smooth animations\n  autoZoomToFit: true,              // Automatically zoom to fit all nodes\n  initialNode: ValueKey('startNode'), // Jump to specific node on init\n  panAnimationDuration: Duration(milliseconds: 600),\n  toggleAnimationDuration: Duration(milliseconds: 400),\n  centerGraph: true,                // Center the graph in viewport\n  builder: (Node node) {\n    return YourCustomWidget(node);\n  },\n)\n```\n\n#### Node Expand/Collapse\nUse the `GraphViewController` to manage node visibility:\n\n```dart\nfinal controller = GraphViewController();\n\n// Collapse a node (hide its children)\ncontroller.collapseNode(graph, node, animate: true);\n\n// Expand a collapsed node\ncontroller.expandNode(graph, node, animate: true);\n\n// Toggle collapse/expand state\ncontroller.toggleNodeExpanded(graph, node, animate: true);\n\n// Check if node is collapsed\nbool isCollapsed = controller.isNodeCollapsed(node);\n\n// Set initially collapsed nodes\ncontroller.setInitiallyCollapsedNodes([node1, node2]);\n```\n\n#### Navigation and Camera Control\nNavigate programmatically through the graph:\n\n```dart\n// Jump to a specific node\ncontroller.jumpToNode(ValueKey('nodeId'));\n\n// Animate to a node\ncontroller.animateToNode(ValueKey('nodeId'));\n\n// Zoom to fit all visible nodes\ncontroller.zoomToFit();\n\n// Reset view to origin\ncontroller.resetView();\n\n// Force recalculation of layout\ncontroller.forceRecalculation();\n```\n\n### Algorithm Examples\n\n#### Balloon Layout\n```dart\nGraphView.builder(\n  graph: graph,\n  algorithm: BalloonLayoutAlgorithm(\n    BuchheimWalkerConfiguration(), \n    null\n  ),\n  builder: (node) => nodeWidget(node),\n)\n```\n\n#### Circular Layout\n```dart\nGraphView.builder(\n  graph: graph,\n  algorithm: CircleLayoutAlgorithm(\n    CircleLayoutConfiguration(\n      radius: 200.0,\n      reduceEdgeCrossing: true,\n    ), \n    null\n  ),\n  builder: (node) => nodeWidget(node),\n)\n```\n\n#### Radial Tree Layout\n```dart\nGraphView.builder(\n  graph: graph,\n  algorithm: RadialTreeLayoutAlgorithm(\n    BuchheimWalkerConfiguration(), \n    null\n  ),\n  builder: (node) => nodeWidget(node),\n)\n```\n\n#### Tidier Tree Layout\n```dart\nGraphView.builder(\n  graph: graph,\n  algorithm: TidierTreeLayoutAlgorithm(\n    BuchheimWalkerConfiguration(), \n    TreeEdgeRenderer(config)\n  ),\n  builder: (node) => nodeWidget(node),\n)\n```\n\n#### Mindmap Layout\n```dart\nGraphView.builder(\n  graph: graph,\n  algorithm: MindmapAlgorithm(\n    BuchheimWalkerConfiguration(), \n    MindmapEdgeRenderer(config)\n  ),\n  builder: (node) => nodeWidget(node),\n)\n```\n\n### Using builder mechanism to build Nodes\nYou can use any widget inside the node:\n\n```dart\nNode node = Node.Id(fromNodeId) ;\n\nbuilder: (Node node) {\n                  // I can decide what widget should be shown here based on the id\n                  var a = node.key.value as int;\n                  if(a ==2)\n                    return rectangleWidget(a);\n                  else \n                    return circleWidget(a);\n                },\n```\n\n### Using Paint to color and line thickness\nYou can specify the edge color and thickness by using a custom paint\n\n```dart\n\ngetGraphView() {\n        return GraphView(\n                graph: graph,\n                algorithm: SugiyamaAlgorithm(builder),\n                paint: Paint()..color = Colors.green..strokeWidth = 1..style = PaintingStyle.stroke,\n              );\n}\n```\n\n### Color Edges individually\nAdd an additional parameter paint. Applicable for ArrowEdgeRenderer for now.\n\n```dart\nvar a = Node();\nvar b = Node();\n graph.addEdge(a, b, paint: Paint()..color = Colors.red);\n```\n\n### Add focused Node\nYou can focus on a specific node. This will allow scrolling to that node in the future, but for now , using it we can drag a node with realtime updates in force directed graph\n\n```dart\n onPanUpdate: (details) {\n        var x = details.globalPosition.dx;\n        var y = details.globalPosition.dy;\n        setState(() {\n          builder.setFocusedNode(graph.getNodeAtPosition(i));\n          graph.getNodeAtPosition(i).position = Offset(x,y);\n        });\n      },\n```\n\n### Extract info from any json to Graph Object\nNow its a bit easy to use Ids to extract info from any json to Graph Object\n\nFor example, if the json is like this:\n```dart\nvar json = {\n   \"nodes\": [\n     {\"id\": 1, \"label\": 'circle'},\n     {\"id\": 2, \"label\": 'ellipse'},\n     {\"id\": 3, \"label\": 'database'},\n     {\"id\": 4, \"label\": 'box'},\n     {\"id\": 5, \"label\": 'diamond'},\n     {\"id\": 6, \"label\": 'dot'},\n     {\"id\": 7, \"label\": 'square'},\n     {\"id\": 8, \"label\": 'triangle'},\n   ],\n   \"edges\": [\n     {\"from\": 1, \"to\": 2},\n     {\"from\": 2, \"to\": 3},\n     {\"from\": 2, \"to\": 4},\n     {\"from\": 2, \"to\": 5},\n     {\"from\": 5, \"to\": 6},\n     {\"from\": 5, \"to\": 7},\n     {\"from\": 6, \"to\": 8}\n   ]\n };\n```\n\nStep 1, add the edges by using ids\n```dart\n  edges.forEach((element) {\n      var fromNodeId = element['from'];\n      var toNodeId = element['to'];\n      graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId));\n    });\n```\n\nStep 2: Then using builder and find the nodeValues from the json using id and then set the value of that.\n\n```dart\n builder: (Node node) {\n                  // I can decide what widget should be shown here based on the id\n                  var a = node.key.value as int;\n                  var nodes = json['nodes'];\n                  var nodeValue = nodes.firstWhere((element) => element['id'] == a);\n                  return rectangleWidget(nodeValue['label'] as String);\n                },\n```\n\n### Using any widget inside the Node (Deprecated)\nYou can use any widget inside the node:\n\n```dart\nNode node = Node(getNodeText);\n\ngetNodeText() {\n    return Container(\n        padding: EdgeInsets.all(16),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(4),\n          boxShadow: [\n            BoxShadow(color: Colors.blue[100], spreadRadius: 1),\n          ],\n        ),\n        child: Text(\"Node ${n++}\"));\n  }\n```\n\nExamples\n========\n#### Rooted Tree\n![alt Example](image/TopDownTree.png \"Tree Example\")\n\n#### Rooted Tree (Bottom to Top)\n![alt Example](image/BottomTopTree.png \"Tree Example\")\n\n#### Rooted Tree (Left to Right)\n![alt Example](image/LeftRightTree.png \"Tree Example\")\n\n#### Rooted Tree (Right to Left)\n![alt Example](image/RightLeftTree.png \"Tree Example\")\n\n#### Directed Graph\n![alt Example](image/Graph.png \"Directed Graph Example\")\n![alt Example](https://media.giphy.com/media/eNuoOOcbvWlRmJjkDZ/giphy.gif \"Force Directed Graph\")\n\n#### Layered Graph\n![alt Example](image/LayeredGraph.png \"Layered Graph Example\")\n\n#### Balloon Layout\n![alt Example](image/BalloonTreeLayout.gif \"Balloon Layout Example\")\n\n#### Circular Layout\n![alt Example](image/CircleLayout.gif \"Circular Layout Example\")\n\n#### Radial Tree Layout\n![alt Example](image/RadialTreeLayout.gif \"Radial Tree Layout Example\")\n\n#### Tidier Tree Layout\n![alt Example](image/TidierTreeLayout.gif \"Tidier Tree Layout Example\")\n\n#### Mindmap Layout\n![alt Example](image/MindMapLayout.gif \"Mindmap Layout Example\")\n\n#### Node Expand/Collapse Animation\n![alt Example](image/NodeExpandCollapseAnimation.gif \"Node Expand/Collapse Animation\")\n\n#### Auto Navigation\n![alt Example](image/AutoNavigationExample.gif \"Auto Navigation Example\")\n\nInspirations\n========\nThis library is basically a dart representation of the excellent Android Library [GraphView](https://github.com/Team-Blox/GraphView) by Team-Blox\n\nI would like to thank them for open sourcing their code for which reason I was able to port their code to dart and use for flutter.\n\nFuture Works\n========\n\n- [x] Add nodeOnTap\n- [x] Add Layered Graph\n- [x] Animations\n- [x] Dynamic Node Position update for directed graph\n- [x] Node expand/collapse functionality\n- [x] Auto-navigation and camera control\n- [x] Multiple new layout algorithms (Balloon, Circular, Radial, Tidier, Mindmap)\n- [ ] Finish Eiglsperger Algorithm\n- [ ] Custom Edge Label Rendering\n- [ ] Use a builder pattern to draw items on demand.\n\nLicense\n=======\n\nMIT License\n\nCopyright (c) 2020 Nabil Mosharraf\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "linter:\n  rules:\n    - always_declare_return_types\n    - annotate_overrides\n    - avoid_empty_else\n    - avoid_init_to_null\n    - avoid_null_checks_in_equality_operators\n    - avoid_relative_lib_imports\n    - avoid_return_types_on_setters\n    - avoid_shadowing_type_parameters\n    - avoid_types_as_parameter_names\n    - camel_case_extensions\n    - curly_braces_in_flow_control_structures\n    - empty_catches\n    - empty_constructor_bodies\n    - library_names\n    - library_prefixes\n    - no_duplicate_case_values\n    - null_closures\n    - omit_local_variable_types\n    - prefer_adjacent_string_concatenation\n    - prefer_collection_literals\n    - prefer_conditional_assignment\n    - prefer_contains\n    # REMOVED: prefer_equal_for_default_values (removed in Dart 3.0)\n    - prefer_final_fields\n    - prefer_for_elements_to_map_fromIterable\n    - prefer_generic_function_type_aliases\n    - prefer_if_null_operators\n    - prefer_is_empty\n    - prefer_is_not_empty\n    - prefer_iterable_whereType\n    - prefer_single_quotes\n    - prefer_spread_collections\n    - recursive_getters\n    - slash_for_doc_comments\n    - type_init_formals\n    - unawaited_futures\n    - unnecessary_const\n    - unnecessary_new\n    - unnecessary_null_in_if_null_operators\n    - unnecessary_this\n    - unrelated_type_equality_checks\n    - use_function_type_syntax_for_parameters\n    - use_rethrow_when_possible\n    - valid_regexps\n\nanalyzer:\n  strong-mode:\n    implicit-casts: false"
  },
  {
    "path": "example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Web related\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n"
  },
  {
    "path": "example/analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at\n  # https://dart-lang.github.io/linter/lints/index.html.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n   avoid_print: false  # Uncomment to disable the `avoid_print` rule\n   prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "example/lib/algorithm_selector_graphview.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\n// Enum for algorithm types\nenum LayoutAlgorithmType {\n  tidierTree,\n  buchheimWalker,\n  balloon,\n  radialTree,\n  circle,\n}\n\nclass AlgorithmSelectedVIewPage extends StatefulWidget {\n  @override\n  _TreeViewPageState createState() => _TreeViewPageState();\n}\n\nclass _TreeViewPageState extends State<AlgorithmSelectedVIewPage> with TickerProviderStateMixin {\n  GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 1;\n\n  // Algorithm selection\n  LayoutAlgorithmType _selectedAlgorithm = LayoutAlgorithmType.tidierTree;\n  Algorithm? _currentAlgorithm;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n          title: Text('Tree View - Multiple Algorithms'),\n        ),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            // Algorithm selection dropdown\n            Container(\n              padding: EdgeInsets.all(8),\n              child: Row(\n                children: [\n                  Text('Layout Algorithm: '),\n                  SizedBox(width: 8),\n                  Expanded(\n                    child: DropdownButton<LayoutAlgorithmType>(\n                      value: _selectedAlgorithm,\n                      isExpanded: true,\n                      onChanged: (LayoutAlgorithmType? newValue) {\n                        if (newValue != null) {\n                          setState(() {\n                            _selectedAlgorithm = newValue;\n                            _updateAlgorithm();\n                          });\n                        }\n                      },\n                      items: LayoutAlgorithmType.values.map<DropdownMenuItem<LayoutAlgorithmType>>((LayoutAlgorithmType value) {\n                        return DropdownMenuItem<LayoutAlgorithmType>(\n                          value: value,\n                          child: Text(_getAlgorithmDisplayName(value)),\n                        );\n                      }).toList(),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n\n            // Configuration controls\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.siblingSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Sibling Separation'),\n                    onChanged: (text) {\n                      builder.siblingSeparation = int.tryParse(text) ?? 100;\n                      _updateAlgorithm();\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      _updateAlgorithm();\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.subtreeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Subtree separation'),\n                    onChanged: (text) {\n                      builder.subtreeSeparation = int.tryParse(text) ?? 100;\n                      _updateAlgorithm();\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      _updateAlgorithm();\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                ElevatedButton(\n                  onPressed: () {\n                    final node12 = Node.Id(r.nextInt(100));\n                    var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount()));\n                    print(edge);\n                    graph.addEdge(edge, node12);\n                    setState(() {});\n                  },\n                  child: Text('Add'),\n                ),\n                ElevatedButton(\n                  onPressed: _navigateToRandomNode,\n                  child: Text('Go to Node $nextNodeId'),\n                ),\n                SizedBox(width: 8),\n                ElevatedButton(\n                  onPressed: _resetView,\n                  child: Text('Reset View'),\n                ),\n                SizedBox(width: 8,),\n                ElevatedButton(\n                    onPressed: (){\n                      _controller.zoomToFit();\n                    },\n                    child: Text('Zoom to fit')\n                )\n              ],\n            ),\n\n            Expanded(\n                child: GraphView.builder(\n                  controller: _controller,\n                  graph: graph,\n                  algorithm: _currentAlgorithm ?? TidierTreeLayoutAlgorithm(builder, null),\n                  builder: (Node node) => Container(\n                    padding: EdgeInsets.all(8),\n                    decoration: BoxDecoration(\n                      color: Colors.lightBlue[100],\n                      borderRadius: BorderRadius.circular(8),\n                    ),\n                    child: Text(node.key?.value.toString() ?? ''),\n                  ),\n                )\n            ),\n          ],\n        ));\n  }\n\n  String _getAlgorithmDisplayName(LayoutAlgorithmType type) {\n    switch (type) {\n      case LayoutAlgorithmType.tidierTree:\n        return 'Tidier Tree Layout';\n      case LayoutAlgorithmType.buchheimWalker:\n        return 'Buchheim Walker Tree Layout';\n      case LayoutAlgorithmType.balloon:\n        return 'Balloon Layout';\n      case LayoutAlgorithmType.radialTree:\n        return 'Radial Tree Layout';\n      case LayoutAlgorithmType.circle:\n        return 'Circle Layout';\n    }\n  }\n\n  void _updateAlgorithm() {\n    switch (_selectedAlgorithm) {\n      case LayoutAlgorithmType.tidierTree:\n        _currentAlgorithm = TidierTreeLayoutAlgorithm(builder, null);\n        break;\n      case LayoutAlgorithmType.buchheimWalker:\n        _currentAlgorithm = BuchheimWalkerAlgorithm(builder, null);\n        break;\n      case LayoutAlgorithmType.balloon:\n        _currentAlgorithm = BalloonLayoutAlgorithm(builder, null);\n        break;\n      case LayoutAlgorithmType.radialTree:\n        _currentAlgorithm = RadialTreeLayoutAlgorithm(builder, null);\n        break;\n      case LayoutAlgorithmType.circle:\n        final circleConfig = CircleLayoutConfiguration(\n          radius: 200.0,\n          reduceEdgeCrossing: true,\n          reduceEdgeCrossingMaxEdges: 200,\n        );\n        _currentAlgorithm = CircleLayoutAlgorithm(circleConfig, null);\n        break;\n    }\n  }\n\n  Widget rectangleWidget(int? a) {\n    return InkWell(\n      onTap: () {\n        print('clicked node $a');\n      },\n      child: Container(\n          padding: EdgeInsets.all(16),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(4),\n            boxShadow: [\n              BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),\n            ],\n          ),\n          child: Text('Node ${a} ')),\n    );\n  }\n\n  final Graph graph = Graph()..isTree = true;\n  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n\n    final randomNode = graph.nodes.firstWhere(\n          (node) => node.key != null && node.key!.value == nextNodeId,\n      orElse: () => graph.nodes.first,\n    );\n    final nodeId = randomNode.key!;\n    _controller.animateToNode(nodeId);\n\n    setState(() {\n      nextNodeId = r.nextInt(graph.nodes.length) + 1;\n    });\n  }\n\n  void _resetView() {\n    _controller.resetView();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    var json = {\n      'edges': [\n        // A0 -> B0, B1, B2\n        {'from': 1, 'to': 2},   // A0 -> B0\n        {'from': 1, 'to': 3},   // A0 -> B1\n        {'from': 1, 'to': 4},   // A0 -> B2\n\n        // B0 -> C0, C1, C2, C3\n        {'from': 2, 'to': 5},   // B0 -> C0\n        {'from': 2, 'to': 6},   // B0 -> C1\n        {'from': 2, 'to': 7},   // B0 -> C2\n        {'from': 2, 'to': 8},   // B0 -> C3\n\n        // C2 -> H0, H1\n        {'from': 7, 'to': 9},   // C2 -> H0\n        {'from': 7, 'to': 10},  // C2 -> H1\n\n        // H1 -> H2, H3\n        {'from': 10, 'to': 11}, // H1 -> H2\n        {'from': 10, 'to': 12}, // H1 -> H3\n\n        // H3 -> H4, H5\n        {'from': 12, 'to': 13}, // H3 -> H4\n        {'from': 12, 'to': 14}, // H3 -> H5\n\n        // H5 -> H6, H7\n        {'from': 14, 'to': 15}, // H5 -> H6\n        {'from': 14, 'to': 16}, // H5 -> H7\n\n        // B1 -> D0, D1, D2\n        {'from': 3, 'to': 17},  // B1 -> D0\n        {'from': 3, 'to': 18},  // B1 -> D1\n        {'from': 3, 'to': 19},  // B1 -> D2\n\n        // B2 -> E0, E1, E2\n        {'from': 4, 'to': 20},  // B2 -> E0\n        {'from': 4, 'to': 21},  // B2 -> E1\n        {'from': 4, 'to': 22},  // B2 -> E2\n\n        // D0 -> F0, F1, F2\n        {'from': 17, 'to': 23}, // D0 -> F0\n        {'from': 17, 'to': 24}, // D0 -> F1\n        {'from': 17, 'to': 25}, // D0 -> F2\n\n        // D1 -> G0, G1, G2, G3, G4, G5, G6, G7\n        {'from': 18, 'to': 26}, // D1 -> G0\n        {'from': 18, 'to': 27}, // D1 -> G1\n        {'from': 18, 'to': 28}, // D1 -> G2\n        {'from': 18, 'to': 29}, // D1 -> G3\n        {'from': 18, 'to': 30}, // D1 -> G4\n        {'from': 18, 'to': 31}, // D1 -> G5\n        {'from': 18, 'to': 32}, // D1 -> G6\n        {'from': 18, 'to': 33}, // D1 -> G7\n      ]\n    };\n\n    // Usage code (as in your example)\n    var edges = json['edges']!;\n    edges.forEach((element) {\n      var fromNodeId = element['from'];\n      var toNodeId = element['to'];\n      graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId));\n    });\n\n    builder\n      ..siblingSeparation = (100)\n      ..levelSeparation = (150)\n      ..subtreeSeparation = (150)\n      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n\n    // Initialize with default algorithm\n    _updateAlgorithm();\n  }\n}"
  },
  {
    "path": "example/lib/decision_tree_screen.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass DecisionTreeScreen extends StatefulWidget {\n  @override\n  _DecisionTreeScreenState createState() => _DecisionTreeScreenState();\n}\n\nclass _DecisionTreeScreenState extends State<DecisionTreeScreen> {\n  final _graph = Graph()..isTree = true;\n\n  final _configuration = SugiyamaConfiguration()\n    ..orientation = 1\n    ..nodeSeparation = 40\n    ..levelSeparation = 50;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _graph.addEdge(Node.Id(1), Node.Id(2));\n    _graph.addEdge(Node.Id(2), Node.Id(3));\n    _graph.addEdge(Node.Id(2), Node.Id(11));\n    _graph.addEdge(Node.Id(3), Node.Id(4));\n    _graph.addEdge(Node.Id(4), Node.Id(5));\n\n    _graph.addEdge(Node.Id(1), Node.Id(6));\n    _graph.addEdge(Node.Id(6), Node.Id(7));\n    _graph.addEdge(Node.Id(7), Node.Id(3));\n\n    _graph.addEdge(Node.Id(1), Node.Id(10));\n    _graph.addEdge(Node.Id(10), Node.Id(11));\n    _graph.addEdge(Node.Id(11), Node.Id(7));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(),\n      body: InteractiveViewer(\n        minScale: 0.1,\n        constrained: false,\n        boundaryMargin: const EdgeInsets.all(64),\n        child: GraphView(\n          graph: _graph,\n          algorithm: SugiyamaAlgorithm(_configuration),\n          builder: (node) {\n            final id = node.key!.value as int;\n\n            final text = List.generate(id == 1 || id == 4 ? 500 : 10, (index) => 'X').join(' ');\n\n            return Container(\n              width: 180,\n              decoration: BoxDecoration(\n                color: Color((Random().nextDouble() * 0xFFFFFF).toInt()).withValues(alpha: 1.0),\n                border: Border.all(width: 2),\n              ),\n              padding: const EdgeInsets.all(16),\n              child: Text('$id $text'),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/example.dart",
    "content": "import 'package:example/layer_graphview.dart';\nimport 'package:flutter/material.dart';\n\nimport 'force_directed_graphview.dart';\nimport 'tree_graphview.dart';\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Home(),\n    );\n  }\n}\n\nclass Home extends StatelessWidget {\n  const Home({\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return SafeArea(\n      child: Scaffold(\n        body: Center(\n          child: Column(children: [\n            TextButton(\n                onPressed: () => Navigator.push(\n                      context,\n                      MaterialPageRoute(\n                          builder: (context) => Scaffold(\n                                appBar: AppBar(),\n                                body: TreeViewPage(),\n                              )),\n                    ),\n                child: Text(\n                  'Tree View (BuchheimWalker)',\n                  style: TextStyle(color: Theme.of(context).primaryColor),\n                )),\n            TextButton(\n                onPressed: () => Navigator.push(\n                      context,\n                      MaterialPageRoute(\n                          builder: (context) => Scaffold(\n                                appBar: AppBar(),\n                                body: GraphClusterViewPage(),\n                              )),\n                    ),\n                child: Text(\n                  'Graph Cluster View (FruchtermanReingold)',\n                  style: TextStyle(color: Theme.of(context).primaryColor),\n                )),\n            TextButton(\n                onPressed: () => Navigator.push(\n                      context,\n                      MaterialPageRoute(\n                          builder: (context) => Scaffold(\n                                appBar: AppBar(),\n                                body: LayeredGraphViewPage(),\n                              )),\n                    ),\n                child: Text(\n                  'Layered View (Sugiyama)',\n                  style: TextStyle(color: Theme.of(context).primaryColor),\n                )),\n          ]),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "example/lib/force_directed_graphview.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass GraphClusterViewPage extends StatefulWidget {\n  @override\n  _GraphClusterViewPageState createState() => _GraphClusterViewPageState();\n}\n\nclass _GraphClusterViewPageState extends State<GraphClusterViewPage> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(),\n        body: Column(\n          children: [\n            Expanded(\n              child: InteractiveViewer(\n                  constrained: false,\n                  boundaryMargin: EdgeInsets.all(8),\n                  minScale: 0.001,\n                  maxScale: 10000,\n                  child: GraphViewCustomPainter(\n                      graph: graph,\n                      algorithm: algorithm,\n                      paint: Paint()\n                        ..color = Colors.green\n                        ..strokeWidth = 1\n                        ..style = PaintingStyle.fill,\n                      builder: (Node node) {\n                        // I can decide what widget should be shown here based on the id\n                        var a = node.key!.value as int?;\n                        if (a == 2) {\n                          return rectangWidget(a);\n                        }\n                        return rectangWidget(a);\n                      })),\n            ),\n          ],\n        ));\n  }\n\n  int n = 8;\n  Random r = Random();\n\n  Widget rectangWidget(int? i) {\n    return Container(\n        padding: EdgeInsets.all(16),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(4),\n          boxShadow: [\n            BoxShadow(color: Colors.blue, spreadRadius: 1),\n          ],\n        ),\n        child: Text('Node $i'));\n  }\n\n  final Graph graph = Graph();\n  late FruchtermanReingoldAlgorithm algorithm;\n\n  @override\n  void initState() {\n    final a = Node.Id(1);\n    final b = Node.Id(2);\n    final c = Node.Id(3);\n    final d = Node.Id(4);\n    final e = Node.Id(5);\n    final f = Node.Id(6);\n    final g = Node.Id(7);\n    final h = Node.Id(8);\n\n    graph.addEdge(a, b, paint: Paint()..color = Colors.red);\n    graph.addEdge(a, c);\n    graph.addEdge(a, d);\n    graph.addEdge(c, e);\n    graph.addEdge(d, f);\n    graph.addEdge(f, c);\n    graph.addEdge(g, c);\n    graph.addEdge(h, g);\n    var config = FruchtermanReingoldConfiguration()\n      ..iterations = 1000;\n    algorithm = FruchtermanReingoldAlgorithm(config);\n  }\n}\n"
  },
  {
    "path": "example/lib/graph_cluster_animated.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass GraphScreen extends StatefulWidget {\n  Graph graph;\n  FruchtermanReingoldAlgorithm algorithm;\n  final Paint? paint;\n\n  GraphScreen(this.graph, this.algorithm, this.paint);\n\n  @override\n  _GraphScreenState createState() => _GraphScreenState();\n}\n\nclass _GraphScreenState extends State<GraphScreen> {\n  bool animated = true;\n  Random r = Random();\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text('Graph Screen'),\n        actions: [\n          IconButton(\n            icon: Icon(Icons.add),\n            onPressed: () async {\n              setState(() {\n                final node12 = Node.Id(r.nextInt(100).toString());\n                var edge = widget.graph.getNodeAtPosition(r.nextInt(widget.graph.nodeCount()));\n                print(edge);\n                widget.graph.addEdge(edge, node12);\n                setState(() {});\n              });\n            },\n          ),\n          IconButton(\n            icon: Icon(Icons.animation),\n            onPressed: () async {\n              setState(() {\n                animated = !animated;\n              });\n            },\n          )\n        ],\n      ),\n      body: InteractiveViewer(\n          constrained: false,\n          boundaryMargin: EdgeInsets.all(100),\n          minScale: 0.0001,\n          maxScale: 10.6,\n          child: GraphViewCustomPainter(\n            graph: widget.graph,\n            algorithm: widget.algorithm,\n            builder: (Node node) {\n              // I can decide what widget should be shown here based on the id\n              var a = node.key!.value as String;\n              return rectangWidget(a);\n            },\n          )),\n    );\n  }\n\n  Widget rectangWidget(String? i) {\n    return Container(\n        padding: EdgeInsets.all(16),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(4),\n          boxShadow: [\n            BoxShadow(color: Colors.blue, spreadRadius: 1),\n          ],\n        ),\n        child: Center(child: Text('Node $i')));\n  }\n\n  Future<void> update() async {\n    setState(() {});\n  }\n}\n"
  },
  {
    "path": "example/lib/large_tree_graphview.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LargeTreeViewPage extends StatefulWidget {\n  @override\n  _LargeTreeViewPageState createState() => _LargeTreeViewPageState();\n}\n\nclass _LargeTreeViewPageState extends State<LargeTreeViewPage> with TickerProviderStateMixin {\n\n  GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 1;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n          title: Text('Tree View'),\n        ),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            // Configuration controls\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.siblingSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Sibling Separation'),\n                    onChanged: (text) {\n                      builder.siblingSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.subtreeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Subtree separation'),\n                    onChanged: (text) {\n                      builder.subtreeSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                ElevatedButton(\n                  onPressed: () {\n                    final node12 = Node.Id(r.nextInt(100));\n                    var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount()));\n                    print(edge);\n                    graph.addEdge(edge, node12);\n                    setState(() {});\n                  },\n                  child: Text('Add'),\n                ),\n                ElevatedButton(\n                  onPressed: _navigateToRandomNode,\n                  child: Text('Go to Node $nextNodeId'),\n                ),\n                SizedBox(width: 8),\n                ElevatedButton(\n                  onPressed: _resetView,\n                  child: Text('Reset View'),\n                ),\n                SizedBox(width: 8,),\n                ElevatedButton(onPressed: (){\n                  _controller.zoomToFit();\n                }, child: Text('Zoom to fit'))\n              ],\n            ),\n\n            Expanded(\n              child: GraphView.builder(\n                controller: _controller,\n                graph: graph,\n                algorithm: algorithm,\n                centerGraph: true,\n                initialNode: ValueKey(1),\n                panAnimationDuration: Duration(milliseconds: 750),\n                toggleAnimationDuration: Duration(milliseconds: 750),\n                // edgeBuilder: (Edge edge, EdgeGeometry geometry) {\n                //   return InteractiveEdge(\n                //     edge: edge,\n                //     geometry: geometry,\n                //     onTap: () => print('Edge tapped: ${edge.key}'),\n                //     color: Colors.red,\n                //     strokeWidth: 3.0,\n                //   );\n                // },\n                builder: (Node node) => InkWell(\n                  onTap: () => _toggleCollapse(node),\n                  child: Container(\n                    padding: EdgeInsets.all(16),\n                    decoration: BoxDecoration(\n                      shape: BoxShape.circle,\n                      boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)],\n                    ),\n                    child: Text(\n                      '${node.key?.value}',\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ));\n  }\n\n  final Graph graph = Graph()..isTree = true;\n  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();\n  late final algorithm = BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder));\n\n  void _toggleCollapse(Node node) {\n    _controller.toggleNodeExpanded(graph, node, animate: true);\n  }\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n\n    final randomNode = graph.nodes.firstWhere(\n      (node) => node.key != null && node.key!.value == nextNodeId,\n      orElse: () => graph.nodes.first,\n    );\n    final nodeId = randomNode.key!;\n    _controller.animateToNode(nodeId);\n\n    setState(() {\n      // nextNodeId = r.nextInt(graph.nodes.length) + 1;\n    });\n  }\n\n  void _resetView() {\n    _controller.animateToNode(ValueKey(1));\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    var n = 1000;\n    final nodes = List.generate(n, (i) => Node.Id(i + 1));\n\n// Generate tree edges using a queue-based approach\n    int currentChild = 1; // Start from node 1 (node 0 is root)\n\n    for (var i = 0; i < n && currentChild < n; i++) {\n      final children = (i < n ~/ 3) ? 3 : 2;\n\n      for (var j = 0; j < children && currentChild < n; j++) {\n        graph.addEdge(nodes[i], nodes[currentChild]);\n        currentChild++;\n      }\n    }\n\n    builder\n      ..siblingSeparation = (10)\n      ..levelSeparation = (100)\n      ..subtreeSeparation = (10)\n      ..useCurvedConnections = true\n      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT);\n  }\n\n}"
  },
  {
    "path": "example/lib/layer_eiglesperger_graphview.dart",
    "content": "import 'dart:math';\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LayeredEiglspergerGraphViewPage extends StatefulWidget {\n  @override\n  _LayeredEiglspergerGraphViewPageState createState() => _LayeredEiglspergerGraphViewPageState();\n}\n\nclass _LayeredEiglspergerGraphViewPageState extends State<LayeredEiglspergerGraphViewPage> {\n  GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.nodeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Node Separation'),\n                    onChanged: (text) {\n                      builder.nodeSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 120,\n                  child: Column(\n                    children: [\n                      Text('Alignment'),\n                      DropdownButton<CoordinateAssignment>(\n                        value: builder.coordinateAssignment,\n                        items: CoordinateAssignment.values.map((coordinateAssignment) {\n                          return DropdownMenuItem<CoordinateAssignment>(\n                            value: coordinateAssignment,\n                            child: Text(coordinateAssignment.name),\n                          );\n                        }).toList(),\n                        onChanged: (value) {\n                          setState(() {\n                            builder.coordinateAssignment = value!;\n                          });\n                        },\n                      ),\n                    ],\n                  ),\n                ),\n                ElevatedButton(\n                  onPressed: () {\n                    final node12 = Node.Id(r.nextInt(100));\n                    var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount()));\n                    print(edge);\n                    graph.addEdge(edge, node12);\n                    setState(() {});\n                  },\n                  child: Text('Add'),\n                ),\n                ElevatedButton(\n                  onPressed: () => _navigateToRandomNode(),\n                  child: Text('Go to Node $nextNodeId'),\n                ),\n                ElevatedButton(\n                  onPressed: () => _controller.resetView(),\n                  child: Text('Reset View'),\n                ),\n                ElevatedButton(\n                  onPressed: () => _controller.zoomToFit(),\n                  child: Text('Zoom to fit'),\n                ),\n              ],\n            ),\n            Expanded(\n              child: GraphView.builder(\n                controller: _controller,\n                graph: graph,\n                algorithm: EiglspergerAlgorithm(builder),\n                paint: Paint()\n                  ..color = Colors.green\n                  ..strokeWidth = 1\n                  ..style = PaintingStyle.stroke,\n                builder: (Node node) {\n                  var a = node.key!.value as int?;\n                  return rectangleWidget(a);\n                },\n              ),\n            ),\n          ],\n        ));\n  }\n\n  Widget rectangleWidget(int? a) {\n    return Container(\n        padding: EdgeInsets.all(16),\n        decoration: BoxDecoration(\n          shape: BoxShape.circle,\n          boxShadow: [\n            BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),\n          ],\n        ),\n        child: Text('${a}'));\n  }\n\n  final Graph graph = Graph();\n  SugiyamaConfiguration builder = SugiyamaConfiguration()\n    ..bendPointShape = CurvedBendPointShape(curveLength: 20);\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n\n    final randomNode = graph.nodes.firstWhere(\n          (node) => node.key != null && node.key!.value == nextNodeId,\n      orElse: () => graph.nodes.first,\n    );\n    final nodeId = randomNode.key!;\n    _controller.animateToNode(nodeId);\n\n    setState(() {\n      nextNodeId = r.nextInt(graph.nodes.length) + 1;\n    });\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    final node1 = Node.Id(1);\n    final node2 = Node.Id(2);\n    final node3 = Node.Id(3);\n    final node4 = Node.Id(4);\n    final node5 = Node.Id(5);\n    final node6 = Node.Id(6);\n    final node8 = Node.Id(7);\n    final node7 = Node.Id(8);\n    final node9 = Node.Id(9);\n    final node10 = Node.Id(10);\n    final node11 = Node.Id(11);\n    final node12 = Node.Id(12);\n    final node13 = Node.Id(13);\n    final node14 = Node.Id(14);\n    final node15 = Node.Id(15);\n    final node16 = Node.Id(16);\n    final node17 = Node.Id(17);\n    final node18 = Node.Id(18);\n    final node19 = Node.Id(19);\n    final node20 = Node.Id(20);\n    final node21 = Node.Id(21);\n    final node22 = Node.Id(22);\n    final node23 = Node.Id(23);\n\n    graph.addEdge(node1, node13, paint: Paint()..color = Colors.red);\n    graph.addEdge(node1, node21);\n    graph.addEdge(node1, node4);\n    graph.addEdge(node1, node3);\n    graph.addEdge(node2, node3);\n    graph.addEdge(node2, node20);\n    graph.addEdge(node3, node4);\n    graph.addEdge(node3, node5);\n    graph.addEdge(node3, node23);\n    graph.addEdge(node4, node6);\n    graph.addEdge(node5, node7);\n    graph.addEdge(node6, node8);\n    graph.addEdge(node6, node16);\n    graph.addEdge(node6, node23);\n    graph.addEdge(node7, node9);\n    graph.addEdge(node8, node10);\n    graph.addEdge(node8, node11);\n    graph.addEdge(node9, node12);\n    graph.addEdge(node10, node13);\n    graph.addEdge(node10, node14);\n    graph.addEdge(node10, node15);\n    graph.addEdge(node11, node15);\n    graph.addEdge(node11, node16);\n    graph.addEdge(node12, node20);\n    graph.addEdge(node13, node17);\n    graph.addEdge(node14, node17);\n    graph.addEdge(node14, node18);\n    graph.addEdge(node16, node18);\n    graph.addEdge(node16, node19);\n    graph.addEdge(node16, node20);\n    graph.addEdge(node18, node21);\n    graph.addEdge(node19, node22);\n    graph.addEdge(node21, node23);\n    graph.addEdge(node22, node23);\n    graph.addEdge(node1, node22);\n    graph.addEdge(node7, node8);\n\n    builder\n      ..nodeSeparation = (15)\n      ..levelSeparation = (15)\n      ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM;\n\n    // Set initial random node for navigation\n    nextNodeId = r.nextInt(22); // 0-21 nodes exist\n  }\n}"
  },
  {
    "path": "example/lib/layer_graphview.dart",
    "content": "import 'dart:math';\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LayeredGraphViewPage extends StatefulWidget {\n  @override\n  _LayeredGraphViewPageState createState() => _LayeredGraphViewPageState();\n}\n\nclass _LayeredGraphViewPageState extends State<LayeredGraphViewPage> {\n  final GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 0;\n  bool _showControls = true;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      backgroundColor: Colors.grey[50],\n      appBar: AppBar(\n        title: Text('Graph Visualizer', style: TextStyle(fontWeight: FontWeight.w600)),\n        backgroundColor: Colors.white,\n        foregroundColor: Colors.grey[800],\n        elevation: 0,\n        actions: [\n          IconButton(\n            icon: Icon(_showControls ? Icons.visibility_off : Icons.visibility),\n            onPressed: () => setState(() => _showControls = !_showControls),\n            tooltip: 'Toggle Controls',\n          ),\n          IconButton(\n            icon: Icon(Icons.shuffle),\n            onPressed: _navigateToRandomNode,\n            tooltip: 'Random Node',\n          ),\n        ],\n      ),\n      body: Column(\n        children: [\n          AnimatedContainer(\n            duration: Duration(milliseconds: 300),\n            height: _showControls ? null : 0,\n            child: AnimatedOpacity(\n              duration: Duration(milliseconds: 300),\n              opacity: _showControls ? 1.0 : 0.0,\n              child: _buildControlPanel(),\n            ),\n          ),\n          Expanded(child: _buildGraphView()),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildControlPanel() {\n    return Container(\n      margin: EdgeInsets.all(16),\n      padding: EdgeInsets.all(20),\n      decoration: BoxDecoration(\n        color: Colors.white,\n        borderRadius: BorderRadius.circular(16),\n        boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 2))],\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          SizedBox(height: 16),\n          _buildNumericControls(),\n          SizedBox(height: 16),\n          _buildShapeControls(),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildNumericControls() {\n    return Wrap(\n      spacing: 12,\n      runSpacing: 12,\n      children: [\n        _buildSliderControl('Node Sep', builder.nodeSeparation, 5, 50, (v) => builder.nodeSeparation = v),\n        _buildSliderControl('Level Sep', builder.levelSeparation, 5, 100, (v) => builder.levelSeparation = v),\n        _buildDropdown<CoordinateAssignment>('Alignment', builder.coordinateAssignment, CoordinateAssignment.values, (v) => builder.coordinateAssignment = v),\n        _buildDropdown<LayeringStrategy>('Layering', builder.layeringStrategy, LayeringStrategy.values, (v) => builder.layeringStrategy = v),\n        _buildDropdown<CrossMinimizationStrategy>('Cross Min', builder.crossMinimizationStrategy, CrossMinimizationStrategy.values, (v) => builder.crossMinimizationStrategy = v),\n        _buildDropdown<CycleRemovalStrategy>('Cycle Removal', builder.cycleRemovalStrategy, CycleRemovalStrategy.values, (v) => builder.cycleRemovalStrategy = v),\n      ],\n    );\n  }\n\n  Widget _buildSliderControl(String label, int value, int min, int max, Function(int) onChanged) {\n    return Container(\n      width: 200,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),\n          Slider(\n            value: value.toDouble().clamp(min.toDouble(), max.toDouble()),\n            min: min.toDouble(),\n            max: max.toDouble(),\n            divisions: max - min,\n            label: value.toString(),\n            onChanged: (v) => setState(() => onChanged(v.round())),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildDropdown<T>(String label, T value, List<T> items, Function(T) onChanged) {\n    return Container(\n      width: 160,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),\n          SizedBox(height: 4),\n          Container(\n            padding: EdgeInsets.symmetric(horizontal: 12),\n            decoration: BoxDecoration(\n              border: Border.all(color: Colors.grey[300]!),\n              borderRadius: BorderRadius.circular(8),\n            ),\n            child: DropdownButtonHideUnderline(\n              child: DropdownButton<T>(\n                value: value,\n                isExpanded: true,\n                items: items.map((item) => DropdownMenuItem(value: item, child: Text(item.toString().split('.').last, style: TextStyle(fontSize: 12)))).toList(),\n                onChanged: (v) => setState(() => onChanged(v!)),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildShapeControls() {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text('Edge Shape', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),\n        SizedBox(height: 8),\n        Row(\n          children: [\n            _buildShapeButton('Sharp', builder.bendPointShape is SharpBendPointShape, () => builder.bendPointShape = SharpBendPointShape()),\n            SizedBox(width: 8),\n            _buildShapeButton('Curved', builder.bendPointShape is CurvedBendPointShape, () => builder.bendPointShape = CurvedBendPointShape(curveLength: 20)),\n            SizedBox(width: 8),\n            _buildShapeButton('Max Curved', builder.bendPointShape is MaxCurvedBendPointShape, () => builder.bendPointShape = MaxCurvedBendPointShape()),\n            Spacer(),\n            Row(\n              children: [\n                Text('Post Straighten', style: TextStyle(fontSize: 12)),\n                Switch(\n                  value: builder.postStraighten,\n                  onChanged: (v) => setState(() => builder.postStraighten = v),\n                  activeThumbColor: Colors.blue,\n                ),\n              ],\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _buildShapeButton(String text, bool isSelected, VoidCallback onPressed) {\n    return ElevatedButton(\n      onPressed: () => setState(onPressed),\n      child: Text(text, style: TextStyle(fontSize: 11)),\n      style: ElevatedButton.styleFrom(\n        backgroundColor: isSelected ? Colors.blue : Colors.grey[100],\n        foregroundColor: isSelected ? Colors.white : Colors.grey[700],\n        elevation: isSelected ? 2 : 0,\n        padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n      ),\n    );\n  }\n\n  Widget _buildGraphView() {\n    return Container(\n      margin: EdgeInsets.fromLTRB(16, 0, 16, 16),\n      decoration: BoxDecoration(\n        color: Colors.white,\n        borderRadius: BorderRadius.circular(16),\n        boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 2))],\n      ),\n      child: ClipRRect(\n        borderRadius: BorderRadius.circular(16),\n        child: GraphView.builder(\n          controller: _controller,\n          graph: graph,\n          algorithm: SugiyamaAlgorithm(builder),\n          paint: Paint()\n            ..color = Colors.blue[300]!\n            ..strokeWidth = 2\n            ..style = PaintingStyle.stroke,\n          builder: (Node node) {\n            final nodeId = node.key!.value as int;\n            return Container(\n              width: 40,\n              height: 40,\n              decoration: BoxDecoration(\n                gradient: LinearGradient(\n                  colors: [Colors.blue[400]!, Colors.blue[600]!],\n                  begin: Alignment.topLeft,\n                  end: Alignment.bottomRight,\n                ),\n                shape: BoxShape.circle,\n                boxShadow: [BoxShadow(color: Colors.blue[100]!, blurRadius: 8, offset: Offset(0, 2))],\n              ),\n              child: Center(\n                child: Text('$nodeId', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 14)),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  final Graph graph = Graph();\n  SugiyamaConfiguration builder = SugiyamaConfiguration()\n    ..bendPointShape = CurvedBendPointShape(curveLength: 20)\n    ..nodeSeparation = 15\n    ..levelSeparation = 15\n    ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM;\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n    final randomNode = graph.nodes[r.nextInt(graph.nodes.length)];\n    _controller.animateToNode(randomNode.key!);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _initializeGraph();\n  }\n\n  void _initializeGraph() {\n    // Define edges more concisely\n    final node1 = Node.Id(1);\n    final node2 = Node.Id(2);\n    final node3 = Node.Id(3);\n    final node4 = Node.Id(4);\n    final node5 = Node.Id(5);\n    final node6 = Node.Id(6);\n    final node8 = Node.Id(7);\n    final node7 = Node.Id(8);\n    final node9 = Node.Id(9);\n    final node10 = Node.Id(10);\n    final node11 = Node.Id(11);\n    final node12 = Node.Id(12);\n    final node13 = Node.Id(13);\n    final node14 = Node.Id(14);\n    final node15 = Node.Id(15);\n    final node16 = Node.Id(16);\n    final node17 = Node.Id(17);\n    final node18 = Node.Id(18);\n    final node19 = Node.Id(19);\n    final node20 = Node.Id(20);\n    final node21 = Node.Id(21);\n    final node22 = Node.Id(22);\n    final node23 = Node.Id(23);\n\n    graph.addEdge(node1, node13, paint: Paint()..color = Colors.red);\n    graph.addEdge(node1, node21);\n    graph.addEdge(node1, node4);\n    graph.addEdge(node1, node3);\n    graph.addEdge(node2, node3);\n    graph.addEdge(node2, node20);\n    graph.addEdge(node3, node4);\n    graph.addEdge(node3, node5);\n    graph.addEdge(node3, node23);\n    graph.addEdge(node4, node6);\n    graph.addEdge(node5, node7);\n    graph.addEdge(node6, node8);\n    graph.addEdge(node6, node16);\n    graph.addEdge(node6, node23);\n    graph.addEdge(node7, node9);\n    graph.addEdge(node8, node10);\n    graph.addEdge(node8, node11);\n    graph.addEdge(node9, node12);\n    graph.addEdge(node10, node13);\n    graph.addEdge(node10, node14);\n    graph.addEdge(node10, node15);\n    graph.addEdge(node11, node15);\n    graph.addEdge(node11, node16);\n    graph.addEdge(node12, node20);\n    graph.addEdge(node13, node17);\n    graph.addEdge(node14, node17);\n    graph.addEdge(node14, node18);\n    graph.addEdge(node16, node18);\n    graph.addEdge(node16, node19);\n    graph.addEdge(node16, node20);\n    graph.addEdge(node18, node21);\n    graph.addEdge(node19, node22);\n    graph.addEdge(node21, node23);\n    graph.addEdge(node22, node23);\n    graph.addEdge(node1, node22);\n    graph.addEdge(node7, node8);\n  }\n}"
  },
  {
    "path": "example/lib/layer_graphview_json.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LayerGraphPageFromJson extends StatefulWidget {\n  @override\n  _LayerGraphPageFromJsonState createState() => _LayerGraphPageFromJsonState();\n}\n\nclass _LayerGraphPageFromJsonState extends State<LayerGraphPageFromJson> {\n  var  json =   {\n    'edges': [\n      {\n        'from': '1',\n        'to': '2'\n      },\n      {\n        'from': '3',\n        'to': '2'\n      },\n      {\n        'from': '4',\n        'to': '5'\n      },\n      {\n        'from': '6',\n        'to': '4'\n      },\n      {\n        'from': '2',\n        'to': '4'\n      },\n      {\n        'from': '2',\n        'to': '7'\n      },\n      {\n        'from': '2',\n        'to': '8'\n      },\n      {\n        'from': '9',\n        'to': '10'\n      },\n      {\n        'from': '9',\n        'to': '11'\n      },\n      {\n        'from': '5',\n        'to': '12'\n      },\n      {\n        'from': '4',\n        'to': '9'\n      },\n      {\n        'from': '6',\n        'to': '13'\n      },\n      {\n        'from': '6',\n        'to': '14'\n      },\n      {\n        'from': '6',\n        'to': '15'\n      },\n      {\n        'from': '16',\n        'to': '3'\n      },\n      {\n        'from': '17',\n        'to': '3'\n      },\n      {\n        'from': '18',\n        'to': '16'\n      },\n      {\n        'from': '19',\n        'to': '17'\n      },\n      {\n        'from': '11',\n        'to': '1'\n      },\n\n    ]\n  };\n\n  GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.nodeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Node Separation'),\n                    onChanged: (text) {\n                      builder.nodeSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: Column(\n                    children: [\n                      Text('Alignment'),\n                      DropdownButton<CoordinateAssignment>(\n                        value: builder.coordinateAssignment,\n                        items: CoordinateAssignment.values.map((coordinateAssignment) {\n                          return DropdownMenuItem<CoordinateAssignment>(\n                            value: coordinateAssignment,\n                            child: Text(coordinateAssignment.name),\n                          );\n                        }).toList(),\n                        onChanged: (value) {\n                          setState(() {\n                            builder.coordinateAssignment = value!;\n                          });\n                        },\n                      ),\n                    ],\n                  ),\n                ),\n                ElevatedButton(\n                  onPressed: () => _navigateToRandomNode(),\n                  child: Text('Go to Node $nextNodeId'),\n                ),\n                ElevatedButton(\n                  onPressed: () => _controller.resetView(),\n                  child: Text('Reset View'),\n                ),\n                ElevatedButton(\n                  onPressed: () => _controller.zoomToFit(),\n                  child: Text('Zoom to fit'),\n                ),\n              ],\n            ),\n            Expanded(\n              child: GraphView.builder(\n                controller: _controller,\n                graph: graph,\n                algorithm: SugiyamaAlgorithm(builder),\n                paint: Paint()\n                  ..color = Colors.green\n                  ..strokeWidth = 1\n                  ..style = PaintingStyle.stroke,\n                builder: (Node node) {\n                  // I can decide what widget should be shown here based on the id\n                  var a = node.key!.value;\n                  return rectangleWidget(a, node);\n                },\n              ),\n            ),\n          ],\n        ));\n  }\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n\n    final randomNode = graph.nodes.firstWhere(\n          (node) => node.key != null && node.key!.value == nextNodeId,\n      orElse: () => graph.nodes.first,\n    );\n    final nodeId = randomNode.key!;\n    _controller.animateToNode(nodeId);\n\n    setState(() {\n      nextNodeId = r.nextInt(graph.nodes.length) + 1;\n    });\n  }\n\n\n  Widget rectangleWidget(String? a, Node node) {\n    return Container(\n      color: Colors.amber,\n      child: InkWell(\n        onTap: () {\n          print('clicked');\n        },\n        child: Container(\n            padding: EdgeInsets.all(16),\n            decoration: BoxDecoration(\n              borderRadius: BorderRadius.circular(4),\n              boxShadow: [\n                BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),\n              ],\n            ),\n            child: Text('${a}')),\n      ),\n    );\n  }\n\n  final Graph graph = Graph();\n  @override\n  void initState() {\n    super.initState();\n    var edges = json['edges']!;\n    edges.forEach((element) {\n      var fromNodeId = element['from'];\n      var toNodeId = element['to'];\n      graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId));\n    });\n\n    builder\n      ..nodeSeparation = (15)\n      ..levelSeparation = (15)\n      ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM;\n    }\n\n  }\n\n  var builder = SugiyamaConfiguration();\n\n"
  },
  {
    "path": "example/lib/main.dart",
    "content": "import 'package:example/algorithm_selector_graphview.dart';\nimport 'package:example/decision_tree_screen.dart';\nimport 'package:example/large_tree_graphview.dart';\nimport 'package:example/layer_graphview.dart';\nimport 'package:example/mindmap_graphview.dart';\nimport 'package:example/mutliple_forest_graphview.dart';\nimport 'package:example/tree_graphview_json.dart';\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nimport 'force_directed_graphview.dart';\nimport 'graph_cluster_animated.dart';\nimport 'layer_eiglesperger_graphview.dart';\nimport 'layer_graphview_json.dart';\nimport 'tree_graphview.dart';\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'GraphView Demo',\n      theme: ThemeData(\n        primarySwatch: Colors.blue,\n        fontFamily: 'SF Pro Display',\n        visualDensity: VisualDensity.adaptivePlatformDensity,\n      ),\n      home: Home(),\n      debugShowCheckedModeBanner: false,\n    );\n  }\n}\n\nclass Home extends StatelessWidget {\n  const Home({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Container(\n        decoration: BoxDecoration(\n          gradient: LinearGradient(\n            begin: Alignment.topLeft,\n            end: Alignment.bottomRight,\n            colors: [\n              Color(0xFF667eea),\n              Color(0xFF764ba2),\n            ],\n          ),\n        ),\n        child: SafeArea(\n          child: Column(\n            children: [\n              Expanded(\n                child: _buildScrollableContent(),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildScrollableContent() {\n    return SingleChildScrollView(\n      physics: BouncingScrollPhysics(),\n      padding: EdgeInsets.all(20),\n      child: Column(\n        children: [\n          _buildSection('Tree Algorithms', [\n            _buildButton(\n              'Tree View',\n              'BuchheimWalker Algorithm',\n              Icons.account_tree,\n              Colors.deepPurple,\n              () => TreeViewPage(),\n            ),\n            _buildButton(\n              'Tree from JSON',\n              'Dynamic tree generation',\n              Icons.data_object,\n              Colors.indigo,\n              () => TreeViewPageFromJson(),\n            ),\n            _buildButton(\n              'Large Tree View',\n              '1000 nodes',\n              Icons.data_object,\n              Colors.indigo,\n              () => LargeTreeViewPage(),\n            ),\n            _buildButton(\n              'Multiple Forest Tree View',\n              'Multiple Nodes',\n              Icons.data_object,\n              Colors.indigo,\n              () => MultipleForestTreeViewPage(),\n            ),\n          ]),\n          _buildSection('Layered Algorithms', [\n            _buildButton(\n              'Layered View',\n              'Sugiyama Algorithm',\n              Icons.layers,\n              Colors.teal,\n              () => LayeredGraphViewPage(),\n            ),\n            _buildButton(\n              'Layer from JSON',\n              'JSON-based layered graphs',\n              Icons.timeline,\n              Colors.cyan,\n              () => LayerGraphPageFromJson(),\n            ),\n            _buildButton(\n              'Decision Tree',\n              'Decision-making visualization',\n              Icons.device_hub,\n              Colors.green,\n              () => DecisionTreeScreen(),\n            ),\n          ]),\n          _buildSection('Cluster Algorithms', [\n            _buildButton(\n              'Graph Cluster',\n              'FruchtermanReingold Algorithm',\n              Icons.bubble_chart,\n              Colors.orange,\n              () => GraphClusterViewPage(),\n            ),\n            _buildCustomGraphButton(\n              'Square Grid',\n              'Structured 3x3 layout',\n              Icons.grid_3x3,\n              Colors.pink,\n              _createSquareGraph,\n            ),\n            _buildCustomGraphButton(\n              'Triangle Grid',\n              'Complex network topology',\n              Icons.change_history,\n              Colors.deepOrange,\n              _createTriangleGraph,\n            ),\n          ]),\n          _buildSection('Specialized Views', [\n            _buildButton(\n              'Algorithm SelectorPage',\n              'Multiple Algorithms using the same graph',\n              Icons.code,\n              Colors.brown,\n              () => AlgorithmSelectedVIewPage(),\n            ),\n            _buildButton(\n              'Mind Map',\n              'Conceptual mapping',\n              Icons.psychology,\n              Colors.purple,\n              () => MindMapPage(),\n            ),\n            _buildButton(\n              'Layered View',\n              'Eiglesperger Algorithm (Broken)',\n              Icons.layers,\n              Colors.teal,\n                  () => LayeredEiglspergerGraphViewPage(),\n            ),\n          ]),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildSection(String title, List<Widget> buttons) {\n    return Container(\n      margin: EdgeInsets.only(bottom: 24),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Padding(\n            padding: EdgeInsets.only(left: 4, bottom: 12),\n            child: Text(\n              title,\n              style: TextStyle(\n                fontSize: 20,\n                fontWeight: FontWeight.w600,\n                color: Colors.white,\n                letterSpacing: -0.3,\n              ),\n            ),\n          ),\n          ...buttons.map((button) => Padding(\n                padding: EdgeInsets.only(bottom: 12),\n                child: button,\n              )),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildButton(\n    String title,\n    String subtitle,\n    IconData icon,\n    Color color,\n    Widget Function() pageBuilder,\n  ) {\n    return Builder(\n      builder: (context) => Container(\n        height: 80,\n        child: Material(\n          borderRadius: BorderRadius.circular(16),\n          elevation: 4,\n          shadowColor: Colors.black.withValues(alpha: 0.1),\n          child: InkWell(\n            borderRadius: BorderRadius.circular(16),\n            onTap: () => Navigator.push(\n              context,\n              MaterialPageRoute(builder: (context) => pageBuilder()),\n            ),\n            child: Container(\n              padding: EdgeInsets.all(16),\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(16),\n                gradient: LinearGradient(\n                  begin: Alignment.centerLeft,\n                  end: Alignment.centerRight,\n                  colors: [\n                    color.withValues(alpha: 0.1),\n                    Colors.white,\n                  ],\n                ),\n                border: Border.all(\n                  color: color.withValues(alpha: 0.3),\n                  width: 1,\n                ),\n              ),\n              child: Row(\n                children: [\n                  Container(\n                    width: 48,\n                    height: 48,\n                    decoration: BoxDecoration(\n                      color: color.withValues(alpha: 0.1),\n                      borderRadius: BorderRadius.circular(12),\n                    ),\n                    child: Icon(\n                      icon,\n                      color: color,\n                      size: 24,\n                    ),\n                  ),\n                  SizedBox(width: 16),\n                  Expanded(\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Text(\n                          title,\n                          style: TextStyle(\n                            fontSize: 16,\n                            fontWeight: FontWeight.w600,\n                            color: Colors.grey[800],\n                          ),\n                        ),\n                        SizedBox(height: 2),\n                        Text(\n                          subtitle,\n                          style: TextStyle(\n                            fontSize: 13,\n                            color: Colors.grey[600],\n                            fontWeight: FontWeight.w400,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                  Icon(\n                    Icons.arrow_forward_ios,\n                    color: Colors.grey[400],\n                    size: 16,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCustomGraphButton(\n    String title,\n    String subtitle,\n    IconData icon,\n    Color color,\n    Graph Function() graphBuilder,\n  ) {\n    return Builder(\n      builder: (context) => Container(\n        height: 80,\n        child: Material(\n          borderRadius: BorderRadius.circular(16),\n          elevation: 4,\n          shadowColor: Colors.black.withValues(alpha: 0.1),\n          child: InkWell(\n            borderRadius: BorderRadius.circular(16),\n            onTap: () {\n              var graph = graphBuilder();\n\n              var builder = FruchtermanReingoldAlgorithm(\n                  FruchtermanReingoldConfiguration());\n              Navigator.push(\n                context,\n                MaterialPageRoute(\n                  builder: (context) => GraphScreen(graph, builder, null),\n                ),\n              );\n            },\n            child: Container(\n              padding: EdgeInsets.all(16),\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(16),\n                gradient: LinearGradient(\n                  begin: Alignment.centerLeft,\n                  end: Alignment.centerRight,\n                  colors: [\n                    color.withValues(alpha: 0.1),\n                    Colors.white,\n                  ],\n                ),\n                border: Border.all(\n                  color: color.withValues(alpha: 0.3),\n                  width: 1,\n                ),\n              ),\n              child: Row(\n                children: [\n                  Container(\n                    width: 48,\n                    height: 48,\n                    decoration: BoxDecoration(\n                      color: color.withValues(alpha: 0.1),\n                      borderRadius: BorderRadius.circular(12),\n                    ),\n                    child: Icon(\n                      icon,\n                      color: color,\n                      size: 24,\n                    ),\n                  ),\n                  SizedBox(width: 16),\n                  Expanded(\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        Text(\n                          title,\n                          style: TextStyle(\n                            fontSize: 16,\n                            fontWeight: FontWeight.w600,\n                            color: Colors.grey[800],\n                          ),\n                        ),\n                        SizedBox(height: 2),\n                        Text(\n                          subtitle,\n                          style: TextStyle(\n                            fontSize: 13,\n                            color: Colors.grey[600],\n                            fontWeight: FontWeight.w400,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                  Icon(\n                    Icons.arrow_forward_ios,\n                    color: Colors.grey[400],\n                    size: 16,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Graph _createSquareGraph() {\n    var graph = Graph();\n    Node node1 = Node.Id('One');\n    Node node2 = Node.Id('Two');\n    Node node3 = Node.Id('Three');\n    Node node4 = Node.Id('Four');\n    Node node5 = Node.Id('Five');\n    Node node6 = Node.Id('Six');\n    Node node7 = Node.Id('Seven');\n    Node node8 = Node.Id('Eight');\n    Node node9 = Node.Id('Nine');\n\n    graph.addEdge(node1, node2);\n    graph.addEdge(node1, node4);\n    graph.addEdge(node2, node3);\n    graph.addEdge(node2, node5);\n    graph.addEdge(node3, node6);\n    graph.addEdge(node4, node5);\n    graph.addEdge(node4, node7);\n    graph.addEdge(node5, node6);\n    graph.addEdge(node5, node8);\n    graph.addEdge(node6, node9);\n    graph.addEdge(node7, node8);\n    graph.addEdge(node8, node9);\n\n    return graph;\n  }\n\n  Graph _createTriangleGraph() {\n    var graph = Graph();\n    Node node1 = Node.Id('One');\n    Node node2 = Node.Id('Two');\n    Node node3 = Node.Id('Three');\n    Node node4 = Node.Id('Four');\n    Node node5 = Node.Id('Five');\n    Node node6 = Node.Id('Six');\n    Node node7 = Node.Id('Seven');\n    Node node8 = Node.Id('Eight');\n    Node node9 = Node.Id('Nine');\n    Node node10 = Node.Id('Ten');\n\n    graph.addEdge(node1, node2);\n    graph.addEdge(node1, node3);\n    graph.addEdge(node2, node4);\n    graph.addEdge(node2, node5);\n    graph.addEdge(node2, node3);\n    graph.addEdge(node3, node5);\n    graph.addEdge(node3, node6);\n    graph.addEdge(node4, node7);\n    graph.addEdge(node4, node8);\n    graph.addEdge(node4, node5);\n    graph.addEdge(node5, node8);\n    graph.addEdge(node5, node9);\n    graph.addEdge(node5, node6);\n    graph.addEdge(node9, node6);\n    graph.addEdge(node10, node6);\n    graph.addEdge(node7, node8);\n    graph.addEdge(node8, node9);\n    graph.addEdge(node9, node10);\n\n    return graph;\n  }\n}\n"
  },
  {
    "path": "example/lib/mindmap_graphview.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass MindMapPage extends StatefulWidget {\n  @override\n  _MindMapPageState createState() => _MindMapPageState();\n}\n\nclass _MindMapPageState extends State<MindMapPage> with TickerProviderStateMixin {\n\n  GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 1;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n          title: Text('Tree View'),\n        ),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            // Configuration controls\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.siblingSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Sibling Separation'),\n                    onChanged: (text) {\n                      builder.siblingSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.subtreeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Subtree separation'),\n                    onChanged: (text) {\n                      builder.subtreeSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                ElevatedButton(\n                  onPressed: () {\n                    final node12 = Node.Id(r.nextInt(100));\n                    var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount()));\n                    print(edge);\n                    graph.addEdge(edge, node12);\n                    setState(() {});\n                  },\n                  child: Text('Add'),\n                ),\n                ElevatedButton(\n                  onPressed: _navigateToRandomNode,\n                  child: Text('Go to Node $nextNodeId'),\n                ),\n                SizedBox(width: 8),\n                ElevatedButton(\n                  onPressed: _resetView,\n                  child: Text('Reset View'),\n                ),\n                SizedBox(width: 8,),\n                ElevatedButton(onPressed: (){\n                  _controller.zoomToFit();\n                }, child: Text('Zoom to fit'))\n              ],\n            ),\n\n            Expanded(\n              child: GraphView.builder(\n                controller: _controller,\n                graph: graph,\n                algorithm: MindmapAlgorithm(\n                  builder, MindmapEdgeRenderer(builder)\n                ),\n                builder: (Node node) => Container(\n                  padding: EdgeInsets.all(16),\n                  decoration: BoxDecoration(\n                    color: Colors.white,\n                    borderRadius: BorderRadius.circular(4),\n                    boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)],\n                  ),\n                  child: Text(\n                    'Node ${node.key?.value}',\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ));\n  }\n\n  Widget rectangleWidget(int? a) {\n    return InkWell(\n      onTap: () {\n        print('clicked node $a');\n      },\n      child: Container(\n          padding: EdgeInsets.all(16),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(4),\n            boxShadow: [\n              BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),\n            ],\n          ),\n          child: Text('Node ${a} ')),\n    );\n  }\n\n  final Graph graph = Graph()..isTree = true;\n  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n\n    final randomNode = graph.nodes.firstWhere(\n      (node) => node.key != null && node.key!.value == nextNodeId,\n      orElse: () => graph.nodes.first,\n    );\n    final nodeId = randomNode.key!;\n    _controller.animateToNode(nodeId);\n\n    setState(() {\n      nextNodeId = r.nextInt(graph.nodes.length) + 1;\n    });\n  }\n\n  void _resetView() {\n    _controller.resetView();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n\n// Complex Mindmap Test - This will stress test the balancing algorithm\n\n// Create all nodes\n    final root = Node.Id(1);  // Central topic\n\n// Left side - Technology branch (will be large)\n    final tech = Node.Id(2);\n    final ai = Node.Id(3);\n    final web = Node.Id(4);\n    final mobile = Node.Id(5);\n    final aiSubtopics = [\n      Node.Id(6),   // Machine Learning\n      Node.Id(7),   // Deep Learning\n      Node.Id(8),   // NLP\n      Node.Id(9),   // Computer Vision\n    ];\n    final webSubtopics = [\n      Node.Id(10),  // Frontend\n      Node.Id(11),  // Backend\n      Node.Id(12),  // DevOps\n    ];\n    final frontendDetails = [\n      Node.Id(13),  // React\n      Node.Id(14),  // Vue\n      Node.Id(15),  // Angular\n    ];\n    final backendDetails = [\n      Node.Id(16),  // Node.js\n      Node.Id(17),  // Python\n      Node.Id(18),  // Java\n      Node.Id(19),  // Go\n    ];\n\n// Right side - Business branch (will be smaller to test balancing)\n    final business = Node.Id(20);\n    final marketing = Node.Id(21);\n    final sales = Node.Id(22);\n    final finance = Node.Id(23);\n    final marketingDetails = [\n      Node.Id(24),  // Digital Marketing\n      Node.Id(25),  // Content Strategy\n    ];\n    final salesDetails = [\n      Node.Id(26),  // B2B Sales\n      Node.Id(27),  // Customer Success\n    ];\n\n// Additional right side - Personal branch\n    final personal = Node.Id(28);\n    final health = Node.Id(29);\n    final hobbies = Node.Id(30);\n    final healthDetails = [\n      Node.Id(31),  // Exercise\n      Node.Id(32),  // Nutrition\n      Node.Id(33),  // Mental Health\n    ];\n    final exerciseDetails = [\n      Node.Id(34),  // Cardio\n      Node.Id(35),  // Strength Training\n      Node.Id(36),  // Yoga\n    ];\n\n// Build the graph structure\n    graph.addEdge(root, tech);\n    graph.addEdge(root, business, paint: Paint()..color = Colors.blue);\n    graph.addEdge(root, personal, paint: Paint()..color = Colors.green);\n\n// Technology branch (left side - large subtree)\n    graph.addEdge(tech, ai);\n    graph.addEdge(tech, web);\n    graph.addEdge(tech, mobile);\n\n// AI subtree\n    for (final aiNode in aiSubtopics) {\n      graph.addEdge(ai, aiNode, paint: Paint()..color = Colors.purple);\n    }\n\n// Web subtree with deep nesting\n    for (final webNode in webSubtopics) {\n      graph.addEdge(web, webNode, paint: Paint()..color = Colors.orange);\n    }\n\n// Frontend details (3rd level)\n    for (final frontendNode in frontendDetails) {\n      graph.addEdge(webSubtopics[0], frontendNode, paint: Paint()..color = Colors.cyan);\n    }\n\n// Backend details (3rd level) - even deeper\n    for (final backendNode in backendDetails) {\n      graph.addEdge(webSubtopics[1], backendNode, paint: Paint()..color = Colors.teal);\n    }\n\n// Business branch (right side - smaller subtree)\n    graph.addEdge(business, marketing);\n    graph.addEdge(business, sales);\n    graph.addEdge(business, finance);\n\n// Marketing details\n    for (final marketingNode in marketingDetails) {\n      graph.addEdge(marketing, marketingNode, paint: Paint()..color = Colors.red);\n    }\n\n// Sales details\n    for (final salesNode in salesDetails) {\n      graph.addEdge(sales, salesNode, paint: Paint()..color = Colors.indigo);\n    }\n\n// Personal branch (right side - medium subtree)\n    graph.addEdge(personal, health);\n    graph.addEdge(personal, hobbies);\n\n// Health details\n    for (final healthNode in healthDetails) {\n      graph.addEdge(health, healthNode, paint: Paint()..color = Colors.lightGreen);\n    }\n\n// Exercise details (3rd level)\n    for (final exerciseNode in exerciseDetails) {\n      graph.addEdge(healthDetails[0], exerciseNode, paint: Paint()..color = Colors.amber);\n    }\n\n    builder\n      ..siblingSeparation = (100)\n      ..levelSeparation = (150)\n      ..subtreeSeparation = (150)\n      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n  }\n\n}"
  },
  {
    "path": "example/lib/mutliple_forest_graphview.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass MultipleForestTreeViewPage extends StatefulWidget {\n  @override\n  _TreeViewPageState createState() => _TreeViewPageState();\n}\n\nclass _TreeViewPageState extends State<MultipleForestTreeViewPage> with TickerProviderStateMixin {\n\n  GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 1;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n          title: Text('Tree View'),\n        ),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            // Configuration controls\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.siblingSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Sibling Separation'),\n                    onChanged: (text) {\n                      builder.siblingSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.subtreeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Subtree separation'),\n                    onChanged: (text) {\n                      builder.subtreeSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                ElevatedButton(\n                  onPressed: () {\n                    final node12 = Node.Id(r.nextInt(100));\n                    var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount()));\n                    print(edge);\n                    graph.addEdge(edge, node12);\n                    setState(() {});\n                  },\n                  child: Text('Add'),\n                ),\n                ElevatedButton(\n                  onPressed: _navigateToRandomNode,\n                  child: Text('Go to Node $nextNodeId'),\n                ),\n                SizedBox(width: 8),\n                ElevatedButton(\n                  onPressed: _resetView,\n                  child: Text('Reset View'),\n                ),\n                SizedBox(width: 8,),\n                ElevatedButton(onPressed: (){\n                  _controller.zoomToFit();\n                }, child: Text('Zoom to fit'))\n              ],\n            ),\n\n            Expanded(\n              child: GraphView.builder(\n                controller: _controller,\n                graph: graph,\n                algorithm: TidierTreeLayoutAlgorithm(builder, null),\n                builder: (Node node) => Container(\n                  padding: EdgeInsets.all(8),\n                  decoration: BoxDecoration(\n                    color: Colors.lightBlue[100],\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                  child: Text(node.key?.value.toString() ?? ''),\n                ),\n              )\n            ),\n          ],\n        ));\n  }\n\n  Widget rectangleWidget(int? a) {\n    return InkWell(\n      onTap: () {\n        print('clicked node $a');\n      },\n      child: Container(\n          padding: EdgeInsets.all(16),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(4),\n            boxShadow: [\n              BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),\n            ],\n          ),\n          child: Text('Node ${a} ')),\n    );\n  }\n\n  final Graph graph = Graph()..isTree = true;\n  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n\n    final randomNode = graph.nodes.firstWhere(\n      (node) => node.key != null && node.key!.value == nextNodeId,\n      orElse: () => graph.nodes.first,\n    );\n    final nodeId = randomNode.key!;\n    _controller.animateToNode(nodeId);\n\n    setState(() {\n      nextNodeId = r.nextInt(graph.nodes.length) + 1;\n    });\n  }\n\n  void _resetView() {\n    _controller.resetView();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    var json = {\n      'edges': [\n        {'from': 1, 'to': 2},\n        {'from': 9, 'to': 2},\n        {'from': 10, 'to': 2},\n        {'from': 2, 'to': 3},\n        {'from': 2, 'to': 4},\n        {'from': 2, 'to': 5},\n        {'from': 5, 'to': 6},\n        {'from': 5, 'to': 7},\n        {'from': 6, 'to': 8},\n        {'from': 12, 'to': 11},\n      ]\n    };\n\n    var edges = json['edges']!;\n    edges.forEach((element) {\n      var fromNodeId = element['from'];\n      var toNodeId = element['to'];\n      graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId));\n    });\n\n    builder\n      ..siblingSeparation = (100)\n      ..levelSeparation = (150)\n      ..subtreeSeparation = (150)\n      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n  }\n\n}"
  },
  {
    "path": "example/lib/tree_graphview.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass TreeViewPage extends StatefulWidget {\n  @override\n  _TreeViewPageState createState() => _TreeViewPageState();\n}\n\nclass _TreeViewPageState extends State<TreeViewPage> with TickerProviderStateMixin {\n\n  GraphViewController _controller = GraphViewController();\n  final Random r = Random();\n  int nextNodeId = 1;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(\n          title: Text('Tree View'),\n        ),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            // Configuration controls\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.siblingSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Sibling Separation'),\n                    onChanged: (text) {\n                      builder.siblingSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.subtreeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Subtree separation'),\n                    onChanged: (text) {\n                      builder.subtreeSeparation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      this.setState(() {});\n                    },\n                  ),\n                ),\n                ElevatedButton(\n                  onPressed: () {\n                    final node12 = Node.Id(r.nextInt(100));\n                    var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount()));\n                    print(edge);\n                    graph.addEdge(edge, node12);\n                    setState(() {});\n                  },\n                  child: Text('Add'),\n                ),\n                ElevatedButton(\n                  onPressed: _navigateToRandomNode,\n                  child: Text('Go to Node $nextNodeId'),\n                ),\n                SizedBox(width: 8),\n                ElevatedButton(\n                  onPressed: _resetView,\n                  child: Text('Reset View'),\n                ),\n                SizedBox(width: 8,),\n                ElevatedButton(onPressed: (){\n                  _controller.zoomToFit();\n                }, child: Text('Zoom to fit'))\n              ],\n            ),\n\n            Expanded(\n              child: GraphView.builder(\n                controller: _controller,\n                graph: graph,\n                algorithm: algorithm,\n                initialNode: ValueKey(1),\n                panAnimationDuration: Duration(milliseconds: 600),\n                toggleAnimationDuration: Duration(milliseconds: 600),\n                centerGraph: true,\n                builder: (Node node) => GestureDetector(\n                  onTap: () => _toggleCollapse(node),\n                  child: Container(\n                    padding: EdgeInsets.all(16),\n                    decoration: BoxDecoration(\n                      color: Colors.white,\n                      borderRadius: BorderRadius.circular(4),\n                      boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)],\n                    ),\n                    child: Text(\n                      'Node ${node.key?.value}',\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ));\n  }\n\n  final Graph graph = Graph()..isTree = true;\n  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();\n  late final algorithm = BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder));\n\n  void _toggleCollapse(Node node) {\n    _controller.toggleNodeExpanded(graph, node, animate: true);\n  }\n\n  void _navigateToRandomNode() {\n    if (graph.nodes.isEmpty) return;\n\n    final randomNode = graph.nodes.firstWhere(\n      (node) => node.key != null && node.key!.value == nextNodeId,\n      orElse: () => graph.nodes.first,\n    );\n    final nodeId = randomNode.key!;\n    _controller.animateToNode(nodeId);\n\n    setState(() {\n      // nextNodeId = r.nextInt(graph.nodes.length) + 1;\n    });\n  }\n\n  void _resetView() {\n    _controller.animateToNode(ValueKey(1));\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n\n\n// Create all nodes\n    final root = Node.Id(1);  // Central topic\n\n// Left side - Technology branch (will be large)\n    final tech = Node.Id(2);\n    final ai = Node.Id(3);\n    final web = Node.Id(4);\n    final mobile = Node.Id(5);\n    final aiSubtopics = [\n      Node.Id(6),   // Machine Learning\n      Node.Id(7),   // Deep Learning\n      Node.Id(8),   // NLP\n      Node.Id(9),   // Computer Vision\n    ];\n    final webSubtopics = [\n      Node.Id(10),  // Frontend\n      Node.Id(11),  // Backend\n      Node.Id(12),  // DevOps\n    ];\n    final frontendDetails = [\n      Node.Id(13),  // React\n      Node.Id(14),  // Vue\n      Node.Id(15),  // Angular\n    ];\n    final backendDetails = [\n      Node.Id(16),  // Node.js\n      Node.Id(17),  // Python\n      Node.Id(18),  // Java\n      Node.Id(19),  // Go\n    ];\n\n// Right side - Business branch (will be smaller to test balancing)\n    final business = Node.Id(20);\n    final marketing = Node.Id(21);\n    final sales = Node.Id(22);\n    final finance = Node.Id(23);\n    final marketingDetails = [\n      Node.Id(24),  // Digital Marketing\n      Node.Id(25),  // Content Strategy\n    ];\n    final salesDetails = [\n      Node.Id(26),  // B2B Sales\n      Node.Id(27),  // Customer Success\n    ];\n\n// Additional right side - Personal branch\n    final personal = Node.Id(28);\n    final health = Node.Id(29);\n    final hobbies = Node.Id(30);\n    final healthDetails = [\n      Node.Id(31),  // Exercise\n      Node.Id(32),  // Nutrition\n      Node.Id(33),  // Mental Health\n    ];\n    final exerciseDetails = [\n      Node.Id(34),  // Cardio\n      Node.Id(35),  // Strength Training\n      Node.Id(36),  // Yoga\n    ];\n\n    // Build the graph structure\n    graph.addEdge(root, tech);\n    graph.addEdge(root, business, paint: Paint()..color = Colors.blue);\n    graph.addEdge(root, personal, paint: Paint()..color = Colors.green);\n\n// // Technology branch (left side - large subtree)\n    graph.addEdge(tech, ai);\n    graph.addEdge(tech, web);\n    graph.addEdge(tech, mobile);\n\n// AI subtree\n    for (final aiNode in aiSubtopics) {\n      graph.addEdge(ai, aiNode, paint: Paint()..color = Colors.purple);\n    }\n\n// Web subtree with deep nesting\n    for (final webNode in webSubtopics) {\n      graph.addEdge(web, webNode, paint: Paint()..color = Colors.orange);\n    }\n\n// Frontend details (3rd level)\n    for (final frontendNode in frontendDetails) {\n      graph.addEdge(webSubtopics[0], frontendNode, paint: Paint()..color = Colors.cyan);\n    }\n\n// Backend details (3rd level) - even deeper\n    for (final backendNode in backendDetails) {\n      graph.addEdge(webSubtopics[1], backendNode, paint: Paint()..color = Colors.teal);\n    }\n\n// Business branch (right side - smaller subtree)\n    graph.addEdge(business, marketing);\n    graph.addEdge(business, sales);\n    graph.addEdge(business, finance);\n\n// Marketing details\n    for (final marketingNode in marketingDetails) {\n      graph.addEdge(marketing, marketingNode, paint: Paint()..color = Colors.red);\n    }\n\n// Sales details\n    for (final salesNode in salesDetails) {\n      graph.addEdge(sales, salesNode, paint: Paint()..color = Colors.indigo);\n    }\n\n// Personal branch (right side - medium subtree)\n    graph.addEdge(personal, health);\n    graph.addEdge(personal, hobbies);\n\n// Health details\n    for (final healthNode in healthDetails) {\n      graph.addEdge(health, healthNode, paint: Paint()..color = Colors.lightGreen);\n    }\n\n// Exercise details (3rd level)\n    for (final exerciseNode in exerciseDetails) {\n      graph.addEdge(healthDetails[0], exerciseNode, paint: Paint()..color = Colors.amber);\n    }\n    _controller.setInitiallyCollapsedNodes(graph, [tech, business, personal]);\n\n    builder\n      ..siblingSeparation = (100)\n      ..levelSeparation = (150)\n      ..subtreeSeparation = (150)\n      ..useCurvedConnections = true\n      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n  }\n\n}"
  },
  {
    "path": "example/lib/tree_graphview_json.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass TreeViewPageFromJson extends StatefulWidget {\n  @override\n  _TreeViewPageFromJsonState createState() => _TreeViewPageFromJsonState();\n}\n\nclass _TreeViewPageFromJsonState extends State<TreeViewPageFromJson> {\n  var json = {\n    'nodes': [\n      {'id': 1, 'label': 'circle'},\n      {'id': 2, 'label': 'ellipse'},\n      {'id': 3, 'label': 'database'},\n      {'id': 4, 'label': 'box'},\n      {'id': 5, 'label': 'diamond'},\n      {'id': 6, 'label': 'dot'},\n      {'id': 7, 'label': 'square'},\n      {'id': 8, 'label': 'triangle'},\n    ],\n    'edges': [\n      {'from': 1, 'to': 2},\n      {'from': 2, 'to': 3},\n      {'from': 2, 'to': 4},\n      {'from': 2, 'to': 5},\n      {'from': 5, 'to': 6},\n      {'from': 5, 'to': 7},\n      {'from': 6, 'to': 8}\n    ]\n  };\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n        appBar: AppBar(),\n        body: Column(\n          mainAxisSize: MainAxisSize.max,\n          children: [\n            Wrap(\n              children: [\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.siblingSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Sibling Separation'),\n                    onChanged: (text) {\n                      builder.siblingSeparation = int.tryParse(text) ?? 100;\n                      setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.levelSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Level Separation'),\n                    onChanged: (text) {\n                      builder.levelSeparation = int.tryParse(text) ?? 100;\n                      setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.subtreeSeparation.toString(),\n                    decoration: InputDecoration(labelText: 'Subtree separation'),\n                    onChanged: (text) {\n                      builder.subtreeSeparation = int.tryParse(text) ?? 100;\n                      setState(() {});\n                    },\n                  ),\n                ),\n                Container(\n                  width: 100,\n                  child: TextFormField(\n                    initialValue: builder.orientation.toString(),\n                    decoration: InputDecoration(labelText: 'Orientation'),\n                    onChanged: (text) {\n                      builder.orientation = int.tryParse(text) ?? 100;\n                      setState(() {});\n                    },\n                  ),\n                ),\n              ],\n            ),\n            Expanded(\n              child: InteractiveViewer(\n                  constrained: false,\n                  boundaryMargin: EdgeInsets.all(100),\n                  minScale: 0.01,\n                  maxScale: 5.6,\n                  child: GraphView(\n                    graph: graph,\n                    algorithm: BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)),\n                    paint: Paint()\n                      ..color = Colors.green\n                      ..strokeWidth = 1\n                      ..style = PaintingStyle.stroke,\n                    builder: (Node node) {\n                      // I can decide what widget should be shown here based on the id\n                      var a = node.key!.value as int?;\n                      var nodes = json['nodes']!;\n                      var nodeValue = nodes.firstWhere((element) => element['id'] == a);\n                      return rectangleWidget(nodeValue['label'] as String?);\n                    },\n                  )),\n            ),\n          ],\n        ));\n  }\n\n  Widget rectangleWidget(String? a) {\n    return InkWell(\n      onTap: () {\n        print('clicked');\n      },\n      child: Container(\n          padding: EdgeInsets.all(16),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(4),\n            boxShadow: [\n              BoxShadow(color: Colors.blue[100]!, spreadRadius: 1),\n            ],\n          ),\n          child: Text('${a}')),\n    );\n  }\n\n  final Graph graph = Graph()..isTree = true;\n  BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();\n\n  @override\n  void initState() {\n    super.initState();\n\n    var edges = json['edges']!;\n    edges.forEach((element) {\n      var fromNodeId = element['from'];\n      var toNodeId = element['to'];\n      graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId));\n    });\n\n    builder\n      ..siblingSeparation = (100)\n      ..levelSeparation = (150)\n      ..subtreeSeparation = (150)\n      ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n  }\n}\n"
  },
  {
    "path": "example/pubspec.yaml",
    "content": "name: example\ndescription: A new Flutter project.\n\n# The following line prevents the package from being accidentally published to\n# pub.dev using `pub publish`. This is preferred for private packages.\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\n\n# The following defines the version and build number for your application.\n# A version number is three numbers separated by dots, like 1.2.43\n# followed by an optional build number separated by a +.\n# Both the version and the builder number may be overridden in flutter\n# build by specifying --build-name and --build-number, respectively.\n# In Android, build-name is used as versionName while build-number used as versionCode.\n# Read more about Android versioning at https://developer.android.com/studio/publish/versioning\n# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.\n# Read more about iOS versioning at\n# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html\nversion: 1.0.0+1\n\nenvironment:\n  sdk: '>=2.15.0 <4.0.0'\n\ndependencies:\n  flutter:\n    sdk: flutter\n  graphview:\n    path: ../\n  provider: ^6.0.3\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\n# The following section is specific to Flutter.\nflutter:\n\n  # The following line ensures that the Material Icons font is\n  # included with your application, so that you can use the icons in\n  # the material Icons class.\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  # assets:\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware.\n\n  # For details regarding adding assets from package dependencies, see\n  # https://flutter.dev/assets-and-images/#from-packages\n\n  # To add custom fonts to your application, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n  #\n  # For details regarding fonts from package dependencies,\n  # see https://flutter.dev/custom-fonts/#from-packages\n"
  },
  {
    "path": "lib/Algorithm.dart",
    "content": "part of graphview;\n\nabstract class Algorithm {\n  EdgeRenderer? renderer;\n\n  /// Executes the algorithm.\n  /// @param shiftY Shifts the y-coordinate origin\n  /// @param shiftX Shifts the x-coordinate origin\n  /// @return The size of the graph\n  Size run(Graph? graph, double shiftX, double shiftY);\n\n  void init(Graph? graph);\n\n  void setDimensions(double width, double height);\n}\n"
  },
  {
    "path": "lib/Graph.dart",
    "content": "part of graphview;\n\nclass Graph {\n  final List<Node> _nodes = [];\n  final List<Edge> _edges = [];\n  List<GraphObserver> graphObserver = [];\n\n  // Cache\n  final Map<Node, List<Node>> _successorCache = {};\n  final Map<Node, List<Node>> _predecessorCache = {};\n  bool _cacheValid = false;\n\n  List<Node> get nodes => _nodes;\n\n  List<Edge> get edges => _edges;\n\n  var isTree = false;\n\n  int nodeCount() => _nodes.length;\n\n  void addNode(Node node) {\n    _nodes.add(node);\n    _cacheValid = false;\n    notifyGraphObserver();\n  }\n\n  void addNodes(List<Node> nodes) => nodes.forEach((it) => addNode(it));\n\n  void removeNode(Node? node) {\n    if (!_nodes.contains(node)) return;\n\n    if (isTree) {\n      successorsOf(node).forEach((element) => removeNode(element));\n    }\n\n    _nodes.remove(node);\n    _edges\n        .removeWhere((edge) => edge.source == node || edge.destination == node);\n    _cacheValid = false;\n    notifyGraphObserver();\n  }\n\n  void removeNodes(List<Node> nodes) => nodes.forEach((it) => removeNode(it));\n\n  Edge addEdge(Node source, Node destination, {Paint? paint}) {\n    final edge = Edge(source, destination, paint: paint);\n    addEdgeS(edge);\n    return edge;\n  }\n\n  void addEdgeS(Edge edge) {\n    var sourceSet = false;\n    var destinationSet = false;\n    for (var node in _nodes) {\n      if (!sourceSet && node == edge.source) {\n        edge.source = node;\n        sourceSet = true;\n      }\n\n      if (!destinationSet && node == edge.destination) {\n        edge.destination = node;\n        destinationSet = true;\n      }\n\n      if (sourceSet && destinationSet) {\n        break;\n      }\n    }\n    if (!sourceSet) {\n      _nodes.add(edge.source);\n      sourceSet = true;\n      if (!destinationSet && edge.destination == edge.source) {\n        destinationSet = true;\n      }\n    }\n    if (!destinationSet) {\n      _nodes.add(edge.destination);\n      destinationSet = true;\n    }\n\n    if (!_edges.contains(edge)) {\n      _edges.add(edge);\n      _cacheValid = false;\n      notifyGraphObserver();\n    }\n  }\n\n  void addEdges(List<Edge> edges) => edges.forEach((it) => addEdgeS(it));\n\n  void removeEdge(Edge edge) {\n    _edges.remove(edge);\n    _cacheValid = false;\n  }\n\n  void removeEdges(List<Edge> edges) => edges.forEach((it) => removeEdge(it));\n\n  void removeEdgeFromPredecessor(Node? predecessor, Node? current) {\n    _edges.removeWhere(\n        (edge) => edge.source == predecessor && edge.destination == current);\n    _cacheValid = false;\n  }\n\n  bool hasNodes() => _nodes.isNotEmpty;\n\n  Edge? getEdgeBetween(Node source, Node? destination) =>\n      _edges.firstWhereOrNull((element) =>\n          element.source == source && element.destination == destination);\n\n  bool hasSuccessor(Node? node) => successorsOf(node).isNotEmpty;\n\n  List<Node> successorsOf(Node? node) {\n    if (node == null) return [];\n    if (!_cacheValid) _buildCache();\n    return _successorCache[node] ?? [];\n  }\n\n  bool hasPredecessor(Node node) => predecessorsOf(node).isNotEmpty;\n\n  List<Node> predecessorsOf(Node? node) {\n    if (node == null) return [];\n    if (!_cacheValid) _buildCache();\n    return _predecessorCache[node] ?? [];\n  }\n\n  void _buildCache() {\n    _successorCache.clear();\n    _predecessorCache.clear();\n\n    for (var node in _nodes) {\n      _successorCache[node] = [];\n      _predecessorCache[node] = [];\n    }\n\n    for (var edge in _edges) {\n      _successorCache[edge.source]!.add(edge.destination);\n      _predecessorCache[edge.destination]!.add(edge.source);\n    }\n\n    _cacheValid = true;\n  }\n\n  bool contains({Node? node, Edge? edge}) =>\n      node != null && _nodes.contains(node) ||\n      edge != null && _edges.contains(edge);\n\n  bool containsData(data) => _nodes.any((element) => element.data == data);\n\n  Node getNodeAtPosition(int position) {\n    if (position < 0) {\n//            throw IllegalArgumentException(\"position can't be negative\")\n    }\n\n    final size = _nodes.length;\n    if (position >= size) {\n//            throw IndexOutOfBoundsException(\"Position: $position, Size: $size\")\n    }\n\n    return _nodes[position];\n  }\n\n  @Deprecated('Please use the builder and id mechanism to build the widgets')\n  Node getNodeAtUsingData(Widget data) =>\n      _nodes.firstWhere((element) => element.data == data);\n\n  Node getNodeUsingKey(ValueKey key) =>\n      _nodes.firstWhere((element) => element.key == key);\n\n  Node getNodeUsingId(dynamic id) =>\n      _nodes.firstWhere((element) => element.key == ValueKey(id));\n\n  List<Edge> getOutEdges(Node node) =>\n      _edges.where((element) => element.source == node).toList();\n\n  List<Edge> getInEdges(Node node) =>\n      _edges.where((element) => element.destination == node).toList();\n\n  void notifyGraphObserver() => graphObserver.forEach((element) {\n        element.notifyGraphInvalidated();\n      });\n\n  String toJson() {\n    var jsonString = {\n      'nodes': [..._nodes.map((e) => e.hashCode.toString())],\n      'edges': [\n        ..._edges.map((e) => {\n              'from': e.source.hashCode.toString(),\n              'to': e.destination.hashCode.toString()\n            })\n      ]\n    };\n\n    return json.encode(jsonString);\n  }\n\n}\n\nextension GraphExtension on Graph {\n  Rect calculateGraphBounds() {\n    var minX = double.infinity;\n    var minY = double.infinity;\n    var maxX = double.negativeInfinity;\n    var maxY = double.negativeInfinity;\n\n    for (final node in nodes) {\n        minX = min(minX, node.x);\n        minY = min(minY, node.y);\n        maxX = max(maxX, node.x + node.width);\n        maxY = max(maxY, node.y + node.height);\n    }\n\n    return Rect.fromLTRB(minX, minY, maxX, maxY);\n  }\n\n  Size calculateGraphSize() {\n    final bounds = calculateGraphBounds();\n    return bounds.size;\n  }\n}\n\nenum LineType {\n  Default,\n  DottedLine,\n  DashedLine,\n  SineLine,\n}\n\nclass Node {\n  ValueKey? key;\n\n  @Deprecated('Please use the builder and id mechanism to build the widgets')\n  Widget? data;\n\n  @Deprecated('Please use the Node.Id')\n  Node(this.data, {Key? key}) {\n    this.key = ValueKey(key?.hashCode ?? data.hashCode);\n  }\n\n  Node.Id(dynamic id) {\n    key = ValueKey(id);\n  }\n\n  Size size = Size(0, 0);\n\n  Offset position = Offset(0, 0);\n\n  LineType lineType = LineType.Default;\n\n  double get height => size.height;\n\n  double get width => size.width;\n\n  double get x => position.dx;\n\n  double get y => position.dy;\n\n  set y(double value) {\n    position = Offset(position.dx, value);\n  }\n\n  set x(double value) {\n    position = Offset(value, position.dy);\n  }\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) || other is Node && hashCode == other.hashCode;\n\n  @override\n  int get hashCode {\n    return key?.value.hashCode ?? key.hashCode;\n  }\n\n  @override\n  String toString() {\n    return 'Node{position: $position, key: $key, _size: $size, lineType: $lineType}';\n  }\n}\n\nclass Edge {\n  Node source;\n  Node destination;\n\n  Key? key;\n  Paint? paint;\n\n  Edge(this.source, this.destination, {this.key, this.paint});\n\n  @override\n  bool operator ==(Object? other) =>\n      identical(this, other) || other is Edge && hashCode == other.hashCode;\n\n  @override\n  int get hashCode => key?.hashCode ?? Object.hash(source, destination);\n}\n\nabstract class GraphObserver {\n  void notifyGraphInvalidated();\n}\n"
  },
  {
    "path": "lib/GraphView.dart",
    "content": "library graphview;\n\nimport 'dart:async';\nimport 'dart:collection';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:collection/collection.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\n\npart 'Algorithm.dart';\npart 'Graph.dart';\npart 'edgerenderer/ArrowEdgeRenderer.dart';\npart 'edgerenderer/EdgeRenderer.dart';\npart 'forcedirected/FruchtermanReingoldAlgorithm.dart';\npart 'forcedirected/FruchtermanReingoldConfiguration.dart';\npart 'layered/EiglspergerAlgorithm.dart';\npart 'layered/SugiyamaAlgorithm.dart';\npart 'layered/SugiyamaConfiguration.dart';\npart 'layered/SugiyamaEdgeData.dart';\npart 'layered/SugiyamaEdgeRenderer.dart';\npart 'layered/SugiyamaNodeData.dart';\npart 'mindmap/MindMapAlgorithm.dart';\npart 'mindmap/MindmapEdgeRenderer.dart';\npart 'tree/BaloonLayoutAlgorithm.dart';\npart 'tree/BuchheimWalkerAlgorithm.dart';\npart 'tree/BuchheimWalkerConfiguration.dart';\npart 'tree/BuchheimWalkerNodeData.dart';\npart 'tree/CircleLayoutAlgorithm.dart';\npart 'tree/RadialTreeLayoutAlgorithm.dart';\npart 'tree/TidierTreeLayoutAlgorithm.dart';\npart 'tree/TreeEdgeRenderer.dart';\n\ntypedef NodeWidgetBuilder = Widget Function(Node node);\ntypedef EdgeWidgetBuilder = Widget Function(Edge edge);\n\nclass GraphViewController {\n  _GraphViewState? _state;\n  final TransformationController? transformationController;\n\n  final Map<Node, bool> collapsedNodes = {};\n  final Map<Node, bool> expandingNodes = {};\n  final Map<Node, Node> hiddenBy = {};\n\n  Node? collapsedNode;\n  Node? focusedNode;\n\n  GraphViewController({\n    this.transformationController,\n  });\n\n  void _attach(_GraphViewState? state) => _state = state;\n\n  void _detach() => _state = null;\n\n  void animateToNode(ValueKey key) => _state?.jumpToNodeUsingKey(key, true);\n\n  void jumpToNode(ValueKey key) => _state?.jumpToNodeUsingKey(key, false);\n\n  void animateToMatrix(Matrix4 target) => _state?.animateToMatrix(target);\n\n  void resetView() => _state?.resetView();\n\n  void zoomToFit() => _state?.zoomToFit();\n\n  void forceRecalculation() => _state?.forceRecalculation();\n\n  // Visibility management methods\n  bool isNodeCollapsed(Node node) => collapsedNodes.containsKey(node);\n\n  bool isNodeHidden(Node node) => hiddenBy.containsKey(node);\n\n  bool isNodeVisible(Graph graph, Node node) {\n    return !hiddenBy.containsKey(node);\n  }\n\n  Node? findClosestVisibleAncestor(Graph graph, Node node) {\n    var current = graph.predecessorsOf(node).firstOrNull;\n\n    // Walk up until we find a visible ancestor\n    while (current != null) {\n      if (isNodeVisible(graph, current)) {\n        return current; // Return the first (closest) visible ancestor\n      }\n      current = graph.predecessorsOf(current).firstOrNull;\n    }\n\n    return null;\n  }\n\n  void _markDescendantsHiddenBy(\n      Graph graph, Node collapsedNode, Node currentNode) {\n    for (final child in graph.successorsOf(currentNode)) {\n      // Only mark as hidden if:\n      // 1. Not already hidden, OR\n      // 2. Was hidden by a node that's no longer collapsed\n      if (!hiddenBy.containsKey(child) ||\n          !collapsedNodes.containsKey(hiddenBy[child])) {\n        hiddenBy[child] = collapsedNode;\n      }\n\n      // Recurse only if this child isn't itself a collapsed node\n      if (!collapsedNodes.containsKey(child)) {\n        _markDescendantsHiddenBy(graph, collapsedNode, child);\n      }\n    }\n  }\n\n  void _markExpandingDescendants(Graph graph, Node node) {\n    for (final child in graph.successorsOf(node)) {\n        expandingNodes[child] = true;\n        if (!collapsedNodes.containsKey(child)) {\n        _markExpandingDescendants(graph, child);\n      }\n    }\n  }\n\n  void expandNode(Graph graph, Node node, {animate = false}) {\n    collapsedNodes.remove(node);\n    hiddenBy.removeWhere((hiddenNode, hiddenBy) => hiddenBy == node);\n\n    expandingNodes.clear();\n    _markExpandingDescendants(graph, node);\n\n    if (animate) {\n      focusedNode = node;\n    }\n    forceRecalculation();\n  }\n\n  void collapseNode(Graph graph, Node node, {animate = false}) {\n    if (graph.hasSuccessor(node)) {\n      collapsedNodes[node] = true;\n      collapsedNode = node;\n      if (animate) {\n        focusedNode = node;\n      }\n      _markDescendantsHiddenBy(graph, node, node);\n      forceRecalculation();\n    }\n    expandingNodes.clear();\n  }\n\n  void toggleNodeExpanded(Graph graph, Node node, {animate = false}) {\n    if (isNodeCollapsed(node)) {\n      expandNode(graph, node, animate: animate);\n    } else {\n      collapseNode(graph, node, animate: animate);\n    }\n  }\n\n  List<Edge> getCollapsingEdges(Graph graph) {\n    if (collapsedNode == null) return [];\n\n    return graph.edges.where((edge) {\n      return hiddenBy[edge.destination] == collapsedNode;\n    }).toList();\n  }\n\n  List<Edge> getExpandingEdges(Graph graph) {\n    final expandingEdges = <Edge>[];\n\n    for (final node in expandingNodes.keys) {\n      // Get all incoming edges to expanding nodes\n      for (final edge in graph.getInEdges(node)) {\n        expandingEdges.add(edge);\n      }\n    }\n\n    return expandingEdges;\n  }\n\n  // Additional convenience methods for setting initial state\n  void setInitiallyCollapsedNodes(Graph graph, List<Node> nodes) {\n    for (final node in nodes) {\n      collapsedNodes[node] = true;\n      // Mark descendants as hidden by this node\n      _markDescendantsHiddenBy(graph, node, node);\n    }\n  }\n\n  void setInitiallyCollapsedByKeys(Graph graph, Set<ValueKey> keys) {\n    for (final key in keys) {\n      try {\n        final node = graph.getNodeUsingKey(key);\n        collapsedNodes[node] = true;\n        // Mark descendants as hidden by this node\n        _markDescendantsHiddenBy(graph, node, node);\n      } catch (e) {\n        // Node with key not found, ignore\n      }\n    }\n  }\n\n  bool isNodeExpanding(Node node) => expandingNodes.containsKey(node);\n\n  void removeCollapsingNodes() {\n    collapsedNode = null;\n  }\n\n  void jumpToFocusedNode() {\n    if (focusedNode != null) {\n      final nodeCenter = Offset(\n        focusedNode!.position.dx + focusedNode!.width / 2,\n        focusedNode!.position.dy + focusedNode!.height / 2,\n      );\n      _state?.jumpToOffset(nodeCenter, true);\n      focusedNode = null;\n    }\n  }\n}\n\nclass GraphChildDelegate {\n  final Graph graph;\n  final Algorithm algorithm;\n  final NodeWidgetBuilder builder;\n  GraphViewController? controller;\n  final bool centerGraph;\n  Graph? _cachedVisibleGraph;\n  bool _needsRecalculation = true;\n\n  GraphChildDelegate({\n    required this.graph,\n    required this.algorithm,\n    required this.builder,\n    required this.controller,\n    this.centerGraph = false,\n  });\n\n  Graph getVisibleGraph() {\n    if (_cachedVisibleGraph != null && !_needsRecalculation) {\n      return _cachedVisibleGraph!;\n    }\n\n    final visibleGraph = getVisibleGraphOnly();\n\n    final collapsingEdges = controller?.getCollapsingEdges(graph) ?? [];\n    visibleGraph.addEdges(collapsingEdges);\n\n    _cachedVisibleGraph = visibleGraph;\n    _needsRecalculation = false;\n    return visibleGraph;\n  }\n\n  Graph getVisibleGraphOnly() {\n    final visibleGraph = Graph();\n    for (final edge in graph.edges) {\n      if (isNodeVisible(edge.source) && isNodeVisible(edge.destination)) {\n        visibleGraph.addEdgeS(edge);\n      }\n    }\n\n    if (visibleGraph.nodes.isEmpty && graph.nodes.isNotEmpty) {\n      visibleGraph.addNode(graph.nodes.first);\n    }\n    return visibleGraph;\n  }\n\n  Widget? build(Node node) {\n    var child = node.data ?? builder(node);\n    return KeyedSubtree(key: node.key, child: child);\n  }\n\n  bool shouldRebuild(GraphChildDelegate oldDelegate) {\n    final result =\n        graph != oldDelegate.graph || algorithm != oldDelegate.algorithm;\n    if (result) _needsRecalculation = true;\n    return result;\n  }\n\n  Size runAlgorithm() {\n    final visibleGraph = getVisibleGraphOnly();\n\n    if (centerGraph) {\n      // Use large viewport and center the graph\n      var viewPortSize = Size(200000, 200000);\n      var centerX = viewPortSize.width / 2;\n      var centerY = viewPortSize.height / 2;\n      algorithm.run(visibleGraph, centerX, centerY);\n      return viewPortSize;\n    } else {\n      // Use default algorithm behavior\n      return algorithm.run(visibleGraph, 0, 0);\n    }\n  }\n\n  bool isNodeVisible(Node node) {\n    return controller?.isNodeVisible(graph, node) ?? true;\n  }\n\n  Node? findClosestVisibleAncestor(Node node) {\n    return controller?.findClosestVisibleAncestor(graph, node);\n  }\n}\n\nclass GraphView extends StatefulWidget {\n  final Graph graph;\n  final Algorithm algorithm;\n  final Paint? paint;\n  final NodeWidgetBuilder builder;\n  final bool animated;\n  final GraphViewController? controller;\n  final bool _isBuilder;\n\n  Duration? panAnimationDuration;\n  Duration? toggleAnimationDuration;\n  ValueKey? initialNode;\n  bool autoZoomToFit = false;\n  late GraphChildDelegate delegate;\n  final bool centerGraph;\n  final double horizontalBias;\n  final double verticalBias;\n\n  GraphView({\n    Key? key,\n    required this.graph,\n    required this.algorithm,\n    this.paint,\n    required this.builder,\n    this.animated = true,\n    this.controller,\n    this.toggleAnimationDuration,\n    this.centerGraph = false,\n    this.horizontalBias = 0.5,\n    this.verticalBias = 0.5,\n  })  : _isBuilder = false,\n        delegate = GraphChildDelegate(\n            graph: graph,\n            algorithm: algorithm,\n            builder: builder,\n            controller: null),\n        super(key: key);\n\n  GraphView.builder({\n    Key? key,\n    required this.graph,\n    required this.algorithm,\n    this.paint,\n    required this.builder,\n    this.controller,\n    this.animated = true,\n    this.initialNode,\n    this.autoZoomToFit = false,\n    this.panAnimationDuration,\n    this.toggleAnimationDuration,\n    this.centerGraph = false,\n    this.horizontalBias = 0.5,\n    this.verticalBias = 0.5,\n  })  : _isBuilder = true,\n        delegate = GraphChildDelegate(\n            graph: graph,\n            algorithm: algorithm,\n            builder: builder,\n            controller: controller,\n            centerGraph: centerGraph),\n        assert(!(autoZoomToFit && initialNode != null),\n            'Cannot use both autoZoomToFit and initialNode together. Choose one.'),\n        super(key: key);\n\n  @override\n  _GraphViewState createState() => _GraphViewState();\n}\n\nclass _GraphViewState extends State<GraphView> with TickerProviderStateMixin {\n  late TransformationController _transformationController;\n  late final AnimationController _panController;\n  late final AnimationController _nodeController;\n  Animation<Matrix4>? _panAnimation;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _transformationController = widget.controller?.transformationController ??\n        TransformationController();\n\n    _panController = AnimationController(\n      vsync: this,\n      duration:\n          widget.panAnimationDuration ?? const Duration(milliseconds: 600),\n    );\n\n    _nodeController = AnimationController(\n      vsync: this,\n      duration:\n          widget.toggleAnimationDuration ?? const Duration(milliseconds: 600),\n    );\n\n    widget.controller?._attach(this);\n\n    if (widget.autoZoomToFit || widget.initialNode != null) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (widget.autoZoomToFit) {\n          zoomToFit();\n        } else if (widget.initialNode != null) {\n          jumpToNodeUsingKey(widget.initialNode!, false);\n        }\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    widget.controller?._detach();\n    _panController.dispose();\n    _nodeController.dispose();\n    _transformationController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final view = GraphViewWidget(\n      paint: widget.paint,\n      nodeAnimationController: _nodeController,\n      enableAnimation: widget.animated,\n      delegate: widget.delegate,\n    );\n\n    if (widget._isBuilder) {\n      return InteractiveViewer.builder(\n          transformationController: _transformationController,\n          boundaryMargin: EdgeInsets.all(double.infinity),\n          minScale: 0.01,\n          maxScale: 10,\n          builder: (context, viewport) {\n            return view;\n          });\n    }\n\n    return view;\n  }\n\n  void jumpToNodeUsingKey(ValueKey key, bool animated) {\n    final node = widget.graph.nodes.firstWhereOrNull((n) => n.key == key);\n    if (node == null) return;\n\n    jumpToNode(node, animated);\n  }\n\n  void jumpToNode(Node node, bool animated) {\n    final nodeCenter = Offset(\n        node.position.dx + node.width / 2, node.position.dy + node.height / 2);\n\n    jumpToOffset(nodeCenter, animated);\n  }\n\n  void jumpToOffset(Offset offset, bool animated) {\n    final renderBox = context.findRenderObject() as RenderBox?;\n    if (renderBox == null) return;\n\n    final viewport = renderBox.size;\n    final center = Offset(\n        viewport.width * widget.horizontalBias,\n        viewport.height * widget.verticalBias);\n\n    final currentScale = _transformationController.value.getMaxScaleOnAxis();\n\n    final scaledNodeCenter = offset * currentScale;\n    final translation = center - scaledNodeCenter;\n\n    final target = Matrix4.identity()\n      ..translate(translation.dx, translation.dy)\n      ..scale(currentScale);\n\n    if (animated) {\n      animateToMatrix(target);\n    } else {\n      _transformationController.value = target;\n    }\n  }\n\n  void resetView() => animateToMatrix(Matrix4.identity());\n\n  void zoomToFit() {\n    var graph = widget.delegate.getVisibleGraphOnly();\n    final renderBox = context.findRenderObject() as RenderBox?;\n    if (renderBox == null) return;\n\n    final vp = renderBox.size;\n    final bounds = graph.calculateGraphBounds();\n\n    const paddingFactor = 0.95;\n    final scaleX = (vp.width / bounds.width) * paddingFactor;\n    final scaleY = (vp.height / bounds.height) * paddingFactor;\n    final scale = min(scaleX, scaleY);\n\n    final scaledWidth = bounds.width * scale;\n    final scaledHeight = bounds.height * scale;\n\n    final centerOffset = Offset(\n        (vp.width - scaledWidth) * widget.horizontalBias - bounds.left * scale,\n        (vp.height - scaledHeight) * widget.verticalBias - bounds.top * scale);\n\n    final target = Matrix4.identity()\n      ..translate(centerOffset.dx, centerOffset.dy)\n      ..scale(scale);\n    animateToMatrix(target);\n  }\n\n  void animateToMatrix(Matrix4 target) {\n    _panController.reset();\n    _panAnimation = Matrix4Tween(\n            begin: _transformationController.value, end: target)\n        .animate(\n            CurvedAnimation(parent: _panController, curve: Curves.linear));\n    _panAnimation!.addListener(_onPanTick);\n    _panController.forward();\n  }\n\n  void _onPanTick() {\n    if (_panAnimation == null) return;\n    _transformationController.value = _panAnimation!.value;\n    if (!_panController.isAnimating) {\n      _panAnimation!.removeListener(_onPanTick);\n      _panAnimation = null;\n      _panController.reset();\n    }\n  }\n\n  void forceRecalculation() {\n    // Invalidate the delegate's cached graph\n    widget.delegate._needsRecalculation = true;\n\n    setState(() {});\n  }\n}\n\nabstract class GraphChildManager {\n  void startLayout();\n\n  void buildChild(Node node);\n\n  void reuseChild(Node node);\n\n  void endLayout();\n}\n\nclass GraphViewWidget extends RenderObjectWidget {\n  final GraphChildDelegate delegate;\n  final Paint? paint;\n  final AnimationController nodeAnimationController;\n  final bool enableAnimation;\n\n  const GraphViewWidget({\n    Key? key,\n    required this.delegate,\n    this.paint,\n    required this.nodeAnimationController,\n    required this.enableAnimation,\n  }) : super(key: key);\n\n  @override\n  GraphViewElement createElement() => GraphViewElement(this);\n\n  @override\n  RenderCustomLayoutBox createRenderObject(BuildContext context) {\n    return RenderCustomLayoutBox(\n      delegate,\n      paint,\n      enableAnimation,\n      nodeAnimationController: nodeAnimationController,\n      childManager: context as GraphChildManager,\n    );\n  }\n\n  @override\n  void updateRenderObject(\n      BuildContext context, RenderCustomLayoutBox renderObject) {\n    renderObject\n      ..delegate = delegate\n      ..edgePaint = paint\n      ..nodeAnimationController = nodeAnimationController\n      ..enableAnimation = enableAnimation;\n  }\n}\n\nclass GraphViewElement extends RenderObjectElement\n    implements GraphChildManager {\n  GraphViewElement(GraphViewWidget super.widget);\n\n  @override\n  GraphViewWidget get widget => super.widget as GraphViewWidget;\n\n  @override\n  RenderCustomLayoutBox get renderObject =>\n      super.renderObject as RenderCustomLayoutBox;\n\n  // Contains all children, including those that are keyed\n  Map<Node, Element> _nodeToElement = <Node, Element>{};\n  Map<Key, Element> _keyToElement = <Key, Element>{};\n\n  // Used between startLayout() & endLayout() to compute the new values\n  Map<Node, Element>? _newNodeToElement;\n  Map<Key, Element>? _newKeyToElement;\n\n  bool get _debugIsDoingLayout =>\n      _newNodeToElement != null && _newKeyToElement != null;\n\n  @override\n  void performRebuild() {\n    super.performRebuild();\n    // Children list is updated during layout since we only know during layout\n    // which children will be visible\n    renderObject.markNeedsLayout();\n  }\n\n  @override\n  void forgetChild(Element child) {\n    assert(!_debugIsDoingLayout);\n    super.forgetChild(child);\n    _nodeToElement.remove(child.slot as Node);\n    if (child.widget.key != null) {\n      _keyToElement.remove(child.widget.key);\n    }\n  }\n\n  @override\n  void insertRenderObjectChild(RenderBox child, Node slot) {\n    renderObject._insertChild(child, slot);\n  }\n\n  @override\n  void moveRenderObjectChild(RenderBox child, Node oldSlot, Node newSlot) {\n    renderObject._moveChild(child, from: oldSlot, to: newSlot);\n  }\n\n  @override\n  void removeRenderObjectChild(RenderBox child, Node slot) {\n    renderObject._removeChild(child, slot);\n  }\n\n  @override\n  void visitChildren(ElementVisitor visitor) {\n    _nodeToElement.values.forEach(visitor);\n  }\n\n  // ---- GraphChildManager implementation ----\n\n  @override\n  void startLayout() {\n    assert(!_debugIsDoingLayout);\n    _newNodeToElement = <Node, Element>{};\n    _newKeyToElement = <Key, Element>{};\n  }\n\n  @override\n  void buildChild(Node node) {\n    assert(_debugIsDoingLayout);\n    owner!.buildScope(this, () {\n      final newWidget = widget.delegate.build(node);\n      if (newWidget == null) {\n        return;\n      }\n\n      final oldElement = _retrieveOldElement(newWidget, node);\n      final newChild = updateChild(oldElement, newWidget, node);\n\n      if (newChild != null) {\n        // Ensure we are not overwriting an existing child\n        assert(_newNodeToElement![node] == null);\n        _newNodeToElement![node] = newChild;\n        if (newWidget.key != null) {\n          // Ensure we are not overwriting an existing key\n          assert(_newKeyToElement![newWidget.key!] == null);\n          _newKeyToElement![newWidget.key!] = newChild;\n        }\n      }\n    });\n  }\n\n  @override\n  void reuseChild(Node node) {\n    assert(_debugIsDoingLayout);\n    final elementToReuse = _nodeToElement.remove(node);\n    assert(\n      elementToReuse != null,\n      'Expected to re-use an element at $node, but none was found.',\n    );\n    _newNodeToElement![node] = elementToReuse!;\n    if (elementToReuse.widget.key != null) {\n      assert(_keyToElement.containsKey(elementToReuse.widget.key));\n      assert(_keyToElement[elementToReuse.widget.key] == elementToReuse);\n      _newKeyToElement![elementToReuse.widget.key!] =\n          _keyToElement.remove(elementToReuse.widget.key)!;\n    }\n  }\n\n  Element? _retrieveOldElement(Widget newWidget, Node node) {\n    if (newWidget.key != null) {\n      final result = _keyToElement.remove(newWidget.key);\n      if (result != null) {\n        _nodeToElement.remove(result.slot as Node);\n      }\n      return result;\n    }\n\n    final potentialOldElement = _nodeToElement[node];\n    if (potentialOldElement != null && potentialOldElement.widget.key == null) {\n      return _nodeToElement.remove(node);\n    }\n    return null;\n  }\n\n  @override\n  void endLayout() {\n    assert(_debugIsDoingLayout);\n\n    // Unmount all elements that have not been reused in the layout cycle\n    for (final element in _nodeToElement.values) {\n      if (element.widget.key == null) {\n        // If it has a key, we handle it below\n        updateChild(element, null, null);\n      } else {\n        assert(_keyToElement.containsValue(element));\n      }\n    }\n    for (final element in _keyToElement.values) {\n      assert(element.widget.key != null);\n      updateChild(element, null, null);\n    }\n\n    _nodeToElement = _newNodeToElement!;\n    _keyToElement = _newKeyToElement!;\n    _newNodeToElement = null;\n    _newKeyToElement = null;\n    assert(!_debugIsDoingLayout);\n\n    centerNodeWhileToggling();\n  }\n\n  void centerNodeWhileToggling() {\n    widget.delegate.controller?.jumpToFocusedNode();\n  }\n}\n\nclass RenderCustomLayoutBox extends RenderBox\n    with\n        ContainerRenderObjectMixin<RenderBox, NodeBoxData>,\n        RenderBoxContainerDefaultsMixin<RenderBox, NodeBoxData> {\n  late Paint _paint;\n  late AnimationController _nodeAnimationController;\n  late GraphChildDelegate _delegate;\n  GraphChildManager? childManager;\n\n  Size? _cachedSize;\n  bool _isInitialized = false;\n  bool _needsFullRecalculation = false;\n  late bool enableAnimation;\n  final opacityPaint = Paint();\n\n  final animatedPositions = <Node, Offset>{};\n  final _children = <Node, RenderBox>{};\n  final _activeChildrenForLayoutPass = <Node, RenderBox>{};\n\n  RenderCustomLayoutBox(\n    GraphChildDelegate delegate,\n    Paint? paint,\n    bool enableAnimation, {\n    required AnimationController nodeAnimationController,\n    this.childManager,\n  }) {\n    _nodeAnimationController = nodeAnimationController;\n    _delegate = delegate;\n    edgePaint = paint;\n    this.enableAnimation = enableAnimation;\n  }\n\n  RenderBox? buildOrObtainChildFor(Node node) {\n    assert(debugDoingThisLayout);\n\n    if (_needsFullRecalculation || !_children.containsKey(node)) {\n      invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {\n        childManager!.buildChild(node);\n      });\n    } else {\n      childManager!.reuseChild(node);\n    }\n\n    if (!_children.containsKey(node)) {\n      // There is no child for this node, the delegate may not provide one\n      return null;\n    }\n\n    assert(_children.containsKey(node));\n    final child = _children[node]!;\n    _activeChildrenForLayoutPass[node] = child;\n    return child;\n  }\n\n  GraphChildDelegate get delegate => _delegate;\n\n  Graph get graph => _delegate.getVisibleGraph();\n\n  Algorithm get algorithm => _delegate.algorithm;\n\n  set delegate(GraphChildDelegate value) {\n    // if (value != _delegate) {\n    _needsFullRecalculation = true;\n    _isInitialized = false;\n    _delegate = value;\n    markNeedsLayout();\n    // }\n  }\n\n  void markNeedsRecalculation() {\n    _needsFullRecalculation = false;\n    _isInitialized = false;\n    markNeedsLayout();\n  }\n\n  @override\n  void attach(PipelineOwner owner) {\n    super.attach(owner);\n    _nodeAnimationController.addListener(_onAnimationTick);\n    for (final child in _children.values) {\n      child.attach(owner);\n    }\n  }\n\n  @override\n  void detach() {\n    _nodeAnimationController.removeListener(_onAnimationTick);\n    super.detach();\n    for (final child in _children.values) {\n      child.detach();\n    }\n  }\n\n  void forceRecalculation() {\n    _needsFullRecalculation = true;\n    _isInitialized = false;\n    markNeedsLayout();\n  }\n\n  Paint get edgePaint => _paint;\n\n  set edgePaint(Paint? value) {\n    final newPaint = value ??\n        (Paint()\n          ..color = Colors.black\n          ..strokeWidth = 3)\n      ..style = PaintingStyle.stroke\n      ..strokeCap = StrokeCap.butt;\n\n    _paint = newPaint;\n    markNeedsPaint();\n  }\n\n  AnimationController get nodeAnimationController => _nodeAnimationController;\n\n  set nodeAnimationController(AnimationController value) {\n    if (identical(_nodeAnimationController, value)) return;\n    _nodeAnimationController.removeListener(_onAnimationTick);\n    _nodeAnimationController = value;\n    _nodeAnimationController.addListener(_onAnimationTick);\n    markNeedsLayout();\n  }\n\n  void _onAnimationTick() {\n    markNeedsPaint();\n  }\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    if (_children.isEmpty) return;\n\n    if (enableAnimation) {\n      final t = _nodeAnimationController.value;\n      animatedPositions.clear();\n\n      for (final entry in _children.entries) {\n        final node = entry.key;\n        final child = entry.value;\n        final nodeData = child.parentData as NodeBoxData;\n        final pos =\n            Offset.lerp(nodeData.startOffset, nodeData.targetOffset, t)!;\n        animatedPositions[node] = pos;\n      }\n\n      context.canvas.save();\n      context.canvas.translate(offset.dx, offset.dy);\n      algorithm.renderer?.setAnimatedPositions(animatedPositions);\n\n      final collapsingEdges =\n          _delegate.controller?.getCollapsingEdges(graph).toSet() ?? {};\n      final expandingEdges =\n          _delegate.controller?.getExpandingEdges(graph).toSet() ?? {};\n\n     for (final edge in graph.edges) {\n        var edgePaintWithOpacity = Paint.from(edge.paint ?? edgePaint);\n\n        // Apply fade effect for collapsing edges (fade out)\n        if (collapsingEdges.contains(edge)) {\n          edgePaintWithOpacity.color =\n              edgePaint.color.withValues(alpha: 1.0 - t);\n        }\n        // Apply fade effect for expanding edges (fade in)\n        else if (expandingEdges.contains(edge)) {\n          edgePaintWithOpacity.color = edgePaint.color.withValues(alpha: t);\n        }\n\n        algorithm.renderer?.renderEdge(\n          context.canvas,\n          edge,\n          edgePaintWithOpacity,\n        );\n      }\n\n      context.canvas.restore();\n\n      _paintNodes(context, offset, t);\n    } else {\n      context.canvas.save();\n      context.canvas.translate(offset.dx, offset.dy);\n      graph.edges.forEach((edge) {\n        algorithm.renderer?.renderEdge(context.canvas, edge, edgePaint);\n      });\n      context.canvas.restore();\n\n      for (final entry in _children.entries) {\n        final node = entry.key;\n        final child = entry.value;\n\n        if (_delegate.isNodeVisible(node)) {\n          context.paintChild(child, offset + node.position);\n        }\n      }\n    }\n  }\n\n  @override\n  void performLayout() {\n    _activeChildrenForLayoutPass.clear();\n    childManager!.startLayout();\n\n    final looseConstraints = BoxConstraints.loose(constraints.biggest);\n\n    if (_needsFullRecalculation || !_isInitialized) {\n      _layoutNodesLazily(looseConstraints);\n      _cachedSize = _delegate.runAlgorithm();\n      _isInitialized = true;\n      _needsFullRecalculation = false;\n    }\n\n    size = _cachedSize ?? Size.zero;\n\n    invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {\n      childManager!.endLayout();\n    });\n\n    if (enableAnimation) {\n      _updateAnimationStates();\n    } else {\n      _updateNodePositions();\n    }\n  }\n\n  void _paintNodes(PaintingContext context, Offset offset, double t) {\n    for (final entry in _children.entries) {\n      final node = entry.key;\n      final child = entry.value;\n      final nodeData = child.parentData as NodeBoxData;\n      final pos = animatedPositions[node]!;\n\n      final isVisible = _delegate.isNodeVisible(node);\n      if (isVisible) {\n        final isExpanding =\n            _delegate.controller?.isNodeExpanding(node) ?? false;\n        if (_nodeAnimationController.isAnimating && isExpanding) {\n          _paintExpandingNode(context, child, offset, pos, t);\n        } else {\n          context.paintChild(child, offset + pos);\n        }\n      } else {\n        if (_nodeAnimationController.isAnimating &&\n            nodeData.startOffset != nodeData.targetOffset) {\n          _paintCollapsingNode(context, child, offset, pos, t);\n        } else if (_nodeAnimationController.isCompleted) {\n          nodeData.startOffset = nodeData.targetOffset;\n        }\n      }\n\n      if (_nodeAnimationController.isCompleted) {\n        nodeData.offset = node.position;\n      }\n    }\n\n    if (_nodeAnimationController.isCompleted) {\n      _delegate.controller?.removeCollapsingNodes();\n    }\n  }\n\n  void _paintExpandingNode(PaintingContext context, RenderBox child,\n      Offset offset, Offset pos, double t) {\n    final center =\n        pos + offset + Offset(child.size.width * 0.5, child.size.height * 0.5);\n\n    context.canvas.save();\n\n    // Apply scaling from center\n    context.canvas.translate(center.dx, center.dy);\n    context.canvas.scale(t, t);\n    context.canvas.translate(-center.dx, -center.dy);\n\n    // Paint with opacity using saveLayer\n    opacityPaint\n      ..color = Color.fromRGBO(255, 255, 255, t)\n      ..colorFilter = ColorFilter.mode(\n          Colors.white.withValues(alpha: t), BlendMode.modulate);\n\n    context.canvas.saveLayer(\n        Rect.fromLTWH(pos.dx + offset.dx - 20, pos.dy + offset.dy - 20,\n            child.size.width + 40, child.size.height + 40),\n        opacityPaint);\n\n    context.paintChild(child, offset + pos);\n\n    context.canvas.restore(); // Restore saveLayer\n    context.canvas.restore(); // Restore main save\n  }\n\n  void _paintCollapsingNode(PaintingContext context, RenderBox child,\n      Offset offset, Offset pos, double t) {\n    final progress = (1.0 - t);\n    final center =\n        pos + offset + Offset(child.size.width * 0.5, child.size.height * 0.5);\n\n    context.canvas.save();\n\n    // Apply scaling from center\n    context.canvas.translate(center.dx, center.dy);\n    context.canvas.scale(progress, progress);\n    context.canvas.translate(-center.dx, -center.dy);\n\n    // Paint with opacity using saveLayer\n    opacityPaint\n      ..color = Color.fromRGBO(255, 255, 255, progress)\n      ..colorFilter = ColorFilter.mode(\n          Colors.white.withValues(alpha: progress), BlendMode.modulate);\n\n    context.canvas.saveLayer(\n        Rect.fromLTWH(pos.dx + offset.dx - 20, pos.dy + offset.dy - 20,\n            child.size.width + 40, child.size.height + 40),\n        opacityPaint);\n\n    context.paintChild(child, offset + pos);\n\n    context.canvas.restore(); // Restore saveLayer\n    context.canvas.restore(); // Restore main save\n  }\n\n  void _updateNodePositions() {\n    for (final entry in _children.entries) {\n      final node = entry.key;\n      final child = entry.value;\n      final nodeData = child.parentData as NodeBoxData;\n\n      if (_delegate.isNodeVisible(node)) {\n        nodeData.offset = node.position;\n      } else {\n        final parent = delegate.findClosestVisibleAncestor(node);\n        nodeData.offset = parent?.position ?? node.position;\n      }\n    }\n  }\n\n  void _layoutNodesLazily(BoxConstraints constraints) {\n    for (final node in graph.nodes) {\n      final child = buildOrObtainChildFor(node);\n      if (child != null) {\n        child.layout(constraints, parentUsesSize: true);\n        node.size = Size(child.size.width.ceilToDouble(), child.size.height);\n      }\n    }\n  }\n\n  void _updateAnimationStates() {\n    for (final entry in _children.entries) {\n      final node = entry.key;\n      final child = entry.value;\n      final nodeData = child.parentData as NodeBoxData;\n      final isVisible = _delegate.isNodeVisible(node);\n\n      if (isVisible) {\n        _updateVisibleNodeAnimation(nodeData, node);\n      } else {\n        _updateCollapsedNodeAnimation(nodeData, node);\n      }\n    }\n\n    _nodeAnimationController.reset();\n    _nodeAnimationController.forward();\n  }\n\n  void _updateVisibleNodeAnimation(NodeBoxData nodeData, Node graphNode) {\n    final prevTarget = nodeData.targetOffset;\n    var newPos = graphNode.position;\n\n    if (prevTarget == null) {\n      final parent = graph.predecessorsOf(graphNode).firstOrNull;\n      final pastParentPosition = animatedPositions[parent];\n      nodeData.startOffset = pastParentPosition ?? parent?.position ?? newPos;\n      nodeData.targetOffset = newPos;\n    } else if (prevTarget != newPos) {\n      nodeData.startOffset = prevTarget;\n      nodeData.targetOffset = newPos;\n    } else {\n      nodeData.startOffset = newPos;\n      nodeData.targetOffset = newPos;\n    }\n  }\n\n  void _updateCollapsedNodeAnimation(NodeBoxData nodeData, Node graphNode) {\n    final parent = delegate.findClosestVisibleAncestor(graphNode);\n    final parentPos = parent?.position ?? Offset.zero;\n\n    final prevTarget = nodeData.targetOffset;\n\n    if (nodeData.startOffset == nodeData.targetOffset) {\n      nodeData.targetOffset = parentPos;\n    } else if (prevTarget != null && prevTarget != parentPos) {\n      // Just collapsed now → animate toward parent\n      nodeData.startOffset = graphNode.position;\n      nodeData.targetOffset = parentPos;\n    } else {\n      // animation finished → lock to parent\n      nodeData.startOffset = parentPos;\n      nodeData.targetOffset = parentPos;\n    }\n  }\n\n  @override\n  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {\n    if (enableAnimation && !_nodeAnimationController.isCompleted) return false;\n\n    for (final entry in _children.entries) {\n      final node = entry.key;\n\n      if (delegate.isNodeVisible(node)) {\n        final child = entry.value;\n\n        final childParentData = child.parentData as BoxParentData;\n        final isHit = result.addWithPaintOffset(\n          offset: childParentData.offset,\n          position: position,\n          hitTest: (BoxHitTestResult result, Offset transformed) {\n            return child.hitTest(result, position: transformed);\n          },\n        );\n        if (isHit) return true;\n      }\n    }\n    return false;\n  }\n\n  @override\n  void setupParentData(RenderBox child) {\n    if (child.parentData is! NodeBoxData) {\n      child.parentData = NodeBoxData();\n    }\n  }\n\n  // ---- Called from GraphViewElement ----\n  void _insertChild(RenderBox child, Node slot) {\n    _children[slot] = child;\n    adoptChild(child);\n  }\n\n  void _moveChild(RenderBox child, {required Node from, required Node to}) {\n    if (_children[from] == child) {\n      _children.remove(from);\n    }\n    _children[to] = child;\n  }\n\n  void _removeChild(RenderBox child, Node slot) {\n    if (_children[slot] == child) {\n      _children.remove(slot);\n    }\n    dropChild(child);\n  }\n\n  @override\n  void visitChildren(RenderObjectVisitor visitor) {\n    for (final child in _children.values) {\n      visitor(child);\n    }\n  }\n\n  @override\n  void debugFillProperties(DiagnosticPropertiesBuilder properties) {\n    super.debugFillProperties(properties);\n    properties.add(DiagnosticsProperty<Graph>('graph', graph));\n    properties.add(DiagnosticsProperty<Algorithm>('algorithm', algorithm));\n    properties.add(DiagnosticsProperty<Paint>('paint', edgePaint));\n  }\n}\n\nclass NodeBoxData extends ContainerBoxParentData<RenderBox> {\n  Offset? startOffset;\n  Offset? targetOffset;\n}\n\nclass GraphViewCustomPainter extends StatefulWidget {\n  final Graph graph;\n  final FruchtermanReingoldAlgorithm algorithm;\n  final Paint? paint;\n  final NodeWidgetBuilder builder;\n  final stepMilis = 25;\n\n  GraphViewCustomPainter({\n    Key? key,\n    required this.graph,\n    required this.algorithm,\n    this.paint,\n    required this.builder,\n  }) : super(key: key);\n\n  @override\n  _GraphViewCustomPainterState createState() => _GraphViewCustomPainterState();\n}\n\nclass _GraphViewCustomPainterState extends State<GraphViewCustomPainter> {\n  late Timer timer;\n  late Graph graph;\n  late FruchtermanReingoldAlgorithm algorithm;\n\n  @override\n  void initState() {\n    graph = widget.graph;\n\n    algorithm = widget.algorithm;\n    algorithm.init(graph);\n    startTimer();\n\n    super.initState();\n  }\n\n  void startTimer() {\n    timer = Timer.periodic(Duration(milliseconds: widget.stepMilis), (timer) {\n      algorithm.step(graph);\n      update();\n    });\n  }\n\n  @override\n  void dispose() {\n    timer.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    algorithm.setDimensions(\n        MediaQuery.of(context).size.width, MediaQuery.of(context).size.height);\n\n    return Stack(\n      clipBehavior: Clip.none,\n      children: [\n        CustomPaint(\n          size: MediaQuery.of(context).size,\n          painter: EdgeRender(algorithm, graph, Offset(20, 20), widget.paint),\n        ),\n        ...List<Widget>.generate(graph.nodeCount(), (index) {\n          return Positioned(\n            child: GestureDetector(\n              child:\n                  graph.nodes[index].data ?? widget.builder(graph.nodes[index]),\n              onPanUpdate: (details) {\n                graph.getNodeAtPosition(index).position += details.delta;\n                update();\n              },\n            ),\n            top: graph.getNodeAtPosition(index).position.dy,\n            left: graph.getNodeAtPosition(index).position.dx,\n          );\n        }),\n      ],\n    );\n  }\n\n  Future<void> update() async {\n    setState(() {});\n  }\n}\n\nclass EdgeRender extends CustomPainter {\n  Algorithm algorithm;\n  Graph graph;\n  Offset offset;\n  Paint? customPaint;\n\n  EdgeRender(this.algorithm, this.graph, this.offset, this.customPaint);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    var edgePaint = customPaint ??\n        (Paint()\n          ..color = Colors.black\n          ..strokeWidth = 3\n          ..style = PaintingStyle.stroke\n          ..strokeCap = StrokeCap.butt);\n\n    canvas.save();\n    canvas.translate(offset.dx, offset.dy);\n\n    for (var value in graph.edges) {\n      algorithm.renderer?.renderEdge(canvas, value, edgePaint);\n    }\n    canvas.restore();\n  }\n\n  @override\n  bool shouldRepaint(CustomPainter oldDelegate) {\n    return true;\n  }\n}\n"
  },
  {
    "path": "lib/edgerenderer/ArrowEdgeRenderer.dart",
    "content": "part of graphview;\n\nconst double ARROW_DEGREES = 0.5;\nconst double ARROW_LENGTH = 10;\n\nclass ArrowEdgeRenderer extends EdgeRenderer {\n  var trianglePath = Path();\n  final bool noArrow;\n\n  ArrowEdgeRenderer({this.noArrow = false});\n\n  Offset _getNodeCenter(Node node) {\n    final nodePosition = getNodePosition(node);\n    return Offset(\n      nodePosition.dx + node.width * 0.5,\n      nodePosition.dy + node.height * 0.5,\n    );\n  }\n\n  void render(Canvas canvas, Graph graph, Paint paint) {\n    graph.edges.forEach((edge) {\n      renderEdge(canvas, edge, paint);\n    });\n  }\n\n  @override\n  void renderEdge(Canvas canvas, Edge edge, Paint paint) {\n    var source = edge.source;\n    var destination = edge.destination;\n\n    final currentPaint = (edge.paint ?? paint)..style = PaintingStyle.stroke;\n    final lineType = _getLineType(destination);\n\n    if (source == destination) {\n      final loopResult = buildSelfLoopPath(\n        edge,\n        arrowLength: noArrow ? 0.0 : ARROW_LENGTH,\n      );\n\n      if (loopResult != null) {\n        drawStyledPath(canvas, loopResult.path, currentPaint, lineType: lineType);\n\n        if (!noArrow) {\n          final trianglePaint = Paint()\n            ..color = edge.paint?.color ?? paint.color\n            ..style = PaintingStyle.fill;\n          final triangleCentroid = drawTriangle(\n            canvas,\n            trianglePaint,\n            loopResult.arrowBase.dx,\n            loopResult.arrowBase.dy,\n            loopResult.arrowTip.dx,\n            loopResult.arrowTip.dy,\n          );\n\n          drawStyledLine(\n            canvas,\n            loopResult.arrowBase,\n            triangleCentroid,\n            currentPaint,\n            lineType: lineType,\n          );\n        }\n\n        return;\n      }\n    }\n\n    var sourceOffset = getNodePosition(source);\n    var destinationOffset = getNodePosition(destination);\n\n    var startX = sourceOffset.dx + source.width * 0.5;\n    var startY = sourceOffset.dy + source.height * 0.5;\n    var stopX = destinationOffset.dx + destination.width * 0.5;\n    var stopY = destinationOffset.dy + destination.height * 0.5;\n\n    var clippedLine = clipLineEnd(\n        startX,\n        startY,\n        stopX,\n        stopY,\n        destinationOffset.dx,\n        destinationOffset.dy,\n        destination.width,\n        destination.height);\n\n    if (noArrow) {\n      // Draw line without arrow, respecting line type\n      drawStyledLine(\n        canvas,\n        Offset(clippedLine[0], clippedLine[1]),\n        Offset(clippedLine[2], clippedLine[3]),\n        currentPaint,\n        lineType: lineType,\n      );\n    } else {\n      var trianglePaint = Paint()\n        ..color = paint.color\n        ..style = PaintingStyle.fill;\n\n      // Draw line with arrow\n      Paint? edgeTrianglePaint;\n      if (edge.paint != null) {\n        edgeTrianglePaint = Paint()\n          ..color = edge.paint?.color ?? paint.color\n          ..style = PaintingStyle.fill;\n      }\n\n      var triangleCentroid = drawTriangle(\n          canvas,\n          edgeTrianglePaint ?? trianglePaint,\n          clippedLine[0],\n          clippedLine[1],\n          clippedLine[2],\n          clippedLine[3]);\n\n      // Draw the line with the appropriate style\n      drawStyledLine(\n        canvas,\n        Offset(clippedLine[0], clippedLine[1]),\n        triangleCentroid,\n        currentPaint,\n        lineType: lineType,\n      );\n    }\n  }\n\n  /// Helper to get line type from node data if available\n  LineType? _getLineType(Node node) {\n    // This assumes you have a way to access node data\n    // You may need to adjust this based on your actual implementation\n    if (node is SugiyamaNodeData) {\n      return node.lineType;\n    }\n    return null;\n  }\n\n  Offset drawTriangle(Canvas canvas, Paint paint, double lineStartX,\n      double lineStartY, double arrowTipX, double arrowTipY) {\n    // Calculate direction from line start to arrow tip, then flip 180° to point backwards from tip\n    var lineDirection =\n    (atan2(arrowTipY - lineStartY, arrowTipX - lineStartX) + pi);\n\n    // Calculate the two base points of the arrowhead triangle\n    var leftWingX =\n    (arrowTipX + ARROW_LENGTH * cos((lineDirection - ARROW_DEGREES)));\n    var leftWingY =\n    (arrowTipY + ARROW_LENGTH * sin((lineDirection - ARROW_DEGREES)));\n    var rightWingX =\n    (arrowTipX + ARROW_LENGTH * cos((lineDirection + ARROW_DEGREES)));\n    var rightWingY =\n    (arrowTipY + ARROW_LENGTH * sin((lineDirection + ARROW_DEGREES)));\n\n    // Draw the triangle: tip -> left wing -> right wing -> back to tip\n    trianglePath.moveTo(arrowTipX, arrowTipY); // Arrow tip\n    trianglePath.lineTo(leftWingX, leftWingY); // Left wing\n    trianglePath.lineTo(rightWingX, rightWingY); // Right wing\n    trianglePath.close(); // Back to tip\n    canvas.drawPath(trianglePath, paint);\n\n    // Calculate center point of the triangle\n    var triangleCenterX = (arrowTipX + leftWingX + rightWingX) / 3;\n    var triangleCenterY = (arrowTipY + leftWingY + rightWingY) / 3;\n\n    trianglePath.reset();\n    return Offset(triangleCenterX, triangleCenterY);\n  }\n\n  List<double> clipLineEnd(\n      double startX,\n      double startY,\n      double stopX,\n      double stopY,\n      double destX,\n      double destY,\n      double destWidth,\n      double destHeight) {\n    var clippedStopX = stopX;\n    var clippedStopY = stopY;\n\n    if (startX == stopX && startY == stopY) {\n      return [startX, startY, clippedStopX, clippedStopY];\n    }\n\n    var slope = (startY - stopY) / (startX - stopX);\n    final halfHeight = destHeight * 0.5;\n    final halfWidth = destWidth * 0.5;\n\n    // Check vertical edge intersections\n    if (startX != stopX) {\n      final halfSlopeWidth = slope * halfWidth;\n      if (halfSlopeWidth.abs() <= halfHeight) {\n        if (destX > startX) {\n          // Left edge intersection\n          return [startX, startY, stopX - halfWidth, stopY - halfSlopeWidth];\n        } else if (destX < startX) {\n          // Right edge intersection\n          return [startX, startY, stopX + halfWidth, stopY + halfSlopeWidth];\n        }\n      }\n    }\n\n    // Check horizontal edge intersections\n    if (startY != stopY && slope != 0) {\n      final halfSlopeHeight = halfHeight / slope;\n      if (halfSlopeHeight.abs() <= halfWidth) {\n        if (destY < startY) {\n          // Bottom edge intersection\n          clippedStopX = stopX + halfSlopeHeight;\n          clippedStopY = stopY + halfHeight;\n        } else if (destY > startY) {\n          // Top edge intersection\n          clippedStopX = stopX - halfSlopeHeight;\n          clippedStopY = stopY - halfHeight;\n        }\n      }\n    }\n\n    return [startX, startY, clippedStopX, clippedStopY];\n  }\n\n  List<double> clipLine(double startX, double startY, double stopX,\n      double stopY, Node destination) {\n    final resultLine = [startX, startY, stopX, stopY];\n\n    if (startX == stopX && startY == stopY) return resultLine;\n\n    var slope = (startY - stopY) / (startX - stopX);\n    final halfHeight = destination.height * 0.5;\n    final halfWidth = destination.width * 0.5;\n\n    // Check vertical edge intersections\n    if (startX != stopX) {\n      final halfSlopeWidth = slope * halfWidth;\n      if (halfSlopeWidth.abs() <= halfHeight) {\n        if (destination.x > startX) {\n          // Left edge intersection\n          resultLine[2] = stopX - halfWidth;\n          resultLine[3] = stopY - halfSlopeWidth;\n          return resultLine;\n        } else if (destination.x < startX) {\n          // Right edge intersection\n          resultLine[2] = stopX + halfWidth;\n          resultLine[3] = stopY + halfSlopeWidth;\n          return resultLine;\n        }\n      }\n    }\n\n    // Check horizontal edge intersections\n    if (startY != stopY && slope != 0) {\n      final halfSlopeHeight = halfHeight / slope;\n      if (halfSlopeHeight.abs() <= halfWidth) {\n        if (destination.y < startY) {\n          // Bottom edge intersection\n          resultLine[2] = stopX + halfSlopeHeight;\n          resultLine[3] = stopY + halfHeight;\n        } else if (destination.y > startY) {\n          // Top edge intersection\n          resultLine[2] = stopX - halfSlopeHeight;\n          resultLine[3] = stopY - halfHeight;\n        }\n      }\n    }\n\n    return resultLine;\n  }\n}\n"
  },
  {
    "path": "lib/edgerenderer/EdgeRenderer.dart",
    "content": "part of graphview;\n\nabstract class EdgeRenderer {\n  Map<Node, Offset>? _animatedPositions;\n\n  void setAnimatedPositions(Map<Node, Offset> positions) => _animatedPositions = positions;\n\n  Offset getNodePosition(Node node) => _animatedPositions?[node] ?? node.position;\n\n  void renderEdge(Canvas canvas, Edge edge, Paint paint);\n\n  Offset getNodeCenter(Node node) {\n    final nodePosition = getNodePosition(node);\n    return Offset(\n      nodePosition.dx + node.width * 0.5,\n      nodePosition.dy + node.height * 0.5,\n    );\n  }\n\n  /// Draws a line between two points respecting the node's line type\n  void drawStyledLine(Canvas canvas, Offset start, Offset end, Paint paint,\n      {LineType? lineType}) {\n    switch (lineType) {\n      case LineType.DashedLine:\n        drawDashedLine(canvas, start, end, paint, 0.6);\n        break;\n      case LineType.DottedLine:\n        drawDashedLine(canvas, start, end, paint, 0.0);\n        break;\n      case LineType.SineLine:\n        drawSineLine(canvas, start, end, paint);\n        break;\n      default:\n        canvas.drawLine(start, end, paint);\n        break;\n    }\n  }\n\n  /// Draws a styled path respecting the node's line type\n  void drawStyledPath(Canvas canvas, Path path, Paint paint,\n      {LineType? lineType}) {\n    if (lineType == null || lineType == LineType.Default) {\n      canvas.drawPath(path, paint);\n    } else {\n      // For non-solid lines, we need to convert the path to segments\n      // This is a simplified approach - for complex paths with curves,\n      // you might need a more sophisticated solution\n      canvas.drawPath(path, paint);\n    }\n  }\n\n  /// Draws a dashed line between two points\n  void drawDashedLine(Canvas canvas, Offset source, Offset destination,\n      Paint paint, double lineLength) {\n    final dx = destination.dx - source.dx;\n    final dy = destination.dy - source.dy;\n    final distance = sqrt(dx * dx + dy * dy);\n\n    if (distance == 0) return;\n\n    final numLines = lineLength == 0.0 ? (distance / 5).ceil() : 14;\n    final stepX = dx / numLines;\n    final stepY = dy / numLines;\n\n    if (lineLength == 0.0) {\n      // Draw dots\n      final circleRadius = 1.0;\n      final circlePaint = Paint()\n        ..color = paint.color\n        ..strokeWidth = 1.0\n        ..style = PaintingStyle.fill;\n\n      for (var i = 0; i < numLines; i++) {\n        final x = source.dx + (i * stepX);\n        final y = source.dy + (i * stepY);\n        canvas.drawCircle(Offset(x, y), circleRadius, circlePaint);\n      }\n    } else {\n      // Draw dashes\n      for (var i = 0; i < numLines; i++) {\n        final startX = source.dx + (i * stepX);\n        final startY = source.dy + (i * stepY);\n        final endX = startX + (stepX * lineLength);\n        final endY = startY + (stepY * lineLength);\n        canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint);\n      }\n    }\n  }\n\n  /// Draws a sine wave line between two points\n  void drawSineLine(Canvas canvas, Offset source, Offset destination, Paint paint) {\n    final originalStrokeWidth = paint.strokeWidth;\n    paint.strokeWidth = 1.5;\n\n    final dx = destination.dx - source.dx;\n    final dy = destination.dy - source.dy;\n    final distance = sqrt(dx * dx + dy * dy);\n\n    if (distance == 0 || (dx == 0 && dy == 0)) {\n      paint.strokeWidth = originalStrokeWidth;\n      return;\n    }\n\n    const lineLength = 6.0;\n    const phaseOffset = 2.0;\n    var distanceTraveled = 0.0;\n    var phase = 0.0;\n\n    final path = Path()..moveTo(source.dx, source.dy);\n\n    while (distanceTraveled < distance) {\n      final segmentLength = min(lineLength, distance - distanceTraveled);\n      final segmentFraction = (distanceTraveled + segmentLength) / distance;\n      final segmentDestination = Offset(\n        source.dx + dx * segmentFraction,\n        source.dy + dy * segmentFraction,\n      );\n\n      final waveAmplitude = sin(phase + phaseOffset) * segmentLength;\n\n      double perpX, perpY;\n      if ((dx > 0 && dy < 0) || (dx < 0 && dy > 0)) {\n        perpX = waveAmplitude;\n        perpY = waveAmplitude;\n      } else {\n        perpX = -waveAmplitude;\n        perpY = waveAmplitude;\n      }\n\n      path.lineTo(segmentDestination.dx + perpX, segmentDestination.dy + perpY);\n\n      distanceTraveled += segmentLength;\n      phase += pi * segmentLength / lineLength;\n    }\n\n    canvas.drawPath(path, paint);\n    paint.strokeWidth = originalStrokeWidth;\n  }\n\n  /// Builds a loop path for self-referential edges and returns geometry\n  /// data that renderers can use to draw arrows or style the segment.\n  LoopRenderResult? buildSelfLoopPath(\n    Edge edge, {\n    double loopPadding = 16.0,\n    double arrowLength = 12.0,\n  }) {\n    if (edge.source != edge.destination) {\n      return null;\n    }\n\n    final node = edge.source;\n    final nodeCenter = getNodeCenter(node);\n\n    final anchorRadius = node.size.shortestSide * 0.5;\n\n    final start = nodeCenter + Offset(anchorRadius, 0);\n\n    final end = nodeCenter + Offset(0, -anchorRadius);\n\n    final loopRadius = max(\n      loopPadding + anchorRadius,\n      anchorRadius * 1.5,\n    );\n\n    final controlPoint1 = start + Offset(loopRadius, 0);\n\n    final controlPoint2 = end + Offset(0, -loopRadius);\n\n    final path = Path()\n      ..moveTo(start.dx, start.dy)\n      ..cubicTo(\n        controlPoint1.dx,\n        controlPoint1.dy,\n        controlPoint2.dx,\n        controlPoint2.dy,\n        end.dx,\n        end.dy,\n      );\n\n    final metrics = path.computeMetrics().toList();\n    if (metrics.isEmpty) {\n      return LoopRenderResult(path, start, end);\n    }\n\n    final metric = metrics.first;\n    final totalLength = metric.length;\n    final effectiveArrowLength = arrowLength <= 0\n        ? 0.0\n        : min(arrowLength, totalLength * 0.3);\n    final arrowBaseOffset = max(0.0, totalLength - effectiveArrowLength);\n    final arrowBaseTangent = metric.getTangentForOffset(arrowBaseOffset);\n    final arrowTipTangent = metric.getTangentForOffset(totalLength);\n\n    return LoopRenderResult(\n      path,\n      arrowBaseTangent?.position ?? end,\n      arrowTipTangent?.position ?? end,\n    );\n  }\n}\n\nclass LoopRenderResult {\n  final Path path;\n  final Offset arrowBase;\n  final Offset arrowTip;\n\n  const LoopRenderResult(this.path, this.arrowBase, this.arrowTip);\n}\n"
  },
  {
    "path": "lib/forcedirected/FruchtermanReingoldAlgorithm.dart",
    "content": "part of graphview;\n\nclass FruchtermanReingoldAlgorithm implements Algorithm {\n  static const double DEFAULT_TICK_FACTOR = 0.1;\n  static const double CONVERGENCE_THRESHOLD = 1.0;\n\n  Map<Node, Offset> displacement = {};\n  Map<Node, Rect> nodeRects = {};\n  Random rand = Random();\n  double graphHeight = 500; //default value, change ahead of time\n  double graphWidth = 500;\n  late double tick;\n\n  FruchtermanReingoldConfiguration configuration;\n\n  @override\n  EdgeRenderer? renderer;\n\n  FruchtermanReingoldAlgorithm(this.configuration, {this.renderer}) {\n    renderer = renderer ?? ArrowEdgeRenderer(noArrow: true);\n  }\n\n  @override\n  void init(Graph? graph) {\n    graph!.nodes.forEach((node) {\n      displacement[node] = Offset.zero;\n      nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height);\n\n      if (configuration.shuffleNodes) {\n        node.position = Offset(\n            rand.nextDouble() * graphWidth, rand.nextDouble() * graphHeight);\n        // Update cached rect after position change\n        nodeRects[node] =\n            Rect.fromLTWH(node.x, node.y, node.width, node.height);\n      }\n    });\n  }\n\n  void moveNodes(Graph graph) {\n    final lerpFactor = configuration.lerpFactor;\n\n    graph.nodes.forEach((node) {\n      final nodeDisplacement = displacement[node]!;\n      var target = node.position + nodeDisplacement;\n      var newPosition = Offset.lerp(node.position, target, lerpFactor)!;\n      double newDX = min(graphWidth - node.size.width * 0.5,\n          max(node.size.width * 0.5, newPosition.dx));\n      double newDY = min(graphHeight - node.size.height * 0.5,\n          max(node.size.height * 0.5, newPosition.dy));\n\n      node.position = Offset(newDX, newDY);\n      // Update cached rect after position change\n      nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height);\n    });\n  }\n\n  void cool(int currentIteration) {\n    // tick *= 1.0 - currentIteration / configuration.iterations;\n    const alpha = 0.99; // tweakable decay factor (0.8–0.99 typical)\n    tick *= alpha;\n  }\n\n  void limitMaximumDisplacement(List<Node> nodes) {\n    final epsilon = configuration.epsilon;\n\n    nodes.forEach((node) {\n      final nodeDisplacement = displacement[node]!;\n      var dispLength = max(epsilon, nodeDisplacement.distance);\n      node.position += nodeDisplacement / dispLength * min(dispLength, tick);\n      // Update cached rect after position change\n      nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height);\n    });\n  }\n\n  void calculateAttraction(List<Edge> edges) {\n    final attractionRate = configuration.attractionRate;\n    final epsilon = configuration.epsilon;\n\n    // Optimal distance (k) based on area and node count\n    final k = sqrt((graphWidth * graphHeight) / (edges.length + 1));\n\n    for (var edge in edges) {\n      var source = edge.source;\n      var destination = edge.destination;\n      var delta = source.position - destination.position;\n      var deltaDistance = max(epsilon, delta.distance);\n\n      // Standard FR attraction: proportional to distance² / k\n      var attractionForce = (deltaDistance * deltaDistance) / k;\n      var attractionVector =\n          delta / deltaDistance * attractionForce * attractionRate;\n\n      displacement[source] = displacement[source]! - attractionVector;\n      displacement[destination] = displacement[destination]! + attractionVector;\n    }\n  }\n\n  void calculateRepulsion(List<Node> nodes) {\n    final repulsionRate = configuration.repulsionRate;\n    final repulsionPercentage = configuration.repulsionPercentage;\n    final epsilon = configuration.epsilon;\n    final nodeCountDouble = nodes.length.toDouble();\n    final maxRepulsionDistance = min(\n        graphWidth * repulsionPercentage, graphHeight * repulsionPercentage);\n\n    for (var i = 0; i < nodeCountDouble; i++) {\n      final currentNode = nodes[i];\n\n      for (var j = i + 1; j < nodeCountDouble; j++) {\n        final otherNode = nodes[j];\n        if (currentNode != otherNode) {\n          // Calculate distance between node rectangles, not just centers\n          var delta = _getNodeRectDistance(currentNode, otherNode);\n          var deltaDistance = max(epsilon, delta.distance); //protect for 0\n          var repulsionForce = max(0, maxRepulsionDistance - deltaDistance) /\n              maxRepulsionDistance; //value between 0-1\n          var repulsionVector = delta * repulsionForce * repulsionRate;\n\n          displacement[currentNode] =\n              displacement[currentNode]! + repulsionVector;\n          displacement[otherNode] = displacement[otherNode]! - repulsionVector;\n        }\n      }\n    }\n  }\n\n  // Calculate closest distance vector between two node rectangles using cached rects\n  Offset _getNodeRectDistance(Node nodeA, Node nodeB) {\n    final rectA = nodeRects[nodeA]!;\n    final rectB = nodeRects[nodeB]!;\n\n    final centerA = rectA.center;\n    final centerB = rectB.center;\n\n    if (rectA.overlaps(rectB)) {\n      // Push overlapping nodes apart by at least half their combined size\n      final dx =\n          (centerA.dx - centerB.dx).sign * (rectA.width / 2 + rectB.width / 2);\n      final dy = (centerA.dy - centerB.dy).sign *\n          (rectA.height / 2 + rectB.height / 2);\n      return Offset(dx, dy);\n    }\n\n    // Non-overlapping: distance along nearest edges\n    final dx = (centerA.dx < rectB.left)\n        ? (rectB.left - rectA.right)\n        : (centerA.dx > rectB.right)\n            ? (rectA.left - rectB.right)\n            : 0.0;\n\n    final dy = (centerA.dy < rectB.top)\n        ? (rectB.top - rectA.bottom)\n        : (centerA.dy > rectB.bottom)\n            ? (rectA.top - rectB.bottom)\n            : 0.0;\n\n    return Offset(dx == 0 ? centerA.dx - centerB.dx : dx,\n        dy == 0 ? centerA.dy - centerB.dy : dy);\n  }\n\n  bool step(Graph graph) {\n    var moved = false;\n    displacement = {};\n    for (var node in graph.nodes) {\n      displacement[node] = Offset.zero;\n    }\n\n    calculateRepulsion(graph.nodes);\n    calculateAttraction(graph.edges);\n\n    for (var node in graph.nodes) {\n      final nodeDisplacement = displacement[node]!;\n      if (nodeDisplacement.distance > configuration.movementThreshold) {\n        moved = true;\n      }\n    }\n\n    moveNodes(graph);\n    return moved;\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    if (graph == null) {\n      return Size.zero;\n    }\n    var size = findBiggestSize(graph) * graph.nodeCount();\n    graphWidth = size;\n    graphHeight = size;\n\n    var nodes = graph.nodes;\n    var edges = graph.edges;\n\n    tick = DEFAULT_TICK_FACTOR * sqrt(graphWidth / 2 * graphHeight / 2);\n\n    if (graph.nodes.any((node) => node.position == Offset.zero)) {\n      init(graph);\n    }\n\n    for (var i = 0; i < configuration.iterations; i++) {\n      calculateRepulsion(nodes);\n      calculateAttraction(edges);\n      limitMaximumDisplacement(nodes);\n\n      cool(i);\n\n      if (done()) {\n        break;\n      }\n    }\n\n    positionNodes(graph);\n\n    shiftCoordinates(graph, shiftX, shiftY);\n\n    return graph.calculateGraphSize();\n  }\n\n  void shiftCoordinates(Graph graph, double shiftX, double shiftY) {\n    graph.nodes.forEach((node) {\n      node.position = Offset(node.x + shiftX, node.y + shiftY);\n      // Update cached rect after position change\n      nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height);\n    });\n  }\n\n  void positionNodes(Graph graph) {\n    var offset = getOffset(graph);\n    var x = offset.dx;\n    var y = offset.dy;\n    var nodesVisited = <Node>[];\n    var nodeClusters = <NodeCluster>[];\n    graph.nodes.forEach((node) {\n      node.position = Offset(node.x - x, node.y - y);\n      // Update cached rect after position change\n      nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height);\n    });\n\n    graph.nodes.forEach((node) {\n      if (!nodesVisited.contains(node)) {\n        nodesVisited.add(node);\n        var cluster = findClusterOf(nodeClusters, node);\n        if (cluster == null) {\n          cluster = NodeCluster();\n          cluster.add(node);\n          nodeClusters.add(cluster);\n        }\n\n        followEdges(graph, cluster, node, nodesVisited);\n      }\n    });\n\n    positionCluster(nodeClusters);\n  }\n\n  void positionCluster(List<NodeCluster> nodeClusters) {\n    combineSingleNodeCluster(nodeClusters);\n\n    var cluster = nodeClusters[0];\n    // move first cluster to 0,0\n    cluster.offset(-cluster.rect!.left, -cluster.rect!.top);\n\n    for (var i = 1; i < nodeClusters.length; i++) {\n      var nextCluster = nodeClusters[i];\n      var xDiff = nextCluster.rect!.left -\n          cluster.rect!.right -\n          configuration.clusterPadding;\n      var yDiff = nextCluster.rect!.top - cluster.rect!.top;\n      nextCluster.offset(-xDiff, -yDiff);\n      cluster = nextCluster;\n    }\n  }\n\n  void combineSingleNodeCluster(List<NodeCluster> nodeClusters) {\n    NodeCluster? firstSingleNodeCluster;\n\n    nodeClusters.forEach((cluster) {\n      if (cluster.size() == 1) {\n        if (firstSingleNodeCluster == null) {\n          firstSingleNodeCluster = cluster;\n        } else {\n          firstSingleNodeCluster!.concat(cluster);\n        }\n      }\n    });\n\n    nodeClusters.removeWhere((element) => element.size() == 1);\n  }\n\n  void followEdges(\n      Graph graph, NodeCluster cluster, Node node, List nodesVisited) {\n    graph.successorsOf(node).forEach((successor) {\n      if (!nodesVisited.contains(successor)) {\n        nodesVisited.add(successor);\n        cluster.add(successor);\n\n        followEdges(graph, cluster, successor, nodesVisited);\n      }\n    });\n\n    graph.predecessorsOf(node).forEach((predecessor) {\n      if (!nodesVisited.contains(predecessor)) {\n        nodesVisited.add(predecessor);\n        cluster.add(predecessor);\n\n        followEdges(graph, cluster, predecessor, nodesVisited);\n      }\n    });\n  }\n\n  NodeCluster? findClusterOf(List<NodeCluster> clusters, Node node) {\n    return clusters.firstWhereOrNull((element) => element.contains(node));\n  }\n\n  double findBiggestSize(Graph graph) {\n    return graph.nodes.map((it) => max(it.height, it.width)).reduce(max);\n  }\n\n  Offset getOffset(Graph graph) {\n    var offsetX = double.infinity;\n    var offsetY = double.infinity;\n\n    graph.nodes.forEach((node) {\n      offsetX = min(offsetX, node.x);\n      offsetY = min(offsetY, node.y);\n    });\n\n    return Offset(offsetX, offsetY);\n  }\n\n  bool done() {\n    return tick < CONVERGENCE_THRESHOLD / max(graphHeight, graphWidth);\n  }\n\n  void drawEdges(Canvas canvas, Graph graph, Paint linePaint) {}\n\n  @override\n  void setDimensions(double width, double height) {\n    graphWidth = width;\n    graphHeight = height;\n  }\n}\n\nclass NodeCluster {\n  List<Node> nodes;\n  Rect? rect;\n\n  List<Node> getNodes() {\n    return nodes;\n  }\n\n  Rect? getRect() {\n    return rect;\n  }\n\n  void setRect(Rect newRect) {\n    rect = newRect;\n  }\n\n  void add(Node node) {\n    nodes.add(node);\n\n    if (nodes.length == 1) {\n      rect = Rect.fromLTRB(\n          node.x, node.y, node.x + node.width, node.y + node.height);\n    } else {\n      rect = Rect.fromLTRB(\n          min(rect!.left, node.x),\n          min(rect!.top, node.y),\n          max(rect!.right, node.x + node.width),\n          max(rect!.bottom, node.y + node.height));\n    }\n  }\n\n  bool contains(Node node) {\n    return nodes.contains(node);\n  }\n\n  int size() {\n    return nodes.length;\n  }\n\n  void concat(NodeCluster cluster) {\n    cluster.nodes.forEach((node) {\n      node.position = (Offset(\n          rect!.right +\n              FruchtermanReingoldConfiguration.DEFAULT_CLUSTER_PADDING,\n          rect!.top));\n      add(node);\n    });\n  }\n\n  void offset(double xDiff, double yDiff) {\n    nodes.forEach((node) {\n      node.position = (node.position + Offset(xDiff, yDiff));\n    });\n\n    rect = rect!.translate(xDiff, yDiff);\n  }\n\n  NodeCluster()\n      : nodes = <Node>[],\n        rect = Rect.zero;\n}\n"
  },
  {
    "path": "lib/forcedirected/FruchtermanReingoldConfiguration.dart",
    "content": "part of graphview;\n\nclass FruchtermanReingoldConfiguration {\n  static const int DEFAULT_ITERATIONS = 100;\n  static const double DEFAULT_REPULSION_RATE = 0.2;\n  static const double DEFAULT_REPULSION_PERCENTAGE = 0.4;\n  static const double DEFAULT_ATTRACTION_RATE = 0.15;\n  static const double DEFAULT_ATTRACTION_PERCENTAGE = 0.15;\n  static const int DEFAULT_CLUSTER_PADDING = 15;\n  static const double DEFAULT_EPSILON = 0.0001;\n  static const double DEFAULT_LERP_FACTOR = 0.05;\n  static const double DEFAULT_MOVEMENT_THRESHOLD = 0.6;\n\n  int iterations;\n  double repulsionRate;\n  double repulsionPercentage;\n  double attractionRate;\n  double attractionPercentage;\n  int clusterPadding;\n  double epsilon;\n  double lerpFactor;\n  double movementThreshold;\n  bool shuffleNodes = true;\n\n  FruchtermanReingoldConfiguration({\n    this.iterations = DEFAULT_ITERATIONS,\n    this.repulsionRate = DEFAULT_REPULSION_RATE,\n    this.attractionRate = DEFAULT_ATTRACTION_RATE,\n    this.repulsionPercentage = DEFAULT_REPULSION_PERCENTAGE,\n    this.attractionPercentage = DEFAULT_ATTRACTION_PERCENTAGE,\n    this.clusterPadding = DEFAULT_CLUSTER_PADDING,\n    this.epsilon = DEFAULT_EPSILON,\n    this.lerpFactor = DEFAULT_LERP_FACTOR,\n    this.movementThreshold = DEFAULT_MOVEMENT_THRESHOLD,\n    this.shuffleNodes = true\n  });\n\n}"
  },
  {
    "path": "lib/layered/EiglspergerAlgorithm.dart",
    "content": "part of graphview;\n\nclass ContainerX {\n  List<Segment> segments = [];\n  int index = -1;\n  int pos = -1;\n  double measure = -1;\n\n  ContainerX();\n\n  void append(Segment segment) {\n    segments.add(segment);\n  }\n\n  void join(ContainerX other) {\n    segments.addAll(other.segments);\n    other.segments.clear();\n  }\n\n  int size() => segments.length;\n\n  bool contains(Segment segment) => segments.contains(segment);\n\n  bool get isEmpty => segments.isEmpty;\n\n  static ContainerX createEmpty() => ContainerX();\n\n  // Split container at segment position\n  static ContainerPair split(ContainerX container, Segment key) {\n    final index = container.segments.indexOf(key);\n    if (index == -1) {\n      return ContainerPair(container, ContainerX());\n    }\n\n    final leftSegments = container.segments.sublist(0, index);\n    final rightSegments = container.segments.sublist(index + 1);\n\n    final leftContainer = ContainerX();\n    leftContainer.segments = leftSegments;\n\n    final rightContainer = ContainerX();\n    rightContainer.segments = rightSegments;\n\n    return ContainerPair(leftContainer, rightContainer);\n  }\n\n  // Split container at position\n  static ContainerPair splitAt(ContainerX container, int position) {\n    if (position <= 0) {\n      return ContainerPair(ContainerX(), container);\n    }\n    if (position >= container.size()) {\n      return ContainerPair(container, ContainerX());\n    }\n\n    final leftSegments = container.segments.sublist(0, position);\n    final rightSegments = container.segments.sublist(position);\n\n    final leftContainer = ContainerX();\n    leftContainer.segments = leftSegments;\n\n    final rightContainer = ContainerX();\n    rightContainer.segments = rightSegments;\n\n    return ContainerPair(leftContainer, rightContainer);\n  }\n\n  @override\n  String toString() => 'Container(${segments.length} segments, pos: $pos, measure: $measure)';\n}\n\nclass ContainerPair {\n  final ContainerX left;\n  final ContainerX right;\n\n  ContainerPair(this.left, this.right);\n}\n\n// Segment represents a vertical edge span between P and Q vertices\nclass Segment {\n  final Node pVertex; // top vertex (P-vertex)\n  final Node qVertex; // bottom vertex (Q-vertex)\n  int index = -1;\n  final int id;\n\n  static int _nextId = 0;\n\n  Segment(this.pVertex, this.qVertex) : id = _nextId++;\n\n  @override\n  bool operator ==(Object other) => identical(this, other);\n\n  @override\n  int get hashCode => id;\n\n  @override\n  String toString() => 'Segment($id)';\n}\n\nclass EiglspergerNodeData {\n  bool isDummy = false;\n  bool isPVertex = false;\n  bool isQVertex = false;\n  Segment? segment;\n  int layer = -1;\n  int position = -1;\n  int rank = -1;\n  double measure = -1;\n  Set<Node> reversed = {};\n  List<Node> predecessorNodes = [];\n  List<Node> successorNodes = [];\n  LineType lineType;\n\n  EiglspergerNodeData(this.lineType);\n\n  bool get isSegmentVertex => isPVertex || isQVertex;\n  bool get isReversed => reversed.isNotEmpty;\n}\n\nclass EiglspergerEdgeData {\n  List<double> bendPoints = [];\n}\n\n// Virtual edge for container connections\nclass VirtualEdge {\n  final dynamic source;\n  final dynamic target;\n  final int weight;\n\n  VirtualEdge(this.source, this.target, this.weight);\n\n  @override\n  String toString() => 'VirtualEdge($source -> $target, weight: $weight)';\n}\n\n// Layer element that can be either a Node or Container\nabstract class LayerElement {\n  int index = -1;\n  int pos = -1;\n  double measure = -1;\n}\n\n// Node wrapper for layer elements\nclass NodeElement extends LayerElement {\n  final Node node;\n  NodeElement(this.node);\n\n  @override\n  String toString() => 'NodeElement(${node.toString()})';\n}\n\n// Container wrapper for layer elements\nclass ContainerElement extends LayerElement {\n  final ContainerX container;\n  ContainerElement(this.container);\n\n  @override\n  String toString() => 'ContainerElement(${container.toString()})';\n}\n\nclass EiglspergerAlgorithm extends Algorithm {\n  Map<Node, EiglspergerNodeData> nodeData = {};\n  final Map<Edge, EiglspergerEdgeData> _edgeData = {};\n  Set<Node> stack = {};\n  Set<Node> visited = {};\n  List<List<Node>> layers = [];\n  List<Segment> segments = [];\n  Set<Edge> typeOneConflicts = {};\n  late Graph graph;\n  SugiyamaConfiguration configuration;\n\n  @override\n  EdgeRenderer? renderer;\n\n  var nodeCount = 1;\n\n  EiglspergerAlgorithm(this.configuration) {\n    // renderer = SugiyamaEdgeRenderer(nodeData, edgeData, configuration.bendPointShape, configuration.addTriangleToEdge);\n  }\n\n  int get dummyId => 'Dummy ${nodeCount++}'.hashCode;\n\n  bool isVertical() {\n    var orientation = configuration.orientation;\n    return orientation == SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ||\n        orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP;\n  }\n\n  bool needReverseOrder() {\n    var orientation = configuration.orientation;\n    return orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP ||\n        orientation == SugiyamaConfiguration.ORIENTATION_RIGHT_LEFT;\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    this.graph = copyGraph(graph!);\n    reset();\n    initNodeData();\n    cycleRemoval();\n    layerAssignment();\n    nodeOrdering(); // Eiglsperger 6-step process\n    coordinateAssignment();\n    shiftCoordinates(shiftX, shiftY);\n    final graphSize = graph.calculateGraphSize();\n    denormalize();\n    restoreCycle();\n    return graphSize;\n  }\n\n  void shiftCoordinates(double shiftX, double shiftY) {\n    layers.forEach((List<Node?> arrayList) {\n      arrayList.forEach((it) {\n        it!.position = Offset(it.x + shiftX, it.y + shiftY);\n      });\n    });\n  }\n\n  void reset() {\n    layers.clear();\n    stack.clear();\n    visited.clear();\n    nodeData.clear();\n    _edgeData.clear();\n    segments.clear();\n    typeOneConflicts.clear();\n    nodeCount = 1;\n  }\n\n  void initNodeData() {\n    graph.nodes.forEach((node) {\n      node.position = Offset(0, 0);\n      nodeData[node] = EiglspergerNodeData(node.lineType);\n    });\n\n    graph.edges.forEach((edge) {\n      _edgeData[edge] = EiglspergerEdgeData();\n    });\n\n    graph.edges.forEach((edge) {\n      nodeData[edge.source]?.successorNodes.add(edge.destination);\n      nodeData[edge.destination]?.predecessorNodes.add(edge.source);\n    });\n  }\n\n  void cycleRemoval() {\n    graph.nodes.forEach((node) {\n      dfs(node);\n    });\n  }\n\n  void dfs(Node node) {\n    if (visited.contains(node)) {\n      return;\n    }\n    visited.add(node);\n    stack.add(node);\n    graph.getOutEdges(node).forEach((edge) {\n      final target = edge.destination;\n      if (stack.contains(target)) {\n        graph.removeEdge(edge);\n        graph.addEdge(target, node);\n        nodeData[node]!.reversed.add(target);\n      } else {\n        dfs(target);\n      }\n    });\n    stack.remove(node);\n  }\n\n  void layerAssignment() {\n    if (graph.nodes.isEmpty) {\n      return;\n    }\n\n    // Build layers using topological sort\n    final copiedGraph = copyGraph(graph);\n    var roots = getRootNodes(copiedGraph);\n\n    while (roots.isNotEmpty) {\n      layers.add(roots);\n      copiedGraph.removeNodes(roots);\n      roots = getRootNodes(copiedGraph);\n    }\n\n    // Create segments for long edges\n    createSegmentsForLongEdges();\n  }\n\n  void createSegmentsForLongEdges() {\n    // Create segments for edges spanning more than one layer\n    for (var i = 0; i < layers.length - 1; i++) {\n      var currentLayer = layers[i];\n\n      for (var node in List.from(currentLayer)) {\n        final edges = graph.getOutEdges(node)\n            .where((e) => (nodeData[e.destination]!.layer - nodeData[node]!.layer).abs() > 1)\n            .toList();\n\n        for (var edge in edges) {\n          if (nodeData[edge.destination]!.layer - nodeData[node]!.layer == 2) {\n            // Simple case: only one layer between source and target\n            createSingleDummyVertex(edge, i + 1);\n          } else {\n            // Complex case: multiple layers between source and target\n            createSegment(edge);\n          }\n          graph.removeEdge(edge);\n        }\n      }\n    }\n  }\n\n  void createSingleDummyVertex(Edge edge, int dummyLayer) {\n    final dummy = Node.Id(dummyId);\n\n    final dummyData = EiglspergerNodeData(edge.source.lineType);\n    dummyData.isDummy = true;\n    dummyData.layer = dummyLayer;\n    nodeData[dummy] = dummyData;\n\n    dummy.size = Size(edge.source.width, 0);\n\n    layers[dummyLayer].add(dummy);\n    graph.addNode(dummy);\n\n    final edge1 = graph.addEdge(edge.source, dummy);\n    final edge2 = graph.addEdge(dummy, edge.destination);\n\n    _edgeData[edge1] = EiglspergerEdgeData();\n    _edgeData[edge2] = EiglspergerEdgeData();\n  }\n\n  void createSegment(Edge edge) {\n    final sourceLayer = nodeData[edge.source]!.layer;\n    final targetLayer = nodeData[edge.destination]!.layer;\n\n    // Create P-vertex (top of segment)\n    final pVertex = Node.Id(dummyId);\n    final pData = EiglspergerNodeData(edge.source.lineType);\n    pData.isDummy = true;\n    pData.isPVertex = true;\n    pData.layer = sourceLayer + 1;\n    nodeData[pVertex] = pData;\n    pVertex.size = Size(edge.source.width, 0);\n\n    // Create Q-vertex (bottom of segment)\n    final qVertex = Node.Id(dummyId);\n    final qData = EiglspergerNodeData(edge.source.lineType);\n    qData.isDummy = true;\n    qData.isQVertex = true;\n    qData.layer = targetLayer - 1;\n    nodeData[qVertex] = qData;\n    qVertex.size = Size(edge.source.width, 0);\n\n    // Create segment and link vertices\n    final segment = Segment(pVertex, qVertex);\n    pData.segment = segment;\n    qData.segment = segment;\n    segments.add(segment);\n\n    // Add to layers and graph\n    layers[sourceLayer + 1].add(pVertex);\n    layers[targetLayer - 1].add(qVertex);\n    graph.addNode(pVertex);\n    graph.addNode(qVertex);\n\n    // Create edges\n    final edgeToP = graph.addEdge(edge.source, pVertex);\n    final segmentEdge = graph.addEdge(pVertex, qVertex);\n    final edgeFromQ = graph.addEdge(qVertex, edge.destination);\n\n    _edgeData[edgeToP] = EiglspergerEdgeData();\n    _edgeData[segmentEdge] = EiglspergerEdgeData();\n    _edgeData[edgeFromQ] = EiglspergerEdgeData();\n  }\n\n  List<Node> getRootNodes(Graph graph) {\n    final predecessors = <Node, bool>{};\n    graph.edges.forEach((element) {\n      predecessors[element.destination] = true;\n    });\n\n    var roots = graph.nodes.where((node) => predecessors[node] == null);\n    roots.forEach((node) {\n      nodeData[node]?.layer = layers.length;\n    });\n\n    return roots.toList();\n  }\n\n  Graph copyGraph(Graph graph) {\n    final copy = Graph();\n    copy.addNodes(graph.nodes);\n    copy.addEdges(graph.edges);\n    return copy;\n  }\n\n  void nodeOrdering() {\n    final best = <List<Node>>[...layers];\n\n    // Precalculate neighbor information\n\n\n    var bestCrossCount = double.infinity;\n\n    for (var i = 0; i < configuration.iterations; i++) {\n      var crossCount = 0.0;\n\n      if (i % 2 == 0) {\n        crossCount = forwardSweep(layers);\n      } else {\n        crossCount = backwardSweep(layers);\n      }\n\n      if (crossCount < bestCrossCount) {\n        bestCrossCount = crossCount;\n        // Save best configuration\n        for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) {\n          best[layerIndex] = List.from(layers[layerIndex]);\n        }\n      }\n\n      if (crossCount == 0) break;\n    }\n\n    // Restore best configuration\n    for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) {\n      layers[layerIndex] = best[layerIndex];\n    }\n\n    // Set final positions\n    updateNodePositions();\n  }\n\n  double forwardSweep(List<List<Node>> layers) {\n    var totalCrossings = 0.0;\n\n    for (var i = 0; i < layers.length - 1; i++) {\n      var currentLayer = layers[i];\n      var nextLayer = layers[i + 1];\n\n      // Convert to layer elements with containers\n      var currentElements = createLayerElements(currentLayer);\n      var nextElements = createLayerElements(nextLayer);\n\n      // Eiglsperger 6-step process\n      stepOne(currentElements, true); // Handle P-vertices\n      stepTwo(currentElements, nextElements);\n      stepThree(nextElements);\n      stepFour(nextElements, i + 1);\n      totalCrossings += stepFive(currentElements, nextElements, i, i + 1);\n      stepSix(nextElements);\n\n      // Convert back to node layer\n      layers[i + 1] = extractNodes(nextElements);\n    }\n\n    return totalCrossings;\n  }\n\n  double backwardSweep(List<List<Node>> layers) {\n    var totalCrossings = 0.0;\n\n    for (var i = layers.length - 1; i > 0; i--) {\n      var currentLayer = layers[i];\n      var prevLayer = layers[i - 1];\n\n      var currentElements = createLayerElements(currentLayer);\n      var prevElements = createLayerElements(prevLayer);\n\n      stepOne(currentElements, false); // Handle Q-vertices\n      stepTwo(currentElements, prevElements);\n      stepThree(prevElements);\n      stepFour(prevElements, i - 1);\n      totalCrossings += stepFive(currentElements, prevElements, i, i - 1);\n      stepSix(prevElements);\n\n      layers[i - 1] = extractNodes(prevElements);\n    }\n\n    return totalCrossings;\n  }\n\n  List<LayerElement> createLayerElements(List<Node> layer) {\n    return layer.map((node) => NodeElement(node)).cast<LayerElement>().toList();\n  }\n\n  List<Node> extractNodes(List<LayerElement> elements) {\n    var nodes = <Node>[];\n    for (var element in elements) {\n      if (element is NodeElement) {\n        nodes.add(element.node);\n      } else if (element is ContainerElement) {\n        // Extract nodes from segments in container\n        for (var segment in element.container.segments) {\n          if (!nodes.contains(segment.pVertex)) {\n            nodes.add(segment.pVertex);\n          }\n          if (!nodes.contains(segment.qVertex)) {\n            nodes.add(segment.qVertex);\n          }\n        }\n      }\n    }\n    return nodes;\n  }\n\n  // Eiglsperger Step 1: Handle P-vertices (forward) or Q-vertices (backward)\n  void stepOne(List<LayerElement> layer, bool isForward) {\n    var processedElements = <LayerElement>[];\n    ContainerX? currentContainer;\n\n    for (var element in layer) {\n      if (element is NodeElement) {\n        var node = element.node;\n        var data = nodeData[node];\n\n        var shouldMerge = isForward ?\n        (data?.isPVertex ?? false) :\n        (data?.isQVertex ?? false);\n\n        if (shouldMerge && data?.segment != null) {\n          // Merge into container\n          currentContainer ??= ContainerX();\n          currentContainer.append(data!.segment!);\n\n          if (!processedElements.any((e) => e is ContainerElement && e.container == currentContainer)) {\n            processedElements.add(ContainerElement(currentContainer));\n          }\n        } else {\n          // Regular node\n          processedElements.add(element);\n          currentContainer = null;\n        }\n      } else {\n        processedElements.add(element);\n        currentContainer = null;\n      }\n    }\n\n    layer.clear();\n    layer.addAll(processedElements);\n  }\n\n  // Eiglsperger Step 2: Compute position values and measures\n  void stepTwo(List<LayerElement> currentLayer, List<LayerElement> nextLayer) {\n    // Assign positions to current layer\n    assignPositions(currentLayer);\n\n    // Compute measures for next layer based on current layer positions\n    for (var element in nextLayer) {\n      if (element is NodeElement) {\n        var node = element.node;\n        var predecessors = predecessorsOf(node);\n\n        if (predecessors.isNotEmpty) {\n          var positions = predecessors.map((p) => nodeData[p]?.position ?? 0).toList();\n          positions.sort();\n          element.measure = medianValue(positions).toDouble();\n        } else {\n          element.measure = element.pos.toDouble();\n        }\n      } else if (element is ContainerElement) {\n        element.measure = element.pos.toDouble();\n      }\n    }\n  }\n\n  void assignPositions(List<LayerElement> layer) {\n    var currentPos = 0;\n    for (var element in layer) {\n      element.pos = currentPos;\n\n      if (element is NodeElement) {\n        nodeData[element.node]?.position = currentPos;\n        currentPos++;\n      } else if (element is ContainerElement) {\n        currentPos += element.container.size();\n      }\n    }\n  }\n\n  // Eiglsperger Step 3: Initial ordering based on measures\n  void stepThree(List<LayerElement> layer) {\n    var vertices = <LayerElement>[];\n    var containers = <ContainerElement>[];\n\n    // Separate vertices and containers\n    for (var element in layer) {\n      if (element is ContainerElement && element.container.size() > 0) {\n        containers.add(element);\n      } else if (element is NodeElement) {\n        var data = nodeData[element.node];\n        if (!(data?.isSegmentVertex ?? false)) {\n          vertices.add(element);\n        }\n      }\n    }\n\n    // Sort by measure\n    vertices.sort((a, b) => a.measure.compareTo(b.measure));\n    containers.sort((a, b) => a.measure.compareTo(b.measure));\n\n    // Merge lists according to Eiglsperger algorithm\n    var merged = mergeSortedLists(vertices, containers);\n\n    layer.clear();\n    layer.addAll(merged);\n  }\n\n  List<LayerElement> mergeSortedLists(List<LayerElement> vertices, List<ContainerElement> containers) {\n    var result = <LayerElement>[];\n    var vIndex = 0;\n    var cIndex = 0;\n\n    while (vIndex < vertices.length && cIndex < containers.length) {\n      var vertex = vertices[vIndex];\n      var container = containers[cIndex];\n\n      if (vertex.measure <= container.pos) {\n        result.add(vertex);\n        vIndex++;\n      } else if (vertex.measure >= (container.pos + container.container.size() - 1)) {\n        result.add(container);\n        cIndex++;\n      } else {\n        // Split container\n        var k = (vertex.measure - container.pos).ceil();\n        var split = ContainerX.splitAt(container.container, k);\n\n        if (split.left.size() > 0) {\n          result.add(ContainerElement(split.left));\n        }\n        result.add(vertex);\n        if (split.right.size() > 0) {\n          split.right.pos = container.pos + k;\n          containers.insert(cIndex + 1, ContainerElement(split.right));\n        }\n        vIndex++;\n        cIndex++;\n      }\n    }\n\n    // Add remaining elements\n    while (vIndex < vertices.length) {\n      result.add(vertices[vIndex++]);\n    }\n    while (cIndex < containers.length) {\n      result.add(containers[cIndex++]);\n    }\n\n    return result;\n  }\n\n  // Eiglsperger Step 4: Place Q-vertices according to their segments\n  void stepFour(List<LayerElement> layer, int layerIndex) {\n    var segmentVertices = <NodeElement>[];\n\n    // Find segment vertices in this layer\n    for (var element in List.from(layer)) {\n      if (element is NodeElement) {\n        var data = nodeData[element.node];\n        if (data?.isSegmentVertex ?? false) {\n          segmentVertices.add(element);\n          layer.remove(element);\n        }\n      }\n    }\n\n    // Place each segment vertex\n    for (var segmentElement in segmentVertices) {\n      var segmentNode = segmentElement.node;\n      var data = nodeData[segmentNode];\n      var segment = data?.segment;\n\n      if (segment != null) {\n        // Find container containing this segment\n        ContainerElement? containerElement;\n        for (var element in layer) {\n          if (element is ContainerElement && element.container.contains(segment)) {\n            containerElement = element;\n            break;\n          }\n        }\n\n        if (containerElement != null) {\n          var containerIndex = layer.indexOf(containerElement);\n          var split = ContainerX.split(containerElement.container, segment);\n\n          layer.removeAt(containerIndex);\n\n          if (split.left.size() > 0) {\n            layer.insert(containerIndex, ContainerElement(split.left));\n            containerIndex++;\n          }\n\n          layer.insert(containerIndex, segmentElement);\n          containerIndex++;\n\n          if (split.right.size() > 0) {\n            layer.insert(containerIndex, ContainerElement(split.right));\n          }\n        } else {\n          // No container found, just add the segment vertex\n          layer.add(segmentElement);\n        }\n      }\n    }\n\n    updateIndices(layer);\n  }\n\n  void updateIndices(List<LayerElement> layer) {\n    for (var i = 0; i < layer.length; i++) {\n      layer[i].index = i;\n      if (layer[i] is NodeElement) {\n        var node = (layer[i] as NodeElement).node;\n        nodeData[node]?.position = i;\n      }\n    }\n  }\n\n  // Eiglsperger Step 5: Cross counting with virtual edges\n  double stepFive(List<LayerElement> currentLayer, List<LayerElement> nextLayer,\n      int currentRank, int nextRank) {\n    // Remove empty containers\n    currentLayer.removeWhere((e) => e is ContainerElement && e.container.isEmpty);\n    nextLayer.removeWhere((e) => e is ContainerElement && e.container.isEmpty);\n\n    updateIndices(currentLayer);\n    updateIndices(nextLayer);\n\n    // Collect all edges including virtual edges\n    var allEdges = <dynamic>[];\n\n    // Add regular graph edges between these layers\n    for (var edge in graph.edges) {\n      if (nodeData[edge.source]?.layer == currentRank &&\n          nodeData[edge.destination]?.layer == nextRank) {\n        allEdges.add(edge);\n      }\n    }\n\n    // Add virtual edges for containers\n    for (var element in nextLayer) {\n      if (element is ContainerElement && element.container.size() > 0) {\n        var virtualEdge = VirtualEdge('virtual', element, element.container.size());\n        allEdges.add(virtualEdge);\n      } else if (element is NodeElement) {\n        var data = nodeData[element.node];\n        if (data?.isSegmentVertex ?? false) {\n          var virtualEdge = VirtualEdge('virtual', element.node, 1);\n          allEdges.add(virtualEdge);\n        }\n      }\n    }\n\n    // Count crossings with weights\n    return countWeightedCrossings(allEdges, nextLayer);\n  }\n\n  double countWeightedCrossings(List<dynamic> edges, List<LayerElement> nextLayer) {\n    var crossings = 0.0;\n\n    for (var i = 0; i < edges.length - 1; i++) {\n      for (var j = i + 1; j < edges.length; j++) {\n        var edge1 = edges[i];\n        var edge2 = edges[j];\n\n        var weight1 = getEdgeWeight(edge1);\n        var weight2 = getEdgeWeight(edge2);\n\n        var pos1 = getTargetPosition(edge1, nextLayer);\n        var pos2 = getTargetPosition(edge2, nextLayer);\n\n        if (pos1 > pos2) {\n          crossings += weight1 * weight2;\n        }\n      }\n    }\n\n    return crossings;\n  }\n\n  int getEdgeWeight(dynamic edge) {\n    if (edge is VirtualEdge) {\n      return edge.weight;\n    }\n    return 1;\n  }\n\n  int getTargetPosition(dynamic edge, List<LayerElement> nextLayer) {\n    if (edge is VirtualEdge) {\n      for (var i = 0; i < nextLayer.length; i++) {\n        if ((nextLayer[i] is ContainerElement && nextLayer[i] == edge.target) ||\n            (nextLayer[i] is NodeElement && (nextLayer[i] as NodeElement).node == edge.target)) {\n          return i;\n        }\n      }\n    } else if (edge is Edge) {\n      for (var i = 0; i < nextLayer.length; i++) {\n        if (nextLayer[i] is NodeElement &&\n            (nextLayer[i] as NodeElement).node == edge.destination) {\n          return i;\n        }\n      }\n    }\n    return 0;\n  }\n\n  // Eiglsperger Step 6: Scan and ensure alternating structure\n  void stepSix(List<LayerElement> layer) {\n    var scanned = <LayerElement>[];\n\n    for (var i = 0; i < layer.length; i++) {\n      var element = layer[i];\n\n      if (scanned.isEmpty) {\n        if (element is ContainerElement) {\n          scanned.add(element);\n        } else {\n          scanned.add(ContainerElement(ContainerX.createEmpty()));\n          scanned.add(element);\n        }\n      } else {\n        var previous = scanned.last;\n\n        if (previous is ContainerElement && element is ContainerElement) {\n          // Join containers\n          previous.container.join(element.container);\n        } else if (previous is NodeElement && element is NodeElement) {\n          // Insert empty container between nodes\n          scanned.add(ContainerElement(ContainerX.createEmpty()));\n          scanned.add(element);\n        } else {\n          scanned.add(element);\n        }\n      }\n    }\n\n    // Ensure ends with container\n    if (scanned.isNotEmpty && scanned.last is NodeElement) {\n      scanned.add(ContainerElement(ContainerX.createEmpty()));\n    }\n\n    layer.clear();\n    layer.addAll(scanned);\n    updateIndices(layer);\n  }\n\n  void updateNodePositions() {\n    for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) {\n      for (var nodeIndex = 0; nodeIndex < layers[layerIndex].length; nodeIndex++) {\n        var node = layers[layerIndex][nodeIndex];\n        nodeData[node]?.position = nodeIndex;\n\n        var data = nodeData[node];\n        if (data != null) {\n          data.rank = layerIndex;\n        }\n      }\n    }\n  }\n\n  void coordinateAssignment() {\n    assignX();\n    assignY();\n    var offset = getOffset(graph, needReverseOrder());\n\n    graph.nodes.forEach((v) {\n      v.position = getPosition(v, offset);\n    });\n  }\n\n  void assignX() {\n    // Simplified coordinate assignment - can be enhanced with full Brandes-Köpf algorithm\n    var separation = configuration.nodeSeparation;\n    var vertical = isVertical();\n\n    for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) {\n      var layer = layers[layerIndex];\n      var x = 0.0;\n\n      for (var nodeIndex = 0; nodeIndex < layer.length; nodeIndex++) {\n        var node = layer[nodeIndex];\n        var width = vertical ? node.width + separation : node.height;\n        node.x = x + width / 2;\n        x += width + separation;\n      }\n    }\n  }\n\n  void assignXx() {\n    // Existing implementation remains the same\n    final root = <Map<Node, Node>>[];\n    // each node points to its aligned neighbor in the layer below.;\n    final align = <Map<Node, Node>>[];\n    final sink = <Map<Node, Node>>[];\n    final x = <Map<Node, double>>[];\n    // minimal separation between the roots of different classes.;\n    final shift = <Map<Node, double>>[];\n    // the width of each block (max width of node in block);\n    final blockWidth = <Map<Node, double>>[];\n\n    for (var i = 0; i < 4; i++) {\n      root.add({});\n      align.add({});\n      sink.add({});\n      shift.add({});\n      x.add({});\n      blockWidth.add({});\n\n      graph.nodes.forEach((n) {\n        root[i][n] = n;\n        align[i][n] = n;\n        sink[i][n] = n;\n        shift[i][n] = double.infinity;\n        x[i][n] = double.negativeInfinity;\n        blockWidth[i][n] = 0;\n      });\n    }\n    var separation = configuration.nodeSeparation;\n\n    var vertical = isVertical();\n    for (var downward = 0; downward <= 1; downward++) {\n      var isDownward = downward == 0;\n      final type1Conflicts = <int, int>{};\n      for (var leftToRight = 0; leftToRight <= 1; leftToRight++) {\n        final k = 2 * downward + leftToRight;\n        var isLeftToRight = leftToRight == 0;\n        verticalAlignment(\n            root[k], align[k], type1Conflicts, isDownward, isLeftToRight);\n        graph.nodes.forEach((v) {\n          final r = root[k][v]!;\n          blockWidth[k][r] = max(\n              blockWidth[k][r]!, vertical ? v.width + separation : v.height);\n        });\n        horizontalCompactation(align[k], root[k], sink[k], shift[k], blockWidth[k], x[k], isLeftToRight, isDownward, layers, separation);\n      }\n    }\n\n    balance(x, blockWidth);\n  }\n\n  void balance(List<Map<Node, double>> x, List<Map<Node?, double>> blockWidth) {\n    final coordinates = <Node, double>{};\n\n    // switch (configuration.coordinateAssignment) {\n    //   case CoordinateAssignment.Average:\n    //     var minWidth = double.infinity;\n    //\n    //     var smallestWidthLayout = 0;\n    //     final minArray = List.filled(4, 0.0);\n    //     final maxArray = List.filled(4, 0.0);\n    //\n    //     // Get the layout with the smallest width and set minimum and maximum value for each direction;\n    //     for (var i = 0; i < 4; i++) {\n    //       minArray[i] = double.infinity;\n    //       maxArray[i] = 0;\n    //\n    //       graph.nodes.forEach((v) {\n    //         final bw = 0.5 * blockWidth[i][v]!;\n    //         var xp = x[i][v]! - bw;\n    //         if (xp < minArray[i]) {\n    //           minArray[i] = xp;\n    //         }\n    //         xp = x[i][v]! + bw;\n    //         if (xp > maxArray[i]) {\n    //           maxArray[i] = xp;\n    //         }\n    //       });\n    //\n    //       final width = maxArray[i] - minArray[i];\n    //       if (width < minWidth) {\n    //         minWidth = width;\n    //         smallestWidthLayout = i;\n    //       }\n    //     }\n    //\n    //     // Align the layouts to the one with the smallest width\n    //     for (var layout = 0; layout < 4; layout++) {\n    //       if (layout != smallestWidthLayout) {\n    //         // Align the left to right layouts to the left border of the smallest layout\n    //         var diff = 0.0;\n    //         if (layout < 2) {\n    //           diff = minArray[layout] - minArray[smallestWidthLayout];\n    //         } else {\n    //           // Align the right to left layouts to the right border of the smallest layout\n    //           diff = maxArray[layout] - maxArray[smallestWidthLayout];\n    //         }\n    //         if (diff > 0) {\n    //           x[layout].keys.forEach((n) {\n    //             x[layout][n] = x[layout][n]! - diff;\n    //           });\n    //         } else {\n    //           x[layout].keys.forEach((n) {\n    //             x[layout][n] = x[layout][n]! + diff;\n    //           });\n    //         }\n    //       }\n    //     }\n    //\n    //     // Get the average median of each coordinate\n    //     var values = List.filled(4, 0.0);\n    //     graph.nodes.forEach((n) {\n    //       for (var i = 0; i < 4; i++) {\n    //         values[i] = x[i][n]!;\n    //       }\n    //       values.sort();\n    //       var average = (values[1] + values[2]) * 0.5;\n    //       coordinates[n] = average;\n    //     });\n    //     break;\n    //   case CoordinateAssignment.DownRight:\n    //     graph.nodes.forEach((n) {\n    //       coordinates[n] = x[0][n] ?? 0.0;\n    //     });\n    //     break;\n    //   case CoordinateAssignment.DownLeft:\n    //     graph.nodes.forEach((n) {\n    //       coordinates[n] = x[1][n] ?? 0.0;\n    //     });\n    //     break;\n    //   case CoordinateAssignment.UpRight:\n    //     graph.nodes.forEach((n) {\n    //       coordinates[n] = x[2][n] ?? 0.0;\n    //     });\n    //     break;\n    //   case CoordinateAssignment.UpLeft:\n    //     graph.nodes.forEach((n) {\n    //       coordinates[n] = x[3][n] ?? 0.0;\n    //     });\n    //     break;\n    // }\n\n    graph.nodes.forEach((n) {\n      coordinates[n] = x[3][n] ?? 0.0;\n    });\n    // Get the minimum coordinate value\n    var minValue = coordinates.values.reduce(min);\n\n    // Set left border to 0\n    if (minValue != 0) {\n      coordinates.keys.forEach((n) {\n        coordinates[n] = coordinates[n]! - minValue;\n      });\n    }\n\n    // resolveOverlaps(coordinates);\n\n\n    graph.nodes.forEach((v) {\n      v.x = coordinates[v]!;\n    });\n  }\n\n  void resolveOverlaps(Map<Node, double> coordinates) {\n    for (var layer in layers) {\n      var layerNodes = List<Node>.from(layer);\n      layerNodes.sort(\n              (a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position));\n\n      var data = nodeData[layerNodes.first];\n      if (data?.layer != 0) {\n        var leftCoordinate = 0.0;\n        for (var i = 1; i < layerNodes.length; i++) {\n          var currentNode = layerNodes[i];\n          if (!nodeData[currentNode]!.isDummy) {\n            var previousNode = getPreviousNonDummyNode(layerNodes, i);\n\n            if (previousNode != null) {\n              leftCoordinate = coordinates[previousNode]! +\n                  previousNode.width +\n                  configuration.nodeSeparation;\n            } else {\n              leftCoordinate = 0.0;\n            }\n\n            if (leftCoordinate > coordinates[currentNode]!) {\n              var adjustment = leftCoordinate - coordinates[currentNode]!;\n              if (coordinates[currentNode] != null) {\n                coordinates[currentNode] =\n                    coordinates[currentNode]! + adjustment;\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  Node? getPreviousNonDummyNode(List<Node> layerNodes, int currentIndex) {\n    for (var i = currentIndex - 1; i >= 0; i--) {\n      var previousNode = layerNodes[i];\n      if (!nodeData[previousNode]!.isDummy) {\n        return previousNode;\n      }\n    }\n    return null;\n  }\n\n  // Map<int, int> markType1Conflicts(bool downward) {\n  //   if (layers.length >= 4) {\n  //     int upper;\n  //     int lower; // iteration bounds;\n  //     int k1; // node position boundaries of closest inner segments;\n  //     if (downward) {\n  //       lower = 1;\n  //       upper = layers.length - 2;\n  //     } else {\n  //       lower = layers.length - 1;\n  //       upper = 2;\n  //     }\n  //     /*;\n  //            * iterate level[2..h-2] in the given direction;\n  //            * available 1 levels to h;\n  //            */\n  //     for (var i = lower;\n  //     downward ? i <= upper : i >= upper;\n  //     i += downward ? 1 : -1) {\n  //       var k0 = 0;\n  //       var firstIndex = 0; // index of first node on layer;\n  //       final currentLevel = layers[i];\n  //       final nextLevel = downward ? layers[i + 1] : layers[i - 1];\n  //\n  //       // for all nodes on next level;\n  //       for (var l1 = 0; l1 < nextLevel.length; l1++) {\n  //         final virtualTwin = virtualTwinNode(nextLevel[l1], downward);\n  //\n  //         if (l1 == nextLevel.length - 1 || virtualTwin != null) {\n  //           k1 = currentLevel.length - 1;\n  //\n  //           if (virtualTwin != null) {\n  //             k1 = positionOfNode(virtualTwin);\n  //           }\n  //\n  //           while (firstIndex <= l1) {\n  //             final upperNeighbours = getAdjNodes(nextLevel[l1], downward);\n  //\n  //             for (var currentNeighbour in upperNeighbours) {\n  //               /*;\n  //               *  XXX< 0 in first iteration is still ok for indizes starting;\n  //               * with 0 because no index can be smaller than 0;\n  //                */\n  //               final currentNeighbourIndex = positionOfNode(currentNeighbour);\n  //\n  //               if (currentNeighbourIndex < k0 || currentNeighbourIndex > k1) {\n  //                 type1Conflicts[l1] = currentNeighbourIndex;\n  //               }\n  //             }\n  //             firstIndex++;\n  //           }\n  //\n  //           k0 = k1;\n  //         }\n  //       }\n  //     }\n  //   }\n  //   return type1Conflicts;\n  // }\n\n  void verticalAlignment(Map<Node?, Node?> root, Map<Node?, Node?> align,\n      Map<int, int> type1Conflicts, bool downward, bool leftToRight) {\n    // for all Level;\n\n    var layersa = downward ? layers : layers.reversed;\n\n    for (var layer in layersa) {\n      // As with layers, we need a reversed iterator for blocks for different directions\n      var nodes = leftToRight ? layer : layer.reversed;\n      // Do an initial placement for all blocks\n      var r = leftToRight ? -1 : double.infinity;\n      for (var v in nodes) {\n        final adjNodes = getAdjNodes(v, downward);\n        if (adjNodes.isNotEmpty) {\n          var midLevelValue = adjNodes.length / 2;\n          // Calculate medians\n          final medians = adjNodes.length % 2 == 1\n              ? [adjNodes[midLevelValue.floor()]]\n              : [\n            adjNodes[midLevelValue.toInt() - 1],\n            adjNodes[midLevelValue.toInt()]\n          ];\n\n          // For all median neighbours in direction of H\n          for (var m in medians) {\n            final posM = positionOfNode(m);\n            // if segment (u,v) not marked by type1 conflicts AND ...;\n            if (align[v] == v &&\n                type1Conflicts[positionOfNode(v)] != posM &&\n                (leftToRight ? r < posM : r > posM)) {\n              align[m] = v;\n              root[v] = root[m];\n              align[v] = root[v];\n              r = posM;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  void horizontalCompactation(\n      Map<Node, Node> align,\n      Map<Node, Node> root,\n      Map<Node, Node> sink,\n      Map<Node, double> shift,\n      Map<Node, double> blockWidth,\n      Map<Node, double> x,\n      bool leftToRight,\n      bool downward,\n      List<List<Node>> layers,\n      int separation) {\n    // calculate class relative coordinates for all roots;\n    // If the layers are traversed from right to left, a reverse iterator is needed (note that this does not change the original list of layers)\n    var layersa = leftToRight ? layers : layers.reversed;\n\n    for (var layer in layersa) {\n      // As with layers, we need a reversed iterator for blocks for different directions\n      var nodes = downward ? layer : layer.reversed;\n      // Do an initial placement for all blocks\n      for (var v in nodes) {\n        if (root[v] == v) {\n          placeBlock(v, sink, shift, x, align, blockWidth, root, leftToRight,\n              layers, separation);\n        }\n      }\n    }\n\n    var d = 0;\n    var i = downward ? 0 : layers.length - 1;\n    while (downward && i <= layers.length - 1 || !downward && i >= 0) {\n      final currentLevel = layers[i];\n      final v = currentLevel[leftToRight ? 0 : currentLevel.length - 1];\n      if (v == sink[root[v]]) {\n        final oldShift = shift[v]!;\n        if (oldShift < double.infinity) {\n          shift[v] = oldShift + d;\n          d += oldShift.toInt();\n        } else {\n          shift[v] = 0;\n        }\n      }\n      i = downward ? i + 1 : i - 1;\n    }\n\n    // apply root coordinates for all aligned nodes;\n    // (place block did this only for the roots)+;\n    graph.nodes.forEach((v) {\n      x[v] = x[root[v]]!;\n      final shiftVal = shift[sink[root[v]]]!;\n      if (shiftVal < double.infinity) {\n        x[v] = x[v]! + shiftVal; // apply shift for each class;\n      }\n    });\n  }\n\n  void placeBlock(\n      Node v,\n      Map<Node, Node> sink,\n      Map<Node, double> shift,\n      Map<Node, double> x,\n      Map<Node, Node> align,\n      Map<Node, double> blockWidth,\n      Map<Node, Node> root,\n      bool leftToRight,\n      List<List<Node>> layers,\n      int separation) {\n    if (x[v] == double.negativeInfinity) {\n      x[v] = 0;\n      var currentNode = v;\n\n      try {\n        do {\n          // if not first node on layer;\n          final hasPredecessor =\n              leftToRight && positionOfNode(currentNode) > 0 ||\n                  !leftToRight &&\n                      positionOfNode(currentNode) <\n                          layers[getLayerIndex(currentNode)].length - 1;\n          // print(\"Pred  $hasPredecessor ${getLayerIndex(currentNode)>0} ${positionOfNode(currentNode)>0}\");\n          if (hasPredecessor) {\n            final pred = predecessor(currentNode, leftToRight);\n            /* Get the root of u (proceeding all the way upwards in the block) */\n            final u = root[pred]!;\n            /* Place the block of u recursively */\n            placeBlock(u, sink, shift, x, align, blockWidth, root, leftToRight,\n                layers, separation);\n            /* If v is its own sink yet, set its sink to the sink of u */\n            if (sink[v] == v) {\n              sink[v] = sink[u]!;\n            }\n            /* If v and u have different sinks (i.e. they are in different classes),\n             * shift the sink of u so that the two blocks are separated by the preferred gap  */\n            var gap = separation + 0.5 * (blockWidth[u]! + blockWidth[v]!);\n            if (sink[v] != sink[u]) {\n              if (leftToRight) {\n                shift[sink[u]!] = min(shift[sink[u]]!, x[v]! - x[u]! - gap);\n              } else {\n                shift[sink[u]!] = max(shift[sink[u]]!, x[v]! - x[u]! + gap);\n              }\n            } else {\n              /* v and u have the same sink, i.e. they are in the same level.\n              Make sure that v is separated from u by at least gap.*/\n              if (leftToRight) {\n                x[v] = max(x[v]!, x[u]! + gap);\n              } else {\n                x[v] = min(x[v]!, x[u]! - gap);\n              }\n            }\n          }\n          currentNode = align[currentNode]!;\n        } while (currentNode != v);\n      } catch (e) {\n        print(e);\n      }\n    }\n  }\n\n  List<Node> successorsOf(Node? node) {\n    return graph.successorsOf(node);\n  }\n\n  List<Node> predecessorsOf(Node? node) {\n    return graph.predecessorsOf(node);\n  }\n\n  List<Node> getAdjNodes(Node node, bool downward) {\n    if (downward) {\n      return predecessorsOf(node);\n    } else {\n      return successorsOf(node);\n    }\n  }\n\n  // predecessor;\n  Node? predecessor(Node? v, bool leftToRight) {\n    final pos = positionOfNode(v);\n    final rank = getLayerIndex(v);\n    final level = layers[rank];\n    if (leftToRight && pos != 0 || !leftToRight && pos != level.length - 1) {\n      return level[(leftToRight) ? pos - 1 : pos + 1];\n    } else {\n      return null;\n    }\n  }\n\n  Node? virtualTwinNode(Node node, bool downward) {\n    if (!isLongEdgeDummy(node)) {\n      return null;\n    }\n    final adjNodes = getAdjNodes(node, downward);\n    return adjNodes.isEmpty ? null : adjNodes[0];\n  }\n\n  // get node index in layer;\n  int positionOfNode(Node? node) {\n    return nodeData[node]?.position ?? -1;\n  }\n\n  int getLayerIndex(Node? node) {\n    return nodeData[node]?.layer ?? -1;\n  }\n\n  bool isLongEdgeDummy(Node? v) {\n    final successors = successorsOf(v);\n    return nodeData[v!]!.isDummy &&\n        successors.length == 1 &&\n        nodeData[successors[0]]!.isDummy;\n  }\n\n  void assignY() {\n    var k = layers.length;\n    var yPos = 0.0;\n    var vertical = isVertical();\n\n    for (var i = 0; i < k; i++) {\n      var level = layers[i];\n      var maxHeight = 0.0;\n\n      level.forEach((node) {\n        var h = nodeData[node]!.isDummy\n            ? 0.0\n            : vertical\n            ? node.height\n            : node.width;\n        if (h > maxHeight) {\n          maxHeight = h;\n        }\n        node.y = yPos;\n      });\n\n      if (i < k - 1) {\n        yPos += configuration.levelSeparation + maxHeight;\n      }\n    }\n  }\n\n  void denormalize() {\n    // Remove dummy vertices and create bend points for articulated edges\n    for (var i = 1; i < layers.length - 1; i++) {\n      final iterator = layers[i].iterator;\n\n      while (iterator.moveNext()) {\n        final current = iterator.current;\n        if (nodeData[current]!.isDummy) {\n          final predecessor = graph.predecessorsOf(current)[0];\n          final successor = graph.successorsOf(current)[0];\n          final bendPoints = _edgeData[graph.getEdgeBetween(predecessor, current)!]!.bendPoints;\n\n          if (bendPoints.isEmpty || !bendPoints.contains(current.x + predecessor.width / 2)) {\n            bendPoints.add(predecessor.x + predecessor.width / 2);\n            bendPoints.add(predecessor.y + predecessor.height / 2);\n            bendPoints.add(current.x + predecessor.width / 2);\n            bendPoints.add(current.y);\n          }\n\n          if (!nodeData[predecessor]!.isDummy) {\n            bendPoints.add(current.x + predecessor.width / 2);\n          } else {\n            bendPoints.add(current.x);\n          }\n          bendPoints.add(current.y);\n\n          if (nodeData[successor]!.isDummy) {\n            bendPoints.add(successor.x + predecessor.width / 2);\n          } else {\n            bendPoints.add(successor.x + successor.width / 2);\n          }\n          bendPoints.add(successor.y + successor.height / 2);\n\n          graph.removeEdgeFromPredecessor(predecessor, current);\n          graph.removeEdgeFromPredecessor(current, successor);\n\n          final edge = graph.addEdge(predecessor, successor);\n          final edgeData = EiglspergerEdgeData();\n          edgeData.bendPoints = bendPoints;\n          _edgeData[edge] = edgeData;\n\n          graph.removeNode(current);\n        }\n      }\n    }\n  }\n\n  void restoreCycle() {\n    graph.nodes.forEach((n) {\n      if (nodeData[n]!.isReversed) {\n        nodeData[n]!.reversed.forEach((target) {\n          final bendPoints = _edgeData[graph.getEdgeBetween(target, n)!]!.bendPoints;\n          graph.removeEdgeFromPredecessor(target, n);\n          final edge = graph.addEdge(n, target);\n\n          final edgeData = EiglspergerEdgeData();\n          edgeData.bendPoints = bendPoints;\n          _edgeData[edge] = edgeData;\n        });\n      }\n    });\n  }\n\n  Offset getOffset(Graph graph, bool needReverseOrder) {\n    var offsetX = double.infinity;\n    var offsetY = double.infinity;\n\n    if (needReverseOrder) {\n      offsetY = double.minPositive;\n    }\n\n    graph.nodes.forEach((node) {\n      if (needReverseOrder) {\n        offsetX = min(offsetX, node.x);\n        offsetY = max(offsetY, node.y);\n      } else {\n        offsetX = min(offsetX, node.x);\n        offsetY = min(offsetY, node.y);\n      }\n    });\n\n    return Offset(offsetX, offsetY);\n  }\n\n  Offset getPosition(Node node, Offset offset) {\n    Offset finalOffset;\n    switch (configuration.orientation) {\n      case 1:\n        finalOffset = Offset(node.x - offset.dx, node.y);\n        break;\n      case 2:\n        finalOffset = Offset(node.x - offset.dx, offset.dy - node.y);\n        break;\n      case 3:\n        finalOffset = Offset(node.y, node.x - offset.dx);\n        break;\n      case 4:\n        finalOffset = Offset(offset.dy - node.y, node.x - offset.dx);\n        break;\n      default:\n        finalOffset = Offset(0, 0);\n        break;\n    }\n\n    return finalOffset;\n  }\n\n  static double medianValue(List<int> positions) {\n    if (positions.isEmpty) return 0.0;\n    if (positions.length == 1) return positions[0].toDouble();\n\n    positions.sort();\n    final mid = positions.length ~/ 2;\n\n    if (positions.length % 2 == 1) {\n      return positions[mid].toDouble();\n    } else if (positions.length == 2) {\n      return (positions[0] + positions[1]) / 2.0;\n    } else {\n      final left = positions[mid - 1] - positions[0];\n      final right = positions[positions.length - 1] - positions[mid];\n      if (left + right == 0) return 0.0;\n      return (positions[mid - 1] * right + positions[mid] * left) / (left + right);\n    }\n  }\n\n  @override\n  void init(Graph? graph) {\n    this.graph = copyGraph(graph!);\n    reset();\n    initNodeData();\n    cycleRemoval();\n    layerAssignment();\n    nodeOrdering();\n    coordinateAssignment();\n    denormalize();\n    restoreCycle();\n  }\n\n  @override\n  void setDimensions(double width, double height) {\n    // Can be used to set layout bounds if needed\n  }\n}"
  },
  {
    "path": "lib/layered/SugiyamaAlgorithm.dart",
    "content": "part of graphview;\n\nclass SugiyamaAlgorithm extends Algorithm {\n  Map<Node, SugiyamaNodeData> nodeData = {};\n  Map<Edge, SugiyamaEdgeData> edgeData = {};\n  Set<Node> stack = {};\n  Set<Node> visited = {};\n  List<List<Node>> layers = [];\n  final type1Conflicts = <int, int>{};\n  late Graph graph;\n  SugiyamaConfiguration configuration;\n\n  @override\n  EdgeRenderer? renderer;\n\n  var nodeCount = 1;\n\n  SugiyamaAlgorithm(this.configuration) {\n    renderer = SugiyamaEdgeRenderer(nodeData, edgeData,\n        configuration.bendPointShape, configuration.addTriangleToEdge);\n  }\n\n  int get dummyId => 'Dummy ${nodeCount++}'.hashCode;\n\n  bool isVertical() {\n    var orientation = configuration.orientation;\n    return orientation == SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ||\n        orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP;\n  }\n\n  bool needReverseOrder() {\n    var orientation = configuration.orientation;\n    return orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP ||\n        orientation == SugiyamaConfiguration.ORIENTATION_RIGHT_LEFT;\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    this.graph = copyGraph(graph!);\n    reset();\n    initSugiyamaData();\n    cycleRemoval();\n    layerAssignment();\n    nodeOrdering(); //expensive operation\n    coordinateAssignment(); //expensive operation\n    // if (configuration.enableAngleOptimization) {\n    //   final optimizer = CrossingAngleOptimizer(this.graph, layers, nodeData, edgeData, configuration);\n    //   optimizer.optimize();\n    //   // The optimizer modifies the Y coordinates in place, so no need to call assignY() again.\n    // }\n    shiftCoordinates(shiftX, shiftY);\n    final graphSize = graph.calculateGraphSize();\n    denormalize();\n    restoreCycle();\n    return graphSize;\n  }\n\n  void shiftCoordinates(double shiftX, double shiftY) {\n    layers.forEach((List<Node?> arrayList) {\n      arrayList.forEach((it) {\n        it!.position = Offset(it.x + shiftX, it.y + shiftY);\n      });\n    });\n  }\n\n  void reset() {\n    layers.clear();\n    stack.clear();\n    visited.clear();\n    nodeData.clear();\n    edgeData.clear();\n    nodeCount = 1;\n  }\n\n  void initSugiyamaData() {\n    graph.nodes.forEach((node) {\n      node.position = Offset(0, 0);\n      nodeData[node] = SugiyamaNodeData(node.lineType);\n    });\n\n    graph.edges.forEach((edge) {\n      edgeData[edge] = SugiyamaEdgeData();\n    });\n\n  }\n\n  void dfs(Node node) {\n    if (visited.contains(node)) {\n      return;\n    }\n    visited.add(node);\n    stack.add(node);\n    graph.getOutEdges(node).toList().forEach((edge) {\n      final target = edge.destination;\n      if (stack.contains(target)) {\n        final storedData = edgeData.remove(edge);\n        graph.removeEdge(edge);\n        final reversedEdge = graph.addEdge(target, node);\n        edgeData[reversedEdge] = storedData ?? SugiyamaEdgeData();\n        nodeData[node]!.reversed.add(target);\n      } else {\n        dfs(target);\n      }\n    });\n    stack.remove(node);\n  }\n\n  void layerAssignment() {\n    switch (configuration.layeringStrategy) {\n      case LayeringStrategy.topDown:\n        layerAssignmentTopDown();\n        break;\n      case LayeringStrategy.longestPath:\n        layerAssignmentLongestPath();\n        break;\n      case LayeringStrategy.coffmanGraham:\n        layerAssignmentCoffmanGraham();\n        break;\n      case LayeringStrategy.networkSimplex:\n        layerAssignmentNetworkSimplex();\n        break;\n    }\n\n    // Add dummy nodes for long edges\n    addDummyNodes();\n  }\n\n  void layerAssignmentTopDown() {\n    if (graph.nodes.isEmpty) return;\n\n    final copiedGraph = copyGraph(graph);\n    var roots = getRootNodes(copiedGraph);\n\n    while (roots.isNotEmpty) {\n      layers.add(roots);\n      copiedGraph.removeNodes(roots);\n      roots = getRootNodes(copiedGraph);\n    }\n\n    // Set layer metadata\n    for (var i = 0; i < layers.length; i++) {\n      for (var j = 0; j < layers[i].length; j++) {\n        nodeData[layers[i][j]]!.layer = i;\n        nodeData[layers[i][j]]!.position = j;\n      }\n    }\n  }\n\n  void layerAssignmentLongestPath() {\n    if (graph.nodes.isEmpty) return;\n\n    var U = <Node>{};\n    var Z = <Node>{};\n    var V = Set<Node>.from(graph.nodes);\n    var currentLayer = 0;\n    layers = [[]];\n\n    while (U.length != graph.nodes.length) {\n      var candidates = V\n          .where((v) => !U.contains(v) && Z.containsAll(graph.successorsOf(v)));\n\n      if (candidates.isNotEmpty) {\n        var node = candidates.first;\n        layers[currentLayer].add(node);\n        U.add(node);\n      } else {\n        currentLayer++;\n        layers.add([]);\n        Z.addAll(U);\n      }\n    }\n\n    // Reverse layers and set metadata\n    layers = layers.reversed.where((layer) => layer.isNotEmpty).toList();\n    for (var i = 0; i < layers.length; i++) {\n      for (var j = 0; j < layers[i].length; j++) {\n        nodeData[layers[i][j]]!.layer = i;\n        nodeData[layers[i][j]]!.position = j;\n      }\n    }\n  }\n\n  void layerAssignmentCoffmanGraham() {\n    if (graph.nodes.isEmpty) return;\n\n    var width = (graph.nodes.length / 10).ceil();\n\n    var Z = <Node>{};\n    var lambda = <Node, int>{};\n    var V = Set<Node>.from(graph.nodes);\n\n    // Assign lambda values based on in-degree\n    V.forEach((v) => lambda[v] = double.maxFinite.toInt());\n    for (var i = 0; i < V.length; i++) {\n      var mv = V.where((v) => lambda[v] == double.maxFinite.toInt()).reduce(\n          (a, b) =>\n              graph.getInEdges(a).length <= graph.getInEdges(b).length ? a : b);\n      lambda[mv] = i;\n    }\n\n    var k = 0;\n    layers = [[]];\n    var U = <Node>{};\n\n    while (U.length != graph.nodes.length) {\n      var candidates = V\n          .where((v) => !U.contains(v) && U.containsAll(graph.successorsOf(v)));\n\n      if (candidates.isNotEmpty) {\n        var got = candidates.reduce((a, b) => lambda[a]! > lambda[b]! ? a : b);\n\n        if (layers[k].length < width &&\n            Z.containsAll(graph.successorsOf(got))) {\n          layers[k].add(got);\n        } else {\n          Z.addAll(layers[k]);\n          k++;\n          layers.add([]);\n          layers[k].add(got);\n        }\n        U.add(got);\n      }\n    }\n\n    // Remove empty layers and reverse\n    layers = layers.where((l) => l.isNotEmpty).toList().reversed.toList();\n\n    // Set metadata\n    for (var i = 0; i < layers.length; i++) {\n      for (var j = 0; j < layers[i].length; j++) {\n        nodeData[layers[i][j]]!.layer = i;\n        nodeData[layers[i][j]]!.position = j;\n      }\n    }\n  }\n\n  void layerAssignmentNetworkSimplex() {\n    // Start with longest path as base\n    layerAssignmentLongestPath();\n\n    // Simple optimization: try to minimize edge span\n    var improved = true;\n    var iterations = 5;\n\n    while (improved && iterations > 0) {\n      improved = false;\n      iterations--;\n\n      for (var i = layers.length - 1; i >= 0; i--) {\n        var layer = List<Node>.from(layers[i]);\n        var nodesToMove = <Node, int>{};\n\n        for (var v in layer) {\n          if (graph.getOutEdges(v).isEmpty) continue;\n\n          var outgoingEdges = graph.getOutEdges(v);\n          if (outgoingEdges.isNotEmpty) {\n            var minRank = outgoingEdges\n                .map((e) => nodeData[e.destination]!.layer - 1)\n                .reduce(min);\n\n            if (minRank != nodeData[v]!.layer && minRank >= 0) {\n              nodesToMove[v] = minRank;\n              improved = true;\n            }\n          }\n        }\n\n        // Move nodes\n        for (var entry in nodesToMove.entries) {\n          var node = entry.key;\n          var newRank = entry.value;\n          var oldRank = nodeData[node]!.layer;\n\n          layers[oldRank].remove(node);\n          if (newRank < layers.length) {\n            layers[newRank].add(node);\n            nodeData[node]!.layer = newRank;\n          }\n        }\n      }\n\n      // Recompute positions\n      for (var i = 0; i < layers.length; i++) {\n        for (var j = 0; j < layers[i].length; j++) {\n          nodeData[layers[i][j]]!.position = j;\n        }\n      }\n    }\n  }\n\n  void addDummyNodes() {\n    for (var i = 0; i < layers.length - 1; i++) {\n      var indexNextLayer = i + 1;\n      var currentLayer = layers[i];\n      var nextLayer = layers[indexNextLayer];\n\n      for (var node in currentLayer) {\n        final edges = graph.edges\n            .where((element) =>\n                element.source == node &&\n                (nodeData[element.destination]!.layer - nodeData[node]!.layer)\n                        .abs() >\n                    1)\n            .toList();\n\n        final iterator = edges.iterator;\n\n        while (iterator.moveNext()) {\n          final edge = iterator.current;\n          final dummy = Node.Id(dummyId.hashCode);\n          final dummyNodeData = SugiyamaNodeData(node.lineType);\n          dummyNodeData.isDummy = true;\n          dummyNodeData.layer = indexNextLayer;\n          nextLayer.add(dummy);\n          nodeData[dummy] = dummyNodeData;\n          dummy.size =\n              Size(edge.source.width, 0); // calc TODO avg layer height;\n          final dummyEdge1 = graph.addEdge(edge.source, dummy);\n          final dummyEdge2 = graph.addEdge(dummy, edge.destination);\n          edgeData[dummyEdge1] = SugiyamaEdgeData();\n          edgeData[dummyEdge2] = SugiyamaEdgeData();\n          graph.removeEdge(edge);\n//                    iterator.remove();\n        }\n      }\n    }\n  }\n\n  List<Node> getRootNodes(Graph graph) {\n    final predecessors = <Node, bool>{};\n    graph.edges.forEach((element) {\n      predecessors[element.destination] = true;\n    });\n\n    var roots = graph.nodes.where((node) => predecessors[node] == null);\n    roots.forEach((node) {\n      nodeData[node]?.layer = layers.length;\n    });\n\n    return roots.toList();\n  }\n\n  Graph copyGraph(Graph graph) {\n    final copy = Graph();\n    copy.addNodes(graph.nodes);\n    copy.addEdges(graph.edges);\n    return copy;\n  }\n\n  void nodeOrdering() {\n    // The `layers` variable is the member variable of the class.\n    // We will modify it directly. There is no need for a separate 'best' copy\n    // with the current iterative improvement strategy.\n\n    // Precalculate predecessor and successor info, must be done here after adding the dummy nodes\n    graph.edges.forEach((element) {\n      nodeData[element.source]?.successorNodes.add(element.destination);\n      nodeData[element.destination]?.predecessorNodes.add(element.source);\n    });\n\n    for (var i = 0; i < configuration.iterations; i++) {\n      // Apply the median heuristic to reorder nodes in each layer.\n      median(layers, i);\n\n      // Apply the transpose heuristic to fine-tune the ordering by swapping adjacent nodes.\n      // This will use the efficient AccumulatorTree-based approach we defined.\n      var changed = configuration.crossMinimizationStrategy ==\n              CrossMinimizationStrategy.simple\n          ? transposeSimple(layers)\n          : transposeAccumulator(layers);\n      // If a full pass of transpose made no improvements, we've stabilized.\n      if (!changed) {\n        break;\n      }\n    }\n\n    // Set final positions based on the optimized order.\n    for (var currentLayer in layers) {\n      for (var pos = 0; pos < currentLayer.length; pos++) {\n        nodeData[currentLayer[pos]]?.position = pos;\n      }\n    }\n  }\n\n  void median(List<List<Node?>> layers, int currentIteration) {\n    if (currentIteration % 2 == 0) {\n      for (var i = 1; i < layers.length; i++) {\n        var currentLayer = layers[i];\n        var previousLayer = layers[i - 1];\n\n        // get the positions of adjacent vertices in adj_rank\n        var positions = <int>[];\n        var pos = 0;\n        previousLayer.forEach((node) {\n          successorsOf(node).forEach((element) {\n            positions.add(pos);\n          });\n          pos++;\n        });\n        positions.sort();\n\n        // set the position in terms of median based on adjacent values\n        if (positions.isNotEmpty) {\n          var median = positions.length ~/ 2;\n\n          if (positions.length == 1) {\n            median = -1;\n          } else if (positions.length == 2) {\n            median = (positions[0] + positions[1]) ~/ 2;\n          } else if (positions.length % 2 == 1) {\n            median = positions[median];\n          } else {\n            final left = positions[median - 1] - positions[0];\n            final right = positions[positions.length - 1] - positions[median];\n            if (left + right != 0) {\n              median =\n                  (positions[median - 1] * right + positions[median] * left) ~/\n                      (left + right);\n            }\n          }\n\n          for (var node in currentLayer) {\n            nodeData[node!]!.median = median;\n          }\n        }\n\n        currentLayer\n            .sort((n1, n2) => nodeData[n1!]!.median - nodeData[n2!]!.median);\n      }\n    } else {\n      for (var l = 1; l < layers.length; l++) {\n        var currentLayer = layers[l];\n        var previousLayer = layers[l - 1];\n\n        var positions = <int>[];\n        var pos = 0;\n        previousLayer.forEach((node) {\n          successorsOf(node).forEach((element) {\n            positions.add(pos);\n          });\n          pos++;\n        });\n        positions.sort();\n\n        if (positions.isNotEmpty) {\n          var median = 0;\n\n          if (positions.length == 1) {\n            median = positions[0];\n          } else {\n            median = (positions[(positions.length / 2.0).ceil()] +\n                    positions[(positions.length / 2.0).ceil() - 1]) ~/\n                2;\n          }\n\n          for (var i = currentLayer.length - 1; i > 1; i--) {\n            final node = currentLayer[i];\n            nodeData[node!]!.median = median;\n          }\n        }\n\n        currentLayer\n            .sort((n1, n2) => nodeData[n1!]!.median - nodeData[n2!]!.median);\n      }\n    }\n  }\n\n  bool transposeSimple(List<List<Node>> layers) {\n    var changed = false;\n    var improved = true;\n\n    while (improved) {\n      improved = false;\n      for (var l = 0; l < layers.length - 1; l++) {\n        final northernNodes = layers[l];\n        final southernNodes = layers[l + 1];\n\n        // Create a map that holds the index of every [Node]. Key is the [Node] and value is the index of the item.\n        final indexMap = HashMap.of(\n            northernNodes.asMap().map((key, value) => MapEntry(value, key)));\n\n        for (var i = 0; i < southernNodes.length - 1; i++) {\n          final v = southernNodes[i];\n          final w = southernNodes[i + 1];\n          if (crossingCount(indexMap, v, w) > crossingCount(indexMap, w, v)) {\n            improved = true;\n            exchange(southernNodes, v, w);\n            changed = true;\n          }\n        }\n      }\n    }\n    return changed;\n  }\n\n  bool transposeAccumulator(List<List<Node>> layers) {\n    var changed = false;\n    var improved = true;\n\n    while (improved) {\n      improved = false;\n      for (var l = 0; l < layers.length - 1; l++) {\n        final upperLayer = layers[l];\n        final lowerLayer = layers[l + 1];\n\n        // Calculate the total crossings for this pair of layers before any swaps.\n        var crossingsBefore = _getBiLayerCrossings(upperLayer, lowerLayer);\n        if (crossingsBefore == 0) continue;\n\n        for (var i = 0; i < lowerLayer.length - 1; i++) {\n          final v = lowerLayer[i];\n          final w = lowerLayer[i + 1];\n\n          // Perform a trial swap\n          exchange(lowerLayer, v, w);\n\n          // Recalculate total crossings with the more efficient method.\n          var crossingsAfter = _getBiLayerCrossings(upperLayer, lowerLayer);\n\n          if (crossingsAfter < crossingsBefore) {\n            // The swap was good, keep it.\n            improved = true;\n            changed = true;\n            crossingsBefore =\n                crossingsAfter; // Update the baseline crossing count\n          } else {\n            // The swap was not beneficial, revert it.\n            exchange(lowerLayer, w, v);\n          }\n        }\n      }\n    }\n    return changed;\n  }\n\n  /// Calculates the number of crossings between two specific layers using the AccumulatorTree.\n  int _getBiLayerCrossings(List<Node> upperLayer, List<Node> lowerLayer) {\n    if (upperLayer.isEmpty || lowerLayer.isEmpty) {\n      return 0;\n    }\n\n    // Update positions in nodeData based on the current list order.\n    // This is crucial as the transpose function modifies the list directly.\n    for (var i = 0; i < lowerLayer.length; i++) {\n      nodeData[lowerLayer[i]]!.position = i;\n    }\n\n    var targetIndices = <int>[];\n    // Ensure upper layer nodes are sorted by their original position to maintain a stable sort.\n    var sortedUpperLayer = List<Node>.from(upperLayer)\n      ..sort((a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position));\n\n    for (var source in sortedUpperLayer) {\n      var successors = successorsOf(source)\n          .where((succ) => lowerLayer.contains(succ))\n          .toList()\n        ..sort(\n            (a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position));\n\n      for (var successor in successors) {\n        targetIndices.add(nodeData[successor]!.position);\n      }\n    }\n\n    if (targetIndices.isNotEmpty) {\n      var maxIndex = targetIndices.reduce(max);\n      var accumTree = AccumulatorTree(maxIndex + 1);\n      return accumTree.crossCount(targetIndices);\n    }\n\n    return 0;\n  }\n\n  void exchange(List<Node> nodes, Node v, Node w) {\n    var i = nodes.indexOf(v);\n    var j = nodes.indexOf(w);\n    var temp = nodes[i];\n    nodes[i] = nodes[j];\n    nodes[j] = temp;\n  }\n\n  // counts the number of edge crossings if n2 appears to the left of n1 in their layer.;\n  int crossingCount(HashMap<Node, int> northernNodes, Node? n1, Node? n2) {\n    final indexOf = (Node node) => northernNodes[node]!;\n    var crossing = 0;\n    final parentNodesN1 = graph.predecessorsOf(n1);\n    final parentNodesN2 = graph.predecessorsOf(n2);\n    parentNodesN2.forEach((pn2) {\n      final indexOfPn2 = indexOf(pn2);\n      parentNodesN1.where((it) => indexOfPn2 < indexOf(it)).forEach((element) {\n        crossing++;\n      });\n    });\n\n    return crossing;\n  }\n\n  int crossing(List<List<Node>> layers) {\n    var crossinga = 0;\n\n    for (var l = 0; l < layers.length - 1; l++) {\n      final southernNodes = layers[l];\n      final northernNodes = layers[l + 1];\n\n      final indexMap = HashMap.of(\n          northernNodes.asMap().map((key, value) => MapEntry(value, key)));\n\n      for (var i = 0; i < southernNodes.length - 2; i++) {\n        final v = southernNodes[i];\n        final w = southernNodes[i + 1];\n\n        crossinga += crossingCount(indexMap, v, w);\n      }\n    }\n    return crossinga;\n  }\n\n  void coordinateAssignment() {\n    assignX();\n    assignY();\n    var offset = getOffset(graph, needReverseOrder());\n\n    graph.nodes.forEach((v) {\n      v.position = getPosition(v, offset);\n    });\n\n    if (configuration.postStraighten) {\n      postStraighten();\n    }\n  }\n\n  void assignX() {\n    // Existing implementation remains the same\n    final root = <Map<Node, Node>>[];\n    // each node points to its aligned neighbor in the layer below.;\n    final align = <Map<Node, Node>>[];\n    final sink = <Map<Node, Node>>[];\n    final x = <Map<Node, double>>[];\n    // minimal separation between the roots of different classes.;\n    final shift = <Map<Node, double>>[];\n    // the width of each block (max width of node in block);\n    final blockWidth = <Map<Node, double>>[];\n\n    for (var i = 0; i < 4; i++) {\n      root.add({});\n      align.add({});\n      sink.add({});\n      shift.add({});\n      x.add({});\n      blockWidth.add({});\n\n      graph.nodes.forEach((n) {\n        root[i][n] = n;\n        align[i][n] = n;\n        sink[i][n] = n;\n        shift[i][n] = double.infinity;\n        x[i][n] = double.negativeInfinity;\n        blockWidth[i][n] = 0;\n      });\n    }\n    var separation = configuration.nodeSeparation;\n\n    var vertical = isVertical();\n    for (var downward = 0; downward <= 1; downward++) {\n      var isDownward = downward == 0;\n      final type1Conflicts = markType1Conflicts(isDownward);\n      for (var leftToRight = 0; leftToRight <= 1; leftToRight++) {\n        final k = 2 * downward + leftToRight;\n        var isLeftToRight = leftToRight == 0;\n        verticalAlignment(\n            root[k], align[k], type1Conflicts, isDownward, isLeftToRight);\n        graph.nodes.forEach((v) {\n          final r = root[k][v]!;\n          blockWidth[k][r] = max(\n              blockWidth[k][r]!, vertical ? v.width + separation : v.height);\n        });\n        horizontalCompactation(align[k], root[k], sink[k], shift[k],\n            blockWidth[k], x[k], isLeftToRight, isDownward, layers, separation);\n      }\n    }\n\n    balance(x, blockWidth);\n  }\n\n  void balance(List<Map<Node, double>> x, List<Map<Node?, double>> blockWidth) {\n    final coordinates = <Node, double>{};\n\n    switch (configuration.coordinateAssignment) {\n      case CoordinateAssignment.Average:\n        var minWidth = double.infinity;\n\n        var smallestWidthLayout = 0;\n        final minArray = List.filled(4, 0.0);\n        final maxArray = List.filled(4, 0.0);\n\n        // Get the layout with the smallest width and set minimum and maximum value for each direction;\n        for (var i = 0; i < 4; i++) {\n          minArray[i] = double.infinity;\n          maxArray[i] = 0;\n\n          graph.nodes.forEach((v) {\n            final bw = 0.5 * blockWidth[i][v]!;\n            var xp = x[i][v]! - bw;\n            if (xp < minArray[i]) {\n              minArray[i] = xp;\n            }\n            xp = x[i][v]! + bw;\n            if (xp > maxArray[i]) {\n              maxArray[i] = xp;\n            }\n          });\n\n          final width = maxArray[i] - minArray[i];\n          if (width < minWidth) {\n            minWidth = width;\n            smallestWidthLayout = i;\n          }\n        }\n\n        // Align the layouts to the one with the smallest width\n        for (var layout = 0; layout < 4; layout++) {\n          if (layout != smallestWidthLayout) {\n            // Align the left to right layouts to the left border of the smallest layout\n            var diff = 0.0;\n            if (layout < 2) {\n              diff = minArray[layout] - minArray[smallestWidthLayout];\n            } else {\n              // Align the right to left layouts to the right border of the smallest layout\n              diff = maxArray[layout] - maxArray[smallestWidthLayout];\n            }\n            if (diff > 0) {\n              x[layout].keys.forEach((n) {\n                x[layout][n] = x[layout][n]! - diff;\n              });\n            } else {\n              x[layout].keys.forEach((n) {\n                x[layout][n] = x[layout][n]! + diff;\n              });\n            }\n          }\n        }\n\n        // Get the average median of each coordinate\n        var values = List.filled(4, 0.0);\n        graph.nodes.forEach((n) {\n          for (var i = 0; i < 4; i++) {\n            values[i] = x[i][n]!;\n          }\n          values.sort();\n          var average = (values[1] + values[2]) * 0.5;\n          coordinates[n] = average;\n        });\n        break;\n      case CoordinateAssignment.DownRight:\n        graph.nodes.forEach((n) {\n          coordinates[n] = x[0][n] ?? 0.0;\n        });\n        break;\n      case CoordinateAssignment.DownLeft:\n        graph.nodes.forEach((n) {\n          coordinates[n] = x[1][n] ?? 0.0;\n        });\n        break;\n      case CoordinateAssignment.UpRight:\n        graph.nodes.forEach((n) {\n          coordinates[n] = x[2][n] ?? 0.0;\n        });\n        break;\n      case CoordinateAssignment.UpLeft:\n        graph.nodes.forEach((n) {\n          coordinates[n] = x[3][n] ?? 0.0;\n        });\n        break;\n    }\n\n    if (coordinates.isEmpty) {\n      for (final node in graph.nodes) {\n        coordinates[node] = 0.0;\n      }\n    }\n\n    // Get the minimum coordinate value\n    var minValue = coordinates.values.reduce(min);\n\n    // Set left border to 0\n    if (minValue != 0) {\n      coordinates.keys.forEach((n) {\n        coordinates[n] = coordinates[n]! - minValue;\n      });\n    }\n\n    resolveOverlaps(coordinates);\n\n    graph.nodes.forEach((v) {\n      v.x = coordinates[v]!;\n    });\n  }\n\n  void resolveOverlaps(Map<Node, double> coordinates) {\n    for (var layer in layers) {\n      if (layer.isEmpty) {\n        continue;\n      }\n\n      var layerNodes = List<Node>.from(layer);\n      layerNodes.sort(\n          (a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position));\n\n      var data = nodeData[layerNodes.first];\n      if (data?.layer != 0) {\n        var leftCoordinate = 0.0;\n        for (var i = 1; i < layerNodes.length; i++) {\n          var currentNode = layerNodes[i];\n          if (!nodeData[currentNode]!.isDummy) {\n            var previousNode = getPreviousNonDummyNode(layerNodes, i);\n\n            if (previousNode != null) {\n              leftCoordinate = coordinates[previousNode]! +\n                  previousNode.width +\n                  configuration.nodeSeparation;\n            } else {\n              leftCoordinate = 0.0;\n            }\n\n            if (leftCoordinate > coordinates[currentNode]!) {\n              var adjustment = leftCoordinate - coordinates[currentNode]!;\n              if (coordinates[currentNode] != null) {\n                coordinates[currentNode] =\n                    coordinates[currentNode]! + adjustment;\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  Node? getPreviousNonDummyNode(List<Node> layerNodes, int currentIndex) {\n    for (var i = currentIndex - 1; i >= 0; i--) {\n      var previousNode = layerNodes[i];\n      if (!nodeData[previousNode]!.isDummy) {\n        return previousNode;\n      }\n    }\n    return null;\n  }\n\n  Map<int, int> markType1Conflicts(bool downward) {\n    if (layers.length >= 4) {\n      int upper;\n      int lower; // iteration bounds;\n      int k1; // node position boundaries of closest inner segments;\n      if (downward) {\n        lower = 1;\n        upper = layers.length - 2;\n      } else {\n        lower = layers.length - 1;\n        upper = 2;\n      }\n      /*;\n             * iterate level[2..h-2] in the given direction;\n             * available 1 levels to h;\n             */\n      for (var i = lower;\n          downward ? i <= upper : i >= upper;\n          i += downward ? 1 : -1) {\n        var k0 = 0;\n        var firstIndex = 0; // index of first node on layer;\n        final currentLevel = layers[i];\n        final nextLevel = downward ? layers[i + 1] : layers[i - 1];\n\n        // for all nodes on next level;\n        for (var l1 = 0; l1 < nextLevel.length; l1++) {\n          final virtualTwin = virtualTwinNode(nextLevel[l1], downward);\n\n          if (l1 == nextLevel.length - 1 || virtualTwin != null) {\n            k1 = currentLevel.length - 1;\n\n            if (virtualTwin != null) {\n              k1 = positionOfNode(virtualTwin);\n            }\n\n            while (firstIndex <= l1) {\n              final upperNeighbours = getAdjNodes(nextLevel[l1], downward);\n\n              for (var currentNeighbour in upperNeighbours) {\n                /*;\n                *  XXX< 0 in first iteration is still ok for indizes starting;\n                * with 0 because no index can be smaller than 0;\n                 */\n                final currentNeighbourIndex = positionOfNode(currentNeighbour);\n\n                if (currentNeighbourIndex < k0 || currentNeighbourIndex > k1) {\n                  type1Conflicts[l1] = currentNeighbourIndex;\n                }\n              }\n              firstIndex++;\n            }\n\n            k0 = k1;\n          }\n        }\n      }\n    }\n    return type1Conflicts;\n  }\n\n  void verticalAlignment(Map<Node?, Node?> root, Map<Node?, Node?> align,\n      Map<int, int> type1Conflicts, bool downward, bool leftToRight) {\n    // for all Level;\n\n    var layersa = downward ? layers : layers.reversed;\n\n    for (var layer in layersa) {\n      // As with layers, we need a reversed iterator for blocks for different directions\n      var nodes = leftToRight ? layer : layer.reversed;\n      // Do an initial placement for all blocks\n      var r = leftToRight ? -1 : double.infinity;\n      for (var v in nodes) {\n        final adjNodes = getAdjNodes(v, downward);\n        if (adjNodes.isNotEmpty) {\n          var midLevelValue = adjNodes.length / 2;\n          // Calculate medians\n          final medians = adjNodes.length % 2 == 1\n              ? [adjNodes[midLevelValue.floor()]]\n              : [\n                  adjNodes[midLevelValue.toInt() - 1],\n                  adjNodes[midLevelValue.toInt()]\n                ];\n\n          // For all median neighbours in direction of H\n          for (var m in medians) {\n            final posM = positionOfNode(m);\n            // if segment (u,v) not marked by type1 conflicts AND ...;\n            if (align[v] == v &&\n                type1Conflicts[positionOfNode(v)] != posM &&\n                (leftToRight ? r < posM : r > posM)) {\n              align[m] = v;\n              root[v] = root[m];\n              align[v] = root[v];\n              r = posM;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  void horizontalCompactation(\n      Map<Node, Node> align,\n      Map<Node, Node> root,\n      Map<Node, Node> sink,\n      Map<Node, double> shift,\n      Map<Node, double> blockWidth,\n      Map<Node, double> x,\n      bool leftToRight,\n      bool downward,\n      List<List<Node>> layers,\n      int separation) {\n    // calculate class relative coordinates for all roots;\n    // If the layers are traversed from right to left, a reverse iterator is needed (note that this does not change the original list of layers)\n    var layersa = leftToRight ? layers : layers.reversed;\n\n    for (var layer in layersa) {\n      // As with layers, we need a reversed iterator for blocks for different directions\n      var nodes = downward ? layer : layer.reversed;\n      // Do an initial placement for all blocks\n      for (var v in nodes) {\n        if (root[v] == v) {\n          placeBlock(v, sink, shift, x, align, blockWidth, root, leftToRight,\n              layers, separation);\n        }\n      }\n    }\n\n    var d = 0;\n    var i = downward ? 0 : layers.length - 1;\n    while (downward && i <= layers.length - 1 || !downward && i >= 0) {\n      final currentLevel = layers[i];\n      final v = currentLevel[leftToRight ? 0 : currentLevel.length - 1];\n      if (v == sink[root[v]]) {\n        final oldShift = shift[v]!;\n        if (oldShift < double.infinity) {\n          shift[v] = oldShift + d;\n          d += oldShift.toInt();\n        } else {\n          shift[v] = 0;\n        }\n      }\n      i = downward ? i + 1 : i - 1;\n    }\n\n    // apply root coordinates for all aligned nodes;\n    // (place block did this only for the roots)+;\n    graph.nodes.forEach((v) {\n      x[v] = x[root[v]]!;\n      final shiftVal = shift[sink[root[v]]]!;\n      if (shiftVal < double.infinity) {\n        x[v] = x[v]! + shiftVal; // apply shift for each class;\n      }\n    });\n  }\n\n  void placeBlock(\n      Node v,\n      Map<Node, Node> sink,\n      Map<Node, double> shift,\n      Map<Node, double> x,\n      Map<Node, Node> align,\n      Map<Node, double> blockWidth,\n      Map<Node, Node> root,\n      bool leftToRight,\n      List<List<Node>> layers,\n      int separation) {\n    if (x[v] == double.negativeInfinity) {\n      x[v] = 0;\n      var currentNode = v;\n\n      try {\n        do {\n          // if not first node on layer;\n          final hasPredecessor =\n              leftToRight && positionOfNode(currentNode) > 0 ||\n                  !leftToRight &&\n                      positionOfNode(currentNode) <\n                          layers[getLayerIndex(currentNode)].length - 1;\n          // print(\"Pred  $hasPredecessor ${getLayerIndex(currentNode)>0} ${positionOfNode(currentNode)>0}\");\n          if (hasPredecessor) {\n            final pred = predecessor(currentNode, leftToRight);\n            /* Get the root of u (proceeding all the way upwards in the block) */\n            final u = root[pred]!;\n            /* Place the block of u recursively */\n            placeBlock(u, sink, shift, x, align, blockWidth, root, leftToRight,\n                layers, separation);\n            /* If v is its own sink yet, set its sink to the sink of u */\n            if (sink[v] == v) {\n              sink[v] = sink[u]!;\n            }\n            /* If v and u have different sinks (i.e. they are in different classes),\n             * shift the sink of u so that the two blocks are separated by the preferred gap  */\n            var gap = separation + 0.5 * (blockWidth[u]! + blockWidth[v]!);\n            if (sink[v] != sink[u]) {\n              if (leftToRight) {\n                shift[sink[u]!] = min(shift[sink[u]]!, x[v]! - x[u]! - gap);\n              } else {\n                shift[sink[u]!] = max(shift[sink[u]]!, x[v]! - x[u]! + gap);\n              }\n            } else {\n              /* v and u have the same sink, i.e. they are in the same level.\n              Make sure that v is separated from u by at least gap.*/\n              if (leftToRight) {\n                x[v] = max(x[v]!, x[u]! + gap);\n              } else {\n                x[v] = min(x[v]!, x[u]! - gap);\n              }\n            }\n          }\n          currentNode = align[currentNode]!;\n        } while (currentNode != v);\n      } catch (e) {\n        print(e);\n      }\n    }\n  }\n\n  List<Node> successorsOf(Node? node) {\n    return nodeData[node]?.successorNodes ?? [];\n  }\n\n  List<Node> predecessorsOf(Node? node) {\n    return nodeData[node]?.predecessorNodes ?? [];\n  }\n\n  List<Node> getAdjNodes(Node node, bool downward) {\n    if (downward) {\n      return predecessorsOf(node);\n    } else {\n      return successorsOf(node);\n    }\n  }\n\n  // predecessor;\n  Node? predecessor(Node? v, bool leftToRight) {\n    final pos = positionOfNode(v);\n    final rank = getLayerIndex(v);\n    final level = layers[rank];\n    if (leftToRight && pos != 0 || !leftToRight && pos != level.length - 1) {\n      return level[(leftToRight) ? pos - 1 : pos + 1];\n    } else {\n      return null;\n    }\n  }\n\n  Node? virtualTwinNode(Node node, bool downward) {\n    if (!isLongEdgeDummy(node)) {\n      return null;\n    }\n    final adjNodes = getAdjNodes(node, downward);\n    return adjNodes.isEmpty ? null : adjNodes[0];\n  }\n\n  // get node index in layer;\n  int positionOfNode(Node? node) {\n    return nodeData[node]?.position ?? -1;\n  }\n\n  int getLayerIndex(Node? node) {\n    return nodeData[node]?.layer ?? -1;\n  }\n\n  bool isLongEdgeDummy(Node? v) {\n    final successors = successorsOf(v);\n    return nodeData[v!]!.isDummy &&\n        successors.length == 1 &&\n        nodeData[successors[0]]!.isDummy;\n  }\n\n  void assignY() {\n    // compute y-coordinates;\n    final k = layers.length;\n\n    // assign y-coordinates\n    var yPos = 0.0;\n    var vertical = isVertical();\n    for (var i = 0; i < k; i++) {\n      var level = layers[i];\n      var maxHeight = 0;\n      level.forEach((node) {\n        var h = nodeData[node]!.isDummy\n            ? 0\n            : vertical\n                ? node.height\n                : node.width;\n        if (h > maxHeight) {\n          maxHeight = h.toInt();\n        }\n        node.y = yPos;\n      });\n\n      if (i < k - 1) {\n        yPos += configuration.levelSeparation + maxHeight;\n      }\n    }\n  }\n\n  void denormalize() {\n    // remove dummy's;\n    for (var i = 1; i < layers.length - 1; i++) {\n      final iterator = layers[i].iterator;\n\n      while (iterator.moveNext()) {\n        final current = iterator.current;\n        if (nodeData[current]!.isDummy) {\n          final predecessor = graph.predecessorsOf(current)[0];\n          final successor = graph.successorsOf(current)[0];\n          final bendPoints =\n              edgeData[graph.getEdgeBetween(predecessor, current)!]!.bendPoints;\n\n          if (bendPoints.isEmpty ||\n              !bendPoints.contains(current.x + predecessor.width / 2)) {\n            bendPoints.add(predecessor.x + predecessor.width / 2);\n            bendPoints.add(predecessor.y + predecessor.height / 2);\n            bendPoints.add(current.x + predecessor.width / 2);\n            bendPoints.add(current.y);\n          }\n          if (!nodeData[predecessor]!.isDummy) {\n            bendPoints.add(current.x + predecessor.width / 2);\n          } else {\n            bendPoints.add(current.x);\n          }\n          bendPoints.add(current.y);\n          if (nodeData[successor]!.isDummy) {\n            bendPoints.add(successor.x + predecessor.width / 2);\n          } else {\n            bendPoints.add(successor.x + successor.width / 2);\n          }\n          bendPoints.add(successor.y + successor.height / 2);\n          graph.removeEdgeFromPredecessor(predecessor, current);\n          graph.removeEdgeFromPredecessor(current, successor);\n\n          final edge = graph.addEdge(predecessor, successor);\n          final sugiyamaEdgeData = SugiyamaEdgeData();\n          sugiyamaEdgeData.bendPoints = bendPoints;\n          edgeData[edge] = sugiyamaEdgeData;\n\n//          iterator.remove();\n          graph.removeNode(current);\n        }\n      }\n    }\n  }\n\n  void restoreCycle() {\n    graph.nodes.forEach((n) {\n      final nodeInfo = nodeData[n];\n      if (nodeInfo == null || !nodeInfo.isReversed) {\n        return;\n      }\n\n      for (final target in nodeInfo.reversed.toList()) {\n        final existingEdge = graph.getEdgeBetween(target, n);\n        if (existingEdge == null) {\n          continue;\n        }\n        final existingData = this.edgeData.remove(existingEdge);\n        final bendPoints = existingData?.bendPoints ?? <double>[];\n        graph.removeEdgeFromPredecessor(target, n);\n        final edge = graph.addEdge(n, target);\n\n        final restoredData = existingData ?? SugiyamaEdgeData();\n        restoredData.bendPoints = bendPoints;\n        this.edgeData[edge] = restoredData;\n      }\n\n      nodeInfo.reversed.clear();\n    });\n  }\n\n  void cycleRemoval() {\n    switch (configuration.cycleRemovalStrategy) {\n      case CycleRemovalStrategy.dfs:\n        _dfsRecursiveCycleRemoval();\n        break;\n      case CycleRemovalStrategy.greedy:\n        _greedyCycleRemoval();\n        break;\n    }\n  }\n\n  void _dfsRecursiveCycleRemoval() {\n    graph.nodes.forEach((node) {\n      dfs(node);\n    });\n  }\n\n  void _greedyCycleRemoval() {\n    var greedyRemoval = GreedyCycleRemoval(graph);\n    var feedbackArcs = greedyRemoval.getFeedbackArcs();\n\n    for (var edge in feedbackArcs) {\n      var source = edge.source;\n      var target = edge.destination;\n      final storedData = edgeData.remove(edge);\n      graph.removeEdge(edge);\n      final reversedEdge = graph.addEdge(target, source);\n      edgeData[reversedEdge] = storedData ?? SugiyamaEdgeData();\n      nodeData[source]!.reversed.add(target);\n    }\n  }\n\n  void postStraighten() {\n    if (!configuration.postStraighten) return;\n\n    // Align dummy vertices to create straighter edges\n    var dummyNodes = <Node>[];\n    for (var layer in layers) {\n      dummyNodes.addAll(layer.where((n) => nodeData[n]!.isDummy));\n    }\n\n    // Group dummy nodes by their original edge\n    var edgeGroups = <List<Node>>[];\n    var processed = <Node>{};\n\n    for (var dummy in dummyNodes) {\n      if (processed.contains(dummy)) continue;\n\n      var group = <Node>[dummy];\n      processed.add(dummy);\n\n      // Find connected dummy nodes (same edge)\n      _findConnectedDummies(dummy, group, processed, dummyNodes);\n\n      if (group.length > 1) {\n        edgeGroups.add(group);\n      }\n    }\n\n    // Align each group vertically\n    for (var group in edgeGroups) {\n      group.sort((a, b) => nodeData[a]!.layer.compareTo(nodeData[b]!.layer));\n\n      // Calculate average x position\n      var avgX = group.map((n) => n.x).reduce((a, b) => a + b) / group.length;\n\n      // Set all dummy nodes to average x\n      for (var node in group) {\n        node.x = avgX;\n      }\n    }\n  }\n\n  void _findConnectedDummies(\n      Node current, List<Node> group, Set<Node> processed, List<Node> dummies) {\n    var successors = successorsOf(current);\n    var predecessors = predecessorsOf(current);\n\n    for (var target in successors) {\n      if (dummies.contains(target) && !processed.contains(target)) {\n        group.add(target);\n        processed.add(target);\n        _findConnectedDummies(target, group, processed, dummies);\n      }\n    }\n\n    for (var source in predecessors) {\n      if (dummies.contains(source) && !processed.contains(source)) {\n        group.add(source);\n        processed.add(source);\n        _findConnectedDummies(source, group, processed, dummies);\n      }\n    }\n  }\n\n  Offset getOffset(Graph graph, bool needReverseOrder) {\n    var offsetX = double.infinity;\n    var offsetY = double.infinity;\n\n    if (needReverseOrder) {\n      offsetY = double.minPositive;\n    }\n\n    graph.nodes.forEach((node) {\n      if (needReverseOrder) {\n        offsetX = min(offsetX, node.x);\n        offsetY = max(offsetY, node.y);\n      } else {\n        offsetX = min(offsetX, node.x);\n        offsetY = min(offsetY, node.y);\n      }\n    });\n\n    return Offset(offsetX, offsetY);\n  }\n\n  Offset getPosition(Node node, Offset offset) {\n    Offset finalOffset;\n    switch (configuration.orientation) {\n      case 1:\n        finalOffset = Offset(node.x - offset.dx, node.y);\n        break;\n      case 2:\n        finalOffset = Offset(node.x - offset.dx, offset.dy - node.y);\n        break;\n      case 3:\n        finalOffset = Offset(node.y, node.x - offset.dx);\n        break;\n      case 4:\n        finalOffset = Offset(offset.dy - node.y, node.x - offset.dx);\n        break;\n      default:\n        finalOffset = Offset(0, 0);\n        break;\n    }\n\n    return finalOffset;\n  }\n\n  @override\n  void init(Graph? graph) {\n    this.graph = copyGraph(graph!);\n    reset();\n    initSugiyamaData();\n    cycleRemoval();\n    layerAssignment();\n    nodeOrdering(); //expensive operation\n    coordinateAssignment(); //expensive operation\n    // shiftCoordinates(shiftX, shiftY);\n    //final graphSize = calculateGraphSize(this.graph);\n    denormalize();\n    restoreCycle();\n    // shiftCoordinates(graph, shiftX, shiftY);\n  }\n\n  @override\n  void setDimensions(double width, double height) {\n    // graphWidth = width;\n    // graphHeight = height;\n  }\n}\n\nclass AccumulatorTree {\n  late List<int> tree;\n  late int firstIndex;\n  late int treeSize;\n  late int base;\n  late int last;\n\n  AccumulatorTree(int size) {\n    firstIndex = 1;\n    while (firstIndex < size) {\n      firstIndex *= 2;\n    }\n    treeSize = 2 * firstIndex - 1;\n    firstIndex--;\n    base = size - 1;\n    last = size - 1;\n    tree = List.filled(treeSize, 0);\n  }\n\n  int crossCount(List<int> southSequence) {\n    var crossCount = 0;\n    for (var k = 0; k < southSequence.length; k++) {\n      var index = southSequence[k] + firstIndex;\n      tree[index]++;\n      while (index > 0) {\n        if (index % 2 != 0) {\n          crossCount += tree[index + 1];\n        }\n        index = (index - 1) ~/ 2;\n        tree[index]++;\n      }\n    }\n    return crossCount;\n  }\n}\n\nclass GreedyCycleRemoval {\n  final Graph graph;\n  final Set<Edge> feedbackArcs = {};\n\n  GreedyCycleRemoval(this.graph);\n\n  Set<Edge> getFeedbackArcs() {\n    var copy = _copyGraph();\n    _removeCycles(copy);\n    return feedbackArcs;\n  }\n\n  Graph _copyGraph() {\n    var copy = Graph();\n    copy.addNodes(graph.nodes);\n    copy.addEdges(graph.edges);\n    return copy;\n  }\n\n  void _removeCycles(Graph g) {\n    while (g.nodes.isNotEmpty) {\n      // Remove sinks\n      var sinks = g.nodes.where((n) => !g.hasSuccessor(n)).toList();\n      if (sinks.isNotEmpty) {\n        for (var sink in sinks) {\n          g.removeNode(sink);\n        }\n        continue;\n      }\n\n      // Remove sources\n      var sources = g.nodes.where((n) => !g.hasPredecessor(n)).toList();\n      if (sources.isNotEmpty) {\n        for (var source in sources) {\n          g.removeNode(source);\n        }\n        continue;\n      }\n\n      // Choose nodes with highest out-degree - in-degree\n      var best = g.nodes.reduce((a, b) {\n        var aDiff = g.getOutEdges(a).length - g.getInEdges(a).length;\n        var bDiff = g.getOutEdges(b).length - g.getInEdges(b).length;\n        return aDiff > bDiff ? a : b;\n      });\n\n      // Add incoming edges to feedback arcs\n      feedbackArcs.addAll(g.getInEdges(best));\n      g.removeNode(best);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/layered/SugiyamaConfiguration.dart",
    "content": "part of graphview;\n\nclass SugiyamaConfiguration {\n  static const ORIENTATION_TOP_BOTTOM = 1;\n  static const ORIENTATION_BOTTOM_TOP = 2;\n  static const ORIENTATION_LEFT_RIGHT = 3;\n  static const ORIENTATION_RIGHT_LEFT = 4;\n  static const DEFAULT_ORIENTATION = 1;\n  static const int DEFAULT_ITERATIONS = 10;\n\n  static const int X_SEPARATION = 100;\n  static const int Y_SEPARATION = 100;\n\n  int levelSeparation = Y_SEPARATION;\n  int nodeSeparation = X_SEPARATION;\n  int orientation = DEFAULT_ORIENTATION;\n  int iterations = DEFAULT_ITERATIONS;\n  BendPointShape bendPointShape = SharpBendPointShape();\n  CoordinateAssignment coordinateAssignment = CoordinateAssignment.Average;\n\n  LayeringStrategy layeringStrategy = LayeringStrategy.topDown;\n  CrossMinimizationStrategy crossMinimizationStrategy = CrossMinimizationStrategy.simple;\n  CycleRemovalStrategy cycleRemovalStrategy = CycleRemovalStrategy.greedy;\n\n  bool postStraighten = true;\n\n  bool addTriangleToEdge = true;\n\n  int getLevelSeparation() {\n    return levelSeparation;\n  }\n\n  int getNodeSeparation() {\n    return nodeSeparation;\n  }\n\n  int getOrientation() {\n    return orientation;\n  }\n}\n\nenum CoordinateAssignment {\n  DownRight, // 0\n  DownLeft, // 1\n  UpRight, // 2\n  UpLeft, // 3\n  Average, // 4\n}\n\nenum LayeringStrategy {\n  topDown,\n  longestPath,\n  coffmanGraham,\n  networkSimplex\n}\n\nenum CrossMinimizationStrategy {\n  simple,\n  accumulatorTree\n}\n\nenum CycleRemovalStrategy {\n  dfs,\n  greedy,\n}\n\nabstract class BendPointShape {}\n\nclass SharpBendPointShape extends BendPointShape {}\n\nclass MaxCurvedBendPointShape extends BendPointShape {}\n\nclass CurvedBendPointShape extends BendPointShape {\n  final double curveLength;\n\n  CurvedBendPointShape({\n    required this.curveLength,\n  });\n}\n"
  },
  {
    "path": "lib/layered/SugiyamaEdgeData.dart",
    "content": "part of graphview;\n\nclass SugiyamaEdgeData {\n  List<double> bendPoints = [];\n}\n"
  },
  {
    "path": "lib/layered/SugiyamaEdgeRenderer.dart",
    "content": "part of graphview;\n\nclass SugiyamaEdgeRenderer extends ArrowEdgeRenderer {\n  Map<Node, SugiyamaNodeData> nodeData;\n  Map<Edge, SugiyamaEdgeData> edgeData;\n  BendPointShape bendPointShape;\n  bool addTriangleToEdge;\n  var path = Path();\n\n  SugiyamaEdgeRenderer(this.nodeData, this.edgeData, this.bendPointShape, this.addTriangleToEdge);\n\n  bool hasBendEdges(Edge edge) => edgeData.containsKey(edge) && edgeData[edge]!.bendPoints.isNotEmpty;\n\n  void render(Canvas canvas, Graph graph, Paint paint) {\n    graph.edges.forEach((edge) {\n      renderEdge(canvas, edge, paint);\n    });\n  }\n\n  @override\n  void renderEdge(Canvas canvas, Edge edge, Paint paint) {\n    var trianglePaint = Paint()\n      ..color = paint.color\n      ..style = PaintingStyle.fill;\n\n      Paint? edgeTrianglePaint;\n      if (edge.paint != null) {\n        edgeTrianglePaint = Paint()\n          ..color = edge.paint?.color ?? paint.color\n          ..style = PaintingStyle.fill;\n      }\n\n      var currentPaint = (edge.paint ?? paint)\n        ..style = PaintingStyle.stroke;\n\n      if (edge.source == edge.destination) {\n        final loopResult = buildSelfLoopPath(\n          edge,\n          arrowLength: addTriangleToEdge ? ARROW_LENGTH : 0.0,\n        );\n\n        if (loopResult != null) {\n          final lineType = nodeData[edge.destination]?.lineType;\n          drawStyledPath(canvas, loopResult.path, currentPaint, lineType: lineType);\n\n          if (addTriangleToEdge) {\n            final triangleCentroid = drawTriangle(\n              canvas,\n              edgeTrianglePaint ?? trianglePaint,\n              loopResult.arrowBase.dx,\n              loopResult.arrowBase.dy,\n              loopResult.arrowTip.dx,\n              loopResult.arrowTip.dy,\n            );\n\n            drawStyledLine(\n              canvas,\n              loopResult.arrowBase,\n              triangleCentroid,\n              currentPaint,\n              lineType: lineType,\n            );\n          }\n\n          return;\n        }\n      }\n\n      if (hasBendEdges(edge)) {\n        _renderEdgeWithBendPoints(canvas, edge, currentPaint, edgeTrianglePaint ?? trianglePaint);\n      } else {\n        _renderStraightEdge(canvas, edge, currentPaint, edgeTrianglePaint ?? trianglePaint);\n      }\n    }\n\n  void _renderEdgeWithBendPoints(Canvas canvas, Edge edge, Paint currentPaint, Paint trianglePaint) {\n    final source = edge.source;\n    final destination = edge.destination;\n    var bendPoints = edgeData[edge]!.bendPoints;\n\n    var sourceCenter = _getNodeCenter(source);\n\n    // Calculate the transition/offset from the original bend point to animated position\n    final transitionDx = sourceCenter.dx - bendPoints[0];\n    final transitionDy = sourceCenter.dy - bendPoints[1];\n\n    path.reset();\n    path.moveTo(sourceCenter.dx, sourceCenter.dy);\n\n    final bendPointsWithoutDuplication = <Offset>[];\n\n    for (var i = 0; i < bendPoints.length; i += 2) {\n      final isLastPoint = i == bendPoints.length - 2;\n\n      // Apply the same transition to all bend points\n      final x = bendPoints[i] + transitionDx;\n      final y = bendPoints[i + 1] + transitionDy;\n      final x2 = isLastPoint ? -1 : bendPoints[i + 2] + transitionDx;\n      final y2 = isLastPoint ? -1 : bendPoints[i + 3] + transitionDy;\n\n      if (x == x2 && y == y2) {\n        // Skip when two consecutive points are identical\n        // because drawing a line between would be redundant in this case.\n        continue;\n      }\n      bendPointsWithoutDuplication.add(Offset(x, y));\n    }\n\n    if (bendPointShape is MaxCurvedBendPointShape) {\n      _drawMaxCurvedBendPointsEdge(bendPointsWithoutDuplication);\n    } else if (bendPointShape is CurvedBendPointShape) {\n      final shape = bendPointShape as CurvedBendPointShape;\n      _drawCurvedBendPointsEdge(bendPointsWithoutDuplication, shape.curveLength);\n    } else {\n      _drawSharpBendPointsEdge(bendPointsWithoutDuplication);\n    }\n\n    var descOffset = getNodePosition(destination);\n    var stopX = descOffset.dx + destination.width * 0.5;\n    var stopY = descOffset.dy + destination.height * 0.5;\n\n    if (addTriangleToEdge) {\n      var clippedLine = <double>[];\n      final size = bendPoints.length;\n      if (nodeData[source]!.isReversed) {\n        clippedLine = clipLineEnd(bendPoints[2], bendPoints[3], stopX, stopY, destination.x,\n            destination.y, destination.width, destination.height);\n      } else {\n        clippedLine = clipLineEnd(bendPoints[size - 4], bendPoints[size - 3],\n            stopX, stopY, descOffset.dx,\n            descOffset.dy, destination.width, destination.height);\n      }\n      final triangleCentroid = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]);\n      path.lineTo(triangleCentroid.dx, triangleCentroid.dy);\n    } else {\n      path.lineTo(stopX, stopY);\n    }\n    canvas.drawPath(path, currentPaint);\n  }\n\n  void _renderStraightEdge(Canvas canvas, Edge edge, Paint currentPaint, Paint trianglePaint) {\n    final source = edge.source;\n    final destination = edge.destination;\n    final sourceCenter = _getNodeCenter(source);\n    var destCenter = _getNodeCenter(destination);\n\n    if (addTriangleToEdge) {\n      final clippedLine = clipLineEnd(sourceCenter.dx, sourceCenter.dy,\n          destCenter.dx, destCenter.dy, destination.x,\n          destination.y, destination.width, destination.height);\n\n      destCenter = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]);\n    }\n\n    // Draw the line with appropriate line type using the base class method\n    final lineType = nodeData[destination]?.lineType;\n    drawStyledLine(canvas, sourceCenter, destCenter, currentPaint, lineType: lineType);\n  }\n\n  void _drawSharpBendPointsEdge(List<Offset> bendPoints) {\n    for (var i = 1; i < bendPoints.length - 1; i++) {\n      path.lineTo(bendPoints[i].dx, bendPoints[i].dy);\n    }\n  }\n\n  void _drawMaxCurvedBendPointsEdge(List<Offset> bendPoints) {\n    for (var i = 1; i < bendPoints.length - 1; i++) {\n      final nextNode = bendPoints[i];\n      final afterNextNode = bendPoints[i + 1];\n      final curveEndPoint = Offset((nextNode.dx + afterNextNode.dx) / 2, (nextNode.dy + afterNextNode.dy) / 2);\n      path.quadraticBezierTo(nextNode.dx, nextNode.dy, curveEndPoint.dx, curveEndPoint.dy);\n    }\n  }\n\n  void _drawCurvedBendPointsEdge(List<Offset> bendPoints, double curveLength) {\n    for (var i = 1; i < bendPoints.length - 1; i++) {\n      final previousNode = i == 1 ? null : bendPoints[i - 2];\n      final currentNode = bendPoints[i - 1];\n      final nextNode = bendPoints[i];\n      final afterNextNode = bendPoints[i + 1];\n\n      final arcStartPointRadians = atan2(nextNode.dy - currentNode.dy, nextNode.dx - currentNode.dx);\n      final arcStartPoint = nextNode - Offset.fromDirection(arcStartPointRadians, curveLength);\n      final arcEndPointRadians = atan2(nextNode.dy - afterNextNode.dy, nextNode.dx - afterNextNode.dx);\n      final arcEndPoint = nextNode - Offset.fromDirection(arcEndPointRadians, curveLength);\n\n      if (previousNode != null && ((currentNode.dx == nextNode.dx && nextNode.dx == afterNextNode.dx) || (currentNode.dy == nextNode.dy && nextNode.dy == afterNextNode.dy))) {\n        path.lineTo(nextNode.dx, nextNode.dy);\n      } else {\n        path.lineTo(arcStartPoint.dx, arcStartPoint.dy);\n        path.quadraticBezierTo(nextNode.dx, nextNode.dy, arcEndPoint.dx, arcEndPoint.dy);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/layered/SugiyamaNodeData.dart",
    "content": "part of graphview;\n\nclass SugiyamaNodeData {\n  Set<Node> reversed = {};\n  bool isDummy = false;\n  int median = -1;\n  int layer = -1;\n  int position = -1;\n  List<Node> predecessorNodes = [];\n  List<Node> successorNodes = [];\n  LineType lineType;\n\n  SugiyamaNodeData(this.lineType);\n\n  bool get isReversed => reversed.isNotEmpty;\n\n  @override\n  String toString() {\n    return 'SugiyamaNodeData{reversed: $reversed, isDummy: $isDummy, median: $median, layer: $layer, position: $position, lineType: $lineType}';\n  }\n}\n"
  },
  {
    "path": "lib/mindmap/MindMapAlgorithm.dart",
    "content": "part of graphview;\n\nenum MindmapSide { LEFT, RIGHT, ROOT }\n\nclass _SideData {\n  MindmapSide side = MindmapSide.ROOT;\n}\n\nclass MindmapAlgorithm extends BuchheimWalkerAlgorithm {\n  final Map<Node, _SideData> _side = {};\n\n  MindmapAlgorithm(BuchheimWalkerConfiguration config, EdgeRenderer? renderer)\n      : super(config, renderer ?? MindmapEdgeRenderer(config));\n\n  @override\n  void initData(Graph? graph) {\n    super.initData(graph);\n    _side.clear();\n    graph?.nodes.forEach((n) => _side[n] = _SideData());\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    initData(graph);\n    _detectCycles(graph!);\n    final root = getFirstNode(graph);\n    _applyBuchheimWalkerSpacing(graph, root);\n    _createMindmapLayout(graph, root);\n    shiftCoordinates(graph, shiftX, shiftY);\n    return graph.calculateGraphSize();\n  }\n\n  void _markSubtree(Node node, MindmapSide side) {\n    final d = _side[node]!;\n    d.side = side;\n\n    for (final child in successorsOf(node)) {\n      _markSubtree(child, side);\n    }\n  }\n\n  void _applyBuchheimWalkerSpacing(Graph graph, Node root) {\n    // Apply the standard Buchheim-Walker algorithm to get proper spacing\n    // This gives us optimal spacing relationships between all nodes\n    firstWalk(graph, root, 0, 0);\n    secondWalk(graph, root, 0.0);\n    positionNodes(graph);\n\n    // At this point, all nodes have positions with proper spacing,\n    // but they're in a traditional tree layout. We'll reposition them next.\n  }\n\n  void _createMindmapLayout(Graph graph, Node root) {\n    final vertical = isVertical();\n    final rootPos = vertical ? root.x : root.y;\n\n    // Mark subtrees and position nodes in one pass\n    for (final child in successorsOf(root)) {\n      final childPos = vertical ? child.x : child.y;\n      final side = childPos < rootPos ? MindmapSide.LEFT : MindmapSide.RIGHT;\n      _markSubtree(child, side);\n    }\n\n    // Position all non-root nodes\n    for (final node in graph.nodes) {\n      final info = nodeData[node]!;\n      if (info.depth == 0) continue; // Skip root\n\n      final sideMultiplier = _side[node]!.side == MindmapSide.LEFT ? -1 : 1;\n      final secondary = vertical ? node.x : node.y;\n      final distanceFromRoot = info.depth * configuration.levelSeparation +\n          (vertical ? maxNodeWidth : maxNodeHeight) / 2;\n\n      if (vertical) {\n        node.position = Offset(\n            secondary - root.x * 0.5 * sideMultiplier,\n            sideMultiplier * distanceFromRoot\n        );\n      } else {\n        node.position = Offset(\n            sideMultiplier * distanceFromRoot,\n            secondary - root.y * 0.5 * sideMultiplier\n        );\n      }\n    }\n\n    // Adjust root and apply final transformations\n    if (needReverseOrder()) {\n      if (vertical) {\n        root.y = 0.0;\n      } else {\n        root.x = 0.0;\n      }\n    }\n\n    for (final node in graph.nodes) {\n      final info = nodeData[node]!;\n      if (info.depth == 0) {\n        if (vertical) {\n          node.x = node.x * 0.5;\n        } else {\n          node.y = node.y * 0.5;\n        }\n      } else {\n        if (vertical) {\n          node.x = node.x - root.x;\n        } else {\n          node.y = node.y - root.y;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/mindmap/MindmapEdgeRenderer.dart",
    "content": "part of graphview;\n\nclass MindmapEdgeRenderer extends TreeEdgeRenderer {\n  MindmapEdgeRenderer(BuchheimWalkerConfiguration configuration)\n      : super(configuration);\n\n  @override\n  int getEffectiveOrientation(dynamic node, dynamic child) {\n    var orientation = configuration.orientation;\n\n    if (child.y < 0) {\n      if (configuration.orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) {\n        orientation = BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP;\n      } else {\n        // orientation = BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM;\n      }\n    } else if (child.x < 0) {\n      if (configuration.orientation == BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT) {\n        orientation = BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT;\n      } else {\n        orientation = BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT;\n      }\n    }\n\n    return orientation;\n  }\n}"
  },
  {
    "path": "lib/tree/BaloonLayoutAlgorithm.dart",
    "content": "part of graphview;\n\n// Polar coordinate representation\nclass PolarPoint {\n  final double theta; // angle in radians\n  final double radius;\n\n  const PolarPoint(this.theta, this.radius);\n\n  static const PolarPoint origin = PolarPoint(0, 0);\n\n  // Convert polar coordinates to cartesian\n  Offset toCartesian() {\n    final x = radius * cos(theta);\n    final y = radius * sin(theta);\n    return Offset(x, y);\n  }\n\n  // Create polar point from angle and radius\n  static PolarPoint of(double theta, double radius) {\n    return PolarPoint(theta, radius);\n  }\n\n  @override\n  String toString() => 'PolarPoint(theta: $theta, radius: $radius)';\n}\n\nclass BalloonLayoutAlgorithm extends Algorithm {\n  late BuchheimWalkerConfiguration config;\n  final Map<Node, TreeLayoutNodeData> nodeData = {};\n  final Map<Node, PolarPoint> polarLocations = {};\n  final Map<Node, double> radii = {};\n\n  BalloonLayoutAlgorithm(this.config, EdgeRenderer? renderer) {\n    this.renderer = renderer ?? ArrowEdgeRenderer();\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    if (graph == null || graph.nodes.isEmpty) {\n      return Size.zero;\n    }\n\n    nodeData.clear();\n    polarLocations.clear();\n    radii.clear();\n\n    // Handle single node case\n    if (graph.nodes.length == 1) {\n      final node = graph.nodes.first;\n      node.position = Offset(shiftX + 100, shiftY + 100);\n      return Size(200, 200);\n    }\n\n    _initializeData(graph);\n    final roots = _findRoots(graph);\n\n    if (roots.isEmpty) {\n      final spanningTree = _createSpanningTree(graph);\n      return _layoutSpanningTree(spanningTree, shiftX, shiftY);\n    }\n\n    _setRootPolars(graph, roots);\n    _shiftCoordinates(graph, shiftX, shiftY);\n    return graph.calculateGraphSize();\n  }\n\n  void _initializeData(Graph graph) {\n    // Initialize node data\n    for (final node in graph.nodes) {\n      nodeData[node] = TreeLayoutNodeData();\n    }\n\n    // Build tree structure from edges\n    for (final edge in graph.edges) {\n      final source = edge.source;\n      final target = edge.destination;\n\n      nodeData[source]!.successorNodes.add(target);\n      nodeData[target]!.parent = source;\n    }\n  }\n\n  List<Node> _findRoots(Graph graph) {\n    return graph.nodes.where((node) {\n      return nodeData[node]!.parent == null;\n    }).toList();\n  }\n\n  void _setRootPolars(Graph graph, List<Node> roots) {\n    final center = _getGraphCenter(graph);\n    final width = graph.calculateGraphBounds().width;\n    final defaultRadius = max(width / 2, 200.0);\n\n    if (roots.length == 1) {\n      // Single tree - place root at center\n      final root = roots.first;\n      _setRootPolar(root, center);\n      final children = successorsOf(root);\n      _setPolars(children, center, 0, defaultRadius, <Node>{});\n    } else if (roots.length > 1) {\n      // Multiple trees - arrange roots in circle\n      _setPolars(roots, center, 0, defaultRadius, <Node>{});\n    }\n  }\n\n  void _setRootPolar(Node root, Offset center) {\n    polarLocations[root] = PolarPoint.origin;\n    root.position = center;\n  }\n\n  void _setPolars(List<Node> nodes, Offset parentLocation, double angleToParent,\n      double parentRadius, Set<Node> seen) {\n    final childCount = nodes.length;\n    if (childCount == 0) return;\n\n    // Calculate child placement parameters\n    final angle = max(0, pi / 2 * (1 - 2.0 / childCount));\n    final childRadius = parentRadius * cos(angle) / (1 + cos(angle));\n    final radius = parentRadius - childRadius;\n\n    // Angle between children\n    final angleBetweenKids = 2 * pi / childCount;\n    final offset = angleBetweenKids / 2 - angleToParent;\n\n    for (var i = 0; i < nodes.length; i++) {\n      final child = nodes[i];\n      if (seen.contains(child)) continue;\n\n      // Calculate angle for this child\n      final theta = i * angleBetweenKids + offset;\n\n      // Store radius and polar coordinates\n      radii[child] = childRadius;\n      final polarPoint = PolarPoint.of(theta, radius);\n      polarLocations[child] = polarPoint;\n\n      // Convert to cartesian and position node\n      final cartesian = polarPoint.toCartesian();\n      final position = Offset(\n        parentLocation.dx + cartesian.dx,\n        parentLocation.dy + cartesian.dy,\n      );\n      child.position = position;\n\n      final newAngleToParent = atan2(\n        parentLocation.dy - position.dy,\n        parentLocation.dx - position.dx,\n      );\n\n      final grandChildren = successorsOf(child)\n          .where((node) => !seen.contains(node))\n          .toList();\n\n      if (grandChildren.isNotEmpty) {\n        final newSeen = Set<Node>.from(seen);\n        newSeen.add(child); // Add current child to prevent cycles\n        _setPolars(grandChildren, position, newAngleToParent, childRadius, newSeen);\n      }\n    }\n  }\n\n  Offset _getGraphCenter(Graph graph) {\n    final bounds = graph.calculateGraphBounds();\n    return Offset(\n      bounds.left + bounds.width / 2,\n      bounds.top + bounds.height / 2,\n    );\n  }\n\n  void _shiftCoordinates(Graph graph, double shiftX, double shiftY) {\n    for (final node in graph.nodes) {\n      node.position = Offset(node.x + shiftX, node.y + shiftY);\n    }\n  }\n\n  Graph _createSpanningTree(Graph graph) {\n    final visited = <Node>{};\n    final spanningEdges = <Edge>[];\n\n    if (graph.nodes.isNotEmpty) {\n      final startNode = graph.nodes.first;\n      final queue = <Node>[startNode];\n      visited.add(startNode);\n\n      while (queue.isNotEmpty) {\n        final current = queue.removeAt(0);\n\n        for (final edge in graph.edges) {\n          Node? neighbor;\n          if (edge.source == current && !visited.contains(edge.destination)) {\n            neighbor = edge.destination;\n            spanningEdges.add(edge);\n          } else if (edge.destination == current && !visited.contains(edge.source)) {\n            neighbor = edge.source;\n            spanningEdges.add(Edge(current, edge.source));\n          }\n\n          if (neighbor != null && !visited.contains(neighbor)) {\n            visited.add(neighbor);\n            queue.add(neighbor);\n          }\n        }\n      }\n    }\n\n    return Graph()..addEdges(spanningEdges);\n  }\n\n  Size _layoutSpanningTree(Graph spanningTree, double shiftX, double shiftY) {\n    nodeData.clear();\n    polarLocations.clear();\n    radii.clear();\n\n    _initializeData(spanningTree);\n    final roots = _findRoots(spanningTree);\n\n    if (roots.isEmpty && spanningTree.nodes.isNotEmpty) {\n      final fakeRoot = spanningTree.nodes.first;\n      _setRootPolars(spanningTree, [fakeRoot]);\n    } else {\n      _setRootPolars(spanningTree, roots);\n    }\n\n    _shiftCoordinates(spanningTree, shiftX, shiftY);\n    return spanningTree.calculateGraphSize();\n  }\n\n  List<Node> successorsOf(Node? node) {\n    return nodeData[node]!.successorNodes;\n  }\n\n  PolarPoint? getPolarLocation(Node node) {\n    return polarLocations[node];\n  }\n\n  double? getRadius(Node node) {\n    return radii[node];\n  }\n\n  Map<Node, double> getRadii() {\n    return Map.from(radii);\n  }\n\n  Map<Node, PolarPoint> getPolarLocations() {\n    return Map.from(polarLocations);\n  }\n\n  @override\n  void init(Graph? graph) {\n    // Implementation can be added if needed\n  }\n\n  @override\n  void setDimensions(double width, double height) {\n    // Implementation can be added if needed\n  }\n\n  @override\n  EdgeRenderer? renderer;\n}"
  },
  {
    "path": "lib/tree/BuchheimWalkerAlgorithm.dart",
    "content": "part of graphview;\n\nclass BuchheimWalkerAlgorithm extends Algorithm {\n  Map<Node, BuchheimWalkerNodeData> nodeData = {};\n  double minNodeHeight = double.infinity;\n  double minNodeWidth = double.infinity;\n  double maxNodeWidth = double.negativeInfinity;\n  double maxNodeHeight = double.negativeInfinity;\n  BuchheimWalkerConfiguration configuration;\n\n  bool isVertical() {\n    var orientation = configuration.orientation;\n    return orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM ||\n        orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP;\n  }\n\n  bool needReverseOrder() {\n    var orientation = configuration.orientation;\n    return orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP ||\n        orientation == BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT;\n  }\n\n  void _detectCycles(Graph graph) {\n    var visiting = <Node>{};\n\n    bool hasCycle(Node node) {\n      if (visiting.contains(node)) return true;\n      visiting.add(node);\n      var cycleFound = successorsOf(node).any(hasCycle);\n      visiting.remove(node);\n      return cycleFound;\n    }\n\n    if (graph.nodes.any(hasCycle)) {\n      throw Exception('Cyclic dependency detected - tree structure required');\n    }\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    if (graph == null) return Size.zero;\n    nodeData.clear();\n    if (graph.nodes.length == 1) {\n      final node = graph.nodes.first;\n      node.position = Offset(shiftX, shiftY);\n      return node.size * 2;\n    }\n    initData(graph);\n    _detectCycles(graph);\n    var firstNode = getFirstNode(graph);\n    firstWalk(graph, firstNode, 0, 0);\n    secondWalk(graph, firstNode, 0.0);\n    checkUnconnectedNotes(graph);\n    positionNodes(graph);\n    shiftCoordinates(graph, shiftX, shiftY);\n    return graph.calculateGraphSize();\n  }\n\n  Node getFirstNode(Graph graph) =>\n      graph.nodes.firstWhere((element) => !hasPredecessor(element));\n\n  void checkUnconnectedNotes(Graph graph) {\n    graph.nodes.forEach((element) {\n      if (getNodeData(element) == null) {\n        if (!kReleaseMode) {\n          print('$element is not connected to primary ancestor');\n        }\n      }\n    });\n  }\n\n  int compare(int x, int y) {\n    return x < y ? -1 : (x == y ? 0 : 1);\n  }\n\n  void firstWalk(Graph graph, Node node, int depth, int number) {\n    final nodeData = getNodeData(node)!;\n    nodeData.depth = depth;\n    nodeData.number = number;\n    minNodeHeight = min(minNodeHeight, node.height);\n    minNodeWidth = min(minNodeWidth, node.width);\n    maxNodeWidth = max(maxNodeWidth, node.width);\n    maxNodeHeight = max(maxNodeHeight, node.height);\n\n    if (isLeaf(graph, node)) {\n      // if the node has no left sibling, prelim(node) should be set to 0, but we don't have to set it\n      // here, because it's already initialized with 0\n      if (hasLeftSibling(graph, node)) {\n        final leftSibling = getLeftSibling(graph, node);\n        nodeData.prelim = getPrelim(leftSibling) + getSpacing(graph, leftSibling, node);\n      }\n    } else {\n      final leftMost = getLeftMostChild(graph, node);\n      final rightMost = getRightMostChild(graph, node);\n      var defaultAncestor = leftMost;\n\n      Node? next = leftMost;\n      var i = 1;\n      while (next != null) {\n        firstWalk(graph, next, depth + 1, i++);\n        defaultAncestor = apportion(graph, next, defaultAncestor);\n\n        next = getRightSibling(graph, next);\n      }\n\n      executeShifts(graph, node);\n\n      var vertical = isVertical();\n      var midPoint = 0.5 *\n          ((getPrelim(leftMost) + getPrelim(rightMost) + (vertical ? rightMost!.width : rightMost!.height)) -\n              (vertical ? node.width : node.height));\n\n      if (hasLeftSibling(graph, node)) {\n        final leftSibling = getLeftSibling(graph, node);\n        nodeData.prelim = getPrelim(leftSibling) + getSpacing(graph, leftSibling, node);\n        nodeData.modifier = nodeData.prelim - midPoint;\n      } else {\n        nodeData.prelim = midPoint;\n      }\n    }\n  }\n\n  void secondWalk(Graph graph, Node node, double modifier) {\n    var nodeData = getNodeData(node)!;\n    var depth = nodeData.depth;\n    var vertical = isVertical();\n\n    node.position = Offset((nodeData.prelim + modifier),\n        (depth * (vertical ? minNodeHeight : minNodeWidth) + depth * configuration.levelSeparation).ceilToDouble());\n\n    graph.successorsOf(node).forEach((w) {\n      secondWalk(graph, w, modifier + nodeData.modifier);\n    });\n  }\n\n  void executeShifts(Graph graph, Node node) {\n    var shift = 0.0;\n    var change = 0.0;\n\n    var w = getRightMostChild(graph, node);\n    while (w != null) {\n      final nodeData = getNodeData(w) ?? BuchheimWalkerNodeData();\n\n      nodeData.prelim = nodeData.prelim + shift;\n      nodeData.modifier = nodeData.modifier + shift;\n      change += nodeData.change;\n      shift += nodeData.shift + change;\n\n      w = getLeftSibling(graph, w);\n    }\n  }\n\n  Node apportion(Graph graph, Node node, Node defaultAncestor) {\n    var ancestor = defaultAncestor;\n    if (hasLeftSibling(graph, node)) {\n      var leftSibling = getLeftSibling(graph, node);\n      Node? vop = node;\n      Node? vom = getLeftMostChild(graph, predecessorsOf(node).first);\n      var sip = getModifier(node);\n\n      var sop = getModifier(node);\n\n      var sim = getModifier(leftSibling);\n\n      var som = getModifier(vom);\n      var nextRight = this.nextRight(graph, leftSibling);\n\n      Node? nextLeft;\n      for (nextLeft = this.nextLeft(graph, node);\n          nextRight != null && nextLeft != null;\n          nextLeft = this.nextLeft(graph, nextLeft)) {\n        vom = this.nextLeft(graph, vom);\n        vop = this.nextRight(graph, vop);\n\n        setAncestor(vop, node);\n        var shift = getPrelim(nextRight) + sim - (getPrelim(nextLeft) + sip) + getSpacing(graph, nextRight, node);\n        if (shift > 0) {\n          moveSubtree(this.ancestor(graph, nextRight, node, ancestor), node, shift);\n          sip += shift;\n          sop += shift;\n        }\n\n        sim += getModifier(nextRight);\n        sip += getModifier(nextLeft);\n\n        som += getModifier(vom);\n        sop += getModifier(vop);\n        nextRight = this.nextRight(graph, nextRight);\n      }\n\n      if (nextRight != null && this.nextRight(graph, vop) == null) {\n        setThread(vop, nextRight);\n        setModifier(vop, getModifier(vop) + sim - sop);\n      }\n\n      if (nextLeft != null && this.nextLeft(graph, vom) == null) {\n        setThread(vom, nextLeft);\n        setModifier(vom, getModifier(vom) + sip - som);\n        ancestor = node;\n      }\n    }\n\n    return ancestor;\n  }\n\n  void setAncestor(Node? v, Node ancestor) {\n      getNodeData(v)?.ancestor = ancestor;\n  }\n\n  void setModifier(Node? v, double modifier) {\n    getNodeData(v)?.modifier = modifier;\n  }\n\n  void setThread(Node? v, Node thread) {\n    getNodeData(v)?.thread = thread;\n  }\n\n  double getPrelim(Node? v) {\n    return getNodeData(v)?.prelim ?? 0;\n  }\n\n  double getModifier(Node? vip) {\n    return getNodeData(vip)?.modifier ?? 0;\n  }\n\n  void moveSubtree(Node? wm, Node wp, double shift) {\n    var wpNodeData = getNodeData(wp)!;\n    var wmNodeData = getNodeData(wm)!;\n    var subtrees = wpNodeData.number - wmNodeData.number;\n    wpNodeData.change = (wpNodeData.change - shift / subtrees);\n    wpNodeData.shift = (wpNodeData.shift + shift);\n    wmNodeData.change = (wmNodeData.change + shift / subtrees);\n    wpNodeData.prelim = (wpNodeData.prelim + shift);\n    wpNodeData.modifier = (wpNodeData.modifier + shift);\n  }\n\n  Node? ancestor(Graph graph, Node vim, Node node, Node defaultAncestor) {\n    var vipNodeData = getNodeData(vim)!;\n    return predecessorsOf(vipNodeData.ancestor).first == predecessorsOf(node).first\n        ? vipNodeData.ancestor\n        : defaultAncestor;\n  }\n\n  Node? nextRight(Graph graph, Node? node) {\n    return graph.hasSuccessor(node) ? getRightMostChild(graph, node) : getNodeData(node)?.thread;\n  }\n\n  Node? nextLeft(Graph graph, Node? node) {\n    return hasSuccessor(node)\n        ? getLeftMostChild(graph, node)\n        : getNodeData(node)?.thread;\n  }\n\n  num getSpacing(Graph graph, Node? leftNode, Node rightNode) {\n    var separation = configuration.getSubtreeSeparation();\n    if (isSibling(graph, leftNode, rightNode)) {\n      separation = configuration.getSiblingSeparation();\n    }\n\n    num length = isVertical() ? leftNode!.width : leftNode!.height;\n\n    return separation + length;\n  }\n\n  bool isSibling(Graph graph, Node? leftNode, Node rightNode) {\n    var leftParent = predecessorsOf(leftNode).first;\n    return successorsOf(leftParent).contains(rightNode);\n  }\n\n  bool isLeaf(Graph graph, Node node) {\n    return successorsOf(node).isEmpty;\n  }\n\n  Node? getLeftSibling(Graph graph, Node node) {\n    if (!hasLeftSibling(graph, node)) {\n      return null;\n    } else {\n      var parent = predecessorsOf(node).first;\n      var children = successorsOf(parent);\n      var nodeIndex = children.indexOf(node);\n      return children[nodeIndex - 1];\n    }\n  }\n\n  bool hasLeftSibling(Graph graph, Node node) {\n    var parents = predecessorsOf(node);\n    if (parents.isEmpty) {\n      return false;\n    } else {\n      var parent = parents.first;\n      var nodeIndex = successorsOf(parent).indexOf(node);\n      return nodeIndex > 0;\n    }\n  }\n\n  Node? getRightSibling(Graph graph, Node node) {\n    if (!hasRightSibling(graph, node)) {\n      return null;\n    } else {\n      var parent = predecessorsOf(node).first;\n      var children = successorsOf(parent);\n      var nodeIndex = children.indexOf(node);\n      return children[nodeIndex + 1];\n    }\n  }\n\n  bool hasRightSibling(Graph graph, Node node) {\n    var parents = predecessorsOf(node);\n    if (parents.isEmpty) {\n      return false;\n    } else {\n      var parent = parents[0];\n      List children = successorsOf(parent);\n      var nodeIndex = children.indexOf(node);\n      return nodeIndex < children.length - 1;\n    }\n  }\n\n  Node getLeftMostChild(Graph graph, Node? node) {\n    return successorsOf(node).first;\n  }\n\n  Node? getRightMostChild(Graph graph, Node? node) {\n    var children = successorsOf(node);\n    return children.isEmpty ? null : children.last;\n  }\n\n  void positionNodes(Graph graph) {\n    var doesNeedReverseOrder = needReverseOrder();\n\n    var offset = getOffset(graph, doesNeedReverseOrder);\n    var nodes = sortByLevel(graph, doesNeedReverseOrder);\n    var firstLevel = getNodeData(nodes.first)?.depth ?? 0;\n    var localMaxSize = findMaxSize(filterByLevel(nodes, firstLevel));\n    var currentLevel = doesNeedReverseOrder ? firstLevel : 0;\n\n    var globalPadding = 0.0;\n    var localPadding = 0.0;\n    nodes.forEach((node) {\n      final depth = getNodeData(node)?.depth ?? 0;\n      if (depth != currentLevel) {\n        if (doesNeedReverseOrder) {\n          globalPadding -= localPadding;\n        } else {\n          globalPadding += localPadding;\n        }\n        localPadding = 0.0;\n        currentLevel = depth;\n\n        localMaxSize = findMaxSize(filterByLevel(nodes, currentLevel));\n      }\n\n      final height = node.height;\n      final width = node.width;\n      switch (configuration.orientation) {\n        case BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM:\n          if (height > minNodeHeight) {\n            final diff = height - minNodeHeight;\n            localPadding = max(localPadding, diff);\n          }\n          break;\n        case BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP:\n          if (height < localMaxSize.height) {\n            var diff = localMaxSize.height - height;\n            node.position -= Offset(0, diff);\n            localPadding = max(localPadding, diff);\n          }\n          break;\n        case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT:\n          if (width > minNodeWidth) {\n            final diff = width - minNodeWidth;\n            localPadding = max(localPadding, diff);\n          }\n          break;\n        case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT:\n          if (width < localMaxSize.width) {\n            var diff = localMaxSize.width - width;\n            node.position -= Offset(0, diff);\n            localPadding = max(localPadding, diff);\n          }\n      }\n\n      node.position = getPosition(node, globalPadding, offset);\n    });\n  }\n\n  void shiftCoordinates(Graph graph, double shiftX, double shiftY) {\n    graph.nodes.forEach((node) {\n      node.position = (Offset(node.x + shiftX, node.y + shiftY));\n    });\n  }\n\n  Size findMaxSize(List<Node> nodes) {\n    var width = double.negativeInfinity;\n    var height = double.negativeInfinity;\n\n    nodes.forEach((node) {\n      width = max(width, node.width);\n      height = max(height, node.height);\n    });\n\n    return Size(width, height);\n  }\n\n  Offset getOffset(Graph graph, bool needReverseOrder) {\n    var offsetX = double.infinity;\n    var offsetY = double.infinity;\n\n    if (needReverseOrder) {\n      offsetY = double.minPositive;\n    }\n\n    graph.nodes.forEach((node) {\n      if (needReverseOrder) {\n        offsetX = min(offsetX, node.x);\n        offsetY = max(offsetY, node.y);\n      } else {\n        offsetX = min(offsetX, node.x);\n        offsetY = min(offsetY, node.y);\n      }\n    });\n\n    return Offset(offsetX, offsetY);\n  }\n\n  Offset getPosition(Node node, double globalPadding, Offset offset) {\n    Offset finalOffset;\n    switch (configuration.orientation) {\n      case 1:\n        finalOffset = Offset(node.x - offset.dx, node.y + globalPadding);\n        break;\n      case 2:\n        finalOffset = Offset(node.x - offset.dx, offset.dy - node.y - globalPadding);\n        break;\n      case 3:\n        finalOffset = Offset(node.y + globalPadding, node.x - offset.dx);\n        break;\n      case 4:\n        finalOffset = Offset(offset.dy - node.y - globalPadding, node.x - offset.dx);\n        break;\n      default:\n        finalOffset = Offset(0, 0);\n        break;\n    }\n\n    return finalOffset;\n  }\n\n  List<Node> sortByLevel(Graph graph, bool descending) {\n    var nodes = <Node>[...graph.nodes];\n    if (descending) {\n      nodes.reversed;\n    }\n    nodes.sort((data1, data2) => compare(getNodeData(data1)?.depth ?? 0, getNodeData(data2)?.depth ?? 0));\n\n    return nodes;\n  }\n\n  List<Node> filterByLevel(List<Node> nodes, int? level) {\n    return nodes.where((node) => getNodeData(node)?.depth == level).toList();\n  }\n\n  @override\n  EdgeRenderer? renderer;\n\n  void initData(Graph? graph) {\n    graph?.nodes.forEach((node) {\n      var nodeDatab = BuchheimWalkerNodeData();\n      nodeDatab.ancestor = node;\n\n      nodeData[node] = nodeDatab;\n    });\n\n    graph?.edges.forEach((element) {\n      nodeData[element.source]?.successorNodes.add(element.destination);\n      nodeData[element.destination]?.predecessorNodes.add(element.source);\n    });\n  }\n\n  BuchheimWalkerNodeData? getNodeData(Node? node) {\n    return node == null ? null : nodeData[node];\n  }\n\n  bool hasSuccessor(Node? node) => successorsOf(node).isNotEmpty;\n\n  bool hasPredecessor(Node node) => predecessorsOf(node).isNotEmpty;\n\n  List<Node> successorsOf(Node? node) {\n    return nodeData[node]?.successorNodes ?? [];\n  }\n\n  List<Node> predecessorsOf(Node? node) {\n    return nodeData[node]?.predecessorNodes ?? [];\n  }\n\n  BuchheimWalkerAlgorithm(this.configuration, EdgeRenderer? renderer) {\n    this.renderer = renderer ?? TreeEdgeRenderer(configuration);\n  }\n\n  @override\n  void init(Graph? graph) {\n    var firstNode = getFirstNode(graph!);\n    firstWalk(graph, firstNode, 0, 0);\n    secondWalk(graph, firstNode, 0.0);\n    checkUnconnectedNotes(graph);\n    positionNodes(graph);\n    // shiftCoordinates(graph, shiftX, shiftY);\n  }\n\n  @override\n  void setDimensions(double width, double height) {\n    // graphWidth = width;\n    // graphHeight = height;\n  }\n}\n"
  },
  {
    "path": "lib/tree/BuchheimWalkerConfiguration.dart",
    "content": "part of graphview;\n\nclass BuchheimWalkerConfiguration {\n  int siblingSeparation = DEFAULT_SIBLING_SEPARATION;\n  int levelSeparation = DEFAULT_LEVEL_SEPARATION;\n  int subtreeSeparation = DEFAULT_SUBTREE_SEPARATION;\n  int orientation = DEFAULT_ORIENTATION;\n  static const ORIENTATION_TOP_BOTTOM = 1;\n  static const ORIENTATION_BOTTOM_TOP = 2;\n  static const ORIENTATION_LEFT_RIGHT = 3;\n  static const ORIENTATION_RIGHT_LEFT = 4;\n  static const DEFAULT_SIBLING_SEPARATION = 100;\n  static const DEFAULT_SUBTREE_SEPARATION = 100;\n  static const DEFAULT_LEVEL_SEPARATION = 100;\n  static const DEFAULT_ORIENTATION = 1;\n  bool useCurvedConnections = true;\n\n  int getSiblingSeparation() {\n    return siblingSeparation;\n  }\n\n  int getLevelSeparation() {\n    return levelSeparation;\n  }\n\n  int getSubtreeSeparation() {\n    return subtreeSeparation;\n  }\n  BuchheimWalkerConfiguration(\n      {this.siblingSeparation = DEFAULT_SIBLING_SEPARATION,\n        this.levelSeparation = DEFAULT_LEVEL_SEPARATION,\n        this.subtreeSeparation = DEFAULT_SUBTREE_SEPARATION,\n        this.orientation = DEFAULT_ORIENTATION});\n\n}\n"
  },
  {
    "path": "lib/tree/BuchheimWalkerNodeData.dart",
    "content": "part of graphview;\n\nclass BuchheimWalkerNodeData {\n  Node? ancestor;\n  Node? thread;\n  int number = 0;\n  int depth = 0;\n  double prelim = 0.toDouble();\n  double modifier = 0.toDouble();\n  double shift = 0.toDouble();\n  double change = 0.toDouble();\n  List<Node> predecessorNodes = [];\n  List<Node> successorNodes = [];\n}\n"
  },
  {
    "path": "lib/tree/CircleLayoutAlgorithm.dart",
    "content": "part of graphview;\n\nclass CircleLayoutConfiguration {\n  final double radius;\n  final bool reduceEdgeCrossing;\n  final int reduceEdgeCrossingMaxEdges;\n\n  CircleLayoutConfiguration({\n    this.radius = 0.0, // 0 means auto-calculate\n    this.reduceEdgeCrossing = true,\n    this.reduceEdgeCrossingMaxEdges = 200,\n  });\n}\n\nclass CircleLayoutAlgorithm extends Algorithm {\n  final CircleLayoutConfiguration config;\n  double _radius = 0.0;\n  List<Node> nodeOrderedList = [];\n\n  CircleLayoutAlgorithm(this.config, EdgeRenderer? renderer) {\n    this.renderer = renderer ?? ArrowEdgeRenderer();\n    _radius = config.radius;\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    if (graph == null || graph.nodes.isEmpty) {\n      return Size.zero;\n    }\n\n    // Handle single node case\n    if (graph.nodes.length == 1) {\n      final node = graph.nodes.first;\n      node.position = Offset(shiftX + 100, shiftY + 100);\n      return Size(200, 200);\n    }\n\n    _computeNodeOrder(graph);\n    final size = _layoutNodes(graph);\n    _shiftCoordinates(graph, shiftX, shiftY);\n\n    return size;\n  }\n\n  void _computeNodeOrder(Graph graph) {\n    final shouldReduceCrossing = config.reduceEdgeCrossing &&\n        graph.edges.length < config.reduceEdgeCrossingMaxEdges;\n\n    if (shouldReduceCrossing) {\n      nodeOrderedList = _reduceEdgeCrossing(graph);\n    } else {\n      nodeOrderedList = List.from(graph.nodes);\n    }\n  }\n\n  List<Node> _reduceEdgeCrossing(Graph graph) {\n    // Check if graph has multiple components\n    final components = _findConnectedComponents(graph);\n    final orderedList = <Node>[];\n\n    if (components.length > 1) {\n      // Handle each component separately\n      for (final component in components) {\n        final componentGraph = _createSubgraph(graph, component);\n        final componentOrder = _optimizeNodeOrder(componentGraph);\n        orderedList.addAll(componentOrder);\n      }\n    } else {\n      // Single component\n      orderedList.addAll(_optimizeNodeOrder(graph));\n    }\n\n    return orderedList;\n  }\n\n  List<Set<Node>> _findConnectedComponents(Graph graph) {\n    final visited = <Node>{};\n    final components = <Set<Node>>[];\n\n    for (final node in graph.nodes) {\n      if (!visited.contains(node)) {\n        final component = <Node>{};\n        _dfsComponent(graph, node, visited, component);\n        components.add(component);\n      }\n    }\n\n    return components;\n  }\n\n  void _dfsComponent(Graph graph, Node node, Set<Node> visited, Set<Node> component) {\n    visited.add(node);\n    component.add(node);\n\n    for (final edge in graph.edges) {\n      Node? neighbor;\n      if (edge.source == node && !visited.contains(edge.destination)) {\n        neighbor = edge.destination;\n      } else if (edge.destination == node && !visited.contains(edge.source)) {\n        neighbor = edge.source;\n      }\n\n      if (neighbor != null) {\n        _dfsComponent(graph, neighbor, visited, component);\n      }\n    }\n  }\n\n  Graph _createSubgraph(Graph originalGraph, Set<Node> nodes) {\n    final subgraph = Graph();\n\n    // Add nodes\n    for (final node in nodes) {\n      subgraph.addNode(node);\n    }\n\n    // Add edges within the component\n    for (final edge in originalGraph.edges) {\n      if (nodes.contains(edge.source) && nodes.contains(edge.destination)) {\n        subgraph.addEdgeS(edge);\n      }\n    }\n\n    return subgraph;\n  }\n\n  List<Node> _optimizeNodeOrder(Graph graph) {\n    if (graph.nodes.length <= 2) {\n      return List.from(graph.nodes);\n    }\n\n    // Simple greedy optimization to reduce edge crossings\n    var bestOrder = List<Node>.from(graph.nodes);\n    var bestCrossings = _countCrossings(graph, bestOrder);\n\n    // Try a few different starting arrangements\n    final attempts = min(10, graph.nodes.length);\n\n    for (var attempt = 0; attempt < attempts; attempt++) {\n      var currentOrder = List<Node>.from(graph.nodes);\n\n      // Shuffle starting order\n      if (attempt > 0) {\n        currentOrder.shuffle();\n      }\n\n      // Local optimization: try swapping adjacent nodes\n      var improved = true;\n      var iterations = 0;\n      const maxIterations = 50;\n\n      while (improved && iterations < maxIterations) {\n        improved = false;\n        iterations++;\n\n        for (var i = 0; i < currentOrder.length - 1; i++) {\n          // Try swapping positions i and i+1\n          final temp = currentOrder[i];\n          currentOrder[i] = currentOrder[i + 1];\n          currentOrder[i + 1] = temp;\n\n          final crossings = _countCrossings(graph, currentOrder);\n\n          if (crossings < bestCrossings) {\n            bestOrder = List.from(currentOrder);\n            bestCrossings = crossings;\n            improved = true;\n          } else {\n            // Swap back if no improvement\n            currentOrder[i + 1] = currentOrder[i];\n            currentOrder[i] = temp;\n          }\n        }\n      }\n    }\n\n    return bestOrder;\n  }\n\n  int _countCrossings(Graph graph, List<Node> nodeOrder) {\n    if (nodeOrder.length < 3) return 0;\n\n    final nodePositions = <Node, int>{};\n    for (var i = 0; i < nodeOrder.length; i++) {\n      nodePositions[nodeOrder[i]] = i;\n    }\n\n    var crossings = 0;\n    final edges = graph.edges;\n\n    // Count crossings between all pairs of edges\n    for (var i = 0; i < edges.length; i++) {\n      final edge1 = edges[i];\n      final pos1a = nodePositions[edge1.source]!;\n      final pos1b = nodePositions[edge1.destination]!;\n\n      for (var j = i + 1; j < edges.length; j++) {\n        final edge2 = edges[j];\n        final pos2a = nodePositions[edge2.source]!;\n        final pos2b = nodePositions[edge2.destination]!;\n\n        // Check if edges cross when nodes are arranged in a circle\n        if (_edgesCross(pos1a, pos1b, pos2a, pos2b, nodeOrder.length)) {\n          crossings++;\n        }\n      }\n    }\n\n    return crossings;\n  }\n\n  bool _edgesCross(int pos1a, int pos1b, int pos2a, int pos2b, int totalNodes) {\n    // Normalize positions so smaller is first\n    if (pos1a > pos1b) {\n      final temp = pos1a;\n      pos1a = pos1b;\n      pos1b = temp;\n    }\n    if (pos2a > pos2b) {\n      final temp = pos2a;\n      pos2a = pos2b;\n      pos2b = temp;\n    }\n\n    // Check if one edge's endpoints separate the other edge's endpoints on the circle\n    return (pos1a < pos2a && pos2a < pos1b && pos1b < pos2b) ||\n        (pos2a < pos1a && pos1a < pos2b && pos2b < pos1b);\n  }\n\n  Size _layoutNodes(Graph graph) {\n    // Calculate bounds for auto-sizing\n    var width = 400.0;\n    var height = 400.0;\n\n    if (_radius <= 0) {\n      _radius = 0.35 * max(width, height);\n    }\n\n    final centerX = width / 2;\n    final centerY = height / 2;\n\n    // Position nodes in circle\n    for (var i = 0; i < nodeOrderedList.length; i++) {\n      final node = nodeOrderedList[i];\n      final angle = (2 * pi * i) / nodeOrderedList.length;\n\n      final posX = cos(angle) * _radius + centerX;\n      final posY = sin(angle) * _radius + centerY;\n\n      node.position = Offset(posX, posY);\n    }\n\n    // Calculate actual bounds based on positioned nodes\n    final bounds = graph.calculateGraphBounds();\n    return Size(bounds.width + 40, bounds.height + 40); // Add some padding\n  }\n  \n\n  void _shiftCoordinates(Graph graph, double shiftX, double shiftY) {\n    for (final node in graph.nodes) {\n      node.position = Offset(node.x + shiftX, node.y + shiftY);\n    }\n  }\n\n  @override\n  void init(Graph? graph) {\n    // Implementation can be added if needed\n  }\n\n  @override\n  void setDimensions(double width, double height) {\n    // Implementation can be added if needed\n  }\n\n \n  @override\n  EdgeRenderer? renderer;\n}"
  },
  {
    "path": "lib/tree/RadialTreeLayoutAlgorithm.dart",
    "content": "part of graphview;\n\nclass TreeLayoutNodeData {\n  Rectangle? bounds;\n  int depth = 0;\n  bool visited = false;\n  List<Node> successorNodes = [];\n  Node? parent;\n\n  TreeLayoutNodeData();\n}\n\nclass RadialTreeLayoutAlgorithm extends Algorithm {\n  late BuchheimWalkerConfiguration config;\n  final Map<Node, TreeLayoutNodeData> nodeData = {};\n  final Map<Node, Size> baseBounds = {};\n  final Map<Node, PolarPoint> polarLocations = {};\n\n  RadialTreeLayoutAlgorithm(this.config, EdgeRenderer? renderer) {\n    this.renderer = renderer ?? ArrowEdgeRenderer();\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    if (graph == null || graph.nodes.isEmpty) {\n      return Size.zero;\n    }\n\n    nodeData.clear();\n    baseBounds.clear();\n    polarLocations.clear();\n\n    // Handle single node case\n    if (graph.nodes.length == 1) {\n      final node = graph.nodes.first;\n      node.position = Offset(shiftX + 100, shiftY + 100);\n      return Size(200, 200);\n    }\n\n    _initializeData(graph);\n    final roots = _findRoots(graph);\n\n    if (roots.isEmpty) {\n      final spanningTree = _createSpanningTree(graph);\n      return _layoutSpanningTree(spanningTree, shiftX, shiftY);\n    }\n\n    // First, build the tree using regular tree layout\n    _buildRegularTree(graph, roots);\n\n    // Then convert to radial coordinates\n    _setRadialLocations(graph);\n\n    // Convert polar to cartesian and position nodes\n    _putRadialPointsInModel(graph);\n\n    _shiftCoordinates(graph, shiftX, shiftY);\n\n    return graph.calculateGraphSize();\n  }\n\n  void _initializeData(Graph graph) {\n    // Initialize node data\n    for (final node in graph.nodes) {\n      nodeData[node] = TreeLayoutNodeData();\n    }\n\n    // Build tree structure from edges\n    for (final edge in graph.edges) {\n      final source = edge.source;\n      final target = edge.destination;\n\n      nodeData[source]!.successorNodes.add(target);\n      nodeData[target]!.parent = source;\n    }\n  }\n\n  List<Node> _findRoots(Graph graph) {\n    return graph.nodes.where((node) {\n      return nodeData[node]!.parent == null && successorsOf(node).isNotEmpty;\n    }).toList();\n  }\n\n  void _buildRegularTree(Graph graph, List<Node> roots) {\n    _calculateSubtreeDimensions(roots);\n    _positionNodes(roots);\n  }\n\n  void _calculateSubtreeDimensions(List<Node> roots) {\n    final visited = <Node>{};\n\n    for (final root in roots) {\n      _calculateWidth(root, visited);\n    }\n\n    visited.clear();\n    for (final root in roots) {\n      _calculateHeight(root, visited);\n    }\n  }\n\n  int _calculateWidth(Node node, Set<Node> visited) {\n    if (!visited.add(node)) return 0;\n\n    final children = successorsOf(node);\n    if (children.isEmpty) {\n      final width = max(node.width.toInt(), config.siblingSeparation);\n      baseBounds[node] = Size(width.toDouble(), 0);\n      return width;\n    }\n\n    var totalWidth = 0;\n    for (var i = 0; i < children.length; i++) {\n      totalWidth += _calculateWidth(children[i], visited);\n      if (i < children.length - 1) {\n        totalWidth += config.siblingSeparation;\n      }\n    }\n\n    baseBounds[node] = Size(totalWidth.toDouble(), 0);\n    return totalWidth;\n  }\n\n  int _calculateHeight(Node node, Set<Node> visited) {\n    if (!visited.add(node)) return 0;\n\n    final children = successorsOf(node);\n    if (children.isEmpty) {\n      final height = max(node.height.toInt(), config.levelSeparation);\n      final current = baseBounds[node]!;\n      baseBounds[node] = Size(current.width, height.toDouble());\n      return height;\n    }\n\n    var maxChildHeight = 0;\n    for (final child in children) {\n      maxChildHeight = max(maxChildHeight, _calculateHeight(child, visited));\n    }\n\n    final totalHeight = maxChildHeight + config.levelSeparation;\n    final current = baseBounds[node]!;\n    baseBounds[node] = Size(current.width, totalHeight.toDouble());\n    return totalHeight;\n  }\n\n  void _positionNodes(List<Node> roots) {\n    var currentX = config.siblingSeparation.toDouble();\n\n    for (final root in roots) {\n      final rootWidth = baseBounds[root]!.width;\n      currentX += rootWidth / 2;\n\n      _buildTree(root, currentX, config.levelSeparation.toDouble(), <Node>{});\n\n      currentX += rootWidth / 2 + config.siblingSeparation;\n    }\n  }\n\n  void _buildTree(Node node, double x, double y, Set<Node> visited) {\n    if (!visited.add(node)) return;\n\n    node.position = Offset(x, y);\n\n    final children = successorsOf(node);\n    if (children.isEmpty) return;\n\n    final nextY = y + config.levelSeparation;\n    final totalWidth = baseBounds[node]!.width;\n    var childX = x - totalWidth / 2;\n\n    for (final child in children) {\n      final childWidth = baseBounds[child]!.width;\n      childX += childWidth / 2;\n\n      _buildTree(child, childX, nextY, visited);\n\n      childX += childWidth / 2 + config.siblingSeparation;\n    }\n  }\n\n  void _setRadialLocations(Graph graph) {\n    final bounds = graph.calculateGraphBounds();\n    final maxPoint =  bounds.width;\n\n    // Calculate theta step based on maximum x coordinate\n    final theta = 2 * pi / maxPoint;\n    final deltaRadius = 1.0;\n    final offset = _findRoots(graph).length > 1 ? config.levelSeparation.toDouble() : 0.0;\n\n    for (final node in graph.nodes) {\n      final position = node.position;\n\n      // Convert cartesian tree coordinates to polar coordinates\n      final polarTheta = position.dx * theta;\n      final polarRadius = (offset + position.dy - config.levelSeparation) * deltaRadius;\n\n      final polarPoint = PolarPoint.of(polarTheta, polarRadius);\n      polarLocations[node] = polarPoint;\n    }\n  }\n\n  void _putRadialPointsInModel(Graph graph) {\n    final diameter = _calculateDiameter();\n    final center = diameter * 0.5 * 0.5;\n\n    polarLocations.forEach((node, polarPoint) {\n      final cartesian = polarPoint.toCartesian();\n      node.position = Offset(center + cartesian.dx, center + cartesian.dy);\n    });\n  }\n\n  double _calculateDiameter() {\n    if (polarLocations.isEmpty) return 400.0;\n\n    double maxRadius = 0;\n    polarLocations.values.forEach((polarPoint) {\n      maxRadius = max(maxRadius, polarPoint.radius * 2);\n    });\n\n    return maxRadius + config.siblingSeparation;\n  }\n\n  void _shiftCoordinates(Graph graph, double shiftX, double shiftY) {\n    for (final node in graph.nodes) {\n      node.position = Offset(node.x + shiftX, node.y + shiftY);\n    }\n  }\n\n  Graph _createSpanningTree(Graph graph) {\n    final visited = <Node>{};\n    final spanningEdges = <Edge>[];\n\n    if (graph.nodes.isNotEmpty) {\n      final startNode = graph.nodes.first;\n      final queue = <Node>[startNode];\n      visited.add(startNode);\n\n      while (queue.isNotEmpty) {\n        final current = queue.removeAt(0);\n\n        for (final edge in graph.edges) {\n          Node? neighbor;\n          if (edge.source == current && !visited.contains(edge.destination)) {\n            neighbor = edge.destination;\n            spanningEdges.add(edge);\n          } else if (edge.destination == current && !visited.contains(edge.source)) {\n            neighbor = edge.source;\n            spanningEdges.add(Edge(current, edge.source));\n          }\n\n          if (neighbor != null && !visited.contains(neighbor)) {\n            visited.add(neighbor);\n            queue.add(neighbor);\n          }\n        }\n      }\n    }\n\n    return Graph()..addEdges(spanningEdges);\n  }\n\n  Size _layoutSpanningTree(Graph spanningTree, double shiftX, double shiftY) {\n    nodeData.clear();\n    baseBounds.clear();\n    polarLocations.clear();\n\n    _initializeData(spanningTree);\n    final roots = _findRoots(spanningTree);\n\n    if (roots.isEmpty && spanningTree.nodes.isNotEmpty) {\n      final fakeRoot = spanningTree.nodes.first;\n      _buildRegularTree(spanningTree, [fakeRoot]);\n    } else {\n      _buildRegularTree(spanningTree, roots);\n    }\n\n    _setRadialLocations(spanningTree);\n    _putRadialPointsInModel(spanningTree);\n\n    _shiftCoordinates(spanningTree, shiftX, shiftY);\n    return spanningTree.calculateGraphSize();\n  }\n\n  @override\n  void init(Graph? graph) {\n    // Implementation can be added if needed\n  }\n\n  @override\n  void setDimensions(double width, double height) {\n    // Implementation can be added if needed\n  }\n\n  List<Node> successorsOf(Node? node) {\n    return nodeData[node]!.successorNodes;\n  }\n\n\n\n  @override\n  EdgeRenderer? renderer;\n}"
  },
  {
    "path": "lib/tree/TidierTreeLayoutAlgorithm.dart",
    "content": "part of graphview;\n\nclass TidierTreeNodeData {\n  int mod = 0;\n  Node? thread;\n  int shift = 0;\n  Node? ancestor;\n  int x = 0;\n  int change = 0;\n  int childCount = 0;\n  List<Node> successorNodes = [];\n  List<Node> predecessorNodes = [];\n\n  TidierTreeNodeData();\n}\n\nclass TidierTreeLayoutAlgorithm extends Algorithm {\n  late BuchheimWalkerConfiguration config;\n  final Map<Node, TidierTreeNodeData> nodeData = {};\n  final Map<Node, Size> baseBounds = {};\n  final List<int> heights = [];\n  late List<Node> roots;\n  Rect bounds = Rect.zero;\n  late Graph tree;\n\n  TidierTreeLayoutAlgorithm(this.config, EdgeRenderer? renderer) {\n    this.renderer = renderer ?? TreeEdgeRenderer(config);\n  }\n\n  bool isVertical() {\n    var orientation = config.orientation;\n    return orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM ||\n        orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP;\n  }\n\n  bool needReverseOrder() {\n    var orientation = config.orientation;\n    return orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP ||\n        orientation == BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT;\n  }\n\n  @override\n  Size run(Graph? graph, double shiftX, double shiftY) {\n    if (graph == null || graph.nodes.isEmpty) {\n      return Size.zero;\n    }\n\n    _clearMetadata();\n\n    if (graph.nodes.length == 1) {\n      final node = graph.nodes.first;\n      node.position = Offset(shiftX + 100, shiftY + 100);\n      return Size(200, 200);\n    }\n\n    _buildTree(graph);\n    _applyOrientation(graph);\n    _shiftCoordinates(graph, shiftX, shiftY);\n\n    final size = graph.calculateGraphSize();\n    _clearMetadata();\n    return size;\n  }\n\n  void _clearMetadata() {\n    heights.clear();\n    baseBounds.clear();\n    bounds = Rect.zero;\n  }\n\n  void _buildTree(Graph graph) {\n    nodeData.clear();\n    heights.clear();\n\n    _initializeData(graph);\n    roots = _findRoots(graph);\n\n    if (roots.isEmpty) {\n      final spanningTree = _createSpanningTree(graph);\n      _buildTree(spanningTree);\n      return;\n    }\n\n    tree = graph;\n\n    final virtualRoot = roots.length > 1 ? null : roots.first;\n\n    _firstWalk(virtualRoot, null);\n    _computeMaxHeights(virtualRoot, 0);\n    _secondWalk(\n        virtualRoot, virtualRoot != null ? -_nodeData(virtualRoot).x : 0, 0, 0);\n\n    _normalizePositions(graph);\n  }\n\n  void _initializeData(Graph graph) {\n    // Initialize node data\n    for (final node in graph.nodes) {\n      nodeData[node] = TidierTreeNodeData();\n    }\n\n    // Build tree structure from edges\n    for (final edge in graph.edges) {\n      final source = edge.source;\n      final target = edge.destination;\n\n      nodeData[source]?.successorNodes.add(target);\n      nodeData[target]?.predecessorNodes.add(source);\n    }\n  }\n\n  List<Node> _findRoots(Graph graph) {\n    final incomingCounts = <Node, int>{};\n    for (final node in graph.nodes) {\n      incomingCounts[node] = 0;\n    }\n\n    for (final edge in graph.edges) {\n      incomingCounts[edge.destination] =\n          (incomingCounts[edge.destination] ?? 0) + 1;\n    }\n\n    return graph.nodes.where((node) => incomingCounts[node] == 0).toList();\n  }\n\n  TidierTreeNodeData _nodeData(Node? v) {\n    if (v == null) return TidierTreeNodeData();\n    return nodeData.putIfAbsent(v, () => TidierTreeNodeData());\n  }\n\n  void _firstWalk(Node? v, Node? leftSibling) {\n    if (successorsOf(v).isEmpty) {\n      if (leftSibling != null) {\n        _nodeData(v).x =\n            _nodeData(leftSibling).x + _getDistance(v, leftSibling, true);\n      }\n    } else {\n      final children = successorsOf(v);\n      var defaultAncestor = children.isNotEmpty ? children.first : null;\n      Node? previousChild;\n\n      for (final child in children) {\n        _firstWalk(child, previousChild);\n        defaultAncestor = _apportion(child, defaultAncestor, previousChild, v);\n        previousChild = child;\n      }\n\n      _shift(v);\n\n      final firstChild = children.isNotEmpty ? children.first : null;\n      final lastChild = children.isNotEmpty ? children.last : null;\n\n      if (firstChild != null && lastChild != null) {\n        final midpoint =\n            (_nodeData(firstChild).x + _nodeData(lastChild).x) ~/ 2;\n\n        if (leftSibling != null) {\n          _nodeData(v).x =\n              _nodeData(leftSibling).x + _getDistance(v, leftSibling, true);\n          _nodeData(v).mod = _nodeData(v).x - midpoint;\n        } else {\n          _nodeData(v).x = midpoint;\n        }\n      }\n    }\n  }\n\n  void _secondWalk(Node? v, int m, int depth, int yOffset) {\n    if (v == null) {\n      // Handle multiple roots with subtree separation\n      var rootOffset = 0;\n      for (var i = 0; i < roots.length; i++) {\n        _secondWalk(roots[i], m + rootOffset, depth, yOffset);\n        if (i < roots.length - 1) {\n          rootOffset += config.subtreeSeparation;\n        }\n      }\n      return;\n    }\n\n    final levelHeight =\n        depth < heights.length ? heights[depth] : config.levelSeparation;\n    final x = _nodeData(v).x + m;\n    final y = yOffset + levelHeight ~/ 2;\n\n    v.position = Offset(x.toDouble(), y.toDouble());\n    _updateBounds(v, x, y);\n\n    final children = successorsOf(v);\n    if (children.isNotEmpty) {\n      final newYOffset = yOffset + levelHeight + config.levelSeparation;\n      for (final child in children) {\n        _secondWalk(child, m + _nodeData(v).mod, depth + 1, newYOffset);\n      }\n    }\n  }\n\n  void _updateBounds(Node node, int centerX, int centerY) {\n    final width = node.width.toInt();\n    final height = node.height.toInt();\n    final left = centerX - width ~/ 2;\n    final right = centerX + width ~/ 2;\n    final top = centerY - height ~/ 2;\n    final bottom = centerY + height ~/ 2;\n\n    final nodeBounds = Rect.fromLTRB(\n        left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble());\n    bounds =\n        bounds == Rect.zero ? nodeBounds : bounds.expandToInclude(nodeBounds);\n  }\n\n  void _computeMaxHeights(Node? node, int depth) {\n    if (node == null) {\n      for (final root in roots) {\n        _computeMaxHeights(root, depth);\n      }\n      return;\n    }\n\n    while (heights.length <= depth) {\n      heights.add(0);\n    }\n\n    final nodeHeight = isVertical()\n        ? max(node.height.toInt(), config.levelSeparation)\n        : max(node.width.toInt(), config.levelSeparation);\n    heights[depth] = max(heights[depth], nodeHeight);\n\n    for (final child in successorsOf(node)) {\n      _computeMaxHeights(child, depth + 1);\n    }\n  }\n\n  Node? _leftChild(Node? v) {\n    final children = successorsOf(v);\n    return children.isNotEmpty ? children.first : _nodeData(v).thread;\n  }\n\n  Node? _rightChild(Node? v) {\n    final children = successorsOf(v);\n    return children.isNotEmpty ? children.last : _nodeData(v).thread;\n  }\n\n  int _getDistance(Node? v, Node? w, bool isSibling) {\n    if (v == null || w == null) return config.siblingSeparation;\n\n    // Use appropriate separation based on relationship\n    final separation =\n        isSibling ? config.siblingSeparation : config.subtreeSeparation;\n\n    // Consider node sizes in the calculation\n    final vSize = isVertical() ? v.width.toInt() : v.height.toInt();\n    final wSize = isVertical() ? w.width.toInt() : w.height.toInt();\n\n    return (vSize + wSize) ~/ 2 + separation;\n  }\n\n  Node? _apportion(\n      Node? v, Node? defaultAncestor, Node? leftSibling, Node? parentOfV) {\n    if (leftSibling == null) return defaultAncestor;\n\n    var vor = v;\n    var vir = v;\n    Node? vil = leftSibling;\n    var vol = successorsOf(parentOfV).isNotEmpty\n        ? successorsOf(parentOfV).first\n        : null;\n\n    var innerRight = _nodeData(vir).mod;\n    var outerRight = _nodeData(vor).mod;\n    var innerLeft = _nodeData(vil).mod;\n    var outerLeft = _nodeData(vol).mod;\n\n    var nextRightOfVil = _rightChild(vil);\n    var nextLeftOfVir = _leftChild(vir);\n\n    while (nextRightOfVil != null && nextLeftOfVir != null) {\n      vil = nextRightOfVil;\n      vir = nextLeftOfVir;\n      vol = _leftChild(vol);\n      vor = _rightChild(vor);\n\n      if (vor != null) {\n        _nodeData(vor).ancestor = v;\n      }\n\n      final shift = (_nodeData(vil).x + innerLeft) -\n          (_nodeData(vir).x + innerRight) +\n          _getDistance(vil, vir, true);\n\n      if (shift > 0) {\n        _moveSubtree(\n            _ancestor(vil, parentOfV, defaultAncestor), v, parentOfV, shift);\n        innerRight += shift;\n        outerRight += shift;\n      }\n\n      innerLeft += _nodeData(vil).mod;\n      innerRight += _nodeData(vir).mod;\n      outerLeft += _nodeData(vol).mod;\n      outerRight += _nodeData(vor).mod;\n\n      nextRightOfVil = _rightChild(vil);\n      nextLeftOfVir = _leftChild(vir);\n    }\n\n    if (nextRightOfVil != null && _rightChild(vor) == null) {\n      _nodeData(vor).thread = nextRightOfVil;\n      _nodeData(vor).mod += innerLeft - outerRight;\n    }\n\n    if (nextLeftOfVir != null && _leftChild(vol) == null) {\n      _nodeData(vol).thread = nextLeftOfVir;\n      _nodeData(vol).mod += innerRight - outerLeft;\n      defaultAncestor = v;\n    }\n\n    return defaultAncestor;\n  }\n\n  Node? _ancestor(Node? vil, Node? parentOfV, Node? defaultAncestor) {\n    final ancestor = _nodeData(vil).ancestor ?? vil;\n    final predecessors = predecessorsOf(ancestor!);\n\n    if (predecessors.contains(parentOfV)) {\n      return ancestor;\n    }\n    return defaultAncestor;\n  }\n\n  void _moveSubtree(\n      Node? leftNode, Node? rightNode, Node? parentNode, int shift) {\n    if (leftNode == null || rightNode == null) return;\n\n    final subtreeCount = _childPosition(rightNode, parentNode) -\n        _childPosition(leftNode, parentNode);\n\n    if (subtreeCount > 0) {\n      final rightData = _nodeData(rightNode);\n      final leftData = _nodeData(leftNode);\n\n      rightData.change -= shift ~/ subtreeCount;\n      rightData.shift += shift;\n      leftData.change += shift ~/ subtreeCount;\n      rightData.x += shift;\n      rightData.mod += shift;\n    }\n  }\n\n  int _childPosition(Node? node, Node? parentNode) {\n    if (parentNode == null) {\n      return roots.indexOf(node!) + 1;\n    }\n\n    if (_nodeData(node).childCount != 0) {\n      return _nodeData(node).childCount;\n    }\n\n    final children = successorsOf(parentNode);\n    for (var i = 0; i < children.length; i++) {\n      _nodeData(children[i]).childCount = i + 1;\n    }\n\n    return _nodeData(node).childCount;\n  }\n\n  void _shift(Node? v) {\n    final children = successorsOf(v);\n\n    var shift = 0;\n    var change = 0;\n\n    for (final child in children.reversed) {\n      final childData = _nodeData(child);\n      childData.x += shift;\n      childData.mod += shift;\n      change += childData.change;\n      shift += childData.shift + change;\n    }\n  }\n\n  void _normalizePositions(Graph graph) {\n    final graphBounds = graph.calculateGraphBounds();\n    final xOffset = config.subtreeSeparation - graphBounds.left;\n    final yOffset = config.levelSeparation - graphBounds.top;\n\n    for (final node in graph.nodes) {\n      node.position = Offset(\n        node.x + xOffset,\n        node.y + yOffset,\n      );\n    }\n  }\n\n  void _applyOrientation(Graph graph) {\n    if (config.orientation ==\n        BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) {\n      return;\n    }\n\n    final bounds = graph.calculateGraphBounds();\n    final centerX = bounds.left + bounds.width / 2;\n    final centerY = bounds.top + bounds.height / 2;\n\n    for (final node in graph.nodes) {\n      final x = node.x - centerX;\n      final y = node.y - centerY;\n      Offset newPosition;\n\n      switch (config.orientation) {\n        case BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP:\n          newPosition = Offset(x + centerX, centerY - y);\n          break;\n        case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT:\n          newPosition = Offset(-y + centerX, x + centerY);\n          break;\n        case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT:\n          newPosition = Offset(y + centerX, -x + centerY);\n          break;\n        default:\n          newPosition = node.position;\n          break;\n      }\n\n      node.position = newPosition;\n    }\n  }\n\n  void _shiftCoordinates(Graph graph, double shiftX, double shiftY) {\n    for (final node in graph.nodes) {\n      node.position = Offset(node.x + shiftX, node.y + shiftY);\n    }\n  }\n\n  Graph _createSpanningTree(Graph graph) {\n    final visited = <Node>{};\n    final spanningEdges = <Edge>[];\n\n    if (graph.nodes.isNotEmpty) {\n      final startNode = graph.nodes.first;\n      final queue = <Node>[startNode];\n      visited.add(startNode);\n\n      while (queue.isNotEmpty) {\n        final current = queue.removeAt(0);\n\n        for (final edge in graph.edges) {\n          Node? neighbor;\n          if (edge.source == current && !visited.contains(edge.destination)) {\n            neighbor = edge.destination;\n            spanningEdges.add(edge);\n          } else if (edge.destination == current &&\n              !visited.contains(edge.source)) {\n            neighbor = edge.source;\n            spanningEdges.add(Edge(current, edge.source));\n          }\n\n          if (neighbor != null && !visited.contains(neighbor)) {\n            visited.add(neighbor);\n            queue.add(neighbor);\n          }\n        }\n      }\n    }\n\n    return Graph()..addEdges(spanningEdges);\n  }\n\n  List<Node> successorsOf(Node? v) {\n    if (v == null) return roots;\n    var nodes = nodeData[v]!.successorNodes;\n    return nodes;\n  }\n\n  List<Node> predecessorsOf(Node v) {\n    if (roots.contains(v)) return [];\n\n    return nodeData[v]!.predecessorNodes;\n  }\n\n  @override\n  void init(Graph? graph) {}\n\n  @override\n  void setDimensions(double width, double height) {}\n\n  @override\n  EdgeRenderer? renderer;\n}\n"
  },
  {
    "path": "lib/tree/TreeEdgeRenderer.dart",
    "content": "part of graphview;\n\nclass TreeEdgeRenderer extends EdgeRenderer {\n  BuchheimWalkerConfiguration configuration;\n\n  TreeEdgeRenderer(this.configuration);\n\n  var linePath = Path();\n\n  void render(Canvas canvas, Graph graph, Paint paint) {\n    graph.edges.forEach((edge) {\n      renderEdge(canvas, edge, paint);\n    });\n  }\n\n  @override\n  void renderEdge(Canvas canvas, Edge edge, Paint paint) {\n    final edgePaint = (edge.paint ?? paint)..style = PaintingStyle.stroke;\n    var node = edge.source;\n    var child = edge.destination;\n\n    if (node == child) {\n      final loopPath = buildSelfLoopPath(edge, arrowLength: 0.0);\n      if (loopPath != null) {\n        drawStyledPath(canvas, loopPath.path, edgePaint, lineType: child.lineType);\n      }\n      return;\n    }\n\n    final parentPos = getNodePosition(node);\n    final childPos = getNodePosition(child);\n\n    final orientation = getEffectiveOrientation(node, child);\n\n    linePath.reset();\n    buildEdgePath(node, child, parentPos, childPos, orientation);\n\n    // Check if the destination node has a specific line type\n    final lineType = child.lineType;\n\n    if (lineType != LineType.Default) {\n      // For styled lines, we need to draw path segments with the appropriate style\n      _drawStyledPath(canvas, linePath, edgePaint, lineType);\n    } else {\n      canvas.drawPath(linePath, edgePaint);\n    }\n  }\n\n  /// Draws a path with the specified line type by converting it to line segments\n  void _drawStyledPath(Canvas canvas, Path path, Paint paint, LineType lineType) {\n    // Extract path points for styled rendering\n    final points = _extractPathPoints(path);\n\n    // Draw each segment with the appropriate style\n    for (var i = 0; i < points.length - 1; i++) {\n      drawStyledLine(\n        canvas,\n        points[i],\n        points[i + 1],\n        paint,\n        lineType: lineType,\n      );\n    }\n  }\n\n  /// Extracts key points from a path for segment drawing\n  List<Offset> _extractPathPoints(Path path) {\n    // This is a simplified extraction that works for the L-shaped and curved paths\n    // For more complex paths, you might need a more sophisticated approach\n    final points = <Offset>[];\n    final metrics = path.computeMetrics();\n\n    for (var metric in metrics) {\n      final length = metric.length;\n      const sampleDistance = 10.0; // Sample every 10 pixels\n      var distance = 0.0;\n\n      while (distance <= length) {\n        final tangent = metric.getTangentForOffset(distance);\n        if (tangent != null) {\n          points.add(tangent.position);\n        }\n        distance += sampleDistance;\n      }\n\n      // Add the final point\n      final finalTangent = metric.getTangentForOffset(length);\n      if (finalTangent != null) {\n        points.add(finalTangent.position);\n      }\n    }\n\n    return points;\n  }\n\n  int getEffectiveOrientation(Node node, Node child) {\n    return configuration.orientation;\n  }\n\n  /// Builds the path for the edge based on orientation\n  void buildEdgePath(Node node, Node child, Offset parentPos, Offset childPos, int orientation) {\n    final parentCenterX = parentPos.dx + node.width * 0.5;\n    final parentCenterY = parentPos.dy + node.height * 0.5;\n    final childCenterX = childPos.dx + child.width * 0.5;\n    final childCenterY = childPos.dy + child.height * 0.5;\n\n    if (parentCenterY == childCenterY && parentCenterX == childCenterX) return;\n\n    switch (orientation) {\n      case BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM:\n        buildTopBottomPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY);\n        break;\n\n      case BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP:\n        buildBottomTopPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY);\n        break;\n\n      case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT:\n        buildLeftRightPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY);\n        break;\n\n      case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT:\n        buildRightLeftPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY);\n        break;\n    }\n  }\n\n  /// Builds path for top-bottom orientation\n  void buildTopBottomPath(Node node, Node child, Offset parentPos, Offset childPos,\n      double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) {\n    final parentBottomY = parentPos.dy + node.height * 0.5;\n    final childTopY = childPos.dy + child.height * 0.5;\n    final midY = (parentBottomY + childTopY) * 0.5;\n\n    if (configuration.useCurvedConnections) {\n      // Curved connection\n      linePath\n        ..moveTo(childCenterX, childTopY)\n        ..cubicTo(\n          childCenterX, midY,\n          parentCenterX, midY,\n          parentCenterX, parentBottomY,\n        );\n    } else {\n      // L-shaped connection\n      linePath\n        ..moveTo(parentCenterX, parentBottomY)\n        ..lineTo(parentCenterX, midY)\n        ..lineTo(childCenterX, midY)\n        ..lineTo(childCenterX, childTopY);\n    }\n  }\n\n  /// Builds path for bottom-top orientation\n  void buildBottomTopPath(Node node, Node child, Offset parentPos, Offset childPos,\n      double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) {\n    final parentTopY = parentPos.dy + node.height * 0.5;\n    final childBottomY = childPos.dy + child.height * 0.5;\n    final midY = (parentTopY + childBottomY) * 0.5;\n\n    if (configuration.useCurvedConnections) {\n      linePath\n        ..moveTo(childCenterX, childBottomY)\n        ..cubicTo(\n          childCenterX, midY,\n          parentCenterX, midY,\n          parentCenterX, parentTopY,\n        );\n    } else {\n      linePath\n        ..moveTo(parentCenterX, parentTopY)\n        ..lineTo(parentCenterX, midY)\n        ..lineTo(childCenterX, midY)\n        ..lineTo(childCenterX, childBottomY);\n    }\n  }\n\n  /// Builds path for left-right orientation\n  void buildLeftRightPath(Node node, Node child, Offset parentPos, Offset childPos,\n      double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) {\n    final parentRightX = parentPos.dx + node.width * 0.5;\n    final childLeftX = childPos.dx + child.width * 0.5;\n    final midX = (parentRightX + childLeftX) * 0.5;\n\n    if (configuration.useCurvedConnections) {\n      linePath\n        ..moveTo(childLeftX, childCenterY)\n        ..cubicTo(\n          midX, childCenterY,\n          midX, parentCenterY,\n          parentRightX, parentCenterY,\n        );\n    } else {\n      linePath\n        ..moveTo(parentRightX, parentCenterY)\n        ..lineTo(midX, parentCenterY)\n        ..lineTo(midX, childCenterY)\n        ..lineTo(childLeftX, childCenterY);\n    }\n  }\n\n  /// Builds path for right-left orientation\n  void buildRightLeftPath(Node node, Node child, Offset parentPos, Offset childPos,\n      double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) {\n    final parentLeftX = parentPos.dx + node.width * 0.5;\n    final childRightX = childPos.dx + child.width * 0.5;\n    final midX = (parentLeftX + childRightX) * 0.5;\n\n    if (configuration.useCurvedConnections) {\n      linePath\n        ..moveTo(childRightX, childCenterY)\n        ..cubicTo(\n          midX, childCenterY,\n          midX, parentCenterY,\n          parentLeftX, parentCenterY,\n        );\n    } else {\n      linePath\n        ..moveTo(parentLeftX, parentCenterY)\n        ..lineTo(midX, parentCenterY)\n        ..lineTo(midX, childCenterY)\n        ..lineTo(childRightX, childCenterY);\n    }\n  }\n}\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: graphview\ndescription:  GraphView is used to display data in graph structures. It can display Tree layout, Directed and Layered graph. Useful for Family Tree, Hierarchy View.\nversion: 1.5.1\nhomepage: https://github.com/nabil6391/graphview\n\nenvironment:\n  sdk: '>=2.17.0 <4.0.0'\n  flutter: \">=1.17.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  collection: ^1.15.0\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\nflutter:\n"
  },
  {
    "path": "test/algorithm_performance_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/GraphView.dart';\n\nconst itemHeight = 100.0;\nconst itemWidth = 100.0;\nconst runs = 5;\n\nvoid main() {\n  Graph _createGraph(int n) {\n    final graph = Graph();\n    final nodes = List.generate(n, (i) => Node.Id(i + 1));\n    for (var i = 0; i < n - 1; i++) {\n      final children = (i < n / 3) ? 3 : 2;\n      for (var j = 1; j <= children && i * children + j < n; j++) {\n        graph.addEdge(nodes[i], nodes[i * children + j]);\n      }\n    }\n    for (var i = 0; i < graph.nodeCount(); i++) {\n      graph.getNodeAtPosition(i).size = const Size(itemWidth, itemHeight);\n    }\n    return graph;\n  }\n\n  test('Algorithm performance', () {\n    final algorithms = {\n      'Buchheim': BuchheimWalkerAlgorithm(BuchheimWalkerConfiguration(), null),\n      'Balloon': BalloonLayoutAlgorithm(BuchheimWalkerConfiguration(), null),\n      'RadialTree': RadialTreeLayoutAlgorithm(BuchheimWalkerConfiguration(), null),\n      'TidierTree': TidierTreeLayoutAlgorithm(BuchheimWalkerConfiguration(), null),\n      'Eiglsperger': EiglspergerAlgorithm(SugiyamaConfiguration()),\n      'Sugiyama': SugiyamaAlgorithm(SugiyamaConfiguration()),\n      'Circle': CircleLayoutAlgorithm(CircleLayoutConfiguration(), null),\n    };\n\n    final results = <String, double>{};\n    final graph = _createGraph(1000);\n\n    for (final entry in algorithms.entries) {\n      final times = <int>[];\n      for (var i = 0; i < runs; i++) {\n        final sw = Stopwatch()..start();\n        entry.value.run(graph, 0, 0);\n        times.add(sw.elapsed.inMilliseconds);\n      }\n      // results[entry.key] = times.reduce((a, b) => a + b) / times.length;\n      results[entry.key] = times.reduce((a, b) => a + b).toDouble();\n    }\n\n    final sorted = results.entries.toList()..sort((a, b) => a.value.compareTo(b.value));\n\n    print('\\nPerformance Results (${runs} runs avg):');\n    for (var i = 0; i < sorted.length; i++) {\n      print('${(i + 1).toString().padLeft(2)}. ${sorted[i].key.padRight(12)}: ${sorted[i].value.toStringAsFixed(1)} ms');\n    }\n\n    for (final result in results.values) {\n      expect(result < 30000, true);\n    }\n  });\n}"
  },
  {
    "path": "test/buchheim_walker_algorithm_test.dart",
    "content": "\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/GraphView.dart';\n\nconst itemHeight = 100.0;\nconst itemWidth = 100.0;\n\nvoid main() {\n  group('Buchheim Graph', () {\n    final graph = Graph();\n    final node1 = Node.Id(1);\n    final node2 = Node.Id(2);\n    final node3 = Node.Id(3);\n    final node4 = Node.Id(4);\n    final node5 = Node.Id(5);\n    final node6 = Node.Id(6);\n    final node8 = Node.Id(7);\n    final node7 = Node.Id(8);\n    final node9 = Node.Id(9);\n    final node10 = Node.Id(10);\n    final node11 = Node.Id(11);\n    final node12 = Node.Id(12);\n    graph.addEdge(node1, node2);\n    graph.addEdge(node1, node3, paint: Paint()..color = Colors.red);\n    graph.addEdge(node1, node4, paint: Paint()..color = Colors.blue);\n    graph.addEdge(node2, node5);\n    graph.addEdge(node2, node6);\n    graph.addEdge(node6, node7, paint: Paint()..color = Colors.red);\n    graph.addEdge(node6, node8, paint: Paint()..color = Colors.red);\n    graph.addEdge(node4, node9);\n    graph.addEdge(node4, node10, paint: Paint()..color = Colors.black);\n    graph.addEdge(node4, node11, paint: Paint()..color = Colors.red);\n    graph.addEdge(node11, node12);\n\n    test('Buchheim Node positions are correct for Top_Bottom', () {\n      final _configuration = BuchheimWalkerConfiguration()\n        ..siblingSeparation = (100)\n        ..levelSeparation = (150)\n        ..subtreeSeparation = (150)\n        ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n\n      var algorithm = BuchheimWalkerAlgorithm(\n          _configuration, TreeEdgeRenderer(_configuration));\n\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      var stopwatch = Stopwatch()..start();\n      var size = algorithm.run(graph, 10, 10);\n      var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n      expect(timeTaken < 1000, true);\n\n      expect(graph.getNodeAtPosition(0).position, Offset(385, 10));\n      expect(graph.getNodeAtPosition(6).position, Offset(110.0, 760.0));\n      expect(graph.getNodeUsingId(3).position, Offset(385.0, 260.0));\n      expect(graph.getNodeUsingId(4).position, Offset(660.0, 260.0));\n\n      expect(size, Size(950.0, 850.0));\n    });\n\n    test('Buchheim detects cyclic dependencies', () {\n      // Create a graph with a cycle\n      final cyclicGraph = Graph();\n      final nodeA = Node.Id('A');\n      final nodeB = Node.Id('B');\n      final nodeC = Node.Id('C');\n\n      // Create cycle: A -> B -> C -> A\n      cyclicGraph.addEdge(nodeA, nodeB);\n      cyclicGraph.addEdge(nodeB, nodeC);\n      cyclicGraph.addEdge(nodeC, nodeA); // This creates the cycle\n\n      for (var i = 0; i < cyclicGraph.nodeCount(); i++) {\n        cyclicGraph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      final configuration = BuchheimWalkerConfiguration()\n        ..siblingSeparation = (100)\n        ..levelSeparation = (150)\n        ..subtreeSeparation = (150)\n        ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n\n      var algorithm = BuchheimWalkerAlgorithm(\n          configuration, TreeEdgeRenderer(configuration));\n\n      // Should throw exception when cycle is detected\n      expect(\n            () => algorithm.run(cyclicGraph, 0, 0),\n        throwsA(isA<Exception>().having(\n              (e) => e.toString(),\n          'message',\n          contains('Cyclic dependency detected'),\n        )),\n      );\n    });\n\n    test('Buchheim Performance for 1000 nodes to be less than 40ms', () {\n      Graph _createGraph(int n) {\n        final graph = Graph();\n        final nodes = List.generate(n, (i) => Node.Id(i + 1));\n        var currentChild = 1; // Start from node 1 (node 0 is root)\n        for (var i = 0; i < n && currentChild < n; i++) {\n          final children = (i < n ~/ 3) ? 3 : 2;\n\n          for (var j = 0; j < children && currentChild < n; j++) {\n            graph.addEdge(nodes[i], nodes[currentChild]);\n            currentChild++;\n          }\n        }\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = const Size(itemWidth, itemHeight);\n        }\n        return graph;\n      }\n\n      final _configuration = BuchheimWalkerConfiguration()\n        ..siblingSeparation = (100)\n        ..levelSeparation = (150)\n        ..subtreeSeparation = (150)\n        ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n\n      var algorithm = BuchheimWalkerAlgorithm(\n          _configuration, TreeEdgeRenderer(_configuration));\n\n      var graph = _createGraph(1000);\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      var stopwatch = Stopwatch()..start();\n      algorithm.run(graph, 0, 0);\n      var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n      print('Timetaken $timeTaken for ${graph.nodeCount()} nodes');\n\n      expect(timeTaken < 40, true);\n    });\n  });\n}\n"
  },
  {
    "path": "test/controller_tests.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/GraphView.dart';\n\nvoid main() {\n  group('GraphView Controller Tests', () {\n    testWidgets('animateToNode centers the target node',\n        (WidgetTester tester) async {\n      // Setup graph\n      final graph = Graph();\n      final targetNode = Node.Id('target');\n      targetNode.key = const ValueKey('target');\n      final otherNode = Node.Id('other');\n\n      graph.addEdge(targetNode, otherNode);\n\n      final transformationController = TransformationController();\n      final controller = GraphViewController(\n          transformationController: transformationController);\n      final configuration = BuchheimWalkerConfiguration();\n      final algorithm = BuchheimWalkerAlgorithm(\n          configuration, TreeEdgeRenderer(configuration));\n\n      // Build widget\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Scaffold(\n            body: SizedBox(\n              width: 400,\n              height: 600,\n              child: GraphView.builder(\n                graph: graph,\n                algorithm: algorithm,\n                controller: controller,\n                builder: (node) => Container(\n                  width: 100,\n                  height: 50,\n                  color: Colors.blue,\n                  child: Text(node.key?.value ?? ''),\n                ),\n              ),\n            ),\n          ),\n        ),\n      );\n\n      await tester.pumpAndSettle();\n\n      // Get the actual position of target node after algorithm runs\n      final actualNodePosition = targetNode.position;\n      final nodeCenter = Offset(\n        actualNodePosition.dx + targetNode.width / 2,\n        actualNodePosition.dy + targetNode.height / 2,\n      );\n\n      // Get initial transformation\n      final initialMatrix = transformationController.value;\n\n      // Animate to target node\n      controller.animateToNode(const ValueKey('target'));\n\n      // Let animation complete\n      await tester.pump(const Duration(milliseconds: 100));\n      await tester.pumpAndSettle();\n\n      // Verify transformation changed\n      final finalMatrix = transformationController.value;\n      expect(finalMatrix, isNot(equals(initialMatrix)));\n\n      // With viewport size 400x600, center should be at (200, 300)\n      // Expected translation should center the node at viewport center\n      final expectedTranslationX =\n          200 - nodeCenter.dx; // viewport_center_x - node_center_x\n      final expectedTranslationY =\n          300 - nodeCenter.dy; // viewport_center_y - node_center_y\n\n      expect(finalMatrix.getTranslation().x, closeTo(expectedTranslationX, 5));\n      expect(finalMatrix.getTranslation().y, closeTo(expectedTranslationY, 5));\n    });\n\n    testWidgets('animateToNode handles non-existent node gracefully',\n        (WidgetTester tester) async {\n      final graph = Graph();\n      final node = Node.Id('exists');\n      graph.nodes.add(node);\n\n      final transformationController = TransformationController();\n      final controller = GraphViewController(\n          transformationController: transformationController);\n      final algorithm = BuchheimWalkerAlgorithm(BuchheimWalkerConfiguration(),\n          TreeEdgeRenderer(BuchheimWalkerConfiguration()));\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: GraphView.builder(\n            graph: graph,\n            algorithm: algorithm,\n            controller: controller,\n            builder: (node) => Container(),\n          ),\n        ),\n      );\n\n      await tester.pumpAndSettle();\n\n      final initialMatrix = transformationController.value;\n\n      // Try to animate to non-existent node\n      controller.animateToNode(const ValueKey('nonexistent'));\n      await tester.pumpAndSettle();\n\n      // Matrix should remain unchanged\n      final finalMatrix = transformationController.value;\n      expect(finalMatrix, equals(initialMatrix));\n    });\n  });\n\n  group('Collapse Tests', () {\n    late Graph graph;\n    late GraphViewController controller;\n\n    setUp(() {\n      graph = Graph();\n      controller = GraphViewController();\n    });\n\n    // Helper function to create a graph with multiple branches\n    Graph createComplexGraph() {\n      final g = Graph();\n\n      final root = Node.Id(0);\n      final branch1 = Node.Id(1);\n      final branch2 = Node.Id(2);\n      final leaf1 = Node.Id(3);\n      final leaf2 = Node.Id(4);\n      final leaf3 = Node.Id(5);\n      final leaf4 = Node.Id(6);\n\n      g.addEdge(root, branch1);\n      g.addEdge(root, branch2);\n      g.addEdge(branch1, leaf1);\n      g.addEdge(branch1, leaf2);\n      g.addEdge(branch2, leaf3);\n      g.addEdge(branch2, leaf4);\n\n      return g;\n    }\n\n    test('Complex graph - multiple branches', () {\n      final g = createComplexGraph();\n      final root = g.getNodeAtPosition(0);\n\n      controller.collapseNode(g, root);\n\n      final edges = controller.getCollapsingEdges(g);\n\n      // Should get all 6 edges (root->branch1, root->branch2, branch1->leaf1, branch1->leaf2, branch2->leaf3, branch2->leaf4)\n      expect(edges.length, 6);\n    });\n\n    test('Nested collapse preserves original hide relationships', () {\n      final graph = Graph();\n      final parent = Node.Id(0);\n      final child = Node.Id(1);\n      final grandchild = Node.Id(2);\n\n      graph.addEdge(parent, child);\n      graph.addEdge(child, grandchild);\n\n      final controller = GraphViewController();\n\n      // Step 1: Collapse child\n      controller.collapseNode(graph, child);\n\n      expect(controller.isNodeVisible(graph, parent), true);\n      expect(controller.isNodeVisible(graph, child), true);\n      expect(controller.isNodeVisible(graph, grandchild), false);\n      expect(\n          controller.hiddenBy[grandchild], child); // grandchild hidden by child\n\n      // Step 2: Collapse parent\n      controller.collapseNode(graph, parent);\n\n      expect(controller.isNodeVisible(graph, parent), true);\n      expect(controller.isNodeVisible(graph, child), false);\n      expect(controller.isNodeVisible(graph, grandchild), false);\n      expect(controller.hiddenBy[child], parent); // child hidden by parent\n      expect(controller.hiddenBy[grandchild],\n          child); // grandchild STILL hidden by child!\n\n      // Step 3: Get collapsing edges for parent\n      controller.collapsedNode = parent;\n      final parentEdges = controller.getCollapsingEdges(graph);\n\n      // Should only include parent -> child, NOT child -> grandchild\n      expect(parentEdges.length, 1);\n      expect(parentEdges.first.source, parent);\n      expect(parentEdges.first.destination, child);\n\n      // Step 4: Expand parent\n      controller.expandNode(graph, parent);\n\n      expect(controller.isNodeVisible(graph, parent), true);\n      expect(controller.isNodeVisible(graph, child), true);\n      expect(\n          controller.isNodeVisible(graph, grandchild), false); // Still hidden!\n      expect(controller.hiddenBy[grandchild], child); // Still hidden by child!\n\n      // Step 5: Expand child\n      controller.expandNode(graph, child);\n\n      expect(controller.isNodeVisible(graph, parent), true);\n      expect(controller.isNodeVisible(graph, child), true);\n      expect(controller.isNodeVisible(graph, grandchild), true); // Now visible!\n      expect(controller.hiddenBy.containsKey(grandchild), false);\n    });\n  });\n}\n"
  },
  {
    "path": "test/example_trees.dart",
    "content": "const exampleTreeWith140Nodes = {\n  'edges': [\n    {'from': '7045321', 'to': '308264215'},\n    {'from': '308264215', 'to': '205893853'},\n    {'from': '205893853', 'to': '673966248'},\n    {'from': '673966248', 'to': '358204164'},\n    {'from': '358204164', 'to': '215888392'},\n    {'from': '215888392', 'to': '403621992'},\n    {'from': '215888392', 'to': '777909510'},\n    {'from': '777909510', 'to': '100213815'},\n    {'from': '100213815', 'to': '499504374'},\n    {'from': '499504374', 'to': '855703404'},\n    {'from': '499504374', 'to': '991104907'},\n    {'from': '991104907', 'to': '374555325'},\n    {'from': '991104907', 'to': '58236163'},\n    {'from': '991104907', 'to': '1051662797'},\n    {'from': '1051662797', 'to': '523457656'},\n    {'from': '1051662797', 'to': '178236248'},\n    {'from': '178236248', 'to': '403818044'},\n    {'from': '403818044', 'to': '633692579'},\n    {'from': '403818044', 'to': '326876433'},\n    {'from': '178236248', 'to': '294992198'},\n    {'from': '178236248', 'to': '207728643'},\n    {'from': '207728643', 'to': '474861525'},\n    {'from': '207728643', 'to': '704015142'},\n    {'from': '704015142', 'to': '891912594'},\n    {'from': '704015142', 'to': '93790829'},\n    {'from': '704015142', 'to': '713878610'},\n    {'from': '704015142', 'to': '568109301'},\n    {'from': '100213815', 'to': '298138012'},\n    {'from': '298138012', 'to': '1051662797'},\n    {'from': '777909510', 'to': '344277619'},\n    {'from': '344277619', 'to': '311541390'},\n    {'from': '311541390', 'to': '761787449'},\n    {'from': '761787449', 'to': '30973213'},\n    {'from': '30973213', 'to': '523457656'},\n    {'from': '30973213', 'to': '178236248'},\n    {'from': '761787449', 'to': '259733602'},\n    {'from': '311541390', 'to': '128821445'},\n    {'from': '344277619', 'to': '1003131136'},\n    {'from': '1003131136', 'to': '130000569'},\n    {'from': '1003131136', 'to': '319536467'},\n    {'from': '319536467', 'to': '299942125'},\n    {'from': '299942125', 'to': '178926206'},\n    {'from': '299942125', 'to': '675835322'},\n    {'from': '299942125', 'to': '1000135767'},\n    {'from': '319536467', 'to': '483940059'},\n    {'from': '483940059', 'to': '497866879'},\n    {'from': '483940059', 'to': '606660618'},\n    {'from': '483940059', 'to': '841482899'},\n    {'from': '358204164', 'to': '963021319'},\n    {'from': '963021319', 'to': '130000569'},\n    {'from': '963021319', 'to': '319536467'},\n    {'from': '358204164', 'to': '803634418'},\n    {'from': '803634418', 'to': '142291521'},\n    {'from': '142291521', 'to': '525361131'},\n    {'from': '525361131', 'to': '422007713'},\n    {'from': '422007713', 'to': '184596308'},\n    {'from': '422007713', 'to': '1020140270'},\n    {'from': '422007713', 'to': '779910731'},\n    {'from': '525361131', 'to': '859310299'},\n    {'from': '859310299', 'to': '514613187'},\n    {'from': '514613187', 'to': '680752017'},\n    {'from': '680752017', 'to': '1058283666'},\n    {'from': '680752017', 'to': '887688252'},\n    {'from': '680752017', 'to': '717256682'},\n    {'from': '717256682', 'to': '409719617'},\n    {'from': '409719617', 'to': '1014464856'},\n    {'from': '1014464856', 'to': '773448863'},\n    {'from': '773448863', 'to': '988347957'},\n    {'from': '773448863', 'to': '152738454'},\n    {'from': '773448863', 'to': '338899146'},\n    {'from': '1014464856', 'to': '629986173'},\n    {'from': '629986173', 'to': '773448863'},\n    {'from': '629986173', 'to': '835742723'},\n    {'from': '1014464856', 'to': '835742723'},\n    {'from': '409719617', 'to': '81570852'},\n    {'from': '717256682', 'to': '136164004'},\n    {'from': '136164004', 'to': '852978894'},\n    {'from': '852978894', 'to': '344862780'},\n    {'from': '344862780', 'to': '1001389664'},\n    {'from': '1001389664', 'to': '404010795'},\n    {'from': '1001389664', 'to': '644174136'},\n    {'from': '644174136', 'to': '979597620'},\n    {'from': '979597620', 'to': '267068484'},\n    {'from': '979597620', 'to': '660658782'},\n    {'from': '644174136', 'to': '1041729484'},\n    {'from': '1041729484', 'to': '184754595'},\n    {'from': '184754595', 'to': '564383463'},\n    {'from': '564383463', 'to': '328736689'},\n    {'from': '564383463', 'to': '371898357'},\n    {'from': '371898357', 'to': '1035929373'},\n    {'from': '1035929373', 'to': '619697312'},\n    {'from': '619697312', 'to': '64229994'},\n    {'from': '619697312', 'to': '865071585'},\n    {'from': '619697312', 'to': '834626072'},\n    {'from': '1035929373', 'to': '201892784'},\n    {'from': '201892784', 'to': '160374239'},\n    {'from': '201892784', 'to': '925759772'},\n    {'from': '371898357', 'to': '601412432'},\n    {'from': '184754595', 'to': '371898357'},\n    {'from': '1041729484', 'to': '371898357'},\n    {'from': '344862780', 'to': '409719617'},\n    {'from': '852978894', 'to': '63704729'},\n    {'from': '136164004', 'to': '293710340'},\n    {'from': '514613187', 'to': '136164004'},\n    {'from': '859310299', 'to': '81570852'},\n    {'from': '859310299', 'to': '1014464856'},\n    {'from': '142291521', 'to': '985700044'},\n    {'from': '142291521', 'to': '756415350'},\n    {'from': '803634418', 'to': '420237319'},\n    {'from': '420237319', 'to': '450548638'},\n    {'from': '420237319', 'to': '210548489'},\n    {'from': '210548489', 'to': '809729654'},\n    {'from': '210548489', 'to': '736196011'},\n    {'from': '736196011', 'to': '763132131'},\n    {'from': '763132131', 'to': '139733908'},\n    {'from': '139733908', 'to': '141077435'},\n    {'from': '139733908', 'to': '601580192'},\n    {'from': '601580192', 'to': '29466216'},\n    {'from': '601580192', 'to': '530702767'},\n    {'from': '530702767', 'to': '1181832'},\n    {'from': '530702767', 'to': '514613187'},\n    {'from': '530702767', 'to': '1014464856'},\n    {'from': '139733908', 'to': '530702767'},\n    {'from': '763132131', 'to': '805599981'},\n    {'from': '805599981', 'to': '596402985'},\n    {'from': '805599981', 'to': '207631270'},\n    {'from': '207631270', 'to': '528636695'},\n    {'from': '207631270', 'to': '142291521'},\n    {'from': '805599981', 'to': '148019367'},\n    {'from': '148019367', 'to': '894038421'},\n    {'from': '148019367', 'to': '544426319'},\n    {'from': '148019367', 'to': '878212306'},\n    {'from': '878212306', 'to': '94541671'},\n    {'from': '878212306', 'to': '1007715424'},\n    {'from': '1007715424', 'to': '258386700'},\n    {'from': '1007715424', 'to': '546819439'},\n    {'from': '546819439', 'to': '836825089'},\n    {'from': '836825089', 'to': '16287329'},\n    {'from': '836825089', 'to': '256254716'},\n    {'from': '256254716', 'to': '631230382'},\n    {'from': '631230382', 'to': '900886483'},\n    {'from': '631230382', 'to': '133436503'},\n    {'from': '256254716', 'to': '751624200'},\n    {'from': '836825089', 'to': '716757473'},\n    {'from': '546819439', 'to': '470041669'},\n    {'from': '546819439', 'to': '180888016'},\n    {'from': '736196011', 'to': '901547914'},\n    {'from': '901547914', 'to': '425184961'},\n    {'from': '425184961', 'to': '760673978'},\n    {'from': '760673978', 'to': '825228914'},\n    {'from': '760673978', 'to': '530702767'},\n    {'from': '425184961', 'to': '955125232'},\n    {'from': '955125232', 'to': '167653392'},\n    {'from': '955125232', 'to': '530702767'},\n    {'from': '901547914', 'to': '530702767'},\n    {'from': '210548489', 'to': '640144001'},\n    {'from': '640144001', 'to': '135966238'},\n    {'from': '640144001', 'to': '959156288'},\n    {'from': '803634418', 'to': '358204164'},\n    {'from': '673966248', 'to': '803634418'},\n    {'from': '308264215', 'to': '1039602752'},\n    {'from': '1039602752', 'to': '673966248'}\n  ]\n};\n\nfinal exampleTrees = <Map<String, Object>>[\n  {\n    'edges': [\n      {'from': '7045321', 'to': '308264215'},\n      {'from': '308264215', 'to': '205893853'},\n      {'from': '205893853', 'to': '673966248'},\n      {'from': '673966248', 'to': '358204164'},\n      {'from': '358204164', 'to': '215888392'},\n      {'from': '215888392', 'to': '403621992'},\n      {'from': '215888392', 'to': '777909510'},\n      {'from': '777909510', 'to': '100213815'},\n      {'from': '100213815', 'to': '499504374'},\n      {'from': '499504374', 'to': '855703404'},\n      {'from': '499504374', 'to': '991104907'},\n      {'from': '991104907', 'to': '374555325'},\n      {'from': '991104907', 'to': '58236163'},\n      {'from': '991104907', 'to': '1051662797'},\n      {'from': '1051662797', 'to': '523457656'},\n      {'from': '1051662797', 'to': '178236248'},\n      {'from': '178236248', 'to': '403818044'},\n      {'from': '403818044', 'to': '633692579'},\n      {'from': '403818044', 'to': '326876433'},\n      {'from': '178236248', 'to': '294992198'},\n      {'from': '178236248', 'to': '207728643'},\n      {'from': '207728643', 'to': '474861525'},\n      {'from': '207728643', 'to': '704015142'},\n      {'from': '704015142', 'to': '891912594'},\n      {'from': '704015142', 'to': '93790829'},\n      {'from': '704015142', 'to': '713878610'},\n      {'from': '704015142', 'to': '568109301'},\n      {'from': '100213815', 'to': '298138012'},\n      {'from': '298138012', 'to': '1051662797'},\n      {'from': '777909510', 'to': '344277619'},\n      {'from': '344277619', 'to': '311541390'},\n      {'from': '311541390', 'to': '761787449'},\n      {'from': '761787449', 'to': '30973213'},\n      {'from': '30973213', 'to': '523457656'},\n      {'from': '30973213', 'to': '178236248'},\n      {'from': '761787449', 'to': '259733602'},\n      {'from': '311541390', 'to': '128821445'},\n      {'from': '344277619', 'to': '1003131136'},\n      {'from': '1003131136', 'to': '130000569'},\n      {'from': '1003131136', 'to': '319536467'},\n      {'from': '319536467', 'to': '299942125'},\n      {'from': '299942125', 'to': '178926206'},\n      {'from': '299942125', 'to': '675835322'},\n      {'from': '299942125', 'to': '1000135767'},\n      {'from': '319536467', 'to': '483940059'},\n      {'from': '483940059', 'to': '497866879'},\n      {'from': '483940059', 'to': '606660618'},\n      {'from': '483940059', 'to': '841482899'},\n      {'from': '358204164', 'to': '963021319'},\n      {'from': '963021319', 'to': '130000569'},\n      {'from': '963021319', 'to': '319536467'},\n      {'from': '358204164', 'to': '803634418'},\n      {'from': '803634418', 'to': '142291521'},\n      {'from': '142291521', 'to': '525361131'},\n      {'from': '525361131', 'to': '422007713'},\n      {'from': '422007713', 'to': '184596308'},\n      {'from': '422007713', 'to': '1020140270'},\n      {'from': '422007713', 'to': '779910731'},\n      {'from': '525361131', 'to': '859310299'},\n      {'from': '859310299', 'to': '514613187'},\n      {'from': '514613187', 'to': '680752017'},\n      {'from': '680752017', 'to': '1058283666'},\n      {'from': '680752017', 'to': '887688252'},\n      {'from': '680752017', 'to': '717256682'},\n      {'from': '717256682', 'to': '409719617'},\n      {'from': '409719617', 'to': '1014464856'},\n      {'from': '1014464856', 'to': '773448863'},\n      {'from': '773448863', 'to': '988347957'},\n      {'from': '773448863', 'to': '152738454'},\n      {'from': '773448863', 'to': '338899146'},\n      {'from': '1014464856', 'to': '629986173'},\n      {'from': '629986173', 'to': '773448863'},\n      {'from': '629986173', 'to': '835742723'},\n      {'from': '1014464856', 'to': '835742723'},\n      {'from': '409719617', 'to': '81570852'},\n      {'from': '717256682', 'to': '136164004'},\n      {'from': '136164004', 'to': '852978894'},\n      {'from': '852978894', 'to': '344862780'},\n      {'from': '344862780', 'to': '1001389664'},\n      {'from': '1001389664', 'to': '404010795'},\n      {'from': '1001389664', 'to': '644174136'},\n      {'from': '644174136', 'to': '979597620'},\n      {'from': '979597620', 'to': '267068484'},\n      {'from': '979597620', 'to': '660658782'},\n      {'from': '644174136', 'to': '1041729484'},\n      {'from': '1041729484', 'to': '184754595'},\n      {'from': '184754595', 'to': '564383463'},\n      {'from': '564383463', 'to': '328736689'},\n      {'from': '564383463', 'to': '371898357'},\n      {'from': '371898357', 'to': '1035929373'},\n      {'from': '1035929373', 'to': '619697312'},\n      {'from': '619697312', 'to': '64229994'},\n      {'from': '619697312', 'to': '865071585'},\n      {'from': '619697312', 'to': '834626072'},\n      {'from': '1035929373', 'to': '201892784'},\n      {'from': '201892784', 'to': '160374239'},\n      {'from': '201892784', 'to': '925759772'},\n      {'from': '371898357', 'to': '601412432'},\n      {'from': '184754595', 'to': '371898357'},\n      {'from': '1041729484', 'to': '371898357'},\n      {'from': '344862780', 'to': '409719617'},\n      {'from': '852978894', 'to': '63704729'},\n      {'from': '136164004', 'to': '293710340'},\n      {'from': '514613187', 'to': '136164004'},\n      {'from': '859310299', 'to': '81570852'},\n      {'from': '859310299', 'to': '1014464856'},\n      {'from': '142291521', 'to': '985700044'},\n      {'from': '142291521', 'to': '756415350'},\n      {'from': '803634418', 'to': '420237319'},\n      {'from': '420237319', 'to': '450548638'},\n      {'from': '420237319', 'to': '210548489'},\n      {'from': '210548489', 'to': '809729654'},\n      {'from': '210548489', 'to': '736196011'},\n      {'from': '736196011', 'to': '763132131'},\n      {'from': '763132131', 'to': '139733908'},\n      {'from': '139733908', 'to': '141077435'},\n      {'from': '139733908', 'to': '601580192'},\n      {'from': '601580192', 'to': '29466216'},\n      {'from': '601580192', 'to': '530702767'},\n      {'from': '530702767', 'to': '1181832'},\n      {'from': '530702767', 'to': '514613187'},\n      {'from': '530702767', 'to': '1014464856'},\n      {'from': '139733908', 'to': '530702767'},\n      {'from': '763132131', 'to': '805599981'},\n      {'from': '805599981', 'to': '596402985'},\n      {'from': '805599981', 'to': '207631270'},\n      {'from': '207631270', 'to': '528636695'},\n      {'from': '207631270', 'to': '142291521'},\n      {'from': '805599981', 'to': '148019367'},\n      {'from': '148019367', 'to': '894038421'},\n      {'from': '148019367', 'to': '544426319'},\n      {'from': '148019367', 'to': '878212306'},\n      {'from': '878212306', 'to': '94541671'},\n      {'from': '878212306', 'to': '1007715424'},\n      {'from': '1007715424', 'to': '258386700'},\n      {'from': '1007715424', 'to': '546819439'},\n      {'from': '546819439', 'to': '836825089'},\n      {'from': '836825089', 'to': '16287329'},\n      {'from': '836825089', 'to': '256254716'},\n      {'from': '256254716', 'to': '631230382'},\n      {'from': '631230382', 'to': '900886483'},\n      {'from': '631230382', 'to': '133436503'},\n      {'from': '256254716', 'to': '751624200'},\n      {'from': '836825089', 'to': '716757473'},\n      {'from': '546819439', 'to': '470041669'},\n      {'from': '546819439', 'to': '180888016'},\n      {'from': '736196011', 'to': '901547914'},\n      {'from': '901547914', 'to': '425184961'},\n      {'from': '425184961', 'to': '760673978'},\n      {'from': '760673978', 'to': '825228914'},\n      {'from': '760673978', 'to': '530702767'},\n      {'from': '425184961', 'to': '955125232'},\n      {'from': '955125232', 'to': '167653392'},\n      {'from': '955125232', 'to': '530702767'},\n      {'from': '901547914', 'to': '530702767'},\n      {'from': '210548489', 'to': '640144001'},\n      {'from': '640144001', 'to': '135966238'},\n      {'from': '640144001', 'to': '959156288'},\n      {'from': '803634418', 'to': '358204164'},\n      {'from': '673966248', 'to': '803634418'},\n      {'from': '308264215', 'to': '1039602752'},\n      {'from': '1039602752', 'to': '673966248'}\n    ]\n  },\n  {\n    'edges': [\n      {'from': '651372822', 'to': '780273411'},\n      {'from': '780273411', 'to': '347969226'},\n      {'from': '347969226', 'to': '157648240'},\n      {'from': '157648240', 'to': '676569359'},\n      {'from': '676569359', 'to': '91606809'},\n      {'from': '676569359', 'to': '154477528'},\n      {'from': '676569359', 'to': '843017499'},\n      {'from': '843017499', 'to': '983981562'},\n      {'from': '843017499', 'to': '504040588'},\n      {'from': '504040588', 'to': '446062329'},\n      {'from': '446062329', 'to': '622974985'},\n      {'from': '622974985', 'to': '1044667060'},\n      {'from': '622974985', 'to': '556331086'},\n      {'from': '556331086', 'to': '995470137'},\n      {'from': '995470137', 'to': '1056219149'},\n      {'from': '1056219149', 'to': '239427950'},\n      {'from': '995470137', 'to': '239427950'},\n      {'from': '995470137', 'to': '175942639'},\n      {'from': '175942639', 'to': '239427950'},\n      {'from': '995470137', 'to': '914018177'},\n      {'from': '914018177', 'to': '239427950'},\n      {'from': '556331086', 'to': '776412718'},\n      {'from': '776412718', 'to': '311423239'},\n      {'from': '311423239', 'to': '71054174'},\n      {'from': '71054174', 'to': '436868910'},\n      {'from': '436868910', 'to': '86163114'},\n      {'from': '86163114', 'to': '876219077'},\n      {'from': '436868910', 'to': '385178969'},\n      {'from': '385178969', 'to': '18115125'},\n      {'from': '71054174', 'to': '869070735'},\n      {'from': '776412718', 'to': '71054174'},\n      {'from': '776412718', 'to': '978694637'},\n      {'from': '978694637', 'to': '71054174'},\n      {'from': '776412718', 'to': '481786088'},\n      {'from': '481786088', 'to': '71054174'},\n      {'from': '622974985', 'to': '657744632'},\n      {'from': '657744632', 'to': '995470137'},\n      {'from': '657744632', 'to': '776412718'},\n      {'from': '622974985', 'to': '398317434'},\n      {'from': '843017499', 'to': '441827615'},\n      {'from': '843017499', 'to': '345074369'},\n      {'from': '345074369', 'to': '983981562'},\n      {'from': '345074369', 'to': '504040588'},\n      {'from': '345074369', 'to': '441827615'},\n      {'from': '843017499', 'to': '1038969179'},\n      {'from': '1038969179', 'to': '983981562'},\n      {'from': '1038969179', 'to': '504040588'},\n      {'from': '1038969179', 'to': '441827615'},\n      {'from': '1038969179', 'to': '345074369'},\n      {'from': '676569359', 'to': '582216004'},\n      {'from': '582216004', 'to': '983981562'},\n      {'from': '582216004', 'to': '853366903'},\n      {'from': '853366903', 'to': '549040211'},\n      {'from': '549040211', 'to': '438987595'},\n      {'from': '438987595', 'to': '1044667060'},\n      {'from': '438987595', 'to': '927647245'},\n      {'from': '927647245', 'to': '995470137'},\n      {'from': '927647245', 'to': '286211157'},\n      {'from': '286211157', 'to': '466182692'},\n      {'from': '466182692', 'to': '724424756'},\n      {'from': '724424756', 'to': '739317534'},\n      {'from': '739317534', 'to': '315526883'},\n      {'from': '724424756', 'to': '869070735'},\n      {'from': '286211157', 'to': '724424756'},\n      {'from': '286211157', 'to': '175042175'},\n      {'from': '175042175', 'to': '724424756'},\n      {'from': '286211157', 'to': '567113513'},\n      {'from': '567113513', 'to': '724424756'},\n      {'from': '438987595', 'to': '625227999'},\n      {'from': '625227999', 'to': '995470137'},\n      {'from': '625227999', 'to': '286211157'},\n      {'from': '438987595', 'to': '398317434'},\n      {'from': '582216004', 'to': '441827615'},\n      {'from': '582216004', 'to': '306330186'},\n      {'from': '306330186', 'to': '983981562'},\n      {'from': '306330186', 'to': '853366903'},\n      {'from': '306330186', 'to': '441827615'},\n      {'from': '582216004', 'to': '476307185'},\n      {'from': '476307185', 'to': '983981562'},\n      {'from': '476307185', 'to': '853366903'},\n      {'from': '476307185', 'to': '441827615'},\n      {'from': '157648240', 'to': '1031140514'},\n      {'from': '1031140514', 'to': '983981562'},\n      {'from': '1031140514', 'to': '329379632'},\n      {'from': '1031140514', 'to': '441827615'},\n      {'from': '1031140514', 'to': '722519336'},\n      {'from': '722519336', 'to': '983981562'},\n      {'from': '722519336', 'to': '329379632'},\n      {'from': '722519336', 'to': '441827615'},\n      {'from': '722519336', 'to': '431136131'},\n      {'from': '431136131', 'to': '329379632'},\n      {'from': '1031140514', 'to': '431136131'},\n      {'from': '347969226', 'to': '91606809'},\n      {'from': '347969226', 'to': '154477528'},\n      {'from': '347969226', 'to': '843017499'},\n      {'from': '347969226', 'to': '582216004'},\n      {'from': '780273411', 'to': '383221931'}\n    ],\n  },\n  {\n    'edges': [\n      {'from': '426129611', 'to': '118422731'},\n      {'from': '118422731', 'to': '471276419'},\n      {'from': '471276419', 'to': '487798489'},\n      {'from': '487798489', 'to': '699788790'},\n      {'from': '699788790', 'to': '757478418'},\n      {'from': '757478418', 'to': '551628972'},\n      {'from': '551628972', 'to': '648068209'},\n      {'from': '648068209', 'to': '307394546'},\n      {'from': '307394546', 'to': '854888443'},\n      {'from': '648068209', 'to': '425563742'},\n      {'from': '425563742', 'to': '854888443'},\n      {'from': '648068209', 'to': '286239803'},\n      {'from': '286239803', 'to': '842737235'},\n      {'from': '842737235', 'to': '220477426'},\n      {'from': '220477426', 'to': '665742373'},\n      {'from': '665742373', 'to': '151341470'},\n      {'from': '151341470', 'to': '533503762'},\n      {'from': '665742373', 'to': '4111645'},\n      {'from': '4111645', 'to': '605315905'},\n      {'from': '842737235', 'to': '43693604'},\n      {'from': '43693604', 'to': '27818437'},\n      {'from': '27818437', 'to': '151341470'},\n      {'from': '27818437', 'to': '4111645'},\n      {'from': '286239803', 'to': '746039689'},\n      {'from': '746039689', 'to': '153736120'},\n      {'from': '153736120', 'to': '8283699'},\n      {'from': '8283699', 'to': '443674830'},\n      {'from': '746039689', 'to': '749435060'},\n      {'from': '749435060', 'to': '8283699'},\n      {'from': '551628972', 'to': '286239803'},\n      {'from': '757478418', 'to': '155543721'},\n      {'from': '155543721', 'to': '648068209'},\n      {'from': '155543721', 'to': '286239803'},\n      {'from': '699788790', 'to': '551628972'},\n      {'from': '487798489', 'to': '180915528'},\n      {'from': '180915528', 'to': '699788790'},\n      {'from': '180915528', 'to': '757478418'},\n      {'from': '118422731', 'to': '221121315'},\n      {'from': '221121315', 'to': '487798489'}\n    ]\n  },\n  {\n    'edges': [\n      {'from': '925654661', 'to': '280745044'},\n      {'from': '280745044', 'to': '539063600'},\n      {'from': '539063600', 'to': '936176129'},\n      {'from': '936176129', 'to': '947166361'},\n      {'from': '947166361', 'to': '178822296'},\n      {'from': '178822296', 'to': '735844241'},\n      {'from': '735844241', 'to': '614582366'},\n      {'from': '614582366', 'to': '535521044'},\n      {'from': '535521044', 'to': '1007301027'},\n      {'from': '535521044', 'to': '245744777'},\n      {'from': '735844241', 'to': '518569195'},\n      {'from': '518569195', 'to': '535521044'},\n      {'from': '936176129', 'to': '106575972'},\n      {'from': '106575972', 'to': '178822296'},\n      {'from': '925654661', 'to': '303124879'},\n      {'from': '303124879', 'to': '539063600'}\n    ]\n  },\n  exampleTreeWith140Nodes,\n];\n"
  },
  {
    "path": "test/graph_test.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:graphview/GraphView.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Graph', () {\n    test('Graph Node counts are correct', () {\n      final graph = Graph();\n      var node1 = Node.Id('One');\n      var node2 = Node.Id('Two');\n      var node3 = Node.Id('Three');\n      var node4 = Node.Id('Four');\n      var node5 = Node.Id('Five');\n      var node6 = Node.Id('Six');\n      var node7 = Node.Id('Seven');\n      var node8 = Node.Id('Eight');\n      var node9 = Node.Id('Nine');\n\n      graph.addEdge(node1, node2);\n      graph.addEdge(node1, node4);\n      graph.addEdge(node2, node3);\n      graph.addEdge(node2, node5);\n      graph.addEdge(node3, node6);\n      graph.addEdge(node4, node5);\n      graph.addEdge(node4, node7);\n      graph.addEdge(node5, node6);\n      graph.addEdge(node5, node8);\n      graph.addEdge(node6, node9);\n      graph.addEdge(node7, node8);\n      graph.addEdge(node8, node9);\n\n      expect(graph.nodeCount(), 9);\n\n      graph.removeNode(Node.Id('One'));\n      graph.removeNode(Node.Id('Ten'));\n\n      expect(graph.nodeCount(), 8);\n\n      graph.addNode(Node.Id('Ten'));\n\n      expect(graph.nodeCount(), 9);\n    });\n\n    test('Node Hash Implementation is performant', () {\n      final graph = Graph();\n\n      var rows = 1000000;\n\n      var integerNode = Node.Id(1);\n      var stringNode = Node.Id('123');\n      var stringNode2 = Node.Id('G9Q84H1R9-1619338713.000900');\n      var widgetNode = Node.Id(Text('Lovely'));\n      var widgetNode2 = Node.Id(Text('Lovely'));\n      var doubleNode = Node.Id(5.6);\n\n      var edge = graph.addEdge(integerNode, Node.Id(4));\n\n      var nodes = [\n        integerNode,\n        stringNode,\n        stringNode2,\n        widgetNode,\n        widgetNode2,\n        doubleNode\n      ];\n\n      for (var node in nodes) {\n        var stopwatch = Stopwatch()\n          ..start();\n        for (var i = 1; i <= rows; i++) {\n          var hash = node.hashCode;\n        }\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n        print('Time taken: $timeTaken ms for ${node.runtimeType} node');\n        expect(timeTaken < 100, true);\n      }\n    });\n\n    test('Graph does not duplicate nodes for self loops', () {\n      final graph = Graph();\n      final node = Node.Id('self');\n\n      graph.addEdge(node, node);\n\n      expect(graph.nodes.length, 1);\n      expect(graph.edges.length, 1);\n      expect(graph.nodes.single, node);\n    });\n\n    test('ArrowEdgeRenderer builds self-loop path', () {\n      final renderer = ArrowEdgeRenderer();\n      final node = Node.Id('self')\n        ..size = const Size(40, 40)\n        ..position = const Offset(100, 100);\n\n      final edge = Edge(node, node);\n      final result = renderer.buildSelfLoopPath(edge);\n\n      expect(result, isNotNull);\n\n      final metrics = result!.path.computeMetrics().toList();\n      expect(metrics, isNotEmpty);\n      final metric = metrics.first;\n      expect(metric.length, greaterThan(0));\n      expect(result.arrowTip, isNot(equals(const Offset(0, 0))));\n\n      final tangentStart = metric.getTangentForOffset(0);\n      expect(tangentStart, isNotNull);\n      expect(tangentStart!.vector.dy.abs(),\n          lessThan(tangentStart.vector.dx.abs() * 0.1));\n      expect(tangentStart.vector.dx, greaterThan(0));\n\n      final tangentEnd = metric.getTangentForOffset(metric.length);\n      expect(tangentEnd, isNotNull);\n      expect(tangentEnd!.vector.dx.abs(),\n          lessThan(tangentEnd.vector.dy.abs() * 0.1));\n      expect(tangentEnd.vector.dy, greaterThan(0));\n    });\n\n    test('SugiyamaAlgorithm handles single node self loop', () {\n      final graph = Graph();\n      final node = Node.Id('self')\n        ..size = const Size(40, 40);\n\n      graph.addEdge(node, node);\n\n      final config = SugiyamaConfiguration()\n        ..nodeSeparation = 20\n        ..levelSeparation = 20;\n\n      final algorithm = SugiyamaAlgorithm(config);\n\n      expect(() => algorithm.run(graph, 0, 0), returnsNormally);\n      expect(graph.nodes.length, 1);\n    });\n  });\n}\n"
  },
  {
    "path": "test/graphview_perfomance_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/GraphView.dart';\n\nvoid main() {\n  group('GraphView Performance Tests', () {\n\n    testWidgets('hitTest performance with 1000+ nodes less than 20s', (WidgetTester tester) async {\n      final graph = _createLargeGraph(1000);\n\n      final _configuration = BuchheimWalkerConfiguration()\n        ..siblingSeparation = (100)\n        ..levelSeparation = (150)\n        ..subtreeSeparation = (150)\n        ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n\n      var algorithm = BuchheimWalkerAlgorithm(\n          _configuration, TreeEdgeRenderer(_configuration));\n\n      await tester.pumpWidget(MaterialApp(\n        home: Scaffold(\n          body: GraphView.builder(\n            graph: graph,\n            algorithm: algorithm,\n            builder: (Node node) => Container(\n              width: 50,\n              height: 50,\n              decoration: BoxDecoration(\n                color: Colors.blue,\n                shape: BoxShape.circle,\n              ),\n              child: Center(child: Text(node.key.toString())),\n            ),\n          ),\n        ),\n      ));\n\n      await tester.pumpAndSettle();\n\n      final renderBox = tester.renderObject<RenderCustomLayoutBox>(\n          find.byType(GraphViewWidget)\n      );\n\n      final stopwatch = Stopwatch()..start();\n\n      // Test multiple hit tests at different positions\n      for (var i = 0; i < 10; i++) {\n        final result = BoxHitTestResult();\n        renderBox.hitTest(result, position: Offset(i * 10.0, i * 10.0));\n      }\n\n      stopwatch.stop();\n      final hitTestTime = stopwatch.elapsedMilliseconds;\n\n      print('HitTest time for 1000 nodes (10 tests): ${hitTestTime}ms');\n      expect(hitTestTime, lessThan(20), reason: 'HitTest should complete in under 10ms');\n    });\n\n    testWidgets('paint performance with 1000+ nodes', (WidgetTester tester) async {\n      final graph = _createLargeGraph(1000);\n\n      final _configuration = BuchheimWalkerConfiguration()\n        ..siblingSeparation = (100)\n        ..levelSeparation = (150)\n        ..subtreeSeparation = (150)\n        ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n\n      var algorithm = BuchheimWalkerAlgorithm(\n          _configuration, TreeEdgeRenderer(_configuration));\n\n      await tester.pumpWidget(MaterialApp(\n        home: Scaffold(\n          body: GraphView.builder(\n            graph: graph,\n            algorithm: algorithm,\n            builder: (Node node) => Container(\n              width: 30,\n              height: 30,\n              color: Colors.red,\n            ),\n          ),\n        ),\n      ));\n\n      final stopwatch = Stopwatch()..start();\n\n      // Trigger multiple repaints\n      for (var i = 0; i < 10; i++) {\n        await tester.pump();\n      }\n\n      stopwatch.stop();\n      final paintTime = stopwatch.elapsedMilliseconds;\n\n      print('Paint time for 1000 nodes (10 repaints): ${paintTime}ms');\n      expect(paintTime, lessThan(50), reason: 'Paint should complete in under 50ms');\n    });\n\n    test('algorithm run performance with 1000+ nodes', () {\n      final graph = _createLargeGraph(1000);\n\n      final _configuration = BuchheimWalkerConfiguration()\n        ..siblingSeparation = (100)\n        ..levelSeparation = (150)\n        ..subtreeSeparation = (150)\n        ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);\n\n      var algorithm = BuchheimWalkerAlgorithm(\n          _configuration, TreeEdgeRenderer(_configuration));\n\n      final stopwatch = Stopwatch()..start();\n\n      algorithm.run(graph, 0, 0);\n\n      stopwatch.stop();\n      final algorithmTime = stopwatch.elapsedMilliseconds;\n\n      print('Algorithm run time for 1000 nodes: ${algorithmTime}ms');\n      expect(algorithmTime, lessThan(10), reason: 'Algorithm should complete in under 10 milisecond');\n    });\n\n  });\n}\n\n/// Creates a large graph with connected nodes for performance testing\nGraph _createLargeGraph(int n) {\n  final graph = Graph();\n  // Create nodes\n  final nodes = List.generate(n, (i) => Node.Id(i + 1));\n\n// Generate tree edges using a queue-based approach\n  var currentChild = 1; // Start from node 1 (node 0 is root)\n\n  for (var i = 0; i < n && currentChild < n; i++) {\n    final children = (i < n ~/ 3) ? 3 : 2;\n\n    for (var j = 0; j < children && currentChild < n; j++) {\n      graph.addEdge(nodes[i], nodes[currentChild]);\n      currentChild++;\n    }\n  }\n\n  return graph;\n}"
  },
  {
    "path": "test/sugiyama_algorithm_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/GraphView.dart';\n\nimport 'example_trees.dart';\n\nconst itemHeight = 100.0;\nconst itemWidth = 100.0;\n\nextension on Graph {\n  void inflateWithJson(Map<String, Object> json) {\n    var edges = json['edges']! as List;\n    edges.forEach((element) {\n      var fromNodeId = element['from'];\n      var toNodeId = element['to'];\n      addEdge(Node.Id(fromNodeId), Node.Id(toNodeId));\n    });\n  }\n}\n\nextension on Node {\n  Rect toRect() => Rect.fromLTRB(x, y, x + width, y + height);\n}\n\nvoid main() {\n  group('Sugiyama Graph', () {\n    final graph = Graph();\n    final node1 = Node.Id(1);\n    final node2 = Node.Id(2);\n    final node3 = Node.Id(3);\n    final node4 = Node.Id(4);\n    final node5 = Node.Id(5);\n    final node6 = Node.Id(6);\n    final node8 = Node.Id(7);\n    final node7 = Node.Id(8);\n    final node9 = Node.Id(9);\n    final node10 = Node.Id(10);\n    final node11 = Node.Id(11);\n    final node12 = Node.Id(12);\n    final node13 = Node.Id(13);\n    final node14 = Node.Id(14);\n    final node15 = Node.Id(15);\n    final node16 = Node.Id(16);\n    final node17 = Node.Id(17);\n    final node18 = Node.Id(18);\n    final node19 = Node.Id(19);\n    final node20 = Node.Id(20);\n    final node21 = Node.Id(21);\n    final node22 = Node.Id(22);\n    final node23 = Node.Id(23);\n\n    graph.addEdge(node1, node13, paint: Paint()..color = Colors.red);\n    graph.addEdge(node1, node21);\n    graph.addEdge(node1, node4);\n    graph.addEdge(node1, node3);\n    graph.addEdge(node2, node3);\n    graph.addEdge(node2, node20);\n    graph.addEdge(node3, node4);\n    graph.addEdge(node3, node5);\n    graph.addEdge(node3, node23);\n    graph.addEdge(node4, node6);\n    graph.addEdge(node5, node7);\n    graph.addEdge(node6, node8);\n    graph.addEdge(node6, node16);\n    graph.addEdge(node6, node23);\n    graph.addEdge(node7, node9);\n    graph.addEdge(node8, node10);\n    graph.addEdge(node8, node11);\n    graph.addEdge(node9, node12);\n    graph.addEdge(node10, node13);\n    graph.addEdge(node10, node14);\n    graph.addEdge(node10, node15);\n    graph.addEdge(node11, node15);\n    graph.addEdge(node11, node16);\n    graph.addEdge(node12, node20);\n    graph.addEdge(node13, node17);\n    graph.addEdge(node14, node17);\n    graph.addEdge(node14, node18);\n    graph.addEdge(node16, node18);\n    graph.addEdge(node16, node19);\n    graph.addEdge(node16, node20);\n    graph.addEdge(node18, node21);\n    graph.addEdge(node19, node22);\n    graph.addEdge(node21, node23);\n    graph.addEdge(node22, node23);\n    graph.addEdge(node1, node22);\n    graph.addEdge(node7, node8);\n\n    test('Sugiyama for unconnected nodes', () {\n      final graph = Graph();\n\n      graph.addEdge(Node.Id(1), Node.Id(3));\n      graph.addEdge(Node.Id(4), Node.Id(7));\n\n      final _configuration = SugiyamaConfiguration()\n        ..nodeSeparation = 15\n        ..levelSeparation = 15\n        ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n        ..postStraighten = true;\n\n      var algorithm = SugiyamaAlgorithm(_configuration);\n\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      var stopwatch = Stopwatch()..start();\n      var size = algorithm.run(graph, 10, 10);\n      var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n      expect(timeTaken < 1000, true);\n\n      expect(graph.getNodeUsingId(1).position, Offset(10.0, 10.0));\n      expect(graph.getNodeUsingId(3).position, Offset(125.0, 10.0));\n\n      expect(size, Size(215.0, 215.0));\n    });\n\n    test('Sugiyama for a single directional graph', () {\n      final graph = Graph();\n\n      graph.addEdge(Node.Id(1), Node.Id(3));\n      graph.addEdge(Node.Id(3), Node.Id(4));\n      graph.addEdge(Node.Id(4), Node.Id(7));\n      graph.addEdge(Node.Id(7), Node.Id(9));\n      graph.addEdge(Node.Id(9), Node.Id(111));\n\n      final _configuration = SugiyamaConfiguration()\n        ..nodeSeparation = 15\n        ..levelSeparation = 15\n        ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n        ..postStraighten = true;\n\n      var algorithm = SugiyamaAlgorithm(_configuration);\n\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      var stopwatch = Stopwatch()..start();\n      var size = algorithm.run(graph, 10, 10);\n      var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n      expect(timeTaken < 1000, true);\n\n      expect(graph.getNodeUsingId(1).position, Offset(10.0, 10.0));\n      expect(graph.getNodeUsingId(3).position, Offset(125.0, 10.0));\n      expect(graph.getNodeUsingId(9).position, Offset(470.0, 10.0));\n\n      expect(size, Size(675.0, 100.0));\n    });\n\n    test('Sugiyama for a cyclic graph', () {\n      final graph = Graph();\n\n      graph.addEdge(Node.Id(1), Node.Id(3));\n      graph.addEdge(Node.Id(3), Node.Id(4));\n      graph.addEdge(Node.Id(4), Node.Id(7));\n      graph.addEdge(Node.Id(7), Node.Id(9));\n      graph.addEdge(Node.Id(9), Node.Id(1));\n\n      final _configuration = SugiyamaConfiguration()\n        ..nodeSeparation = 15\n        ..levelSeparation = 15\n        ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n        ..postStraighten = true;\n\n      var algorithm = SugiyamaAlgorithm(_configuration);\n\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      var stopwatch = Stopwatch()..start();\n      var size = algorithm.run(graph, 10, 10);\n      var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n      expect(timeTaken < 1000, true);\n\n      expect(graph.getNodeUsingId(1).position, Offset(125.0, 10.0));\n      expect(graph.getNodeUsingId(3).position, Offset(240.0, 10.0));\n      expect(graph.getNodeUsingId(9).position, Offset(10.0, 17.5));\n\n      expect(size, Size(560.0, 157.5));\n    });\n\n    group('Layering Strategy Tests', () {\n      test('TopDown Strategy - Node Positioning TOP_BOTTOM', () {\n        final _configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..layeringStrategy = LayeringStrategy.topDown\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        var algorithm = SugiyamaAlgorithm(_configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print(\n            'TopDown Strategy TOP_BOTTOM - Time: ${timeTaken}ms, Size: $size');\n\n        expect(timeTaken < 1000, true);\n\n        expect(graph.getNodeAtPosition(0).position, Offset(660.0, 10));\n        expect(graph.getNodeAtPosition(6).position, Offset(1180.0, 815.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(1180.0, 470.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(790, 930.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(660.0, 240.0));\n        expect(graph.getNodeAtPosition(4).position, Offset(920.0, 125.0));\n\n        expect(size, Size(1270.0, 1135.0));\n      });\n\n      test('TopDown Strategy - Node Positioning LEFT_RIGHT', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..layeringStrategy = LayeringStrategy.topDown\n          ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n          ..postStraighten = true;\n\n        var algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print(\n            'TopDown Strategy LEFT_RIGHT - Time: ${timeTaken}ms, Size: $size');\n\n        expect(timeTaken < 1000, true);\n\n        expect(graph.getNodeAtPosition(0).position, Offset(10, 385.0));\n        expect(graph.getNodeAtPosition(6).position, Offset(815.0, 745.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(470.0, 745.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(930, 500.0));\n        expect(graph.getNodeUsingId(3).position, Offset(125.0, 465.0));\n        expect(graph.getNodeUsingId(4).position, Offset(240.0, 342.5));\n\n        expect(size, Size(1135.0, 835.0));\n      });\n\n      test('LongestPath Strategy - Node Positioning', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..layeringStrategy = LayeringStrategy.longestPath\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('LongestPath Strategy - Time: ${timeTaken}ms, Size: $size');\n\n        expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10));\n\n        expect(graph.getNodeAtPosition(6).position, Offset(1505.0, 1045.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(1700.0, 815.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(985.0, 930.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(725.0, 240.0));\n        expect(graph.getNodeAtPosition(4).position, Offset(1115.0, 125.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1660.0, 1135.0));\n      });\n\n      test('CoffmanGraham Strategy - Node Positioning', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..layeringStrategy = LayeringStrategy.coffmanGraham\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('CoffmanGraham Strategy - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(0).position, Offset(1440.0, 10.0));\n        expect(graph.getNodeAtPosition(6).position, Offset(335.0, 1160.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(140.0, 470.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(400.0, 1045.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(1375.0, 240.0));\n        expect(graph.getNodeAtPosition(4).position, Offset(1050.0, 125.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1530.0, 1250.0));\n      });\n\n      test('NetworkSimplex Strategy - Node Positioning', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..layeringStrategy = LayeringStrategy.networkSimplex\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('NetworkSimplex Strategy - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10.0));\n        expect(graph.getNodeAtPosition(6).position, Offset(1505.0, 1045.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(1700.0, 815.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(985.0, 930.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(725.0, 240.0));\n        expect(graph.getNodeAtPosition(4).position, Offset(1115.0, 125.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1660.0, 1135.0));\n      });\n    });\n\n    group('Cross Minimization Strategy Tests', () {\n      test('Simple CrossMinimization - Positioning', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..crossMinimizationStrategy = CrossMinimizationStrategy.simple\n          ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('Simple CrossMin - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(6).position, Offset(815.0, 745.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(470.0, 745.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(930.0, 500.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(240.0, 342.5));\n        expect(graph.getNodeAtPosition(4).position, Offset(125.0, 465.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1135.0, 835.0));\n      });\n\n      test('AccumulatorTree CrossMinimization - Positioning', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..crossMinimizationStrategy =\n              CrossMinimizationStrategy.accumulatorTree\n          ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('AccumulatorTree CrossMin - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(0).position, Offset(10.0, 385.0));\n        expect(graph.getNodeAtPosition(6).position, Offset(815.0, 715.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(470.0, 715.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(930.0, 470.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(240.0, 342.5));\n        expect(graph.getNodeAtPosition(4).position, Offset(125.0, 465.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1135.0, 805.0));\n      });\n    });\n\n    // Test Cycle Removal Strategies\n    group('Cycle Removal Strategy Tests', () {\n      final graph = Graph();\n      final node1 = Node.Id(1);\n      final node2 = Node.Id(2);\n      final node3 = Node.Id(3);\n      final node4 = Node.Id(4);\n      final node5 = Node.Id(5);\n\n      // Create a cyclic graph\n      graph.addEdge(node1, node2);\n      graph.addEdge(node2, node3);\n      graph.addEdge(node3, node4);\n      graph.addEdge(node4, node1); // Creates cycle\n      graph.addEdge(node2, node5);\n\n      test('DFS Cycle Removal - Positioning', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..cycleRemovalStrategy = CycleRemovalStrategy.dfs\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('DFS Cycle Removal - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions - layout should be acyclic\n        expect(graph.getNodeAtPosition(1).position, Offset(75.0, 125.0));\n        expect(graph.getNodeAtPosition(2).position, Offset(10.0, 240.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(140.0, 355.0));\n        expect(timeTaken < 1000, true);\n        expect(size, Size(230, 445.0));\n      });\n\n      test('Greedy Cycle Removal - Positioning', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..cycleRemovalStrategy = CycleRemovalStrategy.greedy\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('Greedy Cycle Removal - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions - layout should be acyclic\n        expect(graph.getNodeAtPosition(1).position, Offset(75.0, 240.0));\n        expect(graph.getNodeAtPosition(2).position, Offset(140.0, 355.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(75.0, 10.0));\n        expect(timeTaken < 1000, true);\n        expect(size, Size(230.0, 445.0));\n      });\n    });\n\n    group('Coordinate Assignment Strategy Tests', () {\n      test('DownRight Coordinate Assignment', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..coordinateAssignment = CoordinateAssignment.DownRight\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('DownRight Assignment - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(1).position, Offset(790.0, 700.0));\n        expect(graph.getNodeAtPosition(2).position, Offset(1050.0, 930.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(530.0, 240.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1790.0, 1135.0));\n      });\n\n      test('DownLeft Coordinate Assignment', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..coordinateAssignment = CoordinateAssignment.DownLeft\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('DownLeft Assignment - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(0).position, Offset(1310.0, 10.0));\n        expect(graph.getNodeAtPosition(6).position, Offset(1180.0, 815.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(530.0, 930.0));\n        expect(graph.getNodeUsingId(3).position, Offset(1310.0, 125.0));\n        expect(graph.getNodeUsingId(4).position, Offset(920.0, 240.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1530.0, 1135.0));\n      });\n\n      test('Average Coordinate Assignment', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..coordinateAssignment = CoordinateAssignment.Average\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('Average Assignment - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(0).position, Offset(660.0, 10.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(1180.0, 470.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(790, 930.0));\n        expect(graph.getNodeUsingId(3).position, Offset(920.0, 125.0));\n        expect(graph.getNodeUsingId(4).position, Offset(660.0, 240.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1270.0, 1135.0));\n      });\n\n      test('UpRight Coordinate Assignment', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..coordinateAssignment = CoordinateAssignment.UpRight\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('UpRight Assignment - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10.0));\n        expect(graph.getNodeAtPosition(6).position, Offset(1050.0, 815.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(1050.0, 470.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(400.0, 930.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(400.0, 240.0));\n        expect(graph.getNodeAtPosition(4).position, Offset(1050.0, 125.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1140.0, 1135.0));\n      });\n\n      test('UpLeft Coordinate Assignment', () {\n        final configuration = SugiyamaConfiguration()\n          ..nodeSeparation = 15\n          ..levelSeparation = 15\n          ..coordinateAssignment = CoordinateAssignment.UpLeft\n          ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n          ..postStraighten = true;\n\n        final algorithm = SugiyamaAlgorithm(configuration);\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n        var size = algorithm.run(graph, 10, 10);\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('UpLeft Assignment - Time: ${timeTaken}ms, Size: $size');\n\n        // Test exact positions\n        expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10.0));\n        expect(graph.getNodeAtPosition(6).position, Offset(1440.0, 815.0));\n        expect(graph.getNodeAtPosition(13).position, Offset(1440.0, 470.0));\n        expect(graph.getNodeAtPosition(22).position, Offset(1310.0, 930.0));\n        expect(graph.getNodeAtPosition(3).position, Offset(270.0, 240.0));\n        expect(graph.getNodeAtPosition(4).position, Offset(1440.0, 125.0));\n\n        expect(timeTaken < 1000, true);\n        expect(size, Size(1660.0, 1135.0));\n      });\n    });\n\n    // Performance Tests for 140 Node Graph\n    group('140 Node Graph Performance Tests', () {\n      test('Layering Strategy Performance Comparison - 140 Nodes', () {\n        print('\\n=== 140 Node Graph - Layering Strategy Performance ===');\n\n        final strategies = [\n          {'strategy': LayeringStrategy.topDown, 'name': 'TopDown'},\n          {'strategy': LayeringStrategy.longestPath, 'name': 'LongestPath'},\n          {'strategy': LayeringStrategy.coffmanGraham, 'name': 'CoffmanGraham'},\n          {\n            'strategy': LayeringStrategy.networkSimplex,\n            'name': 'NetworkSimplex'\n          },\n        ];\n\n        for (final strategy in strategies) {\n          final graph = Graph();\n          graph.inflateWithJson(exampleTreeWith140Nodes);\n\n          for (var i = 0; i < graph.nodeCount(); i++) {\n            graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n          }\n\n          final configuration = SugiyamaConfiguration()\n            ..nodeSeparation = 15\n            ..levelSeparation = 15\n            ..layeringStrategy = strategy['strategy'] as LayeringStrategy\n            ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n            ..postStraighten = true;\n\n          final algorithm = SugiyamaAlgorithm(configuration);\n\n          final stopwatch = Stopwatch()..start();\n          final size = algorithm.run(graph, 10, 10);\n          final timeTaken = stopwatch.elapsed.inMilliseconds;\n\n          print(\n              '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}');\n\n          expect(timeTaken < 3000, true,\n              reason:\n                  '${strategy['name']} should complete within 3 seconds for 140 nodes');\n        }\n      });\n\n      test('CrossMinimization Strategy Performance - 140 Nodes', () {\n        print('\\n=== 140 Node Graph - Cross Minimization Performance ===');\n\n        final strategies = [\n          {'strategy': CrossMinimizationStrategy.simple, 'name': 'Simple'},\n          {\n            'strategy': CrossMinimizationStrategy.accumulatorTree,\n            'name': 'AccumulatorTree'\n          },\n        ];\n\n        for (final strategy in strategies) {\n          final graph = Graph();\n          graph.inflateWithJson(exampleTreeWith140Nodes);\n\n          for (var i = 0; i < graph.nodeCount(); i++) {\n            graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n          }\n\n          final configuration = SugiyamaConfiguration()\n            ..nodeSeparation = 15\n            ..levelSeparation = 15\n            ..crossMinimizationStrategy =\n                strategy['strategy'] as CrossMinimizationStrategy\n            ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n            ..postStraighten = true;\n\n          final algorithm = SugiyamaAlgorithm(configuration);\n\n          final stopwatch = Stopwatch()..start();\n          final size = algorithm.run(graph, 10, 10);\n          final timeTaken = stopwatch.elapsed.inMilliseconds;\n\n          print(\n              '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}');\n\n          expect(timeTaken < 3000, true,\n              reason: '${strategy['name']} should complete within 3 seconds');\n        }\n      });\n\n      test('Cycle Removal Strategy Performance - 140 Nodes', () {\n        print('\\n=== 140 Node Graph - Cycle Removal Performance ===');\n\n        final strategies = [\n          {'strategy': CycleRemovalStrategy.dfs, 'name': 'DFS'},\n          {'strategy': CycleRemovalStrategy.greedy, 'name': 'Greedy'},\n        ];\n\n        for (final strategy in strategies) {\n          final graph = Graph();\n          graph.inflateWithJson(exampleTreeWith140Nodes);\n\n          for (var i = 0; i < graph.nodeCount(); i++) {\n            graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n          }\n\n          final configuration = SugiyamaConfiguration()\n            ..nodeSeparation = 15\n            ..levelSeparation = 15\n            ..cycleRemovalStrategy =\n                strategy['strategy'] as CycleRemovalStrategy\n            ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n            ..postStraighten = true;\n\n          final algorithm = SugiyamaAlgorithm(configuration);\n\n          final stopwatch = Stopwatch()..start();\n          final size = algorithm.run(graph, 10, 10);\n          final timeTaken = stopwatch.elapsed.inMilliseconds;\n\n          print(\n              '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}');\n\n          expect(timeTaken < 3000, true,\n              reason: '${strategy['name']} should complete within 3 seconds');\n        }\n      });\n\n      test('Coordinate Assignment Performance - 140 Nodes', () {\n        print('\\n=== 140 Node Graph - Coordinate Assignment Performance ===');\n\n        final strategies = [\n          {'strategy': CoordinateAssignment.DownRight, 'name': 'DownRight'},\n          {'strategy': CoordinateAssignment.DownLeft, 'name': 'DownLeft'},\n          {'strategy': CoordinateAssignment.UpRight, 'name': 'UpRight'},\n          {'strategy': CoordinateAssignment.UpLeft, 'name': 'UpLeft'},\n          {'strategy': CoordinateAssignment.Average, 'name': 'Average'},\n        ];\n\n        for (final strategy in strategies) {\n          final graph = Graph();\n          graph.inflateWithJson(exampleTreeWith140Nodes);\n\n          for (var i = 0; i < graph.nodeCount(); i++) {\n            graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n          }\n\n          final configuration = SugiyamaConfiguration()\n            ..nodeSeparation = 15\n            ..levelSeparation = 15\n            ..coordinateAssignment =\n                strategy['strategy'] as CoordinateAssignment\n            ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n            ..postStraighten = true;\n\n          final algorithm = SugiyamaAlgorithm(configuration);\n\n          final stopwatch = Stopwatch()..start();\n          final size = algorithm.run(graph, 10, 10);\n          final timeTaken = stopwatch.elapsed.inMilliseconds;\n\n          print(\n              '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}');\n\n          expect(timeTaken < 3000, true,\n              reason: '${strategy['name']} should complete within 3 seconds');\n        }\n      });\n    });\n\n    test('PostStraighten Effect on Node Positioning', () {\n      // Test with PostStraighten ON\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      final configurationOn = SugiyamaConfiguration()\n        ..nodeSeparation = 15\n        ..levelSeparation = 15\n        ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM\n        ..postStraighten = false;\n\n      final algorithmOn = SugiyamaAlgorithm(configurationOn);\n      algorithmOn.run(graph, 10, 10);\n\n      expect(graph.getNodeAtPosition(0).position, Offset(660.0, 10));\n      expect(graph.getNodeAtPosition(6).position, Offset(1180.0, 815.0));\n      expect(graph.getNodeUsingId(3).position, Offset(920.0, 125.0));\n    });\n\n    test('Sugiyama for a complex graph with 140 nodes', () {\n      final json = exampleTreeWith140Nodes;\n\n      final graph = Graph();\n\n      var edges = json['edges']!;\n      edges.forEach((element) {\n        var fromNodeId = element['from'];\n        var toNodeId = element['to'];\n        graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId));\n      });\n\n      final _configuration = SugiyamaConfiguration()\n        ..nodeSeparation = 15\n        ..levelSeparation = 15\n        ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n        ..postStraighten = true;\n\n      final algorithm = SugiyamaAlgorithm(_configuration);\n\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      var stopwatch = Stopwatch()..start();\n      algorithm.run(graph, 10, 10);\n      var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n      print('Timetaken $timeTaken ${graph.nodeCount()}');\n\n    expect(graph.getNodeAtPosition(0).position, Offset(10.0, 1715.0));\n    expect(graph.getNodeAtPosition(6).position, Offset(815.0, 1757.5));\n    expect(graph.getNodeAtPosition(10).position, Offset(1160.0, 1872.5));\n    expect(graph.getNodeAtPosition(13).position, Offset(1275.0, 2117.5));\n    expect(graph.getNodeAtPosition(22).position, Offset(1620.0, 2635.0));\n    expect(graph.getNodeAtPosition(50).position, Offset(1505.0, 1232.5));\n    expect(graph.getNodeAtPosition(67).position, Offset(2655.0, 1700.0));\n    expect(graph.getNodeAtPosition(100).position, Offset(815.0, 412.5));\n    expect(graph.getNodeAtPosition(122).position, Offset(1735.0,2060.0));\n  });\n\n    test('Sugiyama child nodes never overlaps', () {\n      for (final json in exampleTrees) {\n        final graph = Graph()..inflateWithJson(json);\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n        }\n\n        var stopwatch = Stopwatch()..start();\n\n        SugiyamaAlgorithm(SugiyamaConfiguration()..postStraighten = true)\n          ..run(graph, 10, 10);\n\n        var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n        print('Timetaken $timeTaken ${graph.nodeCount()}');\n\n        for (var i = 0; i < graph.nodeCount(); i++) {\n          final currentNode = graph.getNodeAtPosition(i);\n          for (var j = 0; j < graph.nodeCount(); j++) {\n            final otherNode = graph.getNodeAtPosition(j);\n\n            if (currentNode.key == otherNode.key) continue;\n            final currentRect = currentNode.toRect();\n            final otherRect = otherNode.toRect();\n\n            final overlaps = currentRect.overlaps(otherRect);\n            expect(false, overlaps, reason: '$currentNode overlaps $otherNode');\n          }\n        }\n      }\n    });\n\n    test('Sugiyama Performance for 100 nodes to be less than 5.2s', () {\n      final graph = Graph();\n\n      var rows = 100;\n\n      for (var i = 1; i <= rows; i++) {\n        for (var j = 1; j <= i; j++) {\n          graph.addEdge(Node.Id(i), Node.Id(j));\n        }\n      }\n\n      final _configuration = SugiyamaConfiguration()\n        ..nodeSeparation = 15\n        ..levelSeparation = 15\n        ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT\n        ..postStraighten = true;\n\n      var algorithm = SugiyamaAlgorithm(_configuration);\n\n      for (var i = 0; i < graph.nodeCount(); i++) {\n        graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight);\n      }\n\n      var stopwatch = Stopwatch()..start();\n      algorithm.run(graph, 10, 10);\n      var timeTaken = stopwatch.elapsed.inMilliseconds;\n\n      print('Timetaken $timeTaken ${graph.nodeCount()}');\n\n      expect(timeTaken < 5200, true);\n    });\n  });\n}\n"
  }
]