Repository: nabil6391/graphview Branch: master Commit: 3e1954c196f3 Files: 56 Total size: 438.3 KB Directory structure: gitextract_lzoxm4t9/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── tests.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example/ │ ├── .gitignore │ ├── analysis_options.yaml │ ├── lib/ │ │ ├── algorithm_selector_graphview.dart │ │ ├── decision_tree_screen.dart │ │ ├── example.dart │ │ ├── force_directed_graphview.dart │ │ ├── graph_cluster_animated.dart │ │ ├── large_tree_graphview.dart │ │ ├── layer_eiglesperger_graphview.dart │ │ ├── layer_graphview.dart │ │ ├── layer_graphview_json.dart │ │ ├── main.dart │ │ ├── mindmap_graphview.dart │ │ ├── mutliple_forest_graphview.dart │ │ ├── tree_graphview.dart │ │ └── tree_graphview_json.dart │ └── pubspec.yaml ├── lib/ │ ├── Algorithm.dart │ ├── Graph.dart │ ├── GraphView.dart │ ├── edgerenderer/ │ │ ├── ArrowEdgeRenderer.dart │ │ └── EdgeRenderer.dart │ ├── forcedirected/ │ │ ├── FruchtermanReingoldAlgorithm.dart │ │ └── FruchtermanReingoldConfiguration.dart │ ├── layered/ │ │ ├── EiglspergerAlgorithm.dart │ │ ├── SugiyamaAlgorithm.dart │ │ ├── SugiyamaConfiguration.dart │ │ ├── SugiyamaEdgeData.dart │ │ ├── SugiyamaEdgeRenderer.dart │ │ └── SugiyamaNodeData.dart │ ├── mindmap/ │ │ ├── MindMapAlgorithm.dart │ │ └── MindmapEdgeRenderer.dart │ └── tree/ │ ├── BaloonLayoutAlgorithm.dart │ ├── BuchheimWalkerAlgorithm.dart │ ├── BuchheimWalkerConfiguration.dart │ ├── BuchheimWalkerNodeData.dart │ ├── CircleLayoutAlgorithm.dart │ ├── RadialTreeLayoutAlgorithm.dart │ ├── TidierTreeLayoutAlgorithm.dart │ └── TreeEdgeRenderer.dart ├── pubspec.yaml └── test/ ├── algorithm_performance_test.dart ├── buchheim_walker_algorithm_test.dart ├── controller_tests.dart ├── example_trees.dart ├── graph_test.dart ├── graphview_perfomance_test.dart └── sugiyama_algorithm_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ['https://www.paypal.me/nabil6391'] ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: [ master ] pull_request: branches: [ master ] jobs: tests: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install and set Flutter version uses: subosito/flutter-action@v1.5.3 with: channel: 'beta' - name: Get packages run: flutter pub get # - name: Analyze # run: flutter analyze - name: Run tests run: flutter test ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ build/ # Android related **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 AGENTS.md ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 7c6f9dd2396dfe7deb6fd11edc12c10786490083 channel: master project_type: package ================================================ FILE: CHANGELOG.md ================================================ ## 1.5.1 - Fix Zoom To fit for hidden nodes - Add Fade in Support for Edges - Add Loopback support ## 1.5.0 - **MAJOR UPDATE**: Added 5 new layout algorithms - BalloonLayoutAlgorithm: Radial tree layout with circular child arrangements around parents - CircleLayoutAlgorithm: Arranges nodes in circular formations with edge crossing reduction - RadialTreeLayoutAlgorithm: Converts tree structures to polar coordinate system - TidierTreeLayoutAlgorithm: Improved tree layout with better spacing and positioning - MindmapAlgorithm: Specialized layout for mindmap-style distributions - **NEW**: Node expand/collapse functionality with GraphViewController - `collapseNode()`, `expandNode()`, `toggleNodeExpanded()` methods - Hierarchical visibility control with animated transitions - Initial collapsed state support via `setInitiallyCollapsedNodes()` - **NEW**: Advanced animation system - Smooth expand/collapse animations with customizable duration - Node scaling and opacity transitions during state changes - `toggleAnimationDuration` parameter for fine-tuning animations - **NEW**: Enhanced GraphView.builder constructor - `animated`: Enable/disable smooth animations (default: true) - `autoZoomToFit`: Automatically zoom to fit all nodes on initialization - `initialNode`: Jump to specific node on startup - `panAnimationDuration`: Customizable navigation movement timing - `centerGraph`: Center the graph within viewport having a fixed large size of 2000000 - `controller`: GraphViewController for programmatic control - **NEW**: Navigation and pan control features - `jumpToNode()` and `animateToNode()` for programmatic navigation - `zoomToFit()` for automatic viewport adjustment - `resetView()` for returning to origin - `forceRecalculation()` for layout updates - **IMPROVED** TreeEdgeRenderer with curved/straight connection options - **IMPROVED**: Better performance with caching for graphs - **IMPROVED**: Sugiyama Algorithm with postStraighten and additional strategies ## 1.2.0 - Resolved Overlaping for Sugiyama Algorithm (#56, #93, #87) - Added Enum for Coordinate Assignment in Sugiyama : DownRight, DownLeft, UpRight, UpLeft, Average(Default) ## 1.1.1 - Fixed bug for SugiyamaAlgorithm where horizontal placement was overlapping - Buchheim Algorithm Performance Improvements ## 1.1.0 - Massive Sugiyama Algorithm Performance Improvements! (5x times faster) - Encourage usage of Node.id(int) for better performance - Added tests to better check regressions ## 1.0.0 - Full Null Safety Support - Sugiyama Algorithm Performance Improvements - Sugiyama Algorithm TOP_BOTTOM Height Issue Solved (#48) ## 1.0.0-nullsafety.0 - Null Safety Support ## 0.7.0 - Added methods for builder pattern and deprecated directly setting Widget Data in nodes. ## 0.6.7 - Fix rect value not being set in FruchtermanReingoldAlgorithm (#27) ## 0.6.6 - Fix Index out of range for Sugiyama Algorithm (#20) ## 0.6.5 - Fix edge coloring not picked up by TreeEdgeRenderer (#15) - Added Orientation Support in Sugiyama Configuration (#6) ## 0.6.1 - Fix coloring not happening for the whole graphview - Fix coloring for sugiyama and tree edge render - Use interactive viewer correctly to make the view constrained ## 0.6.0 - Add coloring to individual edges. Applicable for ArrowEdgeRenderer - Add example for focused node for Force Directed Graph. It also showcases dynamic update ## 0.5.1 - Fix a bug where the paint was not applied after setstate. - Proper Key validation to match Nodes and Edges ## 0.5.0 - Minor Breaking change. We now pass edge renderers as part of Layout - Added Layered Graph (SugiyamaAlgorithm) - Added Paint Object to change color and stroke parameters of the edges easily - Fixed a bug where by onTap in GestureDetector and Inkwell was not working ## 0.1.2 - Used part of library properly. Now we can only implement single graphview ## 0.1.0 - Initial release. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Nabil Mosharraf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ GraphView =========== Get it from [![pub package](https://img.shields.io/pub/v/graphview.svg)](https://pub.dev/packages/graphview) [![pub points](https://img.shields.io/pub/points/graphview/?color=2E8B57&label=pub%20points)](https://pub.dev/packages/graphview/score) Flutter GraphView is used to display data in graph structures. It can display Tree layout, Directed and Layered graph. Useful for Family Tree, Hierarchy View. ![alt Example](https://media.giphy.com/media/Wsd5Uwm72UBZKXb77s/giphy.gif "Force Directed Graph") ![alt Example](https://media.giphy.com/media/jQ7fdMc5HmyQRoikaK/giphy.gif "Tree") ![alt Example](image/LayeredGraph.png "Layered Graph Example") Overview ======== The 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. You can have a look at the flutter web implementation here: http://graphview.surge.sh/ Features ======== - **Multiple Layout Algorithms**: Tree, Directed Graph, Layered Graph, Balloon, Circular, Radial, Tidier Tree, and Mindmap layouts - **Node Animations**: Smooth expand/collapse animations with customizable duration - **Interactive Navigation**: Jump to nodes, zoom to fit, auto-centering capabilities - **Node Expand/Collapse**: Hierarchical node visibility control with animated transitions - **Customizable Rendering**: Custom edge renderers, paint styling, and node builders - **Touch Interactions**: Pan, zoom, and tap handling with InteractiveViewer integration Layouts ====== ### Tree Uses 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 `ORIENTATION_BOTTOM_TOP` (default). Furthermore parameters like sibling-, level-, subtree separation can be set. Useful for: Family Tree, Hierarchy View, Flutter Widget Tree ### Tidier Tree An improved tree layout algorithm (`TidierTreeLayoutAlgorithm` class) that provides better spacing and positioning for complex hierarchical structures. Supports all orientations and provides cleaner node arrangements. ![alt Example](image/TidierTree.gif "Tidier Tree Animation") Useful for: Complex hierarchies, Organizational charts, Decision trees ### Directed graph Directed graph drawing by simulating attraction/repulsion forces. For this the algorithm by Fruchterman and Reingold (`FruchtermanReingoldAlgorithm` class) was implemented. Useful for: Social network, Mind Map, Cluster, Graphs, Intercity Road Network ### Layered graph Algorithm 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). Useful for: Hierarchical Graph which it can have weird edges/multiple paths ### Balloon Layout A 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. ![alt Example](image/BalloonLayout.gif "Balloon Layout Animation") Useful for: Mind maps, Radial trees, Circular hierarchies ### Circular Layout Arranges all nodes in a circle (`CircleLayoutAlgorithm` class). Includes edge crossing reduction algorithms for better readability. Supports automatic radius calculation and custom positioning. ![alt Example](image/CircularLayout.gif "Circular Layout Animation") Useful for: Network visualization, Relationship diagrams, Cyclic structures ### Radial Tree Layout A 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. ![alt Example](image/RadialTree.gif "Radial Tree Animation") Useful for: Radial dendrograms, Phylogenetic trees, Sunburst-style hierarchies ### Mindmap Layout Specialized layout for mindmap-style visualizations (`MindmapAlgorithm` class) where child nodes are distributed on left and right sides of the root node. ![alt Example](image/MindmapLayout.gif "Mindmap Layout Animation") Useful for: Mind maps, Concept maps, Brainstorming diagrams Usage ====== ### Basic Setup Currently 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. To create a graph, we need to instantiate the `Graph` class. Then we need to pass the layout and also optional the edge renderer. ```dart import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => MaterialApp(home: TreeViewPage()); } class TreeViewPage extends StatefulWidget { const TreeViewPage({super.key}); @override State createState() => _TreeViewPageState(); } class _TreeViewPageState extends State { final GraphViewController controller = GraphViewController(); @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisSize: MainAxisSize.max, children: [ Wrap( children: [ SizedBox( width: 100, child: TextFormField( initialValue: builder.siblingSeparation.toString(), decoration: InputDecoration(labelText: "Sibling Separation"), onChanged: (text) { builder.siblingSeparation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), SizedBox( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: "Level Separation"), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), SizedBox( width: 100, child: TextFormField( initialValue: builder.subtreeSeparation.toString(), decoration: InputDecoration(labelText: "Subtree separation"), onChanged: (text) { builder.subtreeSeparation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), SizedBox( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: "Orientation"), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), ElevatedButton( onPressed: () { final node12 = Node.Id(r.nextInt(100)); var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount())); debugPrint(edge.toString()); graph.addEdge(edge, node12); setState(() {}); }, child: Text("Add"), ), ], ), Expanded( child: GraphView.builder( graph: graph, algorithm: BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)), controller: controller, animated: true, autoZoomToFit: true, builder: (Node node) { // I can decide what widget should be shown here based on the id var a = node.key?.value as int; return rectangleWidget(a); }, ), ), ], ), ); } Random r = Random(); Widget rectangleWidget(int a) { return InkWell( onTap: () { debugPrint('clicked'); }, child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)], ), child: Text('Node $a'), ), ); } final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); @override void initState() { super.initState(); final node1 = Node.Id(1); final node2 = Node.Id(2); final node3 = Node.Id(3); final node4 = Node.Id(4); final node5 = Node.Id(5); final node6 = Node.Id(6); final node8 = Node.Id(7); final node7 = Node.Id(8); final node9 = Node.Id(9); final node10 = Node.Id(10); final node11 = Node.Id(11); final node12 = Node.Id(12); graph.addEdge(node1, node2); graph.addEdge(node1, node3, paint: Paint()..color = Colors.red); graph.addEdge(node1, node4, paint: Paint()..color = Colors.blue); graph.addEdge(node2, node5); graph.addEdge(node2, node6); graph.addEdge(node6, node7, paint: Paint()..color = Colors.red); graph.addEdge(node6, node8, paint: Paint()..color = Colors.red); graph.addEdge(node4, node9); graph.addEdge(node4, node10, paint: Paint()..color = Colors.black); graph.addEdge(node4, node11, paint: Paint()..color = Colors.red); graph.addEdge(node11, node12); builder ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); } } ``` ### Advanced Features #### GraphView.builder The enhanced `GraphView.builder` constructor provides additional capabilities: ```dart GraphView.builder( graph: graph, algorithm: BuchheimWalkerAlgorithm(config, TreeEdgeRenderer(config)), controller: controller, animated: true, // Enable smooth animations autoZoomToFit: true, // Automatically zoom to fit all nodes initialNode: ValueKey('startNode'), // Jump to specific node on init panAnimationDuration: Duration(milliseconds: 600), toggleAnimationDuration: Duration(milliseconds: 400), centerGraph: true, // Center the graph in viewport builder: (Node node) { return YourCustomWidget(node); }, ) ``` #### Node Expand/Collapse Use the `GraphViewController` to manage node visibility: ```dart final controller = GraphViewController(); // Collapse a node (hide its children) controller.collapseNode(graph, node, animate: true); // Expand a collapsed node controller.expandNode(graph, node, animate: true); // Toggle collapse/expand state controller.toggleNodeExpanded(graph, node, animate: true); // Check if node is collapsed bool isCollapsed = controller.isNodeCollapsed(node); // Set initially collapsed nodes controller.setInitiallyCollapsedNodes([node1, node2]); ``` #### Navigation and Camera Control Navigate programmatically through the graph: ```dart // Jump to a specific node controller.jumpToNode(ValueKey('nodeId')); // Animate to a node controller.animateToNode(ValueKey('nodeId')); // Zoom to fit all visible nodes controller.zoomToFit(); // Reset view to origin controller.resetView(); // Force recalculation of layout controller.forceRecalculation(); ``` ### Algorithm Examples #### Balloon Layout ```dart GraphView.builder( graph: graph, algorithm: BalloonLayoutAlgorithm( BuchheimWalkerConfiguration(), null ), builder: (node) => nodeWidget(node), ) ``` #### Circular Layout ```dart GraphView.builder( graph: graph, algorithm: CircleLayoutAlgorithm( CircleLayoutConfiguration( radius: 200.0, reduceEdgeCrossing: true, ), null ), builder: (node) => nodeWidget(node), ) ``` #### Radial Tree Layout ```dart GraphView.builder( graph: graph, algorithm: RadialTreeLayoutAlgorithm( BuchheimWalkerConfiguration(), null ), builder: (node) => nodeWidget(node), ) ``` #### Tidier Tree Layout ```dart GraphView.builder( graph: graph, algorithm: TidierTreeLayoutAlgorithm( BuchheimWalkerConfiguration(), TreeEdgeRenderer(config) ), builder: (node) => nodeWidget(node), ) ``` #### Mindmap Layout ```dart GraphView.builder( graph: graph, algorithm: MindmapAlgorithm( BuchheimWalkerConfiguration(), MindmapEdgeRenderer(config) ), builder: (node) => nodeWidget(node), ) ``` ### Using builder mechanism to build Nodes You can use any widget inside the node: ```dart Node node = Node.Id(fromNodeId) ; builder: (Node node) { // I can decide what widget should be shown here based on the id var a = node.key.value as int; if(a ==2) return rectangleWidget(a); else return circleWidget(a); }, ``` ### Using Paint to color and line thickness You can specify the edge color and thickness by using a custom paint ```dart getGraphView() { return GraphView( graph: graph, algorithm: SugiyamaAlgorithm(builder), paint: Paint()..color = Colors.green..strokeWidth = 1..style = PaintingStyle.stroke, ); } ``` ### Color Edges individually Add an additional parameter paint. Applicable for ArrowEdgeRenderer for now. ```dart var a = Node(); var b = Node(); graph.addEdge(a, b, paint: Paint()..color = Colors.red); ``` ### Add focused Node You 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 ```dart onPanUpdate: (details) { var x = details.globalPosition.dx; var y = details.globalPosition.dy; setState(() { builder.setFocusedNode(graph.getNodeAtPosition(i)); graph.getNodeAtPosition(i).position = Offset(x,y); }); }, ``` ### Extract info from any json to Graph Object Now its a bit easy to use Ids to extract info from any json to Graph Object For example, if the json is like this: ```dart var json = { "nodes": [ {"id": 1, "label": 'circle'}, {"id": 2, "label": 'ellipse'}, {"id": 3, "label": 'database'}, {"id": 4, "label": 'box'}, {"id": 5, "label": 'diamond'}, {"id": 6, "label": 'dot'}, {"id": 7, "label": 'square'}, {"id": 8, "label": 'triangle'}, ], "edges": [ {"from": 1, "to": 2}, {"from": 2, "to": 3}, {"from": 2, "to": 4}, {"from": 2, "to": 5}, {"from": 5, "to": 6}, {"from": 5, "to": 7}, {"from": 6, "to": 8} ] }; ``` Step 1, add the edges by using ids ```dart edges.forEach((element) { var fromNodeId = element['from']; var toNodeId = element['to']; graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId)); }); ``` Step 2: Then using builder and find the nodeValues from the json using id and then set the value of that. ```dart builder: (Node node) { // I can decide what widget should be shown here based on the id var a = node.key.value as int; var nodes = json['nodes']; var nodeValue = nodes.firstWhere((element) => element['id'] == a); return rectangleWidget(nodeValue['label'] as String); }, ``` ### Using any widget inside the Node (Deprecated) You can use any widget inside the node: ```dart Node node = Node(getNodeText); getNodeText() { return Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue[100], spreadRadius: 1), ], ), child: Text("Node ${n++}")); } ``` Examples ======== #### Rooted Tree ![alt Example](image/TopDownTree.png "Tree Example") #### Rooted Tree (Bottom to Top) ![alt Example](image/BottomTopTree.png "Tree Example") #### Rooted Tree (Left to Right) ![alt Example](image/LeftRightTree.png "Tree Example") #### Rooted Tree (Right to Left) ![alt Example](image/RightLeftTree.png "Tree Example") #### Directed Graph ![alt Example](image/Graph.png "Directed Graph Example") ![alt Example](https://media.giphy.com/media/eNuoOOcbvWlRmJjkDZ/giphy.gif "Force Directed Graph") #### Layered Graph ![alt Example](image/LayeredGraph.png "Layered Graph Example") #### Balloon Layout ![alt Example](image/BalloonTreeLayout.gif "Balloon Layout Example") #### Circular Layout ![alt Example](image/CircleLayout.gif "Circular Layout Example") #### Radial Tree Layout ![alt Example](image/RadialTreeLayout.gif "Radial Tree Layout Example") #### Tidier Tree Layout ![alt Example](image/TidierTreeLayout.gif "Tidier Tree Layout Example") #### Mindmap Layout ![alt Example](image/MindMapLayout.gif "Mindmap Layout Example") #### Node Expand/Collapse Animation ![alt Example](image/NodeExpandCollapseAnimation.gif "Node Expand/Collapse Animation") #### Auto Navigation ![alt Example](image/AutoNavigationExample.gif "Auto Navigation Example") Inspirations ======== This library is basically a dart representation of the excellent Android Library [GraphView](https://github.com/Team-Blox/GraphView) by Team-Blox I 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. Future Works ======== - [x] Add nodeOnTap - [x] Add Layered Graph - [x] Animations - [x] Dynamic Node Position update for directed graph - [x] Node expand/collapse functionality - [x] Auto-navigation and camera control - [x] Multiple new layout algorithms (Balloon, Circular, Radial, Tidier, Mindmap) - [ ] Finish Eiglsperger Algorithm - [ ] Custom Edge Label Rendering - [ ] Use a builder pattern to draw items on demand. License ======= MIT License Copyright (c) 2020 Nabil Mosharraf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: analysis_options.yaml ================================================ linter: rules: - always_declare_return_types - annotate_overrides - avoid_empty_else - avoid_init_to_null - avoid_null_checks_in_equality_operators - avoid_relative_lib_imports - avoid_return_types_on_setters - avoid_shadowing_type_parameters - avoid_types_as_parameter_names - camel_case_extensions - curly_braces_in_flow_control_structures - empty_catches - empty_constructor_bodies - library_names - library_prefixes - no_duplicate_case_values - null_closures - omit_local_variable_types - prefer_adjacent_string_concatenation - prefer_collection_literals - prefer_conditional_assignment - prefer_contains # REMOVED: prefer_equal_for_default_values (removed in Dart 3.0) - prefer_final_fields - prefer_for_elements_to_map_fromIterable - prefer_generic_function_type_aliases - prefer_if_null_operators - prefer_is_empty - prefer_is_not_empty - prefer_iterable_whereType - prefer_single_quotes - prefer_spread_collections - recursive_getters - slash_for_doc_comments - type_init_formals - unawaited_futures - unnecessary_const - unnecessary_new - unnecessary_null_in_if_null_operators - unnecessary_this - unrelated_type_equality_checks - use_function_type_syntax_for_parameters - use_rethrow_when_possible - valid_regexps analyzer: strong-mode: implicit-casts: false ================================================ FILE: example/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related # Symbolication related app.*.symbols # Obfuscation related app.*.map.json ================================================ FILE: example/analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: avoid_print: false # Uncomment to disable the `avoid_print` rule prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: example/lib/algorithm_selector_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; // Enum for algorithm types enum LayoutAlgorithmType { tidierTree, buchheimWalker, balloon, radialTree, circle, } class AlgorithmSelectedVIewPage extends StatefulWidget { @override _TreeViewPageState createState() => _TreeViewPageState(); } class _TreeViewPageState extends State with TickerProviderStateMixin { GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 1; // Algorithm selection LayoutAlgorithmType _selectedAlgorithm = LayoutAlgorithmType.tidierTree; Algorithm? _currentAlgorithm; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Tree View - Multiple Algorithms'), ), body: Column( mainAxisSize: MainAxisSize.max, children: [ // Algorithm selection dropdown Container( padding: EdgeInsets.all(8), child: Row( children: [ Text('Layout Algorithm: '), SizedBox(width: 8), Expanded( child: DropdownButton( value: _selectedAlgorithm, isExpanded: true, onChanged: (LayoutAlgorithmType? newValue) { if (newValue != null) { setState(() { _selectedAlgorithm = newValue; _updateAlgorithm(); }); } }, items: LayoutAlgorithmType.values.map>((LayoutAlgorithmType value) { return DropdownMenuItem( value: value, child: Text(_getAlgorithmDisplayName(value)), ); }).toList(), ), ), ], ), ), // Configuration controls Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.siblingSeparation.toString(), decoration: InputDecoration(labelText: 'Sibling Separation'), onChanged: (text) { builder.siblingSeparation = int.tryParse(text) ?? 100; _updateAlgorithm(); this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; _updateAlgorithm(); this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.subtreeSeparation.toString(), decoration: InputDecoration(labelText: 'Subtree separation'), onChanged: (text) { builder.subtreeSeparation = int.tryParse(text) ?? 100; _updateAlgorithm(); this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; _updateAlgorithm(); this.setState(() {}); }, ), ), ElevatedButton( onPressed: () { final node12 = Node.Id(r.nextInt(100)); var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount())); print(edge); graph.addEdge(edge, node12); setState(() {}); }, child: Text('Add'), ), ElevatedButton( onPressed: _navigateToRandomNode, child: Text('Go to Node $nextNodeId'), ), SizedBox(width: 8), ElevatedButton( onPressed: _resetView, child: Text('Reset View'), ), SizedBox(width: 8,), ElevatedButton( onPressed: (){ _controller.zoomToFit(); }, child: Text('Zoom to fit') ) ], ), Expanded( child: GraphView.builder( controller: _controller, graph: graph, algorithm: _currentAlgorithm ?? TidierTreeLayoutAlgorithm(builder, null), builder: (Node node) => Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.lightBlue[100], borderRadius: BorderRadius.circular(8), ), child: Text(node.key?.value.toString() ?? ''), ), ) ), ], )); } String _getAlgorithmDisplayName(LayoutAlgorithmType type) { switch (type) { case LayoutAlgorithmType.tidierTree: return 'Tidier Tree Layout'; case LayoutAlgorithmType.buchheimWalker: return 'Buchheim Walker Tree Layout'; case LayoutAlgorithmType.balloon: return 'Balloon Layout'; case LayoutAlgorithmType.radialTree: return 'Radial Tree Layout'; case LayoutAlgorithmType.circle: return 'Circle Layout'; } } void _updateAlgorithm() { switch (_selectedAlgorithm) { case LayoutAlgorithmType.tidierTree: _currentAlgorithm = TidierTreeLayoutAlgorithm(builder, null); break; case LayoutAlgorithmType.buchheimWalker: _currentAlgorithm = BuchheimWalkerAlgorithm(builder, null); break; case LayoutAlgorithmType.balloon: _currentAlgorithm = BalloonLayoutAlgorithm(builder, null); break; case LayoutAlgorithmType.radialTree: _currentAlgorithm = RadialTreeLayoutAlgorithm(builder, null); break; case LayoutAlgorithmType.circle: final circleConfig = CircleLayoutConfiguration( radius: 200.0, reduceEdgeCrossing: true, reduceEdgeCrossingMaxEdges: 200, ); _currentAlgorithm = CircleLayoutAlgorithm(circleConfig, null); break; } } Widget rectangleWidget(int? a) { return InkWell( onTap: () { print('clicked node $a'); }, child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue[100]!, spreadRadius: 1), ], ), child: Text('Node ${a} ')), ); } final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes.firstWhere( (node) => node.key != null && node.key!.value == nextNodeId, orElse: () => graph.nodes.first, ); final nodeId = randomNode.key!; _controller.animateToNode(nodeId); setState(() { nextNodeId = r.nextInt(graph.nodes.length) + 1; }); } void _resetView() { _controller.resetView(); } @override void initState() { super.initState(); var json = { 'edges': [ // A0 -> B0, B1, B2 {'from': 1, 'to': 2}, // A0 -> B0 {'from': 1, 'to': 3}, // A0 -> B1 {'from': 1, 'to': 4}, // A0 -> B2 // B0 -> C0, C1, C2, C3 {'from': 2, 'to': 5}, // B0 -> C0 {'from': 2, 'to': 6}, // B0 -> C1 {'from': 2, 'to': 7}, // B0 -> C2 {'from': 2, 'to': 8}, // B0 -> C3 // C2 -> H0, H1 {'from': 7, 'to': 9}, // C2 -> H0 {'from': 7, 'to': 10}, // C2 -> H1 // H1 -> H2, H3 {'from': 10, 'to': 11}, // H1 -> H2 {'from': 10, 'to': 12}, // H1 -> H3 // H3 -> H4, H5 {'from': 12, 'to': 13}, // H3 -> H4 {'from': 12, 'to': 14}, // H3 -> H5 // H5 -> H6, H7 {'from': 14, 'to': 15}, // H5 -> H6 {'from': 14, 'to': 16}, // H5 -> H7 // B1 -> D0, D1, D2 {'from': 3, 'to': 17}, // B1 -> D0 {'from': 3, 'to': 18}, // B1 -> D1 {'from': 3, 'to': 19}, // B1 -> D2 // B2 -> E0, E1, E2 {'from': 4, 'to': 20}, // B2 -> E0 {'from': 4, 'to': 21}, // B2 -> E1 {'from': 4, 'to': 22}, // B2 -> E2 // D0 -> F0, F1, F2 {'from': 17, 'to': 23}, // D0 -> F0 {'from': 17, 'to': 24}, // D0 -> F1 {'from': 17, 'to': 25}, // D0 -> F2 // D1 -> G0, G1, G2, G3, G4, G5, G6, G7 {'from': 18, 'to': 26}, // D1 -> G0 {'from': 18, 'to': 27}, // D1 -> G1 {'from': 18, 'to': 28}, // D1 -> G2 {'from': 18, 'to': 29}, // D1 -> G3 {'from': 18, 'to': 30}, // D1 -> G4 {'from': 18, 'to': 31}, // D1 -> G5 {'from': 18, 'to': 32}, // D1 -> G6 {'from': 18, 'to': 33}, // D1 -> G7 ] }; // Usage code (as in your example) var edges = json['edges']!; edges.forEach((element) { var fromNodeId = element['from']; var toNodeId = element['to']; graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId)); }); builder ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); // Initialize with default algorithm _updateAlgorithm(); } } ================================================ FILE: example/lib/decision_tree_screen.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class DecisionTreeScreen extends StatefulWidget { @override _DecisionTreeScreenState createState() => _DecisionTreeScreenState(); } class _DecisionTreeScreenState extends State { final _graph = Graph()..isTree = true; final _configuration = SugiyamaConfiguration() ..orientation = 1 ..nodeSeparation = 40 ..levelSeparation = 50; @override void initState() { super.initState(); _graph.addEdge(Node.Id(1), Node.Id(2)); _graph.addEdge(Node.Id(2), Node.Id(3)); _graph.addEdge(Node.Id(2), Node.Id(11)); _graph.addEdge(Node.Id(3), Node.Id(4)); _graph.addEdge(Node.Id(4), Node.Id(5)); _graph.addEdge(Node.Id(1), Node.Id(6)); _graph.addEdge(Node.Id(6), Node.Id(7)); _graph.addEdge(Node.Id(7), Node.Id(3)); _graph.addEdge(Node.Id(1), Node.Id(10)); _graph.addEdge(Node.Id(10), Node.Id(11)); _graph.addEdge(Node.Id(11), Node.Id(7)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: InteractiveViewer( minScale: 0.1, constrained: false, boundaryMargin: const EdgeInsets.all(64), child: GraphView( graph: _graph, algorithm: SugiyamaAlgorithm(_configuration), builder: (node) { final id = node.key!.value as int; final text = List.generate(id == 1 || id == 4 ? 500 : 10, (index) => 'X').join(' '); return Container( width: 180, decoration: BoxDecoration( color: Color((Random().nextDouble() * 0xFFFFFF).toInt()).withValues(alpha: 1.0), border: Border.all(width: 2), ), padding: const EdgeInsets.all(16), child: Text('$id $text'), ); }, ), ), ); } } ================================================ FILE: example/lib/example.dart ================================================ import 'package:example/layer_graphview.dart'; import 'package:flutter/material.dart'; import 'force_directed_graphview.dart'; import 'tree_graphview.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Home(), ); } } class Home extends StatelessWidget { const Home({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( body: Center( child: Column(children: [ TextButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar(), body: TreeViewPage(), )), ), child: Text( 'Tree View (BuchheimWalker)', style: TextStyle(color: Theme.of(context).primaryColor), )), TextButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar(), body: GraphClusterViewPage(), )), ), child: Text( 'Graph Cluster View (FruchtermanReingold)', style: TextStyle(color: Theme.of(context).primaryColor), )), TextButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar(), body: LayeredGraphViewPage(), )), ), child: Text( 'Layered View (Sugiyama)', style: TextStyle(color: Theme.of(context).primaryColor), )), ]), ), ), ); } } ================================================ FILE: example/lib/force_directed_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class GraphClusterViewPage extends StatefulWidget { @override _GraphClusterViewPageState createState() => _GraphClusterViewPageState(); } class _GraphClusterViewPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Column( children: [ Expanded( child: InteractiveViewer( constrained: false, boundaryMargin: EdgeInsets.all(8), minScale: 0.001, maxScale: 10000, child: GraphViewCustomPainter( graph: graph, algorithm: algorithm, paint: Paint() ..color = Colors.green ..strokeWidth = 1 ..style = PaintingStyle.fill, builder: (Node node) { // I can decide what widget should be shown here based on the id var a = node.key!.value as int?; if (a == 2) { return rectangWidget(a); } return rectangWidget(a); })), ), ], )); } int n = 8; Random r = Random(); Widget rectangWidget(int? i) { return Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue, spreadRadius: 1), ], ), child: Text('Node $i')); } final Graph graph = Graph(); late FruchtermanReingoldAlgorithm algorithm; @override void initState() { final a = Node.Id(1); final b = Node.Id(2); final c = Node.Id(3); final d = Node.Id(4); final e = Node.Id(5); final f = Node.Id(6); final g = Node.Id(7); final h = Node.Id(8); graph.addEdge(a, b, paint: Paint()..color = Colors.red); graph.addEdge(a, c); graph.addEdge(a, d); graph.addEdge(c, e); graph.addEdge(d, f); graph.addEdge(f, c); graph.addEdge(g, c); graph.addEdge(h, g); var config = FruchtermanReingoldConfiguration() ..iterations = 1000; algorithm = FruchtermanReingoldAlgorithm(config); } } ================================================ FILE: example/lib/graph_cluster_animated.dart ================================================ import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class GraphScreen extends StatefulWidget { Graph graph; FruchtermanReingoldAlgorithm algorithm; final Paint? paint; GraphScreen(this.graph, this.algorithm, this.paint); @override _GraphScreenState createState() => _GraphScreenState(); } class _GraphScreenState extends State { bool animated = true; Random r = Random(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Graph Screen'), actions: [ IconButton( icon: Icon(Icons.add), onPressed: () async { setState(() { final node12 = Node.Id(r.nextInt(100).toString()); var edge = widget.graph.getNodeAtPosition(r.nextInt(widget.graph.nodeCount())); print(edge); widget.graph.addEdge(edge, node12); setState(() {}); }); }, ), IconButton( icon: Icon(Icons.animation), onPressed: () async { setState(() { animated = !animated; }); }, ) ], ), body: InteractiveViewer( constrained: false, boundaryMargin: EdgeInsets.all(100), minScale: 0.0001, maxScale: 10.6, child: GraphViewCustomPainter( graph: widget.graph, algorithm: widget.algorithm, builder: (Node node) { // I can decide what widget should be shown here based on the id var a = node.key!.value as String; return rectangWidget(a); }, )), ); } Widget rectangWidget(String? i) { return Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue, spreadRadius: 1), ], ), child: Center(child: Text('Node $i'))); } Future update() async { setState(() {}); } } ================================================ FILE: example/lib/large_tree_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class LargeTreeViewPage extends StatefulWidget { @override _LargeTreeViewPageState createState() => _LargeTreeViewPageState(); } class _LargeTreeViewPageState extends State with TickerProviderStateMixin { GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 1; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Tree View'), ), body: Column( mainAxisSize: MainAxisSize.max, children: [ // Configuration controls Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.siblingSeparation.toString(), decoration: InputDecoration(labelText: 'Sibling Separation'), onChanged: (text) { builder.siblingSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.subtreeSeparation.toString(), decoration: InputDecoration(labelText: 'Subtree separation'), onChanged: (text) { builder.subtreeSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), ElevatedButton( onPressed: () { final node12 = Node.Id(r.nextInt(100)); var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount())); print(edge); graph.addEdge(edge, node12); setState(() {}); }, child: Text('Add'), ), ElevatedButton( onPressed: _navigateToRandomNode, child: Text('Go to Node $nextNodeId'), ), SizedBox(width: 8), ElevatedButton( onPressed: _resetView, child: Text('Reset View'), ), SizedBox(width: 8,), ElevatedButton(onPressed: (){ _controller.zoomToFit(); }, child: Text('Zoom to fit')) ], ), Expanded( child: GraphView.builder( controller: _controller, graph: graph, algorithm: algorithm, centerGraph: true, initialNode: ValueKey(1), panAnimationDuration: Duration(milliseconds: 750), toggleAnimationDuration: Duration(milliseconds: 750), // edgeBuilder: (Edge edge, EdgeGeometry geometry) { // return InteractiveEdge( // edge: edge, // geometry: geometry, // onTap: () => print('Edge tapped: ${edge.key}'), // color: Colors.red, // strokeWidth: 3.0, // ); // }, builder: (Node node) => InkWell( onTap: () => _toggleCollapse(node), child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)], ), child: Text( '${node.key?.value}', ), ), ), ), ), ], )); } final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); late final algorithm = BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)); void _toggleCollapse(Node node) { _controller.toggleNodeExpanded(graph, node, animate: true); } void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes.firstWhere( (node) => node.key != null && node.key!.value == nextNodeId, orElse: () => graph.nodes.first, ); final nodeId = randomNode.key!; _controller.animateToNode(nodeId); setState(() { // nextNodeId = r.nextInt(graph.nodes.length) + 1; }); } void _resetView() { _controller.animateToNode(ValueKey(1)); } @override void initState() { super.initState(); var n = 1000; final nodes = List.generate(n, (i) => Node.Id(i + 1)); // Generate tree edges using a queue-based approach int currentChild = 1; // Start from node 1 (node 0 is root) for (var i = 0; i < n && currentChild < n; i++) { final children = (i < n ~/ 3) ? 3 : 2; for (var j = 0; j < children && currentChild < n; j++) { graph.addEdge(nodes[i], nodes[currentChild]); currentChild++; } } builder ..siblingSeparation = (10) ..levelSeparation = (100) ..subtreeSeparation = (10) ..useCurvedConnections = true ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT); } } ================================================ FILE: example/lib/layer_eiglesperger_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class LayeredEiglspergerGraphViewPage extends StatefulWidget { @override _LayeredEiglspergerGraphViewPageState createState() => _LayeredEiglspergerGraphViewPageState(); } class _LayeredEiglspergerGraphViewPageState extends State { GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Column( mainAxisSize: MainAxisSize.max, children: [ Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.nodeSeparation.toString(), decoration: InputDecoration(labelText: 'Node Separation'), onChanged: (text) { builder.nodeSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 120, child: Column( children: [ Text('Alignment'), DropdownButton( value: builder.coordinateAssignment, items: CoordinateAssignment.values.map((coordinateAssignment) { return DropdownMenuItem( value: coordinateAssignment, child: Text(coordinateAssignment.name), ); }).toList(), onChanged: (value) { setState(() { builder.coordinateAssignment = value!; }); }, ), ], ), ), ElevatedButton( onPressed: () { final node12 = Node.Id(r.nextInt(100)); var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount())); print(edge); graph.addEdge(edge, node12); setState(() {}); }, child: Text('Add'), ), ElevatedButton( onPressed: () => _navigateToRandomNode(), child: Text('Go to Node $nextNodeId'), ), ElevatedButton( onPressed: () => _controller.resetView(), child: Text('Reset View'), ), ElevatedButton( onPressed: () => _controller.zoomToFit(), child: Text('Zoom to fit'), ), ], ), Expanded( child: GraphView.builder( controller: _controller, graph: graph, algorithm: EiglspergerAlgorithm(builder), paint: Paint() ..color = Colors.green ..strokeWidth = 1 ..style = PaintingStyle.stroke, builder: (Node node) { var a = node.key!.value as int?; return rectangleWidget(a); }, ), ), ], )); } Widget rectangleWidget(int? a) { return Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow(color: Colors.blue[100]!, spreadRadius: 1), ], ), child: Text('${a}')); } final Graph graph = Graph(); SugiyamaConfiguration builder = SugiyamaConfiguration() ..bendPointShape = CurvedBendPointShape(curveLength: 20); void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes.firstWhere( (node) => node.key != null && node.key!.value == nextNodeId, orElse: () => graph.nodes.first, ); final nodeId = randomNode.key!; _controller.animateToNode(nodeId); setState(() { nextNodeId = r.nextInt(graph.nodes.length) + 1; }); } @override void initState() { super.initState(); final node1 = Node.Id(1); final node2 = Node.Id(2); final node3 = Node.Id(3); final node4 = Node.Id(4); final node5 = Node.Id(5); final node6 = Node.Id(6); final node8 = Node.Id(7); final node7 = Node.Id(8); final node9 = Node.Id(9); final node10 = Node.Id(10); final node11 = Node.Id(11); final node12 = Node.Id(12); final node13 = Node.Id(13); final node14 = Node.Id(14); final node15 = Node.Id(15); final node16 = Node.Id(16); final node17 = Node.Id(17); final node18 = Node.Id(18); final node19 = Node.Id(19); final node20 = Node.Id(20); final node21 = Node.Id(21); final node22 = Node.Id(22); final node23 = Node.Id(23); graph.addEdge(node1, node13, paint: Paint()..color = Colors.red); graph.addEdge(node1, node21); graph.addEdge(node1, node4); graph.addEdge(node1, node3); graph.addEdge(node2, node3); graph.addEdge(node2, node20); graph.addEdge(node3, node4); graph.addEdge(node3, node5); graph.addEdge(node3, node23); graph.addEdge(node4, node6); graph.addEdge(node5, node7); graph.addEdge(node6, node8); graph.addEdge(node6, node16); graph.addEdge(node6, node23); graph.addEdge(node7, node9); graph.addEdge(node8, node10); graph.addEdge(node8, node11); graph.addEdge(node9, node12); graph.addEdge(node10, node13); graph.addEdge(node10, node14); graph.addEdge(node10, node15); graph.addEdge(node11, node15); graph.addEdge(node11, node16); graph.addEdge(node12, node20); graph.addEdge(node13, node17); graph.addEdge(node14, node17); graph.addEdge(node14, node18); graph.addEdge(node16, node18); graph.addEdge(node16, node19); graph.addEdge(node16, node20); graph.addEdge(node18, node21); graph.addEdge(node19, node22); graph.addEdge(node21, node23); graph.addEdge(node22, node23); graph.addEdge(node1, node22); graph.addEdge(node7, node8); builder ..nodeSeparation = (15) ..levelSeparation = (15) ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM; // Set initial random node for navigation nextNodeId = r.nextInt(22); // 0-21 nodes exist } } ================================================ FILE: example/lib/layer_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class LayeredGraphViewPage extends StatefulWidget { @override _LayeredGraphViewPageState createState() => _LayeredGraphViewPageState(); } class _LayeredGraphViewPageState extends State { final GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 0; bool _showControls = true; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[50], appBar: AppBar( title: Text('Graph Visualizer', style: TextStyle(fontWeight: FontWeight.w600)), backgroundColor: Colors.white, foregroundColor: Colors.grey[800], elevation: 0, actions: [ IconButton( icon: Icon(_showControls ? Icons.visibility_off : Icons.visibility), onPressed: () => setState(() => _showControls = !_showControls), tooltip: 'Toggle Controls', ), IconButton( icon: Icon(Icons.shuffle), onPressed: _navigateToRandomNode, tooltip: 'Random Node', ), ], ), body: Column( children: [ AnimatedContainer( duration: Duration(milliseconds: 300), height: _showControls ? null : 0, child: AnimatedOpacity( duration: Duration(milliseconds: 300), opacity: _showControls ? 1.0 : 0.0, child: _buildControlPanel(), ), ), Expanded(child: _buildGraphView()), ], ), ); } Widget _buildControlPanel() { return Container( margin: EdgeInsets.all(16), padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16), _buildNumericControls(), SizedBox(height: 16), _buildShapeControls(), ], ), ); } Widget _buildNumericControls() { return Wrap( spacing: 12, runSpacing: 12, children: [ _buildSliderControl('Node Sep', builder.nodeSeparation, 5, 50, (v) => builder.nodeSeparation = v), _buildSliderControl('Level Sep', builder.levelSeparation, 5, 100, (v) => builder.levelSeparation = v), _buildDropdown('Alignment', builder.coordinateAssignment, CoordinateAssignment.values, (v) => builder.coordinateAssignment = v), _buildDropdown('Layering', builder.layeringStrategy, LayeringStrategy.values, (v) => builder.layeringStrategy = v), _buildDropdown('Cross Min', builder.crossMinimizationStrategy, CrossMinimizationStrategy.values, (v) => builder.crossMinimizationStrategy = v), _buildDropdown('Cycle Removal', builder.cycleRemovalStrategy, CycleRemovalStrategy.values, (v) => builder.cycleRemovalStrategy = v), ], ); } Widget _buildSliderControl(String label, int value, int min, int max, Function(int) onChanged) { return Container( width: 200, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), Slider( value: value.toDouble().clamp(min.toDouble(), max.toDouble()), min: min.toDouble(), max: max.toDouble(), divisions: max - min, label: value.toString(), onChanged: (v) => setState(() => onChanged(v.round())), ), ], ), ); } Widget _buildDropdown(String label, T value, List items, Function(T) onChanged) { return Container( width: 160, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), SizedBox(height: 4), Container( padding: EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(8), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, items: items.map((item) => DropdownMenuItem(value: item, child: Text(item.toString().split('.').last, style: TextStyle(fontSize: 12)))).toList(), onChanged: (v) => setState(() => onChanged(v!)), ), ), ), ], ), ); } Widget _buildShapeControls() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Edge Shape', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), SizedBox(height: 8), Row( children: [ _buildShapeButton('Sharp', builder.bendPointShape is SharpBendPointShape, () => builder.bendPointShape = SharpBendPointShape()), SizedBox(width: 8), _buildShapeButton('Curved', builder.bendPointShape is CurvedBendPointShape, () => builder.bendPointShape = CurvedBendPointShape(curveLength: 20)), SizedBox(width: 8), _buildShapeButton('Max Curved', builder.bendPointShape is MaxCurvedBendPointShape, () => builder.bendPointShape = MaxCurvedBendPointShape()), Spacer(), Row( children: [ Text('Post Straighten', style: TextStyle(fontSize: 12)), Switch( value: builder.postStraighten, onChanged: (v) => setState(() => builder.postStraighten = v), activeThumbColor: Colors.blue, ), ], ), ], ), ], ); } Widget _buildShapeButton(String text, bool isSelected, VoidCallback onPressed) { return ElevatedButton( onPressed: () => setState(onPressed), child: Text(text, style: TextStyle(fontSize: 11)), style: ElevatedButton.styleFrom( backgroundColor: isSelected ? Colors.blue : Colors.grey[100], foregroundColor: isSelected ? Colors.white : Colors.grey[700], elevation: isSelected ? 2 : 0, padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ); } Widget _buildGraphView() { return Container( margin: EdgeInsets.fromLTRB(16, 0, 16, 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 2))], ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: GraphView.builder( controller: _controller, graph: graph, algorithm: SugiyamaAlgorithm(builder), paint: Paint() ..color = Colors.blue[300]! ..strokeWidth = 2 ..style = PaintingStyle.stroke, builder: (Node node) { final nodeId = node.key!.value as int; return Container( width: 40, height: 40, decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue[400]!, Colors.blue[600]!], begin: Alignment.topLeft, end: Alignment.bottomRight, ), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.blue[100]!, blurRadius: 8, offset: Offset(0, 2))], ), child: Center( child: Text('$nodeId', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 14)), ), ); }, ), ), ); } final Graph graph = Graph(); SugiyamaConfiguration builder = SugiyamaConfiguration() ..bendPointShape = CurvedBendPointShape(curveLength: 20) ..nodeSeparation = 15 ..levelSeparation = 15 ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM; void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes[r.nextInt(graph.nodes.length)]; _controller.animateToNode(randomNode.key!); } @override void initState() { super.initState(); _initializeGraph(); } void _initializeGraph() { // Define edges more concisely final node1 = Node.Id(1); final node2 = Node.Id(2); final node3 = Node.Id(3); final node4 = Node.Id(4); final node5 = Node.Id(5); final node6 = Node.Id(6); final node8 = Node.Id(7); final node7 = Node.Id(8); final node9 = Node.Id(9); final node10 = Node.Id(10); final node11 = Node.Id(11); final node12 = Node.Id(12); final node13 = Node.Id(13); final node14 = Node.Id(14); final node15 = Node.Id(15); final node16 = Node.Id(16); final node17 = Node.Id(17); final node18 = Node.Id(18); final node19 = Node.Id(19); final node20 = Node.Id(20); final node21 = Node.Id(21); final node22 = Node.Id(22); final node23 = Node.Id(23); graph.addEdge(node1, node13, paint: Paint()..color = Colors.red); graph.addEdge(node1, node21); graph.addEdge(node1, node4); graph.addEdge(node1, node3); graph.addEdge(node2, node3); graph.addEdge(node2, node20); graph.addEdge(node3, node4); graph.addEdge(node3, node5); graph.addEdge(node3, node23); graph.addEdge(node4, node6); graph.addEdge(node5, node7); graph.addEdge(node6, node8); graph.addEdge(node6, node16); graph.addEdge(node6, node23); graph.addEdge(node7, node9); graph.addEdge(node8, node10); graph.addEdge(node8, node11); graph.addEdge(node9, node12); graph.addEdge(node10, node13); graph.addEdge(node10, node14); graph.addEdge(node10, node15); graph.addEdge(node11, node15); graph.addEdge(node11, node16); graph.addEdge(node12, node20); graph.addEdge(node13, node17); graph.addEdge(node14, node17); graph.addEdge(node14, node18); graph.addEdge(node16, node18); graph.addEdge(node16, node19); graph.addEdge(node16, node20); graph.addEdge(node18, node21); graph.addEdge(node19, node22); graph.addEdge(node21, node23); graph.addEdge(node22, node23); graph.addEdge(node1, node22); graph.addEdge(node7, node8); } } ================================================ FILE: example/lib/layer_graphview_json.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class LayerGraphPageFromJson extends StatefulWidget { @override _LayerGraphPageFromJsonState createState() => _LayerGraphPageFromJsonState(); } class _LayerGraphPageFromJsonState extends State { var json = { 'edges': [ { 'from': '1', 'to': '2' }, { 'from': '3', 'to': '2' }, { 'from': '4', 'to': '5' }, { 'from': '6', 'to': '4' }, { 'from': '2', 'to': '4' }, { 'from': '2', 'to': '7' }, { 'from': '2', 'to': '8' }, { 'from': '9', 'to': '10' }, { 'from': '9', 'to': '11' }, { 'from': '5', 'to': '12' }, { 'from': '4', 'to': '9' }, { 'from': '6', 'to': '13' }, { 'from': '6', 'to': '14' }, { 'from': '6', 'to': '15' }, { 'from': '16', 'to': '3' }, { 'from': '17', 'to': '3' }, { 'from': '18', 'to': '16' }, { 'from': '19', 'to': '17' }, { 'from': '11', 'to': '1' }, ] }; GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Column( mainAxisSize: MainAxisSize.max, children: [ Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.nodeSeparation.toString(), decoration: InputDecoration(labelText: 'Node Separation'), onChanged: (text) { builder.nodeSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: Column( children: [ Text('Alignment'), DropdownButton( value: builder.coordinateAssignment, items: CoordinateAssignment.values.map((coordinateAssignment) { return DropdownMenuItem( value: coordinateAssignment, child: Text(coordinateAssignment.name), ); }).toList(), onChanged: (value) { setState(() { builder.coordinateAssignment = value!; }); }, ), ], ), ), ElevatedButton( onPressed: () => _navigateToRandomNode(), child: Text('Go to Node $nextNodeId'), ), ElevatedButton( onPressed: () => _controller.resetView(), child: Text('Reset View'), ), ElevatedButton( onPressed: () => _controller.zoomToFit(), child: Text('Zoom to fit'), ), ], ), Expanded( child: GraphView.builder( controller: _controller, graph: graph, algorithm: SugiyamaAlgorithm(builder), paint: Paint() ..color = Colors.green ..strokeWidth = 1 ..style = PaintingStyle.stroke, builder: (Node node) { // I can decide what widget should be shown here based on the id var a = node.key!.value; return rectangleWidget(a, node); }, ), ), ], )); } void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes.firstWhere( (node) => node.key != null && node.key!.value == nextNodeId, orElse: () => graph.nodes.first, ); final nodeId = randomNode.key!; _controller.animateToNode(nodeId); setState(() { nextNodeId = r.nextInt(graph.nodes.length) + 1; }); } Widget rectangleWidget(String? a, Node node) { return Container( color: Colors.amber, child: InkWell( onTap: () { print('clicked'); }, child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue[100]!, spreadRadius: 1), ], ), child: Text('${a}')), ), ); } final Graph graph = Graph(); @override void initState() { super.initState(); var edges = json['edges']!; edges.forEach((element) { var fromNodeId = element['from']; var toNodeId = element['to']; graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId)); }); builder ..nodeSeparation = (15) ..levelSeparation = (15) ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM; } } var builder = SugiyamaConfiguration(); ================================================ FILE: example/lib/main.dart ================================================ import 'package:example/algorithm_selector_graphview.dart'; import 'package:example/decision_tree_screen.dart'; import 'package:example/large_tree_graphview.dart'; import 'package:example/layer_graphview.dart'; import 'package:example/mindmap_graphview.dart'; import 'package:example/mutliple_forest_graphview.dart'; import 'package:example/tree_graphview_json.dart'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; import 'force_directed_graphview.dart'; import 'graph_cluster_animated.dart'; import 'layer_eiglesperger_graphview.dart'; import 'layer_graphview_json.dart'; import 'tree_graphview.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'GraphView Demo', theme: ThemeData( primarySwatch: Colors.blue, fontFamily: 'SF Pro Display', visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Home(), debugShowCheckedModeBanner: false, ); } } class Home extends StatelessWidget { const Home({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF667eea), Color(0xFF764ba2), ], ), ), child: SafeArea( child: Column( children: [ Expanded( child: _buildScrollableContent(), ), ], ), ), ), ); } Widget _buildScrollableContent() { return SingleChildScrollView( physics: BouncingScrollPhysics(), padding: EdgeInsets.all(20), child: Column( children: [ _buildSection('Tree Algorithms', [ _buildButton( 'Tree View', 'BuchheimWalker Algorithm', Icons.account_tree, Colors.deepPurple, () => TreeViewPage(), ), _buildButton( 'Tree from JSON', 'Dynamic tree generation', Icons.data_object, Colors.indigo, () => TreeViewPageFromJson(), ), _buildButton( 'Large Tree View', '1000 nodes', Icons.data_object, Colors.indigo, () => LargeTreeViewPage(), ), _buildButton( 'Multiple Forest Tree View', 'Multiple Nodes', Icons.data_object, Colors.indigo, () => MultipleForestTreeViewPage(), ), ]), _buildSection('Layered Algorithms', [ _buildButton( 'Layered View', 'Sugiyama Algorithm', Icons.layers, Colors.teal, () => LayeredGraphViewPage(), ), _buildButton( 'Layer from JSON', 'JSON-based layered graphs', Icons.timeline, Colors.cyan, () => LayerGraphPageFromJson(), ), _buildButton( 'Decision Tree', 'Decision-making visualization', Icons.device_hub, Colors.green, () => DecisionTreeScreen(), ), ]), _buildSection('Cluster Algorithms', [ _buildButton( 'Graph Cluster', 'FruchtermanReingold Algorithm', Icons.bubble_chart, Colors.orange, () => GraphClusterViewPage(), ), _buildCustomGraphButton( 'Square Grid', 'Structured 3x3 layout', Icons.grid_3x3, Colors.pink, _createSquareGraph, ), _buildCustomGraphButton( 'Triangle Grid', 'Complex network topology', Icons.change_history, Colors.deepOrange, _createTriangleGraph, ), ]), _buildSection('Specialized Views', [ _buildButton( 'Algorithm SelectorPage', 'Multiple Algorithms using the same graph', Icons.code, Colors.brown, () => AlgorithmSelectedVIewPage(), ), _buildButton( 'Mind Map', 'Conceptual mapping', Icons.psychology, Colors.purple, () => MindMapPage(), ), _buildButton( 'Layered View', 'Eiglesperger Algorithm (Broken)', Icons.layers, Colors.teal, () => LayeredEiglspergerGraphViewPage(), ), ]), ], ), ); } Widget _buildSection(String title, List buttons) { return Container( margin: EdgeInsets.only(bottom: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.only(left: 4, bottom: 12), child: Text( title, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: Colors.white, letterSpacing: -0.3, ), ), ), ...buttons.map((button) => Padding( padding: EdgeInsets.only(bottom: 12), child: button, )), ], ), ); } Widget _buildButton( String title, String subtitle, IconData icon, Color color, Widget Function() pageBuilder, ) { return Builder( builder: (context) => Container( height: 80, child: Material( borderRadius: BorderRadius.circular(16), elevation: 4, shadowColor: Colors.black.withValues(alpha: 0.1), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () => Navigator.push( context, MaterialPageRoute(builder: (context) => pageBuilder()), ), child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ color.withValues(alpha: 0.1), Colors.white, ], ), border: Border.all( color: color.withValues(alpha: 0.3), width: 1, ), ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( icon, color: color, size: 24, ), ), SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[800], ), ), SizedBox(height: 2), Text( subtitle, style: TextStyle( fontSize: 13, color: Colors.grey[600], fontWeight: FontWeight.w400, ), ), ], ), ), Icon( Icons.arrow_forward_ios, color: Colors.grey[400], size: 16, ), ], ), ), ), ), ), ); } Widget _buildCustomGraphButton( String title, String subtitle, IconData icon, Color color, Graph Function() graphBuilder, ) { return Builder( builder: (context) => Container( height: 80, child: Material( borderRadius: BorderRadius.circular(16), elevation: 4, shadowColor: Colors.black.withValues(alpha: 0.1), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () { var graph = graphBuilder(); var builder = FruchtermanReingoldAlgorithm( FruchtermanReingoldConfiguration()); Navigator.push( context, MaterialPageRoute( builder: (context) => GraphScreen(graph, builder, null), ), ); }, child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ color.withValues(alpha: 0.1), Colors.white, ], ), border: Border.all( color: color.withValues(alpha: 0.3), width: 1, ), ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( icon, color: color, size: 24, ), ), SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[800], ), ), SizedBox(height: 2), Text( subtitle, style: TextStyle( fontSize: 13, color: Colors.grey[600], fontWeight: FontWeight.w400, ), ), ], ), ), Icon( Icons.arrow_forward_ios, color: Colors.grey[400], size: 16, ), ], ), ), ), ), ), ); } Graph _createSquareGraph() { var graph = Graph(); Node node1 = Node.Id('One'); Node node2 = Node.Id('Two'); Node node3 = Node.Id('Three'); Node node4 = Node.Id('Four'); Node node5 = Node.Id('Five'); Node node6 = Node.Id('Six'); Node node7 = Node.Id('Seven'); Node node8 = Node.Id('Eight'); Node node9 = Node.Id('Nine'); graph.addEdge(node1, node2); graph.addEdge(node1, node4); graph.addEdge(node2, node3); graph.addEdge(node2, node5); graph.addEdge(node3, node6); graph.addEdge(node4, node5); graph.addEdge(node4, node7); graph.addEdge(node5, node6); graph.addEdge(node5, node8); graph.addEdge(node6, node9); graph.addEdge(node7, node8); graph.addEdge(node8, node9); return graph; } Graph _createTriangleGraph() { var graph = Graph(); Node node1 = Node.Id('One'); Node node2 = Node.Id('Two'); Node node3 = Node.Id('Three'); Node node4 = Node.Id('Four'); Node node5 = Node.Id('Five'); Node node6 = Node.Id('Six'); Node node7 = Node.Id('Seven'); Node node8 = Node.Id('Eight'); Node node9 = Node.Id('Nine'); Node node10 = Node.Id('Ten'); graph.addEdge(node1, node2); graph.addEdge(node1, node3); graph.addEdge(node2, node4); graph.addEdge(node2, node5); graph.addEdge(node2, node3); graph.addEdge(node3, node5); graph.addEdge(node3, node6); graph.addEdge(node4, node7); graph.addEdge(node4, node8); graph.addEdge(node4, node5); graph.addEdge(node5, node8); graph.addEdge(node5, node9); graph.addEdge(node5, node6); graph.addEdge(node9, node6); graph.addEdge(node10, node6); graph.addEdge(node7, node8); graph.addEdge(node8, node9); graph.addEdge(node9, node10); return graph; } } ================================================ FILE: example/lib/mindmap_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class MindMapPage extends StatefulWidget { @override _MindMapPageState createState() => _MindMapPageState(); } class _MindMapPageState extends State with TickerProviderStateMixin { GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 1; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Tree View'), ), body: Column( mainAxisSize: MainAxisSize.max, children: [ // Configuration controls Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.siblingSeparation.toString(), decoration: InputDecoration(labelText: 'Sibling Separation'), onChanged: (text) { builder.siblingSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.subtreeSeparation.toString(), decoration: InputDecoration(labelText: 'Subtree separation'), onChanged: (text) { builder.subtreeSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), ElevatedButton( onPressed: () { final node12 = Node.Id(r.nextInt(100)); var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount())); print(edge); graph.addEdge(edge, node12); setState(() {}); }, child: Text('Add'), ), ElevatedButton( onPressed: _navigateToRandomNode, child: Text('Go to Node $nextNodeId'), ), SizedBox(width: 8), ElevatedButton( onPressed: _resetView, child: Text('Reset View'), ), SizedBox(width: 8,), ElevatedButton(onPressed: (){ _controller.zoomToFit(); }, child: Text('Zoom to fit')) ], ), Expanded( child: GraphView.builder( controller: _controller, graph: graph, algorithm: MindmapAlgorithm( builder, MindmapEdgeRenderer(builder) ), builder: (Node node) => Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)], ), child: Text( 'Node ${node.key?.value}', ), ), ), ), ], )); } Widget rectangleWidget(int? a) { return InkWell( onTap: () { print('clicked node $a'); }, child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue[100]!, spreadRadius: 1), ], ), child: Text('Node ${a} ')), ); } final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes.firstWhere( (node) => node.key != null && node.key!.value == nextNodeId, orElse: () => graph.nodes.first, ); final nodeId = randomNode.key!; _controller.animateToNode(nodeId); setState(() { nextNodeId = r.nextInt(graph.nodes.length) + 1; }); } void _resetView() { _controller.resetView(); } @override void initState() { super.initState(); // Complex Mindmap Test - This will stress test the balancing algorithm // Create all nodes final root = Node.Id(1); // Central topic // Left side - Technology branch (will be large) final tech = Node.Id(2); final ai = Node.Id(3); final web = Node.Id(4); final mobile = Node.Id(5); final aiSubtopics = [ Node.Id(6), // Machine Learning Node.Id(7), // Deep Learning Node.Id(8), // NLP Node.Id(9), // Computer Vision ]; final webSubtopics = [ Node.Id(10), // Frontend Node.Id(11), // Backend Node.Id(12), // DevOps ]; final frontendDetails = [ Node.Id(13), // React Node.Id(14), // Vue Node.Id(15), // Angular ]; final backendDetails = [ Node.Id(16), // Node.js Node.Id(17), // Python Node.Id(18), // Java Node.Id(19), // Go ]; // Right side - Business branch (will be smaller to test balancing) final business = Node.Id(20); final marketing = Node.Id(21); final sales = Node.Id(22); final finance = Node.Id(23); final marketingDetails = [ Node.Id(24), // Digital Marketing Node.Id(25), // Content Strategy ]; final salesDetails = [ Node.Id(26), // B2B Sales Node.Id(27), // Customer Success ]; // Additional right side - Personal branch final personal = Node.Id(28); final health = Node.Id(29); final hobbies = Node.Id(30); final healthDetails = [ Node.Id(31), // Exercise Node.Id(32), // Nutrition Node.Id(33), // Mental Health ]; final exerciseDetails = [ Node.Id(34), // Cardio Node.Id(35), // Strength Training Node.Id(36), // Yoga ]; // Build the graph structure graph.addEdge(root, tech); graph.addEdge(root, business, paint: Paint()..color = Colors.blue); graph.addEdge(root, personal, paint: Paint()..color = Colors.green); // Technology branch (left side - large subtree) graph.addEdge(tech, ai); graph.addEdge(tech, web); graph.addEdge(tech, mobile); // AI subtree for (final aiNode in aiSubtopics) { graph.addEdge(ai, aiNode, paint: Paint()..color = Colors.purple); } // Web subtree with deep nesting for (final webNode in webSubtopics) { graph.addEdge(web, webNode, paint: Paint()..color = Colors.orange); } // Frontend details (3rd level) for (final frontendNode in frontendDetails) { graph.addEdge(webSubtopics[0], frontendNode, paint: Paint()..color = Colors.cyan); } // Backend details (3rd level) - even deeper for (final backendNode in backendDetails) { graph.addEdge(webSubtopics[1], backendNode, paint: Paint()..color = Colors.teal); } // Business branch (right side - smaller subtree) graph.addEdge(business, marketing); graph.addEdge(business, sales); graph.addEdge(business, finance); // Marketing details for (final marketingNode in marketingDetails) { graph.addEdge(marketing, marketingNode, paint: Paint()..color = Colors.red); } // Sales details for (final salesNode in salesDetails) { graph.addEdge(sales, salesNode, paint: Paint()..color = Colors.indigo); } // Personal branch (right side - medium subtree) graph.addEdge(personal, health); graph.addEdge(personal, hobbies); // Health details for (final healthNode in healthDetails) { graph.addEdge(health, healthNode, paint: Paint()..color = Colors.lightGreen); } // Exercise details (3rd level) for (final exerciseNode in exerciseDetails) { graph.addEdge(healthDetails[0], exerciseNode, paint: Paint()..color = Colors.amber); } builder ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); } } ================================================ FILE: example/lib/mutliple_forest_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class MultipleForestTreeViewPage extends StatefulWidget { @override _TreeViewPageState createState() => _TreeViewPageState(); } class _TreeViewPageState extends State with TickerProviderStateMixin { GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 1; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Tree View'), ), body: Column( mainAxisSize: MainAxisSize.max, children: [ // Configuration controls Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.siblingSeparation.toString(), decoration: InputDecoration(labelText: 'Sibling Separation'), onChanged: (text) { builder.siblingSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.subtreeSeparation.toString(), decoration: InputDecoration(labelText: 'Subtree separation'), onChanged: (text) { builder.subtreeSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), ElevatedButton( onPressed: () { final node12 = Node.Id(r.nextInt(100)); var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount())); print(edge); graph.addEdge(edge, node12); setState(() {}); }, child: Text('Add'), ), ElevatedButton( onPressed: _navigateToRandomNode, child: Text('Go to Node $nextNodeId'), ), SizedBox(width: 8), ElevatedButton( onPressed: _resetView, child: Text('Reset View'), ), SizedBox(width: 8,), ElevatedButton(onPressed: (){ _controller.zoomToFit(); }, child: Text('Zoom to fit')) ], ), Expanded( child: GraphView.builder( controller: _controller, graph: graph, algorithm: TidierTreeLayoutAlgorithm(builder, null), builder: (Node node) => Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.lightBlue[100], borderRadius: BorderRadius.circular(8), ), child: Text(node.key?.value.toString() ?? ''), ), ) ), ], )); } Widget rectangleWidget(int? a) { return InkWell( onTap: () { print('clicked node $a'); }, child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue[100]!, spreadRadius: 1), ], ), child: Text('Node ${a} ')), ); } final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes.firstWhere( (node) => node.key != null && node.key!.value == nextNodeId, orElse: () => graph.nodes.first, ); final nodeId = randomNode.key!; _controller.animateToNode(nodeId); setState(() { nextNodeId = r.nextInt(graph.nodes.length) + 1; }); } void _resetView() { _controller.resetView(); } @override void initState() { super.initState(); var json = { 'edges': [ {'from': 1, 'to': 2}, {'from': 9, 'to': 2}, {'from': 10, 'to': 2}, {'from': 2, 'to': 3}, {'from': 2, 'to': 4}, {'from': 2, 'to': 5}, {'from': 5, 'to': 6}, {'from': 5, 'to': 7}, {'from': 6, 'to': 8}, {'from': 12, 'to': 11}, ] }; var edges = json['edges']!; edges.forEach((element) { var fromNodeId = element['from']; var toNodeId = element['to']; graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId)); }); builder ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); } } ================================================ FILE: example/lib/tree_graphview.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class TreeViewPage extends StatefulWidget { @override _TreeViewPageState createState() => _TreeViewPageState(); } class _TreeViewPageState extends State with TickerProviderStateMixin { GraphViewController _controller = GraphViewController(); final Random r = Random(); int nextNodeId = 1; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Tree View'), ), body: Column( mainAxisSize: MainAxisSize.max, children: [ // Configuration controls Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.siblingSeparation.toString(), decoration: InputDecoration(labelText: 'Sibling Separation'), onChanged: (text) { builder.siblingSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.subtreeSeparation.toString(), decoration: InputDecoration(labelText: 'Subtree separation'), onChanged: (text) { builder.subtreeSeparation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; this.setState(() {}); }, ), ), ElevatedButton( onPressed: () { final node12 = Node.Id(r.nextInt(100)); var edge = graph.getNodeAtPosition(r.nextInt(graph.nodeCount())); print(edge); graph.addEdge(edge, node12); setState(() {}); }, child: Text('Add'), ), ElevatedButton( onPressed: _navigateToRandomNode, child: Text('Go to Node $nextNodeId'), ), SizedBox(width: 8), ElevatedButton( onPressed: _resetView, child: Text('Reset View'), ), SizedBox(width: 8,), ElevatedButton(onPressed: (){ _controller.zoomToFit(); }, child: Text('Zoom to fit')) ], ), Expanded( child: GraphView.builder( controller: _controller, graph: graph, algorithm: algorithm, initialNode: ValueKey(1), panAnimationDuration: Duration(milliseconds: 600), toggleAnimationDuration: Duration(milliseconds: 600), centerGraph: true, builder: (Node node) => GestureDetector( onTap: () => _toggleCollapse(node), child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.blue[100]!, spreadRadius: 1)], ), child: Text( 'Node ${node.key?.value}', ), ), ), ), ), ], )); } final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); late final algorithm = BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)); void _toggleCollapse(Node node) { _controller.toggleNodeExpanded(graph, node, animate: true); } void _navigateToRandomNode() { if (graph.nodes.isEmpty) return; final randomNode = graph.nodes.firstWhere( (node) => node.key != null && node.key!.value == nextNodeId, orElse: () => graph.nodes.first, ); final nodeId = randomNode.key!; _controller.animateToNode(nodeId); setState(() { // nextNodeId = r.nextInt(graph.nodes.length) + 1; }); } void _resetView() { _controller.animateToNode(ValueKey(1)); } @override void initState() { super.initState(); // Create all nodes final root = Node.Id(1); // Central topic // Left side - Technology branch (will be large) final tech = Node.Id(2); final ai = Node.Id(3); final web = Node.Id(4); final mobile = Node.Id(5); final aiSubtopics = [ Node.Id(6), // Machine Learning Node.Id(7), // Deep Learning Node.Id(8), // NLP Node.Id(9), // Computer Vision ]; final webSubtopics = [ Node.Id(10), // Frontend Node.Id(11), // Backend Node.Id(12), // DevOps ]; final frontendDetails = [ Node.Id(13), // React Node.Id(14), // Vue Node.Id(15), // Angular ]; final backendDetails = [ Node.Id(16), // Node.js Node.Id(17), // Python Node.Id(18), // Java Node.Id(19), // Go ]; // Right side - Business branch (will be smaller to test balancing) final business = Node.Id(20); final marketing = Node.Id(21); final sales = Node.Id(22); final finance = Node.Id(23); final marketingDetails = [ Node.Id(24), // Digital Marketing Node.Id(25), // Content Strategy ]; final salesDetails = [ Node.Id(26), // B2B Sales Node.Id(27), // Customer Success ]; // Additional right side - Personal branch final personal = Node.Id(28); final health = Node.Id(29); final hobbies = Node.Id(30); final healthDetails = [ Node.Id(31), // Exercise Node.Id(32), // Nutrition Node.Id(33), // Mental Health ]; final exerciseDetails = [ Node.Id(34), // Cardio Node.Id(35), // Strength Training Node.Id(36), // Yoga ]; // Build the graph structure graph.addEdge(root, tech); graph.addEdge(root, business, paint: Paint()..color = Colors.blue); graph.addEdge(root, personal, paint: Paint()..color = Colors.green); // // Technology branch (left side - large subtree) graph.addEdge(tech, ai); graph.addEdge(tech, web); graph.addEdge(tech, mobile); // AI subtree for (final aiNode in aiSubtopics) { graph.addEdge(ai, aiNode, paint: Paint()..color = Colors.purple); } // Web subtree with deep nesting for (final webNode in webSubtopics) { graph.addEdge(web, webNode, paint: Paint()..color = Colors.orange); } // Frontend details (3rd level) for (final frontendNode in frontendDetails) { graph.addEdge(webSubtopics[0], frontendNode, paint: Paint()..color = Colors.cyan); } // Backend details (3rd level) - even deeper for (final backendNode in backendDetails) { graph.addEdge(webSubtopics[1], backendNode, paint: Paint()..color = Colors.teal); } // Business branch (right side - smaller subtree) graph.addEdge(business, marketing); graph.addEdge(business, sales); graph.addEdge(business, finance); // Marketing details for (final marketingNode in marketingDetails) { graph.addEdge(marketing, marketingNode, paint: Paint()..color = Colors.red); } // Sales details for (final salesNode in salesDetails) { graph.addEdge(sales, salesNode, paint: Paint()..color = Colors.indigo); } // Personal branch (right side - medium subtree) graph.addEdge(personal, health); graph.addEdge(personal, hobbies); // Health details for (final healthNode in healthDetails) { graph.addEdge(health, healthNode, paint: Paint()..color = Colors.lightGreen); } // Exercise details (3rd level) for (final exerciseNode in exerciseDetails) { graph.addEdge(healthDetails[0], exerciseNode, paint: Paint()..color = Colors.amber); } _controller.setInitiallyCollapsedNodes(graph, [tech, business, personal]); builder ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..useCurvedConnections = true ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); } } ================================================ FILE: example/lib/tree_graphview_json.dart ================================================ import 'package:flutter/material.dart'; import 'package:graphview/GraphView.dart'; class TreeViewPageFromJson extends StatefulWidget { @override _TreeViewPageFromJsonState createState() => _TreeViewPageFromJsonState(); } class _TreeViewPageFromJsonState extends State { var json = { 'nodes': [ {'id': 1, 'label': 'circle'}, {'id': 2, 'label': 'ellipse'}, {'id': 3, 'label': 'database'}, {'id': 4, 'label': 'box'}, {'id': 5, 'label': 'diamond'}, {'id': 6, 'label': 'dot'}, {'id': 7, 'label': 'square'}, {'id': 8, 'label': 'triangle'}, ], 'edges': [ {'from': 1, 'to': 2}, {'from': 2, 'to': 3}, {'from': 2, 'to': 4}, {'from': 2, 'to': 5}, {'from': 5, 'to': 6}, {'from': 5, 'to': 7}, {'from': 6, 'to': 8} ] }; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Column( mainAxisSize: MainAxisSize.max, children: [ Wrap( children: [ Container( width: 100, child: TextFormField( initialValue: builder.siblingSeparation.toString(), decoration: InputDecoration(labelText: 'Sibling Separation'), onChanged: (text) { builder.siblingSeparation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.levelSeparation.toString(), decoration: InputDecoration(labelText: 'Level Separation'), onChanged: (text) { builder.levelSeparation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.subtreeSeparation.toString(), decoration: InputDecoration(labelText: 'Subtree separation'), onChanged: (text) { builder.subtreeSeparation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), Container( width: 100, child: TextFormField( initialValue: builder.orientation.toString(), decoration: InputDecoration(labelText: 'Orientation'), onChanged: (text) { builder.orientation = int.tryParse(text) ?? 100; setState(() {}); }, ), ), ], ), Expanded( child: InteractiveViewer( constrained: false, boundaryMargin: EdgeInsets.all(100), minScale: 0.01, maxScale: 5.6, child: GraphView( graph: graph, algorithm: BuchheimWalkerAlgorithm(builder, TreeEdgeRenderer(builder)), paint: Paint() ..color = Colors.green ..strokeWidth = 1 ..style = PaintingStyle.stroke, builder: (Node node) { // I can decide what widget should be shown here based on the id var a = node.key!.value as int?; var nodes = json['nodes']!; var nodeValue = nodes.firstWhere((element) => element['id'] == a); return rectangleWidget(nodeValue['label'] as String?); }, )), ), ], )); } Widget rectangleWidget(String? a) { return InkWell( onTap: () { print('clicked'); }, child: Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.blue[100]!, spreadRadius: 1), ], ), child: Text('${a}')), ); } final Graph graph = Graph()..isTree = true; BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); @override void initState() { super.initState(); var edges = json['edges']!; edges.forEach((element) { var fromNodeId = element['from']; var toNodeId = element['to']; graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId)); }); builder ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); } } ================================================ FILE: example/pubspec.yaml ================================================ name: example description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: sdk: '>=2.15.0 <4.0.0' dependencies: flutter: sdk: flutter graphview: path: ../ provider: ^6.0.3 dev_dependencies: flutter_test: sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: lib/Algorithm.dart ================================================ part of graphview; abstract class Algorithm { EdgeRenderer? renderer; /// Executes the algorithm. /// @param shiftY Shifts the y-coordinate origin /// @param shiftX Shifts the x-coordinate origin /// @return The size of the graph Size run(Graph? graph, double shiftX, double shiftY); void init(Graph? graph); void setDimensions(double width, double height); } ================================================ FILE: lib/Graph.dart ================================================ part of graphview; class Graph { final List _nodes = []; final List _edges = []; List graphObserver = []; // Cache final Map> _successorCache = {}; final Map> _predecessorCache = {}; bool _cacheValid = false; List get nodes => _nodes; List get edges => _edges; var isTree = false; int nodeCount() => _nodes.length; void addNode(Node node) { _nodes.add(node); _cacheValid = false; notifyGraphObserver(); } void addNodes(List nodes) => nodes.forEach((it) => addNode(it)); void removeNode(Node? node) { if (!_nodes.contains(node)) return; if (isTree) { successorsOf(node).forEach((element) => removeNode(element)); } _nodes.remove(node); _edges .removeWhere((edge) => edge.source == node || edge.destination == node); _cacheValid = false; notifyGraphObserver(); } void removeNodes(List nodes) => nodes.forEach((it) => removeNode(it)); Edge addEdge(Node source, Node destination, {Paint? paint}) { final edge = Edge(source, destination, paint: paint); addEdgeS(edge); return edge; } void addEdgeS(Edge edge) { var sourceSet = false; var destinationSet = false; for (var node in _nodes) { if (!sourceSet && node == edge.source) { edge.source = node; sourceSet = true; } if (!destinationSet && node == edge.destination) { edge.destination = node; destinationSet = true; } if (sourceSet && destinationSet) { break; } } if (!sourceSet) { _nodes.add(edge.source); sourceSet = true; if (!destinationSet && edge.destination == edge.source) { destinationSet = true; } } if (!destinationSet) { _nodes.add(edge.destination); destinationSet = true; } if (!_edges.contains(edge)) { _edges.add(edge); _cacheValid = false; notifyGraphObserver(); } } void addEdges(List edges) => edges.forEach((it) => addEdgeS(it)); void removeEdge(Edge edge) { _edges.remove(edge); _cacheValid = false; } void removeEdges(List edges) => edges.forEach((it) => removeEdge(it)); void removeEdgeFromPredecessor(Node? predecessor, Node? current) { _edges.removeWhere( (edge) => edge.source == predecessor && edge.destination == current); _cacheValid = false; } bool hasNodes() => _nodes.isNotEmpty; Edge? getEdgeBetween(Node source, Node? destination) => _edges.firstWhereOrNull((element) => element.source == source && element.destination == destination); bool hasSuccessor(Node? node) => successorsOf(node).isNotEmpty; List successorsOf(Node? node) { if (node == null) return []; if (!_cacheValid) _buildCache(); return _successorCache[node] ?? []; } bool hasPredecessor(Node node) => predecessorsOf(node).isNotEmpty; List predecessorsOf(Node? node) { if (node == null) return []; if (!_cacheValid) _buildCache(); return _predecessorCache[node] ?? []; } void _buildCache() { _successorCache.clear(); _predecessorCache.clear(); for (var node in _nodes) { _successorCache[node] = []; _predecessorCache[node] = []; } for (var edge in _edges) { _successorCache[edge.source]!.add(edge.destination); _predecessorCache[edge.destination]!.add(edge.source); } _cacheValid = true; } bool contains({Node? node, Edge? edge}) => node != null && _nodes.contains(node) || edge != null && _edges.contains(edge); bool containsData(data) => _nodes.any((element) => element.data == data); Node getNodeAtPosition(int position) { if (position < 0) { // throw IllegalArgumentException("position can't be negative") } final size = _nodes.length; if (position >= size) { // throw IndexOutOfBoundsException("Position: $position, Size: $size") } return _nodes[position]; } @Deprecated('Please use the builder and id mechanism to build the widgets') Node getNodeAtUsingData(Widget data) => _nodes.firstWhere((element) => element.data == data); Node getNodeUsingKey(ValueKey key) => _nodes.firstWhere((element) => element.key == key); Node getNodeUsingId(dynamic id) => _nodes.firstWhere((element) => element.key == ValueKey(id)); List getOutEdges(Node node) => _edges.where((element) => element.source == node).toList(); List getInEdges(Node node) => _edges.where((element) => element.destination == node).toList(); void notifyGraphObserver() => graphObserver.forEach((element) { element.notifyGraphInvalidated(); }); String toJson() { var jsonString = { 'nodes': [..._nodes.map((e) => e.hashCode.toString())], 'edges': [ ..._edges.map((e) => { 'from': e.source.hashCode.toString(), 'to': e.destination.hashCode.toString() }) ] }; return json.encode(jsonString); } } extension GraphExtension on Graph { Rect calculateGraphBounds() { var minX = double.infinity; var minY = double.infinity; var maxX = double.negativeInfinity; var maxY = double.negativeInfinity; for (final node in nodes) { minX = min(minX, node.x); minY = min(minY, node.y); maxX = max(maxX, node.x + node.width); maxY = max(maxY, node.y + node.height); } return Rect.fromLTRB(minX, minY, maxX, maxY); } Size calculateGraphSize() { final bounds = calculateGraphBounds(); return bounds.size; } } enum LineType { Default, DottedLine, DashedLine, SineLine, } class Node { ValueKey? key; @Deprecated('Please use the builder and id mechanism to build the widgets') Widget? data; @Deprecated('Please use the Node.Id') Node(this.data, {Key? key}) { this.key = ValueKey(key?.hashCode ?? data.hashCode); } Node.Id(dynamic id) { key = ValueKey(id); } Size size = Size(0, 0); Offset position = Offset(0, 0); LineType lineType = LineType.Default; double get height => size.height; double get width => size.width; double get x => position.dx; double get y => position.dy; set y(double value) { position = Offset(position.dx, value); } set x(double value) { position = Offset(value, position.dy); } @override bool operator ==(Object other) => identical(this, other) || other is Node && hashCode == other.hashCode; @override int get hashCode { return key?.value.hashCode ?? key.hashCode; } @override String toString() { return 'Node{position: $position, key: $key, _size: $size, lineType: $lineType}'; } } class Edge { Node source; Node destination; Key? key; Paint? paint; Edge(this.source, this.destination, {this.key, this.paint}); @override bool operator ==(Object? other) => identical(this, other) || other is Edge && hashCode == other.hashCode; @override int get hashCode => key?.hashCode ?? Object.hash(source, destination); } abstract class GraphObserver { void notifyGraphInvalidated(); } ================================================ FILE: lib/GraphView.dart ================================================ library graphview; import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; part 'Algorithm.dart'; part 'Graph.dart'; part 'edgerenderer/ArrowEdgeRenderer.dart'; part 'edgerenderer/EdgeRenderer.dart'; part 'forcedirected/FruchtermanReingoldAlgorithm.dart'; part 'forcedirected/FruchtermanReingoldConfiguration.dart'; part 'layered/EiglspergerAlgorithm.dart'; part 'layered/SugiyamaAlgorithm.dart'; part 'layered/SugiyamaConfiguration.dart'; part 'layered/SugiyamaEdgeData.dart'; part 'layered/SugiyamaEdgeRenderer.dart'; part 'layered/SugiyamaNodeData.dart'; part 'mindmap/MindMapAlgorithm.dart'; part 'mindmap/MindmapEdgeRenderer.dart'; part 'tree/BaloonLayoutAlgorithm.dart'; part 'tree/BuchheimWalkerAlgorithm.dart'; part 'tree/BuchheimWalkerConfiguration.dart'; part 'tree/BuchheimWalkerNodeData.dart'; part 'tree/CircleLayoutAlgorithm.dart'; part 'tree/RadialTreeLayoutAlgorithm.dart'; part 'tree/TidierTreeLayoutAlgorithm.dart'; part 'tree/TreeEdgeRenderer.dart'; typedef NodeWidgetBuilder = Widget Function(Node node); typedef EdgeWidgetBuilder = Widget Function(Edge edge); class GraphViewController { _GraphViewState? _state; final TransformationController? transformationController; final Map collapsedNodes = {}; final Map expandingNodes = {}; final Map hiddenBy = {}; Node? collapsedNode; Node? focusedNode; GraphViewController({ this.transformationController, }); void _attach(_GraphViewState? state) => _state = state; void _detach() => _state = null; void animateToNode(ValueKey key) => _state?.jumpToNodeUsingKey(key, true); void jumpToNode(ValueKey key) => _state?.jumpToNodeUsingKey(key, false); void animateToMatrix(Matrix4 target) => _state?.animateToMatrix(target); void resetView() => _state?.resetView(); void zoomToFit() => _state?.zoomToFit(); void forceRecalculation() => _state?.forceRecalculation(); // Visibility management methods bool isNodeCollapsed(Node node) => collapsedNodes.containsKey(node); bool isNodeHidden(Node node) => hiddenBy.containsKey(node); bool isNodeVisible(Graph graph, Node node) { return !hiddenBy.containsKey(node); } Node? findClosestVisibleAncestor(Graph graph, Node node) { var current = graph.predecessorsOf(node).firstOrNull; // Walk up until we find a visible ancestor while (current != null) { if (isNodeVisible(graph, current)) { return current; // Return the first (closest) visible ancestor } current = graph.predecessorsOf(current).firstOrNull; } return null; } void _markDescendantsHiddenBy( Graph graph, Node collapsedNode, Node currentNode) { for (final child in graph.successorsOf(currentNode)) { // Only mark as hidden if: // 1. Not already hidden, OR // 2. Was hidden by a node that's no longer collapsed if (!hiddenBy.containsKey(child) || !collapsedNodes.containsKey(hiddenBy[child])) { hiddenBy[child] = collapsedNode; } // Recurse only if this child isn't itself a collapsed node if (!collapsedNodes.containsKey(child)) { _markDescendantsHiddenBy(graph, collapsedNode, child); } } } void _markExpandingDescendants(Graph graph, Node node) { for (final child in graph.successorsOf(node)) { expandingNodes[child] = true; if (!collapsedNodes.containsKey(child)) { _markExpandingDescendants(graph, child); } } } void expandNode(Graph graph, Node node, {animate = false}) { collapsedNodes.remove(node); hiddenBy.removeWhere((hiddenNode, hiddenBy) => hiddenBy == node); expandingNodes.clear(); _markExpandingDescendants(graph, node); if (animate) { focusedNode = node; } forceRecalculation(); } void collapseNode(Graph graph, Node node, {animate = false}) { if (graph.hasSuccessor(node)) { collapsedNodes[node] = true; collapsedNode = node; if (animate) { focusedNode = node; } _markDescendantsHiddenBy(graph, node, node); forceRecalculation(); } expandingNodes.clear(); } void toggleNodeExpanded(Graph graph, Node node, {animate = false}) { if (isNodeCollapsed(node)) { expandNode(graph, node, animate: animate); } else { collapseNode(graph, node, animate: animate); } } List getCollapsingEdges(Graph graph) { if (collapsedNode == null) return []; return graph.edges.where((edge) { return hiddenBy[edge.destination] == collapsedNode; }).toList(); } List getExpandingEdges(Graph graph) { final expandingEdges = []; for (final node in expandingNodes.keys) { // Get all incoming edges to expanding nodes for (final edge in graph.getInEdges(node)) { expandingEdges.add(edge); } } return expandingEdges; } // Additional convenience methods for setting initial state void setInitiallyCollapsedNodes(Graph graph, List nodes) { for (final node in nodes) { collapsedNodes[node] = true; // Mark descendants as hidden by this node _markDescendantsHiddenBy(graph, node, node); } } void setInitiallyCollapsedByKeys(Graph graph, Set keys) { for (final key in keys) { try { final node = graph.getNodeUsingKey(key); collapsedNodes[node] = true; // Mark descendants as hidden by this node _markDescendantsHiddenBy(graph, node, node); } catch (e) { // Node with key not found, ignore } } } bool isNodeExpanding(Node node) => expandingNodes.containsKey(node); void removeCollapsingNodes() { collapsedNode = null; } void jumpToFocusedNode() { if (focusedNode != null) { final nodeCenter = Offset( focusedNode!.position.dx + focusedNode!.width / 2, focusedNode!.position.dy + focusedNode!.height / 2, ); _state?.jumpToOffset(nodeCenter, true); focusedNode = null; } } } class GraphChildDelegate { final Graph graph; final Algorithm algorithm; final NodeWidgetBuilder builder; GraphViewController? controller; final bool centerGraph; Graph? _cachedVisibleGraph; bool _needsRecalculation = true; GraphChildDelegate({ required this.graph, required this.algorithm, required this.builder, required this.controller, this.centerGraph = false, }); Graph getVisibleGraph() { if (_cachedVisibleGraph != null && !_needsRecalculation) { return _cachedVisibleGraph!; } final visibleGraph = getVisibleGraphOnly(); final collapsingEdges = controller?.getCollapsingEdges(graph) ?? []; visibleGraph.addEdges(collapsingEdges); _cachedVisibleGraph = visibleGraph; _needsRecalculation = false; return visibleGraph; } Graph getVisibleGraphOnly() { final visibleGraph = Graph(); for (final edge in graph.edges) { if (isNodeVisible(edge.source) && isNodeVisible(edge.destination)) { visibleGraph.addEdgeS(edge); } } if (visibleGraph.nodes.isEmpty && graph.nodes.isNotEmpty) { visibleGraph.addNode(graph.nodes.first); } return visibleGraph; } Widget? build(Node node) { var child = node.data ?? builder(node); return KeyedSubtree(key: node.key, child: child); } bool shouldRebuild(GraphChildDelegate oldDelegate) { final result = graph != oldDelegate.graph || algorithm != oldDelegate.algorithm; if (result) _needsRecalculation = true; return result; } Size runAlgorithm() { final visibleGraph = getVisibleGraphOnly(); if (centerGraph) { // Use large viewport and center the graph var viewPortSize = Size(200000, 200000); var centerX = viewPortSize.width / 2; var centerY = viewPortSize.height / 2; algorithm.run(visibleGraph, centerX, centerY); return viewPortSize; } else { // Use default algorithm behavior return algorithm.run(visibleGraph, 0, 0); } } bool isNodeVisible(Node node) { return controller?.isNodeVisible(graph, node) ?? true; } Node? findClosestVisibleAncestor(Node node) { return controller?.findClosestVisibleAncestor(graph, node); } } class GraphView extends StatefulWidget { final Graph graph; final Algorithm algorithm; final Paint? paint; final NodeWidgetBuilder builder; final bool animated; final GraphViewController? controller; final bool _isBuilder; Duration? panAnimationDuration; Duration? toggleAnimationDuration; ValueKey? initialNode; bool autoZoomToFit = false; late GraphChildDelegate delegate; final bool centerGraph; final double horizontalBias; final double verticalBias; GraphView({ Key? key, required this.graph, required this.algorithm, this.paint, required this.builder, this.animated = true, this.controller, this.toggleAnimationDuration, this.centerGraph = false, this.horizontalBias = 0.5, this.verticalBias = 0.5, }) : _isBuilder = false, delegate = GraphChildDelegate( graph: graph, algorithm: algorithm, builder: builder, controller: null), super(key: key); GraphView.builder({ Key? key, required this.graph, required this.algorithm, this.paint, required this.builder, this.controller, this.animated = true, this.initialNode, this.autoZoomToFit = false, this.panAnimationDuration, this.toggleAnimationDuration, this.centerGraph = false, this.horizontalBias = 0.5, this.verticalBias = 0.5, }) : _isBuilder = true, delegate = GraphChildDelegate( graph: graph, algorithm: algorithm, builder: builder, controller: controller, centerGraph: centerGraph), assert(!(autoZoomToFit && initialNode != null), 'Cannot use both autoZoomToFit and initialNode together. Choose one.'), super(key: key); @override _GraphViewState createState() => _GraphViewState(); } class _GraphViewState extends State with TickerProviderStateMixin { late TransformationController _transformationController; late final AnimationController _panController; late final AnimationController _nodeController; Animation? _panAnimation; @override void initState() { super.initState(); _transformationController = widget.controller?.transformationController ?? TransformationController(); _panController = AnimationController( vsync: this, duration: widget.panAnimationDuration ?? const Duration(milliseconds: 600), ); _nodeController = AnimationController( vsync: this, duration: widget.toggleAnimationDuration ?? const Duration(milliseconds: 600), ); widget.controller?._attach(this); if (widget.autoZoomToFit || widget.initialNode != null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.autoZoomToFit) { zoomToFit(); } else if (widget.initialNode != null) { jumpToNodeUsingKey(widget.initialNode!, false); } }); } } @override void dispose() { widget.controller?._detach(); _panController.dispose(); _nodeController.dispose(); _transformationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final view = GraphViewWidget( paint: widget.paint, nodeAnimationController: _nodeController, enableAnimation: widget.animated, delegate: widget.delegate, ); if (widget._isBuilder) { return InteractiveViewer.builder( transformationController: _transformationController, boundaryMargin: EdgeInsets.all(double.infinity), minScale: 0.01, maxScale: 10, builder: (context, viewport) { return view; }); } return view; } void jumpToNodeUsingKey(ValueKey key, bool animated) { final node = widget.graph.nodes.firstWhereOrNull((n) => n.key == key); if (node == null) return; jumpToNode(node, animated); } void jumpToNode(Node node, bool animated) { final nodeCenter = Offset( node.position.dx + node.width / 2, node.position.dy + node.height / 2); jumpToOffset(nodeCenter, animated); } void jumpToOffset(Offset offset, bool animated) { final renderBox = context.findRenderObject() as RenderBox?; if (renderBox == null) return; final viewport = renderBox.size; final center = Offset( viewport.width * widget.horizontalBias, viewport.height * widget.verticalBias); final currentScale = _transformationController.value.getMaxScaleOnAxis(); final scaledNodeCenter = offset * currentScale; final translation = center - scaledNodeCenter; final target = Matrix4.identity() ..translate(translation.dx, translation.dy) ..scale(currentScale); if (animated) { animateToMatrix(target); } else { _transformationController.value = target; } } void resetView() => animateToMatrix(Matrix4.identity()); void zoomToFit() { var graph = widget.delegate.getVisibleGraphOnly(); final renderBox = context.findRenderObject() as RenderBox?; if (renderBox == null) return; final vp = renderBox.size; final bounds = graph.calculateGraphBounds(); const paddingFactor = 0.95; final scaleX = (vp.width / bounds.width) * paddingFactor; final scaleY = (vp.height / bounds.height) * paddingFactor; final scale = min(scaleX, scaleY); final scaledWidth = bounds.width * scale; final scaledHeight = bounds.height * scale; final centerOffset = Offset( (vp.width - scaledWidth) * widget.horizontalBias - bounds.left * scale, (vp.height - scaledHeight) * widget.verticalBias - bounds.top * scale); final target = Matrix4.identity() ..translate(centerOffset.dx, centerOffset.dy) ..scale(scale); animateToMatrix(target); } void animateToMatrix(Matrix4 target) { _panController.reset(); _panAnimation = Matrix4Tween( begin: _transformationController.value, end: target) .animate( CurvedAnimation(parent: _panController, curve: Curves.linear)); _panAnimation!.addListener(_onPanTick); _panController.forward(); } void _onPanTick() { if (_panAnimation == null) return; _transformationController.value = _panAnimation!.value; if (!_panController.isAnimating) { _panAnimation!.removeListener(_onPanTick); _panAnimation = null; _panController.reset(); } } void forceRecalculation() { // Invalidate the delegate's cached graph widget.delegate._needsRecalculation = true; setState(() {}); } } abstract class GraphChildManager { void startLayout(); void buildChild(Node node); void reuseChild(Node node); void endLayout(); } class GraphViewWidget extends RenderObjectWidget { final GraphChildDelegate delegate; final Paint? paint; final AnimationController nodeAnimationController; final bool enableAnimation; const GraphViewWidget({ Key? key, required this.delegate, this.paint, required this.nodeAnimationController, required this.enableAnimation, }) : super(key: key); @override GraphViewElement createElement() => GraphViewElement(this); @override RenderCustomLayoutBox createRenderObject(BuildContext context) { return RenderCustomLayoutBox( delegate, paint, enableAnimation, nodeAnimationController: nodeAnimationController, childManager: context as GraphChildManager, ); } @override void updateRenderObject( BuildContext context, RenderCustomLayoutBox renderObject) { renderObject ..delegate = delegate ..edgePaint = paint ..nodeAnimationController = nodeAnimationController ..enableAnimation = enableAnimation; } } class GraphViewElement extends RenderObjectElement implements GraphChildManager { GraphViewElement(GraphViewWidget super.widget); @override GraphViewWidget get widget => super.widget as GraphViewWidget; @override RenderCustomLayoutBox get renderObject => super.renderObject as RenderCustomLayoutBox; // Contains all children, including those that are keyed Map _nodeToElement = {}; Map _keyToElement = {}; // Used between startLayout() & endLayout() to compute the new values Map? _newNodeToElement; Map? _newKeyToElement; bool get _debugIsDoingLayout => _newNodeToElement != null && _newKeyToElement != null; @override void performRebuild() { super.performRebuild(); // Children list is updated during layout since we only know during layout // which children will be visible renderObject.markNeedsLayout(); } @override void forgetChild(Element child) { assert(!_debugIsDoingLayout); super.forgetChild(child); _nodeToElement.remove(child.slot as Node); if (child.widget.key != null) { _keyToElement.remove(child.widget.key); } } @override void insertRenderObjectChild(RenderBox child, Node slot) { renderObject._insertChild(child, slot); } @override void moveRenderObjectChild(RenderBox child, Node oldSlot, Node newSlot) { renderObject._moveChild(child, from: oldSlot, to: newSlot); } @override void removeRenderObjectChild(RenderBox child, Node slot) { renderObject._removeChild(child, slot); } @override void visitChildren(ElementVisitor visitor) { _nodeToElement.values.forEach(visitor); } // ---- GraphChildManager implementation ---- @override void startLayout() { assert(!_debugIsDoingLayout); _newNodeToElement = {}; _newKeyToElement = {}; } @override void buildChild(Node node) { assert(_debugIsDoingLayout); owner!.buildScope(this, () { final newWidget = widget.delegate.build(node); if (newWidget == null) { return; } final oldElement = _retrieveOldElement(newWidget, node); final newChild = updateChild(oldElement, newWidget, node); if (newChild != null) { // Ensure we are not overwriting an existing child assert(_newNodeToElement![node] == null); _newNodeToElement![node] = newChild; if (newWidget.key != null) { // Ensure we are not overwriting an existing key assert(_newKeyToElement![newWidget.key!] == null); _newKeyToElement![newWidget.key!] = newChild; } } }); } @override void reuseChild(Node node) { assert(_debugIsDoingLayout); final elementToReuse = _nodeToElement.remove(node); assert( elementToReuse != null, 'Expected to re-use an element at $node, but none was found.', ); _newNodeToElement![node] = elementToReuse!; if (elementToReuse.widget.key != null) { assert(_keyToElement.containsKey(elementToReuse.widget.key)); assert(_keyToElement[elementToReuse.widget.key] == elementToReuse); _newKeyToElement![elementToReuse.widget.key!] = _keyToElement.remove(elementToReuse.widget.key)!; } } Element? _retrieveOldElement(Widget newWidget, Node node) { if (newWidget.key != null) { final result = _keyToElement.remove(newWidget.key); if (result != null) { _nodeToElement.remove(result.slot as Node); } return result; } final potentialOldElement = _nodeToElement[node]; if (potentialOldElement != null && potentialOldElement.widget.key == null) { return _nodeToElement.remove(node); } return null; } @override void endLayout() { assert(_debugIsDoingLayout); // Unmount all elements that have not been reused in the layout cycle for (final element in _nodeToElement.values) { if (element.widget.key == null) { // If it has a key, we handle it below updateChild(element, null, null); } else { assert(_keyToElement.containsValue(element)); } } for (final element in _keyToElement.values) { assert(element.widget.key != null); updateChild(element, null, null); } _nodeToElement = _newNodeToElement!; _keyToElement = _newKeyToElement!; _newNodeToElement = null; _newKeyToElement = null; assert(!_debugIsDoingLayout); centerNodeWhileToggling(); } void centerNodeWhileToggling() { widget.delegate.controller?.jumpToFocusedNode(); } } class RenderCustomLayoutBox extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { late Paint _paint; late AnimationController _nodeAnimationController; late GraphChildDelegate _delegate; GraphChildManager? childManager; Size? _cachedSize; bool _isInitialized = false; bool _needsFullRecalculation = false; late bool enableAnimation; final opacityPaint = Paint(); final animatedPositions = {}; final _children = {}; final _activeChildrenForLayoutPass = {}; RenderCustomLayoutBox( GraphChildDelegate delegate, Paint? paint, bool enableAnimation, { required AnimationController nodeAnimationController, this.childManager, }) { _nodeAnimationController = nodeAnimationController; _delegate = delegate; edgePaint = paint; this.enableAnimation = enableAnimation; } RenderBox? buildOrObtainChildFor(Node node) { assert(debugDoingThisLayout); if (_needsFullRecalculation || !_children.containsKey(node)) { invokeLayoutCallback((BoxConstraints _) { childManager!.buildChild(node); }); } else { childManager!.reuseChild(node); } if (!_children.containsKey(node)) { // There is no child for this node, the delegate may not provide one return null; } assert(_children.containsKey(node)); final child = _children[node]!; _activeChildrenForLayoutPass[node] = child; return child; } GraphChildDelegate get delegate => _delegate; Graph get graph => _delegate.getVisibleGraph(); Algorithm get algorithm => _delegate.algorithm; set delegate(GraphChildDelegate value) { // if (value != _delegate) { _needsFullRecalculation = true; _isInitialized = false; _delegate = value; markNeedsLayout(); // } } void markNeedsRecalculation() { _needsFullRecalculation = false; _isInitialized = false; markNeedsLayout(); } @override void attach(PipelineOwner owner) { super.attach(owner); _nodeAnimationController.addListener(_onAnimationTick); for (final child in _children.values) { child.attach(owner); } } @override void detach() { _nodeAnimationController.removeListener(_onAnimationTick); super.detach(); for (final child in _children.values) { child.detach(); } } void forceRecalculation() { _needsFullRecalculation = true; _isInitialized = false; markNeedsLayout(); } Paint get edgePaint => _paint; set edgePaint(Paint? value) { final newPaint = value ?? (Paint() ..color = Colors.black ..strokeWidth = 3) ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.butt; _paint = newPaint; markNeedsPaint(); } AnimationController get nodeAnimationController => _nodeAnimationController; set nodeAnimationController(AnimationController value) { if (identical(_nodeAnimationController, value)) return; _nodeAnimationController.removeListener(_onAnimationTick); _nodeAnimationController = value; _nodeAnimationController.addListener(_onAnimationTick); markNeedsLayout(); } void _onAnimationTick() { markNeedsPaint(); } @override void paint(PaintingContext context, Offset offset) { if (_children.isEmpty) return; if (enableAnimation) { final t = _nodeAnimationController.value; animatedPositions.clear(); for (final entry in _children.entries) { final node = entry.key; final child = entry.value; final nodeData = child.parentData as NodeBoxData; final pos = Offset.lerp(nodeData.startOffset, nodeData.targetOffset, t)!; animatedPositions[node] = pos; } context.canvas.save(); context.canvas.translate(offset.dx, offset.dy); algorithm.renderer?.setAnimatedPositions(animatedPositions); final collapsingEdges = _delegate.controller?.getCollapsingEdges(graph).toSet() ?? {}; final expandingEdges = _delegate.controller?.getExpandingEdges(graph).toSet() ?? {}; for (final edge in graph.edges) { var edgePaintWithOpacity = Paint.from(edge.paint ?? edgePaint); // Apply fade effect for collapsing edges (fade out) if (collapsingEdges.contains(edge)) { edgePaintWithOpacity.color = edgePaint.color.withValues(alpha: 1.0 - t); } // Apply fade effect for expanding edges (fade in) else if (expandingEdges.contains(edge)) { edgePaintWithOpacity.color = edgePaint.color.withValues(alpha: t); } algorithm.renderer?.renderEdge( context.canvas, edge, edgePaintWithOpacity, ); } context.canvas.restore(); _paintNodes(context, offset, t); } else { context.canvas.save(); context.canvas.translate(offset.dx, offset.dy); graph.edges.forEach((edge) { algorithm.renderer?.renderEdge(context.canvas, edge, edgePaint); }); context.canvas.restore(); for (final entry in _children.entries) { final node = entry.key; final child = entry.value; if (_delegate.isNodeVisible(node)) { context.paintChild(child, offset + node.position); } } } } @override void performLayout() { _activeChildrenForLayoutPass.clear(); childManager!.startLayout(); final looseConstraints = BoxConstraints.loose(constraints.biggest); if (_needsFullRecalculation || !_isInitialized) { _layoutNodesLazily(looseConstraints); _cachedSize = _delegate.runAlgorithm(); _isInitialized = true; _needsFullRecalculation = false; } size = _cachedSize ?? Size.zero; invokeLayoutCallback((BoxConstraints _) { childManager!.endLayout(); }); if (enableAnimation) { _updateAnimationStates(); } else { _updateNodePositions(); } } void _paintNodes(PaintingContext context, Offset offset, double t) { for (final entry in _children.entries) { final node = entry.key; final child = entry.value; final nodeData = child.parentData as NodeBoxData; final pos = animatedPositions[node]!; final isVisible = _delegate.isNodeVisible(node); if (isVisible) { final isExpanding = _delegate.controller?.isNodeExpanding(node) ?? false; if (_nodeAnimationController.isAnimating && isExpanding) { _paintExpandingNode(context, child, offset, pos, t); } else { context.paintChild(child, offset + pos); } } else { if (_nodeAnimationController.isAnimating && nodeData.startOffset != nodeData.targetOffset) { _paintCollapsingNode(context, child, offset, pos, t); } else if (_nodeAnimationController.isCompleted) { nodeData.startOffset = nodeData.targetOffset; } } if (_nodeAnimationController.isCompleted) { nodeData.offset = node.position; } } if (_nodeAnimationController.isCompleted) { _delegate.controller?.removeCollapsingNodes(); } } void _paintExpandingNode(PaintingContext context, RenderBox child, Offset offset, Offset pos, double t) { final center = pos + offset + Offset(child.size.width * 0.5, child.size.height * 0.5); context.canvas.save(); // Apply scaling from center context.canvas.translate(center.dx, center.dy); context.canvas.scale(t, t); context.canvas.translate(-center.dx, -center.dy); // Paint with opacity using saveLayer opacityPaint ..color = Color.fromRGBO(255, 255, 255, t) ..colorFilter = ColorFilter.mode( Colors.white.withValues(alpha: t), BlendMode.modulate); context.canvas.saveLayer( Rect.fromLTWH(pos.dx + offset.dx - 20, pos.dy + offset.dy - 20, child.size.width + 40, child.size.height + 40), opacityPaint); context.paintChild(child, offset + pos); context.canvas.restore(); // Restore saveLayer context.canvas.restore(); // Restore main save } void _paintCollapsingNode(PaintingContext context, RenderBox child, Offset offset, Offset pos, double t) { final progress = (1.0 - t); final center = pos + offset + Offset(child.size.width * 0.5, child.size.height * 0.5); context.canvas.save(); // Apply scaling from center context.canvas.translate(center.dx, center.dy); context.canvas.scale(progress, progress); context.canvas.translate(-center.dx, -center.dy); // Paint with opacity using saveLayer opacityPaint ..color = Color.fromRGBO(255, 255, 255, progress) ..colorFilter = ColorFilter.mode( Colors.white.withValues(alpha: progress), BlendMode.modulate); context.canvas.saveLayer( Rect.fromLTWH(pos.dx + offset.dx - 20, pos.dy + offset.dy - 20, child.size.width + 40, child.size.height + 40), opacityPaint); context.paintChild(child, offset + pos); context.canvas.restore(); // Restore saveLayer context.canvas.restore(); // Restore main save } void _updateNodePositions() { for (final entry in _children.entries) { final node = entry.key; final child = entry.value; final nodeData = child.parentData as NodeBoxData; if (_delegate.isNodeVisible(node)) { nodeData.offset = node.position; } else { final parent = delegate.findClosestVisibleAncestor(node); nodeData.offset = parent?.position ?? node.position; } } } void _layoutNodesLazily(BoxConstraints constraints) { for (final node in graph.nodes) { final child = buildOrObtainChildFor(node); if (child != null) { child.layout(constraints, parentUsesSize: true); node.size = Size(child.size.width.ceilToDouble(), child.size.height); } } } void _updateAnimationStates() { for (final entry in _children.entries) { final node = entry.key; final child = entry.value; final nodeData = child.parentData as NodeBoxData; final isVisible = _delegate.isNodeVisible(node); if (isVisible) { _updateVisibleNodeAnimation(nodeData, node); } else { _updateCollapsedNodeAnimation(nodeData, node); } } _nodeAnimationController.reset(); _nodeAnimationController.forward(); } void _updateVisibleNodeAnimation(NodeBoxData nodeData, Node graphNode) { final prevTarget = nodeData.targetOffset; var newPos = graphNode.position; if (prevTarget == null) { final parent = graph.predecessorsOf(graphNode).firstOrNull; final pastParentPosition = animatedPositions[parent]; nodeData.startOffset = pastParentPosition ?? parent?.position ?? newPos; nodeData.targetOffset = newPos; } else if (prevTarget != newPos) { nodeData.startOffset = prevTarget; nodeData.targetOffset = newPos; } else { nodeData.startOffset = newPos; nodeData.targetOffset = newPos; } } void _updateCollapsedNodeAnimation(NodeBoxData nodeData, Node graphNode) { final parent = delegate.findClosestVisibleAncestor(graphNode); final parentPos = parent?.position ?? Offset.zero; final prevTarget = nodeData.targetOffset; if (nodeData.startOffset == nodeData.targetOffset) { nodeData.targetOffset = parentPos; } else if (prevTarget != null && prevTarget != parentPos) { // Just collapsed now → animate toward parent nodeData.startOffset = graphNode.position; nodeData.targetOffset = parentPos; } else { // animation finished → lock to parent nodeData.startOffset = parentPos; nodeData.targetOffset = parentPos; } } @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { if (enableAnimation && !_nodeAnimationController.isCompleted) return false; for (final entry in _children.entries) { final node = entry.key; if (delegate.isNodeVisible(node)) { final child = entry.value; final childParentData = child.parentData as BoxParentData; final isHit = result.addWithPaintOffset( offset: childParentData.offset, position: position, hitTest: (BoxHitTestResult result, Offset transformed) { return child.hitTest(result, position: transformed); }, ); if (isHit) return true; } } return false; } @override void setupParentData(RenderBox child) { if (child.parentData is! NodeBoxData) { child.parentData = NodeBoxData(); } } // ---- Called from GraphViewElement ---- void _insertChild(RenderBox child, Node slot) { _children[slot] = child; adoptChild(child); } void _moveChild(RenderBox child, {required Node from, required Node to}) { if (_children[from] == child) { _children.remove(from); } _children[to] = child; } void _removeChild(RenderBox child, Node slot) { if (_children[slot] == child) { _children.remove(slot); } dropChild(child); } @override void visitChildren(RenderObjectVisitor visitor) { for (final child in _children.values) { visitor(child); } } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('graph', graph)); properties.add(DiagnosticsProperty('algorithm', algorithm)); properties.add(DiagnosticsProperty('paint', edgePaint)); } } class NodeBoxData extends ContainerBoxParentData { Offset? startOffset; Offset? targetOffset; } class GraphViewCustomPainter extends StatefulWidget { final Graph graph; final FruchtermanReingoldAlgorithm algorithm; final Paint? paint; final NodeWidgetBuilder builder; final stepMilis = 25; GraphViewCustomPainter({ Key? key, required this.graph, required this.algorithm, this.paint, required this.builder, }) : super(key: key); @override _GraphViewCustomPainterState createState() => _GraphViewCustomPainterState(); } class _GraphViewCustomPainterState extends State { late Timer timer; late Graph graph; late FruchtermanReingoldAlgorithm algorithm; @override void initState() { graph = widget.graph; algorithm = widget.algorithm; algorithm.init(graph); startTimer(); super.initState(); } void startTimer() { timer = Timer.periodic(Duration(milliseconds: widget.stepMilis), (timer) { algorithm.step(graph); update(); }); } @override void dispose() { timer.cancel(); super.dispose(); } @override Widget build(BuildContext context) { algorithm.setDimensions( MediaQuery.of(context).size.width, MediaQuery.of(context).size.height); return Stack( clipBehavior: Clip.none, children: [ CustomPaint( size: MediaQuery.of(context).size, painter: EdgeRender(algorithm, graph, Offset(20, 20), widget.paint), ), ...List.generate(graph.nodeCount(), (index) { return Positioned( child: GestureDetector( child: graph.nodes[index].data ?? widget.builder(graph.nodes[index]), onPanUpdate: (details) { graph.getNodeAtPosition(index).position += details.delta; update(); }, ), top: graph.getNodeAtPosition(index).position.dy, left: graph.getNodeAtPosition(index).position.dx, ); }), ], ); } Future update() async { setState(() {}); } } class EdgeRender extends CustomPainter { Algorithm algorithm; Graph graph; Offset offset; Paint? customPaint; EdgeRender(this.algorithm, this.graph, this.offset, this.customPaint); @override void paint(Canvas canvas, Size size) { var edgePaint = customPaint ?? (Paint() ..color = Colors.black ..strokeWidth = 3 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.butt); canvas.save(); canvas.translate(offset.dx, offset.dy); for (var value in graph.edges) { algorithm.renderer?.renderEdge(canvas, value, edgePaint); } canvas.restore(); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } ================================================ FILE: lib/edgerenderer/ArrowEdgeRenderer.dart ================================================ part of graphview; const double ARROW_DEGREES = 0.5; const double ARROW_LENGTH = 10; class ArrowEdgeRenderer extends EdgeRenderer { var trianglePath = Path(); final bool noArrow; ArrowEdgeRenderer({this.noArrow = false}); Offset _getNodeCenter(Node node) { final nodePosition = getNodePosition(node); return Offset( nodePosition.dx + node.width * 0.5, nodePosition.dy + node.height * 0.5, ); } void render(Canvas canvas, Graph graph, Paint paint) { graph.edges.forEach((edge) { renderEdge(canvas, edge, paint); }); } @override void renderEdge(Canvas canvas, Edge edge, Paint paint) { var source = edge.source; var destination = edge.destination; final currentPaint = (edge.paint ?? paint)..style = PaintingStyle.stroke; final lineType = _getLineType(destination); if (source == destination) { final loopResult = buildSelfLoopPath( edge, arrowLength: noArrow ? 0.0 : ARROW_LENGTH, ); if (loopResult != null) { drawStyledPath(canvas, loopResult.path, currentPaint, lineType: lineType); if (!noArrow) { final trianglePaint = Paint() ..color = edge.paint?.color ?? paint.color ..style = PaintingStyle.fill; final triangleCentroid = drawTriangle( canvas, trianglePaint, loopResult.arrowBase.dx, loopResult.arrowBase.dy, loopResult.arrowTip.dx, loopResult.arrowTip.dy, ); drawStyledLine( canvas, loopResult.arrowBase, triangleCentroid, currentPaint, lineType: lineType, ); } return; } } var sourceOffset = getNodePosition(source); var destinationOffset = getNodePosition(destination); var startX = sourceOffset.dx + source.width * 0.5; var startY = sourceOffset.dy + source.height * 0.5; var stopX = destinationOffset.dx + destination.width * 0.5; var stopY = destinationOffset.dy + destination.height * 0.5; var clippedLine = clipLineEnd( startX, startY, stopX, stopY, destinationOffset.dx, destinationOffset.dy, destination.width, destination.height); if (noArrow) { // Draw line without arrow, respecting line type drawStyledLine( canvas, Offset(clippedLine[0], clippedLine[1]), Offset(clippedLine[2], clippedLine[3]), currentPaint, lineType: lineType, ); } else { var trianglePaint = Paint() ..color = paint.color ..style = PaintingStyle.fill; // Draw line with arrow Paint? edgeTrianglePaint; if (edge.paint != null) { edgeTrianglePaint = Paint() ..color = edge.paint?.color ?? paint.color ..style = PaintingStyle.fill; } var triangleCentroid = drawTriangle( canvas, edgeTrianglePaint ?? trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]); // Draw the line with the appropriate style drawStyledLine( canvas, Offset(clippedLine[0], clippedLine[1]), triangleCentroid, currentPaint, lineType: lineType, ); } } /// Helper to get line type from node data if available LineType? _getLineType(Node node) { // This assumes you have a way to access node data // You may need to adjust this based on your actual implementation if (node is SugiyamaNodeData) { return node.lineType; } return null; } Offset drawTriangle(Canvas canvas, Paint paint, double lineStartX, double lineStartY, double arrowTipX, double arrowTipY) { // Calculate direction from line start to arrow tip, then flip 180° to point backwards from tip var lineDirection = (atan2(arrowTipY - lineStartY, arrowTipX - lineStartX) + pi); // Calculate the two base points of the arrowhead triangle var leftWingX = (arrowTipX + ARROW_LENGTH * cos((lineDirection - ARROW_DEGREES))); var leftWingY = (arrowTipY + ARROW_LENGTH * sin((lineDirection - ARROW_DEGREES))); var rightWingX = (arrowTipX + ARROW_LENGTH * cos((lineDirection + ARROW_DEGREES))); var rightWingY = (arrowTipY + ARROW_LENGTH * sin((lineDirection + ARROW_DEGREES))); // Draw the triangle: tip -> left wing -> right wing -> back to tip trianglePath.moveTo(arrowTipX, arrowTipY); // Arrow tip trianglePath.lineTo(leftWingX, leftWingY); // Left wing trianglePath.lineTo(rightWingX, rightWingY); // Right wing trianglePath.close(); // Back to tip canvas.drawPath(trianglePath, paint); // Calculate center point of the triangle var triangleCenterX = (arrowTipX + leftWingX + rightWingX) / 3; var triangleCenterY = (arrowTipY + leftWingY + rightWingY) / 3; trianglePath.reset(); return Offset(triangleCenterX, triangleCenterY); } List clipLineEnd( double startX, double startY, double stopX, double stopY, double destX, double destY, double destWidth, double destHeight) { var clippedStopX = stopX; var clippedStopY = stopY; if (startX == stopX && startY == stopY) { return [startX, startY, clippedStopX, clippedStopY]; } var slope = (startY - stopY) / (startX - stopX); final halfHeight = destHeight * 0.5; final halfWidth = destWidth * 0.5; // Check vertical edge intersections if (startX != stopX) { final halfSlopeWidth = slope * halfWidth; if (halfSlopeWidth.abs() <= halfHeight) { if (destX > startX) { // Left edge intersection return [startX, startY, stopX - halfWidth, stopY - halfSlopeWidth]; } else if (destX < startX) { // Right edge intersection return [startX, startY, stopX + halfWidth, stopY + halfSlopeWidth]; } } } // Check horizontal edge intersections if (startY != stopY && slope != 0) { final halfSlopeHeight = halfHeight / slope; if (halfSlopeHeight.abs() <= halfWidth) { if (destY < startY) { // Bottom edge intersection clippedStopX = stopX + halfSlopeHeight; clippedStopY = stopY + halfHeight; } else if (destY > startY) { // Top edge intersection clippedStopX = stopX - halfSlopeHeight; clippedStopY = stopY - halfHeight; } } } return [startX, startY, clippedStopX, clippedStopY]; } List clipLine(double startX, double startY, double stopX, double stopY, Node destination) { final resultLine = [startX, startY, stopX, stopY]; if (startX == stopX && startY == stopY) return resultLine; var slope = (startY - stopY) / (startX - stopX); final halfHeight = destination.height * 0.5; final halfWidth = destination.width * 0.5; // Check vertical edge intersections if (startX != stopX) { final halfSlopeWidth = slope * halfWidth; if (halfSlopeWidth.abs() <= halfHeight) { if (destination.x > startX) { // Left edge intersection resultLine[2] = stopX - halfWidth; resultLine[3] = stopY - halfSlopeWidth; return resultLine; } else if (destination.x < startX) { // Right edge intersection resultLine[2] = stopX + halfWidth; resultLine[3] = stopY + halfSlopeWidth; return resultLine; } } } // Check horizontal edge intersections if (startY != stopY && slope != 0) { final halfSlopeHeight = halfHeight / slope; if (halfSlopeHeight.abs() <= halfWidth) { if (destination.y < startY) { // Bottom edge intersection resultLine[2] = stopX + halfSlopeHeight; resultLine[3] = stopY + halfHeight; } else if (destination.y > startY) { // Top edge intersection resultLine[2] = stopX - halfSlopeHeight; resultLine[3] = stopY - halfHeight; } } } return resultLine; } } ================================================ FILE: lib/edgerenderer/EdgeRenderer.dart ================================================ part of graphview; abstract class EdgeRenderer { Map? _animatedPositions; void setAnimatedPositions(Map positions) => _animatedPositions = positions; Offset getNodePosition(Node node) => _animatedPositions?[node] ?? node.position; void renderEdge(Canvas canvas, Edge edge, Paint paint); Offset getNodeCenter(Node node) { final nodePosition = getNodePosition(node); return Offset( nodePosition.dx + node.width * 0.5, nodePosition.dy + node.height * 0.5, ); } /// Draws a line between two points respecting the node's line type void drawStyledLine(Canvas canvas, Offset start, Offset end, Paint paint, {LineType? lineType}) { switch (lineType) { case LineType.DashedLine: drawDashedLine(canvas, start, end, paint, 0.6); break; case LineType.DottedLine: drawDashedLine(canvas, start, end, paint, 0.0); break; case LineType.SineLine: drawSineLine(canvas, start, end, paint); break; default: canvas.drawLine(start, end, paint); break; } } /// Draws a styled path respecting the node's line type void drawStyledPath(Canvas canvas, Path path, Paint paint, {LineType? lineType}) { if (lineType == null || lineType == LineType.Default) { canvas.drawPath(path, paint); } else { // For non-solid lines, we need to convert the path to segments // This is a simplified approach - for complex paths with curves, // you might need a more sophisticated solution canvas.drawPath(path, paint); } } /// Draws a dashed line between two points void drawDashedLine(Canvas canvas, Offset source, Offset destination, Paint paint, double lineLength) { final dx = destination.dx - source.dx; final dy = destination.dy - source.dy; final distance = sqrt(dx * dx + dy * dy); if (distance == 0) return; final numLines = lineLength == 0.0 ? (distance / 5).ceil() : 14; final stepX = dx / numLines; final stepY = dy / numLines; if (lineLength == 0.0) { // Draw dots final circleRadius = 1.0; final circlePaint = Paint() ..color = paint.color ..strokeWidth = 1.0 ..style = PaintingStyle.fill; for (var i = 0; i < numLines; i++) { final x = source.dx + (i * stepX); final y = source.dy + (i * stepY); canvas.drawCircle(Offset(x, y), circleRadius, circlePaint); } } else { // Draw dashes for (var i = 0; i < numLines; i++) { final startX = source.dx + (i * stepX); final startY = source.dy + (i * stepY); final endX = startX + (stepX * lineLength); final endY = startY + (stepY * lineLength); canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint); } } } /// Draws a sine wave line between two points void drawSineLine(Canvas canvas, Offset source, Offset destination, Paint paint) { final originalStrokeWidth = paint.strokeWidth; paint.strokeWidth = 1.5; final dx = destination.dx - source.dx; final dy = destination.dy - source.dy; final distance = sqrt(dx * dx + dy * dy); if (distance == 0 || (dx == 0 && dy == 0)) { paint.strokeWidth = originalStrokeWidth; return; } const lineLength = 6.0; const phaseOffset = 2.0; var distanceTraveled = 0.0; var phase = 0.0; final path = Path()..moveTo(source.dx, source.dy); while (distanceTraveled < distance) { final segmentLength = min(lineLength, distance - distanceTraveled); final segmentFraction = (distanceTraveled + segmentLength) / distance; final segmentDestination = Offset( source.dx + dx * segmentFraction, source.dy + dy * segmentFraction, ); final waveAmplitude = sin(phase + phaseOffset) * segmentLength; double perpX, perpY; if ((dx > 0 && dy < 0) || (dx < 0 && dy > 0)) { perpX = waveAmplitude; perpY = waveAmplitude; } else { perpX = -waveAmplitude; perpY = waveAmplitude; } path.lineTo(segmentDestination.dx + perpX, segmentDestination.dy + perpY); distanceTraveled += segmentLength; phase += pi * segmentLength / lineLength; } canvas.drawPath(path, paint); paint.strokeWidth = originalStrokeWidth; } /// Builds a loop path for self-referential edges and returns geometry /// data that renderers can use to draw arrows or style the segment. LoopRenderResult? buildSelfLoopPath( Edge edge, { double loopPadding = 16.0, double arrowLength = 12.0, }) { if (edge.source != edge.destination) { return null; } final node = edge.source; final nodeCenter = getNodeCenter(node); final anchorRadius = node.size.shortestSide * 0.5; final start = nodeCenter + Offset(anchorRadius, 0); final end = nodeCenter + Offset(0, -anchorRadius); final loopRadius = max( loopPadding + anchorRadius, anchorRadius * 1.5, ); final controlPoint1 = start + Offset(loopRadius, 0); final controlPoint2 = end + Offset(0, -loopRadius); final path = Path() ..moveTo(start.dx, start.dy) ..cubicTo( controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, controlPoint2.dy, end.dx, end.dy, ); final metrics = path.computeMetrics().toList(); if (metrics.isEmpty) { return LoopRenderResult(path, start, end); } final metric = metrics.first; final totalLength = metric.length; final effectiveArrowLength = arrowLength <= 0 ? 0.0 : min(arrowLength, totalLength * 0.3); final arrowBaseOffset = max(0.0, totalLength - effectiveArrowLength); final arrowBaseTangent = metric.getTangentForOffset(arrowBaseOffset); final arrowTipTangent = metric.getTangentForOffset(totalLength); return LoopRenderResult( path, arrowBaseTangent?.position ?? end, arrowTipTangent?.position ?? end, ); } } class LoopRenderResult { final Path path; final Offset arrowBase; final Offset arrowTip; const LoopRenderResult(this.path, this.arrowBase, this.arrowTip); } ================================================ FILE: lib/forcedirected/FruchtermanReingoldAlgorithm.dart ================================================ part of graphview; class FruchtermanReingoldAlgorithm implements Algorithm { static const double DEFAULT_TICK_FACTOR = 0.1; static const double CONVERGENCE_THRESHOLD = 1.0; Map displacement = {}; Map nodeRects = {}; Random rand = Random(); double graphHeight = 500; //default value, change ahead of time double graphWidth = 500; late double tick; FruchtermanReingoldConfiguration configuration; @override EdgeRenderer? renderer; FruchtermanReingoldAlgorithm(this.configuration, {this.renderer}) { renderer = renderer ?? ArrowEdgeRenderer(noArrow: true); } @override void init(Graph? graph) { graph!.nodes.forEach((node) { displacement[node] = Offset.zero; nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height); if (configuration.shuffleNodes) { node.position = Offset( rand.nextDouble() * graphWidth, rand.nextDouble() * graphHeight); // Update cached rect after position change nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height); } }); } void moveNodes(Graph graph) { final lerpFactor = configuration.lerpFactor; graph.nodes.forEach((node) { final nodeDisplacement = displacement[node]!; var target = node.position + nodeDisplacement; var newPosition = Offset.lerp(node.position, target, lerpFactor)!; double newDX = min(graphWidth - node.size.width * 0.5, max(node.size.width * 0.5, newPosition.dx)); double newDY = min(graphHeight - node.size.height * 0.5, max(node.size.height * 0.5, newPosition.dy)); node.position = Offset(newDX, newDY); // Update cached rect after position change nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height); }); } void cool(int currentIteration) { // tick *= 1.0 - currentIteration / configuration.iterations; const alpha = 0.99; // tweakable decay factor (0.8–0.99 typical) tick *= alpha; } void limitMaximumDisplacement(List nodes) { final epsilon = configuration.epsilon; nodes.forEach((node) { final nodeDisplacement = displacement[node]!; var dispLength = max(epsilon, nodeDisplacement.distance); node.position += nodeDisplacement / dispLength * min(dispLength, tick); // Update cached rect after position change nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height); }); } void calculateAttraction(List edges) { final attractionRate = configuration.attractionRate; final epsilon = configuration.epsilon; // Optimal distance (k) based on area and node count final k = sqrt((graphWidth * graphHeight) / (edges.length + 1)); for (var edge in edges) { var source = edge.source; var destination = edge.destination; var delta = source.position - destination.position; var deltaDistance = max(epsilon, delta.distance); // Standard FR attraction: proportional to distance² / k var attractionForce = (deltaDistance * deltaDistance) / k; var attractionVector = delta / deltaDistance * attractionForce * attractionRate; displacement[source] = displacement[source]! - attractionVector; displacement[destination] = displacement[destination]! + attractionVector; } } void calculateRepulsion(List nodes) { final repulsionRate = configuration.repulsionRate; final repulsionPercentage = configuration.repulsionPercentage; final epsilon = configuration.epsilon; final nodeCountDouble = nodes.length.toDouble(); final maxRepulsionDistance = min( graphWidth * repulsionPercentage, graphHeight * repulsionPercentage); for (var i = 0; i < nodeCountDouble; i++) { final currentNode = nodes[i]; for (var j = i + 1; j < nodeCountDouble; j++) { final otherNode = nodes[j]; if (currentNode != otherNode) { // Calculate distance between node rectangles, not just centers var delta = _getNodeRectDistance(currentNode, otherNode); var deltaDistance = max(epsilon, delta.distance); //protect for 0 var repulsionForce = max(0, maxRepulsionDistance - deltaDistance) / maxRepulsionDistance; //value between 0-1 var repulsionVector = delta * repulsionForce * repulsionRate; displacement[currentNode] = displacement[currentNode]! + repulsionVector; displacement[otherNode] = displacement[otherNode]! - repulsionVector; } } } } // Calculate closest distance vector between two node rectangles using cached rects Offset _getNodeRectDistance(Node nodeA, Node nodeB) { final rectA = nodeRects[nodeA]!; final rectB = nodeRects[nodeB]!; final centerA = rectA.center; final centerB = rectB.center; if (rectA.overlaps(rectB)) { // Push overlapping nodes apart by at least half their combined size final dx = (centerA.dx - centerB.dx).sign * (rectA.width / 2 + rectB.width / 2); final dy = (centerA.dy - centerB.dy).sign * (rectA.height / 2 + rectB.height / 2); return Offset(dx, dy); } // Non-overlapping: distance along nearest edges final dx = (centerA.dx < rectB.left) ? (rectB.left - rectA.right) : (centerA.dx > rectB.right) ? (rectA.left - rectB.right) : 0.0; final dy = (centerA.dy < rectB.top) ? (rectB.top - rectA.bottom) : (centerA.dy > rectB.bottom) ? (rectA.top - rectB.bottom) : 0.0; return Offset(dx == 0 ? centerA.dx - centerB.dx : dx, dy == 0 ? centerA.dy - centerB.dy : dy); } bool step(Graph graph) { var moved = false; displacement = {}; for (var node in graph.nodes) { displacement[node] = Offset.zero; } calculateRepulsion(graph.nodes); calculateAttraction(graph.edges); for (var node in graph.nodes) { final nodeDisplacement = displacement[node]!; if (nodeDisplacement.distance > configuration.movementThreshold) { moved = true; } } moveNodes(graph); return moved; } @override Size run(Graph? graph, double shiftX, double shiftY) { if (graph == null) { return Size.zero; } var size = findBiggestSize(graph) * graph.nodeCount(); graphWidth = size; graphHeight = size; var nodes = graph.nodes; var edges = graph.edges; tick = DEFAULT_TICK_FACTOR * sqrt(graphWidth / 2 * graphHeight / 2); if (graph.nodes.any((node) => node.position == Offset.zero)) { init(graph); } for (var i = 0; i < configuration.iterations; i++) { calculateRepulsion(nodes); calculateAttraction(edges); limitMaximumDisplacement(nodes); cool(i); if (done()) { break; } } positionNodes(graph); shiftCoordinates(graph, shiftX, shiftY); return graph.calculateGraphSize(); } void shiftCoordinates(Graph graph, double shiftX, double shiftY) { graph.nodes.forEach((node) { node.position = Offset(node.x + shiftX, node.y + shiftY); // Update cached rect after position change nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height); }); } void positionNodes(Graph graph) { var offset = getOffset(graph); var x = offset.dx; var y = offset.dy; var nodesVisited = []; var nodeClusters = []; graph.nodes.forEach((node) { node.position = Offset(node.x - x, node.y - y); // Update cached rect after position change nodeRects[node] = Rect.fromLTWH(node.x, node.y, node.width, node.height); }); graph.nodes.forEach((node) { if (!nodesVisited.contains(node)) { nodesVisited.add(node); var cluster = findClusterOf(nodeClusters, node); if (cluster == null) { cluster = NodeCluster(); cluster.add(node); nodeClusters.add(cluster); } followEdges(graph, cluster, node, nodesVisited); } }); positionCluster(nodeClusters); } void positionCluster(List nodeClusters) { combineSingleNodeCluster(nodeClusters); var cluster = nodeClusters[0]; // move first cluster to 0,0 cluster.offset(-cluster.rect!.left, -cluster.rect!.top); for (var i = 1; i < nodeClusters.length; i++) { var nextCluster = nodeClusters[i]; var xDiff = nextCluster.rect!.left - cluster.rect!.right - configuration.clusterPadding; var yDiff = nextCluster.rect!.top - cluster.rect!.top; nextCluster.offset(-xDiff, -yDiff); cluster = nextCluster; } } void combineSingleNodeCluster(List nodeClusters) { NodeCluster? firstSingleNodeCluster; nodeClusters.forEach((cluster) { if (cluster.size() == 1) { if (firstSingleNodeCluster == null) { firstSingleNodeCluster = cluster; } else { firstSingleNodeCluster!.concat(cluster); } } }); nodeClusters.removeWhere((element) => element.size() == 1); } void followEdges( Graph graph, NodeCluster cluster, Node node, List nodesVisited) { graph.successorsOf(node).forEach((successor) { if (!nodesVisited.contains(successor)) { nodesVisited.add(successor); cluster.add(successor); followEdges(graph, cluster, successor, nodesVisited); } }); graph.predecessorsOf(node).forEach((predecessor) { if (!nodesVisited.contains(predecessor)) { nodesVisited.add(predecessor); cluster.add(predecessor); followEdges(graph, cluster, predecessor, nodesVisited); } }); } NodeCluster? findClusterOf(List clusters, Node node) { return clusters.firstWhereOrNull((element) => element.contains(node)); } double findBiggestSize(Graph graph) { return graph.nodes.map((it) => max(it.height, it.width)).reduce(max); } Offset getOffset(Graph graph) { var offsetX = double.infinity; var offsetY = double.infinity; graph.nodes.forEach((node) { offsetX = min(offsetX, node.x); offsetY = min(offsetY, node.y); }); return Offset(offsetX, offsetY); } bool done() { return tick < CONVERGENCE_THRESHOLD / max(graphHeight, graphWidth); } void drawEdges(Canvas canvas, Graph graph, Paint linePaint) {} @override void setDimensions(double width, double height) { graphWidth = width; graphHeight = height; } } class NodeCluster { List nodes; Rect? rect; List getNodes() { return nodes; } Rect? getRect() { return rect; } void setRect(Rect newRect) { rect = newRect; } void add(Node node) { nodes.add(node); if (nodes.length == 1) { rect = Rect.fromLTRB( node.x, node.y, node.x + node.width, node.y + node.height); } else { rect = Rect.fromLTRB( min(rect!.left, node.x), min(rect!.top, node.y), max(rect!.right, node.x + node.width), max(rect!.bottom, node.y + node.height)); } } bool contains(Node node) { return nodes.contains(node); } int size() { return nodes.length; } void concat(NodeCluster cluster) { cluster.nodes.forEach((node) { node.position = (Offset( rect!.right + FruchtermanReingoldConfiguration.DEFAULT_CLUSTER_PADDING, rect!.top)); add(node); }); } void offset(double xDiff, double yDiff) { nodes.forEach((node) { node.position = (node.position + Offset(xDiff, yDiff)); }); rect = rect!.translate(xDiff, yDiff); } NodeCluster() : nodes = [], rect = Rect.zero; } ================================================ FILE: lib/forcedirected/FruchtermanReingoldConfiguration.dart ================================================ part of graphview; class FruchtermanReingoldConfiguration { static const int DEFAULT_ITERATIONS = 100; static const double DEFAULT_REPULSION_RATE = 0.2; static const double DEFAULT_REPULSION_PERCENTAGE = 0.4; static const double DEFAULT_ATTRACTION_RATE = 0.15; static const double DEFAULT_ATTRACTION_PERCENTAGE = 0.15; static const int DEFAULT_CLUSTER_PADDING = 15; static const double DEFAULT_EPSILON = 0.0001; static const double DEFAULT_LERP_FACTOR = 0.05; static const double DEFAULT_MOVEMENT_THRESHOLD = 0.6; int iterations; double repulsionRate; double repulsionPercentage; double attractionRate; double attractionPercentage; int clusterPadding; double epsilon; double lerpFactor; double movementThreshold; bool shuffleNodes = true; FruchtermanReingoldConfiguration({ this.iterations = DEFAULT_ITERATIONS, this.repulsionRate = DEFAULT_REPULSION_RATE, this.attractionRate = DEFAULT_ATTRACTION_RATE, this.repulsionPercentage = DEFAULT_REPULSION_PERCENTAGE, this.attractionPercentage = DEFAULT_ATTRACTION_PERCENTAGE, this.clusterPadding = DEFAULT_CLUSTER_PADDING, this.epsilon = DEFAULT_EPSILON, this.lerpFactor = DEFAULT_LERP_FACTOR, this.movementThreshold = DEFAULT_MOVEMENT_THRESHOLD, this.shuffleNodes = true }); } ================================================ FILE: lib/layered/EiglspergerAlgorithm.dart ================================================ part of graphview; class ContainerX { List segments = []; int index = -1; int pos = -1; double measure = -1; ContainerX(); void append(Segment segment) { segments.add(segment); } void join(ContainerX other) { segments.addAll(other.segments); other.segments.clear(); } int size() => segments.length; bool contains(Segment segment) => segments.contains(segment); bool get isEmpty => segments.isEmpty; static ContainerX createEmpty() => ContainerX(); // Split container at segment position static ContainerPair split(ContainerX container, Segment key) { final index = container.segments.indexOf(key); if (index == -1) { return ContainerPair(container, ContainerX()); } final leftSegments = container.segments.sublist(0, index); final rightSegments = container.segments.sublist(index + 1); final leftContainer = ContainerX(); leftContainer.segments = leftSegments; final rightContainer = ContainerX(); rightContainer.segments = rightSegments; return ContainerPair(leftContainer, rightContainer); } // Split container at position static ContainerPair splitAt(ContainerX container, int position) { if (position <= 0) { return ContainerPair(ContainerX(), container); } if (position >= container.size()) { return ContainerPair(container, ContainerX()); } final leftSegments = container.segments.sublist(0, position); final rightSegments = container.segments.sublist(position); final leftContainer = ContainerX(); leftContainer.segments = leftSegments; final rightContainer = ContainerX(); rightContainer.segments = rightSegments; return ContainerPair(leftContainer, rightContainer); } @override String toString() => 'Container(${segments.length} segments, pos: $pos, measure: $measure)'; } class ContainerPair { final ContainerX left; final ContainerX right; ContainerPair(this.left, this.right); } // Segment represents a vertical edge span between P and Q vertices class Segment { final Node pVertex; // top vertex (P-vertex) final Node qVertex; // bottom vertex (Q-vertex) int index = -1; final int id; static int _nextId = 0; Segment(this.pVertex, this.qVertex) : id = _nextId++; @override bool operator ==(Object other) => identical(this, other); @override int get hashCode => id; @override String toString() => 'Segment($id)'; } class EiglspergerNodeData { bool isDummy = false; bool isPVertex = false; bool isQVertex = false; Segment? segment; int layer = -1; int position = -1; int rank = -1; double measure = -1; Set reversed = {}; List predecessorNodes = []; List successorNodes = []; LineType lineType; EiglspergerNodeData(this.lineType); bool get isSegmentVertex => isPVertex || isQVertex; bool get isReversed => reversed.isNotEmpty; } class EiglspergerEdgeData { List bendPoints = []; } // Virtual edge for container connections class VirtualEdge { final dynamic source; final dynamic target; final int weight; VirtualEdge(this.source, this.target, this.weight); @override String toString() => 'VirtualEdge($source -> $target, weight: $weight)'; } // Layer element that can be either a Node or Container abstract class LayerElement { int index = -1; int pos = -1; double measure = -1; } // Node wrapper for layer elements class NodeElement extends LayerElement { final Node node; NodeElement(this.node); @override String toString() => 'NodeElement(${node.toString()})'; } // Container wrapper for layer elements class ContainerElement extends LayerElement { final ContainerX container; ContainerElement(this.container); @override String toString() => 'ContainerElement(${container.toString()})'; } class EiglspergerAlgorithm extends Algorithm { Map nodeData = {}; final Map _edgeData = {}; Set stack = {}; Set visited = {}; List> layers = []; List segments = []; Set typeOneConflicts = {}; late Graph graph; SugiyamaConfiguration configuration; @override EdgeRenderer? renderer; var nodeCount = 1; EiglspergerAlgorithm(this.configuration) { // renderer = SugiyamaEdgeRenderer(nodeData, edgeData, configuration.bendPointShape, configuration.addTriangleToEdge); } int get dummyId => 'Dummy ${nodeCount++}'.hashCode; bool isVertical() { var orientation = configuration.orientation; return orientation == SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM || orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP; } bool needReverseOrder() { var orientation = configuration.orientation; return orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP || orientation == SugiyamaConfiguration.ORIENTATION_RIGHT_LEFT; } @override Size run(Graph? graph, double shiftX, double shiftY) { this.graph = copyGraph(graph!); reset(); initNodeData(); cycleRemoval(); layerAssignment(); nodeOrdering(); // Eiglsperger 6-step process coordinateAssignment(); shiftCoordinates(shiftX, shiftY); final graphSize = graph.calculateGraphSize(); denormalize(); restoreCycle(); return graphSize; } void shiftCoordinates(double shiftX, double shiftY) { layers.forEach((List arrayList) { arrayList.forEach((it) { it!.position = Offset(it.x + shiftX, it.y + shiftY); }); }); } void reset() { layers.clear(); stack.clear(); visited.clear(); nodeData.clear(); _edgeData.clear(); segments.clear(); typeOneConflicts.clear(); nodeCount = 1; } void initNodeData() { graph.nodes.forEach((node) { node.position = Offset(0, 0); nodeData[node] = EiglspergerNodeData(node.lineType); }); graph.edges.forEach((edge) { _edgeData[edge] = EiglspergerEdgeData(); }); graph.edges.forEach((edge) { nodeData[edge.source]?.successorNodes.add(edge.destination); nodeData[edge.destination]?.predecessorNodes.add(edge.source); }); } void cycleRemoval() { graph.nodes.forEach((node) { dfs(node); }); } void dfs(Node node) { if (visited.contains(node)) { return; } visited.add(node); stack.add(node); graph.getOutEdges(node).forEach((edge) { final target = edge.destination; if (stack.contains(target)) { graph.removeEdge(edge); graph.addEdge(target, node); nodeData[node]!.reversed.add(target); } else { dfs(target); } }); stack.remove(node); } void layerAssignment() { if (graph.nodes.isEmpty) { return; } // Build layers using topological sort final copiedGraph = copyGraph(graph); var roots = getRootNodes(copiedGraph); while (roots.isNotEmpty) { layers.add(roots); copiedGraph.removeNodes(roots); roots = getRootNodes(copiedGraph); } // Create segments for long edges createSegmentsForLongEdges(); } void createSegmentsForLongEdges() { // Create segments for edges spanning more than one layer for (var i = 0; i < layers.length - 1; i++) { var currentLayer = layers[i]; for (var node in List.from(currentLayer)) { final edges = graph.getOutEdges(node) .where((e) => (nodeData[e.destination]!.layer - nodeData[node]!.layer).abs() > 1) .toList(); for (var edge in edges) { if (nodeData[edge.destination]!.layer - nodeData[node]!.layer == 2) { // Simple case: only one layer between source and target createSingleDummyVertex(edge, i + 1); } else { // Complex case: multiple layers between source and target createSegment(edge); } graph.removeEdge(edge); } } } } void createSingleDummyVertex(Edge edge, int dummyLayer) { final dummy = Node.Id(dummyId); final dummyData = EiglspergerNodeData(edge.source.lineType); dummyData.isDummy = true; dummyData.layer = dummyLayer; nodeData[dummy] = dummyData; dummy.size = Size(edge.source.width, 0); layers[dummyLayer].add(dummy); graph.addNode(dummy); final edge1 = graph.addEdge(edge.source, dummy); final edge2 = graph.addEdge(dummy, edge.destination); _edgeData[edge1] = EiglspergerEdgeData(); _edgeData[edge2] = EiglspergerEdgeData(); } void createSegment(Edge edge) { final sourceLayer = nodeData[edge.source]!.layer; final targetLayer = nodeData[edge.destination]!.layer; // Create P-vertex (top of segment) final pVertex = Node.Id(dummyId); final pData = EiglspergerNodeData(edge.source.lineType); pData.isDummy = true; pData.isPVertex = true; pData.layer = sourceLayer + 1; nodeData[pVertex] = pData; pVertex.size = Size(edge.source.width, 0); // Create Q-vertex (bottom of segment) final qVertex = Node.Id(dummyId); final qData = EiglspergerNodeData(edge.source.lineType); qData.isDummy = true; qData.isQVertex = true; qData.layer = targetLayer - 1; nodeData[qVertex] = qData; qVertex.size = Size(edge.source.width, 0); // Create segment and link vertices final segment = Segment(pVertex, qVertex); pData.segment = segment; qData.segment = segment; segments.add(segment); // Add to layers and graph layers[sourceLayer + 1].add(pVertex); layers[targetLayer - 1].add(qVertex); graph.addNode(pVertex); graph.addNode(qVertex); // Create edges final edgeToP = graph.addEdge(edge.source, pVertex); final segmentEdge = graph.addEdge(pVertex, qVertex); final edgeFromQ = graph.addEdge(qVertex, edge.destination); _edgeData[edgeToP] = EiglspergerEdgeData(); _edgeData[segmentEdge] = EiglspergerEdgeData(); _edgeData[edgeFromQ] = EiglspergerEdgeData(); } List getRootNodes(Graph graph) { final predecessors = {}; graph.edges.forEach((element) { predecessors[element.destination] = true; }); var roots = graph.nodes.where((node) => predecessors[node] == null); roots.forEach((node) { nodeData[node]?.layer = layers.length; }); return roots.toList(); } Graph copyGraph(Graph graph) { final copy = Graph(); copy.addNodes(graph.nodes); copy.addEdges(graph.edges); return copy; } void nodeOrdering() { final best = >[...layers]; // Precalculate neighbor information var bestCrossCount = double.infinity; for (var i = 0; i < configuration.iterations; i++) { var crossCount = 0.0; if (i % 2 == 0) { crossCount = forwardSweep(layers); } else { crossCount = backwardSweep(layers); } if (crossCount < bestCrossCount) { bestCrossCount = crossCount; // Save best configuration for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) { best[layerIndex] = List.from(layers[layerIndex]); } } if (crossCount == 0) break; } // Restore best configuration for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) { layers[layerIndex] = best[layerIndex]; } // Set final positions updateNodePositions(); } double forwardSweep(List> layers) { var totalCrossings = 0.0; for (var i = 0; i < layers.length - 1; i++) { var currentLayer = layers[i]; var nextLayer = layers[i + 1]; // Convert to layer elements with containers var currentElements = createLayerElements(currentLayer); var nextElements = createLayerElements(nextLayer); // Eiglsperger 6-step process stepOne(currentElements, true); // Handle P-vertices stepTwo(currentElements, nextElements); stepThree(nextElements); stepFour(nextElements, i + 1); totalCrossings += stepFive(currentElements, nextElements, i, i + 1); stepSix(nextElements); // Convert back to node layer layers[i + 1] = extractNodes(nextElements); } return totalCrossings; } double backwardSweep(List> layers) { var totalCrossings = 0.0; for (var i = layers.length - 1; i > 0; i--) { var currentLayer = layers[i]; var prevLayer = layers[i - 1]; var currentElements = createLayerElements(currentLayer); var prevElements = createLayerElements(prevLayer); stepOne(currentElements, false); // Handle Q-vertices stepTwo(currentElements, prevElements); stepThree(prevElements); stepFour(prevElements, i - 1); totalCrossings += stepFive(currentElements, prevElements, i, i - 1); stepSix(prevElements); layers[i - 1] = extractNodes(prevElements); } return totalCrossings; } List createLayerElements(List layer) { return layer.map((node) => NodeElement(node)).cast().toList(); } List extractNodes(List elements) { var nodes = []; for (var element in elements) { if (element is NodeElement) { nodes.add(element.node); } else if (element is ContainerElement) { // Extract nodes from segments in container for (var segment in element.container.segments) { if (!nodes.contains(segment.pVertex)) { nodes.add(segment.pVertex); } if (!nodes.contains(segment.qVertex)) { nodes.add(segment.qVertex); } } } } return nodes; } // Eiglsperger Step 1: Handle P-vertices (forward) or Q-vertices (backward) void stepOne(List layer, bool isForward) { var processedElements = []; ContainerX? currentContainer; for (var element in layer) { if (element is NodeElement) { var node = element.node; var data = nodeData[node]; var shouldMerge = isForward ? (data?.isPVertex ?? false) : (data?.isQVertex ?? false); if (shouldMerge && data?.segment != null) { // Merge into container currentContainer ??= ContainerX(); currentContainer.append(data!.segment!); if (!processedElements.any((e) => e is ContainerElement && e.container == currentContainer)) { processedElements.add(ContainerElement(currentContainer)); } } else { // Regular node processedElements.add(element); currentContainer = null; } } else { processedElements.add(element); currentContainer = null; } } layer.clear(); layer.addAll(processedElements); } // Eiglsperger Step 2: Compute position values and measures void stepTwo(List currentLayer, List nextLayer) { // Assign positions to current layer assignPositions(currentLayer); // Compute measures for next layer based on current layer positions for (var element in nextLayer) { if (element is NodeElement) { var node = element.node; var predecessors = predecessorsOf(node); if (predecessors.isNotEmpty) { var positions = predecessors.map((p) => nodeData[p]?.position ?? 0).toList(); positions.sort(); element.measure = medianValue(positions).toDouble(); } else { element.measure = element.pos.toDouble(); } } else if (element is ContainerElement) { element.measure = element.pos.toDouble(); } } } void assignPositions(List layer) { var currentPos = 0; for (var element in layer) { element.pos = currentPos; if (element is NodeElement) { nodeData[element.node]?.position = currentPos; currentPos++; } else if (element is ContainerElement) { currentPos += element.container.size(); } } } // Eiglsperger Step 3: Initial ordering based on measures void stepThree(List layer) { var vertices = []; var containers = []; // Separate vertices and containers for (var element in layer) { if (element is ContainerElement && element.container.size() > 0) { containers.add(element); } else if (element is NodeElement) { var data = nodeData[element.node]; if (!(data?.isSegmentVertex ?? false)) { vertices.add(element); } } } // Sort by measure vertices.sort((a, b) => a.measure.compareTo(b.measure)); containers.sort((a, b) => a.measure.compareTo(b.measure)); // Merge lists according to Eiglsperger algorithm var merged = mergeSortedLists(vertices, containers); layer.clear(); layer.addAll(merged); } List mergeSortedLists(List vertices, List containers) { var result = []; var vIndex = 0; var cIndex = 0; while (vIndex < vertices.length && cIndex < containers.length) { var vertex = vertices[vIndex]; var container = containers[cIndex]; if (vertex.measure <= container.pos) { result.add(vertex); vIndex++; } else if (vertex.measure >= (container.pos + container.container.size() - 1)) { result.add(container); cIndex++; } else { // Split container var k = (vertex.measure - container.pos).ceil(); var split = ContainerX.splitAt(container.container, k); if (split.left.size() > 0) { result.add(ContainerElement(split.left)); } result.add(vertex); if (split.right.size() > 0) { split.right.pos = container.pos + k; containers.insert(cIndex + 1, ContainerElement(split.right)); } vIndex++; cIndex++; } } // Add remaining elements while (vIndex < vertices.length) { result.add(vertices[vIndex++]); } while (cIndex < containers.length) { result.add(containers[cIndex++]); } return result; } // Eiglsperger Step 4: Place Q-vertices according to their segments void stepFour(List layer, int layerIndex) { var segmentVertices = []; // Find segment vertices in this layer for (var element in List.from(layer)) { if (element is NodeElement) { var data = nodeData[element.node]; if (data?.isSegmentVertex ?? false) { segmentVertices.add(element); layer.remove(element); } } } // Place each segment vertex for (var segmentElement in segmentVertices) { var segmentNode = segmentElement.node; var data = nodeData[segmentNode]; var segment = data?.segment; if (segment != null) { // Find container containing this segment ContainerElement? containerElement; for (var element in layer) { if (element is ContainerElement && element.container.contains(segment)) { containerElement = element; break; } } if (containerElement != null) { var containerIndex = layer.indexOf(containerElement); var split = ContainerX.split(containerElement.container, segment); layer.removeAt(containerIndex); if (split.left.size() > 0) { layer.insert(containerIndex, ContainerElement(split.left)); containerIndex++; } layer.insert(containerIndex, segmentElement); containerIndex++; if (split.right.size() > 0) { layer.insert(containerIndex, ContainerElement(split.right)); } } else { // No container found, just add the segment vertex layer.add(segmentElement); } } } updateIndices(layer); } void updateIndices(List layer) { for (var i = 0; i < layer.length; i++) { layer[i].index = i; if (layer[i] is NodeElement) { var node = (layer[i] as NodeElement).node; nodeData[node]?.position = i; } } } // Eiglsperger Step 5: Cross counting with virtual edges double stepFive(List currentLayer, List nextLayer, int currentRank, int nextRank) { // Remove empty containers currentLayer.removeWhere((e) => e is ContainerElement && e.container.isEmpty); nextLayer.removeWhere((e) => e is ContainerElement && e.container.isEmpty); updateIndices(currentLayer); updateIndices(nextLayer); // Collect all edges including virtual edges var allEdges = []; // Add regular graph edges between these layers for (var edge in graph.edges) { if (nodeData[edge.source]?.layer == currentRank && nodeData[edge.destination]?.layer == nextRank) { allEdges.add(edge); } } // Add virtual edges for containers for (var element in nextLayer) { if (element is ContainerElement && element.container.size() > 0) { var virtualEdge = VirtualEdge('virtual', element, element.container.size()); allEdges.add(virtualEdge); } else if (element is NodeElement) { var data = nodeData[element.node]; if (data?.isSegmentVertex ?? false) { var virtualEdge = VirtualEdge('virtual', element.node, 1); allEdges.add(virtualEdge); } } } // Count crossings with weights return countWeightedCrossings(allEdges, nextLayer); } double countWeightedCrossings(List edges, List nextLayer) { var crossings = 0.0; for (var i = 0; i < edges.length - 1; i++) { for (var j = i + 1; j < edges.length; j++) { var edge1 = edges[i]; var edge2 = edges[j]; var weight1 = getEdgeWeight(edge1); var weight2 = getEdgeWeight(edge2); var pos1 = getTargetPosition(edge1, nextLayer); var pos2 = getTargetPosition(edge2, nextLayer); if (pos1 > pos2) { crossings += weight1 * weight2; } } } return crossings; } int getEdgeWeight(dynamic edge) { if (edge is VirtualEdge) { return edge.weight; } return 1; } int getTargetPosition(dynamic edge, List nextLayer) { if (edge is VirtualEdge) { for (var i = 0; i < nextLayer.length; i++) { if ((nextLayer[i] is ContainerElement && nextLayer[i] == edge.target) || (nextLayer[i] is NodeElement && (nextLayer[i] as NodeElement).node == edge.target)) { return i; } } } else if (edge is Edge) { for (var i = 0; i < nextLayer.length; i++) { if (nextLayer[i] is NodeElement && (nextLayer[i] as NodeElement).node == edge.destination) { return i; } } } return 0; } // Eiglsperger Step 6: Scan and ensure alternating structure void stepSix(List layer) { var scanned = []; for (var i = 0; i < layer.length; i++) { var element = layer[i]; if (scanned.isEmpty) { if (element is ContainerElement) { scanned.add(element); } else { scanned.add(ContainerElement(ContainerX.createEmpty())); scanned.add(element); } } else { var previous = scanned.last; if (previous is ContainerElement && element is ContainerElement) { // Join containers previous.container.join(element.container); } else if (previous is NodeElement && element is NodeElement) { // Insert empty container between nodes scanned.add(ContainerElement(ContainerX.createEmpty())); scanned.add(element); } else { scanned.add(element); } } } // Ensure ends with container if (scanned.isNotEmpty && scanned.last is NodeElement) { scanned.add(ContainerElement(ContainerX.createEmpty())); } layer.clear(); layer.addAll(scanned); updateIndices(layer); } void updateNodePositions() { for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) { for (var nodeIndex = 0; nodeIndex < layers[layerIndex].length; nodeIndex++) { var node = layers[layerIndex][nodeIndex]; nodeData[node]?.position = nodeIndex; var data = nodeData[node]; if (data != null) { data.rank = layerIndex; } } } } void coordinateAssignment() { assignX(); assignY(); var offset = getOffset(graph, needReverseOrder()); graph.nodes.forEach((v) { v.position = getPosition(v, offset); }); } void assignX() { // Simplified coordinate assignment - can be enhanced with full Brandes-Köpf algorithm var separation = configuration.nodeSeparation; var vertical = isVertical(); for (var layerIndex = 0; layerIndex < layers.length; layerIndex++) { var layer = layers[layerIndex]; var x = 0.0; for (var nodeIndex = 0; nodeIndex < layer.length; nodeIndex++) { var node = layer[nodeIndex]; var width = vertical ? node.width + separation : node.height; node.x = x + width / 2; x += width + separation; } } } void assignXx() { // Existing implementation remains the same final root = >[]; // each node points to its aligned neighbor in the layer below.; final align = >[]; final sink = >[]; final x = >[]; // minimal separation between the roots of different classes.; final shift = >[]; // the width of each block (max width of node in block); final blockWidth = >[]; for (var i = 0; i < 4; i++) { root.add({}); align.add({}); sink.add({}); shift.add({}); x.add({}); blockWidth.add({}); graph.nodes.forEach((n) { root[i][n] = n; align[i][n] = n; sink[i][n] = n; shift[i][n] = double.infinity; x[i][n] = double.negativeInfinity; blockWidth[i][n] = 0; }); } var separation = configuration.nodeSeparation; var vertical = isVertical(); for (var downward = 0; downward <= 1; downward++) { var isDownward = downward == 0; final type1Conflicts = {}; for (var leftToRight = 0; leftToRight <= 1; leftToRight++) { final k = 2 * downward + leftToRight; var isLeftToRight = leftToRight == 0; verticalAlignment( root[k], align[k], type1Conflicts, isDownward, isLeftToRight); graph.nodes.forEach((v) { final r = root[k][v]!; blockWidth[k][r] = max( blockWidth[k][r]!, vertical ? v.width + separation : v.height); }); horizontalCompactation(align[k], root[k], sink[k], shift[k], blockWidth[k], x[k], isLeftToRight, isDownward, layers, separation); } } balance(x, blockWidth); } void balance(List> x, List> blockWidth) { final coordinates = {}; // switch (configuration.coordinateAssignment) { // case CoordinateAssignment.Average: // var minWidth = double.infinity; // // var smallestWidthLayout = 0; // final minArray = List.filled(4, 0.0); // final maxArray = List.filled(4, 0.0); // // // Get the layout with the smallest width and set minimum and maximum value for each direction; // for (var i = 0; i < 4; i++) { // minArray[i] = double.infinity; // maxArray[i] = 0; // // graph.nodes.forEach((v) { // final bw = 0.5 * blockWidth[i][v]!; // var xp = x[i][v]! - bw; // if (xp < minArray[i]) { // minArray[i] = xp; // } // xp = x[i][v]! + bw; // if (xp > maxArray[i]) { // maxArray[i] = xp; // } // }); // // final width = maxArray[i] - minArray[i]; // if (width < minWidth) { // minWidth = width; // smallestWidthLayout = i; // } // } // // // Align the layouts to the one with the smallest width // for (var layout = 0; layout < 4; layout++) { // if (layout != smallestWidthLayout) { // // Align the left to right layouts to the left border of the smallest layout // var diff = 0.0; // if (layout < 2) { // diff = minArray[layout] - minArray[smallestWidthLayout]; // } else { // // Align the right to left layouts to the right border of the smallest layout // diff = maxArray[layout] - maxArray[smallestWidthLayout]; // } // if (diff > 0) { // x[layout].keys.forEach((n) { // x[layout][n] = x[layout][n]! - diff; // }); // } else { // x[layout].keys.forEach((n) { // x[layout][n] = x[layout][n]! + diff; // }); // } // } // } // // // Get the average median of each coordinate // var values = List.filled(4, 0.0); // graph.nodes.forEach((n) { // for (var i = 0; i < 4; i++) { // values[i] = x[i][n]!; // } // values.sort(); // var average = (values[1] + values[2]) * 0.5; // coordinates[n] = average; // }); // break; // case CoordinateAssignment.DownRight: // graph.nodes.forEach((n) { // coordinates[n] = x[0][n] ?? 0.0; // }); // break; // case CoordinateAssignment.DownLeft: // graph.nodes.forEach((n) { // coordinates[n] = x[1][n] ?? 0.0; // }); // break; // case CoordinateAssignment.UpRight: // graph.nodes.forEach((n) { // coordinates[n] = x[2][n] ?? 0.0; // }); // break; // case CoordinateAssignment.UpLeft: // graph.nodes.forEach((n) { // coordinates[n] = x[3][n] ?? 0.0; // }); // break; // } graph.nodes.forEach((n) { coordinates[n] = x[3][n] ?? 0.0; }); // Get the minimum coordinate value var minValue = coordinates.values.reduce(min); // Set left border to 0 if (minValue != 0) { coordinates.keys.forEach((n) { coordinates[n] = coordinates[n]! - minValue; }); } // resolveOverlaps(coordinates); graph.nodes.forEach((v) { v.x = coordinates[v]!; }); } void resolveOverlaps(Map coordinates) { for (var layer in layers) { var layerNodes = List.from(layer); layerNodes.sort( (a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position)); var data = nodeData[layerNodes.first]; if (data?.layer != 0) { var leftCoordinate = 0.0; for (var i = 1; i < layerNodes.length; i++) { var currentNode = layerNodes[i]; if (!nodeData[currentNode]!.isDummy) { var previousNode = getPreviousNonDummyNode(layerNodes, i); if (previousNode != null) { leftCoordinate = coordinates[previousNode]! + previousNode.width + configuration.nodeSeparation; } else { leftCoordinate = 0.0; } if (leftCoordinate > coordinates[currentNode]!) { var adjustment = leftCoordinate - coordinates[currentNode]!; if (coordinates[currentNode] != null) { coordinates[currentNode] = coordinates[currentNode]! + adjustment; } } } } } } } Node? getPreviousNonDummyNode(List layerNodes, int currentIndex) { for (var i = currentIndex - 1; i >= 0; i--) { var previousNode = layerNodes[i]; if (!nodeData[previousNode]!.isDummy) { return previousNode; } } return null; } // Map markType1Conflicts(bool downward) { // if (layers.length >= 4) { // int upper; // int lower; // iteration bounds; // int k1; // node position boundaries of closest inner segments; // if (downward) { // lower = 1; // upper = layers.length - 2; // } else { // lower = layers.length - 1; // upper = 2; // } // /*; // * iterate level[2..h-2] in the given direction; // * available 1 levels to h; // */ // for (var i = lower; // downward ? i <= upper : i >= upper; // i += downward ? 1 : -1) { // var k0 = 0; // var firstIndex = 0; // index of first node on layer; // final currentLevel = layers[i]; // final nextLevel = downward ? layers[i + 1] : layers[i - 1]; // // // for all nodes on next level; // for (var l1 = 0; l1 < nextLevel.length; l1++) { // final virtualTwin = virtualTwinNode(nextLevel[l1], downward); // // if (l1 == nextLevel.length - 1 || virtualTwin != null) { // k1 = currentLevel.length - 1; // // if (virtualTwin != null) { // k1 = positionOfNode(virtualTwin); // } // // while (firstIndex <= l1) { // final upperNeighbours = getAdjNodes(nextLevel[l1], downward); // // for (var currentNeighbour in upperNeighbours) { // /*; // * XXX< 0 in first iteration is still ok for indizes starting; // * with 0 because no index can be smaller than 0; // */ // final currentNeighbourIndex = positionOfNode(currentNeighbour); // // if (currentNeighbourIndex < k0 || currentNeighbourIndex > k1) { // type1Conflicts[l1] = currentNeighbourIndex; // } // } // firstIndex++; // } // // k0 = k1; // } // } // } // } // return type1Conflicts; // } void verticalAlignment(Map root, Map align, Map type1Conflicts, bool downward, bool leftToRight) { // for all Level; var layersa = downward ? layers : layers.reversed; for (var layer in layersa) { // As with layers, we need a reversed iterator for blocks for different directions var nodes = leftToRight ? layer : layer.reversed; // Do an initial placement for all blocks var r = leftToRight ? -1 : double.infinity; for (var v in nodes) { final adjNodes = getAdjNodes(v, downward); if (adjNodes.isNotEmpty) { var midLevelValue = adjNodes.length / 2; // Calculate medians final medians = adjNodes.length % 2 == 1 ? [adjNodes[midLevelValue.floor()]] : [ adjNodes[midLevelValue.toInt() - 1], adjNodes[midLevelValue.toInt()] ]; // For all median neighbours in direction of H for (var m in medians) { final posM = positionOfNode(m); // if segment (u,v) not marked by type1 conflicts AND ...; if (align[v] == v && type1Conflicts[positionOfNode(v)] != posM && (leftToRight ? r < posM : r > posM)) { align[m] = v; root[v] = root[m]; align[v] = root[v]; r = posM; } } } } } } void horizontalCompactation( Map align, Map root, Map sink, Map shift, Map blockWidth, Map x, bool leftToRight, bool downward, List> layers, int separation) { // calculate class relative coordinates for all roots; // 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) var layersa = leftToRight ? layers : layers.reversed; for (var layer in layersa) { // As with layers, we need a reversed iterator for blocks for different directions var nodes = downward ? layer : layer.reversed; // Do an initial placement for all blocks for (var v in nodes) { if (root[v] == v) { placeBlock(v, sink, shift, x, align, blockWidth, root, leftToRight, layers, separation); } } } var d = 0; var i = downward ? 0 : layers.length - 1; while (downward && i <= layers.length - 1 || !downward && i >= 0) { final currentLevel = layers[i]; final v = currentLevel[leftToRight ? 0 : currentLevel.length - 1]; if (v == sink[root[v]]) { final oldShift = shift[v]!; if (oldShift < double.infinity) { shift[v] = oldShift + d; d += oldShift.toInt(); } else { shift[v] = 0; } } i = downward ? i + 1 : i - 1; } // apply root coordinates for all aligned nodes; // (place block did this only for the roots)+; graph.nodes.forEach((v) { x[v] = x[root[v]]!; final shiftVal = shift[sink[root[v]]]!; if (shiftVal < double.infinity) { x[v] = x[v]! + shiftVal; // apply shift for each class; } }); } void placeBlock( Node v, Map sink, Map shift, Map x, Map align, Map blockWidth, Map root, bool leftToRight, List> layers, int separation) { if (x[v] == double.negativeInfinity) { x[v] = 0; var currentNode = v; try { do { // if not first node on layer; final hasPredecessor = leftToRight && positionOfNode(currentNode) > 0 || !leftToRight && positionOfNode(currentNode) < layers[getLayerIndex(currentNode)].length - 1; // print("Pred $hasPredecessor ${getLayerIndex(currentNode)>0} ${positionOfNode(currentNode)>0}"); if (hasPredecessor) { final pred = predecessor(currentNode, leftToRight); /* Get the root of u (proceeding all the way upwards in the block) */ final u = root[pred]!; /* Place the block of u recursively */ placeBlock(u, sink, shift, x, align, blockWidth, root, leftToRight, layers, separation); /* If v is its own sink yet, set its sink to the sink of u */ if (sink[v] == v) { sink[v] = sink[u]!; } /* If v and u have different sinks (i.e. they are in different classes), * shift the sink of u so that the two blocks are separated by the preferred gap */ var gap = separation + 0.5 * (blockWidth[u]! + blockWidth[v]!); if (sink[v] != sink[u]) { if (leftToRight) { shift[sink[u]!] = min(shift[sink[u]]!, x[v]! - x[u]! - gap); } else { shift[sink[u]!] = max(shift[sink[u]]!, x[v]! - x[u]! + gap); } } else { /* v and u have the same sink, i.e. they are in the same level. Make sure that v is separated from u by at least gap.*/ if (leftToRight) { x[v] = max(x[v]!, x[u]! + gap); } else { x[v] = min(x[v]!, x[u]! - gap); } } } currentNode = align[currentNode]!; } while (currentNode != v); } catch (e) { print(e); } } } List successorsOf(Node? node) { return graph.successorsOf(node); } List predecessorsOf(Node? node) { return graph.predecessorsOf(node); } List getAdjNodes(Node node, bool downward) { if (downward) { return predecessorsOf(node); } else { return successorsOf(node); } } // predecessor; Node? predecessor(Node? v, bool leftToRight) { final pos = positionOfNode(v); final rank = getLayerIndex(v); final level = layers[rank]; if (leftToRight && pos != 0 || !leftToRight && pos != level.length - 1) { return level[(leftToRight) ? pos - 1 : pos + 1]; } else { return null; } } Node? virtualTwinNode(Node node, bool downward) { if (!isLongEdgeDummy(node)) { return null; } final adjNodes = getAdjNodes(node, downward); return adjNodes.isEmpty ? null : adjNodes[0]; } // get node index in layer; int positionOfNode(Node? node) { return nodeData[node]?.position ?? -1; } int getLayerIndex(Node? node) { return nodeData[node]?.layer ?? -1; } bool isLongEdgeDummy(Node? v) { final successors = successorsOf(v); return nodeData[v!]!.isDummy && successors.length == 1 && nodeData[successors[0]]!.isDummy; } void assignY() { var k = layers.length; var yPos = 0.0; var vertical = isVertical(); for (var i = 0; i < k; i++) { var level = layers[i]; var maxHeight = 0.0; level.forEach((node) { var h = nodeData[node]!.isDummy ? 0.0 : vertical ? node.height : node.width; if (h > maxHeight) { maxHeight = h; } node.y = yPos; }); if (i < k - 1) { yPos += configuration.levelSeparation + maxHeight; } } } void denormalize() { // Remove dummy vertices and create bend points for articulated edges for (var i = 1; i < layers.length - 1; i++) { final iterator = layers[i].iterator; while (iterator.moveNext()) { final current = iterator.current; if (nodeData[current]!.isDummy) { final predecessor = graph.predecessorsOf(current)[0]; final successor = graph.successorsOf(current)[0]; final bendPoints = _edgeData[graph.getEdgeBetween(predecessor, current)!]!.bendPoints; if (bendPoints.isEmpty || !bendPoints.contains(current.x + predecessor.width / 2)) { bendPoints.add(predecessor.x + predecessor.width / 2); bendPoints.add(predecessor.y + predecessor.height / 2); bendPoints.add(current.x + predecessor.width / 2); bendPoints.add(current.y); } if (!nodeData[predecessor]!.isDummy) { bendPoints.add(current.x + predecessor.width / 2); } else { bendPoints.add(current.x); } bendPoints.add(current.y); if (nodeData[successor]!.isDummy) { bendPoints.add(successor.x + predecessor.width / 2); } else { bendPoints.add(successor.x + successor.width / 2); } bendPoints.add(successor.y + successor.height / 2); graph.removeEdgeFromPredecessor(predecessor, current); graph.removeEdgeFromPredecessor(current, successor); final edge = graph.addEdge(predecessor, successor); final edgeData = EiglspergerEdgeData(); edgeData.bendPoints = bendPoints; _edgeData[edge] = edgeData; graph.removeNode(current); } } } } void restoreCycle() { graph.nodes.forEach((n) { if (nodeData[n]!.isReversed) { nodeData[n]!.reversed.forEach((target) { final bendPoints = _edgeData[graph.getEdgeBetween(target, n)!]!.bendPoints; graph.removeEdgeFromPredecessor(target, n); final edge = graph.addEdge(n, target); final edgeData = EiglspergerEdgeData(); edgeData.bendPoints = bendPoints; _edgeData[edge] = edgeData; }); } }); } Offset getOffset(Graph graph, bool needReverseOrder) { var offsetX = double.infinity; var offsetY = double.infinity; if (needReverseOrder) { offsetY = double.minPositive; } graph.nodes.forEach((node) { if (needReverseOrder) { offsetX = min(offsetX, node.x); offsetY = max(offsetY, node.y); } else { offsetX = min(offsetX, node.x); offsetY = min(offsetY, node.y); } }); return Offset(offsetX, offsetY); } Offset getPosition(Node node, Offset offset) { Offset finalOffset; switch (configuration.orientation) { case 1: finalOffset = Offset(node.x - offset.dx, node.y); break; case 2: finalOffset = Offset(node.x - offset.dx, offset.dy - node.y); break; case 3: finalOffset = Offset(node.y, node.x - offset.dx); break; case 4: finalOffset = Offset(offset.dy - node.y, node.x - offset.dx); break; default: finalOffset = Offset(0, 0); break; } return finalOffset; } static double medianValue(List positions) { if (positions.isEmpty) return 0.0; if (positions.length == 1) return positions[0].toDouble(); positions.sort(); final mid = positions.length ~/ 2; if (positions.length % 2 == 1) { return positions[mid].toDouble(); } else if (positions.length == 2) { return (positions[0] + positions[1]) / 2.0; } else { final left = positions[mid - 1] - positions[0]; final right = positions[positions.length - 1] - positions[mid]; if (left + right == 0) return 0.0; return (positions[mid - 1] * right + positions[mid] * left) / (left + right); } } @override void init(Graph? graph) { this.graph = copyGraph(graph!); reset(); initNodeData(); cycleRemoval(); layerAssignment(); nodeOrdering(); coordinateAssignment(); denormalize(); restoreCycle(); } @override void setDimensions(double width, double height) { // Can be used to set layout bounds if needed } } ================================================ FILE: lib/layered/SugiyamaAlgorithm.dart ================================================ part of graphview; class SugiyamaAlgorithm extends Algorithm { Map nodeData = {}; Map edgeData = {}; Set stack = {}; Set visited = {}; List> layers = []; final type1Conflicts = {}; late Graph graph; SugiyamaConfiguration configuration; @override EdgeRenderer? renderer; var nodeCount = 1; SugiyamaAlgorithm(this.configuration) { renderer = SugiyamaEdgeRenderer(nodeData, edgeData, configuration.bendPointShape, configuration.addTriangleToEdge); } int get dummyId => 'Dummy ${nodeCount++}'.hashCode; bool isVertical() { var orientation = configuration.orientation; return orientation == SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM || orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP; } bool needReverseOrder() { var orientation = configuration.orientation; return orientation == SugiyamaConfiguration.ORIENTATION_BOTTOM_TOP || orientation == SugiyamaConfiguration.ORIENTATION_RIGHT_LEFT; } @override Size run(Graph? graph, double shiftX, double shiftY) { this.graph = copyGraph(graph!); reset(); initSugiyamaData(); cycleRemoval(); layerAssignment(); nodeOrdering(); //expensive operation coordinateAssignment(); //expensive operation // if (configuration.enableAngleOptimization) { // final optimizer = CrossingAngleOptimizer(this.graph, layers, nodeData, edgeData, configuration); // optimizer.optimize(); // // The optimizer modifies the Y coordinates in place, so no need to call assignY() again. // } shiftCoordinates(shiftX, shiftY); final graphSize = graph.calculateGraphSize(); denormalize(); restoreCycle(); return graphSize; } void shiftCoordinates(double shiftX, double shiftY) { layers.forEach((List arrayList) { arrayList.forEach((it) { it!.position = Offset(it.x + shiftX, it.y + shiftY); }); }); } void reset() { layers.clear(); stack.clear(); visited.clear(); nodeData.clear(); edgeData.clear(); nodeCount = 1; } void initSugiyamaData() { graph.nodes.forEach((node) { node.position = Offset(0, 0); nodeData[node] = SugiyamaNodeData(node.lineType); }); graph.edges.forEach((edge) { edgeData[edge] = SugiyamaEdgeData(); }); } void dfs(Node node) { if (visited.contains(node)) { return; } visited.add(node); stack.add(node); graph.getOutEdges(node).toList().forEach((edge) { final target = edge.destination; if (stack.contains(target)) { final storedData = edgeData.remove(edge); graph.removeEdge(edge); final reversedEdge = graph.addEdge(target, node); edgeData[reversedEdge] = storedData ?? SugiyamaEdgeData(); nodeData[node]!.reversed.add(target); } else { dfs(target); } }); stack.remove(node); } void layerAssignment() { switch (configuration.layeringStrategy) { case LayeringStrategy.topDown: layerAssignmentTopDown(); break; case LayeringStrategy.longestPath: layerAssignmentLongestPath(); break; case LayeringStrategy.coffmanGraham: layerAssignmentCoffmanGraham(); break; case LayeringStrategy.networkSimplex: layerAssignmentNetworkSimplex(); break; } // Add dummy nodes for long edges addDummyNodes(); } void layerAssignmentTopDown() { if (graph.nodes.isEmpty) return; final copiedGraph = copyGraph(graph); var roots = getRootNodes(copiedGraph); while (roots.isNotEmpty) { layers.add(roots); copiedGraph.removeNodes(roots); roots = getRootNodes(copiedGraph); } // Set layer metadata for (var i = 0; i < layers.length; i++) { for (var j = 0; j < layers[i].length; j++) { nodeData[layers[i][j]]!.layer = i; nodeData[layers[i][j]]!.position = j; } } } void layerAssignmentLongestPath() { if (graph.nodes.isEmpty) return; var U = {}; var Z = {}; var V = Set.from(graph.nodes); var currentLayer = 0; layers = [[]]; while (U.length != graph.nodes.length) { var candidates = V .where((v) => !U.contains(v) && Z.containsAll(graph.successorsOf(v))); if (candidates.isNotEmpty) { var node = candidates.first; layers[currentLayer].add(node); U.add(node); } else { currentLayer++; layers.add([]); Z.addAll(U); } } // Reverse layers and set metadata layers = layers.reversed.where((layer) => layer.isNotEmpty).toList(); for (var i = 0; i < layers.length; i++) { for (var j = 0; j < layers[i].length; j++) { nodeData[layers[i][j]]!.layer = i; nodeData[layers[i][j]]!.position = j; } } } void layerAssignmentCoffmanGraham() { if (graph.nodes.isEmpty) return; var width = (graph.nodes.length / 10).ceil(); var Z = {}; var lambda = {}; var V = Set.from(graph.nodes); // Assign lambda values based on in-degree V.forEach((v) => lambda[v] = double.maxFinite.toInt()); for (var i = 0; i < V.length; i++) { var mv = V.where((v) => lambda[v] == double.maxFinite.toInt()).reduce( (a, b) => graph.getInEdges(a).length <= graph.getInEdges(b).length ? a : b); lambda[mv] = i; } var k = 0; layers = [[]]; var U = {}; while (U.length != graph.nodes.length) { var candidates = V .where((v) => !U.contains(v) && U.containsAll(graph.successorsOf(v))); if (candidates.isNotEmpty) { var got = candidates.reduce((a, b) => lambda[a]! > lambda[b]! ? a : b); if (layers[k].length < width && Z.containsAll(graph.successorsOf(got))) { layers[k].add(got); } else { Z.addAll(layers[k]); k++; layers.add([]); layers[k].add(got); } U.add(got); } } // Remove empty layers and reverse layers = layers.where((l) => l.isNotEmpty).toList().reversed.toList(); // Set metadata for (var i = 0; i < layers.length; i++) { for (var j = 0; j < layers[i].length; j++) { nodeData[layers[i][j]]!.layer = i; nodeData[layers[i][j]]!.position = j; } } } void layerAssignmentNetworkSimplex() { // Start with longest path as base layerAssignmentLongestPath(); // Simple optimization: try to minimize edge span var improved = true; var iterations = 5; while (improved && iterations > 0) { improved = false; iterations--; for (var i = layers.length - 1; i >= 0; i--) { var layer = List.from(layers[i]); var nodesToMove = {}; for (var v in layer) { if (graph.getOutEdges(v).isEmpty) continue; var outgoingEdges = graph.getOutEdges(v); if (outgoingEdges.isNotEmpty) { var minRank = outgoingEdges .map((e) => nodeData[e.destination]!.layer - 1) .reduce(min); if (minRank != nodeData[v]!.layer && minRank >= 0) { nodesToMove[v] = minRank; improved = true; } } } // Move nodes for (var entry in nodesToMove.entries) { var node = entry.key; var newRank = entry.value; var oldRank = nodeData[node]!.layer; layers[oldRank].remove(node); if (newRank < layers.length) { layers[newRank].add(node); nodeData[node]!.layer = newRank; } } } // Recompute positions for (var i = 0; i < layers.length; i++) { for (var j = 0; j < layers[i].length; j++) { nodeData[layers[i][j]]!.position = j; } } } } void addDummyNodes() { for (var i = 0; i < layers.length - 1; i++) { var indexNextLayer = i + 1; var currentLayer = layers[i]; var nextLayer = layers[indexNextLayer]; for (var node in currentLayer) { final edges = graph.edges .where((element) => element.source == node && (nodeData[element.destination]!.layer - nodeData[node]!.layer) .abs() > 1) .toList(); final iterator = edges.iterator; while (iterator.moveNext()) { final edge = iterator.current; final dummy = Node.Id(dummyId.hashCode); final dummyNodeData = SugiyamaNodeData(node.lineType); dummyNodeData.isDummy = true; dummyNodeData.layer = indexNextLayer; nextLayer.add(dummy); nodeData[dummy] = dummyNodeData; dummy.size = Size(edge.source.width, 0); // calc TODO avg layer height; final dummyEdge1 = graph.addEdge(edge.source, dummy); final dummyEdge2 = graph.addEdge(dummy, edge.destination); edgeData[dummyEdge1] = SugiyamaEdgeData(); edgeData[dummyEdge2] = SugiyamaEdgeData(); graph.removeEdge(edge); // iterator.remove(); } } } } List getRootNodes(Graph graph) { final predecessors = {}; graph.edges.forEach((element) { predecessors[element.destination] = true; }); var roots = graph.nodes.where((node) => predecessors[node] == null); roots.forEach((node) { nodeData[node]?.layer = layers.length; }); return roots.toList(); } Graph copyGraph(Graph graph) { final copy = Graph(); copy.addNodes(graph.nodes); copy.addEdges(graph.edges); return copy; } void nodeOrdering() { // The `layers` variable is the member variable of the class. // We will modify it directly. There is no need for a separate 'best' copy // with the current iterative improvement strategy. // Precalculate predecessor and successor info, must be done here after adding the dummy nodes graph.edges.forEach((element) { nodeData[element.source]?.successorNodes.add(element.destination); nodeData[element.destination]?.predecessorNodes.add(element.source); }); for (var i = 0; i < configuration.iterations; i++) { // Apply the median heuristic to reorder nodes in each layer. median(layers, i); // Apply the transpose heuristic to fine-tune the ordering by swapping adjacent nodes. // This will use the efficient AccumulatorTree-based approach we defined. var changed = configuration.crossMinimizationStrategy == CrossMinimizationStrategy.simple ? transposeSimple(layers) : transposeAccumulator(layers); // If a full pass of transpose made no improvements, we've stabilized. if (!changed) { break; } } // Set final positions based on the optimized order. for (var currentLayer in layers) { for (var pos = 0; pos < currentLayer.length; pos++) { nodeData[currentLayer[pos]]?.position = pos; } } } void median(List> layers, int currentIteration) { if (currentIteration % 2 == 0) { for (var i = 1; i < layers.length; i++) { var currentLayer = layers[i]; var previousLayer = layers[i - 1]; // get the positions of adjacent vertices in adj_rank var positions = []; var pos = 0; previousLayer.forEach((node) { successorsOf(node).forEach((element) { positions.add(pos); }); pos++; }); positions.sort(); // set the position in terms of median based on adjacent values if (positions.isNotEmpty) { var median = positions.length ~/ 2; if (positions.length == 1) { median = -1; } else if (positions.length == 2) { median = (positions[0] + positions[1]) ~/ 2; } else if (positions.length % 2 == 1) { median = positions[median]; } else { final left = positions[median - 1] - positions[0]; final right = positions[positions.length - 1] - positions[median]; if (left + right != 0) { median = (positions[median - 1] * right + positions[median] * left) ~/ (left + right); } } for (var node in currentLayer) { nodeData[node!]!.median = median; } } currentLayer .sort((n1, n2) => nodeData[n1!]!.median - nodeData[n2!]!.median); } } else { for (var l = 1; l < layers.length; l++) { var currentLayer = layers[l]; var previousLayer = layers[l - 1]; var positions = []; var pos = 0; previousLayer.forEach((node) { successorsOf(node).forEach((element) { positions.add(pos); }); pos++; }); positions.sort(); if (positions.isNotEmpty) { var median = 0; if (positions.length == 1) { median = positions[0]; } else { median = (positions[(positions.length / 2.0).ceil()] + positions[(positions.length / 2.0).ceil() - 1]) ~/ 2; } for (var i = currentLayer.length - 1; i > 1; i--) { final node = currentLayer[i]; nodeData[node!]!.median = median; } } currentLayer .sort((n1, n2) => nodeData[n1!]!.median - nodeData[n2!]!.median); } } } bool transposeSimple(List> layers) { var changed = false; var improved = true; while (improved) { improved = false; for (var l = 0; l < layers.length - 1; l++) { final northernNodes = layers[l]; final southernNodes = layers[l + 1]; // Create a map that holds the index of every [Node]. Key is the [Node] and value is the index of the item. final indexMap = HashMap.of( northernNodes.asMap().map((key, value) => MapEntry(value, key))); for (var i = 0; i < southernNodes.length - 1; i++) { final v = southernNodes[i]; final w = southernNodes[i + 1]; if (crossingCount(indexMap, v, w) > crossingCount(indexMap, w, v)) { improved = true; exchange(southernNodes, v, w); changed = true; } } } } return changed; } bool transposeAccumulator(List> layers) { var changed = false; var improved = true; while (improved) { improved = false; for (var l = 0; l < layers.length - 1; l++) { final upperLayer = layers[l]; final lowerLayer = layers[l + 1]; // Calculate the total crossings for this pair of layers before any swaps. var crossingsBefore = _getBiLayerCrossings(upperLayer, lowerLayer); if (crossingsBefore == 0) continue; for (var i = 0; i < lowerLayer.length - 1; i++) { final v = lowerLayer[i]; final w = lowerLayer[i + 1]; // Perform a trial swap exchange(lowerLayer, v, w); // Recalculate total crossings with the more efficient method. var crossingsAfter = _getBiLayerCrossings(upperLayer, lowerLayer); if (crossingsAfter < crossingsBefore) { // The swap was good, keep it. improved = true; changed = true; crossingsBefore = crossingsAfter; // Update the baseline crossing count } else { // The swap was not beneficial, revert it. exchange(lowerLayer, w, v); } } } } return changed; } /// Calculates the number of crossings between two specific layers using the AccumulatorTree. int _getBiLayerCrossings(List upperLayer, List lowerLayer) { if (upperLayer.isEmpty || lowerLayer.isEmpty) { return 0; } // Update positions in nodeData based on the current list order. // This is crucial as the transpose function modifies the list directly. for (var i = 0; i < lowerLayer.length; i++) { nodeData[lowerLayer[i]]!.position = i; } var targetIndices = []; // Ensure upper layer nodes are sorted by their original position to maintain a stable sort. var sortedUpperLayer = List.from(upperLayer) ..sort((a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position)); for (var source in sortedUpperLayer) { var successors = successorsOf(source) .where((succ) => lowerLayer.contains(succ)) .toList() ..sort( (a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position)); for (var successor in successors) { targetIndices.add(nodeData[successor]!.position); } } if (targetIndices.isNotEmpty) { var maxIndex = targetIndices.reduce(max); var accumTree = AccumulatorTree(maxIndex + 1); return accumTree.crossCount(targetIndices); } return 0; } void exchange(List nodes, Node v, Node w) { var i = nodes.indexOf(v); var j = nodes.indexOf(w); var temp = nodes[i]; nodes[i] = nodes[j]; nodes[j] = temp; } // counts the number of edge crossings if n2 appears to the left of n1 in their layer.; int crossingCount(HashMap northernNodes, Node? n1, Node? n2) { final indexOf = (Node node) => northernNodes[node]!; var crossing = 0; final parentNodesN1 = graph.predecessorsOf(n1); final parentNodesN2 = graph.predecessorsOf(n2); parentNodesN2.forEach((pn2) { final indexOfPn2 = indexOf(pn2); parentNodesN1.where((it) => indexOfPn2 < indexOf(it)).forEach((element) { crossing++; }); }); return crossing; } int crossing(List> layers) { var crossinga = 0; for (var l = 0; l < layers.length - 1; l++) { final southernNodes = layers[l]; final northernNodes = layers[l + 1]; final indexMap = HashMap.of( northernNodes.asMap().map((key, value) => MapEntry(value, key))); for (var i = 0; i < southernNodes.length - 2; i++) { final v = southernNodes[i]; final w = southernNodes[i + 1]; crossinga += crossingCount(indexMap, v, w); } } return crossinga; } void coordinateAssignment() { assignX(); assignY(); var offset = getOffset(graph, needReverseOrder()); graph.nodes.forEach((v) { v.position = getPosition(v, offset); }); if (configuration.postStraighten) { postStraighten(); } } void assignX() { // Existing implementation remains the same final root = >[]; // each node points to its aligned neighbor in the layer below.; final align = >[]; final sink = >[]; final x = >[]; // minimal separation between the roots of different classes.; final shift = >[]; // the width of each block (max width of node in block); final blockWidth = >[]; for (var i = 0; i < 4; i++) { root.add({}); align.add({}); sink.add({}); shift.add({}); x.add({}); blockWidth.add({}); graph.nodes.forEach((n) { root[i][n] = n; align[i][n] = n; sink[i][n] = n; shift[i][n] = double.infinity; x[i][n] = double.negativeInfinity; blockWidth[i][n] = 0; }); } var separation = configuration.nodeSeparation; var vertical = isVertical(); for (var downward = 0; downward <= 1; downward++) { var isDownward = downward == 0; final type1Conflicts = markType1Conflicts(isDownward); for (var leftToRight = 0; leftToRight <= 1; leftToRight++) { final k = 2 * downward + leftToRight; var isLeftToRight = leftToRight == 0; verticalAlignment( root[k], align[k], type1Conflicts, isDownward, isLeftToRight); graph.nodes.forEach((v) { final r = root[k][v]!; blockWidth[k][r] = max( blockWidth[k][r]!, vertical ? v.width + separation : v.height); }); horizontalCompactation(align[k], root[k], sink[k], shift[k], blockWidth[k], x[k], isLeftToRight, isDownward, layers, separation); } } balance(x, blockWidth); } void balance(List> x, List> blockWidth) { final coordinates = {}; switch (configuration.coordinateAssignment) { case CoordinateAssignment.Average: var minWidth = double.infinity; var smallestWidthLayout = 0; final minArray = List.filled(4, 0.0); final maxArray = List.filled(4, 0.0); // Get the layout with the smallest width and set minimum and maximum value for each direction; for (var i = 0; i < 4; i++) { minArray[i] = double.infinity; maxArray[i] = 0; graph.nodes.forEach((v) { final bw = 0.5 * blockWidth[i][v]!; var xp = x[i][v]! - bw; if (xp < minArray[i]) { minArray[i] = xp; } xp = x[i][v]! + bw; if (xp > maxArray[i]) { maxArray[i] = xp; } }); final width = maxArray[i] - minArray[i]; if (width < minWidth) { minWidth = width; smallestWidthLayout = i; } } // Align the layouts to the one with the smallest width for (var layout = 0; layout < 4; layout++) { if (layout != smallestWidthLayout) { // Align the left to right layouts to the left border of the smallest layout var diff = 0.0; if (layout < 2) { diff = minArray[layout] - minArray[smallestWidthLayout]; } else { // Align the right to left layouts to the right border of the smallest layout diff = maxArray[layout] - maxArray[smallestWidthLayout]; } if (diff > 0) { x[layout].keys.forEach((n) { x[layout][n] = x[layout][n]! - diff; }); } else { x[layout].keys.forEach((n) { x[layout][n] = x[layout][n]! + diff; }); } } } // Get the average median of each coordinate var values = List.filled(4, 0.0); graph.nodes.forEach((n) { for (var i = 0; i < 4; i++) { values[i] = x[i][n]!; } values.sort(); var average = (values[1] + values[2]) * 0.5; coordinates[n] = average; }); break; case CoordinateAssignment.DownRight: graph.nodes.forEach((n) { coordinates[n] = x[0][n] ?? 0.0; }); break; case CoordinateAssignment.DownLeft: graph.nodes.forEach((n) { coordinates[n] = x[1][n] ?? 0.0; }); break; case CoordinateAssignment.UpRight: graph.nodes.forEach((n) { coordinates[n] = x[2][n] ?? 0.0; }); break; case CoordinateAssignment.UpLeft: graph.nodes.forEach((n) { coordinates[n] = x[3][n] ?? 0.0; }); break; } if (coordinates.isEmpty) { for (final node in graph.nodes) { coordinates[node] = 0.0; } } // Get the minimum coordinate value var minValue = coordinates.values.reduce(min); // Set left border to 0 if (minValue != 0) { coordinates.keys.forEach((n) { coordinates[n] = coordinates[n]! - minValue; }); } resolveOverlaps(coordinates); graph.nodes.forEach((v) { v.x = coordinates[v]!; }); } void resolveOverlaps(Map coordinates) { for (var layer in layers) { if (layer.isEmpty) { continue; } var layerNodes = List.from(layer); layerNodes.sort( (a, b) => nodeData[a]!.position.compareTo(nodeData[b]!.position)); var data = nodeData[layerNodes.first]; if (data?.layer != 0) { var leftCoordinate = 0.0; for (var i = 1; i < layerNodes.length; i++) { var currentNode = layerNodes[i]; if (!nodeData[currentNode]!.isDummy) { var previousNode = getPreviousNonDummyNode(layerNodes, i); if (previousNode != null) { leftCoordinate = coordinates[previousNode]! + previousNode.width + configuration.nodeSeparation; } else { leftCoordinate = 0.0; } if (leftCoordinate > coordinates[currentNode]!) { var adjustment = leftCoordinate - coordinates[currentNode]!; if (coordinates[currentNode] != null) { coordinates[currentNode] = coordinates[currentNode]! + adjustment; } } } } } } } Node? getPreviousNonDummyNode(List layerNodes, int currentIndex) { for (var i = currentIndex - 1; i >= 0; i--) { var previousNode = layerNodes[i]; if (!nodeData[previousNode]!.isDummy) { return previousNode; } } return null; } Map markType1Conflicts(bool downward) { if (layers.length >= 4) { int upper; int lower; // iteration bounds; int k1; // node position boundaries of closest inner segments; if (downward) { lower = 1; upper = layers.length - 2; } else { lower = layers.length - 1; upper = 2; } /*; * iterate level[2..h-2] in the given direction; * available 1 levels to h; */ for (var i = lower; downward ? i <= upper : i >= upper; i += downward ? 1 : -1) { var k0 = 0; var firstIndex = 0; // index of first node on layer; final currentLevel = layers[i]; final nextLevel = downward ? layers[i + 1] : layers[i - 1]; // for all nodes on next level; for (var l1 = 0; l1 < nextLevel.length; l1++) { final virtualTwin = virtualTwinNode(nextLevel[l1], downward); if (l1 == nextLevel.length - 1 || virtualTwin != null) { k1 = currentLevel.length - 1; if (virtualTwin != null) { k1 = positionOfNode(virtualTwin); } while (firstIndex <= l1) { final upperNeighbours = getAdjNodes(nextLevel[l1], downward); for (var currentNeighbour in upperNeighbours) { /*; * XXX< 0 in first iteration is still ok for indizes starting; * with 0 because no index can be smaller than 0; */ final currentNeighbourIndex = positionOfNode(currentNeighbour); if (currentNeighbourIndex < k0 || currentNeighbourIndex > k1) { type1Conflicts[l1] = currentNeighbourIndex; } } firstIndex++; } k0 = k1; } } } } return type1Conflicts; } void verticalAlignment(Map root, Map align, Map type1Conflicts, bool downward, bool leftToRight) { // for all Level; var layersa = downward ? layers : layers.reversed; for (var layer in layersa) { // As with layers, we need a reversed iterator for blocks for different directions var nodes = leftToRight ? layer : layer.reversed; // Do an initial placement for all blocks var r = leftToRight ? -1 : double.infinity; for (var v in nodes) { final adjNodes = getAdjNodes(v, downward); if (adjNodes.isNotEmpty) { var midLevelValue = adjNodes.length / 2; // Calculate medians final medians = adjNodes.length % 2 == 1 ? [adjNodes[midLevelValue.floor()]] : [ adjNodes[midLevelValue.toInt() - 1], adjNodes[midLevelValue.toInt()] ]; // For all median neighbours in direction of H for (var m in medians) { final posM = positionOfNode(m); // if segment (u,v) not marked by type1 conflicts AND ...; if (align[v] == v && type1Conflicts[positionOfNode(v)] != posM && (leftToRight ? r < posM : r > posM)) { align[m] = v; root[v] = root[m]; align[v] = root[v]; r = posM; } } } } } } void horizontalCompactation( Map align, Map root, Map sink, Map shift, Map blockWidth, Map x, bool leftToRight, bool downward, List> layers, int separation) { // calculate class relative coordinates for all roots; // 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) var layersa = leftToRight ? layers : layers.reversed; for (var layer in layersa) { // As with layers, we need a reversed iterator for blocks for different directions var nodes = downward ? layer : layer.reversed; // Do an initial placement for all blocks for (var v in nodes) { if (root[v] == v) { placeBlock(v, sink, shift, x, align, blockWidth, root, leftToRight, layers, separation); } } } var d = 0; var i = downward ? 0 : layers.length - 1; while (downward && i <= layers.length - 1 || !downward && i >= 0) { final currentLevel = layers[i]; final v = currentLevel[leftToRight ? 0 : currentLevel.length - 1]; if (v == sink[root[v]]) { final oldShift = shift[v]!; if (oldShift < double.infinity) { shift[v] = oldShift + d; d += oldShift.toInt(); } else { shift[v] = 0; } } i = downward ? i + 1 : i - 1; } // apply root coordinates for all aligned nodes; // (place block did this only for the roots)+; graph.nodes.forEach((v) { x[v] = x[root[v]]!; final shiftVal = shift[sink[root[v]]]!; if (shiftVal < double.infinity) { x[v] = x[v]! + shiftVal; // apply shift for each class; } }); } void placeBlock( Node v, Map sink, Map shift, Map x, Map align, Map blockWidth, Map root, bool leftToRight, List> layers, int separation) { if (x[v] == double.negativeInfinity) { x[v] = 0; var currentNode = v; try { do { // if not first node on layer; final hasPredecessor = leftToRight && positionOfNode(currentNode) > 0 || !leftToRight && positionOfNode(currentNode) < layers[getLayerIndex(currentNode)].length - 1; // print("Pred $hasPredecessor ${getLayerIndex(currentNode)>0} ${positionOfNode(currentNode)>0}"); if (hasPredecessor) { final pred = predecessor(currentNode, leftToRight); /* Get the root of u (proceeding all the way upwards in the block) */ final u = root[pred]!; /* Place the block of u recursively */ placeBlock(u, sink, shift, x, align, blockWidth, root, leftToRight, layers, separation); /* If v is its own sink yet, set its sink to the sink of u */ if (sink[v] == v) { sink[v] = sink[u]!; } /* If v and u have different sinks (i.e. they are in different classes), * shift the sink of u so that the two blocks are separated by the preferred gap */ var gap = separation + 0.5 * (blockWidth[u]! + blockWidth[v]!); if (sink[v] != sink[u]) { if (leftToRight) { shift[sink[u]!] = min(shift[sink[u]]!, x[v]! - x[u]! - gap); } else { shift[sink[u]!] = max(shift[sink[u]]!, x[v]! - x[u]! + gap); } } else { /* v and u have the same sink, i.e. they are in the same level. Make sure that v is separated from u by at least gap.*/ if (leftToRight) { x[v] = max(x[v]!, x[u]! + gap); } else { x[v] = min(x[v]!, x[u]! - gap); } } } currentNode = align[currentNode]!; } while (currentNode != v); } catch (e) { print(e); } } } List successorsOf(Node? node) { return nodeData[node]?.successorNodes ?? []; } List predecessorsOf(Node? node) { return nodeData[node]?.predecessorNodes ?? []; } List getAdjNodes(Node node, bool downward) { if (downward) { return predecessorsOf(node); } else { return successorsOf(node); } } // predecessor; Node? predecessor(Node? v, bool leftToRight) { final pos = positionOfNode(v); final rank = getLayerIndex(v); final level = layers[rank]; if (leftToRight && pos != 0 || !leftToRight && pos != level.length - 1) { return level[(leftToRight) ? pos - 1 : pos + 1]; } else { return null; } } Node? virtualTwinNode(Node node, bool downward) { if (!isLongEdgeDummy(node)) { return null; } final adjNodes = getAdjNodes(node, downward); return adjNodes.isEmpty ? null : adjNodes[0]; } // get node index in layer; int positionOfNode(Node? node) { return nodeData[node]?.position ?? -1; } int getLayerIndex(Node? node) { return nodeData[node]?.layer ?? -1; } bool isLongEdgeDummy(Node? v) { final successors = successorsOf(v); return nodeData[v!]!.isDummy && successors.length == 1 && nodeData[successors[0]]!.isDummy; } void assignY() { // compute y-coordinates; final k = layers.length; // assign y-coordinates var yPos = 0.0; var vertical = isVertical(); for (var i = 0; i < k; i++) { var level = layers[i]; var maxHeight = 0; level.forEach((node) { var h = nodeData[node]!.isDummy ? 0 : vertical ? node.height : node.width; if (h > maxHeight) { maxHeight = h.toInt(); } node.y = yPos; }); if (i < k - 1) { yPos += configuration.levelSeparation + maxHeight; } } } void denormalize() { // remove dummy's; for (var i = 1; i < layers.length - 1; i++) { final iterator = layers[i].iterator; while (iterator.moveNext()) { final current = iterator.current; if (nodeData[current]!.isDummy) { final predecessor = graph.predecessorsOf(current)[0]; final successor = graph.successorsOf(current)[0]; final bendPoints = edgeData[graph.getEdgeBetween(predecessor, current)!]!.bendPoints; if (bendPoints.isEmpty || !bendPoints.contains(current.x + predecessor.width / 2)) { bendPoints.add(predecessor.x + predecessor.width / 2); bendPoints.add(predecessor.y + predecessor.height / 2); bendPoints.add(current.x + predecessor.width / 2); bendPoints.add(current.y); } if (!nodeData[predecessor]!.isDummy) { bendPoints.add(current.x + predecessor.width / 2); } else { bendPoints.add(current.x); } bendPoints.add(current.y); if (nodeData[successor]!.isDummy) { bendPoints.add(successor.x + predecessor.width / 2); } else { bendPoints.add(successor.x + successor.width / 2); } bendPoints.add(successor.y + successor.height / 2); graph.removeEdgeFromPredecessor(predecessor, current); graph.removeEdgeFromPredecessor(current, successor); final edge = graph.addEdge(predecessor, successor); final sugiyamaEdgeData = SugiyamaEdgeData(); sugiyamaEdgeData.bendPoints = bendPoints; edgeData[edge] = sugiyamaEdgeData; // iterator.remove(); graph.removeNode(current); } } } } void restoreCycle() { graph.nodes.forEach((n) { final nodeInfo = nodeData[n]; if (nodeInfo == null || !nodeInfo.isReversed) { return; } for (final target in nodeInfo.reversed.toList()) { final existingEdge = graph.getEdgeBetween(target, n); if (existingEdge == null) { continue; } final existingData = this.edgeData.remove(existingEdge); final bendPoints = existingData?.bendPoints ?? []; graph.removeEdgeFromPredecessor(target, n); final edge = graph.addEdge(n, target); final restoredData = existingData ?? SugiyamaEdgeData(); restoredData.bendPoints = bendPoints; this.edgeData[edge] = restoredData; } nodeInfo.reversed.clear(); }); } void cycleRemoval() { switch (configuration.cycleRemovalStrategy) { case CycleRemovalStrategy.dfs: _dfsRecursiveCycleRemoval(); break; case CycleRemovalStrategy.greedy: _greedyCycleRemoval(); break; } } void _dfsRecursiveCycleRemoval() { graph.nodes.forEach((node) { dfs(node); }); } void _greedyCycleRemoval() { var greedyRemoval = GreedyCycleRemoval(graph); var feedbackArcs = greedyRemoval.getFeedbackArcs(); for (var edge in feedbackArcs) { var source = edge.source; var target = edge.destination; final storedData = edgeData.remove(edge); graph.removeEdge(edge); final reversedEdge = graph.addEdge(target, source); edgeData[reversedEdge] = storedData ?? SugiyamaEdgeData(); nodeData[source]!.reversed.add(target); } } void postStraighten() { if (!configuration.postStraighten) return; // Align dummy vertices to create straighter edges var dummyNodes = []; for (var layer in layers) { dummyNodes.addAll(layer.where((n) => nodeData[n]!.isDummy)); } // Group dummy nodes by their original edge var edgeGroups = >[]; var processed = {}; for (var dummy in dummyNodes) { if (processed.contains(dummy)) continue; var group = [dummy]; processed.add(dummy); // Find connected dummy nodes (same edge) _findConnectedDummies(dummy, group, processed, dummyNodes); if (group.length > 1) { edgeGroups.add(group); } } // Align each group vertically for (var group in edgeGroups) { group.sort((a, b) => nodeData[a]!.layer.compareTo(nodeData[b]!.layer)); // Calculate average x position var avgX = group.map((n) => n.x).reduce((a, b) => a + b) / group.length; // Set all dummy nodes to average x for (var node in group) { node.x = avgX; } } } void _findConnectedDummies( Node current, List group, Set processed, List dummies) { var successors = successorsOf(current); var predecessors = predecessorsOf(current); for (var target in successors) { if (dummies.contains(target) && !processed.contains(target)) { group.add(target); processed.add(target); _findConnectedDummies(target, group, processed, dummies); } } for (var source in predecessors) { if (dummies.contains(source) && !processed.contains(source)) { group.add(source); processed.add(source); _findConnectedDummies(source, group, processed, dummies); } } } Offset getOffset(Graph graph, bool needReverseOrder) { var offsetX = double.infinity; var offsetY = double.infinity; if (needReverseOrder) { offsetY = double.minPositive; } graph.nodes.forEach((node) { if (needReverseOrder) { offsetX = min(offsetX, node.x); offsetY = max(offsetY, node.y); } else { offsetX = min(offsetX, node.x); offsetY = min(offsetY, node.y); } }); return Offset(offsetX, offsetY); } Offset getPosition(Node node, Offset offset) { Offset finalOffset; switch (configuration.orientation) { case 1: finalOffset = Offset(node.x - offset.dx, node.y); break; case 2: finalOffset = Offset(node.x - offset.dx, offset.dy - node.y); break; case 3: finalOffset = Offset(node.y, node.x - offset.dx); break; case 4: finalOffset = Offset(offset.dy - node.y, node.x - offset.dx); break; default: finalOffset = Offset(0, 0); break; } return finalOffset; } @override void init(Graph? graph) { this.graph = copyGraph(graph!); reset(); initSugiyamaData(); cycleRemoval(); layerAssignment(); nodeOrdering(); //expensive operation coordinateAssignment(); //expensive operation // shiftCoordinates(shiftX, shiftY); //final graphSize = calculateGraphSize(this.graph); denormalize(); restoreCycle(); // shiftCoordinates(graph, shiftX, shiftY); } @override void setDimensions(double width, double height) { // graphWidth = width; // graphHeight = height; } } class AccumulatorTree { late List tree; late int firstIndex; late int treeSize; late int base; late int last; AccumulatorTree(int size) { firstIndex = 1; while (firstIndex < size) { firstIndex *= 2; } treeSize = 2 * firstIndex - 1; firstIndex--; base = size - 1; last = size - 1; tree = List.filled(treeSize, 0); } int crossCount(List southSequence) { var crossCount = 0; for (var k = 0; k < southSequence.length; k++) { var index = southSequence[k] + firstIndex; tree[index]++; while (index > 0) { if (index % 2 != 0) { crossCount += tree[index + 1]; } index = (index - 1) ~/ 2; tree[index]++; } } return crossCount; } } class GreedyCycleRemoval { final Graph graph; final Set feedbackArcs = {}; GreedyCycleRemoval(this.graph); Set getFeedbackArcs() { var copy = _copyGraph(); _removeCycles(copy); return feedbackArcs; } Graph _copyGraph() { var copy = Graph(); copy.addNodes(graph.nodes); copy.addEdges(graph.edges); return copy; } void _removeCycles(Graph g) { while (g.nodes.isNotEmpty) { // Remove sinks var sinks = g.nodes.where((n) => !g.hasSuccessor(n)).toList(); if (sinks.isNotEmpty) { for (var sink in sinks) { g.removeNode(sink); } continue; } // Remove sources var sources = g.nodes.where((n) => !g.hasPredecessor(n)).toList(); if (sources.isNotEmpty) { for (var source in sources) { g.removeNode(source); } continue; } // Choose nodes with highest out-degree - in-degree var best = g.nodes.reduce((a, b) { var aDiff = g.getOutEdges(a).length - g.getInEdges(a).length; var bDiff = g.getOutEdges(b).length - g.getInEdges(b).length; return aDiff > bDiff ? a : b; }); // Add incoming edges to feedback arcs feedbackArcs.addAll(g.getInEdges(best)); g.removeNode(best); } } } ================================================ FILE: lib/layered/SugiyamaConfiguration.dart ================================================ part of graphview; class SugiyamaConfiguration { static const ORIENTATION_TOP_BOTTOM = 1; static const ORIENTATION_BOTTOM_TOP = 2; static const ORIENTATION_LEFT_RIGHT = 3; static const ORIENTATION_RIGHT_LEFT = 4; static const DEFAULT_ORIENTATION = 1; static const int DEFAULT_ITERATIONS = 10; static const int X_SEPARATION = 100; static const int Y_SEPARATION = 100; int levelSeparation = Y_SEPARATION; int nodeSeparation = X_SEPARATION; int orientation = DEFAULT_ORIENTATION; int iterations = DEFAULT_ITERATIONS; BendPointShape bendPointShape = SharpBendPointShape(); CoordinateAssignment coordinateAssignment = CoordinateAssignment.Average; LayeringStrategy layeringStrategy = LayeringStrategy.topDown; CrossMinimizationStrategy crossMinimizationStrategy = CrossMinimizationStrategy.simple; CycleRemovalStrategy cycleRemovalStrategy = CycleRemovalStrategy.greedy; bool postStraighten = true; bool addTriangleToEdge = true; int getLevelSeparation() { return levelSeparation; } int getNodeSeparation() { return nodeSeparation; } int getOrientation() { return orientation; } } enum CoordinateAssignment { DownRight, // 0 DownLeft, // 1 UpRight, // 2 UpLeft, // 3 Average, // 4 } enum LayeringStrategy { topDown, longestPath, coffmanGraham, networkSimplex } enum CrossMinimizationStrategy { simple, accumulatorTree } enum CycleRemovalStrategy { dfs, greedy, } abstract class BendPointShape {} class SharpBendPointShape extends BendPointShape {} class MaxCurvedBendPointShape extends BendPointShape {} class CurvedBendPointShape extends BendPointShape { final double curveLength; CurvedBendPointShape({ required this.curveLength, }); } ================================================ FILE: lib/layered/SugiyamaEdgeData.dart ================================================ part of graphview; class SugiyamaEdgeData { List bendPoints = []; } ================================================ FILE: lib/layered/SugiyamaEdgeRenderer.dart ================================================ part of graphview; class SugiyamaEdgeRenderer extends ArrowEdgeRenderer { Map nodeData; Map edgeData; BendPointShape bendPointShape; bool addTriangleToEdge; var path = Path(); SugiyamaEdgeRenderer(this.nodeData, this.edgeData, this.bendPointShape, this.addTriangleToEdge); bool hasBendEdges(Edge edge) => edgeData.containsKey(edge) && edgeData[edge]!.bendPoints.isNotEmpty; void render(Canvas canvas, Graph graph, Paint paint) { graph.edges.forEach((edge) { renderEdge(canvas, edge, paint); }); } @override void renderEdge(Canvas canvas, Edge edge, Paint paint) { var trianglePaint = Paint() ..color = paint.color ..style = PaintingStyle.fill; Paint? edgeTrianglePaint; if (edge.paint != null) { edgeTrianglePaint = Paint() ..color = edge.paint?.color ?? paint.color ..style = PaintingStyle.fill; } var currentPaint = (edge.paint ?? paint) ..style = PaintingStyle.stroke; if (edge.source == edge.destination) { final loopResult = buildSelfLoopPath( edge, arrowLength: addTriangleToEdge ? ARROW_LENGTH : 0.0, ); if (loopResult != null) { final lineType = nodeData[edge.destination]?.lineType; drawStyledPath(canvas, loopResult.path, currentPaint, lineType: lineType); if (addTriangleToEdge) { final triangleCentroid = drawTriangle( canvas, edgeTrianglePaint ?? trianglePaint, loopResult.arrowBase.dx, loopResult.arrowBase.dy, loopResult.arrowTip.dx, loopResult.arrowTip.dy, ); drawStyledLine( canvas, loopResult.arrowBase, triangleCentroid, currentPaint, lineType: lineType, ); } return; } } if (hasBendEdges(edge)) { _renderEdgeWithBendPoints(canvas, edge, currentPaint, edgeTrianglePaint ?? trianglePaint); } else { _renderStraightEdge(canvas, edge, currentPaint, edgeTrianglePaint ?? trianglePaint); } } void _renderEdgeWithBendPoints(Canvas canvas, Edge edge, Paint currentPaint, Paint trianglePaint) { final source = edge.source; final destination = edge.destination; var bendPoints = edgeData[edge]!.bendPoints; var sourceCenter = _getNodeCenter(source); // Calculate the transition/offset from the original bend point to animated position final transitionDx = sourceCenter.dx - bendPoints[0]; final transitionDy = sourceCenter.dy - bendPoints[1]; path.reset(); path.moveTo(sourceCenter.dx, sourceCenter.dy); final bendPointsWithoutDuplication = []; for (var i = 0; i < bendPoints.length; i += 2) { final isLastPoint = i == bendPoints.length - 2; // Apply the same transition to all bend points final x = bendPoints[i] + transitionDx; final y = bendPoints[i + 1] + transitionDy; final x2 = isLastPoint ? -1 : bendPoints[i + 2] + transitionDx; final y2 = isLastPoint ? -1 : bendPoints[i + 3] + transitionDy; if (x == x2 && y == y2) { // Skip when two consecutive points are identical // because drawing a line between would be redundant in this case. continue; } bendPointsWithoutDuplication.add(Offset(x, y)); } if (bendPointShape is MaxCurvedBendPointShape) { _drawMaxCurvedBendPointsEdge(bendPointsWithoutDuplication); } else if (bendPointShape is CurvedBendPointShape) { final shape = bendPointShape as CurvedBendPointShape; _drawCurvedBendPointsEdge(bendPointsWithoutDuplication, shape.curveLength); } else { _drawSharpBendPointsEdge(bendPointsWithoutDuplication); } var descOffset = getNodePosition(destination); var stopX = descOffset.dx + destination.width * 0.5; var stopY = descOffset.dy + destination.height * 0.5; if (addTriangleToEdge) { var clippedLine = []; final size = bendPoints.length; if (nodeData[source]!.isReversed) { clippedLine = clipLineEnd(bendPoints[2], bendPoints[3], stopX, stopY, destination.x, destination.y, destination.width, destination.height); } else { clippedLine = clipLineEnd(bendPoints[size - 4], bendPoints[size - 3], stopX, stopY, descOffset.dx, descOffset.dy, destination.width, destination.height); } final triangleCentroid = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]); path.lineTo(triangleCentroid.dx, triangleCentroid.dy); } else { path.lineTo(stopX, stopY); } canvas.drawPath(path, currentPaint); } void _renderStraightEdge(Canvas canvas, Edge edge, Paint currentPaint, Paint trianglePaint) { final source = edge.source; final destination = edge.destination; final sourceCenter = _getNodeCenter(source); var destCenter = _getNodeCenter(destination); if (addTriangleToEdge) { final clippedLine = clipLineEnd(sourceCenter.dx, sourceCenter.dy, destCenter.dx, destCenter.dy, destination.x, destination.y, destination.width, destination.height); destCenter = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]); } // Draw the line with appropriate line type using the base class method final lineType = nodeData[destination]?.lineType; drawStyledLine(canvas, sourceCenter, destCenter, currentPaint, lineType: lineType); } void _drawSharpBendPointsEdge(List bendPoints) { for (var i = 1; i < bendPoints.length - 1; i++) { path.lineTo(bendPoints[i].dx, bendPoints[i].dy); } } void _drawMaxCurvedBendPointsEdge(List bendPoints) { for (var i = 1; i < bendPoints.length - 1; i++) { final nextNode = bendPoints[i]; final afterNextNode = bendPoints[i + 1]; final curveEndPoint = Offset((nextNode.dx + afterNextNode.dx) / 2, (nextNode.dy + afterNextNode.dy) / 2); path.quadraticBezierTo(nextNode.dx, nextNode.dy, curveEndPoint.dx, curveEndPoint.dy); } } void _drawCurvedBendPointsEdge(List bendPoints, double curveLength) { for (var i = 1; i < bendPoints.length - 1; i++) { final previousNode = i == 1 ? null : bendPoints[i - 2]; final currentNode = bendPoints[i - 1]; final nextNode = bendPoints[i]; final afterNextNode = bendPoints[i + 1]; final arcStartPointRadians = atan2(nextNode.dy - currentNode.dy, nextNode.dx - currentNode.dx); final arcStartPoint = nextNode - Offset.fromDirection(arcStartPointRadians, curveLength); final arcEndPointRadians = atan2(nextNode.dy - afterNextNode.dy, nextNode.dx - afterNextNode.dx); final arcEndPoint = nextNode - Offset.fromDirection(arcEndPointRadians, curveLength); if (previousNode != null && ((currentNode.dx == nextNode.dx && nextNode.dx == afterNextNode.dx) || (currentNode.dy == nextNode.dy && nextNode.dy == afterNextNode.dy))) { path.lineTo(nextNode.dx, nextNode.dy); } else { path.lineTo(arcStartPoint.dx, arcStartPoint.dy); path.quadraticBezierTo(nextNode.dx, nextNode.dy, arcEndPoint.dx, arcEndPoint.dy); } } } } ================================================ FILE: lib/layered/SugiyamaNodeData.dart ================================================ part of graphview; class SugiyamaNodeData { Set reversed = {}; bool isDummy = false; int median = -1; int layer = -1; int position = -1; List predecessorNodes = []; List successorNodes = []; LineType lineType; SugiyamaNodeData(this.lineType); bool get isReversed => reversed.isNotEmpty; @override String toString() { return 'SugiyamaNodeData{reversed: $reversed, isDummy: $isDummy, median: $median, layer: $layer, position: $position, lineType: $lineType}'; } } ================================================ FILE: lib/mindmap/MindMapAlgorithm.dart ================================================ part of graphview; enum MindmapSide { LEFT, RIGHT, ROOT } class _SideData { MindmapSide side = MindmapSide.ROOT; } class MindmapAlgorithm extends BuchheimWalkerAlgorithm { final Map _side = {}; MindmapAlgorithm(BuchheimWalkerConfiguration config, EdgeRenderer? renderer) : super(config, renderer ?? MindmapEdgeRenderer(config)); @override void initData(Graph? graph) { super.initData(graph); _side.clear(); graph?.nodes.forEach((n) => _side[n] = _SideData()); } @override Size run(Graph? graph, double shiftX, double shiftY) { initData(graph); _detectCycles(graph!); final root = getFirstNode(graph); _applyBuchheimWalkerSpacing(graph, root); _createMindmapLayout(graph, root); shiftCoordinates(graph, shiftX, shiftY); return graph.calculateGraphSize(); } void _markSubtree(Node node, MindmapSide side) { final d = _side[node]!; d.side = side; for (final child in successorsOf(node)) { _markSubtree(child, side); } } void _applyBuchheimWalkerSpacing(Graph graph, Node root) { // Apply the standard Buchheim-Walker algorithm to get proper spacing // This gives us optimal spacing relationships between all nodes firstWalk(graph, root, 0, 0); secondWalk(graph, root, 0.0); positionNodes(graph); // At this point, all nodes have positions with proper spacing, // but they're in a traditional tree layout. We'll reposition them next. } void _createMindmapLayout(Graph graph, Node root) { final vertical = isVertical(); final rootPos = vertical ? root.x : root.y; // Mark subtrees and position nodes in one pass for (final child in successorsOf(root)) { final childPos = vertical ? child.x : child.y; final side = childPos < rootPos ? MindmapSide.LEFT : MindmapSide.RIGHT; _markSubtree(child, side); } // Position all non-root nodes for (final node in graph.nodes) { final info = nodeData[node]!; if (info.depth == 0) continue; // Skip root final sideMultiplier = _side[node]!.side == MindmapSide.LEFT ? -1 : 1; final secondary = vertical ? node.x : node.y; final distanceFromRoot = info.depth * configuration.levelSeparation + (vertical ? maxNodeWidth : maxNodeHeight) / 2; if (vertical) { node.position = Offset( secondary - root.x * 0.5 * sideMultiplier, sideMultiplier * distanceFromRoot ); } else { node.position = Offset( sideMultiplier * distanceFromRoot, secondary - root.y * 0.5 * sideMultiplier ); } } // Adjust root and apply final transformations if (needReverseOrder()) { if (vertical) { root.y = 0.0; } else { root.x = 0.0; } } for (final node in graph.nodes) { final info = nodeData[node]!; if (info.depth == 0) { if (vertical) { node.x = node.x * 0.5; } else { node.y = node.y * 0.5; } } else { if (vertical) { node.x = node.x - root.x; } else { node.y = node.y - root.y; } } } } } ================================================ FILE: lib/mindmap/MindmapEdgeRenderer.dart ================================================ part of graphview; class MindmapEdgeRenderer extends TreeEdgeRenderer { MindmapEdgeRenderer(BuchheimWalkerConfiguration configuration) : super(configuration); @override int getEffectiveOrientation(dynamic node, dynamic child) { var orientation = configuration.orientation; if (child.y < 0) { if (configuration.orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) { orientation = BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP; } else { // orientation = BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM; } } else if (child.x < 0) { if (configuration.orientation == BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT) { orientation = BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT; } else { orientation = BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT; } } return orientation; } } ================================================ FILE: lib/tree/BaloonLayoutAlgorithm.dart ================================================ part of graphview; // Polar coordinate representation class PolarPoint { final double theta; // angle in radians final double radius; const PolarPoint(this.theta, this.radius); static const PolarPoint origin = PolarPoint(0, 0); // Convert polar coordinates to cartesian Offset toCartesian() { final x = radius * cos(theta); final y = radius * sin(theta); return Offset(x, y); } // Create polar point from angle and radius static PolarPoint of(double theta, double radius) { return PolarPoint(theta, radius); } @override String toString() => 'PolarPoint(theta: $theta, radius: $radius)'; } class BalloonLayoutAlgorithm extends Algorithm { late BuchheimWalkerConfiguration config; final Map nodeData = {}; final Map polarLocations = {}; final Map radii = {}; BalloonLayoutAlgorithm(this.config, EdgeRenderer? renderer) { this.renderer = renderer ?? ArrowEdgeRenderer(); } @override Size run(Graph? graph, double shiftX, double shiftY) { if (graph == null || graph.nodes.isEmpty) { return Size.zero; } nodeData.clear(); polarLocations.clear(); radii.clear(); // Handle single node case if (graph.nodes.length == 1) { final node = graph.nodes.first; node.position = Offset(shiftX + 100, shiftY + 100); return Size(200, 200); } _initializeData(graph); final roots = _findRoots(graph); if (roots.isEmpty) { final spanningTree = _createSpanningTree(graph); return _layoutSpanningTree(spanningTree, shiftX, shiftY); } _setRootPolars(graph, roots); _shiftCoordinates(graph, shiftX, shiftY); return graph.calculateGraphSize(); } void _initializeData(Graph graph) { // Initialize node data for (final node in graph.nodes) { nodeData[node] = TreeLayoutNodeData(); } // Build tree structure from edges for (final edge in graph.edges) { final source = edge.source; final target = edge.destination; nodeData[source]!.successorNodes.add(target); nodeData[target]!.parent = source; } } List _findRoots(Graph graph) { return graph.nodes.where((node) { return nodeData[node]!.parent == null; }).toList(); } void _setRootPolars(Graph graph, List roots) { final center = _getGraphCenter(graph); final width = graph.calculateGraphBounds().width; final defaultRadius = max(width / 2, 200.0); if (roots.length == 1) { // Single tree - place root at center final root = roots.first; _setRootPolar(root, center); final children = successorsOf(root); _setPolars(children, center, 0, defaultRadius, {}); } else if (roots.length > 1) { // Multiple trees - arrange roots in circle _setPolars(roots, center, 0, defaultRadius, {}); } } void _setRootPolar(Node root, Offset center) { polarLocations[root] = PolarPoint.origin; root.position = center; } void _setPolars(List nodes, Offset parentLocation, double angleToParent, double parentRadius, Set seen) { final childCount = nodes.length; if (childCount == 0) return; // Calculate child placement parameters final angle = max(0, pi / 2 * (1 - 2.0 / childCount)); final childRadius = parentRadius * cos(angle) / (1 + cos(angle)); final radius = parentRadius - childRadius; // Angle between children final angleBetweenKids = 2 * pi / childCount; final offset = angleBetweenKids / 2 - angleToParent; for (var i = 0; i < nodes.length; i++) { final child = nodes[i]; if (seen.contains(child)) continue; // Calculate angle for this child final theta = i * angleBetweenKids + offset; // Store radius and polar coordinates radii[child] = childRadius; final polarPoint = PolarPoint.of(theta, radius); polarLocations[child] = polarPoint; // Convert to cartesian and position node final cartesian = polarPoint.toCartesian(); final position = Offset( parentLocation.dx + cartesian.dx, parentLocation.dy + cartesian.dy, ); child.position = position; final newAngleToParent = atan2( parentLocation.dy - position.dy, parentLocation.dx - position.dx, ); final grandChildren = successorsOf(child) .where((node) => !seen.contains(node)) .toList(); if (grandChildren.isNotEmpty) { final newSeen = Set.from(seen); newSeen.add(child); // Add current child to prevent cycles _setPolars(grandChildren, position, newAngleToParent, childRadius, newSeen); } } } Offset _getGraphCenter(Graph graph) { final bounds = graph.calculateGraphBounds(); return Offset( bounds.left + bounds.width / 2, bounds.top + bounds.height / 2, ); } void _shiftCoordinates(Graph graph, double shiftX, double shiftY) { for (final node in graph.nodes) { node.position = Offset(node.x + shiftX, node.y + shiftY); } } Graph _createSpanningTree(Graph graph) { final visited = {}; final spanningEdges = []; if (graph.nodes.isNotEmpty) { final startNode = graph.nodes.first; final queue = [startNode]; visited.add(startNode); while (queue.isNotEmpty) { final current = queue.removeAt(0); for (final edge in graph.edges) { Node? neighbor; if (edge.source == current && !visited.contains(edge.destination)) { neighbor = edge.destination; spanningEdges.add(edge); } else if (edge.destination == current && !visited.contains(edge.source)) { neighbor = edge.source; spanningEdges.add(Edge(current, edge.source)); } if (neighbor != null && !visited.contains(neighbor)) { visited.add(neighbor); queue.add(neighbor); } } } } return Graph()..addEdges(spanningEdges); } Size _layoutSpanningTree(Graph spanningTree, double shiftX, double shiftY) { nodeData.clear(); polarLocations.clear(); radii.clear(); _initializeData(spanningTree); final roots = _findRoots(spanningTree); if (roots.isEmpty && spanningTree.nodes.isNotEmpty) { final fakeRoot = spanningTree.nodes.first; _setRootPolars(spanningTree, [fakeRoot]); } else { _setRootPolars(spanningTree, roots); } _shiftCoordinates(spanningTree, shiftX, shiftY); return spanningTree.calculateGraphSize(); } List successorsOf(Node? node) { return nodeData[node]!.successorNodes; } PolarPoint? getPolarLocation(Node node) { return polarLocations[node]; } double? getRadius(Node node) { return radii[node]; } Map getRadii() { return Map.from(radii); } Map getPolarLocations() { return Map.from(polarLocations); } @override void init(Graph? graph) { // Implementation can be added if needed } @override void setDimensions(double width, double height) { // Implementation can be added if needed } @override EdgeRenderer? renderer; } ================================================ FILE: lib/tree/BuchheimWalkerAlgorithm.dart ================================================ part of graphview; class BuchheimWalkerAlgorithm extends Algorithm { Map nodeData = {}; double minNodeHeight = double.infinity; double minNodeWidth = double.infinity; double maxNodeWidth = double.negativeInfinity; double maxNodeHeight = double.negativeInfinity; BuchheimWalkerConfiguration configuration; bool isVertical() { var orientation = configuration.orientation; return orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM || orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP; } bool needReverseOrder() { var orientation = configuration.orientation; return orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP || orientation == BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT; } void _detectCycles(Graph graph) { var visiting = {}; bool hasCycle(Node node) { if (visiting.contains(node)) return true; visiting.add(node); var cycleFound = successorsOf(node).any(hasCycle); visiting.remove(node); return cycleFound; } if (graph.nodes.any(hasCycle)) { throw Exception('Cyclic dependency detected - tree structure required'); } } @override Size run(Graph? graph, double shiftX, double shiftY) { if (graph == null) return Size.zero; nodeData.clear(); if (graph.nodes.length == 1) { final node = graph.nodes.first; node.position = Offset(shiftX, shiftY); return node.size * 2; } initData(graph); _detectCycles(graph); var firstNode = getFirstNode(graph); firstWalk(graph, firstNode, 0, 0); secondWalk(graph, firstNode, 0.0); checkUnconnectedNotes(graph); positionNodes(graph); shiftCoordinates(graph, shiftX, shiftY); return graph.calculateGraphSize(); } Node getFirstNode(Graph graph) => graph.nodes.firstWhere((element) => !hasPredecessor(element)); void checkUnconnectedNotes(Graph graph) { graph.nodes.forEach((element) { if (getNodeData(element) == null) { if (!kReleaseMode) { print('$element is not connected to primary ancestor'); } } }); } int compare(int x, int y) { return x < y ? -1 : (x == y ? 0 : 1); } void firstWalk(Graph graph, Node node, int depth, int number) { final nodeData = getNodeData(node)!; nodeData.depth = depth; nodeData.number = number; minNodeHeight = min(minNodeHeight, node.height); minNodeWidth = min(minNodeWidth, node.width); maxNodeWidth = max(maxNodeWidth, node.width); maxNodeHeight = max(maxNodeHeight, node.height); if (isLeaf(graph, node)) { // if the node has no left sibling, prelim(node) should be set to 0, but we don't have to set it // here, because it's already initialized with 0 if (hasLeftSibling(graph, node)) { final leftSibling = getLeftSibling(graph, node); nodeData.prelim = getPrelim(leftSibling) + getSpacing(graph, leftSibling, node); } } else { final leftMost = getLeftMostChild(graph, node); final rightMost = getRightMostChild(graph, node); var defaultAncestor = leftMost; Node? next = leftMost; var i = 1; while (next != null) { firstWalk(graph, next, depth + 1, i++); defaultAncestor = apportion(graph, next, defaultAncestor); next = getRightSibling(graph, next); } executeShifts(graph, node); var vertical = isVertical(); var midPoint = 0.5 * ((getPrelim(leftMost) + getPrelim(rightMost) + (vertical ? rightMost!.width : rightMost!.height)) - (vertical ? node.width : node.height)); if (hasLeftSibling(graph, node)) { final leftSibling = getLeftSibling(graph, node); nodeData.prelim = getPrelim(leftSibling) + getSpacing(graph, leftSibling, node); nodeData.modifier = nodeData.prelim - midPoint; } else { nodeData.prelim = midPoint; } } } void secondWalk(Graph graph, Node node, double modifier) { var nodeData = getNodeData(node)!; var depth = nodeData.depth; var vertical = isVertical(); node.position = Offset((nodeData.prelim + modifier), (depth * (vertical ? minNodeHeight : minNodeWidth) + depth * configuration.levelSeparation).ceilToDouble()); graph.successorsOf(node).forEach((w) { secondWalk(graph, w, modifier + nodeData.modifier); }); } void executeShifts(Graph graph, Node node) { var shift = 0.0; var change = 0.0; var w = getRightMostChild(graph, node); while (w != null) { final nodeData = getNodeData(w) ?? BuchheimWalkerNodeData(); nodeData.prelim = nodeData.prelim + shift; nodeData.modifier = nodeData.modifier + shift; change += nodeData.change; shift += nodeData.shift + change; w = getLeftSibling(graph, w); } } Node apportion(Graph graph, Node node, Node defaultAncestor) { var ancestor = defaultAncestor; if (hasLeftSibling(graph, node)) { var leftSibling = getLeftSibling(graph, node); Node? vop = node; Node? vom = getLeftMostChild(graph, predecessorsOf(node).first); var sip = getModifier(node); var sop = getModifier(node); var sim = getModifier(leftSibling); var som = getModifier(vom); var nextRight = this.nextRight(graph, leftSibling); Node? nextLeft; for (nextLeft = this.nextLeft(graph, node); nextRight != null && nextLeft != null; nextLeft = this.nextLeft(graph, nextLeft)) { vom = this.nextLeft(graph, vom); vop = this.nextRight(graph, vop); setAncestor(vop, node); var shift = getPrelim(nextRight) + sim - (getPrelim(nextLeft) + sip) + getSpacing(graph, nextRight, node); if (shift > 0) { moveSubtree(this.ancestor(graph, nextRight, node, ancestor), node, shift); sip += shift; sop += shift; } sim += getModifier(nextRight); sip += getModifier(nextLeft); som += getModifier(vom); sop += getModifier(vop); nextRight = this.nextRight(graph, nextRight); } if (nextRight != null && this.nextRight(graph, vop) == null) { setThread(vop, nextRight); setModifier(vop, getModifier(vop) + sim - sop); } if (nextLeft != null && this.nextLeft(graph, vom) == null) { setThread(vom, nextLeft); setModifier(vom, getModifier(vom) + sip - som); ancestor = node; } } return ancestor; } void setAncestor(Node? v, Node ancestor) { getNodeData(v)?.ancestor = ancestor; } void setModifier(Node? v, double modifier) { getNodeData(v)?.modifier = modifier; } void setThread(Node? v, Node thread) { getNodeData(v)?.thread = thread; } double getPrelim(Node? v) { return getNodeData(v)?.prelim ?? 0; } double getModifier(Node? vip) { return getNodeData(vip)?.modifier ?? 0; } void moveSubtree(Node? wm, Node wp, double shift) { var wpNodeData = getNodeData(wp)!; var wmNodeData = getNodeData(wm)!; var subtrees = wpNodeData.number - wmNodeData.number; wpNodeData.change = (wpNodeData.change - shift / subtrees); wpNodeData.shift = (wpNodeData.shift + shift); wmNodeData.change = (wmNodeData.change + shift / subtrees); wpNodeData.prelim = (wpNodeData.prelim + shift); wpNodeData.modifier = (wpNodeData.modifier + shift); } Node? ancestor(Graph graph, Node vim, Node node, Node defaultAncestor) { var vipNodeData = getNodeData(vim)!; return predecessorsOf(vipNodeData.ancestor).first == predecessorsOf(node).first ? vipNodeData.ancestor : defaultAncestor; } Node? nextRight(Graph graph, Node? node) { return graph.hasSuccessor(node) ? getRightMostChild(graph, node) : getNodeData(node)?.thread; } Node? nextLeft(Graph graph, Node? node) { return hasSuccessor(node) ? getLeftMostChild(graph, node) : getNodeData(node)?.thread; } num getSpacing(Graph graph, Node? leftNode, Node rightNode) { var separation = configuration.getSubtreeSeparation(); if (isSibling(graph, leftNode, rightNode)) { separation = configuration.getSiblingSeparation(); } num length = isVertical() ? leftNode!.width : leftNode!.height; return separation + length; } bool isSibling(Graph graph, Node? leftNode, Node rightNode) { var leftParent = predecessorsOf(leftNode).first; return successorsOf(leftParent).contains(rightNode); } bool isLeaf(Graph graph, Node node) { return successorsOf(node).isEmpty; } Node? getLeftSibling(Graph graph, Node node) { if (!hasLeftSibling(graph, node)) { return null; } else { var parent = predecessorsOf(node).first; var children = successorsOf(parent); var nodeIndex = children.indexOf(node); return children[nodeIndex - 1]; } } bool hasLeftSibling(Graph graph, Node node) { var parents = predecessorsOf(node); if (parents.isEmpty) { return false; } else { var parent = parents.first; var nodeIndex = successorsOf(parent).indexOf(node); return nodeIndex > 0; } } Node? getRightSibling(Graph graph, Node node) { if (!hasRightSibling(graph, node)) { return null; } else { var parent = predecessorsOf(node).first; var children = successorsOf(parent); var nodeIndex = children.indexOf(node); return children[nodeIndex + 1]; } } bool hasRightSibling(Graph graph, Node node) { var parents = predecessorsOf(node); if (parents.isEmpty) { return false; } else { var parent = parents[0]; List children = successorsOf(parent); var nodeIndex = children.indexOf(node); return nodeIndex < children.length - 1; } } Node getLeftMostChild(Graph graph, Node? node) { return successorsOf(node).first; } Node? getRightMostChild(Graph graph, Node? node) { var children = successorsOf(node); return children.isEmpty ? null : children.last; } void positionNodes(Graph graph) { var doesNeedReverseOrder = needReverseOrder(); var offset = getOffset(graph, doesNeedReverseOrder); var nodes = sortByLevel(graph, doesNeedReverseOrder); var firstLevel = getNodeData(nodes.first)?.depth ?? 0; var localMaxSize = findMaxSize(filterByLevel(nodes, firstLevel)); var currentLevel = doesNeedReverseOrder ? firstLevel : 0; var globalPadding = 0.0; var localPadding = 0.0; nodes.forEach((node) { final depth = getNodeData(node)?.depth ?? 0; if (depth != currentLevel) { if (doesNeedReverseOrder) { globalPadding -= localPadding; } else { globalPadding += localPadding; } localPadding = 0.0; currentLevel = depth; localMaxSize = findMaxSize(filterByLevel(nodes, currentLevel)); } final height = node.height; final width = node.width; switch (configuration.orientation) { case BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM: if (height > minNodeHeight) { final diff = height - minNodeHeight; localPadding = max(localPadding, diff); } break; case BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP: if (height < localMaxSize.height) { var diff = localMaxSize.height - height; node.position -= Offset(0, diff); localPadding = max(localPadding, diff); } break; case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT: if (width > minNodeWidth) { final diff = width - minNodeWidth; localPadding = max(localPadding, diff); } break; case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT: if (width < localMaxSize.width) { var diff = localMaxSize.width - width; node.position -= Offset(0, diff); localPadding = max(localPadding, diff); } } node.position = getPosition(node, globalPadding, offset); }); } void shiftCoordinates(Graph graph, double shiftX, double shiftY) { graph.nodes.forEach((node) { node.position = (Offset(node.x + shiftX, node.y + shiftY)); }); } Size findMaxSize(List nodes) { var width = double.negativeInfinity; var height = double.negativeInfinity; nodes.forEach((node) { width = max(width, node.width); height = max(height, node.height); }); return Size(width, height); } Offset getOffset(Graph graph, bool needReverseOrder) { var offsetX = double.infinity; var offsetY = double.infinity; if (needReverseOrder) { offsetY = double.minPositive; } graph.nodes.forEach((node) { if (needReverseOrder) { offsetX = min(offsetX, node.x); offsetY = max(offsetY, node.y); } else { offsetX = min(offsetX, node.x); offsetY = min(offsetY, node.y); } }); return Offset(offsetX, offsetY); } Offset getPosition(Node node, double globalPadding, Offset offset) { Offset finalOffset; switch (configuration.orientation) { case 1: finalOffset = Offset(node.x - offset.dx, node.y + globalPadding); break; case 2: finalOffset = Offset(node.x - offset.dx, offset.dy - node.y - globalPadding); break; case 3: finalOffset = Offset(node.y + globalPadding, node.x - offset.dx); break; case 4: finalOffset = Offset(offset.dy - node.y - globalPadding, node.x - offset.dx); break; default: finalOffset = Offset(0, 0); break; } return finalOffset; } List sortByLevel(Graph graph, bool descending) { var nodes = [...graph.nodes]; if (descending) { nodes.reversed; } nodes.sort((data1, data2) => compare(getNodeData(data1)?.depth ?? 0, getNodeData(data2)?.depth ?? 0)); return nodes; } List filterByLevel(List nodes, int? level) { return nodes.where((node) => getNodeData(node)?.depth == level).toList(); } @override EdgeRenderer? renderer; void initData(Graph? graph) { graph?.nodes.forEach((node) { var nodeDatab = BuchheimWalkerNodeData(); nodeDatab.ancestor = node; nodeData[node] = nodeDatab; }); graph?.edges.forEach((element) { nodeData[element.source]?.successorNodes.add(element.destination); nodeData[element.destination]?.predecessorNodes.add(element.source); }); } BuchheimWalkerNodeData? getNodeData(Node? node) { return node == null ? null : nodeData[node]; } bool hasSuccessor(Node? node) => successorsOf(node).isNotEmpty; bool hasPredecessor(Node node) => predecessorsOf(node).isNotEmpty; List successorsOf(Node? node) { return nodeData[node]?.successorNodes ?? []; } List predecessorsOf(Node? node) { return nodeData[node]?.predecessorNodes ?? []; } BuchheimWalkerAlgorithm(this.configuration, EdgeRenderer? renderer) { this.renderer = renderer ?? TreeEdgeRenderer(configuration); } @override void init(Graph? graph) { var firstNode = getFirstNode(graph!); firstWalk(graph, firstNode, 0, 0); secondWalk(graph, firstNode, 0.0); checkUnconnectedNotes(graph); positionNodes(graph); // shiftCoordinates(graph, shiftX, shiftY); } @override void setDimensions(double width, double height) { // graphWidth = width; // graphHeight = height; } } ================================================ FILE: lib/tree/BuchheimWalkerConfiguration.dart ================================================ part of graphview; class BuchheimWalkerConfiguration { int siblingSeparation = DEFAULT_SIBLING_SEPARATION; int levelSeparation = DEFAULT_LEVEL_SEPARATION; int subtreeSeparation = DEFAULT_SUBTREE_SEPARATION; int orientation = DEFAULT_ORIENTATION; static const ORIENTATION_TOP_BOTTOM = 1; static const ORIENTATION_BOTTOM_TOP = 2; static const ORIENTATION_LEFT_RIGHT = 3; static const ORIENTATION_RIGHT_LEFT = 4; static const DEFAULT_SIBLING_SEPARATION = 100; static const DEFAULT_SUBTREE_SEPARATION = 100; static const DEFAULT_LEVEL_SEPARATION = 100; static const DEFAULT_ORIENTATION = 1; bool useCurvedConnections = true; int getSiblingSeparation() { return siblingSeparation; } int getLevelSeparation() { return levelSeparation; } int getSubtreeSeparation() { return subtreeSeparation; } BuchheimWalkerConfiguration( {this.siblingSeparation = DEFAULT_SIBLING_SEPARATION, this.levelSeparation = DEFAULT_LEVEL_SEPARATION, this.subtreeSeparation = DEFAULT_SUBTREE_SEPARATION, this.orientation = DEFAULT_ORIENTATION}); } ================================================ FILE: lib/tree/BuchheimWalkerNodeData.dart ================================================ part of graphview; class BuchheimWalkerNodeData { Node? ancestor; Node? thread; int number = 0; int depth = 0; double prelim = 0.toDouble(); double modifier = 0.toDouble(); double shift = 0.toDouble(); double change = 0.toDouble(); List predecessorNodes = []; List successorNodes = []; } ================================================ FILE: lib/tree/CircleLayoutAlgorithm.dart ================================================ part of graphview; class CircleLayoutConfiguration { final double radius; final bool reduceEdgeCrossing; final int reduceEdgeCrossingMaxEdges; CircleLayoutConfiguration({ this.radius = 0.0, // 0 means auto-calculate this.reduceEdgeCrossing = true, this.reduceEdgeCrossingMaxEdges = 200, }); } class CircleLayoutAlgorithm extends Algorithm { final CircleLayoutConfiguration config; double _radius = 0.0; List nodeOrderedList = []; CircleLayoutAlgorithm(this.config, EdgeRenderer? renderer) { this.renderer = renderer ?? ArrowEdgeRenderer(); _radius = config.radius; } @override Size run(Graph? graph, double shiftX, double shiftY) { if (graph == null || graph.nodes.isEmpty) { return Size.zero; } // Handle single node case if (graph.nodes.length == 1) { final node = graph.nodes.first; node.position = Offset(shiftX + 100, shiftY + 100); return Size(200, 200); } _computeNodeOrder(graph); final size = _layoutNodes(graph); _shiftCoordinates(graph, shiftX, shiftY); return size; } void _computeNodeOrder(Graph graph) { final shouldReduceCrossing = config.reduceEdgeCrossing && graph.edges.length < config.reduceEdgeCrossingMaxEdges; if (shouldReduceCrossing) { nodeOrderedList = _reduceEdgeCrossing(graph); } else { nodeOrderedList = List.from(graph.nodes); } } List _reduceEdgeCrossing(Graph graph) { // Check if graph has multiple components final components = _findConnectedComponents(graph); final orderedList = []; if (components.length > 1) { // Handle each component separately for (final component in components) { final componentGraph = _createSubgraph(graph, component); final componentOrder = _optimizeNodeOrder(componentGraph); orderedList.addAll(componentOrder); } } else { // Single component orderedList.addAll(_optimizeNodeOrder(graph)); } return orderedList; } List> _findConnectedComponents(Graph graph) { final visited = {}; final components = >[]; for (final node in graph.nodes) { if (!visited.contains(node)) { final component = {}; _dfsComponent(graph, node, visited, component); components.add(component); } } return components; } void _dfsComponent(Graph graph, Node node, Set visited, Set component) { visited.add(node); component.add(node); for (final edge in graph.edges) { Node? neighbor; if (edge.source == node && !visited.contains(edge.destination)) { neighbor = edge.destination; } else if (edge.destination == node && !visited.contains(edge.source)) { neighbor = edge.source; } if (neighbor != null) { _dfsComponent(graph, neighbor, visited, component); } } } Graph _createSubgraph(Graph originalGraph, Set nodes) { final subgraph = Graph(); // Add nodes for (final node in nodes) { subgraph.addNode(node); } // Add edges within the component for (final edge in originalGraph.edges) { if (nodes.contains(edge.source) && nodes.contains(edge.destination)) { subgraph.addEdgeS(edge); } } return subgraph; } List _optimizeNodeOrder(Graph graph) { if (graph.nodes.length <= 2) { return List.from(graph.nodes); } // Simple greedy optimization to reduce edge crossings var bestOrder = List.from(graph.nodes); var bestCrossings = _countCrossings(graph, bestOrder); // Try a few different starting arrangements final attempts = min(10, graph.nodes.length); for (var attempt = 0; attempt < attempts; attempt++) { var currentOrder = List.from(graph.nodes); // Shuffle starting order if (attempt > 0) { currentOrder.shuffle(); } // Local optimization: try swapping adjacent nodes var improved = true; var iterations = 0; const maxIterations = 50; while (improved && iterations < maxIterations) { improved = false; iterations++; for (var i = 0; i < currentOrder.length - 1; i++) { // Try swapping positions i and i+1 final temp = currentOrder[i]; currentOrder[i] = currentOrder[i + 1]; currentOrder[i + 1] = temp; final crossings = _countCrossings(graph, currentOrder); if (crossings < bestCrossings) { bestOrder = List.from(currentOrder); bestCrossings = crossings; improved = true; } else { // Swap back if no improvement currentOrder[i + 1] = currentOrder[i]; currentOrder[i] = temp; } } } } return bestOrder; } int _countCrossings(Graph graph, List nodeOrder) { if (nodeOrder.length < 3) return 0; final nodePositions = {}; for (var i = 0; i < nodeOrder.length; i++) { nodePositions[nodeOrder[i]] = i; } var crossings = 0; final edges = graph.edges; // Count crossings between all pairs of edges for (var i = 0; i < edges.length; i++) { final edge1 = edges[i]; final pos1a = nodePositions[edge1.source]!; final pos1b = nodePositions[edge1.destination]!; for (var j = i + 1; j < edges.length; j++) { final edge2 = edges[j]; final pos2a = nodePositions[edge2.source]!; final pos2b = nodePositions[edge2.destination]!; // Check if edges cross when nodes are arranged in a circle if (_edgesCross(pos1a, pos1b, pos2a, pos2b, nodeOrder.length)) { crossings++; } } } return crossings; } bool _edgesCross(int pos1a, int pos1b, int pos2a, int pos2b, int totalNodes) { // Normalize positions so smaller is first if (pos1a > pos1b) { final temp = pos1a; pos1a = pos1b; pos1b = temp; } if (pos2a > pos2b) { final temp = pos2a; pos2a = pos2b; pos2b = temp; } // Check if one edge's endpoints separate the other edge's endpoints on the circle return (pos1a < pos2a && pos2a < pos1b && pos1b < pos2b) || (pos2a < pos1a && pos1a < pos2b && pos2b < pos1b); } Size _layoutNodes(Graph graph) { // Calculate bounds for auto-sizing var width = 400.0; var height = 400.0; if (_radius <= 0) { _radius = 0.35 * max(width, height); } final centerX = width / 2; final centerY = height / 2; // Position nodes in circle for (var i = 0; i < nodeOrderedList.length; i++) { final node = nodeOrderedList[i]; final angle = (2 * pi * i) / nodeOrderedList.length; final posX = cos(angle) * _radius + centerX; final posY = sin(angle) * _radius + centerY; node.position = Offset(posX, posY); } // Calculate actual bounds based on positioned nodes final bounds = graph.calculateGraphBounds(); return Size(bounds.width + 40, bounds.height + 40); // Add some padding } void _shiftCoordinates(Graph graph, double shiftX, double shiftY) { for (final node in graph.nodes) { node.position = Offset(node.x + shiftX, node.y + shiftY); } } @override void init(Graph? graph) { // Implementation can be added if needed } @override void setDimensions(double width, double height) { // Implementation can be added if needed } @override EdgeRenderer? renderer; } ================================================ FILE: lib/tree/RadialTreeLayoutAlgorithm.dart ================================================ part of graphview; class TreeLayoutNodeData { Rectangle? bounds; int depth = 0; bool visited = false; List successorNodes = []; Node? parent; TreeLayoutNodeData(); } class RadialTreeLayoutAlgorithm extends Algorithm { late BuchheimWalkerConfiguration config; final Map nodeData = {}; final Map baseBounds = {}; final Map polarLocations = {}; RadialTreeLayoutAlgorithm(this.config, EdgeRenderer? renderer) { this.renderer = renderer ?? ArrowEdgeRenderer(); } @override Size run(Graph? graph, double shiftX, double shiftY) { if (graph == null || graph.nodes.isEmpty) { return Size.zero; } nodeData.clear(); baseBounds.clear(); polarLocations.clear(); // Handle single node case if (graph.nodes.length == 1) { final node = graph.nodes.first; node.position = Offset(shiftX + 100, shiftY + 100); return Size(200, 200); } _initializeData(graph); final roots = _findRoots(graph); if (roots.isEmpty) { final spanningTree = _createSpanningTree(graph); return _layoutSpanningTree(spanningTree, shiftX, shiftY); } // First, build the tree using regular tree layout _buildRegularTree(graph, roots); // Then convert to radial coordinates _setRadialLocations(graph); // Convert polar to cartesian and position nodes _putRadialPointsInModel(graph); _shiftCoordinates(graph, shiftX, shiftY); return graph.calculateGraphSize(); } void _initializeData(Graph graph) { // Initialize node data for (final node in graph.nodes) { nodeData[node] = TreeLayoutNodeData(); } // Build tree structure from edges for (final edge in graph.edges) { final source = edge.source; final target = edge.destination; nodeData[source]!.successorNodes.add(target); nodeData[target]!.parent = source; } } List _findRoots(Graph graph) { return graph.nodes.where((node) { return nodeData[node]!.parent == null && successorsOf(node).isNotEmpty; }).toList(); } void _buildRegularTree(Graph graph, List roots) { _calculateSubtreeDimensions(roots); _positionNodes(roots); } void _calculateSubtreeDimensions(List roots) { final visited = {}; for (final root in roots) { _calculateWidth(root, visited); } visited.clear(); for (final root in roots) { _calculateHeight(root, visited); } } int _calculateWidth(Node node, Set visited) { if (!visited.add(node)) return 0; final children = successorsOf(node); if (children.isEmpty) { final width = max(node.width.toInt(), config.siblingSeparation); baseBounds[node] = Size(width.toDouble(), 0); return width; } var totalWidth = 0; for (var i = 0; i < children.length; i++) { totalWidth += _calculateWidth(children[i], visited); if (i < children.length - 1) { totalWidth += config.siblingSeparation; } } baseBounds[node] = Size(totalWidth.toDouble(), 0); return totalWidth; } int _calculateHeight(Node node, Set visited) { if (!visited.add(node)) return 0; final children = successorsOf(node); if (children.isEmpty) { final height = max(node.height.toInt(), config.levelSeparation); final current = baseBounds[node]!; baseBounds[node] = Size(current.width, height.toDouble()); return height; } var maxChildHeight = 0; for (final child in children) { maxChildHeight = max(maxChildHeight, _calculateHeight(child, visited)); } final totalHeight = maxChildHeight + config.levelSeparation; final current = baseBounds[node]!; baseBounds[node] = Size(current.width, totalHeight.toDouble()); return totalHeight; } void _positionNodes(List roots) { var currentX = config.siblingSeparation.toDouble(); for (final root in roots) { final rootWidth = baseBounds[root]!.width; currentX += rootWidth / 2; _buildTree(root, currentX, config.levelSeparation.toDouble(), {}); currentX += rootWidth / 2 + config.siblingSeparation; } } void _buildTree(Node node, double x, double y, Set visited) { if (!visited.add(node)) return; node.position = Offset(x, y); final children = successorsOf(node); if (children.isEmpty) return; final nextY = y + config.levelSeparation; final totalWidth = baseBounds[node]!.width; var childX = x - totalWidth / 2; for (final child in children) { final childWidth = baseBounds[child]!.width; childX += childWidth / 2; _buildTree(child, childX, nextY, visited); childX += childWidth / 2 + config.siblingSeparation; } } void _setRadialLocations(Graph graph) { final bounds = graph.calculateGraphBounds(); final maxPoint = bounds.width; // Calculate theta step based on maximum x coordinate final theta = 2 * pi / maxPoint; final deltaRadius = 1.0; final offset = _findRoots(graph).length > 1 ? config.levelSeparation.toDouble() : 0.0; for (final node in graph.nodes) { final position = node.position; // Convert cartesian tree coordinates to polar coordinates final polarTheta = position.dx * theta; final polarRadius = (offset + position.dy - config.levelSeparation) * deltaRadius; final polarPoint = PolarPoint.of(polarTheta, polarRadius); polarLocations[node] = polarPoint; } } void _putRadialPointsInModel(Graph graph) { final diameter = _calculateDiameter(); final center = diameter * 0.5 * 0.5; polarLocations.forEach((node, polarPoint) { final cartesian = polarPoint.toCartesian(); node.position = Offset(center + cartesian.dx, center + cartesian.dy); }); } double _calculateDiameter() { if (polarLocations.isEmpty) return 400.0; double maxRadius = 0; polarLocations.values.forEach((polarPoint) { maxRadius = max(maxRadius, polarPoint.radius * 2); }); return maxRadius + config.siblingSeparation; } void _shiftCoordinates(Graph graph, double shiftX, double shiftY) { for (final node in graph.nodes) { node.position = Offset(node.x + shiftX, node.y + shiftY); } } Graph _createSpanningTree(Graph graph) { final visited = {}; final spanningEdges = []; if (graph.nodes.isNotEmpty) { final startNode = graph.nodes.first; final queue = [startNode]; visited.add(startNode); while (queue.isNotEmpty) { final current = queue.removeAt(0); for (final edge in graph.edges) { Node? neighbor; if (edge.source == current && !visited.contains(edge.destination)) { neighbor = edge.destination; spanningEdges.add(edge); } else if (edge.destination == current && !visited.contains(edge.source)) { neighbor = edge.source; spanningEdges.add(Edge(current, edge.source)); } if (neighbor != null && !visited.contains(neighbor)) { visited.add(neighbor); queue.add(neighbor); } } } } return Graph()..addEdges(spanningEdges); } Size _layoutSpanningTree(Graph spanningTree, double shiftX, double shiftY) { nodeData.clear(); baseBounds.clear(); polarLocations.clear(); _initializeData(spanningTree); final roots = _findRoots(spanningTree); if (roots.isEmpty && spanningTree.nodes.isNotEmpty) { final fakeRoot = spanningTree.nodes.first; _buildRegularTree(spanningTree, [fakeRoot]); } else { _buildRegularTree(spanningTree, roots); } _setRadialLocations(spanningTree); _putRadialPointsInModel(spanningTree); _shiftCoordinates(spanningTree, shiftX, shiftY); return spanningTree.calculateGraphSize(); } @override void init(Graph? graph) { // Implementation can be added if needed } @override void setDimensions(double width, double height) { // Implementation can be added if needed } List successorsOf(Node? node) { return nodeData[node]!.successorNodes; } @override EdgeRenderer? renderer; } ================================================ FILE: lib/tree/TidierTreeLayoutAlgorithm.dart ================================================ part of graphview; class TidierTreeNodeData { int mod = 0; Node? thread; int shift = 0; Node? ancestor; int x = 0; int change = 0; int childCount = 0; List successorNodes = []; List predecessorNodes = []; TidierTreeNodeData(); } class TidierTreeLayoutAlgorithm extends Algorithm { late BuchheimWalkerConfiguration config; final Map nodeData = {}; final Map baseBounds = {}; final List heights = []; late List roots; Rect bounds = Rect.zero; late Graph tree; TidierTreeLayoutAlgorithm(this.config, EdgeRenderer? renderer) { this.renderer = renderer ?? TreeEdgeRenderer(config); } bool isVertical() { var orientation = config.orientation; return orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM || orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP; } bool needReverseOrder() { var orientation = config.orientation; return orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP || orientation == BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT; } @override Size run(Graph? graph, double shiftX, double shiftY) { if (graph == null || graph.nodes.isEmpty) { return Size.zero; } _clearMetadata(); if (graph.nodes.length == 1) { final node = graph.nodes.first; node.position = Offset(shiftX + 100, shiftY + 100); return Size(200, 200); } _buildTree(graph); _applyOrientation(graph); _shiftCoordinates(graph, shiftX, shiftY); final size = graph.calculateGraphSize(); _clearMetadata(); return size; } void _clearMetadata() { heights.clear(); baseBounds.clear(); bounds = Rect.zero; } void _buildTree(Graph graph) { nodeData.clear(); heights.clear(); _initializeData(graph); roots = _findRoots(graph); if (roots.isEmpty) { final spanningTree = _createSpanningTree(graph); _buildTree(spanningTree); return; } tree = graph; final virtualRoot = roots.length > 1 ? null : roots.first; _firstWalk(virtualRoot, null); _computeMaxHeights(virtualRoot, 0); _secondWalk( virtualRoot, virtualRoot != null ? -_nodeData(virtualRoot).x : 0, 0, 0); _normalizePositions(graph); } void _initializeData(Graph graph) { // Initialize node data for (final node in graph.nodes) { nodeData[node] = TidierTreeNodeData(); } // Build tree structure from edges for (final edge in graph.edges) { final source = edge.source; final target = edge.destination; nodeData[source]?.successorNodes.add(target); nodeData[target]?.predecessorNodes.add(source); } } List _findRoots(Graph graph) { final incomingCounts = {}; for (final node in graph.nodes) { incomingCounts[node] = 0; } for (final edge in graph.edges) { incomingCounts[edge.destination] = (incomingCounts[edge.destination] ?? 0) + 1; } return graph.nodes.where((node) => incomingCounts[node] == 0).toList(); } TidierTreeNodeData _nodeData(Node? v) { if (v == null) return TidierTreeNodeData(); return nodeData.putIfAbsent(v, () => TidierTreeNodeData()); } void _firstWalk(Node? v, Node? leftSibling) { if (successorsOf(v).isEmpty) { if (leftSibling != null) { _nodeData(v).x = _nodeData(leftSibling).x + _getDistance(v, leftSibling, true); } } else { final children = successorsOf(v); var defaultAncestor = children.isNotEmpty ? children.first : null; Node? previousChild; for (final child in children) { _firstWalk(child, previousChild); defaultAncestor = _apportion(child, defaultAncestor, previousChild, v); previousChild = child; } _shift(v); final firstChild = children.isNotEmpty ? children.first : null; final lastChild = children.isNotEmpty ? children.last : null; if (firstChild != null && lastChild != null) { final midpoint = (_nodeData(firstChild).x + _nodeData(lastChild).x) ~/ 2; if (leftSibling != null) { _nodeData(v).x = _nodeData(leftSibling).x + _getDistance(v, leftSibling, true); _nodeData(v).mod = _nodeData(v).x - midpoint; } else { _nodeData(v).x = midpoint; } } } } void _secondWalk(Node? v, int m, int depth, int yOffset) { if (v == null) { // Handle multiple roots with subtree separation var rootOffset = 0; for (var i = 0; i < roots.length; i++) { _secondWalk(roots[i], m + rootOffset, depth, yOffset); if (i < roots.length - 1) { rootOffset += config.subtreeSeparation; } } return; } final levelHeight = depth < heights.length ? heights[depth] : config.levelSeparation; final x = _nodeData(v).x + m; final y = yOffset + levelHeight ~/ 2; v.position = Offset(x.toDouble(), y.toDouble()); _updateBounds(v, x, y); final children = successorsOf(v); if (children.isNotEmpty) { final newYOffset = yOffset + levelHeight + config.levelSeparation; for (final child in children) { _secondWalk(child, m + _nodeData(v).mod, depth + 1, newYOffset); } } } void _updateBounds(Node node, int centerX, int centerY) { final width = node.width.toInt(); final height = node.height.toInt(); final left = centerX - width ~/ 2; final right = centerX + width ~/ 2; final top = centerY - height ~/ 2; final bottom = centerY + height ~/ 2; final nodeBounds = Rect.fromLTRB( left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble()); bounds = bounds == Rect.zero ? nodeBounds : bounds.expandToInclude(nodeBounds); } void _computeMaxHeights(Node? node, int depth) { if (node == null) { for (final root in roots) { _computeMaxHeights(root, depth); } return; } while (heights.length <= depth) { heights.add(0); } final nodeHeight = isVertical() ? max(node.height.toInt(), config.levelSeparation) : max(node.width.toInt(), config.levelSeparation); heights[depth] = max(heights[depth], nodeHeight); for (final child in successorsOf(node)) { _computeMaxHeights(child, depth + 1); } } Node? _leftChild(Node? v) { final children = successorsOf(v); return children.isNotEmpty ? children.first : _nodeData(v).thread; } Node? _rightChild(Node? v) { final children = successorsOf(v); return children.isNotEmpty ? children.last : _nodeData(v).thread; } int _getDistance(Node? v, Node? w, bool isSibling) { if (v == null || w == null) return config.siblingSeparation; // Use appropriate separation based on relationship final separation = isSibling ? config.siblingSeparation : config.subtreeSeparation; // Consider node sizes in the calculation final vSize = isVertical() ? v.width.toInt() : v.height.toInt(); final wSize = isVertical() ? w.width.toInt() : w.height.toInt(); return (vSize + wSize) ~/ 2 + separation; } Node? _apportion( Node? v, Node? defaultAncestor, Node? leftSibling, Node? parentOfV) { if (leftSibling == null) return defaultAncestor; var vor = v; var vir = v; Node? vil = leftSibling; var vol = successorsOf(parentOfV).isNotEmpty ? successorsOf(parentOfV).first : null; var innerRight = _nodeData(vir).mod; var outerRight = _nodeData(vor).mod; var innerLeft = _nodeData(vil).mod; var outerLeft = _nodeData(vol).mod; var nextRightOfVil = _rightChild(vil); var nextLeftOfVir = _leftChild(vir); while (nextRightOfVil != null && nextLeftOfVir != null) { vil = nextRightOfVil; vir = nextLeftOfVir; vol = _leftChild(vol); vor = _rightChild(vor); if (vor != null) { _nodeData(vor).ancestor = v; } final shift = (_nodeData(vil).x + innerLeft) - (_nodeData(vir).x + innerRight) + _getDistance(vil, vir, true); if (shift > 0) { _moveSubtree( _ancestor(vil, parentOfV, defaultAncestor), v, parentOfV, shift); innerRight += shift; outerRight += shift; } innerLeft += _nodeData(vil).mod; innerRight += _nodeData(vir).mod; outerLeft += _nodeData(vol).mod; outerRight += _nodeData(vor).mod; nextRightOfVil = _rightChild(vil); nextLeftOfVir = _leftChild(vir); } if (nextRightOfVil != null && _rightChild(vor) == null) { _nodeData(vor).thread = nextRightOfVil; _nodeData(vor).mod += innerLeft - outerRight; } if (nextLeftOfVir != null && _leftChild(vol) == null) { _nodeData(vol).thread = nextLeftOfVir; _nodeData(vol).mod += innerRight - outerLeft; defaultAncestor = v; } return defaultAncestor; } Node? _ancestor(Node? vil, Node? parentOfV, Node? defaultAncestor) { final ancestor = _nodeData(vil).ancestor ?? vil; final predecessors = predecessorsOf(ancestor!); if (predecessors.contains(parentOfV)) { return ancestor; } return defaultAncestor; } void _moveSubtree( Node? leftNode, Node? rightNode, Node? parentNode, int shift) { if (leftNode == null || rightNode == null) return; final subtreeCount = _childPosition(rightNode, parentNode) - _childPosition(leftNode, parentNode); if (subtreeCount > 0) { final rightData = _nodeData(rightNode); final leftData = _nodeData(leftNode); rightData.change -= shift ~/ subtreeCount; rightData.shift += shift; leftData.change += shift ~/ subtreeCount; rightData.x += shift; rightData.mod += shift; } } int _childPosition(Node? node, Node? parentNode) { if (parentNode == null) { return roots.indexOf(node!) + 1; } if (_nodeData(node).childCount != 0) { return _nodeData(node).childCount; } final children = successorsOf(parentNode); for (var i = 0; i < children.length; i++) { _nodeData(children[i]).childCount = i + 1; } return _nodeData(node).childCount; } void _shift(Node? v) { final children = successorsOf(v); var shift = 0; var change = 0; for (final child in children.reversed) { final childData = _nodeData(child); childData.x += shift; childData.mod += shift; change += childData.change; shift += childData.shift + change; } } void _normalizePositions(Graph graph) { final graphBounds = graph.calculateGraphBounds(); final xOffset = config.subtreeSeparation - graphBounds.left; final yOffset = config.levelSeparation - graphBounds.top; for (final node in graph.nodes) { node.position = Offset( node.x + xOffset, node.y + yOffset, ); } } void _applyOrientation(Graph graph) { if (config.orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) { return; } final bounds = graph.calculateGraphBounds(); final centerX = bounds.left + bounds.width / 2; final centerY = bounds.top + bounds.height / 2; for (final node in graph.nodes) { final x = node.x - centerX; final y = node.y - centerY; Offset newPosition; switch (config.orientation) { case BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP: newPosition = Offset(x + centerX, centerY - y); break; case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT: newPosition = Offset(-y + centerX, x + centerY); break; case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT: newPosition = Offset(y + centerX, -x + centerY); break; default: newPosition = node.position; break; } node.position = newPosition; } } void _shiftCoordinates(Graph graph, double shiftX, double shiftY) { for (final node in graph.nodes) { node.position = Offset(node.x + shiftX, node.y + shiftY); } } Graph _createSpanningTree(Graph graph) { final visited = {}; final spanningEdges = []; if (graph.nodes.isNotEmpty) { final startNode = graph.nodes.first; final queue = [startNode]; visited.add(startNode); while (queue.isNotEmpty) { final current = queue.removeAt(0); for (final edge in graph.edges) { Node? neighbor; if (edge.source == current && !visited.contains(edge.destination)) { neighbor = edge.destination; spanningEdges.add(edge); } else if (edge.destination == current && !visited.contains(edge.source)) { neighbor = edge.source; spanningEdges.add(Edge(current, edge.source)); } if (neighbor != null && !visited.contains(neighbor)) { visited.add(neighbor); queue.add(neighbor); } } } } return Graph()..addEdges(spanningEdges); } List successorsOf(Node? v) { if (v == null) return roots; var nodes = nodeData[v]!.successorNodes; return nodes; } List predecessorsOf(Node v) { if (roots.contains(v)) return []; return nodeData[v]!.predecessorNodes; } @override void init(Graph? graph) {} @override void setDimensions(double width, double height) {} @override EdgeRenderer? renderer; } ================================================ FILE: lib/tree/TreeEdgeRenderer.dart ================================================ part of graphview; class TreeEdgeRenderer extends EdgeRenderer { BuchheimWalkerConfiguration configuration; TreeEdgeRenderer(this.configuration); var linePath = Path(); void render(Canvas canvas, Graph graph, Paint paint) { graph.edges.forEach((edge) { renderEdge(canvas, edge, paint); }); } @override void renderEdge(Canvas canvas, Edge edge, Paint paint) { final edgePaint = (edge.paint ?? paint)..style = PaintingStyle.stroke; var node = edge.source; var child = edge.destination; if (node == child) { final loopPath = buildSelfLoopPath(edge, arrowLength: 0.0); if (loopPath != null) { drawStyledPath(canvas, loopPath.path, edgePaint, lineType: child.lineType); } return; } final parentPos = getNodePosition(node); final childPos = getNodePosition(child); final orientation = getEffectiveOrientation(node, child); linePath.reset(); buildEdgePath(node, child, parentPos, childPos, orientation); // Check if the destination node has a specific line type final lineType = child.lineType; if (lineType != LineType.Default) { // For styled lines, we need to draw path segments with the appropriate style _drawStyledPath(canvas, linePath, edgePaint, lineType); } else { canvas.drawPath(linePath, edgePaint); } } /// Draws a path with the specified line type by converting it to line segments void _drawStyledPath(Canvas canvas, Path path, Paint paint, LineType lineType) { // Extract path points for styled rendering final points = _extractPathPoints(path); // Draw each segment with the appropriate style for (var i = 0; i < points.length - 1; i++) { drawStyledLine( canvas, points[i], points[i + 1], paint, lineType: lineType, ); } } /// Extracts key points from a path for segment drawing List _extractPathPoints(Path path) { // This is a simplified extraction that works for the L-shaped and curved paths // For more complex paths, you might need a more sophisticated approach final points = []; final metrics = path.computeMetrics(); for (var metric in metrics) { final length = metric.length; const sampleDistance = 10.0; // Sample every 10 pixels var distance = 0.0; while (distance <= length) { final tangent = metric.getTangentForOffset(distance); if (tangent != null) { points.add(tangent.position); } distance += sampleDistance; } // Add the final point final finalTangent = metric.getTangentForOffset(length); if (finalTangent != null) { points.add(finalTangent.position); } } return points; } int getEffectiveOrientation(Node node, Node child) { return configuration.orientation; } /// Builds the path for the edge based on orientation void buildEdgePath(Node node, Node child, Offset parentPos, Offset childPos, int orientation) { final parentCenterX = parentPos.dx + node.width * 0.5; final parentCenterY = parentPos.dy + node.height * 0.5; final childCenterX = childPos.dx + child.width * 0.5; final childCenterY = childPos.dy + child.height * 0.5; if (parentCenterY == childCenterY && parentCenterX == childCenterX) return; switch (orientation) { case BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM: buildTopBottomPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY); break; case BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP: buildBottomTopPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY); break; case BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT: buildLeftRightPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY); break; case BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT: buildRightLeftPath(node, child, parentPos, childPos, parentCenterX, parentCenterY, childCenterX, childCenterY); break; } } /// Builds path for top-bottom orientation void buildTopBottomPath(Node node, Node child, Offset parentPos, Offset childPos, double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) { final parentBottomY = parentPos.dy + node.height * 0.5; final childTopY = childPos.dy + child.height * 0.5; final midY = (parentBottomY + childTopY) * 0.5; if (configuration.useCurvedConnections) { // Curved connection linePath ..moveTo(childCenterX, childTopY) ..cubicTo( childCenterX, midY, parentCenterX, midY, parentCenterX, parentBottomY, ); } else { // L-shaped connection linePath ..moveTo(parentCenterX, parentBottomY) ..lineTo(parentCenterX, midY) ..lineTo(childCenterX, midY) ..lineTo(childCenterX, childTopY); } } /// Builds path for bottom-top orientation void buildBottomTopPath(Node node, Node child, Offset parentPos, Offset childPos, double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) { final parentTopY = parentPos.dy + node.height * 0.5; final childBottomY = childPos.dy + child.height * 0.5; final midY = (parentTopY + childBottomY) * 0.5; if (configuration.useCurvedConnections) { linePath ..moveTo(childCenterX, childBottomY) ..cubicTo( childCenterX, midY, parentCenterX, midY, parentCenterX, parentTopY, ); } else { linePath ..moveTo(parentCenterX, parentTopY) ..lineTo(parentCenterX, midY) ..lineTo(childCenterX, midY) ..lineTo(childCenterX, childBottomY); } } /// Builds path for left-right orientation void buildLeftRightPath(Node node, Node child, Offset parentPos, Offset childPos, double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) { final parentRightX = parentPos.dx + node.width * 0.5; final childLeftX = childPos.dx + child.width * 0.5; final midX = (parentRightX + childLeftX) * 0.5; if (configuration.useCurvedConnections) { linePath ..moveTo(childLeftX, childCenterY) ..cubicTo( midX, childCenterY, midX, parentCenterY, parentRightX, parentCenterY, ); } else { linePath ..moveTo(parentRightX, parentCenterY) ..lineTo(midX, parentCenterY) ..lineTo(midX, childCenterY) ..lineTo(childLeftX, childCenterY); } } /// Builds path for right-left orientation void buildRightLeftPath(Node node, Node child, Offset parentPos, Offset childPos, double parentCenterX, double parentCenterY, double childCenterX, double childCenterY) { final parentLeftX = parentPos.dx + node.width * 0.5; final childRightX = childPos.dx + child.width * 0.5; final midX = (parentLeftX + childRightX) * 0.5; if (configuration.useCurvedConnections) { linePath ..moveTo(childRightX, childCenterY) ..cubicTo( midX, childCenterY, midX, parentCenterY, parentLeftX, parentCenterY, ); } else { linePath ..moveTo(parentLeftX, parentCenterY) ..lineTo(midX, parentCenterY) ..lineTo(midX, childCenterY) ..lineTo(childRightX, childCenterY); } } } ================================================ FILE: pubspec.yaml ================================================ name: graphview description: GraphView is used to display data in graph structures. It can display Tree layout, Directed and Layered graph. Useful for Family Tree, Hierarchy View. version: 1.5.1 homepage: https://github.com/nabil6391/graphview environment: sdk: '>=2.17.0 <4.0.0' flutter: ">=1.17.0" dependencies: flutter: sdk: flutter collection: ^1.15.0 dev_dependencies: flutter_test: sdk: flutter flutter: ================================================ FILE: test/algorithm_performance_test.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:graphview/GraphView.dart'; const itemHeight = 100.0; const itemWidth = 100.0; const runs = 5; void main() { Graph _createGraph(int n) { final graph = Graph(); final nodes = List.generate(n, (i) => Node.Id(i + 1)); for (var i = 0; i < n - 1; i++) { final children = (i < n / 3) ? 3 : 2; for (var j = 1; j <= children && i * children + j < n; j++) { graph.addEdge(nodes[i], nodes[i * children + j]); } } for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = const Size(itemWidth, itemHeight); } return graph; } test('Algorithm performance', () { final algorithms = { 'Buchheim': BuchheimWalkerAlgorithm(BuchheimWalkerConfiguration(), null), 'Balloon': BalloonLayoutAlgorithm(BuchheimWalkerConfiguration(), null), 'RadialTree': RadialTreeLayoutAlgorithm(BuchheimWalkerConfiguration(), null), 'TidierTree': TidierTreeLayoutAlgorithm(BuchheimWalkerConfiguration(), null), 'Eiglsperger': EiglspergerAlgorithm(SugiyamaConfiguration()), 'Sugiyama': SugiyamaAlgorithm(SugiyamaConfiguration()), 'Circle': CircleLayoutAlgorithm(CircleLayoutConfiguration(), null), }; final results = {}; final graph = _createGraph(1000); for (final entry in algorithms.entries) { final times = []; for (var i = 0; i < runs; i++) { final sw = Stopwatch()..start(); entry.value.run(graph, 0, 0); times.add(sw.elapsed.inMilliseconds); } // results[entry.key] = times.reduce((a, b) => a + b) / times.length; results[entry.key] = times.reduce((a, b) => a + b).toDouble(); } final sorted = results.entries.toList()..sort((a, b) => a.value.compareTo(b.value)); print('\nPerformance Results (${runs} runs avg):'); for (var i = 0; i < sorted.length; i++) { print('${(i + 1).toString().padLeft(2)}. ${sorted[i].key.padRight(12)}: ${sorted[i].value.toStringAsFixed(1)} ms'); } for (final result in results.values) { expect(result < 30000, true); } }); } ================================================ FILE: test/buchheim_walker_algorithm_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:graphview/GraphView.dart'; const itemHeight = 100.0; const itemWidth = 100.0; void main() { group('Buchheim Graph', () { final graph = Graph(); final node1 = Node.Id(1); final node2 = Node.Id(2); final node3 = Node.Id(3); final node4 = Node.Id(4); final node5 = Node.Id(5); final node6 = Node.Id(6); final node8 = Node.Id(7); final node7 = Node.Id(8); final node9 = Node.Id(9); final node10 = Node.Id(10); final node11 = Node.Id(11); final node12 = Node.Id(12); graph.addEdge(node1, node2); graph.addEdge(node1, node3, paint: Paint()..color = Colors.red); graph.addEdge(node1, node4, paint: Paint()..color = Colors.blue); graph.addEdge(node2, node5); graph.addEdge(node2, node6); graph.addEdge(node6, node7, paint: Paint()..color = Colors.red); graph.addEdge(node6, node8, paint: Paint()..color = Colors.red); graph.addEdge(node4, node9); graph.addEdge(node4, node10, paint: Paint()..color = Colors.black); graph.addEdge(node4, node11, paint: Paint()..color = Colors.red); graph.addEdge(node11, node12); test('Buchheim Node positions are correct for Top_Bottom', () { final _configuration = BuchheimWalkerConfiguration() ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); var algorithm = BuchheimWalkerAlgorithm( _configuration, TreeEdgeRenderer(_configuration)); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; expect(timeTaken < 1000, true); expect(graph.getNodeAtPosition(0).position, Offset(385, 10)); expect(graph.getNodeAtPosition(6).position, Offset(110.0, 760.0)); expect(graph.getNodeUsingId(3).position, Offset(385.0, 260.0)); expect(graph.getNodeUsingId(4).position, Offset(660.0, 260.0)); expect(size, Size(950.0, 850.0)); }); test('Buchheim detects cyclic dependencies', () { // Create a graph with a cycle final cyclicGraph = Graph(); final nodeA = Node.Id('A'); final nodeB = Node.Id('B'); final nodeC = Node.Id('C'); // Create cycle: A -> B -> C -> A cyclicGraph.addEdge(nodeA, nodeB); cyclicGraph.addEdge(nodeB, nodeC); cyclicGraph.addEdge(nodeC, nodeA); // This creates the cycle for (var i = 0; i < cyclicGraph.nodeCount(); i++) { cyclicGraph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } final configuration = BuchheimWalkerConfiguration() ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); var algorithm = BuchheimWalkerAlgorithm( configuration, TreeEdgeRenderer(configuration)); // Should throw exception when cycle is detected expect( () => algorithm.run(cyclicGraph, 0, 0), throwsA(isA().having( (e) => e.toString(), 'message', contains('Cyclic dependency detected'), )), ); }); test('Buchheim Performance for 1000 nodes to be less than 40ms', () { Graph _createGraph(int n) { final graph = Graph(); final nodes = List.generate(n, (i) => Node.Id(i + 1)); var currentChild = 1; // Start from node 1 (node 0 is root) for (var i = 0; i < n && currentChild < n; i++) { final children = (i < n ~/ 3) ? 3 : 2; for (var j = 0; j < children && currentChild < n; j++) { graph.addEdge(nodes[i], nodes[currentChild]); currentChild++; } } for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = const Size(itemWidth, itemHeight); } return graph; } final _configuration = BuchheimWalkerConfiguration() ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); var algorithm = BuchheimWalkerAlgorithm( _configuration, TreeEdgeRenderer(_configuration)); var graph = _createGraph(1000); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); algorithm.run(graph, 0, 0); var timeTaken = stopwatch.elapsed.inMilliseconds; print('Timetaken $timeTaken for ${graph.nodeCount()} nodes'); expect(timeTaken < 40, true); }); }); } ================================================ FILE: test/controller_tests.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:graphview/GraphView.dart'; void main() { group('GraphView Controller Tests', () { testWidgets('animateToNode centers the target node', (WidgetTester tester) async { // Setup graph final graph = Graph(); final targetNode = Node.Id('target'); targetNode.key = const ValueKey('target'); final otherNode = Node.Id('other'); graph.addEdge(targetNode, otherNode); final transformationController = TransformationController(); final controller = GraphViewController( transformationController: transformationController); final configuration = BuchheimWalkerConfiguration(); final algorithm = BuchheimWalkerAlgorithm( configuration, TreeEdgeRenderer(configuration)); // Build widget await tester.pumpWidget( MaterialApp( home: Scaffold( body: SizedBox( width: 400, height: 600, child: GraphView.builder( graph: graph, algorithm: algorithm, controller: controller, builder: (node) => Container( width: 100, height: 50, color: Colors.blue, child: Text(node.key?.value ?? ''), ), ), ), ), ), ); await tester.pumpAndSettle(); // Get the actual position of target node after algorithm runs final actualNodePosition = targetNode.position; final nodeCenter = Offset( actualNodePosition.dx + targetNode.width / 2, actualNodePosition.dy + targetNode.height / 2, ); // Get initial transformation final initialMatrix = transformationController.value; // Animate to target node controller.animateToNode(const ValueKey('target')); // Let animation complete await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); // Verify transformation changed final finalMatrix = transformationController.value; expect(finalMatrix, isNot(equals(initialMatrix))); // With viewport size 400x600, center should be at (200, 300) // Expected translation should center the node at viewport center final expectedTranslationX = 200 - nodeCenter.dx; // viewport_center_x - node_center_x final expectedTranslationY = 300 - nodeCenter.dy; // viewport_center_y - node_center_y expect(finalMatrix.getTranslation().x, closeTo(expectedTranslationX, 5)); expect(finalMatrix.getTranslation().y, closeTo(expectedTranslationY, 5)); }); testWidgets('animateToNode handles non-existent node gracefully', (WidgetTester tester) async { final graph = Graph(); final node = Node.Id('exists'); graph.nodes.add(node); final transformationController = TransformationController(); final controller = GraphViewController( transformationController: transformationController); final algorithm = BuchheimWalkerAlgorithm(BuchheimWalkerConfiguration(), TreeEdgeRenderer(BuchheimWalkerConfiguration())); await tester.pumpWidget( MaterialApp( home: GraphView.builder( graph: graph, algorithm: algorithm, controller: controller, builder: (node) => Container(), ), ), ); await tester.pumpAndSettle(); final initialMatrix = transformationController.value; // Try to animate to non-existent node controller.animateToNode(const ValueKey('nonexistent')); await tester.pumpAndSettle(); // Matrix should remain unchanged final finalMatrix = transformationController.value; expect(finalMatrix, equals(initialMatrix)); }); }); group('Collapse Tests', () { late Graph graph; late GraphViewController controller; setUp(() { graph = Graph(); controller = GraphViewController(); }); // Helper function to create a graph with multiple branches Graph createComplexGraph() { final g = Graph(); final root = Node.Id(0); final branch1 = Node.Id(1); final branch2 = Node.Id(2); final leaf1 = Node.Id(3); final leaf2 = Node.Id(4); final leaf3 = Node.Id(5); final leaf4 = Node.Id(6); g.addEdge(root, branch1); g.addEdge(root, branch2); g.addEdge(branch1, leaf1); g.addEdge(branch1, leaf2); g.addEdge(branch2, leaf3); g.addEdge(branch2, leaf4); return g; } test('Complex graph - multiple branches', () { final g = createComplexGraph(); final root = g.getNodeAtPosition(0); controller.collapseNode(g, root); final edges = controller.getCollapsingEdges(g); // Should get all 6 edges (root->branch1, root->branch2, branch1->leaf1, branch1->leaf2, branch2->leaf3, branch2->leaf4) expect(edges.length, 6); }); test('Nested collapse preserves original hide relationships', () { final graph = Graph(); final parent = Node.Id(0); final child = Node.Id(1); final grandchild = Node.Id(2); graph.addEdge(parent, child); graph.addEdge(child, grandchild); final controller = GraphViewController(); // Step 1: Collapse child controller.collapseNode(graph, child); expect(controller.isNodeVisible(graph, parent), true); expect(controller.isNodeVisible(graph, child), true); expect(controller.isNodeVisible(graph, grandchild), false); expect( controller.hiddenBy[grandchild], child); // grandchild hidden by child // Step 2: Collapse parent controller.collapseNode(graph, parent); expect(controller.isNodeVisible(graph, parent), true); expect(controller.isNodeVisible(graph, child), false); expect(controller.isNodeVisible(graph, grandchild), false); expect(controller.hiddenBy[child], parent); // child hidden by parent expect(controller.hiddenBy[grandchild], child); // grandchild STILL hidden by child! // Step 3: Get collapsing edges for parent controller.collapsedNode = parent; final parentEdges = controller.getCollapsingEdges(graph); // Should only include parent -> child, NOT child -> grandchild expect(parentEdges.length, 1); expect(parentEdges.first.source, parent); expect(parentEdges.first.destination, child); // Step 4: Expand parent controller.expandNode(graph, parent); expect(controller.isNodeVisible(graph, parent), true); expect(controller.isNodeVisible(graph, child), true); expect( controller.isNodeVisible(graph, grandchild), false); // Still hidden! expect(controller.hiddenBy[grandchild], child); // Still hidden by child! // Step 5: Expand child controller.expandNode(graph, child); expect(controller.isNodeVisible(graph, parent), true); expect(controller.isNodeVisible(graph, child), true); expect(controller.isNodeVisible(graph, grandchild), true); // Now visible! expect(controller.hiddenBy.containsKey(grandchild), false); }); }); } ================================================ FILE: test/example_trees.dart ================================================ const exampleTreeWith140Nodes = { 'edges': [ {'from': '7045321', 'to': '308264215'}, {'from': '308264215', 'to': '205893853'}, {'from': '205893853', 'to': '673966248'}, {'from': '673966248', 'to': '358204164'}, {'from': '358204164', 'to': '215888392'}, {'from': '215888392', 'to': '403621992'}, {'from': '215888392', 'to': '777909510'}, {'from': '777909510', 'to': '100213815'}, {'from': '100213815', 'to': '499504374'}, {'from': '499504374', 'to': '855703404'}, {'from': '499504374', 'to': '991104907'}, {'from': '991104907', 'to': '374555325'}, {'from': '991104907', 'to': '58236163'}, {'from': '991104907', 'to': '1051662797'}, {'from': '1051662797', 'to': '523457656'}, {'from': '1051662797', 'to': '178236248'}, {'from': '178236248', 'to': '403818044'}, {'from': '403818044', 'to': '633692579'}, {'from': '403818044', 'to': '326876433'}, {'from': '178236248', 'to': '294992198'}, {'from': '178236248', 'to': '207728643'}, {'from': '207728643', 'to': '474861525'}, {'from': '207728643', 'to': '704015142'}, {'from': '704015142', 'to': '891912594'}, {'from': '704015142', 'to': '93790829'}, {'from': '704015142', 'to': '713878610'}, {'from': '704015142', 'to': '568109301'}, {'from': '100213815', 'to': '298138012'}, {'from': '298138012', 'to': '1051662797'}, {'from': '777909510', 'to': '344277619'}, {'from': '344277619', 'to': '311541390'}, {'from': '311541390', 'to': '761787449'}, {'from': '761787449', 'to': '30973213'}, {'from': '30973213', 'to': '523457656'}, {'from': '30973213', 'to': '178236248'}, {'from': '761787449', 'to': '259733602'}, {'from': '311541390', 'to': '128821445'}, {'from': '344277619', 'to': '1003131136'}, {'from': '1003131136', 'to': '130000569'}, {'from': '1003131136', 'to': '319536467'}, {'from': '319536467', 'to': '299942125'}, {'from': '299942125', 'to': '178926206'}, {'from': '299942125', 'to': '675835322'}, {'from': '299942125', 'to': '1000135767'}, {'from': '319536467', 'to': '483940059'}, {'from': '483940059', 'to': '497866879'}, {'from': '483940059', 'to': '606660618'}, {'from': '483940059', 'to': '841482899'}, {'from': '358204164', 'to': '963021319'}, {'from': '963021319', 'to': '130000569'}, {'from': '963021319', 'to': '319536467'}, {'from': '358204164', 'to': '803634418'}, {'from': '803634418', 'to': '142291521'}, {'from': '142291521', 'to': '525361131'}, {'from': '525361131', 'to': '422007713'}, {'from': '422007713', 'to': '184596308'}, {'from': '422007713', 'to': '1020140270'}, {'from': '422007713', 'to': '779910731'}, {'from': '525361131', 'to': '859310299'}, {'from': '859310299', 'to': '514613187'}, {'from': '514613187', 'to': '680752017'}, {'from': '680752017', 'to': '1058283666'}, {'from': '680752017', 'to': '887688252'}, {'from': '680752017', 'to': '717256682'}, {'from': '717256682', 'to': '409719617'}, {'from': '409719617', 'to': '1014464856'}, {'from': '1014464856', 'to': '773448863'}, {'from': '773448863', 'to': '988347957'}, {'from': '773448863', 'to': '152738454'}, {'from': '773448863', 'to': '338899146'}, {'from': '1014464856', 'to': '629986173'}, {'from': '629986173', 'to': '773448863'}, {'from': '629986173', 'to': '835742723'}, {'from': '1014464856', 'to': '835742723'}, {'from': '409719617', 'to': '81570852'}, {'from': '717256682', 'to': '136164004'}, {'from': '136164004', 'to': '852978894'}, {'from': '852978894', 'to': '344862780'}, {'from': '344862780', 'to': '1001389664'}, {'from': '1001389664', 'to': '404010795'}, {'from': '1001389664', 'to': '644174136'}, {'from': '644174136', 'to': '979597620'}, {'from': '979597620', 'to': '267068484'}, {'from': '979597620', 'to': '660658782'}, {'from': '644174136', 'to': '1041729484'}, {'from': '1041729484', 'to': '184754595'}, {'from': '184754595', 'to': '564383463'}, {'from': '564383463', 'to': '328736689'}, {'from': '564383463', 'to': '371898357'}, {'from': '371898357', 'to': '1035929373'}, {'from': '1035929373', 'to': '619697312'}, {'from': '619697312', 'to': '64229994'}, {'from': '619697312', 'to': '865071585'}, {'from': '619697312', 'to': '834626072'}, {'from': '1035929373', 'to': '201892784'}, {'from': '201892784', 'to': '160374239'}, {'from': '201892784', 'to': '925759772'}, {'from': '371898357', 'to': '601412432'}, {'from': '184754595', 'to': '371898357'}, {'from': '1041729484', 'to': '371898357'}, {'from': '344862780', 'to': '409719617'}, {'from': '852978894', 'to': '63704729'}, {'from': '136164004', 'to': '293710340'}, {'from': '514613187', 'to': '136164004'}, {'from': '859310299', 'to': '81570852'}, {'from': '859310299', 'to': '1014464856'}, {'from': '142291521', 'to': '985700044'}, {'from': '142291521', 'to': '756415350'}, {'from': '803634418', 'to': '420237319'}, {'from': '420237319', 'to': '450548638'}, {'from': '420237319', 'to': '210548489'}, {'from': '210548489', 'to': '809729654'}, {'from': '210548489', 'to': '736196011'}, {'from': '736196011', 'to': '763132131'}, {'from': '763132131', 'to': '139733908'}, {'from': '139733908', 'to': '141077435'}, {'from': '139733908', 'to': '601580192'}, {'from': '601580192', 'to': '29466216'}, {'from': '601580192', 'to': '530702767'}, {'from': '530702767', 'to': '1181832'}, {'from': '530702767', 'to': '514613187'}, {'from': '530702767', 'to': '1014464856'}, {'from': '139733908', 'to': '530702767'}, {'from': '763132131', 'to': '805599981'}, {'from': '805599981', 'to': '596402985'}, {'from': '805599981', 'to': '207631270'}, {'from': '207631270', 'to': '528636695'}, {'from': '207631270', 'to': '142291521'}, {'from': '805599981', 'to': '148019367'}, {'from': '148019367', 'to': '894038421'}, {'from': '148019367', 'to': '544426319'}, {'from': '148019367', 'to': '878212306'}, {'from': '878212306', 'to': '94541671'}, {'from': '878212306', 'to': '1007715424'}, {'from': '1007715424', 'to': '258386700'}, {'from': '1007715424', 'to': '546819439'}, {'from': '546819439', 'to': '836825089'}, {'from': '836825089', 'to': '16287329'}, {'from': '836825089', 'to': '256254716'}, {'from': '256254716', 'to': '631230382'}, {'from': '631230382', 'to': '900886483'}, {'from': '631230382', 'to': '133436503'}, {'from': '256254716', 'to': '751624200'}, {'from': '836825089', 'to': '716757473'}, {'from': '546819439', 'to': '470041669'}, {'from': '546819439', 'to': '180888016'}, {'from': '736196011', 'to': '901547914'}, {'from': '901547914', 'to': '425184961'}, {'from': '425184961', 'to': '760673978'}, {'from': '760673978', 'to': '825228914'}, {'from': '760673978', 'to': '530702767'}, {'from': '425184961', 'to': '955125232'}, {'from': '955125232', 'to': '167653392'}, {'from': '955125232', 'to': '530702767'}, {'from': '901547914', 'to': '530702767'}, {'from': '210548489', 'to': '640144001'}, {'from': '640144001', 'to': '135966238'}, {'from': '640144001', 'to': '959156288'}, {'from': '803634418', 'to': '358204164'}, {'from': '673966248', 'to': '803634418'}, {'from': '308264215', 'to': '1039602752'}, {'from': '1039602752', 'to': '673966248'} ] }; final exampleTrees = >[ { 'edges': [ {'from': '7045321', 'to': '308264215'}, {'from': '308264215', 'to': '205893853'}, {'from': '205893853', 'to': '673966248'}, {'from': '673966248', 'to': '358204164'}, {'from': '358204164', 'to': '215888392'}, {'from': '215888392', 'to': '403621992'}, {'from': '215888392', 'to': '777909510'}, {'from': '777909510', 'to': '100213815'}, {'from': '100213815', 'to': '499504374'}, {'from': '499504374', 'to': '855703404'}, {'from': '499504374', 'to': '991104907'}, {'from': '991104907', 'to': '374555325'}, {'from': '991104907', 'to': '58236163'}, {'from': '991104907', 'to': '1051662797'}, {'from': '1051662797', 'to': '523457656'}, {'from': '1051662797', 'to': '178236248'}, {'from': '178236248', 'to': '403818044'}, {'from': '403818044', 'to': '633692579'}, {'from': '403818044', 'to': '326876433'}, {'from': '178236248', 'to': '294992198'}, {'from': '178236248', 'to': '207728643'}, {'from': '207728643', 'to': '474861525'}, {'from': '207728643', 'to': '704015142'}, {'from': '704015142', 'to': '891912594'}, {'from': '704015142', 'to': '93790829'}, {'from': '704015142', 'to': '713878610'}, {'from': '704015142', 'to': '568109301'}, {'from': '100213815', 'to': '298138012'}, {'from': '298138012', 'to': '1051662797'}, {'from': '777909510', 'to': '344277619'}, {'from': '344277619', 'to': '311541390'}, {'from': '311541390', 'to': '761787449'}, {'from': '761787449', 'to': '30973213'}, {'from': '30973213', 'to': '523457656'}, {'from': '30973213', 'to': '178236248'}, {'from': '761787449', 'to': '259733602'}, {'from': '311541390', 'to': '128821445'}, {'from': '344277619', 'to': '1003131136'}, {'from': '1003131136', 'to': '130000569'}, {'from': '1003131136', 'to': '319536467'}, {'from': '319536467', 'to': '299942125'}, {'from': '299942125', 'to': '178926206'}, {'from': '299942125', 'to': '675835322'}, {'from': '299942125', 'to': '1000135767'}, {'from': '319536467', 'to': '483940059'}, {'from': '483940059', 'to': '497866879'}, {'from': '483940059', 'to': '606660618'}, {'from': '483940059', 'to': '841482899'}, {'from': '358204164', 'to': '963021319'}, {'from': '963021319', 'to': '130000569'}, {'from': '963021319', 'to': '319536467'}, {'from': '358204164', 'to': '803634418'}, {'from': '803634418', 'to': '142291521'}, {'from': '142291521', 'to': '525361131'}, {'from': '525361131', 'to': '422007713'}, {'from': '422007713', 'to': '184596308'}, {'from': '422007713', 'to': '1020140270'}, {'from': '422007713', 'to': '779910731'}, {'from': '525361131', 'to': '859310299'}, {'from': '859310299', 'to': '514613187'}, {'from': '514613187', 'to': '680752017'}, {'from': '680752017', 'to': '1058283666'}, {'from': '680752017', 'to': '887688252'}, {'from': '680752017', 'to': '717256682'}, {'from': '717256682', 'to': '409719617'}, {'from': '409719617', 'to': '1014464856'}, {'from': '1014464856', 'to': '773448863'}, {'from': '773448863', 'to': '988347957'}, {'from': '773448863', 'to': '152738454'}, {'from': '773448863', 'to': '338899146'}, {'from': '1014464856', 'to': '629986173'}, {'from': '629986173', 'to': '773448863'}, {'from': '629986173', 'to': '835742723'}, {'from': '1014464856', 'to': '835742723'}, {'from': '409719617', 'to': '81570852'}, {'from': '717256682', 'to': '136164004'}, {'from': '136164004', 'to': '852978894'}, {'from': '852978894', 'to': '344862780'}, {'from': '344862780', 'to': '1001389664'}, {'from': '1001389664', 'to': '404010795'}, {'from': '1001389664', 'to': '644174136'}, {'from': '644174136', 'to': '979597620'}, {'from': '979597620', 'to': '267068484'}, {'from': '979597620', 'to': '660658782'}, {'from': '644174136', 'to': '1041729484'}, {'from': '1041729484', 'to': '184754595'}, {'from': '184754595', 'to': '564383463'}, {'from': '564383463', 'to': '328736689'}, {'from': '564383463', 'to': '371898357'}, {'from': '371898357', 'to': '1035929373'}, {'from': '1035929373', 'to': '619697312'}, {'from': '619697312', 'to': '64229994'}, {'from': '619697312', 'to': '865071585'}, {'from': '619697312', 'to': '834626072'}, {'from': '1035929373', 'to': '201892784'}, {'from': '201892784', 'to': '160374239'}, {'from': '201892784', 'to': '925759772'}, {'from': '371898357', 'to': '601412432'}, {'from': '184754595', 'to': '371898357'}, {'from': '1041729484', 'to': '371898357'}, {'from': '344862780', 'to': '409719617'}, {'from': '852978894', 'to': '63704729'}, {'from': '136164004', 'to': '293710340'}, {'from': '514613187', 'to': '136164004'}, {'from': '859310299', 'to': '81570852'}, {'from': '859310299', 'to': '1014464856'}, {'from': '142291521', 'to': '985700044'}, {'from': '142291521', 'to': '756415350'}, {'from': '803634418', 'to': '420237319'}, {'from': '420237319', 'to': '450548638'}, {'from': '420237319', 'to': '210548489'}, {'from': '210548489', 'to': '809729654'}, {'from': '210548489', 'to': '736196011'}, {'from': '736196011', 'to': '763132131'}, {'from': '763132131', 'to': '139733908'}, {'from': '139733908', 'to': '141077435'}, {'from': '139733908', 'to': '601580192'}, {'from': '601580192', 'to': '29466216'}, {'from': '601580192', 'to': '530702767'}, {'from': '530702767', 'to': '1181832'}, {'from': '530702767', 'to': '514613187'}, {'from': '530702767', 'to': '1014464856'}, {'from': '139733908', 'to': '530702767'}, {'from': '763132131', 'to': '805599981'}, {'from': '805599981', 'to': '596402985'}, {'from': '805599981', 'to': '207631270'}, {'from': '207631270', 'to': '528636695'}, {'from': '207631270', 'to': '142291521'}, {'from': '805599981', 'to': '148019367'}, {'from': '148019367', 'to': '894038421'}, {'from': '148019367', 'to': '544426319'}, {'from': '148019367', 'to': '878212306'}, {'from': '878212306', 'to': '94541671'}, {'from': '878212306', 'to': '1007715424'}, {'from': '1007715424', 'to': '258386700'}, {'from': '1007715424', 'to': '546819439'}, {'from': '546819439', 'to': '836825089'}, {'from': '836825089', 'to': '16287329'}, {'from': '836825089', 'to': '256254716'}, {'from': '256254716', 'to': '631230382'}, {'from': '631230382', 'to': '900886483'}, {'from': '631230382', 'to': '133436503'}, {'from': '256254716', 'to': '751624200'}, {'from': '836825089', 'to': '716757473'}, {'from': '546819439', 'to': '470041669'}, {'from': '546819439', 'to': '180888016'}, {'from': '736196011', 'to': '901547914'}, {'from': '901547914', 'to': '425184961'}, {'from': '425184961', 'to': '760673978'}, {'from': '760673978', 'to': '825228914'}, {'from': '760673978', 'to': '530702767'}, {'from': '425184961', 'to': '955125232'}, {'from': '955125232', 'to': '167653392'}, {'from': '955125232', 'to': '530702767'}, {'from': '901547914', 'to': '530702767'}, {'from': '210548489', 'to': '640144001'}, {'from': '640144001', 'to': '135966238'}, {'from': '640144001', 'to': '959156288'}, {'from': '803634418', 'to': '358204164'}, {'from': '673966248', 'to': '803634418'}, {'from': '308264215', 'to': '1039602752'}, {'from': '1039602752', 'to': '673966248'} ] }, { 'edges': [ {'from': '651372822', 'to': '780273411'}, {'from': '780273411', 'to': '347969226'}, {'from': '347969226', 'to': '157648240'}, {'from': '157648240', 'to': '676569359'}, {'from': '676569359', 'to': '91606809'}, {'from': '676569359', 'to': '154477528'}, {'from': '676569359', 'to': '843017499'}, {'from': '843017499', 'to': '983981562'}, {'from': '843017499', 'to': '504040588'}, {'from': '504040588', 'to': '446062329'}, {'from': '446062329', 'to': '622974985'}, {'from': '622974985', 'to': '1044667060'}, {'from': '622974985', 'to': '556331086'}, {'from': '556331086', 'to': '995470137'}, {'from': '995470137', 'to': '1056219149'}, {'from': '1056219149', 'to': '239427950'}, {'from': '995470137', 'to': '239427950'}, {'from': '995470137', 'to': '175942639'}, {'from': '175942639', 'to': '239427950'}, {'from': '995470137', 'to': '914018177'}, {'from': '914018177', 'to': '239427950'}, {'from': '556331086', 'to': '776412718'}, {'from': '776412718', 'to': '311423239'}, {'from': '311423239', 'to': '71054174'}, {'from': '71054174', 'to': '436868910'}, {'from': '436868910', 'to': '86163114'}, {'from': '86163114', 'to': '876219077'}, {'from': '436868910', 'to': '385178969'}, {'from': '385178969', 'to': '18115125'}, {'from': '71054174', 'to': '869070735'}, {'from': '776412718', 'to': '71054174'}, {'from': '776412718', 'to': '978694637'}, {'from': '978694637', 'to': '71054174'}, {'from': '776412718', 'to': '481786088'}, {'from': '481786088', 'to': '71054174'}, {'from': '622974985', 'to': '657744632'}, {'from': '657744632', 'to': '995470137'}, {'from': '657744632', 'to': '776412718'}, {'from': '622974985', 'to': '398317434'}, {'from': '843017499', 'to': '441827615'}, {'from': '843017499', 'to': '345074369'}, {'from': '345074369', 'to': '983981562'}, {'from': '345074369', 'to': '504040588'}, {'from': '345074369', 'to': '441827615'}, {'from': '843017499', 'to': '1038969179'}, {'from': '1038969179', 'to': '983981562'}, {'from': '1038969179', 'to': '504040588'}, {'from': '1038969179', 'to': '441827615'}, {'from': '1038969179', 'to': '345074369'}, {'from': '676569359', 'to': '582216004'}, {'from': '582216004', 'to': '983981562'}, {'from': '582216004', 'to': '853366903'}, {'from': '853366903', 'to': '549040211'}, {'from': '549040211', 'to': '438987595'}, {'from': '438987595', 'to': '1044667060'}, {'from': '438987595', 'to': '927647245'}, {'from': '927647245', 'to': '995470137'}, {'from': '927647245', 'to': '286211157'}, {'from': '286211157', 'to': '466182692'}, {'from': '466182692', 'to': '724424756'}, {'from': '724424756', 'to': '739317534'}, {'from': '739317534', 'to': '315526883'}, {'from': '724424756', 'to': '869070735'}, {'from': '286211157', 'to': '724424756'}, {'from': '286211157', 'to': '175042175'}, {'from': '175042175', 'to': '724424756'}, {'from': '286211157', 'to': '567113513'}, {'from': '567113513', 'to': '724424756'}, {'from': '438987595', 'to': '625227999'}, {'from': '625227999', 'to': '995470137'}, {'from': '625227999', 'to': '286211157'}, {'from': '438987595', 'to': '398317434'}, {'from': '582216004', 'to': '441827615'}, {'from': '582216004', 'to': '306330186'}, {'from': '306330186', 'to': '983981562'}, {'from': '306330186', 'to': '853366903'}, {'from': '306330186', 'to': '441827615'}, {'from': '582216004', 'to': '476307185'}, {'from': '476307185', 'to': '983981562'}, {'from': '476307185', 'to': '853366903'}, {'from': '476307185', 'to': '441827615'}, {'from': '157648240', 'to': '1031140514'}, {'from': '1031140514', 'to': '983981562'}, {'from': '1031140514', 'to': '329379632'}, {'from': '1031140514', 'to': '441827615'}, {'from': '1031140514', 'to': '722519336'}, {'from': '722519336', 'to': '983981562'}, {'from': '722519336', 'to': '329379632'}, {'from': '722519336', 'to': '441827615'}, {'from': '722519336', 'to': '431136131'}, {'from': '431136131', 'to': '329379632'}, {'from': '1031140514', 'to': '431136131'}, {'from': '347969226', 'to': '91606809'}, {'from': '347969226', 'to': '154477528'}, {'from': '347969226', 'to': '843017499'}, {'from': '347969226', 'to': '582216004'}, {'from': '780273411', 'to': '383221931'} ], }, { 'edges': [ {'from': '426129611', 'to': '118422731'}, {'from': '118422731', 'to': '471276419'}, {'from': '471276419', 'to': '487798489'}, {'from': '487798489', 'to': '699788790'}, {'from': '699788790', 'to': '757478418'}, {'from': '757478418', 'to': '551628972'}, {'from': '551628972', 'to': '648068209'}, {'from': '648068209', 'to': '307394546'}, {'from': '307394546', 'to': '854888443'}, {'from': '648068209', 'to': '425563742'}, {'from': '425563742', 'to': '854888443'}, {'from': '648068209', 'to': '286239803'}, {'from': '286239803', 'to': '842737235'}, {'from': '842737235', 'to': '220477426'}, {'from': '220477426', 'to': '665742373'}, {'from': '665742373', 'to': '151341470'}, {'from': '151341470', 'to': '533503762'}, {'from': '665742373', 'to': '4111645'}, {'from': '4111645', 'to': '605315905'}, {'from': '842737235', 'to': '43693604'}, {'from': '43693604', 'to': '27818437'}, {'from': '27818437', 'to': '151341470'}, {'from': '27818437', 'to': '4111645'}, {'from': '286239803', 'to': '746039689'}, {'from': '746039689', 'to': '153736120'}, {'from': '153736120', 'to': '8283699'}, {'from': '8283699', 'to': '443674830'}, {'from': '746039689', 'to': '749435060'}, {'from': '749435060', 'to': '8283699'}, {'from': '551628972', 'to': '286239803'}, {'from': '757478418', 'to': '155543721'}, {'from': '155543721', 'to': '648068209'}, {'from': '155543721', 'to': '286239803'}, {'from': '699788790', 'to': '551628972'}, {'from': '487798489', 'to': '180915528'}, {'from': '180915528', 'to': '699788790'}, {'from': '180915528', 'to': '757478418'}, {'from': '118422731', 'to': '221121315'}, {'from': '221121315', 'to': '487798489'} ] }, { 'edges': [ {'from': '925654661', 'to': '280745044'}, {'from': '280745044', 'to': '539063600'}, {'from': '539063600', 'to': '936176129'}, {'from': '936176129', 'to': '947166361'}, {'from': '947166361', 'to': '178822296'}, {'from': '178822296', 'to': '735844241'}, {'from': '735844241', 'to': '614582366'}, {'from': '614582366', 'to': '535521044'}, {'from': '535521044', 'to': '1007301027'}, {'from': '535521044', 'to': '245744777'}, {'from': '735844241', 'to': '518569195'}, {'from': '518569195', 'to': '535521044'}, {'from': '936176129', 'to': '106575972'}, {'from': '106575972', 'to': '178822296'}, {'from': '925654661', 'to': '303124879'}, {'from': '303124879', 'to': '539063600'} ] }, exampleTreeWith140Nodes, ]; ================================================ FILE: test/graph_test.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:graphview/GraphView.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Graph', () { test('Graph Node counts are correct', () { final graph = Graph(); var node1 = Node.Id('One'); var node2 = Node.Id('Two'); var node3 = Node.Id('Three'); var node4 = Node.Id('Four'); var node5 = Node.Id('Five'); var node6 = Node.Id('Six'); var node7 = Node.Id('Seven'); var node8 = Node.Id('Eight'); var node9 = Node.Id('Nine'); graph.addEdge(node1, node2); graph.addEdge(node1, node4); graph.addEdge(node2, node3); graph.addEdge(node2, node5); graph.addEdge(node3, node6); graph.addEdge(node4, node5); graph.addEdge(node4, node7); graph.addEdge(node5, node6); graph.addEdge(node5, node8); graph.addEdge(node6, node9); graph.addEdge(node7, node8); graph.addEdge(node8, node9); expect(graph.nodeCount(), 9); graph.removeNode(Node.Id('One')); graph.removeNode(Node.Id('Ten')); expect(graph.nodeCount(), 8); graph.addNode(Node.Id('Ten')); expect(graph.nodeCount(), 9); }); test('Node Hash Implementation is performant', () { final graph = Graph(); var rows = 1000000; var integerNode = Node.Id(1); var stringNode = Node.Id('123'); var stringNode2 = Node.Id('G9Q84H1R9-1619338713.000900'); var widgetNode = Node.Id(Text('Lovely')); var widgetNode2 = Node.Id(Text('Lovely')); var doubleNode = Node.Id(5.6); var edge = graph.addEdge(integerNode, Node.Id(4)); var nodes = [ integerNode, stringNode, stringNode2, widgetNode, widgetNode2, doubleNode ]; for (var node in nodes) { var stopwatch = Stopwatch() ..start(); for (var i = 1; i <= rows; i++) { var hash = node.hashCode; } var timeTaken = stopwatch.elapsed.inMilliseconds; print('Time taken: $timeTaken ms for ${node.runtimeType} node'); expect(timeTaken < 100, true); } }); test('Graph does not duplicate nodes for self loops', () { final graph = Graph(); final node = Node.Id('self'); graph.addEdge(node, node); expect(graph.nodes.length, 1); expect(graph.edges.length, 1); expect(graph.nodes.single, node); }); test('ArrowEdgeRenderer builds self-loop path', () { final renderer = ArrowEdgeRenderer(); final node = Node.Id('self') ..size = const Size(40, 40) ..position = const Offset(100, 100); final edge = Edge(node, node); final result = renderer.buildSelfLoopPath(edge); expect(result, isNotNull); final metrics = result!.path.computeMetrics().toList(); expect(metrics, isNotEmpty); final metric = metrics.first; expect(metric.length, greaterThan(0)); expect(result.arrowTip, isNot(equals(const Offset(0, 0)))); final tangentStart = metric.getTangentForOffset(0); expect(tangentStart, isNotNull); expect(tangentStart!.vector.dy.abs(), lessThan(tangentStart.vector.dx.abs() * 0.1)); expect(tangentStart.vector.dx, greaterThan(0)); final tangentEnd = metric.getTangentForOffset(metric.length); expect(tangentEnd, isNotNull); expect(tangentEnd!.vector.dx.abs(), lessThan(tangentEnd.vector.dy.abs() * 0.1)); expect(tangentEnd.vector.dy, greaterThan(0)); }); test('SugiyamaAlgorithm handles single node self loop', () { final graph = Graph(); final node = Node.Id('self') ..size = const Size(40, 40); graph.addEdge(node, node); final config = SugiyamaConfiguration() ..nodeSeparation = 20 ..levelSeparation = 20; final algorithm = SugiyamaAlgorithm(config); expect(() => algorithm.run(graph, 0, 0), returnsNormally); expect(graph.nodes.length, 1); }); }); } ================================================ FILE: test/graphview_perfomance_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:graphview/GraphView.dart'; void main() { group('GraphView Performance Tests', () { testWidgets('hitTest performance with 1000+ nodes less than 20s', (WidgetTester tester) async { final graph = _createLargeGraph(1000); final _configuration = BuchheimWalkerConfiguration() ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); var algorithm = BuchheimWalkerAlgorithm( _configuration, TreeEdgeRenderer(_configuration)); await tester.pumpWidget(MaterialApp( home: Scaffold( body: GraphView.builder( graph: graph, algorithm: algorithm, builder: (Node node) => Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), child: Center(child: Text(node.key.toString())), ), ), ), )); await tester.pumpAndSettle(); final renderBox = tester.renderObject( find.byType(GraphViewWidget) ); final stopwatch = Stopwatch()..start(); // Test multiple hit tests at different positions for (var i = 0; i < 10; i++) { final result = BoxHitTestResult(); renderBox.hitTest(result, position: Offset(i * 10.0, i * 10.0)); } stopwatch.stop(); final hitTestTime = stopwatch.elapsedMilliseconds; print('HitTest time for 1000 nodes (10 tests): ${hitTestTime}ms'); expect(hitTestTime, lessThan(20), reason: 'HitTest should complete in under 10ms'); }); testWidgets('paint performance with 1000+ nodes', (WidgetTester tester) async { final graph = _createLargeGraph(1000); final _configuration = BuchheimWalkerConfiguration() ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); var algorithm = BuchheimWalkerAlgorithm( _configuration, TreeEdgeRenderer(_configuration)); await tester.pumpWidget(MaterialApp( home: Scaffold( body: GraphView.builder( graph: graph, algorithm: algorithm, builder: (Node node) => Container( width: 30, height: 30, color: Colors.red, ), ), ), )); final stopwatch = Stopwatch()..start(); // Trigger multiple repaints for (var i = 0; i < 10; i++) { await tester.pump(); } stopwatch.stop(); final paintTime = stopwatch.elapsedMilliseconds; print('Paint time for 1000 nodes (10 repaints): ${paintTime}ms'); expect(paintTime, lessThan(50), reason: 'Paint should complete in under 50ms'); }); test('algorithm run performance with 1000+ nodes', () { final graph = _createLargeGraph(1000); final _configuration = BuchheimWalkerConfiguration() ..siblingSeparation = (100) ..levelSeparation = (150) ..subtreeSeparation = (150) ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); var algorithm = BuchheimWalkerAlgorithm( _configuration, TreeEdgeRenderer(_configuration)); final stopwatch = Stopwatch()..start(); algorithm.run(graph, 0, 0); stopwatch.stop(); final algorithmTime = stopwatch.elapsedMilliseconds; print('Algorithm run time for 1000 nodes: ${algorithmTime}ms'); expect(algorithmTime, lessThan(10), reason: 'Algorithm should complete in under 10 milisecond'); }); }); } /// Creates a large graph with connected nodes for performance testing Graph _createLargeGraph(int n) { final graph = Graph(); // Create nodes final nodes = List.generate(n, (i) => Node.Id(i + 1)); // Generate tree edges using a queue-based approach var currentChild = 1; // Start from node 1 (node 0 is root) for (var i = 0; i < n && currentChild < n; i++) { final children = (i < n ~/ 3) ? 3 : 2; for (var j = 0; j < children && currentChild < n; j++) { graph.addEdge(nodes[i], nodes[currentChild]); currentChild++; } } return graph; } ================================================ FILE: test/sugiyama_algorithm_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:graphview/GraphView.dart'; import 'example_trees.dart'; const itemHeight = 100.0; const itemWidth = 100.0; extension on Graph { void inflateWithJson(Map json) { var edges = json['edges']! as List; edges.forEach((element) { var fromNodeId = element['from']; var toNodeId = element['to']; addEdge(Node.Id(fromNodeId), Node.Id(toNodeId)); }); } } extension on Node { Rect toRect() => Rect.fromLTRB(x, y, x + width, y + height); } void main() { group('Sugiyama Graph', () { final graph = Graph(); final node1 = Node.Id(1); final node2 = Node.Id(2); final node3 = Node.Id(3); final node4 = Node.Id(4); final node5 = Node.Id(5); final node6 = Node.Id(6); final node8 = Node.Id(7); final node7 = Node.Id(8); final node9 = Node.Id(9); final node10 = Node.Id(10); final node11 = Node.Id(11); final node12 = Node.Id(12); final node13 = Node.Id(13); final node14 = Node.Id(14); final node15 = Node.Id(15); final node16 = Node.Id(16); final node17 = Node.Id(17); final node18 = Node.Id(18); final node19 = Node.Id(19); final node20 = Node.Id(20); final node21 = Node.Id(21); final node22 = Node.Id(22); final node23 = Node.Id(23); graph.addEdge(node1, node13, paint: Paint()..color = Colors.red); graph.addEdge(node1, node21); graph.addEdge(node1, node4); graph.addEdge(node1, node3); graph.addEdge(node2, node3); graph.addEdge(node2, node20); graph.addEdge(node3, node4); graph.addEdge(node3, node5); graph.addEdge(node3, node23); graph.addEdge(node4, node6); graph.addEdge(node5, node7); graph.addEdge(node6, node8); graph.addEdge(node6, node16); graph.addEdge(node6, node23); graph.addEdge(node7, node9); graph.addEdge(node8, node10); graph.addEdge(node8, node11); graph.addEdge(node9, node12); graph.addEdge(node10, node13); graph.addEdge(node10, node14); graph.addEdge(node10, node15); graph.addEdge(node11, node15); graph.addEdge(node11, node16); graph.addEdge(node12, node20); graph.addEdge(node13, node17); graph.addEdge(node14, node17); graph.addEdge(node14, node18); graph.addEdge(node16, node18); graph.addEdge(node16, node19); graph.addEdge(node16, node20); graph.addEdge(node18, node21); graph.addEdge(node19, node22); graph.addEdge(node21, node23); graph.addEdge(node22, node23); graph.addEdge(node1, node22); graph.addEdge(node7, node8); test('Sugiyama for unconnected nodes', () { final graph = Graph(); graph.addEdge(Node.Id(1), Node.Id(3)); graph.addEdge(Node.Id(4), Node.Id(7)); final _configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; var algorithm = SugiyamaAlgorithm(_configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; expect(timeTaken < 1000, true); expect(graph.getNodeUsingId(1).position, Offset(10.0, 10.0)); expect(graph.getNodeUsingId(3).position, Offset(125.0, 10.0)); expect(size, Size(215.0, 215.0)); }); test('Sugiyama for a single directional graph', () { final graph = Graph(); graph.addEdge(Node.Id(1), Node.Id(3)); graph.addEdge(Node.Id(3), Node.Id(4)); graph.addEdge(Node.Id(4), Node.Id(7)); graph.addEdge(Node.Id(7), Node.Id(9)); graph.addEdge(Node.Id(9), Node.Id(111)); final _configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; var algorithm = SugiyamaAlgorithm(_configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; expect(timeTaken < 1000, true); expect(graph.getNodeUsingId(1).position, Offset(10.0, 10.0)); expect(graph.getNodeUsingId(3).position, Offset(125.0, 10.0)); expect(graph.getNodeUsingId(9).position, Offset(470.0, 10.0)); expect(size, Size(675.0, 100.0)); }); test('Sugiyama for a cyclic graph', () { final graph = Graph(); graph.addEdge(Node.Id(1), Node.Id(3)); graph.addEdge(Node.Id(3), Node.Id(4)); graph.addEdge(Node.Id(4), Node.Id(7)); graph.addEdge(Node.Id(7), Node.Id(9)); graph.addEdge(Node.Id(9), Node.Id(1)); final _configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; var algorithm = SugiyamaAlgorithm(_configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; expect(timeTaken < 1000, true); expect(graph.getNodeUsingId(1).position, Offset(125.0, 10.0)); expect(graph.getNodeUsingId(3).position, Offset(240.0, 10.0)); expect(graph.getNodeUsingId(9).position, Offset(10.0, 17.5)); expect(size, Size(560.0, 157.5)); }); group('Layering Strategy Tests', () { test('TopDown Strategy - Node Positioning TOP_BOTTOM', () { final _configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..layeringStrategy = LayeringStrategy.topDown ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; var algorithm = SugiyamaAlgorithm(_configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print( 'TopDown Strategy TOP_BOTTOM - Time: ${timeTaken}ms, Size: $size'); expect(timeTaken < 1000, true); expect(graph.getNodeAtPosition(0).position, Offset(660.0, 10)); expect(graph.getNodeAtPosition(6).position, Offset(1180.0, 815.0)); expect(graph.getNodeAtPosition(13).position, Offset(1180.0, 470.0)); expect(graph.getNodeAtPosition(22).position, Offset(790, 930.0)); expect(graph.getNodeAtPosition(3).position, Offset(660.0, 240.0)); expect(graph.getNodeAtPosition(4).position, Offset(920.0, 125.0)); expect(size, Size(1270.0, 1135.0)); }); test('TopDown Strategy - Node Positioning LEFT_RIGHT', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..layeringStrategy = LayeringStrategy.topDown ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; var algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print( 'TopDown Strategy LEFT_RIGHT - Time: ${timeTaken}ms, Size: $size'); expect(timeTaken < 1000, true); expect(graph.getNodeAtPosition(0).position, Offset(10, 385.0)); expect(graph.getNodeAtPosition(6).position, Offset(815.0, 745.0)); expect(graph.getNodeAtPosition(13).position, Offset(470.0, 745.0)); expect(graph.getNodeAtPosition(22).position, Offset(930, 500.0)); expect(graph.getNodeUsingId(3).position, Offset(125.0, 465.0)); expect(graph.getNodeUsingId(4).position, Offset(240.0, 342.5)); expect(size, Size(1135.0, 835.0)); }); test('LongestPath Strategy - Node Positioning', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..layeringStrategy = LayeringStrategy.longestPath ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('LongestPath Strategy - Time: ${timeTaken}ms, Size: $size'); expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10)); expect(graph.getNodeAtPosition(6).position, Offset(1505.0, 1045.0)); expect(graph.getNodeAtPosition(13).position, Offset(1700.0, 815.0)); expect(graph.getNodeAtPosition(22).position, Offset(985.0, 930.0)); expect(graph.getNodeAtPosition(3).position, Offset(725.0, 240.0)); expect(graph.getNodeAtPosition(4).position, Offset(1115.0, 125.0)); expect(timeTaken < 1000, true); expect(size, Size(1660.0, 1135.0)); }); test('CoffmanGraham Strategy - Node Positioning', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..layeringStrategy = LayeringStrategy.coffmanGraham ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('CoffmanGraham Strategy - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(0).position, Offset(1440.0, 10.0)); expect(graph.getNodeAtPosition(6).position, Offset(335.0, 1160.0)); expect(graph.getNodeAtPosition(13).position, Offset(140.0, 470.0)); expect(graph.getNodeAtPosition(22).position, Offset(400.0, 1045.0)); expect(graph.getNodeAtPosition(3).position, Offset(1375.0, 240.0)); expect(graph.getNodeAtPosition(4).position, Offset(1050.0, 125.0)); expect(timeTaken < 1000, true); expect(size, Size(1530.0, 1250.0)); }); test('NetworkSimplex Strategy - Node Positioning', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..layeringStrategy = LayeringStrategy.networkSimplex ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('NetworkSimplex Strategy - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10.0)); expect(graph.getNodeAtPosition(6).position, Offset(1505.0, 1045.0)); expect(graph.getNodeAtPosition(13).position, Offset(1700.0, 815.0)); expect(graph.getNodeAtPosition(22).position, Offset(985.0, 930.0)); expect(graph.getNodeAtPosition(3).position, Offset(725.0, 240.0)); expect(graph.getNodeAtPosition(4).position, Offset(1115.0, 125.0)); expect(timeTaken < 1000, true); expect(size, Size(1660.0, 1135.0)); }); }); group('Cross Minimization Strategy Tests', () { test('Simple CrossMinimization - Positioning', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..crossMinimizationStrategy = CrossMinimizationStrategy.simple ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('Simple CrossMin - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(6).position, Offset(815.0, 745.0)); expect(graph.getNodeAtPosition(13).position, Offset(470.0, 745.0)); expect(graph.getNodeAtPosition(22).position, Offset(930.0, 500.0)); expect(graph.getNodeAtPosition(3).position, Offset(240.0, 342.5)); expect(graph.getNodeAtPosition(4).position, Offset(125.0, 465.0)); expect(timeTaken < 1000, true); expect(size, Size(1135.0, 835.0)); }); test('AccumulatorTree CrossMinimization - Positioning', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..crossMinimizationStrategy = CrossMinimizationStrategy.accumulatorTree ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('AccumulatorTree CrossMin - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(0).position, Offset(10.0, 385.0)); expect(graph.getNodeAtPosition(6).position, Offset(815.0, 715.0)); expect(graph.getNodeAtPosition(13).position, Offset(470.0, 715.0)); expect(graph.getNodeAtPosition(22).position, Offset(930.0, 470.0)); expect(graph.getNodeAtPosition(3).position, Offset(240.0, 342.5)); expect(graph.getNodeAtPosition(4).position, Offset(125.0, 465.0)); expect(timeTaken < 1000, true); expect(size, Size(1135.0, 805.0)); }); }); // Test Cycle Removal Strategies group('Cycle Removal Strategy Tests', () { final graph = Graph(); final node1 = Node.Id(1); final node2 = Node.Id(2); final node3 = Node.Id(3); final node4 = Node.Id(4); final node5 = Node.Id(5); // Create a cyclic graph graph.addEdge(node1, node2); graph.addEdge(node2, node3); graph.addEdge(node3, node4); graph.addEdge(node4, node1); // Creates cycle graph.addEdge(node2, node5); test('DFS Cycle Removal - Positioning', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..cycleRemovalStrategy = CycleRemovalStrategy.dfs ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('DFS Cycle Removal - Time: ${timeTaken}ms, Size: $size'); // Test exact positions - layout should be acyclic expect(graph.getNodeAtPosition(1).position, Offset(75.0, 125.0)); expect(graph.getNodeAtPosition(2).position, Offset(10.0, 240.0)); expect(graph.getNodeAtPosition(3).position, Offset(140.0, 355.0)); expect(timeTaken < 1000, true); expect(size, Size(230, 445.0)); }); test('Greedy Cycle Removal - Positioning', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..cycleRemovalStrategy = CycleRemovalStrategy.greedy ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('Greedy Cycle Removal - Time: ${timeTaken}ms, Size: $size'); // Test exact positions - layout should be acyclic expect(graph.getNodeAtPosition(1).position, Offset(75.0, 240.0)); expect(graph.getNodeAtPosition(2).position, Offset(140.0, 355.0)); expect(graph.getNodeAtPosition(3).position, Offset(75.0, 10.0)); expect(timeTaken < 1000, true); expect(size, Size(230.0, 445.0)); }); }); group('Coordinate Assignment Strategy Tests', () { test('DownRight Coordinate Assignment', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..coordinateAssignment = CoordinateAssignment.DownRight ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('DownRight Assignment - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(1).position, Offset(790.0, 700.0)); expect(graph.getNodeAtPosition(2).position, Offset(1050.0, 930.0)); expect(graph.getNodeAtPosition(3).position, Offset(530.0, 240.0)); expect(timeTaken < 1000, true); expect(size, Size(1790.0, 1135.0)); }); test('DownLeft Coordinate Assignment', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..coordinateAssignment = CoordinateAssignment.DownLeft ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('DownLeft Assignment - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(0).position, Offset(1310.0, 10.0)); expect(graph.getNodeAtPosition(6).position, Offset(1180.0, 815.0)); expect(graph.getNodeAtPosition(22).position, Offset(530.0, 930.0)); expect(graph.getNodeUsingId(3).position, Offset(1310.0, 125.0)); expect(graph.getNodeUsingId(4).position, Offset(920.0, 240.0)); expect(timeTaken < 1000, true); expect(size, Size(1530.0, 1135.0)); }); test('Average Coordinate Assignment', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..coordinateAssignment = CoordinateAssignment.Average ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('Average Assignment - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(0).position, Offset(660.0, 10.0)); expect(graph.getNodeAtPosition(13).position, Offset(1180.0, 470.0)); expect(graph.getNodeAtPosition(22).position, Offset(790, 930.0)); expect(graph.getNodeUsingId(3).position, Offset(920.0, 125.0)); expect(graph.getNodeUsingId(4).position, Offset(660.0, 240.0)); expect(timeTaken < 1000, true); expect(size, Size(1270.0, 1135.0)); }); test('UpRight Coordinate Assignment', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..coordinateAssignment = CoordinateAssignment.UpRight ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('UpRight Assignment - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10.0)); expect(graph.getNodeAtPosition(6).position, Offset(1050.0, 815.0)); expect(graph.getNodeAtPosition(13).position, Offset(1050.0, 470.0)); expect(graph.getNodeAtPosition(22).position, Offset(400.0, 930.0)); expect(graph.getNodeAtPosition(3).position, Offset(400.0, 240.0)); expect(graph.getNodeAtPosition(4).position, Offset(1050.0, 125.0)); expect(timeTaken < 1000, true); expect(size, Size(1140.0, 1135.0)); }); test('UpLeft Coordinate Assignment', () { final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..coordinateAssignment = CoordinateAssignment.UpLeft ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); var size = algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('UpLeft Assignment - Time: ${timeTaken}ms, Size: $size'); // Test exact positions expect(graph.getNodeAtPosition(0).position, Offset(140.0, 10.0)); expect(graph.getNodeAtPosition(6).position, Offset(1440.0, 815.0)); expect(graph.getNodeAtPosition(13).position, Offset(1440.0, 470.0)); expect(graph.getNodeAtPosition(22).position, Offset(1310.0, 930.0)); expect(graph.getNodeAtPosition(3).position, Offset(270.0, 240.0)); expect(graph.getNodeAtPosition(4).position, Offset(1440.0, 125.0)); expect(timeTaken < 1000, true); expect(size, Size(1660.0, 1135.0)); }); }); // Performance Tests for 140 Node Graph group('140 Node Graph Performance Tests', () { test('Layering Strategy Performance Comparison - 140 Nodes', () { print('\n=== 140 Node Graph - Layering Strategy Performance ==='); final strategies = [ {'strategy': LayeringStrategy.topDown, 'name': 'TopDown'}, {'strategy': LayeringStrategy.longestPath, 'name': 'LongestPath'}, {'strategy': LayeringStrategy.coffmanGraham, 'name': 'CoffmanGraham'}, { 'strategy': LayeringStrategy.networkSimplex, 'name': 'NetworkSimplex' }, ]; for (final strategy in strategies) { final graph = Graph(); graph.inflateWithJson(exampleTreeWith140Nodes); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..layeringStrategy = strategy['strategy'] as LayeringStrategy ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); final stopwatch = Stopwatch()..start(); final size = algorithm.run(graph, 10, 10); final timeTaken = stopwatch.elapsed.inMilliseconds; print( '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}'); expect(timeTaken < 3000, true, reason: '${strategy['name']} should complete within 3 seconds for 140 nodes'); } }); test('CrossMinimization Strategy Performance - 140 Nodes', () { print('\n=== 140 Node Graph - Cross Minimization Performance ==='); final strategies = [ {'strategy': CrossMinimizationStrategy.simple, 'name': 'Simple'}, { 'strategy': CrossMinimizationStrategy.accumulatorTree, 'name': 'AccumulatorTree' }, ]; for (final strategy in strategies) { final graph = Graph(); graph.inflateWithJson(exampleTreeWith140Nodes); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..crossMinimizationStrategy = strategy['strategy'] as CrossMinimizationStrategy ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); final stopwatch = Stopwatch()..start(); final size = algorithm.run(graph, 10, 10); final timeTaken = stopwatch.elapsed.inMilliseconds; print( '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}'); expect(timeTaken < 3000, true, reason: '${strategy['name']} should complete within 3 seconds'); } }); test('Cycle Removal Strategy Performance - 140 Nodes', () { print('\n=== 140 Node Graph - Cycle Removal Performance ==='); final strategies = [ {'strategy': CycleRemovalStrategy.dfs, 'name': 'DFS'}, {'strategy': CycleRemovalStrategy.greedy, 'name': 'Greedy'}, ]; for (final strategy in strategies) { final graph = Graph(); graph.inflateWithJson(exampleTreeWith140Nodes); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..cycleRemovalStrategy = strategy['strategy'] as CycleRemovalStrategy ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); final stopwatch = Stopwatch()..start(); final size = algorithm.run(graph, 10, 10); final timeTaken = stopwatch.elapsed.inMilliseconds; print( '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}'); expect(timeTaken < 3000, true, reason: '${strategy['name']} should complete within 3 seconds'); } }); test('Coordinate Assignment Performance - 140 Nodes', () { print('\n=== 140 Node Graph - Coordinate Assignment Performance ==='); final strategies = [ {'strategy': CoordinateAssignment.DownRight, 'name': 'DownRight'}, {'strategy': CoordinateAssignment.DownLeft, 'name': 'DownLeft'}, {'strategy': CoordinateAssignment.UpRight, 'name': 'UpRight'}, {'strategy': CoordinateAssignment.UpLeft, 'name': 'UpLeft'}, {'strategy': CoordinateAssignment.Average, 'name': 'Average'}, ]; for (final strategy in strategies) { final graph = Graph(); graph.inflateWithJson(exampleTreeWith140Nodes); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } final configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..coordinateAssignment = strategy['strategy'] as CoordinateAssignment ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = true; final algorithm = SugiyamaAlgorithm(configuration); final stopwatch = Stopwatch()..start(); final size = algorithm.run(graph, 10, 10); final timeTaken = stopwatch.elapsed.inMilliseconds; print( '${strategy['name']}: ${timeTaken}ms - Layout size: $size - Nodes: ${graph.nodeCount()}'); expect(timeTaken < 3000, true, reason: '${strategy['name']} should complete within 3 seconds'); } }); }); test('PostStraighten Effect on Node Positioning', () { // Test with PostStraighten ON for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } final configurationOn = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM ..postStraighten = false; final algorithmOn = SugiyamaAlgorithm(configurationOn); algorithmOn.run(graph, 10, 10); expect(graph.getNodeAtPosition(0).position, Offset(660.0, 10)); expect(graph.getNodeAtPosition(6).position, Offset(1180.0, 815.0)); expect(graph.getNodeUsingId(3).position, Offset(920.0, 125.0)); }); test('Sugiyama for a complex graph with 140 nodes', () { final json = exampleTreeWith140Nodes; final graph = Graph(); var edges = json['edges']!; edges.forEach((element) { var fromNodeId = element['from']; var toNodeId = element['to']; graph.addEdge(Node.Id(fromNodeId), Node.Id(toNodeId)); }); final _configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; final algorithm = SugiyamaAlgorithm(_configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('Timetaken $timeTaken ${graph.nodeCount()}'); expect(graph.getNodeAtPosition(0).position, Offset(10.0, 1715.0)); expect(graph.getNodeAtPosition(6).position, Offset(815.0, 1757.5)); expect(graph.getNodeAtPosition(10).position, Offset(1160.0, 1872.5)); expect(graph.getNodeAtPosition(13).position, Offset(1275.0, 2117.5)); expect(graph.getNodeAtPosition(22).position, Offset(1620.0, 2635.0)); expect(graph.getNodeAtPosition(50).position, Offset(1505.0, 1232.5)); expect(graph.getNodeAtPosition(67).position, Offset(2655.0, 1700.0)); expect(graph.getNodeAtPosition(100).position, Offset(815.0, 412.5)); expect(graph.getNodeAtPosition(122).position, Offset(1735.0,2060.0)); }); test('Sugiyama child nodes never overlaps', () { for (final json in exampleTrees) { final graph = Graph()..inflateWithJson(json); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); SugiyamaAlgorithm(SugiyamaConfiguration()..postStraighten = true) ..run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('Timetaken $timeTaken ${graph.nodeCount()}'); for (var i = 0; i < graph.nodeCount(); i++) { final currentNode = graph.getNodeAtPosition(i); for (var j = 0; j < graph.nodeCount(); j++) { final otherNode = graph.getNodeAtPosition(j); if (currentNode.key == otherNode.key) continue; final currentRect = currentNode.toRect(); final otherRect = otherNode.toRect(); final overlaps = currentRect.overlaps(otherRect); expect(false, overlaps, reason: '$currentNode overlaps $otherNode'); } } } }); test('Sugiyama Performance for 100 nodes to be less than 5.2s', () { final graph = Graph(); var rows = 100; for (var i = 1; i <= rows; i++) { for (var j = 1; j <= i; j++) { graph.addEdge(Node.Id(i), Node.Id(j)); } } final _configuration = SugiyamaConfiguration() ..nodeSeparation = 15 ..levelSeparation = 15 ..orientation = SugiyamaConfiguration.ORIENTATION_LEFT_RIGHT ..postStraighten = true; var algorithm = SugiyamaAlgorithm(_configuration); for (var i = 0; i < graph.nodeCount(); i++) { graph.getNodeAtPosition(i).size = Size(itemWidth, itemHeight); } var stopwatch = Stopwatch()..start(); algorithm.run(graph, 10, 10); var timeTaken = stopwatch.elapsed.inMilliseconds; print('Timetaken $timeTaken ${graph.nodeCount()}'); expect(timeTaken < 5200, true); }); }); }