Skip to content

Commit

Permalink
Improved Graph element (#2145)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Nov 23, 2017
1 parent f5e8512 commit 4c1b01d
Show file tree
Hide file tree
Showing 17 changed files with 592 additions and 195 deletions.
10 changes: 6 additions & 4 deletions examples/reference/elements/bokeh/Graph.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"source": [
"#### Additional features\n",
"\n",
"Next we will extend this example by supplying explicit edges:"
"Next we will extend this example by supplying explicit edges, node information and edge weights. By constructing the ``Nodes`` explicitly we can declare an additional value dimensions, which are revealed when hovering and/or can be mapped to the color by specifying the ``color_index``. We can also associate additional information with each edge by supplying a value dimension to the ``Graph`` itself, which we can map to a color using the ``edge_color_index``."
]
},
{
Expand All @@ -100,8 +100,10 @@
"outputs": [],
"source": [
"# Node info\n",
"np.random.seed(7)\n",
"x, y = simple_graph.nodes.array([0, 1]).T\n",
"node_labels = ['Output']+['Input']*(N-1)\n",
"edge_weights = np.random.rand(8)\n",
"\n",
"# Compute edge paths\n",
"def bezier(start, end, control, steps=np.linspace(0, 1, 100)):\n",
Expand All @@ -114,10 +116,10 @@
"\n",
"# Declare Graph\n",
"nodes = hv.Nodes((x, y, node_indices, node_labels), vdims='Type')\n",
"graph = hv.Graph(((source, target), nodes, paths))\n",
"graph = hv.Graph(((source, target, edge_weights), nodes, paths), vdims='Weight')\n",
"\n",
"graph.redim.range(**padding).opts(plot=dict(color_index='Type'),\n",
" style=dict(cmap=['blue', 'yellow']))"
"graph.redim.range(**padding).opts(plot=dict(color_index='Type', edge_color_index='Weight'),\n",
" style=dict(cmap=['blue', 'red'], edge_cmap='viridis'))"
]
}
],
Expand Down
13 changes: 7 additions & 6 deletions examples/reference/elements/matplotlib/Graph.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@
"source": [
"#### Additional features\n",
"\n",
"Next we will extend this example by supplying explicit edges:"
"\n",
"Next we will extend this example by supplying explicit edges, node information and edge weights. By constructing the ``Nodes`` explicitly we can declare an additional value dimensions, which are revealed when hovering and/or can be mapped to the color by specifying the ``color_index``. We can also associate additional information with each edge by supplying a value dimension to the ``Graph`` itself, which we can map to a color using the ``edge_color_index``."
]
},
{
Expand All @@ -99,11 +100,11 @@
"metadata": {},
"outputs": [],
"source": [
"from matplotlib.colors import ListedColormap\n",
"\n",
"# Node info\n",
"np.random.seed(7)\n",
"x, y = simple_graph.nodes.array([0, 1]).T\n",
"node_labels = ['Output']+['Input']*(N-1)\n",
"edge_weights = np.random.rand(8)\n",
"\n",
"# Compute edge paths\n",
"def bezier(start, end, control, steps=np.linspace(0, 1, 100)):\n",
Expand All @@ -116,10 +117,10 @@
"\n",
"# Declare Graph\n",
"nodes = hv.Nodes((x, y, node_indices, node_labels), vdims='Type')\n",
"graph = hv.Graph(((source, target), nodes, paths))\n",
"graph = hv.Graph(((source, target, edge_weights), nodes, paths), vdims='Weight')\n",
"\n",
"graph.redim.range(**padding).opts(plot=dict(color_index='Type'),\n",
" style=dict(cmap=ListedColormap(['blue', 'yellow'])))"
"graph.redim.range(**padding).opts(plot=dict(color_index='Type', edge_color_index='Weight'),\n",
" style=dict(cmap=['blue', 'red'], edge_cmap='viridis'))"
]
}
],
Expand Down
13 changes: 7 additions & 6 deletions examples/user_guide/Network_Graphs.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
"source": [
"# Declare abstract edges\n",
"N = 8\n",
"node_indices = np.arange(N)\n",
"source = np.zeros(N)\n",
"node_indices = np.arange(N, dtype=np.int32)\n",
"source = np.zeros(N, dtype=np.int32)\n",
"target = node_indices\n",
"\n",
"padding = dict(x=(-1.2, 1.2), y=(-1.2, 1.2))\n",
Expand Down Expand Up @@ -148,7 +148,7 @@
"source": [
"#### Additional information\n",
"\n",
"We can also associate additional information with the nodes and edges of a graph. By constructing the ``Nodes`` explicitly we can declare an additional value dimensions, which are revealed when hovering and/or can be mapped to the color by specifying the ``color_index``. We can also associate additional information with each edge by supplying a value dimension to the ``Graph`` itself."
"We can also associate additional information with the nodes and edges of a graph. By constructing the ``Nodes`` explicitly we can declare an additional value dimensions, which are revealed when hovering and/or can be mapped to the color by specifying the ``color_index``. We can also associate additional information with each edge by supplying a value dimension to the ``Graph`` itself, which we can map to a color using the ``edge_color_index``."
]
},
{
Expand All @@ -157,12 +157,13 @@
"metadata": {},
"outputs": [],
"source": [
"%%opts Graph [color_index='Type'] (cmap='Set1')\n",
"%%opts Graph [color_index='Type' edge_color_index='Weight'] (cmap='Set1' edge_cmap='viridis')\n",
"node_labels = ['Output']+['Input']*(N-1)\n",
"edge_labels = list('ABCDEFGH')\n",
"np.random.seed(7)\n",
"edge_labels = np.random.rand(8)\n",
"\n",
"nodes = hv.Nodes((x, y, node_indices, node_labels), vdims='Type')\n",
"graph = hv.Graph(((source, target, edge_labels), nodes, paths), vdims='Label').redim.range(**padding)\n",
"graph = hv.Graph(((source, target, edge_labels), nodes, paths), vdims='Weight').redim.range(**padding)\n",
"graph + graph.opts(plot=dict(inspection_policy='edges'))"
]
},
Expand Down
9 changes: 9 additions & 0 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1569,3 +1569,12 @@ def dt_to_int(value, time_unit='us'):
except:
# Handle python2
return (time.mktime(value.timetuple()) + value.microsecond / 1e6) * tscale


