plotly / dash-cytoscape

Interactive network visualization in Python and Dash, powered by Cytoscape.js
https://dash.plot.ly/cytoscape
MIT License
595 stars 120 forks source link

Selecting all nodes / Multi-selection #161

Open KuechlerO opened 2 years ago

KuechlerO commented 2 years ago

Description

Hey guys, first a huge thx for your work. The cytoscape project is truly awesome! However, I've stumbled above this problem and couldn't find any posts about it in the community chat room, so I thought I give it a go here :)

So the problem is: When I try to implement a button "Select all nodes", that allows the user to select all nodes (via setting class 'selected': True in all nodes), the system gets buggy.

Concrete: The user can play around and do stuff, but if

  1. she clicks on "Select all nodes" -> all nodes are selected
  2. she clicks anywhere outside of the nodes -> all selections are dropped (but the class 'selected': True actually stays in the nodes)
  3. hence, when she now clicks again on the button "Select all nodes", nothing happens, cause the class-attributes are already set.

Steps/Code to Reproduce

import dash
import dash_cytoscape as cyto
import dash_html_components as html
from dash.dependencies import Output, Input, State
import dash_bootstrap_components as dbc

from django_plotly_dash import DjangoDash

app = DjangoDash('Minimal_example_for_select_bug')

app.layout = html.Div([
    cyto.Cytoscape(
        elements=[
            {
                'data': {'id': 1, 'label': 1},
                'selected': True
            },
            {
                'data': {'id': 2, 'label': 2}
            },
            {
                'data': {'id': 3, 'label': 3}
            },
        ],
        id='graph'
    ),
    dbc.Button("Select all", color="info", id="btn_exploration_select_all", className="m-1",
               n_clicks=0, disabled=False),
    dbc.Button("Deselect all", color="warning", id="btn_exploration_deselect_all", className="m-1",
               n_clicks=0, disabled=False),
])

@app.callback(Output('graph', 'elements'),
              # Exploration tab buttons
              Input("btn_exploration_select_all", "n_clicks"),
              Input("btn_exploration_deselect_all", "n_clicks"),
              State("graph", "elements"),
              )
def update_network(btn1, btn2, network_elements):
    ctx = dash.callback_context  # Dash context to figure out, which button has been pressed
    if not ctx.triggered:  # Initial execution
        return network_elements
    else:
        print("elements:", network_elements)
        button_id = ctx.triggered[0]['prop_id'].split('.')[0]
        if button_id == "btn_exploration_select_all":
            for elem in network_elements:
                elem["selected"] = True

        elif button_id == "btn_exploration_deselect_all":
            for elem in network_elements:
                elem["selected"] = False

    print("Now:", network_elements)
    return network_elements

Expected Results

I would expect, that also when the user clicks outside of the nodes and therefore changes the selection, it is somehow represented in the cytoscape-elements, so that it can be manipulated with respective functions.

Actual Results

User selection of specific nodes can not be accessed via the given Callback-Options.

Versions

Django 3.2.7 Dash 2.0.0 Dash Core Components 2.0.0 Dash HTML Components 2.0.0 Dash Renderer 1.9.1 Dash HTML Components 0.2.0

Using Firefox

KuechlerO commented 2 years ago

I've found a work-around:

  1. I also take "selectedNodeData" as input
  2. Then every time, before I further work with the network nodes, I update the "selected"-classes based on the selected Nodes that are returned through Input("graph", "selectedNodeData").

So basically I make sure, that what is being displayed is then also reflected in the selection classes of the network elements.


@app.callback(Output('graph', 'elements'),
              # Exploration tab buttons
              Input("btn_exploration_select_all", "n_clicks"),
              Input("btn_exploration_deselect_all", "n_clicks"),
              Input("graph", "selectedNodeData"),
              State("graph", "elements"),
              )
def update_network(btn1, btn2, selected_elems, network_elements):
    network_elements = reset_selection_classes(network_elements, selected_elems)

    ctx = dash.callback_context  # Dash context to figure out, which button has been pressed
    if not ctx.triggered:  # Initial execution
        return network_elements
    else:
        button_id = ctx.triggered[0]['prop_id'].split('.')[0]
        if button_id == "btn_exploration_select_all":
            for elem in network_elements:
                elem["selected"] = True

        elif button_id == "btn_exploration_deselect_all":
            for elem in network_elements:
                elem["selected"] = False

    return network_elements

def reset_selection_classes(network_elems, selected_elems):
    if selected_elems or selected_elems == []:
        selected_node_ids = [entry["id"] for entry in selected_elems]
        for elem in network_elems:
            if elem["data"]["id"] in selected_node_ids:
                elem["selected"] = True
            else:
                elem["selected"] = False
        return network_elems

    else:
        return network_elems```