Showing preview only (459K chars total). Download the full file or copy to clipboard to get everything.
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
[](https://pub.dev/packages/graphview)
[](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.



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.

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.

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.

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.

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.

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<TreeViewPage> createState() => _TreeViewPageState();
}
class _TreeViewPageState extends State<TreeViewPage> {
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

#### Rooted Tree (Bottom to Top)

#### Rooted Tree (Left to Right)

#### Rooted Tree (Right to Left)

#### Directed Graph


#### Layered Graph

#### Balloon Layout

#### Circular Layout

#### Radial Tree Layout

#### Tidier Tree Layout

#### Mindmap Layout

#### Node Expand/Collapse Animation

#### Auto Navigation

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<AlgorithmSelectedVIewPage> 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<LayoutAlgorithmType>(
value: _selectedAlgorithm,
isExpanded: true,
onChanged: (LayoutAlgorithmType? newValue) {
if (newValue != null) {
setState(() {
_selectedAlgorithm = newValue;
_updateAlgorithm();
});
}
},
items: LayoutAlgorithmType.values.map<DropdownMenuItem<LayoutAlgorithmType>>((LayoutAlgorithmType value) {
return DropdownMenuItem<LayoutAlgorithmType>(
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<DecisionTreeScreen> {
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<GraphClusterViewPage> {
@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<GraphScreen> {
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<void> 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<LargeTreeViewPage> 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<LayeredEiglspergerGraphViewPage> {
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<CoordinateAssignment>(
value: builder.coordinateAssignment,
items: CoordinateAssignment.values.map((coordinateAssignment) {
return DropdownMenuItem<CoordinateAssignment>(
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<LayeredGraphViewPage> {
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<CoordinateAssignment>('Alignment', builder.coordinateAssignment, CoordinateAssignment.values, (v) => builder.coordinateAssignment = v),
_buildDropdown<LayeringStrategy>('Layering', builder.layeringStrategy, LayeringStrategy.values, (v) => builder.layeringStrategy = v),
_buildDropdown<CrossMinimizationStrategy>('Cross Min', builder.crossMinimizationStrategy, CrossMinimizationStrategy.values, (v) => builder.crossMinimizationStrategy = v),
_buildDropdown<CycleRemovalStrategy>('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<T>(String label, T value, List<T> 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<T>(
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<LayerGraphPageFromJson> {
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<CoordinateAssignment>(
value: builder.coordinateAssignment,
items: CoordinateAssignment.values.map((coordinateAssignment) {
return DropdownMenuItem<CoordinateAssignment>(
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<Widget> buttons) {
return Container(
margin: EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 4, bottom: 12),
child: Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: -0.3,
),
),
),
...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<MindMapPage> 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<MultipleForestTreeViewPage> 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<TreeViewPage> 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<TreeViewPageFromJson> {
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<Node> _nodes = [];
final List<Edge> _edges = [];
List<GraphObserver> graphObserver = [];
// Cache
final Map<Node, List<Node>> _successorCache = {};
final Map<Node, List<Node>> _predecessorCache = {};
bool _cacheValid = false;
List<Node> get nodes => _nodes;
List<Edge> get edges => _edges;
var isTree = false;
int nodeCount() => _nodes.length;
void addNode(Node node) {
_nodes.add(node);
_cacheValid = false;
notifyGraphObserver();
}
void addNodes(List<Node> 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<Node> 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<Edge> edges) => edges.forEach((it) => addEdgeS(it));
void removeEdge(Edge edge) {
_edges.remove(edge);
_cacheValid = false;
}
void removeEdges(List<Edge> 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<Node> successorsOf(Node? node) {
if (node == null) return [];
if (!_cacheValid) _buildCache();
return _successorCache[node] ?? [];
}
bool hasPredecessor(Node node) => predecessorsOf(node).isNotEmpty;
List<Node> 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<Edge> getOutEdges(Node node) =>
_edges.where((element) => element.source == node).toList();
List<Edge> 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<Node, bool> collapsedNodes = {};
final Map<Node, bool> expandingNodes = {};
final Map<Node, Node> 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<Edge> getCollapsingEdges(Graph graph) {
if (collapsedNode == null) return [];
return graph.edges.where((edge) {
return hiddenBy[edge.destination] == collapsedNode;
}).toList();
}
List<Edge> getExpandingEdges(Graph graph) {
final expandingEdges = <Edge>[];
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<Node> 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<ValueKey> 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<GraphView> with TickerProviderStateMixin {
late TransformationController _transformationController;
late final AnimationController _panController;
late final AnimationController _nodeController;
Animation<Matrix4>? _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<Node, Element> _nodeToElement = <Node, Element>{};
Map<Key, Element> _keyToElement = <Key, Element>{};
// Used between startLayout() & endLayout() to compute the new values
Map<Node, Element>? _newNodeToElement;
Map<Key, Element>? _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 = <Node, Element>{};
_newKeyToElement = <Key, Element>{};
}
@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<RenderBox, NodeBoxData>,
RenderBoxContainerDefaultsMixin<RenderBox, NodeBoxData> {
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 = <Node, Offset>{};
final _children = <Node, RenderBox>{};
final _activeChildrenForLayoutPass = <Node, RenderBox>{};
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>((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>((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', graph));
properties.add(DiagnosticsProperty<Algorithm>('algorithm', algorithm));
properties.add(DiagnosticsProperty<Paint>('paint', edgePaint));
}
}
class NodeBoxData extends ContainerBoxParentData<RenderBox> {
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<GraphViewCustomPainter> {
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<Widget>.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<void> 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<double> clipLineEnd(
double startX,
double startY,
double stopX,
double stopY,
double destX,
double destY,
double destWidth,
double destHeight) {
var clippedStopX = stopX;
var clippedStopY = stopY;
if (startX == stopX && startY == stopY) {
return [startX, startY, clippedStopX, clippedStopY];
}
var slope = (startY - stopY) / (startX - stopX);
final halfHeight = destHeight * 0.5;
final halfWidth = destWidth * 0.5;
// Check vertical edge intersections
if (startX != stopX) {
final halfSlopeWidth = slope * halfWidth;
if (halfSlopeWidth.abs() <= halfHeight) {
if (destX > startX) {
// Left edge intersection
return [startX, startY, stopX - halfWidth, stopY - halfSlopeWidth];
} else if (destX < startX) {
// Right edge intersection
return [startX, startY, stopX + halfWidth, stopY + halfSlopeWidth];
}
}
}
// Check horizontal edge intersections
if (startY != stopY && slope != 0) {
final halfSlopeHeight = halfHeight / slope;
if (halfSlopeHeight.abs() <= halfWidth) {
if (destY < startY) {
// Bottom edge intersection
clippedStopX = stopX + halfSlopeHeight;
clippedStopY = stopY + halfHeight;
} else if (destY > startY) {
// Top edge intersection
clippedStopX = stopX - halfSlopeHeight;
clippedStopY = stopY - halfHeight;
}
}
}
return [startX, startY, clippedStopX, clippedStopY];
}
List<double> clipLine(double startX, double startY, double stopX,
double stopY, Node destination) {
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<Node, Offset>? _animatedPositions;
void setAnimatedPositions(Map<Node, Offset> 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<Node, Offset> displacement = {};
Map<Node, Rect> 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<Node> 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<Edge> 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<Node> 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 = <Node>[];
var nodeClusters = <NodeCluster>
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
SYMBOL INDEX (641 symbols across 43 files)
FILE: example/lib/algorithm_selector_graphview.dart
type LayoutAlgorithmType (line 7) | enum LayoutAlgorithmType {
class AlgorithmSelectedVIewPage (line 15) | class AlgorithmSelectedVIewPage extends StatefulWidget {
method createState (line 17) | _TreeViewPageState createState()
class _TreeViewPageState (line 20) | class _TreeViewPageState extends State<AlgorithmSelectedVIewPage> with T...
method build (line 30) | Widget build(BuildContext context)
method _getAlgorithmDisplayName (line 168) | String _getAlgorithmDisplayName(LayoutAlgorithmType type)
method _updateAlgorithm (line 183) | void _updateAlgorithm()
method rectangleWidget (line 208) | Widget rectangleWidget(int? a)
method _navigateToRandomNode (line 228) | void _navigateToRandomNode()
method _resetView (line 243) | void _resetView()
method initState (line 248) | void initState()
FILE: example/lib/decision_tree_screen.dart
class DecisionTreeScreen (line 6) | class DecisionTreeScreen extends StatefulWidget {
method createState (line 8) | _DecisionTreeScreenState createState()
class _DecisionTreeScreenState (line 11) | class _DecisionTreeScreenState extends State<DecisionTreeScreen> {
method initState (line 20) | void initState()
method build (line 39) | Widget build(BuildContext context)
FILE: example/lib/example.dart
function main (line 7) | void main()
class MyApp (line 11) | class MyApp extends StatelessWidget {
method build (line 13) | Widget build(BuildContext context)
class Home (line 20) | class Home extends StatelessWidget {
method build (line 26) | Widget build(BuildContext context)
FILE: example/lib/force_directed_graphview.dart
class GraphClusterViewPage (line 6) | class GraphClusterViewPage extends StatefulWidget {
method createState (line 8) | _GraphClusterViewPageState createState()
class _GraphClusterViewPageState (line 11) | class _GraphClusterViewPageState extends State<GraphClusterViewPage> {
method build (line 13) | Widget build(BuildContext context)
method rectangWidget (line 47) | Widget rectangWidget(int? i)
method initState (line 63) | void initState()
FILE: example/lib/graph_cluster_animated.dart
class GraphScreen (line 7) | class GraphScreen extends StatefulWidget {
method createState (line 15) | _GraphScreenState createState()
class _GraphScreenState (line 18) | class _GraphScreenState extends State<GraphScreen> {
method build (line 23) | Widget build(BuildContext context)
method rectangWidget (line 67) | Widget rectangWidget(String? i)
method update (line 79) | Future<void> update()
FILE: example/lib/large_tree_graphview.dart
class LargeTreeViewPage (line 6) | class LargeTreeViewPage extends StatefulWidget {
method createState (line 8) | _LargeTreeViewPageState createState()
class _LargeTreeViewPageState (line 11) | class _LargeTreeViewPageState extends State<LargeTreeViewPage> with Tick...
method build (line 18) | Widget build(BuildContext context)
method _toggleCollapse (line 140) | void _toggleCollapse(Node node)
method _navigateToRandomNode (line 144) | void _navigateToRandomNode()
method _resetView (line 159) | void _resetView()
method initState (line 164) | void initState()
FILE: example/lib/layer_eiglesperger_graphview.dart
class LayeredEiglspergerGraphViewPage (line 5) | class LayeredEiglspergerGraphViewPage extends StatefulWidget {
method createState (line 7) | _LayeredEiglspergerGraphViewPageState createState()
class _LayeredEiglspergerGraphViewPageState (line 10) | class _LayeredEiglspergerGraphViewPageState extends State<LayeredEiglspe...
method build (line 16) | Widget build(BuildContext context)
method rectangleWidget (line 122) | Widget rectangleWidget(int? a)
method _navigateToRandomNode (line 138) | void _navigateToRandomNode()
method initState (line 154) | void initState()
FILE: example/lib/layer_graphview.dart
class LayeredGraphViewPage (line 5) | class LayeredGraphViewPage extends StatefulWidget {
method createState (line 7) | _LayeredGraphViewPageState createState()
class _LayeredGraphViewPageState (line 10) | class _LayeredGraphViewPageState extends State<LayeredGraphViewPage> {
method build (line 17) | Widget build(BuildContext context)
method _buildControlPanel (line 55) | Widget _buildControlPanel()
method _buildNumericControls (line 76) | Widget _buildNumericControls()
method _buildSliderControl (line 91) | Widget _buildSliderControl(String label, int value, int min, int max, ...
method _buildDropdown (line 111) | Widget _buildDropdown<T>(String label, T value, List<T> items, Functio...
method _buildShapeControls (line 139) | Widget _buildShapeControls()
method _buildShapeButton (line 169) | Widget _buildShapeButton(String text, bool isSelected, VoidCallback on...
method _buildGraphView (line 182) | Widget _buildGraphView()
method _navigateToRandomNode (line 231) | void _navigateToRandomNode()
method initState (line 238) | void initState()
method _initializeGraph (line 243) | void _initializeGraph()
FILE: example/lib/layer_graphview_json.dart
class LayerGraphPageFromJson (line 6) | class LayerGraphPageFromJson extends StatefulWidget {
method createState (line 8) | _LayerGraphPageFromJsonState createState()
class _LayerGraphPageFromJsonState (line 11) | class _LayerGraphPageFromJsonState extends State<LayerGraphPageFromJson> {
method build (line 99) | Widget build(BuildContext context)
method _navigateToRandomNode (line 196) | void _navigateToRandomNode()
method rectangleWidget (line 212) | Widget rectangleWidget(String? a, Node node)
method initState (line 234) | void initState()
FILE: example/lib/main.dart
function main (line 17) | void main()
class MyApp (line 21) | class MyApp extends StatelessWidget {
method build (line 23) | Widget build(BuildContext context)
class Home (line 37) | class Home extends StatelessWidget {
method build (line 41) | Widget build(BuildContext context)
method _buildScrollableContent (line 67) | Widget _buildScrollableContent()
method _buildSection (line 177) | Widget _buildSection(String title, List<Widget> buttons)
method _buildButton (line 204) | Widget _buildButton(
method _buildCustomGraphButton (line 296) | Widget _buildCustomGraphButton(
method _createSquareGraph (line 396) | Graph _createSquareGraph()
method _createTriangleGraph (line 424) | Graph _createTriangleGraph()
FILE: example/lib/mindmap_graphview.dart
class MindMapPage (line 6) | class MindMapPage extends StatefulWidget {
method createState (line 8) | _MindMapPageState createState()
class _MindMapPageState (line 11) | class _MindMapPageState extends State<MindMapPage> with TickerProviderSt...
method build (line 18) | Widget build(BuildContext context)
method rectangleWidget (line 123) | Widget rectangleWidget(int? a)
method _navigateToRandomNode (line 143) | void _navigateToRandomNode()
method _resetView (line 158) | void _resetView()
method initState (line 163) | void initState()
FILE: example/lib/mutliple_forest_graphview.dart
class MultipleForestTreeViewPage (line 6) | class MultipleForestTreeViewPage extends StatefulWidget {
method createState (line 8) | _TreeViewPageState createState()
class _TreeViewPageState (line 11) | class _TreeViewPageState extends State<MultipleForestTreeViewPage> with ...
method build (line 18) | Widget build(BuildContext context)
method rectangleWidget (line 118) | Widget rectangleWidget(int? a)
method _navigateToRandomNode (line 138) | void _navigateToRandomNode()
method _resetView (line 153) | void _resetView()
method initState (line 158) | void initState()
FILE: example/lib/tree_graphview.dart
class TreeViewPage (line 6) | class TreeViewPage extends StatefulWidget {
method createState (line 8) | _TreeViewPageState createState()
class _TreeViewPageState (line 11) | class _TreeViewPageState extends State<TreeViewPage> with TickerProvider...
method build (line 18) | Widget build(BuildContext context)
method _toggleCollapse (line 132) | void _toggleCollapse(Node node)
method _navigateToRandomNode (line 136) | void _navigateToRandomNode()
method _resetView (line 151) | void _resetView()
method initState (line 156) | void initState()
FILE: example/lib/tree_graphview_json.dart
class TreeViewPageFromJson (line 4) | class TreeViewPageFromJson extends StatefulWidget {
method createState (line 6) | _TreeViewPageFromJsonState createState()
class _TreeViewPageFromJsonState (line 9) | class _TreeViewPageFromJsonState extends State<TreeViewPageFromJson> {
method build (line 33) | Widget build(BuildContext context)
method rectangleWidget (line 113) | Widget rectangleWidget(String? a)
method initState (line 134) | void initState()
FILE: lib/Algorithm.dart
class Algorithm (line 3) | abstract class Algorithm {
method run (line 10) | Size run(Graph? graph, double shiftX, double shiftY)
method init (line 12) | void init(Graph? graph)
method setDimensions (line 14) | void setDimensions(double width, double height)
FILE: lib/Graph.dart
class Graph (line 3) | class Graph {
method nodeCount (line 19) | int nodeCount()
method addNode (line 21) | void addNode(Node node)
method addNodes (line 27) | void addNodes(List<Node> nodes)
method removeNode (line 29) | void removeNode(Node? node)
method removeNodes (line 43) | void removeNodes(List<Node> nodes)
method addEdge (line 45) | Edge addEdge(Node source, Node destination, {Paint? paint})
method addEdgeS (line 51) | void addEdgeS(Edge edge)
method addEdges (line 88) | void addEdges(List<Edge> edges)
method removeEdge (line 90) | void removeEdge(Edge edge)
method removeEdges (line 95) | void removeEdges(List<Edge> edges)
method removeEdgeFromPredecessor (line 97) | void removeEdgeFromPredecessor(Node? predecessor, Node? current)
method hasNodes (line 103) | bool hasNodes()
method getEdgeBetween (line 105) | Edge? getEdgeBetween(Node source, Node? destination)
method hasSuccessor (line 109) | bool hasSuccessor(Node? node)
method successorsOf (line 111) | List<Node> successorsOf(Node? node)
method hasPredecessor (line 117) | bool hasPredecessor(Node node)
method predecessorsOf (line 119) | List<Node> predecessorsOf(Node? node)
method _buildCache (line 125) | void _buildCache()
method contains (line 142) | bool contains({Node? node, Edge? edge})
method containsData (line 146) | bool containsData(data)
method getNodeAtPosition (line 148) | Node getNodeAtPosition(int position)
method getNodeAtUsingData (line 162) | Node getNodeAtUsingData(Widget data)
method getNodeUsingKey (line 165) | Node getNodeUsingKey(ValueKey key)
method getNodeUsingId (line 168) | Node getNodeUsingId(dynamic id)
method getOutEdges (line 171) | List<Edge> getOutEdges(Node node)
method getInEdges (line 174) | List<Edge> getInEdges(Node node)
method notifyGraphObserver (line 177) | void notifyGraphObserver()
method toJson (line 181) | String toJson()
function calculateGraphBounds (line 198) | Rect calculateGraphBounds()
function calculateGraphSize (line 214) | Size calculateGraphSize()
type LineType (line 220) | enum LineType {
class Node (line 227) | class Node {
method toString (line 274) | String toString()
class Edge (line 279) | class Edge {
class GraphObserver (line 296) | abstract class GraphObserver {
method notifyGraphInvalidated (line 297) | void notifyGraphInvalidated()
FILE: lib/GraphView.dart
type NodeWidgetBuilder (line 36) | typedef NodeWidgetBuilder = Widget Function(Node node);
type EdgeWidgetBuilder (line 37) | typedef EdgeWidgetBuilder = Widget Function(Edge edge);
class GraphViewController (line 39) | class GraphViewController {
method _attach (line 54) | void _attach(_GraphViewState? state)
method _detach (line 56) | void _detach()
method animateToNode (line 58) | void animateToNode(ValueKey key)
method jumpToNode (line 60) | void jumpToNode(ValueKey key)
method animateToMatrix (line 62) | void animateToMatrix(Matrix4 target)
method resetView (line 64) | void resetView()
method zoomToFit (line 66) | void zoomToFit()
method forceRecalculation (line 68) | void forceRecalculation()
method isNodeCollapsed (line 71) | bool isNodeCollapsed(Node node)
method isNodeHidden (line 73) | bool isNodeHidden(Node node)
method isNodeVisible (line 75) | bool isNodeVisible(Graph graph, Node node)
method findClosestVisibleAncestor (line 79) | Node? findClosestVisibleAncestor(Graph graph, Node node)
method _markDescendantsHiddenBy (line 93) | void _markDescendantsHiddenBy(
method _markExpandingDescendants (line 111) | void _markExpandingDescendants(Graph graph, Node node)
method expandNode (line 120) | void expandNode(Graph graph, Node node, {animate = false})
method collapseNode (line 133) | void collapseNode(Graph graph, Node node, {animate = false})
method toggleNodeExpanded (line 146) | void toggleNodeExpanded(Graph graph, Node node, {animate = false})
method getCollapsingEdges (line 154) | List<Edge> getCollapsingEdges(Graph graph)
method getExpandingEdges (line 162) | List<Edge> getExpandingEdges(Graph graph)
method setInitiallyCollapsedNodes (line 176) | void setInitiallyCollapsedNodes(Graph graph, List<Node> nodes)
method setInitiallyCollapsedByKeys (line 184) | void setInitiallyCollapsedByKeys(Graph graph, Set<ValueKey> keys)
method isNodeExpanding (line 197) | bool isNodeExpanding(Node node)
method removeCollapsingNodes (line 199) | void removeCollapsingNodes()
method jumpToFocusedNode (line 203) | void jumpToFocusedNode()
class GraphChildDelegate (line 215) | class GraphChildDelegate {
method getVisibleGraph (line 232) | Graph getVisibleGraph()
method getVisibleGraphOnly (line 247) | Graph getVisibleGraphOnly()
method build (line 261) | Widget? build(Node node)
method shouldRebuild (line 266) | bool shouldRebuild(GraphChildDelegate oldDelegate)
method runAlgorithm (line 273) | Size runAlgorithm()
method isNodeVisible (line 289) | bool isNodeVisible(Node node)
method findClosestVisibleAncestor (line 293) | Node? findClosestVisibleAncestor(Node node)
class GraphView (line 298) | class GraphView extends StatefulWidget {
method createState (line 363) | _GraphViewState createState()
class _GraphViewState (line 366) | class _GraphViewState extends State<GraphView> with TickerProviderStateM...
method initState (line 373) | void initState()
method dispose (line 405) | void dispose()
method build (line 414) | Widget build(BuildContext context)
method jumpToNodeUsingKey (line 436) | void jumpToNodeUsingKey(ValueKey key, bool animated)
method jumpToNode (line 443) | void jumpToNode(Node node, bool animated)
method jumpToOffset (line 450) | void jumpToOffset(Offset offset, bool animated)
method resetView (line 475) | void resetView()
method zoomToFit (line 477) | void zoomToFit()
method animateToMatrix (line 503) | void animateToMatrix(Matrix4 target)
method _onPanTick (line 513) | void _onPanTick()
method forceRecalculation (line 523) | void forceRecalculation()
class GraphChildManager (line 531) | abstract class GraphChildManager {
method startLayout (line 532) | void startLayout()
method buildChild (line 534) | void buildChild(Node node)
method reuseChild (line 536) | void reuseChild(Node node)
method endLayout (line 538) | void endLayout()
class GraphViewWidget (line 541) | class GraphViewWidget extends RenderObjectWidget {
method createElement (line 556) | GraphViewElement createElement()
method createRenderObject (line 559) | RenderCustomLayoutBox createRenderObject(BuildContext context)
method updateRenderObject (line 570) | void updateRenderObject(
class GraphViewElement (line 580) | class GraphViewElement extends RenderObjectElement
method performRebuild (line 603) | void performRebuild()
method forgetChild (line 611) | void forgetChild(Element child)
method insertRenderObjectChild (line 621) | void insertRenderObjectChild(RenderBox child, Node slot)
method moveRenderObjectChild (line 626) | void moveRenderObjectChild(RenderBox child, Node oldSlot, Node newSlot)
method removeRenderObjectChild (line 631) | void removeRenderObjectChild(RenderBox child, Node slot)
method visitChildren (line 636) | void visitChildren(ElementVisitor visitor)
method startLayout (line 643) | void startLayout()
method buildChild (line 650) | void buildChild(Node node)
method reuseChild (line 675) | void reuseChild(Node node)
method _retrieveOldElement (line 691) | Element? _retrieveOldElement(Widget newWidget, Node node)
method endLayout (line 708) | void endLayout()
method centerNodeWhileToggling (line 734) | void centerNodeWhileToggling()
class RenderCustomLayoutBox (line 739) | class RenderCustomLayoutBox extends RenderBox
method buildOrObtainChildFor (line 771) | RenderBox? buildOrObtainChildFor(Node node)
method markNeedsRecalculation (line 808) | void markNeedsRecalculation()
method attach (line 815) | void attach(PipelineOwner owner)
method detach (line 824) | void detach()
method forceRecalculation (line 832) | void forceRecalculation()
method _onAnimationTick (line 862) | void _onAnimationTick()
method paint (line 867) | void paint(PaintingContext context, Offset offset)
method performLayout (line 935) | void performLayout()
method _paintNodes (line 961) | void _paintNodes(PaintingContext context, Offset offset, double t)
method _paintExpandingNode (line 996) | void _paintExpandingNode(PaintingContext context, RenderBox child,
method _paintCollapsingNode (line 1025) | void _paintCollapsingNode(PaintingContext context, RenderBox child,
method _updateNodePositions (line 1055) | void _updateNodePositions()
method _layoutNodesLazily (line 1070) | void _layoutNodesLazily(BoxConstraints constraints)
method _updateAnimationStates (line 1080) | void _updateAnimationStates()
method _updateVisibleNodeAnimation (line 1098) | void _updateVisibleNodeAnimation(NodeBoxData nodeData, Node graphNode)
method _updateCollapsedNodeAnimation (line 1116) | void _updateCollapsedNodeAnimation(NodeBoxData nodeData, Node graphNode)
method hitTestChildren (line 1136) | bool hitTestChildren(BoxHitTestResult result, {required Offset position})
method setupParentData (line 1160) | void setupParentData(RenderBox child)
method _insertChild (line 1167) | void _insertChild(RenderBox child, Node slot)
method _moveChild (line 1172) | void _moveChild(RenderBox child, {required Node from, required Node to})
method _removeChild (line 1179) | void _removeChild(RenderBox child, Node slot)
method visitChildren (line 1187) | void visitChildren(RenderObjectVisitor visitor)
method debugFillProperties (line 1194) | void debugFillProperties(DiagnosticPropertiesBuilder properties)
class NodeBoxData (line 1202) | class NodeBoxData extends ContainerBoxParentData<RenderBox> {
class GraphViewCustomPainter (line 1207) | class GraphViewCustomPainter extends StatefulWidget {
method createState (line 1223) | _GraphViewCustomPainterState createState()
class _GraphViewCustomPainterState (line 1226) | class _GraphViewCustomPainterState extends State<GraphViewCustomPainter> {
method initState (line 1232) | void initState()
method startTimer (line 1242) | void startTimer()
method dispose (line 1250) | void dispose()
method build (line 1256) | Widget build(BuildContext context)
method update (line 1285) | Future<void> update()
class EdgeRender (line 1290) | class EdgeRender extends CustomPainter {
method paint (line 1299) | void paint(Canvas canvas, Size size)
method shouldRepaint (line 1317) | bool shouldRepaint(CustomPainter oldDelegate)
FILE: lib/edgerenderer/ArrowEdgeRenderer.dart
class ArrowEdgeRenderer (line 6) | class ArrowEdgeRenderer extends EdgeRenderer {
method _getNodeCenter (line 12) | Offset _getNodeCenter(Node node)
method render (line 20) | void render(Canvas canvas, Graph graph, Paint paint)
method renderEdge (line 27) | void renderEdge(Canvas canvas, Edge edge, Paint paint)
method _getLineType (line 129) | LineType? _getLineType(Node node)
method drawTriangle (line 138) | Offset drawTriangle(Canvas canvas, Paint paint, double lineStartX,
method clipLineEnd (line 169) | List<double> clipLineEnd(
method clipLine (line 222) | List<double> clipLine(double startX, double startY, double stopX,
FILE: lib/edgerenderer/EdgeRenderer.dart
class EdgeRenderer (line 3) | abstract class EdgeRenderer {
method setAnimatedPositions (line 6) | void setAnimatedPositions(Map<Node, Offset> positions)
method getNodePosition (line 8) | Offset getNodePosition(Node node)
method renderEdge (line 10) | void renderEdge(Canvas canvas, Edge edge, Paint paint)
method getNodeCenter (line 12) | Offset getNodeCenter(Node node)
method drawStyledLine (line 21) | void drawStyledLine(Canvas canvas, Offset start, Offset end, Paint paint,
method drawStyledPath (line 40) | void drawStyledPath(Canvas canvas, Path path, Paint paint,
method drawDashedLine (line 53) | void drawDashedLine(Canvas canvas, Offset source, Offset destination,
method drawSineLine (line 91) | void drawSineLine(Canvas canvas, Offset source, Offset destination, Pa...
method buildSelfLoopPath (line 142) | LoopRenderResult? buildSelfLoopPath(
class LoopRenderResult (line 202) | class LoopRenderResult {
FILE: lib/forcedirected/FruchtermanReingoldAlgorithm.dart
class FruchtermanReingoldAlgorithm (line 3) | class FruchtermanReingoldAlgorithm implements Algorithm {
method init (line 24) | void init(Graph? graph)
method moveNodes (line 39) | void moveNodes(Graph graph)
method cool (line 57) | void cool(int currentIteration)
method limitMaximumDisplacement (line 63) | void limitMaximumDisplacement(List<Node> nodes)
method calculateAttraction (line 75) | void calculateAttraction(List<Edge> edges)
method calculateRepulsion (line 98) | void calculateRepulsion(List<Node> nodes)
method _getNodeRectDistance (line 128) | Offset _getNodeRectDistance(Node nodeA, Node nodeB)
method step (line 161) | bool step(Graph graph)
method run (line 183) | Size run(Graph? graph, double shiftX, double shiftY)
method shiftCoordinates (line 219) | void shiftCoordinates(Graph graph, double shiftX, double shiftY)
method positionNodes (line 227) | void positionNodes(Graph graph)
method positionCluster (line 256) | void positionCluster(List<NodeCluster> nodeClusters)
method combineSingleNodeCluster (line 274) | void combineSingleNodeCluster(List<NodeCluster> nodeClusters)
method followEdges (line 290) | void followEdges(
method findClusterOf (line 311) | NodeCluster? findClusterOf(List<NodeCluster> clusters, Node node)
method findBiggestSize (line 315) | double findBiggestSize(Graph graph)
method getOffset (line 319) | Offset getOffset(Graph graph)
method done (line 331) | bool done()
method drawEdges (line 335) | void drawEdges(Canvas canvas, Graph graph, Paint linePaint)
method setDimensions (line 338) | void setDimensions(double width, double height)
class NodeCluster (line 344) | class NodeCluster {
method getNodes (line 348) | List<Node> getNodes()
method getRect (line 352) | Rect? getRect()
method setRect (line 356) | void setRect(Rect newRect)
method add (line 360) | void add(Node node)
method contains (line 375) | bool contains(Node node)
method size (line 379) | int size()
method concat (line 383) | void concat(NodeCluster cluster)
method offset (line 393) | void offset(double xDiff, double yDiff)
FILE: lib/forcedirected/FruchtermanReingoldConfiguration.dart
class FruchtermanReingoldConfiguration (line 3) | class FruchtermanReingoldConfiguration {
FILE: lib/layered/EiglspergerAlgorithm.dart
class ContainerX (line 3) | class ContainerX {
method append (line 11) | void append(Segment segment)
method join (line 15) | void join(ContainerX other)
method size (line 20) | int size()
method contains (line 22) | bool contains(Segment segment)
method createEmpty (line 26) | ContainerX createEmpty()
method split (line 29) | ContainerPair split(ContainerX container, Segment key)
method splitAt (line 48) | ContainerPair splitAt(ContainerX container, int position)
method toString (line 69) | String toString()
class ContainerPair (line 72) | class ContainerPair {
class Segment (line 80) | class Segment {
method toString (line 97) | String toString()
class EiglspergerNodeData (line 100) | class EiglspergerNodeData {
class EiglspergerEdgeData (line 120) | class EiglspergerEdgeData {
class VirtualEdge (line 125) | class VirtualEdge {
method toString (line 133) | String toString()
class LayerElement (line 137) | abstract class LayerElement {
class NodeElement (line 144) | class NodeElement extends LayerElement {
method toString (line 149) | String toString()
class ContainerElement (line 153) | class ContainerElement extends LayerElement {
method toString (line 158) | String toString()
class EiglspergerAlgorithm (line 161) | class EiglspergerAlgorithm extends Algorithm {
method isVertical (line 183) | bool isVertical()
method needReverseOrder (line 189) | bool needReverseOrder()
method run (line 196) | Size run(Graph? graph, double shiftX, double shiftY)
method shiftCoordinates (line 211) | void shiftCoordinates(double shiftX, double shiftY)
method reset (line 219) | void reset()
method initNodeData (line 230) | void initNodeData()
method cycleRemoval (line 246) | void cycleRemoval()
method dfs (line 252) | void dfs(Node node)
method layerAssignment (line 271) | void layerAssignment()
method createSegmentsForLongEdges (line 290) | void createSegmentsForLongEdges()
method createSingleDummyVertex (line 314) | void createSingleDummyVertex(Edge edge, int dummyLayer)
method createSegment (line 334) | void createSegment(Edge edge)
method getRootNodes (line 378) | List<Node> getRootNodes(Graph graph)
method copyGraph (line 392) | Graph copyGraph(Graph graph)
method nodeOrdering (line 399) | void nodeOrdering()
method forwardSweep (line 436) | double forwardSweep(List<List<Node>> layers)
method backwardSweep (line 462) | double backwardSweep(List<List<Node>> layers)
method createLayerElements (line 485) | List<LayerElement> createLayerElements(List<Node> layer)
method extractNodes (line 489) | List<Node> extractNodes(List<LayerElement> elements)
method stepOne (line 510) | void stepOne(List<LayerElement> layer, bool isForward)
method stepTwo (line 547) | void stepTwo(List<LayerElement> currentLayer, List<LayerElement> nextL...
method assignPositions (line 570) | void assignPositions(List<LayerElement> layer)
method stepThree (line 585) | void stepThree(List<LayerElement> layer)
method mergeSortedLists (line 612) | List<LayerElement> mergeSortedLists(List<LayerElement> vertices, List<...
method stepFour (line 657) | void stepFour(List<LayerElement> layer, int layerIndex)
method updateIndices (line 714) | void updateIndices(List<LayerElement> layer)
method stepFive (line 725) | double stepFive(List<LayerElement> currentLayer, List<LayerElement> ne...
method countWeightedCrossings (line 763) | double countWeightedCrossings(List<dynamic> edges, List<LayerElement> ...
method getEdgeWeight (line 786) | int getEdgeWeight(dynamic edge)
method getTargetPosition (line 793) | int getTargetPosition(dynamic edge, List<LayerElement> nextLayer)
method stepSix (line 813) | void stepSix(List<LayerElement> layer)
method updateNodePositions (line 852) | void updateNodePositions()
method coordinateAssignment (line 866) | void coordinateAssignment()
method assignX (line 876) | void assignX()
method assignXx (line 894) | void assignXx()
method balance (line 946) | void balance(List<Map<Node, double>> x, List<Map<Node?, double>> block...
method resolveOverlaps (line 1058) | void resolveOverlaps(Map<Node, double> coordinates)
method getPreviousNonDummyNode (line 1093) | Node? getPreviousNonDummyNode(List<Node> layerNodes, int currentIndex)
method verticalAlignment (line 1163) | void verticalAlignment(Map<Node?, Node?> root, Map<Node?, Node?> align,
method horizontalCompactation (line 1204) | void horizontalCompactation(
method placeBlock (line 1259) | void placeBlock(
method successorsOf (line 1321) | List<Node> successorsOf(Node? node)
method predecessorsOf (line 1325) | List<Node> predecessorsOf(Node? node)
method getAdjNodes (line 1329) | List<Node> getAdjNodes(Node node, bool downward)
method predecessor (line 1338) | Node? predecessor(Node? v, bool leftToRight)
method virtualTwinNode (line 1349) | Node? virtualTwinNode(Node node, bool downward)
method positionOfNode (line 1358) | int positionOfNode(Node? node)
method getLayerIndex (line 1362) | int getLayerIndex(Node? node)
method isLongEdgeDummy (line 1366) | bool isLongEdgeDummy(Node? v)
method assignY (line 1373) | void assignY()
method denormalize (line 1400) | void denormalize()
method restoreCycle (line 1447) | void restoreCycle()
method getOffset (line 1463) | Offset getOffset(Graph graph, bool needReverseOrder)
method getPosition (line 1484) | Offset getPosition(Node node, Offset offset)
method medianValue (line 1507) | double medianValue(List<int> positions)
method init (line 1527) | void init(Graph? graph)
method setDimensions (line 1540) | void setDimensions(double width, double height)
FILE: lib/layered/SugiyamaAlgorithm.dart
class SugiyamaAlgorithm (line 3) | class SugiyamaAlgorithm extends Algorithm {
method isVertical (line 25) | bool isVertical()
method needReverseOrder (line 31) | bool needReverseOrder()
method run (line 38) | Size run(Graph? graph, double shiftX, double shiftY)
method shiftCoordinates (line 58) | void shiftCoordinates(double shiftX, double shiftY)
method reset (line 66) | void reset()
method initSugiyamaData (line 75) | void initSugiyamaData()
method dfs (line 87) | void dfs(Node node)
method layerAssignment (line 108) | void layerAssignment()
method layerAssignmentTopDown (line 128) | void layerAssignmentTopDown()
method layerAssignmentLongestPath (line 149) | void layerAssignmentLongestPath()
method layerAssignmentCoffmanGraham (line 183) | void layerAssignmentCoffmanGraham()
method layerAssignmentNetworkSimplex (line 237) | void layerAssignmentNetworkSimplex()
method addDummyNodes (line 292) | void addDummyNodes()
method getRootNodes (line 330) | List<Node> getRootNodes(Graph graph)
method copyGraph (line 344) | Graph copyGraph(Graph graph)
method nodeOrdering (line 351) | void nodeOrdering()
method median (line 386) | void median(List<List<Node?>> layers, int currentIteration)
method transposeSimple (line 469) | bool transposeSimple(List<List<Node>> layers)
method transposeAccumulator (line 497) | bool transposeAccumulator(List<List<Node>> layers)
method _getBiLayerCrossings (line 538) | int _getBiLayerCrossings(List<Node> upperLayer, List<Node> lowerLayer)
method exchange (line 575) | void exchange(List<Node> nodes, Node v, Node w)
method crossingCount (line 584) | int crossingCount(HashMap<Node, int> northernNodes, Node? n1, Node? n2)
method crossing (line 599) | int crossing(List<List<Node>> layers)
method coordinateAssignment (line 619) | void coordinateAssignment()
method assignX (line 633) | void assignX()
method balance (line 686) | void balance(List<Map<Node, double>> x, List<Map<Node?, double>> block...
method resolveOverlaps (line 800) | void resolveOverlaps(Map<Node, double> coordinates)
method getPreviousNonDummyNode (line 839) | Node? getPreviousNonDummyNode(List<Node> layerNodes, int currentIndex)
method markType1Conflicts (line 849) | Map<int, int> markType1Conflicts(bool downward)
method verticalAlignment (line 909) | void verticalAlignment(Map<Node?, Node?> root, Map<Node?, Node?> align,
method horizontalCompactation (line 950) | void horizontalCompactation(
method placeBlock (line 1005) | void placeBlock(
method successorsOf (line 1067) | List<Node> successorsOf(Node? node)
method predecessorsOf (line 1071) | List<Node> predecessorsOf(Node? node)
method getAdjNodes (line 1075) | List<Node> getAdjNodes(Node node, bool downward)
method predecessor (line 1084) | Node? predecessor(Node? v, bool leftToRight)
method virtualTwinNode (line 1095) | Node? virtualTwinNode(Node node, bool downward)
method positionOfNode (line 1104) | int positionOfNode(Node? node)
method getLayerIndex (line 1108) | int getLayerIndex(Node? node)
method isLongEdgeDummy (line 1112) | bool isLongEdgeDummy(Node? v)
method assignY (line 1119) | void assignY()
method denormalize (line 1147) | void denormalize()
method restoreCycle (line 1194) | void restoreCycle()
method cycleRemoval (line 1220) | void cycleRemoval()
method _dfsRecursiveCycleRemoval (line 1231) | void _dfsRecursiveCycleRemoval()
method _greedyCycleRemoval (line 1237) | void _greedyCycleRemoval()
method postStraighten (line 1252) | void postStraighten()
method _findConnectedDummies (line 1293) | void _findConnectedDummies(
method getOffset (line 1315) | Offset getOffset(Graph graph, bool needReverseOrder)
method getPosition (line 1336) | Offset getPosition(Node node, Offset offset)
method init (line 1360) | void init(Graph? graph)
method setDimensions (line 1376) | void setDimensions(double width, double height)
class AccumulatorTree (line 1382) | class AccumulatorTree {
method crossCount (line 1401) | int crossCount(List<int> southSequence)
class GreedyCycleRemoval (line 1418) | class GreedyCycleRemoval {
method getFeedbackArcs (line 1424) | Set<Edge> getFeedbackArcs()
method _copyGraph (line 1430) | Graph _copyGraph()
method _removeCycles (line 1437) | void _removeCycles(Graph g)
FILE: lib/layered/SugiyamaConfiguration.dart
class SugiyamaConfiguration (line 3) | class SugiyamaConfiguration {
method getLevelSeparation (line 29) | int getLevelSeparation()
method getNodeSeparation (line 33) | int getNodeSeparation()
method getOrientation (line 37) | int getOrientation()
type CoordinateAssignment (line 42) | enum CoordinateAssignment {
type LayeringStrategy (line 50) | enum LayeringStrategy {
type CrossMinimizationStrategy (line 57) | enum CrossMinimizationStrategy {
type CycleRemovalStrategy (line 62) | enum CycleRemovalStrategy {
class BendPointShape (line 67) | abstract class BendPointShape {}
class SharpBendPointShape (line 69) | class SharpBendPointShape extends BendPointShape {}
class MaxCurvedBendPointShape (line 71) | class MaxCurvedBendPointShape extends BendPointShape {}
class CurvedBendPointShape (line 73) | class CurvedBendPointShape extends BendPointShape {
FILE: lib/layered/SugiyamaEdgeData.dart
class SugiyamaEdgeData (line 3) | class SugiyamaEdgeData {
FILE: lib/layered/SugiyamaEdgeRenderer.dart
class SugiyamaEdgeRenderer (line 3) | class SugiyamaEdgeRenderer extends ArrowEdgeRenderer {
method hasBendEdges (line 12) | bool hasBendEdges(Edge edge)
method render (line 14) | void render(Canvas canvas, Graph graph, Paint paint)
method renderEdge (line 21) | void renderEdge(Canvas canvas, Edge edge, Paint paint)
method _renderEdgeWithBendPoints (line 76) | void _renderEdgeWithBendPoints(Canvas canvas, Edge edge, Paint current...
method _renderStraightEdge (line 141) | void _renderStraightEdge(Canvas canvas, Edge edge, Paint currentPaint,...
method _drawSharpBendPointsEdge (line 160) | void _drawSharpBendPointsEdge(List<Offset> bendPoints)
method _drawMaxCurvedBendPointsEdge (line 166) | void _drawMaxCurvedBendPointsEdge(List<Offset> bendPoints)
method _drawCurvedBendPointsEdge (line 175) | void _drawCurvedBendPointsEdge(List<Offset> bendPoints, double curveLe...
FILE: lib/layered/SugiyamaNodeData.dart
class SugiyamaNodeData (line 3) | class SugiyamaNodeData {
method toString (line 18) | String toString()
FILE: lib/mindmap/MindMapAlgorithm.dart
type MindmapSide (line 3) | enum MindmapSide { LEFT, RIGHT, ROOT }
class _SideData (line 5) | class _SideData {
class MindmapAlgorithm (line 9) | class MindmapAlgorithm extends BuchheimWalkerAlgorithm {
method initData (line 16) | void initData(Graph? graph)
method run (line 23) | Size run(Graph? graph, double shiftX, double shiftY)
method _markSubtree (line 33) | void _markSubtree(Node node, MindmapSide side)
method _applyBuchheimWalkerSpacing (line 42) | void _applyBuchheimWalkerSpacing(Graph graph, Node root)
method _createMindmapLayout (line 53) | void _createMindmapLayout(Graph graph, Node root)
FILE: lib/mindmap/MindmapEdgeRenderer.dart
class MindmapEdgeRenderer (line 3) | class MindmapEdgeRenderer extends TreeEdgeRenderer {
method getEffectiveOrientation (line 8) | int getEffectiveOrientation(dynamic node, dynamic child)
FILE: lib/tree/BaloonLayoutAlgorithm.dart
class PolarPoint (line 4) | class PolarPoint {
method toCartesian (line 13) | Offset toCartesian()
method of (line 20) | PolarPoint of(double theta, double radius)
method toString (line 25) | String toString()
class BalloonLayoutAlgorithm (line 28) | class BalloonLayoutAlgorithm extends Algorithm {
method run (line 39) | Size run(Graph? graph, double shiftX, double shiftY)
method _initializeData (line 68) | void _initializeData(Graph graph)
method _findRoots (line 84) | List<Node> _findRoots(Graph graph)
method _setRootPolars (line 90) | void _setRootPolars(Graph graph, List<Node> roots)
method _setRootPolar (line 107) | void _setRootPolar(Node root, Offset center)
method _setPolars (line 112) | void _setPolars(List<Node> nodes, Offset parentLocation, double angleT...
method _getGraphCenter (line 163) | Offset _getGraphCenter(Graph graph)
method _shiftCoordinates (line 171) | void _shiftCoordinates(Graph graph, double shiftX, double shiftY)
method _createSpanningTree (line 177) | Graph _createSpanningTree(Graph graph)
method _layoutSpanningTree (line 210) | Size _layoutSpanningTree(Graph spanningTree, double shiftX, double shi...
method successorsOf (line 229) | List<Node> successorsOf(Node? node)
method getPolarLocation (line 233) | PolarPoint? getPolarLocation(Node node)
method getRadius (line 237) | double? getRadius(Node node)
method getRadii (line 241) | Map<Node, double> getRadii()
method getPolarLocations (line 245) | Map<Node, PolarPoint> getPolarLocations()
method init (line 250) | void init(Graph? graph)
method setDimensions (line 255) | void setDimensions(double width, double height)
FILE: lib/tree/BuchheimWalkerAlgorithm.dart
class BuchheimWalkerAlgorithm (line 3) | class BuchheimWalkerAlgorithm extends Algorithm {
method isVertical (line 11) | bool isVertical()
method needReverseOrder (line 17) | bool needReverseOrder()
method _detectCycles (line 23) | void _detectCycles(Graph graph)
method hasCycle (line 26) | bool hasCycle(Node node)
method run (line 40) | Size run(Graph? graph, double shiftX, double shiftY)
method getFirstNode (line 59) | Node getFirstNode(Graph graph)
method checkUnconnectedNotes (line 62) | void checkUnconnectedNotes(Graph graph)
method compare (line 72) | int compare(int x, int y)
method firstWalk (line 76) | void firstWalk(Graph graph, Node node, int depth, int number)
method secondWalk (line 123) | void secondWalk(Graph graph, Node node, double modifier)
method executeShifts (line 136) | void executeShifts(Graph graph, Node node)
method apportion (line 153) | Node apportion(Graph graph, Node node, Node defaultAncestor)
method setAncestor (line 206) | void setAncestor(Node? v, Node ancestor)
method setModifier (line 210) | void setModifier(Node? v, double modifier)
method setThread (line 214) | void setThread(Node? v, Node thread)
method getPrelim (line 218) | double getPrelim(Node? v)
method getModifier (line 222) | double getModifier(Node? vip)
method moveSubtree (line 226) | void moveSubtree(Node? wm, Node wp, double shift)
method ancestor (line 237) | Node? ancestor(Graph graph, Node vim, Node node, Node defaultAncestor)
method nextRight (line 244) | Node? nextRight(Graph graph, Node? node)
method nextLeft (line 248) | Node? nextLeft(Graph graph, Node? node)
method getSpacing (line 254) | num getSpacing(Graph graph, Node? leftNode, Node rightNode)
method isSibling (line 265) | bool isSibling(Graph graph, Node? leftNode, Node rightNode)
method isLeaf (line 270) | bool isLeaf(Graph graph, Node node)
method getLeftSibling (line 274) | Node? getLeftSibling(Graph graph, Node node)
method hasLeftSibling (line 285) | bool hasLeftSibling(Graph graph, Node node)
method getRightSibling (line 296) | Node? getRightSibling(Graph graph, Node node)
method hasRightSibling (line 307) | bool hasRightSibling(Graph graph, Node node)
method getLeftMostChild (line 319) | Node getLeftMostChild(Graph graph, Node? node)
method getRightMostChild (line 323) | Node? getRightMostChild(Graph graph, Node? node)
method positionNodes (line 328) | void positionNodes(Graph graph)
method shiftCoordinates (line 387) | void shiftCoordinates(Graph graph, double shiftX, double shiftY)
method findMaxSize (line 393) | Size findMaxSize(List<Node> nodes)
method getOffset (line 405) | Offset getOffset(Graph graph, bool needReverseOrder)
method getPosition (line 426) | Offset getPosition(Node node, double globalPadding, Offset offset)
method sortByLevel (line 449) | List<Node> sortByLevel(Graph graph, bool descending)
method filterByLevel (line 459) | List<Node> filterByLevel(List<Node> nodes, int? level)
method initData (line 466) | void initData(Graph? graph)
method getNodeData (line 480) | BuchheimWalkerNodeData? getNodeData(Node? node)
method hasSuccessor (line 484) | bool hasSuccessor(Node? node)
method hasPredecessor (line 486) | bool hasPredecessor(Node node)
method successorsOf (line 488) | List<Node> successorsOf(Node? node)
method predecessorsOf (line 492) | List<Node> predecessorsOf(Node? node)
method init (line 501) | void init(Graph? graph)
method setDimensions (line 511) | void setDimensions(double width, double height)
FILE: lib/tree/BuchheimWalkerConfiguration.dart
class BuchheimWalkerConfiguration (line 3) | class BuchheimWalkerConfiguration {
method getSiblingSeparation (line 18) | int getSiblingSeparation()
method getLevelSeparation (line 22) | int getLevelSeparation()
method getSubtreeSeparation (line 26) | int getSubtreeSeparation()
FILE: lib/tree/BuchheimWalkerNodeData.dart
class BuchheimWalkerNodeData (line 3) | class BuchheimWalkerNodeData {
FILE: lib/tree/CircleLayoutAlgorithm.dart
class CircleLayoutConfiguration (line 3) | class CircleLayoutConfiguration {
class CircleLayoutAlgorithm (line 15) | class CircleLayoutAlgorithm extends Algorithm {
method run (line 26) | Size run(Graph? graph, double shiftX, double shiftY)
method _computeNodeOrder (line 45) | void _computeNodeOrder(Graph graph)
method _reduceEdgeCrossing (line 56) | List<Node> _reduceEdgeCrossing(Graph graph)
method _findConnectedComponents (line 76) | List<Set<Node>> _findConnectedComponents(Graph graph)
method _dfsComponent (line 91) | void _dfsComponent(Graph graph, Node node, Set<Node> visited, Set<Node...
method _createSubgraph (line 109) | Graph _createSubgraph(Graph originalGraph, Set<Node> nodes)
method _optimizeNodeOrder (line 127) | List<Node> _optimizeNodeOrder(Graph graph)
method _countCrossings (line 180) | int _countCrossings(Graph graph, List<Node> nodeOrder)
method _edgesCross (line 212) | bool _edgesCross(int pos1a, int pos1b, int pos2a, int pos2b, int total...
method _layoutNodes (line 230) | Size _layoutNodes(Graph graph)
method _shiftCoordinates (line 259) | void _shiftCoordinates(Graph graph, double shiftX, double shiftY)
method init (line 266) | void init(Graph? graph)
method setDimensions (line 271) | void setDimensions(double width, double height)
FILE: lib/tree/RadialTreeLayoutAlgorithm.dart
class TreeLayoutNodeData (line 3) | class TreeLayoutNodeData {
class RadialTreeLayoutAlgorithm (line 13) | class RadialTreeLayoutAlgorithm extends Algorithm {
method run (line 24) | Size run(Graph? graph, double shiftX, double shiftY)
method _initializeData (line 62) | void _initializeData(Graph graph)
method _findRoots (line 78) | List<Node> _findRoots(Graph graph)
method _buildRegularTree (line 84) | void _buildRegularTree(Graph graph, List<Node> roots)
method _calculateSubtreeDimensions (line 89) | void _calculateSubtreeDimensions(List<Node> roots)
method _calculateWidth (line 102) | int _calculateWidth(Node node, Set<Node> visited)
method _calculateHeight (line 124) | int _calculateHeight(Node node, Set<Node> visited)
method _positionNodes (line 146) | void _positionNodes(List<Node> roots)
method _buildTree (line 159) | void _buildTree(Node node, double x, double y, Set<Node> visited)
method _setRadialLocations (line 181) | void _setRadialLocations(Graph graph)
method _putRadialPointsInModel (line 202) | void _putRadialPointsInModel(Graph graph)
method _calculateDiameter (line 212) | double _calculateDiameter()
method _shiftCoordinates (line 223) | void _shiftCoordinates(Graph graph, double shiftX, double shiftY)
method _createSpanningTree (line 229) | Graph _createSpanningTree(Graph graph)
method _layoutSpanningTree (line 262) | Size _layoutSpanningTree(Graph spanningTree, double shiftX, double shi...
method init (line 285) | void init(Graph? graph)
method setDimensions (line 290) | void setDimensions(double width, double height)
method successorsOf (line 294) | List<Node> successorsOf(Node? node)
FILE: lib/tree/TidierTreeLayoutAlgorithm.dart
class TidierTreeNodeData (line 3) | class TidierTreeNodeData {
class TidierTreeLayoutAlgorithm (line 17) | class TidierTreeLayoutAlgorithm extends Algorithm {
method isVertical (line 30) | bool isVertical()
method needReverseOrder (line 36) | bool needReverseOrder()
method run (line 43) | Size run(Graph? graph, double shiftX, double shiftY)
method _clearMetadata (line 65) | void _clearMetadata()
method _buildTree (line 71) | void _buildTree(Graph graph)
method _initializeData (line 96) | void _initializeData(Graph graph)
method _findRoots (line 112) | List<Node> _findRoots(Graph graph)
method _nodeData (line 126) | TidierTreeNodeData _nodeData(Node? v)
method _firstWalk (line 131) | void _firstWalk(Node? v, Node? leftSibling)
method _secondWalk (line 168) | void _secondWalk(Node? v, int m, int depth, int yOffset)
method _updateBounds (line 198) | void _updateBounds(Node node, int centerX, int centerY)
method _computeMaxHeights (line 212) | void _computeMaxHeights(Node? node, int depth)
method _leftChild (line 234) | Node? _leftChild(Node? v)
method _rightChild (line 239) | Node? _rightChild(Node? v)
method _getDistance (line 244) | int _getDistance(Node? v, Node? w, bool isSibling)
method _apportion (line 258) | Node? _apportion(
method _ancestor (line 321) | Node? _ancestor(Node? vil, Node? parentOfV, Node? defaultAncestor)
method _moveSubtree (line 331) | void _moveSubtree(
method _childPosition (line 350) | int _childPosition(Node? node, Node? parentNode)
method _shift (line 367) | void _shift(Node? v)
method _normalizePositions (line 382) | void _normalizePositions(Graph graph)
method _applyOrientation (line 395) | void _applyOrientation(Graph graph)
method _shiftCoordinates (line 429) | void _shiftCoordinates(Graph graph, double shiftX, double shiftY)
method _createSpanningTree (line 435) | Graph _createSpanningTree(Graph graph)
method successorsOf (line 469) | List<Node> successorsOf(Node? v)
method predecessorsOf (line 475) | List<Node> predecessorsOf(Node v)
method init (line 482) | void init(Graph? graph)
method setDimensions (line 485) | void setDimensions(double width, double height)
FILE: lib/tree/TreeEdgeRenderer.dart
class TreeEdgeRenderer (line 3) | class TreeEdgeRenderer extends EdgeRenderer {
method render (line 10) | void render(Canvas canvas, Graph graph, Paint paint)
method renderEdge (line 17) | void renderEdge(Canvas canvas, Edge edge, Paint paint)
method _drawStyledPath (line 50) | void _drawStyledPath(Canvas canvas, Path path, Paint paint, LineType l...
method _extractPathPoints (line 67) | List<Offset> _extractPathPoints(Path path)
method getEffectiveOrientation (line 96) | int getEffectiveOrientation(Node node, Node child)
method buildEdgePath (line 101) | void buildEdgePath(Node node, Node child, Offset parentPos, Offset chi...
method buildTopBottomPath (line 129) | void buildTopBottomPath(Node node, Node child, Offset parentPos, Offse...
method buildBottomTopPath (line 155) | void buildBottomTopPath(Node node, Node child, Offset parentPos, Offse...
method buildLeftRightPath (line 179) | void buildLeftRightPath(Node node, Node child, Offset parentPos, Offse...
method buildRightLeftPath (line 203) | void buildRightLeftPath(Node node, Node child, Offset parentPos, Offse...
FILE: test/algorithm_performance_test.dart
function main (line 11) | void main()
function _createGraph (line 12) | Graph _createGraph(int n)
FILE: test/buchheim_walker_algorithm_test.dart
function main (line 9) | void main()
function _createGraph (line 101) | Graph _createGraph(int n)
FILE: test/controller_tests.dart
function main (line 5) | void main()
function createComplexGraph (line 128) | Graph createComplexGraph()
FILE: test/graph_test.dart
function main (line 5) | void main()
FILE: test/graphview_perfomance_test.dart
function main (line 6) | void main()
function _createLargeGraph (line 127) | Graph _createLargeGraph(int n)
FILE: test/sugiyama_algorithm_test.dart
function inflateWithJson (line 11) | void inflateWithJson(Map<String, Object> json)
function toRect (line 22) | Rect toRect()
function main (line 25) | void main()
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (467K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 675,
"preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
},
{
"path": ".github/workflows/tests.yml",
"chars": 497,
"preview": "name: Tests\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n tests:\n runs-on: "
},
{
"path": ".gitignore",
"chars": 1490,
"preview": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.i"
},
{
"path": ".metadata",
"chars": 309,
"preview": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrade"
},
{
"path": "CHANGELOG.md",
"chars": 3992,
"preview": "## 1.5.1\n- Fix Zoom To fit for hidden nodes\n- Add Fade in Support for Edges\n- Add Loopback support \n\n## 1.5.0\n\n- **MAJOR"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2025 Nabil Mosharraf\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 19711,
"preview": "GraphView\n===========\nGet it from\n[](https://pub.dev/packages/"
},
{
"path": "analysis_options.yaml",
"chars": 1453,
"preview": "linter:\n rules:\n - always_declare_return_types\n - annotate_overrides\n - avoid_empty_else\n - avoid_init_to_n"
},
{
"path": "example/.gitignore",
"chars": 584,
"preview": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.i"
},
{
"path": "example/analysis_options.yaml",
"chars": 1447,
"preview": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n#"
},
{
"path": "example/lib/algorithm_selector_graphview.dart",
"chars": 10695,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\n// Enum for alg"
},
{
"path": "example/lib/decision_tree_screen.dart",
"chars": 1946,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass DecisionT"
},
{
"path": "example/lib/example.dart",
"chars": 2268,
"preview": "import 'package:example/layer_graphview.dart';\nimport 'package:flutter/material.dart';\n\nimport 'force_directed_graphview"
},
{
"path": "example/lib/force_directed_graphview.dart",
"chars": 2475,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass GraphClus"
},
{
"path": "example/lib/graph_cluster_animated.dart",
"chars": 2218,
"preview": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.da"
},
{
"path": "example/lib/large_tree_graphview.dart",
"chars": 6463,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LargeTree"
},
{
"path": "example/lib/layer_eiglesperger_graphview.dart",
"chars": 7669,
"preview": "import 'dart:math';\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LayeredEig"
},
{
"path": "example/lib/layer_graphview.dart",
"chars": 10790,
"preview": "import 'dart:math';\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LayeredGra"
},
{
"path": "example/lib/layer_graphview_json.dart",
"chars": 6681,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass LayerGrap"
},
{
"path": "example/lib/main.dart",
"chars": 13892,
"preview": "import 'package:example/algorithm_selector_graphview.dart';\nimport 'package:example/decision_tree_screen.dart';\nimport '"
},
{
"path": "example/lib/mindmap_graphview.dart",
"chars": 9257,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass MindMapPa"
},
{
"path": "example/lib/mutliple_forest_graphview.dart",
"chars": 6020,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass MultipleF"
},
{
"path": "example/lib/tree_graphview.dart",
"chars": 9349,
"preview": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass TreeViewP"
},
{
"path": "example/lib/tree_graphview_json.dart",
"chars": 5014,
"preview": "import 'package:flutter/material.dart';\nimport 'package:graphview/GraphView.dart';\n\nclass TreeViewPageFromJson extends S"
},
{
"path": "example/pubspec.yaml",
"chars": 2775,
"preview": "name: example\ndescription: A new Flutter project.\n\n# The following line prevents the package from being accidentally pub"
},
{
"path": "lib/Algorithm.dart",
"chars": 380,
"preview": "part of graphview;\n\nabstract class Algorithm {\n EdgeRenderer? renderer;\n\n /// Executes the algorithm.\n /// @param shi"
},
{
"path": "lib/Graph.dart",
"chars": 7212,
"preview": "part of graphview;\n\nclass Graph {\n final List<Node> _nodes = [];\n final List<Edge> _edges = [];\n List<GraphObserver> "
},
{
"path": "lib/GraphView.dart",
"chars": 37535,
"preview": "library graphview;\n\nimport 'dart:async';\nimport 'dart:collection';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'p"
},
{
"path": "lib/edgerenderer/ArrowEdgeRenderer.dart",
"chars": 8196,
"preview": "part of graphview;\n\nconst double ARROW_DEGREES = 0.5;\nconst double ARROW_LENGTH = 10;\n\nclass ArrowEdgeRenderer extends E"
},
{
"path": "lib/edgerenderer/EdgeRenderer.dart",
"chars": 6236,
"preview": "part of graphview;\n\nabstract class EdgeRenderer {\n Map<Node, Offset>? _animatedPositions;\n\n void setAnimatedPositions("
},
{
"path": "lib/forcedirected/FruchtermanReingoldAlgorithm.dart",
"chars": 11831,
"preview": "part of graphview;\n\nclass FruchtermanReingoldAlgorithm implements Algorithm {\n static const double DEFAULT_TICK_FACTOR "
},
{
"path": "lib/forcedirected/FruchtermanReingoldConfiguration.dart",
"chars": 1307,
"preview": "part of graphview;\n\nclass FruchtermanReingoldConfiguration {\n static const int DEFAULT_ITERATIONS = 100;\n static const"
},
{
"path": "lib/layered/EiglspergerAlgorithm.dart",
"chars": 46193,
"preview": "part of graphview;\n\nclass ContainerX {\n List<Segment> segments = [];\n int index = -1;\n int pos = -1;\n double measure"
},
{
"path": "lib/layered/SugiyamaAlgorithm.dart",
"chars": 44098,
"preview": "part of graphview;\n\nclass SugiyamaAlgorithm extends Algorithm {\n Map<Node, SugiyamaNodeData> nodeData = {};\n Map<Edge,"
},
{
"path": "lib/layered/SugiyamaConfiguration.dart",
"chars": 1749,
"preview": "part of graphview;\n\nclass SugiyamaConfiguration {\n static const ORIENTATION_TOP_BOTTOM = 1;\n static const ORIENTATION_"
},
{
"path": "lib/layered/SugiyamaEdgeData.dart",
"chars": 79,
"preview": "part of graphview;\n\nclass SugiyamaEdgeData {\n List<double> bendPoints = [];\n}\n"
},
{
"path": "lib/layered/SugiyamaEdgeRenderer.dart",
"chars": 7426,
"preview": "part of graphview;\n\nclass SugiyamaEdgeRenderer extends ArrowEdgeRenderer {\n Map<Node, SugiyamaNodeData> nodeData;\n Map"
},
{
"path": "lib/layered/SugiyamaNodeData.dart",
"chars": 514,
"preview": "part of graphview;\n\nclass SugiyamaNodeData {\n Set<Node> reversed = {};\n bool isDummy = false;\n int median = -1;\n int"
},
{
"path": "lib/mindmap/MindMapAlgorithm.dart",
"chars": 3207,
"preview": "part of graphview;\n\nenum MindmapSide { LEFT, RIGHT, ROOT }\n\nclass _SideData {\n MindmapSide side = MindmapSide.ROOT;\n}\n\n"
},
{
"path": "lib/mindmap/MindmapEdgeRenderer.dart",
"chars": 912,
"preview": "part of graphview;\n\nclass MindmapEdgeRenderer extends TreeEdgeRenderer {\n MindmapEdgeRenderer(BuchheimWalkerConfigurati"
},
{
"path": "lib/tree/BaloonLayoutAlgorithm.dart",
"chars": 7273,
"preview": "part of graphview;\n\n// Polar coordinate representation\nclass PolarPoint {\n final double theta; // angle in radians\n fi"
},
{
"path": "lib/tree/BuchheimWalkerAlgorithm.dart",
"chars": 15645,
"preview": "part of graphview;\n\nclass BuchheimWalkerAlgorithm extends Algorithm {\n Map<Node, BuchheimWalkerNodeData> nodeData = {};"
},
{
"path": "lib/tree/BuchheimWalkerConfiguration.dart",
"chars": 1104,
"preview": "part of graphview;\n\nclass BuchheimWalkerConfiguration {\n int siblingSeparation = DEFAULT_SIBLING_SEPARATION;\n int leve"
},
{
"path": "lib/tree/BuchheimWalkerNodeData.dart",
"chars": 321,
"preview": "part of graphview;\n\nclass BuchheimWalkerNodeData {\n Node? ancestor;\n Node? thread;\n int number = 0;\n int depth = 0;\n"
},
{
"path": "lib/tree/CircleLayoutAlgorithm.dart",
"chars": 7580,
"preview": "part of graphview;\n\nclass CircleLayoutConfiguration {\n final double radius;\n final bool reduceEdgeCrossing;\n final in"
},
{
"path": "lib/tree/RadialTreeLayoutAlgorithm.dart",
"chars": 8288,
"preview": "part of graphview;\n\nclass TreeLayoutNodeData {\n Rectangle? bounds;\n int depth = 0;\n bool visited = false;\n List<Node"
},
{
"path": "lib/tree/TidierTreeLayoutAlgorithm.dart",
"chars": 13609,
"preview": "part of graphview;\n\nclass TidierTreeNodeData {\n int mod = 0;\n Node? thread;\n int shift = 0;\n Node? ancestor;\n int x"
},
{
"path": "lib/tree/TreeEdgeRenderer.dart",
"chars": 7570,
"preview": "part of graphview;\n\nclass TreeEdgeRenderer extends EdgeRenderer {\n BuchheimWalkerConfiguration configuration;\n\n TreeEd"
},
{
"path": "pubspec.yaml",
"chars": 432,
"preview": "name: graphview\ndescription: GraphView is used to display data in graph structures. It can display Tree layout, Directe"
},
{
"path": "test/algorithm_performance_test.dart",
"chars": 2217,
"preview": "import 'dart:ui';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'pack"
},
{
"path": "test/buchheim_walker_algorithm_test.dart",
"chars": 4999,
"preview": "\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/Grap"
},
{
"path": "test/controller_tests.dart",
"chars": 7340,
"preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/Graph"
},
{
"path": "test/example_trees.dart",
"chars": 22785,
"preview": "const exampleTreeWith140Nodes = {\n 'edges': [\n {'from': '7045321', 'to': '308264215'},\n {'from': '308264215', 'to"
},
{
"path": "test/graph_test.dart",
"chars": 4051,
"preview": "import 'package:flutter/widgets.dart';\nimport 'package:graphview/GraphView.dart';\nimport 'package:flutter_test/flutter_t"
},
{
"path": "test/graphview_perfomance_test.dart",
"chars": 4537,
"preview": "import 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter_test/flutter_te"
},
{
"path": "test/sugiyama_algorithm_test.dart",
"chars": 35455,
"preview": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:graphview/Graph"
}
]
About this extraction
This page contains the full source code of the nabil6391/graphview GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (438.3 KB), approximately 111.0k tokens, and a symbol index with 641 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.