orb
orb copied to clipboard
Any way to select only node/edge which is clicked
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 ?
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.
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.
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.:
- Select only the clicked node
- Select the clicked node, but also select the adjacent nodes
- Select the path between two clicked nodes
- Upon clicking, select the clicked node on the first click, and then select 1 hop more for each new click on the same node - more like spread clicking
There are so many cases and we can't cover them all with flags, so my thinking is in this direction:
- The ORB should have a structure to keep nodes/edges by their state (selected, hovered, or even custom user ones - that is the reason why the state can be any number) - this is for easier and faster lookup, currently, you need to iterate through all nodes/edges to get the selected/hovered one which is not an optimal way
- The state change of the node/edge goes through
setState
which will be listened to by ORB and ORB components (renderer). ORB will react to it by updating the internal structure for faster lookup, while the renderer should do the throttling for therender
andsimulate
calls - this requires integration ofRxJS
which we have in plan and which will happen on therelease/1.0.0
branch
With just these above two changes, the default strategy
will be questionable how much sense it has. There will be two paths:
-
If you want custom select/hover logic, you disable the default via settings and implement yours in the event listeners. (like the example above)
-
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?
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.
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!