memgraph / orb

Graph visualization library
Apache License 2.0
349 stars 17 forks source link

Any way to select only node/edge which is clicked #68

Open parmar-abhinav opened 1 year ago

parmar-abhinav commented 1 year ago

When any node is selected then its adjacent edges/nodes also gets selected. passing isDefaultSelectEnabled to false in strategy does not select any node/edge.

any way to select only the node/edge which is clicked ?

tonilastre commented 1 year ago

If you are checking the Orb from release/1.0.0 branch it can be done using the CLICK events and node.state + node.clearState. The same can be done for the default hover.

const orb = new OrbView(container, {
  strategy: {
    isDefaultSelectEnabled: false,
  },
});

let selectedNodeId = null;
orb.events.on(OrbEventType.MOUSE_CLICK, (event) => {
  // Clear the previously selected node
  if (selectedNodeId !== null) {
    const selectedNode = orb.data.getNodeById(selectedNodeId);
    selectedNode?.clearState();
    selectedNodeId = null;
  }

  // isNode can be imported from Orb namespace
  if (isNode(event.subject)) {
    event.subject.state = GraphObjectState.SELECTED;
    selectedNodeId = event.subject.id;
  }

  orb.render();
});

Btw I've used MOUSE_CLICK event instead of NODE_CLICK event because MOUSE_CLICK can give out a canvas click, e.g. to unselect any selected one. You can also go through and get the latest selected nodes and unselect them using:

orb.data.getSelectedNodes().forEach(node => node.clearState());

Where you don't need to remember the last selected node. The iteration to slower though because it needs to go through all the nodes to find the selected ones.

Just FYI, we plan to change the API a bit to add RxJS so all these setters (like state on the node) will be set<XYZ> where you won't need to call orb.render() for orb to know that there is a change in the data and the render is needed.

parmar-abhinav commented 1 year ago

Based on the provided approach, it seems that the code snippet effectively addresses the requirement to select only the clicked node or edge. However, it could be beneficial to have a more flexible solution that allows for passing an argument to control which nodes and edges are selected upon clicking. This could provide greater customization options based on specific use cases.

For example, introducing a parameter like selectClickedOnly to the OrbView constructor or selection strategy could offer more control over the selection behavior. When selectClickedOnly is set to true, only the clicked node or edge would be selected, and adjacent elements would remain unselected. On the other hand, when set to false (or omitted), the current behavior of selecting adjacent elements along with the clicked node or edge would be maintained.

By introducing this parameter, users of the Orb library could easily tailor the selection behavior to suit their specific requirements without having to modify the event handling logic each time. It would enhance the flexibility and usability of the library in different scenarios.

tonilastre commented 1 year ago

I totally agree about providing greater customization based on specific use cases. I think the selectClickedOnly would just add a bit to the default strategy, but it wouldn't solve the general problem: How to enable users of the Orb API to be able to do any kind of select/hover flows. E.g.:

There are so many cases and we can't cover them all with flags, so my thinking is in this direction:

With just these above two changes, the default strategy will be questionable how much sense it has. There will be two paths:

1) If you want custom select/hover logic, you disable the default via settings and implement yours in the event listeners. (like the example above)

2) If you want custom select/hover logic, you will implement a custom callback for each select/hover settings function - I am talking about the functions from the DefaultEventStrategy class: https://github.com/memgraph/orb/blob/main/src/models/strategy.ts - these will be exposed out in the settings.

Additionally, regardless of the two paths (event listener or callback implementation), an update on the setState for nodes and edges can be done with optional options, e.g.

// Select the node
node.setState({ state: SELECTED });

// Select the node if not selected, otherwise unselect it
node.setState({ state: SELECTED, options: { isToggle: true }});

// Select the node, but unselect any other node that is currently selected (aka only the single node will be selected)
node.setState({ state: SELECTED, options: { isSingle: true }}};

What do you think?

parmar-abhinav commented 1 year ago

Thank you for your valuable insights and suggestions! I fully understand the need for greater customization in select/hover flows and the limitations of a simple flag-based approach.

Based on your suggestions, I agree that implementing a structured storage mechanism and introducing the setState method would be beneficial for enabling advanced customization.

I will work on making these changes into the library as per your recommendations.

parmar-abhinav commented 1 year ago

I am thinking of a class with below interface to store nodes and edges id by their state. The StateStorage class will provide methods to update and retrieve nodes and edges with specific states, ensuring easy and quick access. Additionally, I am planning to integrate this class with the graph.ts to ensure that any changes to the state of nodes or edges will be observed by graph.ts which will update the StateStorage object present.

// Define the interface for StateStorage
export interface IStateStorage<N extends INodeBase, E extends IEdgeBase> {
  // key defines the state and set contains the node/edge id that belongs to particular state
  nodeStateMap: Map<number, Set<number>>;
  edgeStateMap: Map<number, Set<number>>;

   // update the map, to add particular node/edge to a given state
  updateState(graphObject: INode<N, E> | IEdge<N, E>, state: number): void;
   // remove node/edge from the map
  clearState(graphObject: INode<N, E> | IEdge<N, E>): void;
   // clear both nodes and edge state maps
  clearAllState(): void;
  // returns node id's belonging to given state
  getNodesWithState(state: number): Set<number>;
  // returns edge id's belonging to given state
  getEdgesWithState(state: number): Set<number>;
}

@tonilastre Kindly review the approach and provide your valuable feedback. Thank you!