def search_indices(values, source):
"""
Given a set of values returns the indices of each of those values
in the source array.
"""
orig_indices = source.argsort()
return orig_indices[np.searchsorted(source[orig_indices], values)]
91 changes: 64 additions & 27 deletions holoviews/element/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ..core.operation import Operation
from .chart import Points
from .path import Path
from .util import split_path, pd
from .util import split_path, pd, circular_layout, connect_edges, connect_edges_pd

try:
from datashader.layout import LayoutAlgorithm as ds_layout
Expand All @@ -33,14 +33,6 @@ def __call__(self, specs=None, **dimensions):
return redimmed.clone(new_data)


def circular_layout(nodes):
N = len(nodes)
circ = np.pi/N*np.arange(N)*2
x = np.cos(circ)
y = np.sin(circ)
return (x, y, nodes)


class layout_nodes(Operation):
"""
Accepts a Graph and lays out the corresponding nodes with the
Expand Down Expand Up @@ -75,10 +67,15 @@ def _process(self, element, key=None):
nodes = nodes[['x', 'y', 'index']]
else:
nodes = circular_layout(nodes)
nodes = Nodes(nodes)
if element._nodes:
for d in element.nodes.vdims:
vals = element.nodes.dimension_values(d)
nodes = nodes.add_dimension(d, len(nodes.vdims), vals, vdim=True)
if self.p.only_nodes:
return Nodes(nodes)
return nodes
return element.clone((element.data, nodes))



class Graph(Dataset, Element2D):
Expand Down Expand Up @@ -123,15 +120,61 @@ def __init__(self, data, kdims=None, vdims=None, **params):
self._nodes = nodes
self._edgepaths = edgepaths
super(Graph, self).__init__(edges, kdims=kdims, vdims=vdims, **params)
if self._nodes is None and node_info:
nodes = self.nodes.clone(datatype=['pandas', 'dictionary'])
for d in node_info.dimensions():
if node_info is not None:
self._add_node_info(node_info)
self._validate()
self.redim = redim_graph(self, mode='dataset')


def _add_node_info(self, node_info):
nodes = self.nodes.clone(datatype=['pandas', 'dictionary'])
if isinstance(node_info, Nodes):
nodes = nodes.redim(**dict(zip(nodes.dimensions('key', label=True),
node_info.kdims)))

if not node_info.kdims and len(node_info) != len(nodes):
raise ValueError("The supplied node data does not match "
"the number of nodes defined by the edges. "
"Ensure that the number of nodes match"
"or supply an index as the sole key "
"dimension to allow the Graph to merge "
"the data.")

if pd is None:
if node_info.kdims and len(node_info) != len(nodes):
raise ValueError("Graph cannot merge node data on index "
"dimension without pandas. Either ensure "
"the node data matches the order of nodes "
"as they appear in the edge data or install "
"pandas.")
dimensions = nodes.dimensions()
for d in node_info.vdims:
if d in dimensions:
continue
nodes = nodes.add_dimension(d, len(nodes.vdims),
node_info.dimension_values(d),
vdim=True)
self._nodes = nodes
self._validate()
self.redim = redim_graph(self, mode='dataset')
else:
left_on = nodes.kdims[-1].name
node_info_df = node_info.dframe()
node_df = nodes.dframe()
if node_info.kdims:
idx = node_info.kdims[-1]
else:
idx = Dimension('index')
node_info_df = node_info_df.reset_index()
if 'index' in node_info_df.columns and not idx.name == 'index':
node_df = node_df.rename(columns={'index': '__index'})
left_on = '__index'
cols = [c for c in node_info_df.columns if c not in
node_df.columns or c == idx.name]
node_info_df = node_info_df[cols]
node_df = pd.merge(node_df, node_info_df, left_on=left_on,
right_on=idx.name, how='left')
nodes = nodes.clone(node_df, kdims=nodes.kdims[:2]+[idx],
vdims=node_info.vdims)

self._nodes = nodes


def _validate(self):
Expand Down Expand Up @@ -300,15 +343,10 @@ def edgepaths(self):
"""
if self._edgepaths:
return self._edgepaths
paths = []
for start, end in self.array(self.kdims):
start_ds = self.nodes[:, :, start]
end_ds = self.nodes[:, :, end]
if not len(start_ds) or not len(end_ds):
raise ValueError('Could not find node positions for all edges')
sx, sy = start_ds.array(start_ds.kdims[:2]).T
ex, ey = end_ds.array(end_ds.kdims[:2]).T
paths.append([(sx[0], sy[0]), (ex[0], ey[0])])
if pd is None:
paths = connect_edges(self)
else:
paths = connect_edges_pd(self)
return EdgePaths(paths, kdims=self.nodes.kdims[:2])


