nabil6391 / graphview

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.
MIT License
420 stars 114 forks source link

Is it possible to call removeNode on a node with successors? #29

Closed sjimbonator closed 3 years ago

sjimbonator commented 3 years ago

Hi when I try to call graph.removeNode() on a node with successors I get the following error:

The following JSNoSuchMethodError was thrown during performLayout():
NoSuchMethodError: invalid member on null: 'depth'

Should graph.removeNode() be able to handle nodes with successors? Or am I supposed to call removeNode() on all the deepest nodes first and work my way up from there?

I am using the TreeEdgeRenderer inside my GraphView based on the pub.dev example: https://pub.dev/packages/graphview#usage

Here is my code in case this is a bug instead of improper usage of removeNode() on my part.

This is my initState method:

  @override
  void initState() {
    super.initState();
    myGraph = Graph();

    final organisation = Organisation();
    final firstNode = Node(
      OrganisationCard(
        organisation,
        addNodeToOrg: (Node node, Organisation org) {
          myGraph.addEdge(
            myGraph.getNodeUsingKey(ObjectKey(org)),
            node,
          );
          setState(() {});
        },
        hideNodes: (List<Node> nodes) {
          myGraph.removeNodes(nodes);
          setState(() {});
        },
        expanded: true,
      ),
      key: ObjectKey(organisation),
    );

    myGraph.addNode(firstNode);

    builder = BuchheimWalkerConfiguration()
      ..siblingSeparation = (50)
      ..levelSeparation = (50)
      ..subtreeSeparation = (50)
      ..orientation = BuchheimWalkerConfiguration.DEFAULT_ORIENTATION;
  }

This is my Build method:

  @override
  Widget build(BuildContext context) {
    return Screen(
      title: Text('Organisations'),
      body: Center(
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: SingleChildScrollView(
            child: Padding(
              padding: const EdgeInsets.only(
                right: 16.0,
                bottom: 16.0,
              ),
              child: GraphView(
                algorithm: BuchheimWalkerAlgorithm(
                  builder,
                  TreeEdgeRenderer(builder),
                ),
                graph: myGraph,
                paint: Paint()
                  ..color = Theme.of(context).primaryIconTheme.color
                  ..strokeWidth = 1
                  ..style = PaintingStyle.fill,
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.search_rounded),
        onPressed: () {},
      ),
    );
  }

And this is my OrganisationCard widget that's used inside the Nodes:

class OrganisationCard extends StatefulWidget {
  final Organisation organisation;

  final void Function(Node node, Organisation org) addNodeToOrg;
  final void Function(List<Node> nodes) hideNodes;

  final bool expanded;

  OrganisationCard(
    this.organisation, {
    @required this.addNodeToOrg,
    @required this.hideNodes,
    this.expanded = false,
  })  : assert(addNodeToOrg != null),
        assert(hideNodes != null);

  @override
  State<StatefulWidget> createState() {
    return OrganisationCardState();
  }
}

enum popupOptions { expand, add }

class OrganisationCardState extends State<OrganisationCard> {
  List<Organisation> subOrganisations;

  final subOrganisationNodes = <Node>[];

  bool expanded;

  bool hasBeenExpanded = false;

  Node organisationToNode(Organisation organisation) => Node(
        OrganisationCard(
          organisation,
          addNodeToOrg: widget.addNodeToOrg,
          hideNodes: widget.hideNodes,
        ),
        key: ObjectKey(organisation),
      );

  void expand() {
    if (!hasBeenExpanded) {
      subOrganisationNodes.addAll(
        subOrganisations.map<Node>(organisationToNode).toList(),
      );
      hasBeenExpanded = true;
    }

    subOrganisationNodes.forEach(
      (node) => widget.addNodeToOrg(node, widget.organisation),
    );
  }

  @override
  void initState() {
    super.initState();
    expanded = widget.expanded;

    //TODO: This class should either use the subOrganisations list from
    // widget.organisation or generate subOrganisations based on data from
    // widget.organisation once the `Organisation` class has been implemented
    subOrganisations = [
      Organisation(),
      Organisation(),
      Organisation(),
    ];

    if (expanded) {
      WidgetsBinding.instance.addPostFrameCallback((_) => expand());
    }
  }

  @override
  Widget build(BuildContext context) {
    const double maxTextWidth = 150;
    const double maxTextHeight = 60;

    return RoundedContainer(
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          customBorder: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(
              Radius.circular(10),
            ),
          ),
          child: Padding(
            padding: EdgeInsets.only(left: 5),
            child: Row(
              children: [
                ConstrainedBox(
                  constraints: BoxConstraints(
                    maxWidth: maxTextWidth,
                    maxHeight: maxTextHeight,
                  ),
                  child: Container(
                    width: maxTextWidth,
                    height: maxTextHeight,
                    child: Center(
                      child: Text(
                        'Mobiléa',
                        style: Theme.of(context).textTheme.subtitle2,
                        softWrap: true,
                      ),
                    ),
                  ),
                ),
                PopupMenuButton(
                  icon: Icon(
                    Icons.more_vert_rounded,
                    size: 18,
                  ),
                  onSelected: (option) {
                    when(option, {
                      popupOptions.expand: () {
                        if (expanded) {
                          widget.hideNodes(subOrganisationNodes);
                        } else {
                          expand();
                        }
                        setState(() => expanded = !expanded);
                      },
                      popupOptions.add: () {
                        //TODO: Show dialog that gets the user input thats required
                        // to construct an `Organisation` instance once
                        // `Organisation` has been implemented.
                        //TODO: Call some bloc to post newOrganisation to backend.
                        final newOrganisation = Organisation();
                        final node = organisationToNode(newOrganisation);

                        subOrganisations.add(newOrganisation);
                        subOrganisationNodes.add(node);

                        if (expanded) {
                          widget.addNodeToOrg(
                            node,
                            widget.organisation,
                          );
                        }
                      },
                    });
                  },
                  itemBuilder: (context) => <PopupMenuEntry<popupOptions>>[
                    PopupMenuItem<popupOptions>(
                      value: popupOptions.expand,
                      child: Text(expanded ? 'Shrink' : 'Expand'),
                    ),
                    PopupMenuItem<popupOptions>(
                      value: popupOptions.add,
                      child: Text('Add'),
                    ),
                  ],
                ),
              ],
            ),
          ),
          onTap: () => Navigator.of(context).pushNamed('/organisation'),
        ),
      ),
    );
  }
}
nabil6391 commented 3 years ago

Thanks for the detailed info. Will try to have a look

nabil6391 commented 3 years ago

@sjimbonator

The functionality is already there , although not properly documented yet

Just set the graph isTree to true. And it should automatically remove all succesors graph.isTree = true

sjimbonator commented 3 years ago

@nabil6391

Thanks! It works now