I looked at datashader/Networks and PyViz/network_packets (which also uses datashader). I want to get a callback to python code on node selection (call a function on a node id, i.e. fun(node_id)
). How can this be done in PyViz/datashader?
I've made a real effort to implement it with datashader or PyViz, however, from how you phrase the question it seems that It's more important to you to get a callback on the networks graph node than the specific technology.
This is a solution for a callback on node_id
based on the maccdc2012_edges
network graph, using NetworkX which is very similar to the technologies specified.
Declared my_callback(node_id)
callback, up to your requirements, which is triggered when clicking on a specific node.
I've limited to the first 10 rows so it can be seen clearly and added scroll zoom for your convenience.
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
import graphistry
from pylab import *
class AnnoteFinder: # thanks to http://www.scipy.org/Cookbook/Matplotlib/Interactive_Plotting
"""
callback for matplotlib to visit a node (display an annotation) when points are clicked on. The
point which is closest to the click and within xtol and ytol is identified.
"""
def __init__(self, xdata, ydata, annotes, callback = None, threshold=None, axis=None, xtol=None, ytol=None):
self.data = list(zip(xdata, ydata, annotes))
if xtol is None: xtol = ((max(xdata) - min(xdata))/float(len(xdata)))/2
if ytol is None: ytol = ((max(ydata) - min(ydata))/float(len(ydata)))/2
self.xtol = xtol
self.ytol = ytol
if axis is None: axis = gca()
self.axis= axis
self.drawnAnnotations = {}
self.links = []
self.callback = callback
self.threshold = threshold if threshold else 1.0e-3
def __call__(self, event):
if event.inaxes:
clickX = event.xdata
clickY = event.ydata
if self.axis is None or self.axis==event.inaxes:
annotes = []
smallest_x_dist = float('inf')
smallest_y_dist = float('inf')
for x,y,a in self.data:
if abs(clickX-x)<=smallest_x_dist and abs(clickY-y)<=smallest_y_dist :
dx, dy = x - clickX, y - clickY
annotes.append((dx*dx+dy*dy,x,y, a) )
smallest_x_dist=abs(clickX-x)
smallest_y_dist=abs(clickY-y)
if annotes:
annotes.sort() # to select the nearest node
distance, x, y, annote = annotes[0]
print(distance)
if distance < self.threshold:
if self.callback:
self.callback(annote)
# https://notebooks.azure.com/seanreed1111/projects/PYVIZ1/html/data/maccdc2012_edges.parq
df = pd.read_parquet('maccdc2012_edges.parq').head(10)
def my_callback(node_id):
print(f'Clicked {node_id}')
# Build your graph
G = nx.from_pandas_edgelist(df, 'source', 'target')
pos = nx.spring_layout(G,k=0.1, iterations=20) # the layout gives us the nodes position x,y,annotes=[],[],[] for key in pos:
x, y, annotes = [], [], []
for key in pos:
d = pos[key]
annotes.append(key)
x.append(d[0])
y.append(d[1])
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
nx.draw(G, pos, font_size=6, node_color='skyblue', edge_color='#BB0000', width=0.5, node_size=200, with_labels=True)
af = AnnoteFinder(x, y, annotes, my_callback)
connect('button_press_event', af)
class ZoomPan:
def __init__(self):
self.press = None
self.cur_xlim = None
self.cur_ylim = None
self.x0 = None
self.y0 = None
self.x1 = None
self.y1 = None
self.xpress = None
self.ypress = None
def zoom_factory(self, ax, base_scale = 2.):
def zoom(event):
cur_xlim = ax.get_xlim()
cur_ylim = ax.get_ylim()
xdata = event.xdata # get event x location
ydata = event.ydata # get event y location
if event.button == 'down':
# deal with zoom in
scale_factor = 1 / base_scale
elif event.button == 'up':
# deal with zoom out
scale_factor = base_scale
else:
# deal with something that should never happen
scale_factor = 1
print(event.button)
new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor
relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0])
rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0])
ax.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)])
ax.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)])
ax.figure.canvas.draw()
fig = ax.get_figure() # get the figure of interest
fig.canvas.mpl_connect('scroll_event', zoom)
return zoom
def pan_factory(self, ax):
def onPress(event):
if event.inaxes != ax: return
self.cur_xlim = ax.get_xlim()
self.cur_ylim = ax.get_ylim()
self.press = self.x0, self.y0, event.xdata, event.ydata
self.x0, self.y0, self.xpress, self.ypress = self.press
def onRelease(event):
self.press = None
ax.figure.canvas.draw()
def onMotion(event):
if self.press is None: return
if event.inaxes != ax: return
dx = event.xdata - self.xpress
dy = event.ydata - self.ypress
self.cur_xlim -= dx
self.cur_ylim -= dy
ax.set_xlim(self.cur_xlim)
ax.set_ylim(self.cur_ylim)
ax.figure.canvas.draw()
fig = ax.get_figure() # get the figure of interest
# attach the call back
fig.canvas.mpl_connect('button_press_event',onPress)
fig.canvas.mpl_connect('button_release_event',onRelease)
fig.canvas.mpl_connect('motion_notify_event',onMotion)
#return the function
return onMotion
scale = 1.1
zp = ZoomPan()
figZoom = zp.zoom_factory(ax, base_scale = scale)
figPan = zp.pan_factory(ax)
show()
Disclaimer: This solution is an adjustment based on these threads: 1 2.