"""
Some useful things to tweak and reproduce the visualizations.
"""
import numpy as np
import networkx as nx
import matplotlib as mpl
import matplotlib.pyplot as pl
from matplotlib.collections import LineCollection, EllipseCollection
def _get_node_index(network_properties,node_id):
"""
Get the node's index position in the node list of the
stylized network.
Parameters
----------
network_properties : dict
The network properties which are returned from the
interactive visualization.
node_id : str or int
The node of which to get the position
Returns
-------
i : int
Index position of the node ``network_properties['nodes']``
Returns `None` if node is not found
Example
-------
>>> props, _ = visualize(G)
>>> i = _get_node_index(props, 0)
"""
N = len(network_properties['nodes'])
for index, node in enumerate(network_properties['nodes']):
if node_id == node['id']:
break
elif index == N-1:
index = None
return index
[docs]def node_pos(network_properties,node_id):
"""
Get the node's position in matplotlib data coordinates.
Parameters
----------
network_properties : dict
The network properties which are returned from the
interactive visualization.
node_id : str or int
The node of which to get the position
Returns
-------
x : float
The x-position in matplotlib data coordinates
y : float
The y-position in matplotlib data coordinates
Example
-------
>>> props, _ = visualize(G)
>>> node_pos(props, 0)
"""
index = _get_node_index(network_properties,node_id)
height = network_properties['ylim'][1] - network_properties['ylim'][0]
node = network_properties['nodes'][index]
return node['x_canvas'], height - node['y_canvas']
[docs]def add_node_label(ax,
network_properties,
node_id,
label=None,
dx=0,
dy=0,
ha='center',
va='center',
**kwargs):
"""
Add a label to a node in the drawn matplotlib axis
Parameters
----------
ax : matplotlib.Axis
The Axis object which has been used to draw the network
network_properties : dict
The network properties which are returned from the
interactive visualization.
node_id : str or int
The focal node's id in the `network_properties` dict
label : str, default : None
The text to write at the node's position
If `None`, the value of `node_id` will be put there.
dx : float, default : 0.0
Label offset in x-direction
dy : float, default : 0.0
Label offset in y-direction
ha : str, default : 'center'
Horizontal anchor orientation of the text
va : str, default : 'center'
Vertical anchor orientation of the text
**kwargs : dict
Additional styling arguments forwarded to Axis.text
Example
-------
>>> netw, _ = netwulf.visualize(G)
>>> fig, ax = netwulf.draw_netwulf(netw)
>>> netwulf.add_node_label(ax,netw,0)
"""
pos = node_pos(network_properties, node_id)
if label is None:
label = str(node_id)
zorder = max( _c.get_zorder() for _c in ax.get_children()) + 1
ax.text(pos[0]+dx,pos[1]+dy,label,ha=ha,va=va,zorder=zorder,**kwargs)
[docs]def add_edge_label(ax,
network_properties,
edge,
label=None,
dscale=0.5,
dx=0,
dy=0,
ha='center',
va='center',
**kwargs):
"""
Add a label to an edge in the drawn matplotlib axis
Parameters
----------
ax : matplotlib.Axis
The Axis object which has been used to draw the network
edge : 2-tuple of str or int
The edge's node ids
network_properties : dict
The network properties which are returned from the
interactive visualization.
label : str, default : None
The text to write at the node's position
If `None`, the tuple of node ids in `edge` will be put there.
dscale : float, default : 0.5
At which position between the two nodes to put the label
(``dscale = 0.0`` refers to the position of node ``edge[0]``
and ``dscale = 1.0`` refers to the position of node ``edge[1]``,
so use any number between 0.0 and 1.0).
dx : float, default : 0.0
Additional label offset in x-direction
dy : float, default : 0.0
Additional label offset in y-direction
ha : str, default : 'center'
Horizontal anchor orientation of the text
va : str, default : 'center'
Vertical anchor orientation of the text
**kwargs : dict
Additional styling arguments forwarded to Axis.text
Example
-------
>>> netw, _ = netwulf.visualize(G)
>>> fig, ax = netwulf.draw_netwulf(netw)
>>> netwulf.add_node_label(ax,netw,0)
"""
v0 = np.array(node_pos(network_properties, edge[0]))
v1 = np.array(node_pos(network_properties, edge[1]))
e = (v1-v0)
if label is None:
label = str("("+str(edge[0])+", "+str(edge[1])+")")
pos = v0 + dscale * e
ax.text(pos[0]+dx,pos[1]+dy,label,ha=ha,va=va,**kwargs)
[docs]def bind_properties_to_network(network,
network_properties,
bind_node_positions=True,
bind_node_color=True,
bind_node_radius=True,
bind_node_stroke_color=True,
bind_node_stroke_width=True,
bind_link_width=True,
bind_link_color=True,
bind_link_alpha=True):
"""
Binds calculated positional values to the network as node attributes `x` and `y`.
Parameters
----------
network : networkx.Graph or something alike
The network object to which the position should be bound
network_properties : dict
The network properties which are returned from the
interactive visualization.
bind_node_positions : bool (default: True)
bind_node_color : bool (default: True)
bind_node_radius : bool (default: True)
bind_node_stroke_color : bool (default: True)
bind_node_stroke_width : bool (default: True)
bind_link_width : bool (default: True)
bind_link_color : bool (default: True)
bind_link_alpha : bool (default: True)
Example
-------
>>> props, _ = netwulf.visualize(G)
>>> netwulf.bind_properties_to_network(G, props)
"""
# Add individial node attributes
if bind_node_positions:
x = { node['id']: node['x'] for node in network_properties['nodes'] }
y = { node['id']: node['y'] for node in network_properties['nodes'] }
nx.set_node_attributes(network, x, 'x')
nx.set_node_attributes(network, y, 'y')
network.graph['rescale'] = False
if bind_node_color:
color = { node['id']: node['color'] for node in network_properties['nodes'] }
nx.set_node_attributes(network, color, 'color')
if bind_node_radius:
radius = { node['id']: node['radius'] for node in network_properties['nodes'] }
nx.set_node_attributes(network, radius, 'radius')
# Add individual link attributes
if bind_link_width:
width = { (link['source'], link['target']): link['width'] for link in network_properties['links'] }
nx.set_edge_attributes(network, width, 'width')
# Add global style properties
if bind_node_stroke_color:
network.graph['nodeStrokeColor'] = network_properties['nodeStrokeColor']
if bind_node_stroke_width:
network.graph['nodeStrokeWidth'] = network_properties['nodeStrokeWidth']
if bind_link_color:
network.graph['linkColor'] = network_properties['linkColor']
if bind_link_alpha:
network.graph['linkAlpha'] = network_properties['linkAlpha']
[docs]def get_filtered_network(network,edge_weight_key=None,node_group_key=None):
"""
Get a copy of a network where the edge attribute ``'weight'`` is
set to the attribute given by the keyword ``edge_weight_key`` and the
nodes are regrouped according to their node attribute provided by
``node_group_key``.
Parameters
----------
network : networkx.Graph or alike
The network object which is about to be filtered
edge_weight_key : str, default : None
If provided, set the edge weight to the edge attribute
given by ``edge_weight_key`` and delete all other edge attributes
node_group_key : str, default : None
If provided, set the node ``'group'`` attribute according to a
new grouping provided by the node attribute ``node_group_key``.
Returns
-------
G : networkx.Graph or alike
A filtered copy of the original network.
"""
G = network.copy()
if edge_weight_key is not None:
for u, v, d in G.edges(data=True):
keep_value = d[edge_weight_key]
d.clear()
G[u][v]['weight'] = keep_value
if node_group_key is not None:
groups = { node[1][node_group_key] for node in network.nodes(data=True) }
groups_enum = {v: k for k,v in enumerate(groups)}
for u in network.nodes():
try:
# networkx v < 2.4
grp = G.node[u].pop(node_group_key)
keep_value = groups_enum[grp]
G.node[u]['group'] = keep_value
except AttributeError as e:
# networkx v >= 2.4
grp = G.nodes[u].pop(node_group_key)
keep_value = groups_enum[grp]
G.nodes[u]['group'] = keep_value
return G
[docs]def draw_netwulf(network_properties, fig=None, ax=None, figsize=None, draw_links=True,draw_nodes=True,link_zorder=-1,node_zorder=1000):
"""
Redraw the visualization using matplotlib. Creates
figure and axes if None provided.
In order to add labels, check out
:mod:`netwulf.tools.add_node_label`
and
:mod:`netwulf.tools.add_edge_label`
Parameters
----------
network_properties : dict
The network properties which are returned from the
interactive visualization.
fig : matplotlib.Figure, default : None
The figure in which to draw
ax : matplotlib.Axes, default : None
The Axes in which to draw
figsize : float, default : None
the size of the figure in inches (sidelength of a square)
if None, will be taken as the minimum of the values in
``matplotlib.rcParams['figure.figsize']``.
draw_links : bool, default : True
Whether the links should be drawn
draw_nodes : bool, default : True
Whether the nodes should be drawn
Returns
-------
fig : matplotlib.Figure, default : None
Resulting figure
ax : matplotlib.Axes, default : None
Resulting axes
"""
# if no figure given, create a square one
if ax is None or fig is None:
if figsize is None:
size = min(mpl.rcParams['figure.figsize'])
else:
size = figsize
fig = pl.figure(figsize=(size,size))
ax = fig.add_axes([0, 0, 1, 1])
# Customize the axis
# remove top and right spines
ax.spines['right'].set_color('none')
ax.spines['left'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
# turn off ticks
ax.xaxis.set_ticks_position('none')
ax.yaxis.set_ticks_position('none')
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
# for conversion of inches to points
# (important for markersize and linewidths).
# Apparently matplotlib uses 72 dpi internally for conversions in all figures even for those
# which do not follow dpi = 72 which is freaking weird but hey why not.
dpi = 72
# set everything square and get the axis size in points
ax.axis('square')
ax.axis('off')
ax.margins(0)
ax.set_xlim(network_properties['xlim'])
ax.set_ylim(network_properties['ylim'])
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
axwidth, axheight = bbox.width*dpi, bbox.height*dpi
# filter out node positions for links
width = network_properties['xlim'][1] - network_properties['xlim'][0]
height = network_properties['ylim'][1] - network_properties['ylim'][0]
pos = { node['id']: np.array([node['x_canvas'], height - node['y_canvas']]) for node in network_properties['nodes'] }
if draw_links:
#zorder = max( _c.get_zorder() for _c in ax.get_children()) + 1
zorder = -1 # make sure that links are very much in the background
lines = []
linewidths = []
linecolors = []
for link in network_properties['links']:
u, v = link['source'], link['target']
lines.append([
[pos[u][0], pos[v][0]],
[pos[u][1], pos[v][1]]
])
linewidths.append(link['width']/width*axwidth)
if 'color' in link.keys():
linecolors.append(link['color'])
else:
linecolors.append(network_properties['linkColor'])
# collapse to line segments
lines = [list(zip(x, y)) for x, y in lines]
# plot Lines
alpha = network_properties['linkAlpha']
ax.add_collection(LineCollection(lines,
colors=linecolors,
alpha=alpha,
linewidths=linewidths,
zorder=zorder
))
if draw_nodes:
zorder = max( _c.get_zorder() for _c in ax.get_children()) + 1
# compute node positions and properties
XY = []
size = []
node_colors = []
for node in network_properties['nodes']:
XY.append([node['x_canvas'], height - node['y_canvas']])
# size has to be given in points*2
size.append( 2*node['radius'] )
node_colors.append(node['color'])
XY = np.array(XY)
size = np.array(size)
circles = EllipseCollection(size,size,np.zeros_like(size),
offsets=XY,
units='x',
transOffset=ax.transData,
facecolors=node_colors,
linewidths=network_properties['nodeStrokeWidth']/width*axwidth,
edgecolors=network_properties['nodeStrokeColor'],
zorder=zorder
)
ax.add_collection(circles)
return fig, ax