Expand Down Expand Up @@ -354,4 +392,3 @@ class EdgePaths(Path):
"""

group = param.String(default='EdgePaths', constant=True)

60 changes: 60 additions & 0 deletions holoviews/element/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,63 @@ def _process(self, obj, key=None):
obj = Dataset(obj, datatype=[dtype])
xcoords, ycoords = self._get_coords(obj)
return self._aggregate_dataset(obj, xcoords, ycoords)


def circular_layout(nodes):
"""
Lay out nodes on a circle and add node index.
"""
N = len(nodes)
circ = np.pi/N*np.arange(N)*2
x = np.cos(circ)
y = np.sin(circ)
return (x, y, nodes)


def connect_edges_pd(graph):
"""
Given a Graph element containing abstract edges compute edge
segments directly connecting the source and target nodes. This
operation depends on pandas and is a lot faster than the pure
NumPy equivalent.
"""
edges = graph.dframe()
edges.index.name = 'graph_edge_index'
edges = edges.reset_index()
nodes = graph.nodes.dframe()
src, tgt = graph.kdims
x, y, idx = graph.nodes.kdims[:3]

df = pd.merge(edges, nodes, left_on=[src.name], right_on=[idx.name])
df = df.rename(columns={x.name: 'src_x', y.name: 'src_y'})

df = pd.merge(df, nodes, left_on=[tgt.name], right_on=[idx.name])
df = df.rename(columns={x.name: 'dst_x', y.name: 'dst_y'})
df = df.sort_values('graph_edge_index').drop(['graph_edge_index'], axis=1)

edge_segments = []
N = len(nodes)
for i, edge in df.iterrows():
start = edge['src_x'], edge['src_y']
end = edge['dst_x'], edge['dst_y']
edge_segments.append(np.array([start, end]))
return edge_segments


def connect_edges(graph):
"""
Given a Graph element containing abstract edges compute edge
segments directly connecting the source and target nodes. This
operation just uses internal HoloViews operations and will be a
lot slower than the pandas equivalent.
"""
paths = []
for start, end in graph.array(graph.kdims):
start_ds = graph.nodes[:, :, start]
end_ds = graph.nodes[:, :, end]
if not len(start_ds) or not len(end_ds):
raise ValueError('Could not find node positions for all edges')
start = start_ds.array(start_ds.kdims[:2])
end = end_ds.array(end_ds.kdims[:2])
paths.append(np.array([start[0], end[0]]))
return paths
9 changes: 4 additions & 5 deletions holoviews/plotting/bokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,17 @@ def colormap_generator(palette):
options.Arrow = Options('style', arrow_size=10)

# Graphs
options.Graph = Options('style', node_size=20, node_fill_color=Cycle(),
options.Graph = Options('style', node_size=15, node_fill_color=Cycle(),
node_line_color='black',
node_selection_fill_color='limegreen',
node_nonselection_fill_color=Cycle(),
node_hover_line_color='black',
node_hover_fill_color='indianred',
node_hover_fill_color='limegreen',
node_nonselection_alpha=0.2,
edge_nonselection_alpha=0.2,
node_nonselection_line_color='black',
edge_line_color='black', edge_line_width=2,
edge_nonselection_line_color='black',
edge_hover_line_color='indianred',
edge_selection_line_color='limegreen')
edge_hover_line_color='limegreen')
options.Nodes = Options('style', line_color='black', color=Cycle(),
size=20, nonselection_fill_color=Cycle(),
selection_fill_color='limegreen',
Expand Down
Loading

0 comments on commit 4c1b01d

Please sign in to comment.