PointDraw for hv.Graph (sync two streams?)

Hi all,

I’m making a network visualization app, and I’m hoping to implement a PointDraw stream on the network (only for drag/drop purposes–not for adding additional nodes). I have gotten this to work for the graph.nodes element of the plot, but I’m struggling to figure out how to also get the graph edgepaths and labels to update with the node positions. Below is my code that successfully updates the node locations.

import pandas as pd
import numpy as np
import itertools
import holoviews as hv
import networkx as nx

hv.extension('bokeh')

# define some test nodes/edges
test_nodes = pd.DataFrame([[50.,],
                           [5.,],
                           [100.,],
                           [20.,],
                           [75.,],
                           [1.,]], columns = ['node score'])

e = np.array(list(itertools.combinations(test_nodes.index, 2)))
test_edges = pd.DataFrame(np.vstack([e.T, np.random.uniform(0, 1, len(e))]).T, columns = ['source', 'target', 'edge score'])

# make the graph, adding columns as node attributes
g = nx.Graph()
g.add_nodes_from(test_nodes.apply(lambda x: (x.name, x.to_dict()), axis=1).values.tolist())
g.add_edges_from(test_edges.apply(lambda x: (x['source'], x['target'], x.to_dict()), axis=1).values.tolist())

# make an hv.Graph from the networkx graph
graph = hv.Graph.from_networkx(g, nx.circular_layout)
labels = hv.Labels(graph.nodes.data, ['x', 'y'], 'index')

# define PointDraw tool (NOTE--this only works with the source set to graph.nodes)
draw = hv.streams.PointDraw(source=graph.nodes, add=False, drag=True)
(graph.edgepaths*(graph.nodes.opts(color='white'))*labels).opts(active_tools=['point_draw'])

I suspect that successfully implementing this will involve linking columns in graph.nodes.data to the graph.edgepaths, but my attempts at this have not been successful. Hopefully someone here can help me out!

Thanks!!

====================== UPDATE ===================

I just discovered the CurveEdit stream, which seems to provide the edge movement functionality that I need. I can now individually move the nodes and edgepaths with the following code–however, I need to be able to link the CurveEdit and PointDraw stream objects to the same values. How can I accomplish this?

# define some test nodes/edges
test_nodes = pd.DataFrame([[50.,],
                           [5.,],
                           [100.,],
                           [20.,],
                           [75.,],
                           [1.,]], columns = ['node score'])

e = np.array(list(itertools.combinations(test_nodes.index, 2)))
test_edges = pd.DataFrame(np.vstack([e.T, np.random.uniform(0, 1, len(e))]).T, columns = ['source', 'target', 'edge score'])

# make the graph, adding columns as node attributes
G = nx.Graph()
G.add_nodes_from(test_nodes.apply(lambda x: (x.name, x.to_dict()), axis=1).values.tolist())
G.add_edges_from(test_edges.apply(lambda x: (x['source'], x['target'], x.to_dict()), axis=1).values.tolist())

# make an hv.Graph from the networkx graph
graph = hv.Graph.from_networkx(G, nx.circular_layout)
labels = hv.Labels(graph.nodes.data, ['x', 'y'], 'index')
curve = hv.Curve(graph.edgepaths.columns())

pt_edit = hv.streams.PointDraw(source=graph.nodes, data=graph.nodes.columns(), add=False)
cv_edit = hv.streams.CurveEdit(source=curve, data=curve.columns(),)

(curve*graph.nodes).opts(xlim=(-1.3, 1.3), ylim=(-1.3, 1.3))

Not the prettiest solution because the labels and edge vertices do not update in real time with the dragging of a node, but the following code at least gave me draggable nodes/edges

import pandas as pd
import numpy as np
import itertools
import holoviews as hv
import networkx as nx
import param
from holoviews import opts, dim
import panel as pn

hv.extension('bokeh')

class DraggableGraph(param.Parameterized):
    
    nodes = param.DataFrame(precedence=-1)
    edges = param.DataFrame(precedence=-1)
    
    node_data = param.DataFrame()
    
    stream = hv.streams.PointDraw(add=False)
    
    def __init__(self, nodes, edges, **params):
        super(DraggableGraph, self).__init__(**params)
        
        self.G = self.make_graph(nodes, edges)
        
        self.nodes = nodes
        self.edges = edges
        
        self.node_data = pd.concat([self.nodes, pd.DataFrame(nx.circular_layout(self.G), index=['x', 'y']).T], axis=1)
        
        self.node_graph = hv.DynamicMap(self.view_nodes)
        self.stream.source = self.node_graph
        
        self.edge_graph = hv.DynamicMap(self.view_edges, streams = [self.stream])
        self.labels = hv.DynamicMap(self.view_labels, streams = [self.stream])
        
        overlay = (self.edge_graph*self.node_graph*self.labels)
        
        
        graph_opts = [
            opts.Nodes(
                active_tools=['point_draw',], 
                color='white',
                line_color='black',
                size = 'node_score',
            ),
            opts.Graph(
                edge_color = dim('edge_score'),
                node_size = 0, # hide nodes from this representation
                tools = []
            ),
            opts.Overlay(
                xlim=(-1.3, 1.3),
                ylim=(-1.3, 1.3),
                responsive=True,
                xaxis=None, 
                yaxis=None
            )
        ]
        
        self.overlay = pn.pane.HoloViews(overlay.opts(*graph_opts), 
                                         sizing_mode='stretch_both', 
                                         height=400, 
                                         width=400
                                         )
    
    def make_graph(self, nodes, edges):
        
        G = nx.Graph()
        for idx, data in nodes.sort_index().iterrows():
            G.add_node(idx, **data.to_dict())
        
        for idx, data in edges.sort_index().iterrows():
            G.add_edge(data['source'], data['target'], **data.to_dict())
         
        return G
        
    def view_nodes(self):
        g = hv.Graph.from_networkx(self.G, dict(zip(self.node_data.index, self.node_data[['x', 'y']].values)))
                
        return g.nodes
        
    def view_edges(self, data):
        if data is None:
            data = self.node_data
        else:
            data = pd.DataFrame(data)
        
        g = hv.Graph.from_networkx(self.G, dict(zip(data.index, data[['x', 'y']].values)))
        
        return g
    
    def view_labels(self, data):
        if data is None:
            data = self.node_data
        else:
            data = pd.DataFrame(data)
        
        return hv.Labels(data, ['x', 'y'], 'label')
        
    @param.depends('stream.data', watch=True)
    def _update_dataframe(self):
        self.node_data = pd.DataFrame(self.stream.data)
        
test_nodes = pd.DataFrame([[20., 'a'],
                           [50., 'b'],
                           [30., 'c'],
                           [50., 'd'],
                           [75., 'e'],
                           [88., 'f']], columns = ['node_score', 'label'])

e = np.array(list(itertools.combinations(test_nodes.index, 2)))
test_edges = pd.DataFrame(np.vstack([e.T, np.random.uniform(0, 256, len(e))]).T, columns = ['source', 'target', 'edge_score'])

graph = DraggableGraph(nodes=test_nodes, edges=test_edges)
graph.overlay

Hopefully this can be helpful to someone else–if anyone has a better approach, let me know!!

3 Likes