From 20380b3430d904d2a52f227f4f689d9ddc7796a6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 28 Aug 2017 17:14:35 +0100 Subject: [PATCH 01/60] Added initial Graph support --- holoviews/element/__init__.py | 1 + holoviews/element/graphs.py | 92 ++++++++++++++++++++++ holoviews/plotting/bokeh/__init__.py | 11 ++- holoviews/plotting/bokeh/graphs.py | 112 +++++++++++++++++++++++++++ 4 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 holoviews/element/graphs.py create mode 100644 holoviews/plotting/bokeh/graphs.py diff --git a/holoviews/element/__init__.py b/holoviews/element/__init__.py index 5ec3f16b45..3e3067af10 100644 --- a/holoviews/element/__init__.py +++ b/holoviews/element/__init__.py @@ -3,6 +3,7 @@ from .annotation import * # noqa (API import) from .chart import * # noqa (API import) from .chart3d import * # noqa (API import) +from .graphs import * # noqa (API import) from .path import * # noqa (API import) from .raster import * # noqa (API import) from .tabular import * # noqa (API import) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py new file mode 100644 index 0000000000..4aa6e1c6d3 --- /dev/null +++ b/holoviews/element/graphs.py @@ -0,0 +1,92 @@ +import param + +from ..core import Dimension, Dataset, Element2D +from .chart import Points +from .path import Path + + +class Graph(Dataset, Element2D): + """ + Graph is high-level Element representing both nodes and edges. + A Graph may be defined in an abstract form representing just + the abstract edges between nodes and optionally may be made + concrete by supplying a Nodes Element defining the concrete + positions of each node. If the node positions are supplied + the NodePaths (defining the concrete edges) can be inferred + automatically or supplied explicitly. + + The constructor accepts regular columnar data defining the edges + or a tuple of the abstract edges and nodes, or a tuple of the + abstract edges, nodes, and nodepaths. + """ + + group = param.String(default='Graph') + + kdims = param.List(default=[Dimension('start'), Dimension('end')], + bounds=(2, 2)) + + def __init__(self, data, **params): + if isinstance(data, tuple): + data = data + (None,)* (3-len(data)) + edges, nodes, nodepaths = data + else: + edges, nodes, nodepaths = data, None, None + if nodes is not None and not isinstance(nodes, Nodes): + nodes = Nodes(nodes) + if nodepaths is not None and not isinstance(nodepaths, NodePaths): + nodepaths = NodePaths(nodepaths) + self.nodes = nodes + self._nodepaths = nodepaths + super(Graph, self).__init__(edges, **params) + + @property + def nodepaths(self): + """ + Returns the fixed NodePaths or computes direct connections + between supplied nodes. + """ + if self.nodes is None: + raise ValueError('Cannot return NodePaths without node positions') + elif self._nodepaths: + return self._nodepaths + paths = [] + for start, end in self.array(self.kdims): + start_ds = self.nodes.select(index=start) + end_ds = self.nodes.select(index=end) + sx, sy = start_ds.array(start_ds.kdims).T + ex, ey = end_ds.array(end_ds.kdims).T + paths.append([(sx, sy), (ex, ey)]) + return NodePaths(paths) + + @classmethod + def from_networkx(cls, G, layout_function, **kwargs): + """ + Generate a HoloViews Graph from a networkx.Graph object and + networkx layout function. Any keyword arguments will be passed + to the layout function. + """ + positions = layout_function(G, **kwargs) + nodes = Nodes([tuple(pos)+(idx,) for idx, pos in sorted(positions.items())]) + return cls((G.edges(), nodes)) + + +class Nodes(Points): + """ + Nodes is a simple Element representing Graph nodes as a set of + Points. Unlike regular Points, Nodes must define a third key + dimension corresponding to the node index. + """ + + kdims = param.List(default=[Dimension('x'), Dimension('y'), + Dimension('index')], bounds=(3, 3)) + + group = param.String(default='Nodes') + + +class NodePaths(Path): + """ + NodePaths is a simple Element representing the paths of edges + connecting nodes in a graph. + """ + + group = param.String(default='NodePaths') diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 9f5ec020b1..a9a6532a7e 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -9,7 +9,8 @@ RGB, Histogram, Spread, HeatMap, Contours, Bars, Box, Bounds, Ellipse, Polygons, BoxWhisker, Arrow, ErrorBars, Text, HLine, VLine, Spline, Spikes, - Table, ItemTable, Area, HSV, QuadMesh, VectorField) + Table, ItemTable, Area, HSV, QuadMesh, VectorField, + Graph, Nodes, NodePaths) from ...core.options import Options, Cycle, Palette try: @@ -23,6 +24,7 @@ from .chart import (PointPlot, CurvePlot, SpreadPlot, ErrorPlot, HistogramPlot, SideHistogramPlot, BarPlot, SpikesPlot, SideSpikesPlot, AreaPlot, VectorFieldPlot, BoxWhiskerPlot) +from .graphs import GraphPlot from .path import PathPlot, PolygonPlot, ContourPlot from .plot import GridPlot, LayoutPlot, AdjointLayoutPlot from .raster import RasterPlot, RGBPlot, HeatMapPlot, HSVPlot, QuadMeshPlot @@ -81,6 +83,11 @@ Spline: SplinePlot, Arrow: ArrowPlot, + # Graph Elements + Graph: GraphPlot, + Nodes: PointPlot, + NodePaths: PathPlot, + # Tabular Table: TablePlot, ItemTable: TablePlot} @@ -102,8 +109,6 @@ framedcls.show_frame = True - - AdjointLayoutPlot.registry[Histogram] = SideHistogramPlot AdjointLayoutPlot.registry[Spikes] = SideSpikesPlot diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py new file mode 100644 index 0000000000..a3b320be07 --- /dev/null +++ b/holoviews/plotting/bokeh/graphs.py @@ -0,0 +1,112 @@ +from bokeh.models import (StaticLayoutProvider, DataRange1d, GraphRenderer, NodesAndLinkedEdges, EdgesAndLinkedNodes, Circle, MultiLine) + +from ...core.options import abbreviated_exception +from .element import CompositeElementPlot, line_properties, fill_properties, property_prefixes +from .util import mpl_to_bokeh + + +class GraphPlot(CompositeElementPlot): + + # X-axis is categorical + _x_range_type = DataRange1d + + # Declare that y-range should auto-range if not bounded + _y_range_type = DataRange1d + + # Map each glyph to a style group + _style_groups = {'scatter': 'node', 'multi_line': 'edge'} + + # Define all the glyph handles to update + _update_handles = ([glyph+'_'+model for model in ['glyph', 'glyph_renderer', 'source'] + for glyph in ['scatter_1', 'multi_line_1']] + + ['color_mapper', 'colorbar']) + + style_opts = (['edge_'+p for p in line_properties] +\ + ['node_'+p for p in fill_properties+line_properties]+['node_size']) + + def get_extents(self, element, ranges): + """ + Extents are set to '' and None because x-axis is categorical and + y-axis auto-ranges. + """ + x0, x1 = element.nodes.range(0) + y0, y1 = element.nodes.range(1) + return (x0, y0, x1, y1) + + def _get_axis_labels(self, *args, **kwargs): + """ + Override axis labels to group all key dimensions together. + """ + element = self.current_frame + xlabel, ylabel = [kd.pprint_label for kd in element.nodes.kdims[:2]] + return xlabel, ylabel, None + + def get_data(self, element, ranges=None, empty=False): + point_data = {'index': element.nodes.dimension_values(2).astype(int)} + point_mapping = {'index': 'index'} + + xidx, yidx = (1, 0) if self.invert_axes else (0, 1) + xs, ys = (element.dimension_values(i) for i in range(2)) + path_data = dict(start=xs, end=ys) + path_mapping = dict(start='start', end='end') + + data = {'scatter_1': point_data, 'multi_line_1': path_data} + mapping = {'scatter_1': point_mapping, 'multi_line_1': path_mapping} + return data, mapping + + def _init_glyphs(self, plot, element, ranges, source): + # Get data and initialize data source + data, mapping = self.get_data(element, ranges, False) + self.handles['previous_id'] = element._plot_id + for key in dict(mapping, **data): + source = self._init_datasource(data.get(key, {})) + self.handles[key+'_source'] = source + properties = self._glyph_properties(plot, element, source, ranges) + properties = self._process_properties(key, properties) + properties = {p: v for p, v in properties.items() if p not in ('legend', 'source')} + for prefix in [''] + property_prefixes: + glyph_key = prefix+'_'+key if prefix else key + other_prefixes = [p for p in property_prefixes if p != prefix] + gprops = {p[len(prefix)+1:] if prefix and prefix in p else p: v for p, v in properties.items() + if not any(pre in p for pre in other_prefixes)} + with abbreviated_exception(): + renderer, glyph = self._init_glyph(plot, mapping.get(key, {}), gprops, key) + self.handles[glyph_key+'_glyph'] = glyph + + # Define static layout + layout_dict = {int(z): (x, y) for x, y, z in element.nodes.array([0, 1, 2])} + layout = StaticLayoutProvider(graph_layout=layout_dict) + + # Initialize GraphRenderer + graph = GraphRenderer(layout_provider=layout) + plot.renderers.append(graph) + for prefix in [''] + property_prefixes: + node_key = 'scatter_1_glyph' + edge_key = 'multi_line_1_glyph' + glyph_key = 'glyph' + if prefix: + glyph_key = '_'.join([prefix, glyph_key]) + node_key = '_'.join([prefix, node_key]) + edge_key = '_'.join([prefix, edge_key]) + if node_key not in self.handles: + continue + setattr(graph.node_renderer, glyph_key, self.handles[node_key]) + setattr(graph.edge_renderer, glyph_key, self.handles[edge_key]) + graph.node_renderer.data_source.data = self.handles['scatter_1_source'].data + graph.edge_renderer.data_source.data = self.handles['multi_line_1_source'].data + graph.selection_policy = NodesAndLinkedEdges() + graph.inspection_policy = NodesAndLinkedEdges() + self.handles['renderer'] = graph + self.handles['scatter_1_renderer'] = graph.node_renderer + self.handles['multi_line_1_renderer'] = graph.edge_renderer + + + def _init_glyph(self, plot, mapping, properties, key): + """ + Returns a Bokeh glyph object. + """ + properties = mpl_to_bokeh(properties) + plot_method = '_'.join(key.split('_')[:-1]) + glyph = MultiLine if plot_method == 'multi_line' else Circle + glyph = glyph(**properties) + return None, glyph From f3d0bae1d1693c8de73cbfc27c42cdc13552b06a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 28 Aug 2017 17:15:02 +0100 Subject: [PATCH 02/60] Small fix for style group handling --- holoviews/plotting/bokeh/element.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index ca3b4b6981..667cba8435 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -986,7 +986,8 @@ def _init_glyphs(self, plot, element, ranges, source): def _process_properties(self, key, properties): - style_group = self._style_groups[key.split('_')[0]] + key = '_'.join(key.split('_')[:-1]) + style_group = self._style_groups[key] group_props = {} for k, v in properties.items(): if k in self.style_opts: From 4ee5c1dfe9e32cd4c46e6861c02cdce1a65b9f3d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 30 Aug 2017 16:21:18 +0100 Subject: [PATCH 03/60] Make graph imports conditional --- holoviews/plotting/bokeh/graphs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index a3b320be07..cfb5046340 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -1,4 +1,10 @@ -from bokeh.models import (StaticLayoutProvider, DataRange1d, GraphRenderer, NodesAndLinkedEdges, EdgesAndLinkedNodes, Circle, MultiLine) +from bokeh.models import DataRange1d, Circle, MultiLine + +try: + from bokeh.models import (StaticLayoutProvider, GraphRenderer, NodesAndLinkedEdges, + EdgesAndLinkedNodes) +except: + pass from ...core.options import abbreviated_exception from .element import CompositeElementPlot, line_properties, fill_properties, property_prefixes From 95bd7d332e03b8df7a72bdec204e66de2c6562b4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 30 Aug 2017 16:22:02 +0100 Subject: [PATCH 04/60] Small bugfix for bokeh property handling --- holoviews/plotting/bokeh/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 667cba8435..a07b75f8cb 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -986,7 +986,7 @@ def _init_glyphs(self, plot, element, ranges, source): def _process_properties(self, key, properties): - key = '_'.join(key.split('_')[:-1]) + key = '_'.join(key.split('_')[:-1]) if '_' in key else key style_group = self._style_groups[key] group_props = {} for k, v in properties.items(): From 8196ed61714080a939a3d7e75766f004cac1bc00 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 1 Sep 2017 18:23:28 +0100 Subject: [PATCH 05/60] Fix bug when computing NodePaths --- holoviews/element/graphs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 4aa6e1c6d3..dde736cc8d 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -53,9 +53,9 @@ def nodepaths(self): for start, end in self.array(self.kdims): start_ds = self.nodes.select(index=start) end_ds = self.nodes.select(index=end) - sx, sy = start_ds.array(start_ds.kdims).T - ex, ey = end_ds.array(end_ds.kdims).T - paths.append([(sx, sy), (ex, ey)]) + 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])]) return NodePaths(paths) @classmethod From 4131560e97c536a2a41c2756346a485336f12c92 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 1 Sep 2017 18:24:35 +0100 Subject: [PATCH 06/60] Added control over Graph inspection/selection policy --- holoviews/plotting/bokeh/graphs.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index cfb5046340..7d937c8874 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -1,3 +1,5 @@ +import param + from bokeh.models import DataRange1d, Circle, MultiLine try: @@ -13,6 +15,14 @@ class GraphPlot(CompositeElementPlot): + selection_policy = param.ObjectSelector(default='nodes', objects=['edges', 'nodes', None], doc=""" + Determines policy for inspection of graph components, i.e. whether to highlight + nodes or edges when selecting connected edges and nodes respectively.""") + + inspection_policy = param.ObjectSelector(default='nodes', objects=['edges', 'nodes', None], doc=""" + Determines policy for inspection of graph components, i.e. whether to highlight + nodes or edges when hovering over connected edges and nodes respectively.""") + # X-axis is categorical _x_range_type = DataRange1d @@ -100,13 +110,23 @@ def _init_glyphs(self, plot, element, ranges, source): setattr(graph.edge_renderer, glyph_key, self.handles[edge_key]) graph.node_renderer.data_source.data = self.handles['scatter_1_source'].data graph.edge_renderer.data_source.data = self.handles['multi_line_1_source'].data - graph.selection_policy = NodesAndLinkedEdges() - graph.inspection_policy = NodesAndLinkedEdges() + if self.selection_policy == 'nodes': + graph.selection_policy = NodesAndLinkedEdges() + elif self.selection_policy == 'edges': + graph.selection_policy = EdgesAndLinkedNodes() + else: + graph.selection_policy = None + + if self.inspection_policy == 'nodes': + graph.inspection_policy = NodesAndLinkedEdges() + elif self.inspection_policy == 'edges': + graph.inspection_policy = EdgesAndLinkedNodes() + else: + graph.inspection_policy = None self.handles['renderer'] = graph self.handles['scatter_1_renderer'] = graph.node_renderer self.handles['multi_line_1_renderer'] = graph.edge_renderer - def _init_glyph(self, plot, mapping, properties, key): """ Returns a Bokeh glyph object. From 140152312b51102b5fae149bc05200cd251fe901 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 1 Sep 2017 18:25:13 +0100 Subject: [PATCH 07/60] Added bokeh version check to Graph support --- holoviews/plotting/bokeh/graphs.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 7d937c8874..c1020b6879 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -8,9 +8,9 @@ except: pass -from ...core.options import abbreviated_exception +from ...core.options import abbreviated_exception, SkipRendering from .element import CompositeElementPlot, line_properties, fill_properties, property_prefixes -from .util import mpl_to_bokeh +from .util import mpl_to_bokeh, bokeh_version class GraphPlot(CompositeElementPlot): @@ -40,6 +40,11 @@ class GraphPlot(CompositeElementPlot): style_opts = (['edge_'+p for p in line_properties] +\ ['node_'+p for p in fill_properties+line_properties]+['node_size']) + def initialize_plot(self, ranges=None, plot=None, plots=None): + if bokeh_version < '0.12.7': + raise SkipRendering('Graph rendering requires bokeh version >=0.12.7.') + super(GraphPlot, self).initialize_plot(ranges, plot, plots) + def get_extents(self, element, ranges): """ Extents are set to '' and None because x-axis is categorical and From 71cb107ea9c8714330e35d84bc498ff85ad188c2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 1 Sep 2017 18:26:53 +0100 Subject: [PATCH 08/60] Added matplotlib GraphPlot implementation --- holoviews/plotting/mpl/__init__.py | 6 +++ holoviews/plotting/mpl/graphs.py | 64 ++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 holoviews/plotting/mpl/graphs.py diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 1c18fff43e..a9cc4dee9c 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -12,6 +12,7 @@ from .annotation import * # noqa (API import) from .chart import * # noqa (API import) from .chart3d import * # noqa (API import) +from .graphs import * # noqa (API import) from .path import * # noqa (API import) from .plot import * # noqa (API import) from .raster import * # noqa (API import) @@ -146,6 +147,11 @@ def grid_selector(grid): RGB: RasterPlot, HSV: RasterPlot, + # Graph Elements + Graph: GraphPlot, + Nodes: PointPlot, + NodePaths: PathPlot, + # Annotation plots VLine: VLinePlot, HLine: HLinePlot, diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py new file mode 100644 index 0000000000..3008bd03de --- /dev/null +++ b/holoviews/plotting/mpl/graphs.py @@ -0,0 +1,64 @@ +from matplotlib.collections import LineCollection + +from .chart import ChartPlot + +class GraphPlot(ChartPlot): + """ + GraphPlot + """ + + style_opts = ['edge_alpha', 'edge_color', 'edge_linestyle', 'edge_linewidth', + 'node_alpha', 'node_color', 'node_edgecolors', 'node_facecolors', + 'node_linewidth', 'node_marker', 'node_size', 'visible'] + + def get_data(self, element, ranges, style): + xidx, yidx = (1, 0) if self.invert_axes else (0, 1) + pxs, pys = (element.nodes.dimension_values(i) for i in range(2)) + dims = element.nodes.dimensions() + + paths = element.nodepaths.data + if self.invert_axes: + paths = [p[:, ::-1] for p in paths] + return {'points': (pxs, pys), 'paths': paths}, style, {'dimensions': dims} + + def get_extents(self, element, ranges): + """ + Extents are set to '' and None because x-axis is categorical and + y-axis auto-ranges. + """ + x0, x1 = element.nodes.range(0) + y0, y1 = element.nodes.range(1) + return (x0, y0, x1, y1) + + def init_artists(self, ax, plot_args, plot_kwargs): + # Draw edges + edge_opts = {k[5:] if 'edge_' in k else k: v + for k, v in plot_kwargs.items() + if 'node_' not in k} + paths = plot_args['paths'] + edges = LineCollection(paths, **edge_opts) + ax.add_collection(edges) + + # Draw nodes + xs, ys = plot_args['points'] + node_opts = {k[5:] if 'node_' in k else k: v + for k, v in plot_kwargs.items() + if 'edge_' not in k} + if 'size' in node_opts: node_opts['s'] = node_opts.pop('size')**2 + nodes = ax.scatter(xs, ys, **node_opts) + + return {'nodes': nodes, 'edges': edges} + + def update_handles(self, key, axis, element, ranges, style): + artist = self.handles['nodes'] + data, style, axis_kwargs = self.get_data(element, ranges, style) + xs, ys = data['nodes'] + artist.set_xdata(xs) + artist.set_ydata(ys) + + edges = self.handles['edges'] + paths = data['edges'] + artist.set_paths(paths) + artist.set_visible(style.get('visible', True)) + + return axis_kwargs From 9ab946c9aed3c46e3cf16558b2b716c74cd5338e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 1 Sep 2017 18:27:21 +0100 Subject: [PATCH 09/60] Defined default styling for graphs --- holoviews/plotting/bokeh/__init__.py | 8 ++++++++ holoviews/plotting/mpl/__init__.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index a9a6532a7e..8a891ece28 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -170,6 +170,14 @@ def colormap_generator(palette): options.VLine = Options('style', color=Cycle(), line_width=3, alpha=1) options.Arrow = Options('style', arrow_size=10) +# Graphs +options.Graph = Options('style', node_size=20, node_fill_color=Cycle(), + edge_line_width=2, node_hover_fill_color='indianred', + edge_hover_line_color='indianred', node_selection_fill_color='limegreen', + edge_selection_line_color='limegreen') +options.Nodes = Options('style', line_color='black', fill_color=Cycle(), size=20) +options.NodePaths = Options('style', color='black') + # Define composite defaults options.GridMatrix = Options('plot', shared_xaxis=True, shared_yaxis=True, xaxis=None, yaxis=None) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index a9cc4dee9c..80d0c39830 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -254,3 +254,10 @@ def grid_selector(grid): # Interface options.TimeSeries = Options('style', color=Cycle()) + +# Graphs +options.Graph = Options('style', node_edgecolors='black', node_facecolors=Cycle(), + edge_color='black', node_size=20) +options.Nodes = Options('style', edgecolors='black', facecolors=Cycle(), + marker='o', s=20**2) +options.Path = Options('style', color='black') From 9b5398944f84fe93376b62102b1f1d28b51789ab Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 3 Sep 2017 21:29:56 +0100 Subject: [PATCH 10/60] Added graph hover info support --- holoviews/plotting/bokeh/graphs.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index c1020b6879..5620aaee2b 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -45,6 +45,10 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): raise SkipRendering('Graph rendering requires bokeh version >=0.12.7.') super(GraphPlot, self).initialize_plot(ranges, plot, plots) + def _hover_opts(self, element): + dims = element.nodes.dimensions()[3:] + return dims, {} + def get_extents(self, element, ranges): """ Extents are set to '' and None because x-axis is categorical and @@ -64,15 +68,19 @@ def _get_axis_labels(self, *args, **kwargs): def get_data(self, element, ranges=None, empty=False): point_data = {'index': element.nodes.dimension_values(2).astype(int)} - point_mapping = {'index': 'index'} + for d in element.nodes.dimensions()[2:]: + point_data[d.name] = element.nodes.dimension_values(d) xidx, yidx = (1, 0) if self.invert_axes else (0, 1) xs, ys = (element.dimension_values(i) for i in range(2)) path_data = dict(start=xs, end=ys) - path_mapping = dict(start='start', end='end') + if element._nodepaths: + edges = element.nodepaths + path_data['xs'] = [path[:, xidx] for path in edges.data] + path_data['ys'] = [path[:, yidx] for path in edges.data] data = {'scatter_1': point_data, 'multi_line_1': path_data} - mapping = {'scatter_1': point_mapping, 'multi_line_1': path_mapping} + mapping = {'scatter_1': {}, 'multi_line_1': {}} return data, mapping def _init_glyphs(self, plot, element, ranges, source): From 5fcfa8f0ada81099d51ed6199ea8d7ee82bb1cf5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 3 Sep 2017 21:30:28 +0100 Subject: [PATCH 11/60] Fix handling for empty HoverTool --- holoviews/plotting/bokeh/element.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index a07b75f8cb..a9cc66df6e 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -233,6 +233,7 @@ def _init_tools(self, element, callbacks=[]): tooltips, hover_opts = self._hover_opts(element) tooltips = [(ttp.pprint_label, '@{%s}' % util.dimension_sanitizer(ttp.name)) if isinstance(ttp, Dimension) else ttp for ttp in tooltips] + if not tooltips: tooltips = None callbacks = callbacks+self.callbacks cb_tools, tool_names = [], [] @@ -1353,10 +1354,11 @@ def _init_tools(self, element, callbacks=[]): else: tool_type = type(tool) if isinstance(tool, HoverTool): - if tuple(tool.tooltips) in hover_tools: + tooltips = tuple(tool.tooltips) if tool.tooltips else () + if tooltips in hover_tools: continue else: - hover_tools[tuple(tool.tooltips)] = tool + hover_tools[tooltips] = tool elif tool_type in tool_types: continue else: @@ -1376,9 +1378,12 @@ def _merge_tools(self, subplot): hover = subplot.handles['hover'] # Datetime formatter may have been applied, remove _dt_strings # to match on the hover tooltips, then merge tool renderers - tooltips = [(name, spec.replace('_dt_strings', '')) - for name, spec in hover.tooltips] - tool = self.handles['hover_tools'].get(tuple(tooltips)) + if hover.tooltips: + tooltips = tuple((name, spec.replace('_dt_strings', '')) + for name, spec in hover.tooltips) + else: + tooltips = () + tool = self.handles['hover_tools'].get(tooltips) if tool: renderers = tool.renderers+hover.renderers tool.renderers = list(util.unique_iterator(renderers)) From d10bc0874bf9d58c084941b0ef6341c7948747a5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 3 Sep 2017 21:31:38 +0100 Subject: [PATCH 12/60] Fixed Graph nodepaths method --- holoviews/element/graphs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index dde736cc8d..9c24fe303e 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -51,8 +51,8 @@ def nodepaths(self): return self._nodepaths paths = [] for start, end in self.array(self.kdims): - start_ds = self.nodes.select(index=start) - end_ds = self.nodes.select(index=end) + start_ds = self.nodes[:, :, start] + end_ds = self.nodes[:, :, end] 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])]) From 4ab5671c1d1421cb4c050049861530c7ba463436 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 3 Sep 2017 21:32:07 +0100 Subject: [PATCH 13/60] Handle Graph cloning --- holoviews/element/graphs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 9c24fe303e..eb16d1953b 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -39,6 +39,17 @@ def __init__(self, data, **params): self._nodepaths = nodepaths super(Graph, self).__init__(edges, **params) + def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): + if data is None: + data = (self.data, self.nodes) + if self._nodepaths: + data = data + (self.nodepaths,) + elif not isinstance(data, tuple): + data = (data, self.nodes) + if self._nodepaths: + data = data + (self.nodepaths,) + return super(Graph, self).clone(data, shared_data, new_type, *args, **overrides) + @property def nodepaths(self): """ From 72d7bf2498fad8d3c80085c62ed30a45ae1a2a2b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 7 Sep 2017 18:43:10 +0100 Subject: [PATCH 14/60] Implemented redim for Graphs --- holoviews/element/graphs.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index eb16d1953b..48b6b97896 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -1,9 +1,25 @@ import param from ..core import Dimension, Dataset, Element2D +from ..core.dimension import redim from .chart import Points from .path import Path +class graph_redim(redim): + """ + Extension for the redim utility that allows re-dimensioning + Graph objects including their nodes and nodepaths. + """ + + def __call__(self, specs=None, **dimensions): + redimmed = super(graph_redim, self).__call__(specs, **dimensions) + new_data = (redimmed.data,) + if self.parent.nodes: + new_data = new_data + (self.parent.nodes.redim(specs, **dimensions),) + if self.parent._nodepaths: + new_data = new_data + (self.parent.nodepaths.redim(specs, **dimensions),) + return redimmed.clone(new_data) + class Graph(Dataset, Element2D): """ @@ -38,6 +54,7 @@ def __init__(self, data, **params): self.nodes = nodes self._nodepaths = nodepaths super(Graph, self).__init__(edges, **params) + self.redim = graph_redim(self) def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): if data is None: From 8f8641936315b08d48c6715586ccbd7191a62f96 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 7 Sep 2017 18:43:39 +0100 Subject: [PATCH 15/60] Allow passing node info to Graph.from_networkx --- holoviews/element/graphs.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 48b6b97896..49338e04b6 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -87,14 +87,19 @@ def nodepaths(self): return NodePaths(paths) @classmethod - def from_networkx(cls, G, layout_function, **kwargs): + def from_networkx(cls, G, layout_function, nodes=None, **kwargs): """ Generate a HoloViews Graph from a networkx.Graph object and networkx layout function. Any keyword arguments will be passed to the layout function. """ positions = layout_function(G, **kwargs) - nodes = Nodes([tuple(pos)+(idx,) for idx, pos in sorted(positions.items())]) + if nodes: + xs, ys = zip(*[v for k, v in sorted(positions.items())]) + nodes = nodes.add_dimension('x', 0, xs) + nodes = nodes.add_dimension('y', 1, ys).clone(new_type=Nodes) + else: + nodes = Nodes([tuple(pos)+(idx,) for idx, pos in sorted(positions.items())]) return cls((G.edges(), nodes)) From b470ef5e5f432768a6a5327306e4079867b92b43 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 7 Sep 2017 18:44:26 +0100 Subject: [PATCH 16/60] Allow coloring Graph nodes --- holoviews/plotting/bokeh/graphs.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 5620aaee2b..f652312c8d 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -9,11 +9,17 @@ pass from ...core.options import abbreviated_exception, SkipRendering +from ...core.util import basestring +from .chart import ColorbarPlot from .element import CompositeElementPlot, line_properties, fill_properties, property_prefixes from .util import mpl_to_bokeh, bokeh_version -class GraphPlot(CompositeElementPlot): +class GraphPlot(CompositeElementPlot, ColorbarPlot): + + color_index = param.ClassSelector(default=None, class_=(basestring, int), + allow_None=True, doc=""" + Index of the dimension from which the color will the drawn""") selection_policy = param.ObjectSelector(default='nodes', objects=['edges', 'nodes', None], doc=""" Determines policy for inspection of graph components, i.e. whether to highlight @@ -38,7 +44,7 @@ class GraphPlot(CompositeElementPlot): ['color_mapper', 'colorbar']) style_opts = (['edge_'+p for p in line_properties] +\ - ['node_'+p for p in fill_properties+line_properties]+['node_size']) + ['node_'+p for p in fill_properties+line_properties]+['node_size', 'cmap']) def initialize_plot(self, ranges=None, plot=None, plots=None): if bokeh_version < '0.12.7': @@ -67,10 +73,15 @@ def _get_axis_labels(self, *args, **kwargs): return xlabel, ylabel, None def get_data(self, element, ranges=None, empty=False): + style = self.style[self.cyclic_index] point_data = {'index': element.nodes.dimension_values(2).astype(int)} for d in element.nodes.dimensions()[2:]: point_data[d.name] = element.nodes.dimension_values(d) + cdata, cmapping = self._get_color_data(element.nodes, ranges, style, 'fill_color') + point_data.update(cdata) + point_mapping = cmapping + xidx, yidx = (1, 0) if self.invert_axes else (0, 1) xs, ys = (element.dimension_values(i) for i in range(2)) path_data = dict(start=xs, end=ys) @@ -80,7 +91,7 @@ def get_data(self, element, ranges=None, empty=False): path_data['ys'] = [path[:, yidx] for path in edges.data] data = {'scatter_1': point_data, 'multi_line_1': path_data} - mapping = {'scatter_1': {}, 'multi_line_1': {}} + mapping = {'scatter_1': point_mapping, 'multi_line_1': {}} return data, mapping def _init_glyphs(self, plot, element, ranges, source): @@ -97,9 +108,10 @@ def _init_glyphs(self, plot, element, ranges, source): glyph_key = prefix+'_'+key if prefix else key other_prefixes = [p for p in property_prefixes if p != prefix] gprops = {p[len(prefix)+1:] if prefix and prefix in p else p: v for p, v in properties.items() - if not any(pre in p for pre in other_prefixes)} + if prefix in p and not any(pre in p for pre in other_prefixes)} + map_key = None if prefix else key with abbreviated_exception(): - renderer, glyph = self._init_glyph(plot, mapping.get(key, {}), gprops, key) + renderer, glyph = self._init_glyph(plot, mapping.get(map_key, {}), gprops, key) self.handles[glyph_key+'_glyph'] = glyph # Define static layout @@ -136,6 +148,7 @@ def _init_glyphs(self, plot, element, ranges, source): graph.inspection_policy = EdgesAndLinkedNodes() else: graph.inspection_policy = None + self.handles['renderer'] = graph self.handles['scatter_1_renderer'] = graph.node_renderer self.handles['multi_line_1_renderer'] = graph.edge_renderer @@ -145,7 +158,8 @@ def _init_glyph(self, plot, mapping, properties, key): Returns a Bokeh glyph object. """ properties = mpl_to_bokeh(properties) + mapping.pop('legend', None) plot_method = '_'.join(key.split('_')[:-1]) glyph = MultiLine if plot_method == 'multi_line' else Circle - glyph = glyph(**properties) + glyph = glyph(**dict(properties, **mapping)) return None, glyph From 731f508a9f81034ceee3f8e789ddd1fc5498859d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 8 Sep 2017 14:41:01 +0100 Subject: [PATCH 17/60] Added user_guide for network graphs --- examples/user_guide/Network_Graphs.ipynb | 234 +++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 examples/user_guide/Network_Graphs.ipynb diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb new file mode 100644 index 0000000000..b34adc7812 --- /dev/null +++ b/examples/user_guide/Network_Graphs.ipynb @@ -0,0 +1,234 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import param\n", + "import numpy as np\n", + "import pandas as pd\n", + "import holoviews as hv\n", + "import networkx as nx\n", + "from bokeh.models import HoverTool\n", + "\n", + "hv.notebook_extension('bokeh', 'matplotlib')\n", + "\n", + "%opts Graph [width=400 height=400]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Visualizing and working with network graphs is a common problem in many different disciplines. HoloViews provides the ability to represent and visualize graphs very simply and easily with facilities for interactively exploring the nodes and edges of the graph, especially using the bokeh plotting interface.\n", + "\n", + "The ``Graph`` ``Element`` differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes, on its own this abstract graph cannot be visualized. In order to visualize it we need to give each node in the ``Graph`` a concrete ``x`` and ``y`` position in form of the ``Nodes``. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``NodePaths``, providing explicit coordinates for each edge to follow.\n", + "\n", + "To summarize a ``Graph`` consists of three different components:\n", + "\n", + "* The ``Graph`` itself holds the abstract edges stored as a table of node indices.\n", + "* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``.\n", + "* The ``NodePaths`` can optionally be supplied to declare explicit node paths.\n", + "\n", + "#### A simple Graph\n", + "\n", + "Let's start by declaring a very simple graph laid out in a circle:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "N = 8\n", + "node_indices = np.arange(N)\n", + "\n", + "# Declare abstract edges\n", + "start = np.zeros(N)\n", + "end = node_indices\n", + "\n", + "### start of layout code\n", + "circ = np.pi/N*node_indices*2\n", + "x = np.cos(circ)\n", + "y = np.sin(circ)\n", + "\n", + "simple_graph = hv.Graph(((start, end), (x, y, node_indices))).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", + "simple_graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Accessing the nodes and edges\n", + "\n", + "We can easily access the ``Nodes`` and ``NodePaths`` on the ``Graph`` element using the corresponding properties:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "simple_graph.nodes + simple_graph.nodepaths" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Supplying explicit paths\n", + "\n", + "Next we will extend this example by supplying explicit edges:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def bezier(start, end, control, steps=np.linspace(0, 1, 100)):\n", + " return (1-steps)*(1-steps)*start + 2*(1-steps)*steps*control+steps*steps*end\n", + "\n", + "paths = []\n", + "for node_index in node_indices:\n", + " ex, ey = x[node_index], y[node_index]\n", + " paths.append(np.column_stack([bezier(x[0], ex, 0), bezier(y[0], ey, 0)]))\n", + " \n", + "bezier_graph = hv.Graph(((start, end), (x, y, node_indices), paths)).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", + "bezier_graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interactive features" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Hover and selection policies\n", + "\n", + "Thanks to Bokeh we can reveal more about the graph by hovering over the nodes and edges. The ``Graph`` element provides an ``inspection_policy`` and a ``selection_policy``, which define whether hovering and selection highlight edges associated with the selected node or nodes associated with the selected edge, these policies can be toggled by setting the policy to ``'nodes'`` and ``'edges'``." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph [tools=['hover']]\n", + "bezier_graph.opts(plot=dict(inspection_policy='edges'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In addition to changing the policy we can also change the colors used when hovering and selecting nodes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph [tools=['hover', 'box_select']] (edge_hover_line_color='green' node_hover_fill_color='red')\n", + "bezier_graph.opts(plot=dict(inspection_policy='nodes'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Additional information\n", + "\n", + "We can also associate additional information with the nodes of a graph. By constructing the ``Nodes`` explicitly we can declare an additional value dimension, which we can reveal when hovering and color the nodes by:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph [tools=['hover'] color_index='Type'] (cmap='Set1')\n", + "nodes = hv.Nodes((x, y, node_indices, ['Input']+['Output']*(N-1)), vdims=['Type'])\n", + "hv.Graph(((start, end), nodes, paths)).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with NetworkX" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "NetworkX is a very useful library when working with network graphs and the Graph Element provides ways of importing a NetworkX Graph directly. Here we will load the Karate Club graph and use the ``circular_layout`` function provided by NetworkX to lay it out:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph [tools=['hover']]\n", + "import networkx as nx\n", + "G = nx.karate_club_graph()\n", + "hv.Graph.from_networkx(G, nx.layout.circular_layout).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Real world graphs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a final example let's look at a slightly larger graph. We will load a dataset of a Facebook network consisting a number of friendship groups identified by their ``'circle'``. We will load the edge and node data using pandas and then color each node by their friendship group using many of the things we learned above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph [width=800 height=800 xaxis=None yaxis=None tools=['hover'] color_index='circle']\n", + "%%opts Graph (node_size=10 edge_line_width=1)\n", + "colors = ['#000000']+hv.Cycle('Category20').values\n", + "edges_df = pd.read_csv('../examples/assets/fb_edges.csv')\n", + "fb_nodes = hv.Nodes(pd.read_csv('../examples/assets/fb_nodes.csv')).sort()\n", + "fb_graph = hv.Graph((edges_df, fb_nodes), label='Facebook Circles')\n", + "fb_graph.redim.range(x=(-0.05, 1.05), y=(-0.05, 1.05)).opts(style=dict(cmap=colors))" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 394e810555745a0e8b1ea9a80d3dee52d1d5056a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 11 Sep 2017 16:02:21 +0100 Subject: [PATCH 18/60] Temp commit --- holoviews/element/graphs.py | 1 + holoviews/plotting/bokeh/graphs.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 49338e04b6..f114eb37d5 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -67,6 +67,7 @@ def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): data = data + (self.nodepaths,) return super(Graph, self).clone(data, shared_data, new_type, *args, **overrides) + @property def nodepaths(self): """ diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index f652312c8d..8cc53e66e8 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -1,6 +1,6 @@ import param -from bokeh.models import DataRange1d, Circle, MultiLine +from bokeh.models import Range1d, Circle, MultiLine try: from bokeh.models import (StaticLayoutProvider, GraphRenderer, NodesAndLinkedEdges, @@ -30,10 +30,10 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot): nodes or edges when hovering over connected edges and nodes respectively.""") # X-axis is categorical - _x_range_type = DataRange1d + _x_range_type = Range1d # Declare that y-range should auto-range if not bounded - _y_range_type = DataRange1d + _y_range_type = Range1d # Map each glyph to a style group _style_groups = {'scatter': 'node', 'multi_line': 'edge'} From c8f18ede1038919f37ed85f8112f03e757afa838 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Sep 2017 16:27:09 +0100 Subject: [PATCH 19/60] Simplified bokeh GraphPlot implementation --- holoviews/plotting/bokeh/__init__.py | 3 +- holoviews/plotting/bokeh/graphs.py | 69 ++++++++-------------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 8a891ece28..7686c95bb2 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -174,7 +174,8 @@ def colormap_generator(palette): options.Graph = Options('style', node_size=20, node_fill_color=Cycle(), edge_line_width=2, node_hover_fill_color='indianred', edge_hover_line_color='indianred', node_selection_fill_color='limegreen', - edge_selection_line_color='limegreen') + edge_selection_line_color='limegreen', edge_line_color='black', + node_line_color='black') options.Nodes = Options('style', line_color='black', fill_color=Cycle(), size=20) options.NodePaths = Options('style', color='black') diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 8cc53e66e8..c49c913ce3 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -78,7 +78,7 @@ def get_data(self, element, ranges=None, empty=False): for d in element.nodes.dimensions()[2:]: point_data[d.name] = element.nodes.dimension_values(d) - cdata, cmapping = self._get_color_data(element.nodes, ranges, style, 'fill_color') + cdata, cmapping = self._get_color_data(element.nodes, ranges, style, 'node_fill_color') point_data.update(cdata) point_mapping = cmapping @@ -98,68 +98,39 @@ def _init_glyphs(self, plot, element, ranges, source): # Get data and initialize data source data, mapping = self.get_data(element, ranges, False) self.handles['previous_id'] = element._plot_id + properties = {} + mappings = {} for key in dict(mapping, **data): source = self._init_datasource(data.get(key, {})) self.handles[key+'_source'] = source - properties = self._glyph_properties(plot, element, source, ranges) - properties = self._process_properties(key, properties) - properties = {p: v for p, v in properties.items() if p not in ('legend', 'source')} - for prefix in [''] + property_prefixes: - glyph_key = prefix+'_'+key if prefix else key - other_prefixes = [p for p in property_prefixes if p != prefix] - gprops = {p[len(prefix)+1:] if prefix and prefix in p else p: v for p, v in properties.items() - if prefix in p and not any(pre in p for pre in other_prefixes)} - map_key = None if prefix else key - with abbreviated_exception(): - renderer, glyph = self._init_glyph(plot, mapping.get(map_key, {}), gprops, key) - self.handles[glyph_key+'_glyph'] = glyph + glyph_props = self._glyph_properties(plot, element, source, ranges) + properties.update(glyph_props) + mappings.update(mapping.get(key, {})) + properties = {p: v for p, v in properties.items() if p not in ('legend', 'source')} + properties.update(mappings) # Define static layout layout_dict = {int(z): (x, y) for x, y, z in element.nodes.array([0, 1, 2])} layout = StaticLayoutProvider(graph_layout=layout_dict) + node_source = self.handles['scatter_1_source'] + edge_source = self.handles['multi_line_1_source'] + renderer = plot.graph(node_source, edge_source, layout, **properties) # Initialize GraphRenderer - graph = GraphRenderer(layout_provider=layout) - plot.renderers.append(graph) - for prefix in [''] + property_prefixes: - node_key = 'scatter_1_glyph' - edge_key = 'multi_line_1_glyph' - glyph_key = 'glyph' - if prefix: - glyph_key = '_'.join([prefix, glyph_key]) - node_key = '_'.join([prefix, node_key]) - edge_key = '_'.join([prefix, edge_key]) - if node_key not in self.handles: - continue - setattr(graph.node_renderer, glyph_key, self.handles[node_key]) - setattr(graph.edge_renderer, glyph_key, self.handles[edge_key]) - graph.node_renderer.data_source.data = self.handles['scatter_1_source'].data - graph.edge_renderer.data_source.data = self.handles['multi_line_1_source'].data if self.selection_policy == 'nodes': - graph.selection_policy = NodesAndLinkedEdges() + renderer.selection_policy = NodesAndLinkedEdges() elif self.selection_policy == 'edges': - graph.selection_policy = EdgesAndLinkedNodes() + renderer.selection_policy = EdgesAndLinkedNodes() else: - graph.selection_policy = None + renderer.selection_policy = None if self.inspection_policy == 'nodes': - graph.inspection_policy = NodesAndLinkedEdges() + renderer.inspection_policy = NodesAndLinkedEdges() elif self.inspection_policy == 'edges': - graph.inspection_policy = EdgesAndLinkedNodes() + renderer.inspection_policy = EdgesAndLinkedNodes() else: - graph.inspection_policy = None + renderer.inspection_policy = None - self.handles['renderer'] = graph - self.handles['scatter_1_renderer'] = graph.node_renderer - self.handles['multi_line_1_renderer'] = graph.edge_renderer - - def _init_glyph(self, plot, mapping, properties, key): - """ - Returns a Bokeh glyph object. - """ - properties = mpl_to_bokeh(properties) - mapping.pop('legend', None) - plot_method = '_'.join(key.split('_')[:-1]) - glyph = MultiLine if plot_method == 'multi_line' else Circle - glyph = glyph(**dict(properties, **mapping)) - return None, glyph + self.handles['renderer'] = renderer + self.handles['scatter_1_renderer'] = renderer.node_renderer + self.handles['multi_line_1_renderer'] = renderer.edge_renderer From 3082d0a1e3c5bb7f0bbd1d461772c406a5a15a33 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 13 Sep 2017 14:28:13 +0100 Subject: [PATCH 20/60] Eliminated usage of dimensions method for internal validation --- holoviews/core/data/array.py | 2 +- holoviews/core/data/dask.py | 2 +- holoviews/core/data/dictionary.py | 6 +++--- holoviews/core/data/iris.py | 2 +- holoviews/core/data/pandas.py | 4 ++-- holoviews/core/element.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/holoviews/core/data/array.py b/holoviews/core/data/array.py index 7864195bbd..24d45f2206 100644 --- a/holoviews/core/data/array.py +++ b/holoviews/core/data/array.py @@ -71,7 +71,7 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def validate(cls, dataset): - ndims = len(dataset.dimensions()) + ndims = len(dataset.kdims+dataset.vdims) ncols = dataset.data.shape[1] if dataset.data.ndim > 1 else 1 if ncols < ndims: raise ValueError("Supplied data does not match specified " diff --git a/holoviews/core/data/dask.py b/holoviews/core/data/dask.py index 15247dd490..3e8b0012a2 100644 --- a/holoviews/core/data/dask.py +++ b/holoviews/core/data/dask.py @@ -250,7 +250,7 @@ def iloc(cls, dataset, index): rows, cols = index scalar = False if isinstance(cols, slice): - cols = [d.name for d in dataset.dimensions()][cols] + cols = [d.name for d in dataset.kdims+dataset.vdims][cols] elif np.isscalar(cols): scalar = np.isscalar(rows) cols = [dataset.get_dimension(cols).name] diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index d2528161c8..686e72e15a 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -87,11 +87,11 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def validate(cls, dataset): - dimensions = dataset.dimensions(label='name') - not_found = [d for d in dimensions if d not in dataset.data] + dimensions = dataset.kdims+dataset.vdims + not_found = [d for d in dimensions if d.name not in dataset.data] if not_found: raise ValueError('Following dimensions not found in data: %s' % not_found) - lengths = [len(dataset.data[dim]) for dim in dimensions] + lengths = [len(dataset.data[dim.name]) for dim in dimensions] if len({l for l in lengths if l > 1}) > 1: raise ValueError('Length of columns do not match') diff --git a/holoviews/core/data/iris.py b/holoviews/core/data/iris.py index d66af4e14b..0edffaa135 100644 --- a/holoviews/core/data/iris.py +++ b/holoviews/core/data/iris.py @@ -137,7 +137,7 @@ def shape(cls, dataset, gridded=False): if gridded: return dataset.data.shape else: - return (cls.length(dataset), len(dataset.dimensions())) + return (cls.length(dataset), len(dataset.kdims+dataset.vdims)) @classmethod diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 29f594a5e6..3f635bed47 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -82,8 +82,8 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def validate(cls, dataset): - not_found = [d for d in dataset.dimensions(label='name') - if d not in dataset.data.columns] + not_found = [d for d in dataset.kdims+dataset.vdims + if d.name not in dataset.data.columns] if not_found: raise ValueError("Supplied data does not contain specified " "dimensions, the following dimensions were " diff --git a/holoviews/core/element.py b/holoviews/core/element.py index 211027e25c..659d689ec7 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -207,7 +207,7 @@ def rows(self): @property def cols(self): - return len(self.dimensions()) + return len(self.kdims+self.vdims) def pprint_cell(self, row, col): From 40110b29fadfdda23c66dbd5c17a54f5623456f0 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 13 Sep 2017 14:28:56 +0100 Subject: [PATCH 21/60] Implement range and dimensions methods for Graph --- holoviews/element/graphs.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index f114eb37d5..685b83434b 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -2,6 +2,7 @@ from ..core import Dimension, Dataset, Element2D from ..core.dimension import redim +from ..core.util import max_range from .chart import Points from .path import Path @@ -68,6 +69,23 @@ def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): return super(Graph, self).clone(data, shared_data, new_type, *args, **overrides) + def range(self, dimension, data_range=True): + if self.nodes and dimension in self.nodes.dimensions(): + node_range = self.nodes.range(dimension, data_range) + if self._nodepaths: + path_range = self._nodepaths.range(dimension, data_range) + return max_range([node_range, path_range]) + return node_range + return super(Graph, self).range(dimension, data_range) + + + def dimensions(self, selection='all', label=False): + dimensions = super(Graph, self).dimensions(selection, label) + if self.nodes and selection == 'all': + return dimensions+self.nodes.dimensions(selection, label) + return dimensions + + @property def nodepaths(self): """ From e01f79a5969ea02f82b01684e35399438f09dd87 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 13 Sep 2017 14:29:44 +0100 Subject: [PATCH 22/60] Improved handling of CompositeElementPlot updates --- holoviews/plotting/bokeh/element.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index a9cc66df6e..63eecb292f 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1014,9 +1014,10 @@ def _update_glyphs(self, element, ranges): self.static_source = (self.dynamic and (current_id == previous_id)) data, mapping = self.get_data(element, ranges, empty) - for key, gdata in data.items(): + for key in dict(mapping, **data): + gdata = data[key] source = self.handles[key+'_source'] - glyph = self.handles[key+'_glyph'] + glyph = self.handles.get(key+'_glyph') if not self.static_source: self._update_datasource(source, gdata) From 5cd239c4472c247715556bea1e50a6b64e6e6430 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 13 Sep 2017 14:30:53 +0100 Subject: [PATCH 23/60] Correctly handle updates to bokeh GraphPlot --- holoviews/plotting/bokeh/graphs.py | 70 +++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index c49c913ce3..d5955d304e 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -1,6 +1,7 @@ import param +import numpy as np -from bokeh.models import Range1d, Circle, MultiLine +from bokeh.models import Range1d, Circle, MultiLine, HoverTool, ColumnDataSource try: from bokeh.models import (StaticLayoutProvider, GraphRenderer, NodesAndLinkedEdges, @@ -41,7 +42,7 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot): # Define all the glyph handles to update _update_handles = ([glyph+'_'+model for model in ['glyph', 'glyph_renderer', 'source'] for glyph in ['scatter_1', 'multi_line_1']] + - ['color_mapper', 'colorbar']) + ['color_mapper', 'colorbar', 'layout_source']) style_opts = (['edge_'+p for p in line_properties] +\ ['node_'+p for p in fill_properties+line_properties]+['node_size', 'cmap']) @@ -53,6 +54,8 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): def _hover_opts(self, element): dims = element.nodes.dimensions()[3:] + if element.nodes.dimension_values(2).dtype.kind not in 'if': + dims = [('Node', '$node')] + dims return dims, {} def get_extents(self, element, ranges): @@ -60,8 +63,9 @@ def get_extents(self, element, ranges): Extents are set to '' and None because x-axis is categorical and y-axis auto-ranges. """ - x0, x1 = element.nodes.range(0) - y0, y1 = element.nodes.range(1) + xdim, ydim = element.nodes.kdims[:2] + x0, x1 = ranges[xdim.name] + y0, y1 = ranges[ydim.name] return (x0, y0, x1, y1) def _get_axis_labels(self, *args, **kwargs): @@ -74,33 +78,67 @@ def _get_axis_labels(self, *args, **kwargs): def get_data(self, element, ranges=None, empty=False): style = self.style[self.cyclic_index] - point_data = {'index': element.nodes.dimension_values(2).astype(int)} - for d in element.nodes.dimensions()[2:]: - point_data[d.name] = element.nodes.dimension_values(d) + xidx, yidx = (1, 0) if self.invert_axes else (0, 1) + # Get node data + nodes = element.nodes.dimension_values(2) + node_positions = element.nodes.array([0, 1, 2]) + # Map node indices to integers + if nodes.dtype.kind not in 'if': + node_indices = {v: i for i, v in enumerate(nodes)} + index = np.array([node_indices[n] for n in nodes], dtype=np.int32) + layout = {node_indices[z]: (y, x) if self.invert_axes else (x, y) + for x, y, z in node_positions} + else: + index = nodes.astype(np.int32) + layout = {int(z): (y, x) if self.invert_axes else (x, y) + for x, y, z in node_positions} + point_data = {'index': index} cdata, cmapping = self._get_color_data(element.nodes, ranges, style, 'node_fill_color') point_data.update(cdata) point_mapping = cmapping - xidx, yidx = (1, 0) if self.invert_axes else (0, 1) + # Get hover data + if any(isinstance(t, HoverTool) for t in self.state.tools): + if nodes.dtype.kind not in 'if': + point_data['node'] = nodes + for d in element.nodes.dimensions()[3:]: + point_data[d.name] = element.nodes.dimension_values(d) + + # Get edge data + nan_node = index.max()+1 xs, ys = (element.dimension_values(i) for i in range(2)) + if nodes.dtype.kind not in 'if': + xs = np.array([node_indices.get(x, nan_node) for x in xs], dtype=np.int32) + ys = np.array([node_indices.get(y, nan_node) for y in ys], dtype=np.int32) path_data = dict(start=xs, end=ys) if element._nodepaths: edges = element.nodepaths path_data['xs'] = [path[:, xidx] for path in edges.data] path_data['ys'] = [path[:, yidx] for path in edges.data] - data = {'scatter_1': point_data, 'multi_line_1': path_data} + data = {'scatter_1': point_data, 'multi_line_1': path_data, 'layout': layout} mapping = {'scatter_1': point_mapping, 'multi_line_1': {}} return data, mapping + + def _update_datasource(self, source, data): + """ + Update datasource with data for a new frame. + """ + if isinstance(source, ColumnDataSource): + source.data.update(data) + else: + source.graph_layout.update(data) + + def _init_glyphs(self, plot, element, ranges, source): # Get data and initialize data source data, mapping = self.get_data(element, ranges, False) self.handles['previous_id'] = element._plot_id properties = {} mappings = {} - for key in dict(mapping, **data): + for key in mapping: source = self._init_datasource(data.get(key, {})) self.handles[key+'_source'] = source glyph_props = self._glyph_properties(plot, element, source, ranges) @@ -110,8 +148,7 @@ def _init_glyphs(self, plot, element, ranges, source): properties.update(mappings) # Define static layout - layout_dict = {int(z): (x, y) for x, y, z in element.nodes.array([0, 1, 2])} - layout = StaticLayoutProvider(graph_layout=layout_dict) + layout = StaticLayoutProvider(graph_layout=data['layout']) node_source = self.handles['scatter_1_source'] edge_source = self.handles['multi_line_1_source'] renderer = plot.graph(node_source, edge_source, layout, **properties) @@ -131,6 +168,9 @@ def _init_glyphs(self, plot, element, ranges, source): else: renderer.inspection_policy = None - self.handles['renderer'] = renderer - self.handles['scatter_1_renderer'] = renderer.node_renderer - self.handles['multi_line_1_renderer'] = renderer.edge_renderer + self.handles['layout_source'] = layout + self.handles['glyph_renderer'] = renderer + self.handles['scatter_1_glyph_renderer'] = renderer.node_renderer + self.handles['multi_line_1_glyph_renderer'] = renderer.edge_renderer + self.handles['scatter_1_glyph'] = renderer.node_renderer.glyph + self.handles['multi_line_1_glyph'] = renderer.edge_renderer.glyph From 4d47fcad868eb6dd7ae327f9083448b53691850f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 00:17:38 +0100 Subject: [PATCH 24/60] Fix for Graph data redim --- holoviews/element/graphs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 685b83434b..c6f3db21ea 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -55,7 +55,7 @@ def __init__(self, data, **params): self.nodes = nodes self._nodepaths = nodepaths super(Graph, self).__init__(edges, **params) - self.redim = graph_redim(self) + self.redim = graph_redim(self, mode='dataset') def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): if data is None: From 39abeae227b91674bd3746e590e21701b83c567a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 01:37:42 +0100 Subject: [PATCH 25/60] Updated GraphPlot with new MultInterface support --- holoviews/plotting/bokeh/graphs.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index d5955d304e..a78985f162 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -107,15 +107,19 @@ def get_data(self, element, ranges=None, empty=False): # Get edge data nan_node = index.max()+1 - xs, ys = (element.dimension_values(i) for i in range(2)) + start, end = (element.dimension_values(i) for i in range(2)) if nodes.dtype.kind not in 'if': - xs = np.array([node_indices.get(x, nan_node) for x in xs], dtype=np.int32) - ys = np.array([node_indices.get(y, nan_node) for y in ys], dtype=np.int32) - path_data = dict(start=xs, end=ys) + start = np.array([node_indices.get(x, nan_node) for x in start], dtype=np.int32) + end = np.array([node_indices.get(y, nan_node) for y in end], dtype=np.int32) + path_data = dict(start=start, end=end) if element._nodepaths: - edges = element.nodepaths - path_data['xs'] = [path[:, xidx] for path in edges.data] - path_data['ys'] = [path[:, yidx] for path in edges.data] + edges = element.nodepaths.split() + if len(edges) == len(start): + path_data['xs'] = [path.dimension_values(xidx) for path in edges] + path_data['ys'] = [path.dimension_values(yidx) for path in edges] + else: + self.warning('Graph edge paths do not match the number of abstract edges ' + 'and will be skipped') data = {'scatter_1': point_data, 'multi_line_1': path_data, 'layout': layout} mapping = {'scatter_1': point_mapping, 'multi_line_1': {}} From 6373a7d33ed890a1e5dce4e0c3e869f4ab0b5edc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 01:39:55 +0100 Subject: [PATCH 26/60] Added support for select on Graph --- holoviews/core/data/multipath.py | 7 ++++++ holoviews/element/graphs.py | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/holoviews/core/data/multipath.py b/holoviews/core/data/multipath.py index 245746cf1a..0f500c9d67 100644 --- a/holoviews/core/data/multipath.py +++ b/holoviews/core/data/multipath.py @@ -109,6 +109,13 @@ def select(cls, dataset, selection_mask=None, **selection): data.append(sel) return data + @classmethod + def select_paths(cls, dataset, selection): + """ + Allows selecting paths with usual NumPy slicing index. + """ + return [s[0] for s in np.array([{0: p} for p in dataset.data])[selection]] + @classmethod def aggregate(cls, columns, dimensions, function, **kwargs): raise NotImplementedError('Aggregation currently not implemented') diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index c6f3db21ea..9e2d7c7bcc 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -69,6 +69,43 @@ def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): return super(Graph, self).clone(data, shared_data, new_type, *args, **overrides) + def select(self, selection_specs=None, **selection): + """ + Allows selecting data by the slices, sets and scalar values + along a particular dimension. The indices should be supplied as + keywords mapping between the selected dimension and + value. Additionally selection_specs (taking the form of a list + of type.group.label strings, types or functions) may be + supplied, which will ensure the selection is only applied if the + specs match the selected object. + """ + selection = {dim: sel for dim, sel in selection.items() + if dim in self.dimensions()+['selection_mask']} + if (selection_specs and not any(self.matches(sp) for sp in selection_specs) + or not selection): + return self + + nodes = self.nodes.select(**selection) + dimensions = self.kdims+self.vdims + selection = {k: v for k, v in selection.items() if k in dimensions} + if len(nodes) != len(self): + xdim, ydim = dimensions[:2] + indices = list(nodes.dimension_values(2)) + selection[xdim.name] = indices + selection[ydim.name] = indices + if selection: + mask = self.interface.select_mask(self, selection) + data = self.interface.select(self, mask) + if self._nodepaths: + paths = self.nodepaths.interface.select_paths(self.nodepaths, mask) + return self.clone((data, nodes, paths)) + else: + data = self.data + if self._nodepaths: + return self.clone((data, nodes, self._nodepaths)) + return self.clone((data, nodes)) + + def range(self, dimension, data_range=True): if self.nodes and dimension in self.nodes.dimensions(): node_range = self.nodes.range(dimension, data_range) From 2a1ff962f9c3f693f543b747193eba1c9f5d41d8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 01:40:43 +0100 Subject: [PATCH 27/60] Added bundle_graph operation based on datashader hammer_bundle --- holoviews/operation/datashader.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index e248c7bb9b..7aecc961f3 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -15,6 +15,7 @@ ds_version = LooseVersion(ds.__version__) from datashader.core import bypixel +from datashader.bundling import hammer_bundle from datashader.pandas import pandas_pipeline from datashader.dask import dask_pipeline from datashape.dispatch import dispatch @@ -651,3 +652,32 @@ def _process(self, element, key=None): return element.clone(new_data) +def split_dataframe(path_df): + """ + Splits a dataframe of paths separated by NaNs into individual + dataframes. + """ + splits = np.where(path_df.iloc[:, 0].isnull())[0]+1 + return [df for df in np.split(path_df, splits) if len(df) > 1] + + +class bundle_graph(Operation, hammer_bundle): + """ + Iteratively group edges and return as paths suitable for datashading. + + Breaks each edge into a path with multiple line segments, and + iteratively curves this path to bundle edges into groups. + """ + + split = param.Boolean(default=True, doc=""" + Determines whether bundled edges will be split into individual edges + or concatenated with NaN separators.""") + + def _process(self, element, key=None): + index = element.nodes.kdims[2].name + position_df = element.nodes.dframe([0, 1, 2]).set_index(index) + rename = {d.name: v for d, v in zip(element.kdims[:2], ['source', 'target'])} + edges_df = element.redim(**rename).dframe([0, 1]) + paths = hammer_bundle.__call__(self, position_df, edges_df, **self.p) + paths = split_dataframe(paths) if self.p.split else [paths] + return element.clone((element.data, element.nodes, paths)) From ddab5a84133d02b308c0a185aafa385ad829fda8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 01:44:12 +0100 Subject: [PATCH 28/60] Small Nodes style fix --- holoviews/plotting/bokeh/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 7686c95bb2..8bf4697a78 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -176,7 +176,7 @@ def colormap_generator(palette): edge_hover_line_color='indianred', node_selection_fill_color='limegreen', edge_selection_line_color='limegreen', edge_line_color='black', node_line_color='black') -options.Nodes = Options('style', line_color='black', fill_color=Cycle(), size=20) +options.Nodes = Options('style', line_color='black', color=Cycle(), size=20) options.NodePaths = Options('style', color='black') # Define composite defaults From e86d0aa45d9759f05a6fc02085554bb9fac2e91b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 01:44:29 +0100 Subject: [PATCH 29/60] Updated Network_Graphs user guide --- examples/user_guide/Network_Graphs.ipynb | 117 +++++++++++++++++++++-- 1 file changed, 108 insertions(+), 9 deletions(-) diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb index b34adc7812..810f0546a8 100644 --- a/examples/user_guide/Network_Graphs.ipynb +++ b/examples/user_guide/Network_Graphs.ipynb @@ -29,7 +29,7 @@ "To summarize a ``Graph`` consists of three different components:\n", "\n", "* The ``Graph`` itself holds the abstract edges stored as a table of node indices.\n", - "* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``.\n", + "* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``. The ``Nodes`` may also define any number of value dimensions, which can be revealed when hovering over the nodes or to color the nodes by.\n", "* The ``NodePaths`` can optionally be supplied to declare explicit node paths.\n", "\n", "#### A simple Graph\n", @@ -55,7 +55,9 @@ "x = np.cos(circ)\n", "y = np.sin(circ)\n", "\n", - "simple_graph = hv.Graph(((start, end), (x, y, node_indices))).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", + "padding = dict(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", + "\n", + "simple_graph = hv.Graph(((start, end), (x, y, node_indices))).redim.range(**padding)\n", "simple_graph" ] }, @@ -93,14 +95,14 @@ "outputs": [], "source": [ "def bezier(start, end, control, steps=np.linspace(0, 1, 100)):\n", - " return (1-steps)*(1-steps)*start + 2*(1-steps)*steps*control+steps*steps*end\n", + " return (1-steps)**2*start + 2*(1-steps)*steps*control+steps**2*end\n", "\n", "paths = []\n", "for node_index in node_indices:\n", " ex, ey = x[node_index], y[node_index]\n", " paths.append(np.column_stack([bezier(x[0], ex, 0), bezier(y[0], ey, 0)]))\n", " \n", - "bezier_graph = hv.Graph(((start, end), (x, y, node_indices), paths)).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", + "bezier_graph = hv.Graph(((start, end), (x, y, node_indices), paths)).redim.range(**padding)\n", "bezier_graph" ] }, @@ -163,8 +165,8 @@ "outputs": [], "source": [ "%%opts Graph [tools=['hover'] color_index='Type'] (cmap='Set1')\n", - "nodes = hv.Nodes((x, y, node_indices, ['Input']+['Output']*(N-1)), vdims=['Type'])\n", - "hv.Graph(((start, end), nodes, paths)).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))" + "nodes = hv.Nodes((x, y, node_indices, ['Output']+['Input']*(N-1)), vdims=['Type'])\n", + "hv.Graph(((start, end), nodes, paths)).redim.range(**padding)" ] }, { @@ -193,6 +195,28 @@ "hv.Graph.from_networkx(G, nx.layout.circular_layout).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Animating graphs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph {+framewise}\n", + "layouts = [nx.layout.circular_layout, nx.layout.fruchterman_reingold_layout, nx.layout.spectral_layout,\n", + " nx.layout.spring_layout, nx.layout.random_layout]\n", + "\n", + "G = nx.karate_club_graph()\n", + "hv.HoloMap({l.__name__[:-7]: hv.Graph.from_networkx(G, l) for l in layouts},\n", + " kdims=['Layout'])" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -216,10 +240,85 @@ "%%opts Graph [width=800 height=800 xaxis=None yaxis=None tools=['hover'] color_index='circle']\n", "%%opts Graph (node_size=10 edge_line_width=1)\n", "colors = ['#000000']+hv.Cycle('Category20').values\n", - "edges_df = pd.read_csv('../examples/assets/fb_edges.csv')\n", - "fb_nodes = hv.Nodes(pd.read_csv('../examples/assets/fb_nodes.csv')).sort()\n", + "edges_df = pd.read_csv('../assets/fb_edges.csv')\n", + "fb_nodes = hv.Nodes(pd.read_csv('../assets/fb_nodes.csv')).sort()\n", "fb_graph = hv.Graph((edges_df, fb_nodes), label='Facebook Circles')\n", - "fb_graph.redim.range(x=(-0.05, 1.05), y=(-0.05, 1.05)).opts(style=dict(cmap=colors))" + "fb_graph = fb_graph.redim.range(x=(-0.05, 1.05), y=(-0.05, 1.05)).opts(style=dict(cmap=colors))\n", + "fb_graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bundling graphs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The datashader library provides algorithms for bundling the edges of a graph and HoloViews provides convenient wrappers around the libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from holoviews.operation.datashader import datashade, bundle_graph\n", + "bundled = bundle_graph(fb_graph)\n", + "bundled" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Datashading graphs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For graphs with a large number of edges we can datashade the paths and display the nodes separately. This loses some of the interactive features but will let you visualize quite large graphs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Nodes [width=800 height=800 xaxis=None yaxis=None tools=['hover'] color_index='circle'] (size=10)\n", + "%%opts Overlay [show_legend=False]\n", + "datashade(bundled.nodepaths) * bundled.nodes.opts(style=dict(cmap=colors))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Applying selections" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively we can select the nodes and edges by an attribute that resides on either. In this case we will select the nodes and edges for a particular circle and then overlay just the selected part of the graph on the datashaded plot. In this way a smaller subgraph can be highlighted and the larger graph can be datashaded." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph [width=800 height=800 xaxis=None yaxis=None tools=['hover']] (node_fill_color='black')\n", + "datashade(bundle_graph(fb_graph, split=False).nodepaths) * bundled.select(circle='circle15')" ] } ], From 8234c5efecf869c5577516f7b8aad621ae9de9a8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 12:40:43 +0100 Subject: [PATCH 30/60] Added support for coloring mpl Graph nodes --- holoviews/plotting/mpl/graphs.py | 59 ++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index 3008bd03de..b7189f4f60 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -1,25 +1,54 @@ +import param +import numpy as np + from matplotlib.collections import LineCollection -from .chart import ChartPlot +from ...core.util import basestring +from .element import ColorbarPlot -class GraphPlot(ChartPlot): +class GraphPlot(ColorbarPlot): """ GraphPlot """ + color_index = param.ClassSelector(default=None, class_=(basestring, int), + allow_None=True, doc=""" + Index of the dimension from which the color will the drawn""") + style_opts = ['edge_alpha', 'edge_color', 'edge_linestyle', 'edge_linewidth', 'node_alpha', 'node_color', 'node_edgecolors', 'node_facecolors', - 'node_linewidth', 'node_marker', 'node_size', 'visible'] + 'node_linewidth', 'node_marker', 'node_size', 'visible', 'cmap'] + + def _compute_styles(self, element, ranges, style): + cdim = element.get_dimension(self.color_index) + color = style.pop('node_color', None) + cmap = style.get('cmap', None) + if cdim and cmap: + cs = element.dimension_values(self.color_index) + # Check if numeric otherwise treat as categorical + if cs.dtype.kind in 'if': + style['c'] = cs + else: + categories = np.unique(cs) + xsorted = np.argsort(categories) + ypos = np.searchsorted(categories[xsorted], cs) + style['c'] = xsorted[ypos] + self._norm_kwargs(element, ranges, style, cdim) + elif color: + style['c'] = color + style['node_edgecolors'] = style.pop('node_edgecolors', 'none') + return style def get_data(self, element, ranges, style): xidx, yidx = (1, 0) if self.invert_axes else (0, 1) pxs, pys = (element.nodes.dimension_values(i) for i in range(2)) dims = element.nodes.dimensions() + self._compute_styles(element.nodes, ranges, style) paths = element.nodepaths.data if self.invert_axes: paths = [p[:, ::-1] for p in paths] - return {'points': (pxs, pys), 'paths': paths}, style, {'dimensions': dims} + return {'nodes': (pxs, pys), 'edges': paths}, style, {'dimensions': dims} def get_extents(self, element, ranges): """ @@ -32,33 +61,33 @@ def get_extents(self, element, ranges): def init_artists(self, ax, plot_args, plot_kwargs): # Draw edges + color_opts = ['c', 'cmap', 'vmin', 'vmax', 'norm'] edge_opts = {k[5:] if 'edge_' in k else k: v for k, v in plot_kwargs.items() - if 'node_' not in k} - paths = plot_args['paths'] + if 'node_' not in k and k not in color_opts} + paths = plot_args['edges'] edges = LineCollection(paths, **edge_opts) ax.add_collection(edges) # Draw nodes - xs, ys = plot_args['points'] + xs, ys = plot_args['nodes'] node_opts = {k[5:] if 'node_' in k else k: v for k, v in plot_kwargs.items() if 'edge_' not in k} if 'size' in node_opts: node_opts['s'] = node_opts.pop('size')**2 nodes = ax.scatter(xs, ys, **node_opts) - + return {'nodes': nodes, 'edges': edges} def update_handles(self, key, axis, element, ranges, style): - artist = self.handles['nodes'] + nodes = self.handles['nodes'] data, style, axis_kwargs = self.get_data(element, ranges, style) xs, ys = data['nodes'] - artist.set_xdata(xs) - artist.set_ydata(ys) - + nodes.set_offsets(np.column_stack([xs, ys])) + edges = self.handles['edges'] paths = data['edges'] - artist.set_paths(paths) - artist.set_visible(style.get('visible', True)) - + edges.set_paths(paths) + edges.set_visible(style.get('visible', True)) + return axis_kwargs From 44312de0ce0909e0b23ed416c1c6b602c2bc2c8c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 12:45:13 +0100 Subject: [PATCH 31/60] Cleaned up network graph user guide --- examples/user_guide/Network_Graphs.ipynb | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb index 810f0546a8..0716c154d3 100644 --- a/examples/user_guide/Network_Graphs.ipynb +++ b/examples/user_guide/Network_Graphs.ipynb @@ -6,14 +6,12 @@ "metadata": {}, "outputs": [], "source": [ - "import param\n", "import numpy as np\n", "import pandas as pd\n", "import holoviews as hv\n", "import networkx as nx\n", - "from bokeh.models import HoverTool\n", "\n", - "hv.notebook_extension('bokeh', 'matplotlib')\n", + "hv.notebook_extension('bokeh')\n", "\n", "%opts Graph [width=400 height=400]" ] @@ -237,7 +235,16 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Graph [width=800 height=800 xaxis=None yaxis=None tools=['hover'] color_index='circle']\n", + "%opts Nodes Graph [width=800 height=800 xaxis=None yaxis=None tools=['hover']]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Graph [color_index='circle']\n", "%%opts Graph (node_size=10 edge_line_width=1)\n", "colors = ['#000000']+hv.Cycle('Category20').values\n", "edges_df = pd.read_csv('../assets/fb_edges.csv')\n", @@ -292,9 +299,8 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Nodes [width=800 height=800 xaxis=None yaxis=None tools=['hover'] color_index='circle'] (size=10)\n", - "%%opts Overlay [show_legend=False]\n", - "datashade(bundled.nodepaths) * bundled.nodes.opts(style=dict(cmap=colors))" + "%%opts Nodes [color_index='circle'] (size=10 cmap=colors) Overlay [show_legend=False]\n", + "datashade(bundled.nodepaths, width=800, height=800) * bundled.nodes" ] }, { @@ -317,8 +323,9 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Graph [width=800 height=800 xaxis=None yaxis=None tools=['hover']] (node_fill_color='black')\n", - "datashade(bundle_graph(fb_graph, split=False).nodepaths) * bundled.select(circle='circle15')" + "%%opts Graph (node_fill_color='white')\n", + "bundled_paths = bundle_graph(fb_graph, split=False).nodepaths\n", + "datashade(bundled_paths, width=800, height=800) * bundled.select(circle='circle15')" ] } ], From ec54acc030f520a3ba1356c109d75c89be82bbcd Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 16:07:21 +0100 Subject: [PATCH 32/60] Rename NodePaths to EdgePaths --- holoviews/element/graphs.py | 58 ++++++++++++++-------------- holoviews/plotting/bokeh/__init__.py | 6 +-- holoviews/plotting/bokeh/graphs.py | 4 +- holoviews/plotting/mpl/__init__.py | 2 +- holoviews/plotting/mpl/graphs.py | 2 +- 5 files changed, 35 insertions(+), 37 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 9e2d7c7bcc..16d3d29de6 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -9,7 +9,7 @@ class graph_redim(redim): """ Extension for the redim utility that allows re-dimensioning - Graph objects including their nodes and nodepaths. + Graph objects including their nodes and edgepaths. """ def __call__(self, specs=None, **dimensions): @@ -17,8 +17,8 @@ def __call__(self, specs=None, **dimensions): new_data = (redimmed.data,) if self.parent.nodes: new_data = new_data + (self.parent.nodes.redim(specs, **dimensions),) - if self.parent._nodepaths: - new_data = new_data + (self.parent.nodepaths.redim(specs, **dimensions),) + if self.parent._edgepaths: + new_data = new_data + (self.parent.edgepaths.redim(specs, **dimensions),) return redimmed.clone(new_data) @@ -29,12 +29,12 @@ class Graph(Dataset, Element2D): the abstract edges between nodes and optionally may be made concrete by supplying a Nodes Element defining the concrete positions of each node. If the node positions are supplied - the NodePaths (defining the concrete edges) can be inferred + the EdgePaths (defining the concrete edges) can be inferred automatically or supplied explicitly. The constructor accepts regular columnar data defining the edges or a tuple of the abstract edges and nodes, or a tuple of the - abstract edges, nodes, and nodepaths. + abstract edges, nodes, and edgepaths. """ group = param.String(default='Graph') @@ -45,27 +45,27 @@ class Graph(Dataset, Element2D): def __init__(self, data, **params): if isinstance(data, tuple): data = data + (None,)* (3-len(data)) - edges, nodes, nodepaths = data + edges, nodes, edgepaths = data else: - edges, nodes, nodepaths = data, None, None + edges, nodes, edgepaths = data, None, None if nodes is not None and not isinstance(nodes, Nodes): nodes = Nodes(nodes) - if nodepaths is not None and not isinstance(nodepaths, NodePaths): - nodepaths = NodePaths(nodepaths) + if edgepaths is not None and not isinstance(edgepaths, EdgePaths): + edgepaths = EdgePaths(edgepaths) self.nodes = nodes - self._nodepaths = nodepaths + self._edgepaths = edgepaths super(Graph, self).__init__(edges, **params) self.redim = graph_redim(self, mode='dataset') def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): if data is None: data = (self.data, self.nodes) - if self._nodepaths: - data = data + (self.nodepaths,) + if self._edgepaths: + data = data + (self.edgepaths,) elif not isinstance(data, tuple): data = (data, self.nodes) - if self._nodepaths: - data = data + (self.nodepaths,) + if self._edgepaths: + data = data + (self.edgepaths,) return super(Graph, self).clone(data, shared_data, new_type, *args, **overrides) @@ -96,21 +96,21 @@ def select(self, selection_specs=None, **selection): if selection: mask = self.interface.select_mask(self, selection) data = self.interface.select(self, mask) - if self._nodepaths: - paths = self.nodepaths.interface.select_paths(self.nodepaths, mask) + if self._edgepaths: + paths = self.edgepaths.interface.select_paths(self.edgepaths, mask) return self.clone((data, nodes, paths)) else: data = self.data - if self._nodepaths: - return self.clone((data, nodes, self._nodepaths)) + if self._edgepaths: + return self.clone((data, nodes, self._edgepaths)) return self.clone((data, nodes)) def range(self, dimension, data_range=True): if self.nodes and dimension in self.nodes.dimensions(): node_range = self.nodes.range(dimension, data_range) - if self._nodepaths: - path_range = self._nodepaths.range(dimension, data_range) + if self._edgepaths: + path_range = self._edgepaths.range(dimension, data_range) return max_range([node_range, path_range]) return node_range return super(Graph, self).range(dimension, data_range) @@ -124,15 +124,13 @@ def dimensions(self, selection='all', label=False): @property - def nodepaths(self): + def edgepaths(self): """ - Returns the fixed NodePaths or computes direct connections + Returns the fixed EdgePaths or computes direct connections between supplied nodes. """ - if self.nodes is None: - raise ValueError('Cannot return NodePaths without node positions') - elif self._nodepaths: - return self._nodepaths + if self._edgepaths: + return self._edgepaths paths = [] for start, end in self.array(self.kdims): start_ds = self.nodes[:, :, start] @@ -140,7 +138,7 @@ def nodepaths(self): 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])]) - return NodePaths(paths) + return EdgePaths(paths) @classmethod def from_networkx(cls, G, layout_function, nodes=None, **kwargs): @@ -172,10 +170,10 @@ class Nodes(Points): group = param.String(default='Nodes') -class NodePaths(Path): +class EdgePaths(Path): """ - NodePaths is a simple Element representing the paths of edges + EdgePaths is a simple Element representing the paths of edges connecting nodes in a graph. """ - group = param.String(default='NodePaths') + group = param.String(default='EdgePaths') diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 8bf4697a78..1a208e1bf1 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -10,7 +10,7 @@ Box, Bounds, Ellipse, Polygons, BoxWhisker, Arrow, ErrorBars, Text, HLine, VLine, Spline, Spikes, Table, ItemTable, Area, HSV, QuadMesh, VectorField, - Graph, Nodes, NodePaths) + Graph, Nodes, EdgePaths) from ...core.options import Options, Cycle, Palette try: @@ -86,7 +86,7 @@ # Graph Elements Graph: GraphPlot, Nodes: PointPlot, - NodePaths: PathPlot, + EdgePaths: PathPlot, # Tabular Table: TablePlot, @@ -177,7 +177,7 @@ def colormap_generator(palette): edge_selection_line_color='limegreen', edge_line_color='black', node_line_color='black') options.Nodes = Options('style', line_color='black', color=Cycle(), size=20) -options.NodePaths = Options('style', color='black') +options.EdgePaths = Options('style', color='black') # Define composite defaults options.GridMatrix = Options('plot', shared_xaxis=True, shared_yaxis=True, diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index a78985f162..19806c5a2a 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -112,8 +112,8 @@ def get_data(self, element, ranges=None, empty=False): start = np.array([node_indices.get(x, nan_node) for x in start], dtype=np.int32) end = np.array([node_indices.get(y, nan_node) for y in end], dtype=np.int32) path_data = dict(start=start, end=end) - if element._nodepaths: - edges = element.nodepaths.split() + if element._edgepaths: + edges = element.edgepaths.split() if len(edges) == len(start): path_data['xs'] = [path.dimension_values(xidx) for path in edges] path_data['ys'] = [path.dimension_values(yidx) for path in edges] diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 80d0c39830..72e4724efb 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -150,7 +150,7 @@ def grid_selector(grid): # Graph Elements Graph: GraphPlot, Nodes: PointPlot, - NodePaths: PathPlot, + EdgePaths: PathPlot, # Annotation plots VLine: VLinePlot, diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index b7189f4f60..76fae51fa7 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -45,7 +45,7 @@ def get_data(self, element, ranges, style): dims = element.nodes.dimensions() self._compute_styles(element.nodes, ranges, style) - paths = element.nodepaths.data + paths = element.edgepaths.data if self.invert_axes: paths = [p[:, ::-1] for p in paths] return {'nodes': (pxs, pys), 'edges': paths}, style, {'dimensions': dims} From 2d828692e37a1ed7d55a6eac246bb1d0ec2f00b4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 16:08:00 +0100 Subject: [PATCH 33/60] Automatically layout abstract graphs --- holoviews/element/graphs.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 16d3d29de6..869b0cfa65 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -1,8 +1,10 @@ import param +import numpy as np from ..core import Dimension, Dataset, Element2D from ..core.dimension import redim from ..core.util import max_range +from ..core.operation import Operation from .chart import Points from .path import Path @@ -22,6 +24,31 @@ 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): + + layout = param.Callable(default=None, doc=""" + A NetworkX layout function""") + + def _process(self, element, key=None): + if self.p.layout: + graph = nx.from_edgelist(element.array([0, 1])) + positions = self.p.layout(graph) + return Nodes([tuple(pos)+(idx,) for idx, pos in sorted(positions.items())]) + else: + source = element.dimension_values(0, expanded=False) + target = element.dimension_values(1, expanded=False) + nodes = np.unique(np.concatenate([source, target])) + return Nodes(circular_layout(nodes)) + + class Graph(Dataset, Element2D): """ Graph is high-level Element representing both nodes and edges. @@ -55,6 +82,8 @@ def __init__(self, data, **params): self.nodes = nodes self._edgepaths = edgepaths super(Graph, self).__init__(edges, **params) + if self.nodes is None: + self.nodes = layout_nodes(self) self.redim = graph_redim(self, mode='dataset') def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): From 1ae76dfe1614d45c99ccfeb086dae750f98fb007 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 16:09:09 +0100 Subject: [PATCH 34/60] Improvements for Graph datashading --- holoviews/operation/datashader.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 7aecc961f3..40193aac00 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -15,18 +15,22 @@ ds_version = LooseVersion(ds.__version__) from datashader.core import bypixel -from datashader.bundling import hammer_bundle from datashader.pandas import pandas_pipeline from datashader.dask import dask_pipeline from datashape.dispatch import dispatch from datashape import discover as dsdiscover +try: + from datashader.bundling import hammer_bundle +except: + hammer_bundle = object + from ..core import (Operation, Element, Dimension, NdOverlay, CompositeOverlay, Dataset) from ..core.data import PandasInterface, DaskInterface, XArrayInterface from ..core.sheetcoords import BoundingBox from ..core.util import get_param_values, basestring -from ..element import Image, Path, Curve, Contours, RGB +from ..element import Image, Path, Curve, Contours, RGB, Graph from ..streams import RangeXY, PlotSize from ..plotting.util import fire @@ -163,9 +167,11 @@ def get_agg_data(cls, obj, category=None): xarray Dataset that can be aggregated. """ paths = [] + if isinstance(obj, Graph): + obj = obj.edgepaths kdims = list(obj.kdims) vdims = list(obj.vdims) - dims = obj.dimensions(label=True)[:2] + dims = obj.dimensions()[:2] if isinstance(obj, Path): glyph = 'line' for p in obj.data: @@ -215,10 +221,10 @@ def get_agg_data(cls, obj, category=None): df[category] = df[category].astype('category') for d in (x, y): - if df[d].dtype.kind == 'M': + if df[d.name].dtype.kind == 'M': param.main.warning('Casting %s dimension data to integer; ' 'datashader cannot process datetime data', d) - df[d] = df[d].astype('int64') / 1000000. + df[d.name] = df[d.name].astype('int64') / 1000000. return x, y, Dataset(df, kdims=kdims, vdims=vdims), glyph @@ -321,11 +327,11 @@ def _process(self, element, key=None): name = column vdims = [element.get_dimension(column)(name) if column else Dimension('Count')] - params = dict(get_param_values(element), kdims=element.dimensions()[:2], + params = dict(get_param_values(element), kdims=[x, y], datatype=['xarray'], vdims=vdims) dfdata = PandasInterface.as_dframe(data) - agg = getattr(cvs, glyph)(dfdata, x, y, self.p.aggregator) + agg = getattr(cvs, glyph)(dfdata, x.name, y.name, self.p.aggregator) if 'x_axis' in agg and 'y_axis' in agg: agg = agg.rename({'x_axis': x, 'y_axis': y}) From 17d8a76348db756ac8feb9f5ea1ad89c7ae6d8d7 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 16:13:34 +0100 Subject: [PATCH 35/60] Updated network graphs user guide --- examples/user_guide/Network_Graphs.ipynb | 41 +++++++++++------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb index 0716c154d3..b2e71e5a21 100644 --- a/examples/user_guide/Network_Graphs.ipynb +++ b/examples/user_guide/Network_Graphs.ipynb @@ -22,17 +22,17 @@ "source": [ "Visualizing and working with network graphs is a common problem in many different disciplines. HoloViews provides the ability to represent and visualize graphs very simply and easily with facilities for interactively exploring the nodes and edges of the graph, especially using the bokeh plotting interface.\n", "\n", - "The ``Graph`` ``Element`` differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes, on its own this abstract graph cannot be visualized. In order to visualize it we need to give each node in the ``Graph`` a concrete ``x`` and ``y`` position in form of the ``Nodes``. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``NodePaths``, providing explicit coordinates for each edge to follow.\n", + "The ``Graph`` ``Element`` differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes, on its own this abstract graph cannot be visualized. In order to visualize it we need to give each node in the ``Graph`` a concrete ``x`` and ``y`` position in form of the ``Nodes``. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``EdgePaths``, providing explicit coordinates for each edge to follow.\n", "\n", "To summarize a ``Graph`` consists of three different components:\n", "\n", "* The ``Graph`` itself holds the abstract edges stored as a table of node indices.\n", "* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``. The ``Nodes`` may also define any number of value dimensions, which can be revealed when hovering over the nodes or to color the nodes by.\n", - "* The ``NodePaths`` can optionally be supplied to declare explicit node paths.\n", + "* The ``EdgePaths`` can optionally be supplied to declare explicit node paths.\n", "\n", "#### A simple Graph\n", "\n", - "Let's start by declaring a very simple graph laid out in a circle:" + "Let's start by declaring a very simple graph connecting one node to all others. If we simply supply the abstract connectivity of the ``Graph``, it will automatically compute a layout for the nodes using the ``layout_nodes`` operation, which defaults to a circular layout:" ] }, { @@ -41,21 +41,15 @@ "metadata": {}, "outputs": [], "source": [ + "# Declare abstract edges\n", "N = 8\n", "node_indices = np.arange(N)\n", - "\n", - "# Declare abstract edges\n", - "start = np.zeros(N)\n", - "end = node_indices\n", - "\n", - "### start of layout code\n", - "circ = np.pi/N*node_indices*2\n", - "x = np.cos(circ)\n", - "y = np.sin(circ)\n", + "source = np.zeros(N)\n", + "target = node_indices\n", "\n", "padding = dict(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", "\n", - "simple_graph = hv.Graph(((start, end), (x, y, node_indices))).redim.range(**padding)\n", + "simple_graph = hv.Graph(((source, target),)).redim.range(**padding)\n", "simple_graph" ] }, @@ -65,7 +59,7 @@ "source": [ "#### Accessing the nodes and edges\n", "\n", - "We can easily access the ``Nodes`` and ``NodePaths`` on the ``Graph`` element using the corresponding properties:" + "We can easily access the ``Nodes`` and ``EdgePaths`` on the ``Graph`` element using the corresponding properties:" ] }, { @@ -74,7 +68,7 @@ "metadata": {}, "outputs": [], "source": [ - "simple_graph.nodes + simple_graph.nodepaths" + "simple_graph.nodes + simple_graph.edgepaths" ] }, { @@ -95,12 +89,14 @@ "def bezier(start, end, control, steps=np.linspace(0, 1, 100)):\n", " return (1-steps)**2*start + 2*(1-steps)*steps*control+steps**2*end\n", "\n", + "x, y = simple_graph.nodes.array([0, 1]).T\n", + "\n", "paths = []\n", "for node_index in node_indices:\n", " ex, ey = x[node_index], y[node_index]\n", " paths.append(np.column_stack([bezier(x[0], ex, 0), bezier(y[0], ey, 0)]))\n", " \n", - "bezier_graph = hv.Graph(((start, end), (x, y, node_indices), paths)).redim.range(**padding)\n", + "bezier_graph = hv.Graph(((source, target), (x, y, node_indices), paths)).redim.range(**padding)\n", "bezier_graph" ] }, @@ -163,8 +159,8 @@ "outputs": [], "source": [ "%%opts Graph [tools=['hover'] color_index='Type'] (cmap='Set1')\n", - "nodes = hv.Nodes((x, y, node_indices, ['Output']+['Input']*(N-1)), vdims=['Type'])\n", - "hv.Graph(((start, end), nodes, paths)).redim.range(**padding)" + "nodes = hv.Nodes((x, y, node_indices, ['Output']+['Input']*(N-1)), vdims=['Type'])\n", + "hv.Graph(((source, target), nodes, paths)).redim.range(**padding)" ] }, { @@ -190,7 +186,7 @@ "%%opts Graph [tools=['hover']]\n", "import networkx as nx\n", "G = nx.karate_club_graph()\n", - "hv.Graph.from_networkx(G, nx.layout.circular_layout).redim.range(x=(-1.2, 1.2), y=(-1.2, 1.2))" + "hv.Graph.from_networkx(G, nx.layout.circular_layout).redim.range(**padding)" ] }, { @@ -211,7 +207,7 @@ " nx.layout.spring_layout, nx.layout.random_layout]\n", "\n", "G = nx.karate_club_graph()\n", - "hv.HoloMap({l.__name__[:-7]: hv.Graph.from_networkx(G, l) for l in layouts},\n", + "hv.HoloMap({l.__name__: hv.Graph.from_networkx(G, l) for l in layouts},\n", " kdims=['Layout'])" ] }, @@ -300,7 +296,7 @@ "outputs": [], "source": [ "%%opts Nodes [color_index='circle'] (size=10 cmap=colors) Overlay [show_legend=False]\n", - "datashade(bundled.nodepaths, width=800, height=800) * bundled.nodes" + "datashade(bundled, width=800, height=800) * bundled.nodes" ] }, { @@ -324,8 +320,7 @@ "outputs": [], "source": [ "%%opts Graph (node_fill_color='white')\n", - "bundled_paths = bundle_graph(fb_graph, split=False).nodepaths\n", - "datashade(bundled_paths, width=800, height=800) * bundled.select(circle='circle15')" + "datashade(bundle_graph(fb_graph, split=False), width=800, height=800) * bundled.select(circle='circle15')" ] } ], From 9b973f2b6b4f35a434e4e52ccea65306048b9939 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 16:14:13 +0100 Subject: [PATCH 36/60] Added graph sample data --- examples/assets/fb_edges.csv | 2520 ++++++++++++++++++++++++++++++++++ examples/assets/fb_nodes.csv | 334 +++++ 2 files changed, 2854 insertions(+) create mode 100644 examples/assets/fb_edges.csv create mode 100644 examples/assets/fb_nodes.csv diff --git a/examples/assets/fb_edges.csv b/examples/assets/fb_edges.csv new file mode 100644 index 0000000000..6ec798375a --- /dev/null +++ b/examples/assets/fb_edges.csv @@ -0,0 +1,2520 @@ +start,end +236,186 +236,84 +236,62 +236,142 +236,252 +236,169 +236,280 +236,257 +236,297 +236,303 +236,105 +236,276 +236,272 +236,88 +236,271 +236,13 +236,69 +236,133 +236,30 +236,121 +236,21 +236,26 +236,304 +236,122 +236,224 +236,314 +236,315 +236,213 +236,67 +236,318 +236,322 +236,141 +236,25 +236,1 +236,200 +236,248 +186,88 +186,213 +186,341 +186,272 +186,25 +186,109 +186,55 +186,199 +186,223 +186,178 +186,345 +186,9 +186,277 +186,325 +186,67 +186,59 +186,123 +186,203 +186,45 +186,104 +186,62 +186,285 +186,200 +186,170 +186,21 +186,128 +186,221 +186,252 +186,239 +186,322 +186,188 +186,323 +186,142 +186,222 +186,56 +186,26 +186,98 +186,331 +186,303 +186,122 +186,113 +186,271 +122,285 +122,322 +122,345 +122,323 +122,200 +122,5 +122,248 +122,141 +122,274 +122,276 +122,284 +122,98 +122,224 +122,142 +122,3 +122,128 +122,119 +122,45 +122,26 +122,56 +122,239 +122,280 +122,123 +122,136 +122,31 +122,169 +122,156 +122,203 +122,261 +122,25 +122,55 +122,303 +122,304 +122,213 +122,252 +122,332 +122,104 +122,272 +122,170 +122,325 +122,342 +122,161 +122,344 +122,235 +122,188 +122,271 +122,62 +122,21 +122,109 +122,277 +122,176 +122,113 +122,297 +122,315 +122,251 +122,9 +122,66 +122,232 +122,67 +122,281 +285,103 +285,232 +285,146 +285,246 +285,188 +285,136 +285,200 +285,271 +285,60 +285,213 +285,98 +285,170 +285,323 +285,208 +285,119 +285,315 +285,313 +285,345 +285,272 +285,142 +285,221 +285,164 +285,117 +285,277 +285,303 +285,203 +285,185 +285,196 +285,211 +285,304 +285,9 +285,109 +285,25 +285,252 +285,56 +285,342 +285,261 +285,10 +285,67 +285,322 +285,113 +285,26 +285,239 +285,332 +24,346 +24,302 +24,53 +24,80 +24,187 +24,249 +24,299 +24,92 +24,266 +24,180 +24,194 +24,94 +24,101 +24,57 +24,242 +346,53 +346,1 +346,92 +346,272 +346,184 +346,194 +346,94 +346,204 +346,299 +346,320 +346,196 +346,101 +346,127 +346,300 +346,187 +346,130 +346,249 +346,254 +346,302 +346,80 +346,57 +346,242 +346,180 +346,330 +346,266 +271,304 +271,169 +271,82 +271,252 +271,188 +271,185 +271,277 +271,98 +271,261 +271,30 +271,232 +271,203 +271,133 +271,79 +271,59 +271,21 +271,103 +271,172 +271,332 +271,142 +271,119 +271,25 +271,113 +271,223 +271,128 +271,85 +271,276 +271,322 +271,40 +271,26 +271,141 +271,331 +271,272 +271,170 +271,67 +271,161 +271,200 +271,199 +271,176 +271,298 +271,313 +271,239 +271,211 +271,297 +271,325 +271,265 +271,109 +271,9 +271,224 +271,315 +271,342 +271,13 +271,118 +271,280 +271,323 +271,268 +271,314 +271,290 +271,148 +271,291 +271,48 +271,318 +271,104 +271,238 +271,212 +271,72 +271,56 +271,63 +304,199 +304,30 +304,168 +304,249 +304,325 +304,158 +304,212 +304,322 +304,21 +304,324 +304,308 +304,172 +304,170 +304,188 +304,161 +304,9 +304,272 +304,280 +304,224 +304,203 +304,109 +304,257 +304,69 +304,334 +304,136 +304,98 +304,141 +304,26 +304,75 +304,134 +304,56 +304,67 +304,239 +304,40 +304,200 +304,118 +304,341 +304,113 +304,332 +304,66 +304,277 +304,252 +304,211 +304,7 +304,347 +304,84 +304,13 +304,246 +304,31 +304,315 +176,9 +176,170 +176,85 +176,128 +176,290 +176,25 +176,239 +176,119 +176,26 +176,148 +176,188 +9,128 +9,105 +9,252 +9,203 +9,322 +9,323 +9,258 +9,75 +9,26 +9,276 +9,231 +9,21 +9,341 +9,67 +9,30 +9,272 +9,56 +9,142 +9,334 +9,297 +9,85 +9,199 +9,280 +9,161 +9,200 +9,119 +9,156 +9,66 +9,188 +9,224 +9,170 +9,342 +9,295 +9,113 +9,79 +9,232 +9,72 +9,169 +9,134 +9,25 +9,329 +9,315 +9,277 +9,148 +9,69 +9,185 +9,141 +9,3 +9,133 +9,291 +130,329 +130,48 +130,300 +130,322 +130,204 +130,73 +130,88 +130,25 +130,257 +130,53 +130,62 +130,191 +130,213 +130,277 +329,324 +329,291 +329,332 +329,184 +329,269 +329,119 +329,272 +329,150 +329,197 +329,54 +329,106 +329,30 +329,40 +329,204 +329,315 +329,322 +329,16 +329,27 +329,308 +329,202 +329,320 +329,172 +329,178 +329,284 +329,280 +329,72 +329,231 +204,213 +204,196 +204,5 +204,94 +204,92 +204,242 +204,53 +204,194 +204,249 +204,266 +204,57 +204,330 +204,101 +204,80 +204,254 +204,180 +204,302 +204,277 +213,57 +213,249 +213,7 +213,340 +213,40 +213,315 +213,291 +213,87 +213,178 +213,246 +213,345 +213,272 +213,324 +213,96 +213,53 +213,224 +213,128 +213,67 +213,277 +213,161 +213,308 +213,30 +213,5 +213,332 +213,136 +213,231 +213,322 +213,80 +213,239 +213,242 +213,56 +213,303 +252,332 +252,334 +252,291 +252,26 +252,248 +252,338 +252,239 +252,72 +252,56 +252,109 +252,134 +252,250 +252,113 +252,119 +252,161 +252,325 +252,29 +252,200 +252,13 +252,172 +252,65 +252,280 +252,25 +252,103 +252,59 +252,123 +252,322 +252,67 +252,82 +252,265 +252,128 +252,315 +252,199 +252,297 +252,313 +252,212 +252,324 +252,79 +252,98 +252,277 +252,308 +252,66 +252,272 +252,31 +252,223 +252,118 +252,188 +252,104 +252,345 +252,142 +252,169 +252,21 +252,261 +252,55 +252,342 +252,238 +252,170 +332,203 +332,248 +332,13 +332,104 +332,98 +332,40 +332,38 +332,25 +332,119 +332,67 +332,291 +332,280 +332,158 +332,56 +332,331 +332,21 +332,277 +332,324 +332,53 +332,224 +332,208 +332,200 +332,77 +332,16 +332,169 +332,170 +332,231 +332,323 +332,109 +332,10 +332,26 +332,172 +332,142 +332,48 +332,106 +82,65 +82,169 +82,231 +82,211 +82,238 +82,203 +82,331 +82,158 +82,118 +82,56 +82,84 +82,148 +82,13 +82,119 +82,314 +82,170 +82,199 +82,59 +82,261 +82,121 +82,172 +82,268 +82,313 +82,123 +82,98 +82,325 +82,67 +82,16 +82,29 +82,222 +82,318 +65,314 +65,261 +65,203 +65,7 +65,118 +65,297 +65,25 +65,339 +65,13 +276,26 +276,25 +276,239 +276,62 +276,98 +276,200 +276,322 +276,224 +276,56 +276,232 +276,141 +276,133 +276,84 +26,277 +26,56 +26,297 +26,134 +26,3 +26,334 +26,109 +26,257 +26,21 +26,158 +26,199 +26,313 +26,118 +26,265 +26,203 +26,72 +26,308 +26,232 +26,248 +26,128 +26,69 +26,113 +26,239 +26,161 +26,169 +26,104 +26,188 +26,325 +26,315 +26,55 +26,67 +26,119 +26,40 +26,298 +26,200 +26,123 +26,295 +26,98 +26,79 +26,280 +26,141 +26,291 +26,170 +26,133 +26,322 +26,62 +26,172 +26,224 +26,185 +26,13 +26,212 +26,223 +26,66 +26,25 +26,142 +26,323 +280,272 +280,1 +280,291 +280,3 +280,141 +280,98 +280,188 +280,121 +280,231 +280,290 +280,257 +280,40 +280,297 +280,119 +280,67 +280,185 +280,105 +280,315 +280,25 +280,153 +280,142 +280,239 +280,200 +280,21 +280,277 +280,322 +280,232 +280,133 +280,323 +280,117 +280,169 +280,31 +280,39 +272,159 +272,203 +272,21 +272,105 +272,108 +272,56 +272,291 +272,211 +272,67 +272,197 +272,297 +272,257 +272,185 +272,98 +272,277 +272,72 +272,148 +272,320 +272,25 +272,40 +272,281 +272,127 +272,75 +272,156 +272,258 +272,239 +272,79 +272,188 +272,184 +272,178 +272,141 +272,85 +211,199 +211,325 +211,239 +211,98 +211,203 +211,331 +211,118 +211,172 +211,56 +211,128 +211,13 +211,342 +211,222 +211,238 +211,261 +211,103 +211,29 +211,315 +211,223 +211,59 +211,313 +211,268 +211,170 +211,265 +199,222 +199,40 +199,265 +199,118 +199,212 +199,261 +199,119 +199,314 +199,98 +199,203 +199,313 +199,315 +199,238 +199,200 +199,141 +199,325 +199,239 +199,172 +199,56 +199,128 +199,223 +199,113 +199,134 +199,25 +199,291 +199,324 +199,161 +199,62 +199,322 +199,67 +199,232 +199,342 +199,298 +199,231 +199,72 +199,288 +199,48 +199,13 +84,224 +84,51 +84,237 +84,25 +84,83 +84,265 +84,313 +84,31 +133,62 +133,188 +133,96 +133,40 +133,1 +133,141 +133,315 +133,232 +133,183 +133,224 +133,322 +62,239 +62,200 +62,161 +62,323 +62,56 +62,67 +62,142 +62,322 +62,170 +62,223 +62,141 +62,341 +62,318 +62,98 +62,261 +62,224 +62,96 +239,172 +239,344 +239,238 +239,141 +239,223 +239,21 +239,56 +239,118 +239,295 +239,212 +239,234 +239,323 +239,325 +239,203 +239,313 +239,232 +239,59 +239,322 +239,188 +239,119 +239,55 +239,109 +239,265 +239,67 +239,169 +239,261 +239,60 +239,13 +239,25 +239,123 +239,66 +239,113 +239,40 +239,277 +239,338 +239,200 +239,170 +239,72 +239,104 +239,231 +239,303 +239,98 +239,142 +172,342 +172,308 +172,232 +172,141 +172,98 +172,313 +172,331 +172,277 +172,238 +172,13 +172,159 +172,223 +172,325 +172,29 +172,212 +172,118 +172,261 +172,113 +172,231 +172,72 +172,103 +172,265 +172,16 +172,128 +172,203 +172,56 +172,40 +172,298 +172,59 +172,268 +322,53 +322,207 +322,25 +322,104 +322,290 +322,142 +322,48 +322,203 +322,45 +322,136 +322,56 +322,128 +322,60 +322,1 +322,161 +322,126 +322,85 +322,323 +322,341 +322,185 +322,118 +322,303 +322,297 +322,246 +322,315 +322,30 +322,109 +322,221 +322,88 +322,308 +322,21 +322,158 +322,178 +322,257 +322,75 +322,7 +322,242 +322,250 +322,339 +322,67 +322,345 +322,170 +322,224 +322,87 +322,260 +322,188 +322,232 +322,277 +322,38 +322,40 +322,342 +322,200 +322,169 +53,299 +53,1 +53,48 +53,196 +53,94 +53,260 +53,266 +53,254 +53,248 +53,242 +53,191 +53,54 +53,317 +53,330 +53,101 +53,249 +53,180 +53,302 +53,146 +53,315 +53,194 +53,88 +53,92 +3,170 +3,283 +3,85 +3,228 +3,200 +3,25 +3,72 +3,142 +3,188 +3,323 +3,67 +3,274 +170,56 +170,223 +170,274 +170,142 +170,119 +170,290 +170,200 +170,21 +170,250 +170,188 +170,315 +170,124 +170,128 +170,55 +170,342 +170,268 +170,203 +170,260 +170,277 +170,313 +170,185 +170,85 +170,98 +170,75 +170,67 +170,25 +170,323 +170,341 +170,334 +175,46 +175,278 +175,263 +175,19 +175,143 +175,321 +175,99 +175,23 +175,68 +175,177 +175,277 +175,86 +175,296 +175,225 +175,102 +175,227 +46,177 +46,278 +46,143 +56,67 +56,134 +56,109 +56,104 +56,88 +56,141 +56,265 +56,185 +56,128 +56,21 +56,297 +56,238 +56,30 +56,103 +56,132 +56,59 +56,55 +56,232 +56,161 +56,75 +56,331 +56,66 +56,290 +56,207 +56,119 +56,169 +56,221 +56,342 +56,136 +56,203 +56,72 +56,188 +56,334 +56,298 +56,60 +56,212 +56,341 +56,222 +56,313 +56,277 +56,231 +56,142 +56,315 +56,40 +56,118 +56,200 +56,291 +56,303 +56,323 +56,325 +56,63 +56,25 +56,13 +56,223 +56,98 +56,261 +56,113 +254,194 +254,302 +254,187 +254,101 +254,249 +254,330 +254,94 +254,266 +254,299 +254,242 +254,180 +254,196 +254,92 +194,242 +194,299 +194,196 +194,92 +194,80 +194,1 +194,266 +194,302 +194,101 +194,180 +194,249 +194,94 +194,187 +231,117 +231,315 +231,178 +231,232 +231,25 +231,40 +231,29 +231,106 +231,291 +231,77 +117,77 +117,38 +127,135 +127,197 +127,284 +127,36 +127,159 +127,320 +127,21 +127,281 +127,108 +127,251 +127,139 +127,309 +127,184 +135,36 +135,309 +135,281 +135,197 +135,184 +135,251 +135,284 +135,320 +103,339 +103,323 +103,169 +103,185 +103,136 +103,98 +103,25 +103,7 +103,200 +188,119 +188,274 +188,200 +188,13 +188,104 +188,315 +188,323 +188,25 +188,67 +188,85 +188,165 +188,208 +188,291 +188,297 +188,30 +188,342 +188,156 +188,258 +188,341 +188,277 +188,79 +188,334 +188,21 +188,75 +188,290 +188,142 +188,224 +188,223 +188,113 +188,185 +23,116 +23,83 +23,230 +23,227 +23,61 +23,144 +23,245 +23,190 +23,240 +23,99 +23,267 +23,51 +23,237 +23,155 +23,124 +116,14 +116,149 +116,326 +116,312 +116,17 +116,214 +116,140 +116,226 +116,115 +116,41 +116,144 +116,343 +116,28 +116,162 +116,2 +73,299 +73,126 +73,1 +73,88 +73,331 +73,48 +73,30 +73,25 +299,101 +299,48 +299,94 +299,187 +299,1 +299,180 +299,92 +299,249 +299,80 +299,330 +299,300 +299,242 +299,302 +288,315 +288,25 +315,345 +315,1 +315,303 +315,80 +315,158 +315,185 +315,249 +315,257 +315,291 +315,87 +315,313 +315,94 +315,246 +315,203 +315,323 +315,98 +315,128 +315,121 +315,169 +315,5 +315,302 +315,40 +315,324 +315,132 +315,223 +315,105 +315,136 +315,339 +315,277 +315,67 +315,142 +315,224 +315,21 +315,7 +119,323 +119,339 +119,48 +119,109 +119,171 +119,342 +119,54 +119,39 +119,318 +119,185 +119,313 +119,148 +119,113 +119,217 +119,67 +119,25 +119,125 +119,208 +119,269 +119,21 +119,324 +119,128 +119,64 +119,325 +119,248 +119,132 +119,13 +119,50 +119,1 +119,189 +119,150 +119,331 +119,27 +119,163 +119,229 +119,203 +119,98 +119,277 +119,297 +119,100 +119,79 +119,105 +119,146 +119,261 +119,200 +323,25 +323,142 +323,85 +323,248 +323,141 +323,96 +323,290 +323,200 +323,100 +323,67 +323,297 +323,341 +323,308 +323,75 +323,258 +323,274 +323,21 +323,277 +323,10 +323,109 +48,57 +48,320 +48,80 +48,126 +48,30 +48,88 +48,54 +48,1 +48,203 +48,180 +48,330 +48,302 +57,242 +57,92 +57,249 +57,302 +57,80 +57,277 +57,184 +57,108 +57,320 +200,98 +200,123 +200,109 +200,277 +200,55 +200,40 +200,39 +200,30 +200,232 +200,224 +200,75 +200,203 +200,72 +200,118 +200,67 +200,104 +200,45 +200,31 +200,297 +200,325 +200,248 +200,113 +200,25 +200,223 +200,165 +200,185 +200,324 +200,79 +200,334 +200,274 +200,142 +200,10 +200,21 +98,21 +98,265 +98,223 +98,331 +98,148 +98,325 +98,203 +98,104 +98,123 +98,113 +98,13 +98,342 +98,261 +98,313 +98,128 +98,59 +98,277 +98,141 +98,25 +98,118 +98,258 +98,291 +98,67 +98,109 +98,238 +313,63 +313,265 +313,146 +313,21 +313,54 +313,203 +313,222 +313,128 +313,60 +313,261 +313,13 +313,342 +313,339 +313,118 +313,113 +313,314 +313,67 +313,325 +313,39 +313,223 +313,238 +63,261 +63,342 +344,185 +344,212 +344,257 +344,104 +344,67 +344,311 +67,141 +67,345 +67,212 +67,342 +67,69 +67,142 +67,196 +67,55 +67,45 +67,297 +67,109 +67,104 +67,136 +67,203 +67,123 +67,290 +67,75 +67,248 +67,265 +67,156 +67,113 +67,224 +67,325 +67,66 +67,134 +67,128 +67,13 +67,277 +67,72 +67,21 +67,79 +67,25 +67,303 +67,261 +67,185 +67,10 +67,158 +67,223 +67,169 +67,308 +67,324 +67,334 +67,118 +67,161 +67,31 +67,40 +118,325 +118,109 +118,134 +118,261 +118,283 +118,13 +118,161 +118,324 +118,268 +118,331 +118,29 +118,222 +118,342 +118,203 +118,277 +118,16 +118,265 +118,238 +118,59 +325,109 +325,261 +325,104 +325,21 +325,161 +325,222 +325,13 +325,265 +325,277 +325,59 +325,311 +325,203 +325,113 +325,268 +325,223 +325,40 +325,158 +325,342 +325,25 +325,238 +277,308 +277,31 +277,248 +277,39 +277,13 +277,105 +277,161 +277,324 +277,50 +277,142 +277,21 +277,55 +277,22 +277,120 +277,334 +277,159 +277,25 +277,342 +277,169 +277,291 +277,339 +277,208 +277,294 +277,132 +277,113 +277,40 +277,311 +277,10 +277,109 +277,290 +277,104 +277,168 +277,203 +277,134 +134,21 +134,132 +134,66 +134,142 +134,203 +134,238 +134,113 +134,334 +134,248 +270,76 +270,203 +270,25 +76,25 +36,251 +36,309 +36,320 +36,284 +36,197 +36,108 +36,184 +36,281 +223,203 +223,261 +223,21 +223,13 +223,265 +223,342 +223,246 +223,128 +274,21 +274,341 +274,96 +274,185 +274,75 +274,40 +274,85 +88,21 +88,126 +88,178 +88,164 +88,1 +88,106 +88,80 +88,303 +88,25 +88,336 +88,242 +21,212 +21,55 +21,66 +21,203 +21,169 +21,284 +21,297 +21,184 +21,108 +21,235 +21,161 +21,342 +21,334 +21,142 +21,105 +21,308 +21,232 +21,248 +21,136 +21,121 +21,251 +21,109 +21,281 +21,165 +21,104 +21,39 +21,345 +21,197 +21,40 +21,159 +21,13 +21,31 +21,25 +21,123 +339,168 +339,38 +339,136 +339,340 +339,161 +339,31 +339,22 +339,334 +339,246 +339,129 +339,87 +339,158 +339,148 +339,146 +339,152 +339,260 +339,7 +339,291 +339,347 +108,197 +108,284 +108,281 +108,251 +108,320 +108,184 +108,159 +197,139 +197,251 +197,320 +197,284 +197,281 +197,309 +197,159 +197,184 +169,136 +169,185 +169,40 +169,25 +169,257 +169,10 +169,248 +169,334 +169,106 +169,318 +169,125 +169,291 +169,104 +169,165 +169,5 +169,142 +169,109 +169,128 +169,113 +169,121 +275,273 +275,218 +275,328 +275,195 +275,78 +275,181 +275,306 +275,152 +275,4 +273,78 +273,328 +273,218 +273,181 +273,195 +273,4 +273,306 +83,25 +83,31 +83,51 +83,237 +28,312 +28,310 +28,144 +28,115 +28,151 +28,226 +28,326 +28,14 +28,140 +28,41 +28,149 +312,137 +312,243 +312,149 +312,326 +312,19 +312,111 +312,115 +312,262 +312,14 +312,44 +312,226 +312,305 +312,220 +312,20 +312,17 +312,2 +312,140 +312,343 +312,214 +312,162 +312,151 +312,41 +312,144 +242,92 +242,330 +242,266 +242,249 +242,180 +242,158 +242,80 +242,101 +242,132 +242,302 +242,94 +242,187 +214,20 +214,326 +214,137 +214,19 +214,41 +214,111 +214,140 +214,14 +214,115 +214,289 +214,333 +214,17 +214,230 +214,343 +20,333 +20,149 +20,14 +20,2 +20,44 +20,115 +20,226 +20,41 +20,111 +20,162 +20,326 +20,343 +307,71 +307,230 +307,40 +71,230 +333,2 +333,149 +333,343 +333,226 +333,162 +168,31 +168,347 +168,129 +168,7 +168,22 +168,291 +168,158 +308,161 +308,31 +308,142 +308,246 +308,203 +308,338 +308,7 +308,87 +308,136 +308,22 +308,334 +308,66 +341,203 +128,150 +128,104 +128,265 +128,248 +128,334 +128,203 +334,113 +334,123 +334,132 +334,40 +334,165 +334,142 +334,203 +334,229 +334,50 +334,318 +334,66 +238,265 +238,342 +238,222 +238,13 +238,203 +238,324 +238,59 +238,261 +238,106 +265,40 +265,311 +265,203 +265,298 +265,331 +265,342 +265,13 +265,261 +265,248 +141,224 +141,72 +141,257 +141,38 +141,258 +141,39 +141,291 +141,25 +141,40 +78,218 +78,328 +78,306 +78,181 +78,195 +78,4 +345,159 +345,142 +345,178 +345,261 +345,303 +345,45 +317,158 +317,132 +317,40 +317,101 +317,146 +158,40 +158,142 +158,25 +158,232 +158,109 +158,132 +158,22 +158,291 +158,113 +158,248 +158,5 +158,60 +38,178 +38,248 +38,7 +302,266 +302,94 +302,249 +302,187 +302,330 +302,101 +302,92 +302,180 +27,54 +27,324 +54,1 +139,159 +139,320 +139,281 +139,184 +139,251 +139,284 +109,331 +109,303 +109,324 +109,148 +109,142 +109,203 +109,229 +109,104 +109,295 +109,31 +109,314 +109,50 +109,13 +109,297 +291,10 +291,25 +291,224 +291,72 +291,31 +291,258 +291,7 +291,208 +291,60 +291,129 +291,87 +291,232 +291,113 +291,248 +142,161 +142,66 +142,10 +142,113 +142,31 +142,104 +142,303 +142,45 +142,297 +142,40 +142,123 +142,203 +203,25 +203,59 +203,161 +203,55 +203,297 +203,331 +203,338 +203,268 +203,261 +203,342 +203,104 +203,50 +203,113 +203,29 +203,40 +203,324 +105,257 +105,69 +105,148 +105,25 +105,39 +232,224 +232,25 +232,212 +232,40 +232,298 +232,72 +64,217 +64,171 +64,150 +64,189 +64,100 +217,324 +217,189 +217,100 +217,171 +217,150 +248,311 +126,260 +126,1 +224,261 +224,96 +224,40 +224,148 +224,25 +224,30 +224,324 +261,16 +261,113 +261,268 +261,13 +261,318 +261,123 +261,59 +261,297 +261,314 +261,342 +261,55 +261,148 +283,25 +283,228 +144,226 +144,326 +144,14 +144,343 +144,151 +144,17 +144,41 +144,115 +144,149 +144,140 +226,343 +226,149 +226,115 +226,14 +226,2 +226,41 +226,326 +290,25 +290,185 +290,342 +25,94 +25,257 +25,104 +25,148 +25,69 +25,72 +25,297 +25,113 +25,185 +25,336 +25,51 +25,40 +25,39 +25,79 +25,31 +25,237 +25,331 +25,221 +25,246 +342,59 +342,123 +342,314 +342,113 +342,13 +342,165 +146,50 +146,136 +146,148 +300,94 +300,80 +300,320 +94,330 +94,80 +94,320 +94,92 +94,180 +94,101 +94,187 +94,249 +94,266 +1,92 +184,159 +184,284 +184,320 +184,281 +184,309 +184,251 +159,284 +159,281 +159,320 +149,343 +149,326 +149,162 +149,115 +149,2 +149,14 +13,148 +13,59 +59,250 +59,268 +17,326 +17,310 +17,140 +17,41 +17,111 +17,19 +17,137 +17,115 +326,343 +326,140 +326,162 +326,115 +326,41 +326,137 +326,14 +326,2 +326,111 +80,187 +80,330 +80,320 +80,101 +80,266 +80,180 +80,92 +80,249 +80,173 +187,249 +187,101 +187,92 +187,180 +187,5 +187,266 +161,87 +161,258 +161,39 +161,314 +66,113 +31,237 +31,51 +31,338 +31,129 +31,7 +136,7 +136,125 +136,156 +136,87 +136,324 +136,120 +136,246 +7,246 +7,129 +7,22 +7,340 +7,347 +7,87 +255,49 +49,192 +49,241 +320,309 +320,281 +320,251 +320,284 +85,156 +85,258 +85,75 +246,87 +246,340 +123,104 +123,318 +123,55 +284,251 +284,281 +284,309 +140,137 +140,115 +140,41 +137,115 +137,167 +137,111 +137,289 +137,41 +137,310 +137,93 +137,32 +137,243 +137,337 +343,115 +343,111 +343,2 +343,162 +343,44 +343,19 +343,41 +343,14 +115,2 +115,19 +115,14 +115,262 +115,41 +115,220 +115,192 +297,185 +297,324 +185,55 +185,22 +185,79 +185,148 +104,303 +104,113 +104,318 +104,45 +104,295 +104,55 +104,212 +324,331 +324,301 +324,22 +324,150 +324,340 +171,107 +171,150 +171,189 +171,58 +111,279 +111,337 +111,310 +111,44 +111,41 +111,93 +14,151 +14,41 +14,310 +310,32 +310,151 +310,93 +310,167 +310,41 +310,337 +310,243 +32,167 +32,337 +32,93 +30,303 +30,178 +30,331 +222,240 +92,180 +92,196 +92,330 +92,266 +92,249 +92,101 +72,40 +72,165 +72,113 +72,298 +72,212 +72,132 +40,298 +40,77 +40,29 +40,212 +40,132 +40,257 +40,258 +40,113 +266,249 +266,330 +266,196 +266,101 +266,180 +212,295 +212,298 +278,177 +278,86 +278,143 +278,225 +278,99 +278,131 +278,227 +237,51 +311,132 +309,281 +309,251 +330,249 +330,196 +330,101 +230,70 +230,41 +230,97 +230,253 +16,331 +16,29 +249,196 +249,180 +249,101 +249,163 +39,121 +39,257 +39,165 +39,69 +251,281 +69,121 +113,132 +113,50 +113,148 +113,165 +113,121 +258,257 +258,295 +258,156 +258,75 +257,295 +196,180 +156,295 +156,5 +156,235 +303,45 +303,221 +303,132 +303,178 +286,81 +81,269 +174,293 +174,112 +174,19 +293,19 +33,42 +347,129 +347,22 +263,99 +263,68 +263,102 +263,227 +263,296 +338,29 +19,112 +19,138 +19,289 +19,319 +19,227 +19,89 +19,41 +218,328 +218,181 +218,195 +218,4 +218,306 +314,50 +314,96 +5,235 +5,180 +5,87 +5,316 +178,206 +192,205 +192,52 +243,41 +243,216 +243,167 +243,337 +195,181 +195,306 +195,4 +195,328 +181,152 +181,328 +181,306 +181,4 +8,245 +8,259 +8,91 +8,264 +8,201 +8,110 +8,193 +245,91 +245,259 +328,306 +328,4 +331,50 +150,100 +150,163 +150,189 +101,180 +99,296 +99,131 +99,68 +99,102 +99,225 +99,177 +99,227 +99,143 +296,102 +296,227 +296,68 +102,227 +163,173 +163,100 +173,34 +173,166 +173,202 +165,121 +121,206 +306,4 +177,143 +177,68 +177,227 +177,131 +177,225 +177,86 +87,22 +189,100 +259,201 +259,110 +259,91 +259,193 +259,264 +45,221 +45,132 +58,269 +58,107 +269,100 +167,93 +167,337 +227,225 +227,143 +227,68 +227,131 +227,86 +96,190 +91,193 +91,264 +91,110 +91,201 +143,131 +143,68 +143,225 +143,35 +143,321 +61,193 +93,337 +93,41 +41,151 +41,337 +41,44 +6,89 +6,147 +6,319 +6,219 +6,95 +89,319 +89,327 +89,95 +89,219 +89,147 +301,47 +106,29 +29,247 +110,264 +225,86 +225,68 +225,131 +152,4 +148,50 +148,229 +260,160 +260,206 +262,220 +97,182 +124,157 +90,179 +179,145 +157,155 +327,95 +327,319 +95,319 +95,147 +120,247 +190,229 +282,244 +319,219 +319,147 +233,256 +166,202 +166,198 +147,219 +77,294 +219,154 +182,253 diff --git a/examples/assets/fb_nodes.csv b/examples/assets/fb_nodes.csv new file mode 100644 index 0000000000..78e3e53e25 --- /dev/null +++ b/examples/assets/fb_nodes.csv @@ -0,0 +1,334 @@ +x,y,index,circle +0.3462312495574256,0.29464411906589866,1,circle15 +0.7546521534536059,0.9031464712829926,2,circle10 +0.2489242083808421,0.5357311209796976,3,circle15 +0.2862548102761658,0.03387849661737338,4,None +0.3858943537769982,0.2640403123623,5,circle16 +0.8759619586010459,0.1747420562577386,6,circle15 +0.5273858529278159,0.39812427523726984,7,circle15 +0.7193079912208751,0.05998793557378446,8,None +0.343527501589265,0.4889243107000379,9,circle16 +0.3785116301788025,0.37716054428683643,10,circle15 +0.42196784247328606,0.5590419002695971,13,circle11 +0.7309831938148964,0.8938265739706063,14,circle10 +0.39747426630770183,0.6435033180053608,16,circle15 +0.7724820513212469,0.8362757651434382,17,circle19 +0.8464566766826852,0.624821127559961,19,None +0.7691170308048924,0.8846837805652074,20,circle19 +0.3322074709317956,0.4514543594039226,21,circle15 +0.5331624314911364,0.3614670407388297,22,circle15 +0.7612302769573882,0.5841809927069698,23,circle15 +0.418964212621371,0.11808314421900483,24,circle16 +0.41303004499036017,0.44388746267742646,25,circle15 +0.3726146610129215,0.5118524123217272,26,circle15 +0.23172639911287032,0.32686869892958254,27,None +0.7179131306308187,0.9109003694914789,28,circle23 +0.4932395984428853,0.6400371081250465,29,circle0 +0.36197099624500556,0.3933310575705394,30,None +0.5068609692007058,0.4592421572020061,31,circle15 +0.5464437281485022,0.9736773868374481,32,circle6 +0.976186806528328,0.6349502009217107,33,circle19 +0.37181807267797923,0.0032766602859083892,34,circle16 +0.9854305567354226,0.5558709778696436,35,circle6 +0.1322977440640587,0.2474649527629982,36,circle16 +0.4776196487938295,0.5330093378584405,38,circle15 +0.28559054803813183,0.5008701966617404,39,circle15 +0.42066207775139297,0.49465371266375197,40,circle15 +0.7279893241081805,0.8370270639038601,41,circle19 +0.95363954271464,0.6915496071266286,42,circle10 +0.6978649915315035,0.931090077064285,44,circle19 +0.25910519491663186,0.47629107648301716,45,circle15 +0.9221024688841588,0.7387214408639695,46,circle7 +0.9534704617610934,0.28401644801933146,47,circle15 +0.3789855105919936,0.3147345307353087,48,circle16 +0.7925781991813007,0.8915634845988827,49,circle15 +0.5258292280198563,0.48418254982679504,50,circle15 +0.6570806109949354,0.4833364320162578,51,circle3 +0.9799398583538994,0.625279237331709,52,circle17 +0.4157541127614675,0.26883960460026735,53,None +0.3046187705690486,0.3177950218653487,54,circle11 +0.3496807337486209,0.5848145331477057,55,circle15 +0.3854471207168209,0.5202986406993984,56,circle15 +0.3362625579490046,0.23829284193510122,57,circle16 +0.048463506105569834,0.6737121066267749,58,circle16 +0.4065724576565726,0.628596994180346,59,circle4 +0.5204876241423742,0.5052327053331379,60,circle15 +0.8451804241565024,0.32713506178843577,61,circle0 +0.35988339009102077,0.4578769155290249,62,circle15 +0.3966888279192297,0.6938075779246382,63,circle15 +0.08426358584278626,0.5144766272763801,64,circle15 +0.5256355695672327,0.5294321385669157,65,circle15 +0.3953991296543274,0.5896408466922922,66,circle11 +0.37972129137197513,0.4867114680486381,67,circle15 +0.9428986797762231,0.686670394634208,68,circle13 +0.24966541191355357,0.44754717085096,69,circle15 +0.980333422553315,0.44548076934339453,70,circle11 +0.9858643766401936,0.5074496691414943,71,circle0 +0.3279743853846823,0.532896823801961,72,circle15 +0.4391453744980247,0.2882955482671383,73,circle9 +0.2585515658113963,0.507982552079408,75,None +0.6101976987247655,0.2290255894279867,76,circle11 +0.34532030181207324,0.7070555006040183,77,circle15 +0.3512419746554157,0.0060450076607966785,78,None +0.2833993312793976,0.5232566926276075,79,circle15 +0.36289149446169217,0.19761843378101177,80,circle16 +0.012760727430980192,0.5993087333483834,81,circle0 +0.43834232295476305,0.5802307115432218,82,circle15 +0.6549048053548742,0.5050742229449073,83,circle3 +0.5459389771346247,0.4751709956882926,84,circle4 +0.22952991868123052,0.49927026366530325,85,circle16 +0.937230556645661,0.7111313847433229,86,circle13 +0.4788469986830789,0.3340879724809435,87,circle15 +0.35844101118214716,0.3422100105578479,88,None +0.9027536897796365,0.30064918388892853,89,circle15 +0.9783596307961953,0.3407143387943412,90,circle17 +0.7336171792645315,0.06579583166134528,91,None +0.38428599370680644,0.14973701089139924,92,circle16 +0.6070944276741465,0.961540175731794,93,circle19 +0.3898109690297578,0.1974383333672636,94,circle16 +0.8880964669067326,0.18834933002296922,95,circle15 +0.48178605628528104,0.4006467720904519,96,circle15 +0.9617684149616877,0.38849561297204277,97,circle11 +0.40221353501267565,0.5151740623602423,98,circle15 +0.928586707798318,0.6424120919681421,99,circle2 +0.13569897502613437,0.45605376147431004,100,None +0.4498086978601929,0.14212169097709715,101,circle16 +0.921489340015995,0.7154083417751549,102,None +0.4765695847733142,0.4735650835878373,103,circle15 +0.3450097006068889,0.5433418485582759,104,circle15 +0.3210765155131092,0.3899793667907369,105,circle17 +0.4929793714339969,0.4981160375620822,106,None +0.06894293669587931,0.7301713305403414,107,None +0.20228193763951974,0.279787167895713,108,circle15 +0.42275647710890435,0.4935351788652953,109,circle15 +0.7111838525275471,0.036946090706152526,110,circle0 +0.7038043380899326,0.9055205308947787,111,circle6 +0.9432854105551257,0.4704565222890456,112,None +0.4043493686399746,0.5309620598110284,113,circle11 +0.7942485614632855,0.8284673411326761,115,circle19 +0.7677725695186962,0.8224809069927157,116,circle2 +0.3252057913613007,0.6685958454916456,117,circle15 +0.4200880129297635,0.5814046685867889,118,circle11 +0.32173222007354046,0.4726727239408485,119,circle15 +0.6079046970954136,0.5888187266441115,120,None +0.26204976574759237,0.5589606039071244,121,circle15 +0.3381592939673191,0.4607947681401084,122,circle15 +0.36533696204810634,0.6007699234866902,123,circle15 +0.5209970310264874,0.7415414373929426,124,circle15 +0.19759108095334688,0.4070800594227385,125,circle15 +0.3898367300332832,0.32115043276880667,126,circle17 +0.18985883502766973,0.28650653558973166,127,circle16 +0.35462906796051147,0.5287135323057888,128,circle15 +0.6166647013413558,0.35867799837114905,129,circle15 +0.3823968578488155,0.2926160805472478,130,None +0.9748217876151197,0.5973558185743209,131,circle13 +0.438791817367228,0.42642067959598157,132,circle0 +0.292904927316936,0.4127312740542006,133,circle15 +0.46206498704877297,0.549262633787249,134,circle11 +0.13482630763947256,0.2628842129762132,135,circle16 +0.43061784420406707,0.40321617148153893,136,circle15 +0.6821865635942534,0.8972628401921026,137,circle19 +0.9190677198900399,0.4667038543676213,138,circle13 +0.11628327465159707,0.29890649460142854,139,circle16 +0.7799080988204948,0.8667441846462167,140,circle2 +0.34938539873506136,0.4691951627368798,141,circle15 +0.38573250649055113,0.4916163674609163,142,circle15 +0.951556579265078,0.6602449283133651,143,circle13 +0.7504166799118239,0.8299023244490518,144,circle2 +0.9302556164142801,0.23153231324130605,145,circle17 +0.4910927807615955,0.35886945530084796,146,circle15 +0.8640468181144997,0.16304024211513754,147,circle2 +0.44818679808258277,0.46393674602056884,148,circle15 +0.7362374200402666,0.9120952487104015,149,circle23 +0.1749322887128082,0.46141720955535326,150,circle2 +0.6820151201528607,0.9382210120906531,151,circle11 +0.35503029496826843,0.0841876003019931,152,circle15 +0.0839495250485375,0.6303022242219772,153,None +0.6838345860589259,0.0565491543718401,154,circle9 +0.5312356220631618,0.7798770943156194,155,circle2 +0.26405543167899226,0.3917884751482729,156,circle4 +0.3215464772969035,0.7986961742244996,157,circle15 +0.46286725765609815,0.4162991529996404,158,circle11 +0.22996559783458573,0.37074132001568394,159,None +0.23604888045603192,0.8271469450000803,160,None +0.4254460225145514,0.45935369513703783,161,circle11 +0.7220691577394367,0.9269117498548455,162,circle23 +0.17951980387999975,0.29909220054764435,163,circle0 +0.254992467648062,0.22945189307525096,164,circle9 +0.29772947907448993,0.6022314268841691,165,circle11 +0.15625258609586237,0.1260654628626207,166,None +0.5666739492921056,0.9719191931470609,167,None +0.5662876982075881,0.381911701154795,168,circle15 +0.36726430908331664,0.47113658625691524,169,circle15 +0.3588058004953097,0.5522748341682788,170,circle15 +0.08846388565891403,0.5808552415240117,171,circle15 +0.39477732997302173,0.5552839178554586,172,circle17 +0.22273426635076812,0.11173341531990548,173,circle16 +0.9446613305620567,0.4960487733127337,174,circle20 +0.8345775714157767,0.6498439436305324,175,circle14 +0.28773068559241605,0.557556705300581,176,circle15 +0.9510222795214606,0.6727950660395919,177,circle18 +0.27974946123265,0.482462268771542,178,None +0.9520505893366911,0.27220370297337,179,circle17 +0.41542484105973554,0.15250661355952505,180,circle16 +0.2631093806390039,0.042628357054860976,181,None +0.9352518077435436,0.29390161213144295,182,None +0.04589778157858947,0.36289695793397625,183,circle15 +0.21409660796763816,0.2917806121915766,184,None +0.36444635657924024,0.4840093616332867,185,circle15 +0.3589061278942452,0.5083679547132434,186,circle15 +0.40243240466330626,0.11183773868178717,187,circle16 +0.3318803101892535,0.5022064440165636,188,circle15 +0.0812748202627534,0.4919493702360156,189,circle15 +0.7240234402430092,0.42899094501706136,190,circle15 +0.49995223890376644,0.09518551056264155,191,None +0.9043515032780175,0.769830030091445,192,circle6 +0.7652187702806612,0.1312995410253476,193,circle0 +0.3846707541596565,0.13661012626906707,194,circle16 +0.2950027901429243,0.0276060668143474,195,None +0.42620922582193704,0.21886438912475722,196,circle15 +0.17057301318705131,0.32769241333691657,197,circle16 +0.11856819226415197,0.1633756298146314,198,circle15 +0.4251886510734621,0.519681096237693,199,circle11 +0.3667971992646741,0.4991757941957171,200,circle15 +0.6494596990201125,0.013703551559127686,201,None +0.14985142794078557,0.18868737242615743,202,circle16 +0.4324464213044054,0.50708420647367,203,circle11 +0.3999145405730149,0.2262902094446351,204,circle9 +0.9814782773108591,0.6081923953974353,205,None +0.17531472139057966,0.6650204614097877,206,circle9 +0.20594988484505874,0.651854289751931,207,circle15 +0.282640303182845,0.57912135824134,208,circle15 +0.4463460332038537,0.5651436359512039,211,circle11 +0.33082995086960904,0.5900137682098582,212,circle11 +0.40195310043635213,0.37704789140268224,213,circle15 +0.7903672394098424,0.7960906847084839,214,circle19 +0.44156102404594033,0.9773548945331565,216,circle10 +0.141077073368584,0.4921236722717229,217,None +0.32720760084920614,0.013596648578411306,218,circle11 +0.8307508142277222,0.1465845220304805,219,circle15 +0.8326991188567988,0.8385700097430309,220,circle20 +0.23129747556369407,0.4559036802449043,221,None +0.5195429572158072,0.6301873459080816,222,circle0 +0.4375478406226887,0.5374760529563022,223,circle4 +0.4019848926728501,0.4306325546693896,224,circle15 +0.9591184554661896,0.6509516350160433,225,circle7 +0.7444939681975998,0.9019141701888109,226,None +0.9155218133546402,0.6600276660231663,227,circle14 +0.059006267125239396,0.5968413137922078,228,None +0.5782424320333843,0.46533278145978124,229,circle0 +0.8868727064902389,0.5842251132943028,230,None +0.37260604793339136,0.5606750718113701,231,None +0.34251501655418964,0.5147204144133883,232,None +0.6449878543776807,0.9586675464644734,233,circle15 +0.2088971306615856,0.7883883059309706,234,None +0.25127217162696497,0.2919227895065213,235,circle11 +0.3701458813700867,0.44258041402872644,236,circle15 +0.6614613436404796,0.46129460605644373,237,circle3 +0.4630896276081067,0.5969232266029778,238,circle11 +0.373509901476991,0.5443921953342843,239,circle4 +0.7276278258342502,0.7002029419897206,240,circle11 +0.6723187427838757,0.958939158484296,241,None +0.42157122505307437,0.23230939096320086,242,circle15 +0.5952676384477112,0.9511311144775502,243,None +1.0,0.4461318854029825,244,circle20 +0.7906107038492644,0.22837376064032103,245,circle0 +0.5009525667379007,0.38255109113352587,246,circle15 +0.632950353123357,0.7524108891715209,247,circle15 +0.4522950914303389,0.4772217541501648,248,circle15 +0.3811360565110027,0.21988885114009302,249,circle16 +0.29226645783574035,0.6798882538208212,250,circle4 +0.1543013560901321,0.33242277061066966,251,circle16 +0.40579750088779365,0.5295853787914848,252,circle11 +0.9690543017707087,0.4063055454825062,253,circle0 +0.43963167786312457,0.12315885085942861,254,circle16 +0.635531984913085,0.9698685044256642,255,circle15 +0.7755622628128837,0.8960293607026552,256,circle15 +0.2962047752803488,0.4466103874485599,257,circle4 +0.2736334677761872,0.4506026209157892,258,circle16 +0.7465850518958228,0.07330327929090999,259,circle0 +0.31335684387284085,0.5505770294883505,260,None +0.4105842221845779,0.5682555983366863,261,circle15 +0.8455585783014009,0.8214206226578487,262,circle20 +0.9340929714372334,0.699977148869377,263,None +0.6769149748466599,0.021382783909968332,264,circle0 +0.4586348948086718,0.5831006724951674,265,circle11 +0.4084278098272706,0.12538631300984535,266,circle16 +0.9451312438691682,0.5487564045599782,267,circle22 +0.4279271558842208,0.6661002098782539,268,circle4 +0.10081420069608679,0.5383325749207449,269,circle15 +0.5827115480473184,0.31677141158655897,270,circle2 +0.3854752453383763,0.519806550137449,271,circle15 +0.3196623658662247,0.4181194395235462,272,None +0.3048655954929361,0.021591326993627227,273,None +0.2744936118016464,0.469614863225024,274,circle15 +0.27482922960677575,0.036972230545308675,275,None +0.3772597894930514,0.4221807549975476,276,circle15 +0.43138104893025175,0.48362907620117596,277,circle15 +0.9617290734136381,0.640089105015965,278,circle12 +0.8173825814577107,0.8656918010087142,279,circle9 +0.3189979771687334,0.4936055519315771,280,circle15 +0.1926226193693487,0.343227385569077,281,circle16 +0.9999515608389689,0.4898780675755634,282,circle20 +0.19606117783657706,0.6114919498884686,283,None +0.19494914516237402,0.32782843746874274,284,None +0.3858914943365378,0.4518427232764108,285,circle15 +0.0,0.5435433968418709,286,circle15 +0.5832003134366176,0.40733175616441686,288,circle15 +0.8358338199417065,0.787362118233959,289,circle6 +0.31508176497121776,0.5706168431817834,290,circle15 +0.42846527937295353,0.44595564550395306,291,circle15 +0.9462527660489385,0.5193982417098519,293,circle20 +0.34805761243247085,0.7862622314493772,294,circle17 +0.23239797167985993,0.5408306900008868,295,circle4 +0.9228026675140926,0.7274704770192529,296,None +0.3991590643804465,0.4667498546046741,297,circle15 +0.365824726462827,0.6394760738443851,298,circle11 +0.39260477729047333,0.16218783658928998,299,circle16 +0.3197620245599974,0.1443660049698326,300,None +0.7341051813547544,0.28640486060851683,301,circle15 +0.41533963758495407,0.1789685473145115,302,None +0.3370991660933083,0.42365232353157584,303,circle15 +0.4164029540503648,0.46270442847471716,304,circle15 +0.5469791242314571,0.956945178666826,305,None +0.31653199750024646,0.018885166168470396,306,None +0.7746472778357737,0.5124677684115587,307,None +0.46276625224180074,0.4496323810433757,308,circle15 +0.1199816061501731,0.2759010572801268,309,circle16 +0.6516656822960815,0.9385471195418259,310,circle6 +0.4556667107200784,0.6424669612607279,311,circle11 +0.7290323522121176,0.861602992385222,312,circle19 +0.43762693584932755,0.5151392961134973,313,circle11 +0.5080371576435736,0.5462036246594856,314,circle15 +0.39719999143002255,0.4082637850973515,315,circle15 +0.4478840002447953,0.0,316,circle15 +0.5043843509768109,0.27737192626871227,317,circle15 +0.33862803348140275,0.6138571708201404,318,circle15 +0.8907682374905652,0.2933707091943504,319,circle15 +0.23602023085239868,0.25767170513819554,320,None +0.9024107464798575,0.7515983379930795,321,circle6 +0.3724150172449574,0.4522667016552154,322,circle15 +0.3341766280005634,0.475670351220716,323,circle15 +0.41407221075271383,0.4135652395062185,324,circle11 +0.4287171706383279,0.5524848623842145,325,circle15 +0.753834705667072,0.8824406695666225,326,circle19 +0.9008247091882886,0.2040829337390073,327,circle2 +0.3392333536974882,0.010921098268613944,328,circle15 +0.3020039703056031,0.40415499613553507,329,None +0.43874925299659345,0.15822650928279472,330,circle16 +0.46432917053427664,0.49796328009858837,331,circle11 +0.399233689959251,0.4830619851282844,332,circle11 +0.7662454767676474,0.9001754936096443,333,circle19 +0.4471226996447133,0.5263644041051199,334,circle0 +0.2822019348905716,0.21074313395297511,336,circle9 +0.6205933542176764,0.9603473359004893,337,circle6 +0.5414859114543875,0.5883780849150385,338,circle15 +0.4751141030968781,0.3829297961805436,339,circle15 +0.5447947204936374,0.2844053432324352,340,circle15 +0.28497826632515183,0.5407217187927555,341,circle15 +0.3875581375242716,0.5752270717616292,342,circle15 +0.7812126880670277,0.847588229270035,343,circle19 +0.28483124832763557,0.6193295204021921,344,circle4 +0.29971647161538173,0.4324462242068765,345,circle15 +0.34536845095739555,0.1958714345530404,346,None +0.60828434610196,0.32640789881836585,347,circle15 From 3e6bdf2648ee951b3f4bdb634e2ab7a7d34a173f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 16:34:59 +0100 Subject: [PATCH 37/60] Always add node index to Graph hover --- holoviews/plotting/bokeh/graphs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 19806c5a2a..31cb7c0f3e 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -53,10 +53,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): super(GraphPlot, self).initialize_plot(ranges, plot, plots) def _hover_opts(self, element): - dims = element.nodes.dimensions()[3:] - if element.nodes.dimension_values(2).dtype.kind not in 'if': - dims = [('Node', '$node')] + dims - return dims, {} + return element.nodes.dimensions()[2:], {} def get_extents(self, element, ranges): """ From 419dd7cec35844a4ec6aa353892d5c6fbeb3ec63 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 16:46:15 +0100 Subject: [PATCH 38/60] Small Graph docstring and parameter declaration improvements --- holoviews/element/graphs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 869b0cfa65..125bae5c56 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -33,6 +33,11 @@ def circular_layout(nodes): class layout_nodes(Operation): + """ + Accepts a Graph and lays out the corresponding nodes with the + supplied networkx layout function. If no layout function is + supplied uses a simple circular_layout function. + """ layout = param.Callable(default=None, doc=""" A NetworkX layout function""") @@ -64,7 +69,7 @@ class Graph(Dataset, Element2D): abstract edges, nodes, and edgepaths. """ - group = param.String(default='Graph') + group = param.String(default='Graph', constant=True) kdims = param.List(default=[Dimension('start'), Dimension('end')], bounds=(2, 2)) From ae7f180ffdf8cc9db28742ceee34f2f6cc1714d6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 17:25:31 +0100 Subject: [PATCH 39/60] Declare Node and EdgePaths group parameters constant --- holoviews/element/graphs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 125bae5c56..69be29505c 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -201,7 +201,7 @@ class Nodes(Points): kdims = param.List(default=[Dimension('x'), Dimension('y'), Dimension('index')], bounds=(3, 3)) - group = param.String(default='Nodes') + group = param.String(default='Nodes', constant=True) class EdgePaths(Path): @@ -210,4 +210,4 @@ class EdgePaths(Path): connecting nodes in a graph. """ - group = param.String(default='EdgePaths') + group = param.String(default='EdgePaths', constant=True) From 7a9b1504dfec0d3382ce3cde306cf98ddf780c9a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 15 Sep 2017 17:52:48 +0100 Subject: [PATCH 40/60] Fixed NodePaths matplotlib style --- holoviews/plotting/mpl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 72e4724efb..0633e133b1 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -260,4 +260,4 @@ def grid_selector(grid): edge_color='black', node_size=20) options.Nodes = Options('style', edgecolors='black', facecolors=Cycle(), marker='o', s=20**2) -options.Path = Options('style', color='black') +options.EdgePaths = Options('style', color='black') From dd8bd80ae2c9734e0b5e678daa24649e8d1a42cc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 00:09:17 +0100 Subject: [PATCH 41/60] Handled GraphPlot hover info correctly --- holoviews/plotting/bokeh/graphs.py | 34 +++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 31cb7c0f3e..f51961404d 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -10,7 +10,7 @@ pass from ...core.options import abbreviated_exception, SkipRendering -from ...core.util import basestring +from ...core.util import basestring, dimension_sanitizer from .chart import ColorbarPlot from .element import CompositeElementPlot, line_properties, fill_properties, property_prefixes from .util import mpl_to_bokeh, bokeh_version @@ -30,6 +30,9 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot): Determines policy for inspection of graph components, i.e. whether to highlight nodes or edges when hovering over connected edges and nodes respectively.""") + tools = param.List(default=['hover', 'tap'], doc=""" + A list of plugin tools to use on the plot.""") + # X-axis is categorical _x_range_type = Range1d @@ -50,10 +53,14 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot): def initialize_plot(self, ranges=None, plot=None, plots=None): if bokeh_version < '0.12.7': raise SkipRendering('Graph rendering requires bokeh version >=0.12.7.') - super(GraphPlot, self).initialize_plot(ranges, plot, plots) + return super(GraphPlot, self).initialize_plot(ranges, plot, plots) def _hover_opts(self, element): - return element.nodes.dimensions()[2:], {} + if self.inspection_policy == 'nodes': + dims = element.nodes.dimensions()[2:] + elif self.inspection_policy == 'edges': + dims = element.kdims+element.vdims + return dims, {} def get_extents(self, element, ranges): """ @@ -94,13 +101,8 @@ def get_data(self, element, ranges=None, empty=False): cdata, cmapping = self._get_color_data(element.nodes, ranges, style, 'node_fill_color') point_data.update(cdata) point_mapping = cmapping - - # Get hover data - if any(isinstance(t, HoverTool) for t in self.state.tools): - if nodes.dtype.kind not in 'if': - point_data['node'] = nodes - for d in element.nodes.dimensions()[3:]: - point_data[d.name] = element.nodes.dimension_values(d) + if 'node_fill_color' in point_mapping: + point_mapping['node_nonselection_fill_color'] = point_mapping['node_fill_color'] # Get edge data nan_node = index.max()+1 @@ -118,6 +120,15 @@ def get_data(self, element, ranges=None, empty=False): self.warning('Graph edge paths do not match the number of abstract edges ' 'and will be skipped') + # Get hover data + if any(isinstance(t, HoverTool) for t in self.state.tools): + if self.inspection_policy == 'nodes': + for d in element.nodes.dimensions()[2:]: + point_data[dimension_sanitizer(d.name)] = element.nodes.dimension_values(d) + elif self.inspection_policy == 'edges': + for d in element.vdims: + path_data[dimension_sanitizer(d.name)] = element.dimension_values(d) + data = {'scatter_1': point_data, 'multi_line_1': path_data, 'layout': layout} mapping = {'scatter_1': point_mapping, 'multi_line_1': {}} return data, mapping @@ -175,3 +186,6 @@ def _init_glyphs(self, plot, element, ranges, source): self.handles['multi_line_1_glyph_renderer'] = renderer.edge_renderer self.handles['scatter_1_glyph'] = renderer.node_renderer.glyph self.handles['multi_line_1_glyph'] = renderer.edge_renderer.glyph + if 'hover' in self.handles: + self.handles['hover'].renderers.append(renderer) + From 2c1efc350c54ad11cd196077f2005132ddec36e9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 00:09:55 +0100 Subject: [PATCH 42/60] Fixes for bokeh graph styles --- holoviews/plotting/bokeh/__init__.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 1a208e1bf1..d06f9fc567 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -172,12 +172,26 @@ def colormap_generator(palette): # Graphs options.Graph = Options('style', node_size=20, node_fill_color=Cycle(), - edge_line_width=2, node_hover_fill_color='indianred', - edge_hover_line_color='indianred', node_selection_fill_color='limegreen', - edge_selection_line_color='limegreen', edge_line_color='black', - node_line_color='black') -options.Nodes = Options('style', line_color='black', color=Cycle(), size=20) -options.EdgePaths = Options('style', color='black') + 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_nonselection_alpha=0.2, + edge_nonselection_alpha=0.2, + edge_line_color='black', edge_line_width=2, + edge_nonselection_line_color='black', + edge_hover_line_color='indianred', + edge_selection_line_color='limegreen') +options.Nodes = Options('style', line_color='black', color=Cycle(), + size=20, nonselection_fill_color=Cycle(), + selection_fill_color='limegreen', + hover_fill_color='indianred') +options.Nodes = Options('plot', tools=['hover', 'tap']) +options.EdgePaths = Options('style', color='black', nonselection_alpha=0.2, + line_width=2, selection_color='limegreen', + hover_line_color='indianred') +options.EdgePaths = Options('plot', tools=['hover', 'tap']) # Define composite defaults options.GridMatrix = Options('plot', shared_xaxis=True, shared_yaxis=True, From b998770c35913f9a5724e027fed613be36783001 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 00:10:29 +0100 Subject: [PATCH 43/60] Handle Graph initialization with node info --- holoviews/element/graphs.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 69be29505c..850b97094e 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -80,15 +80,31 @@ def __init__(self, data, **params): edges, nodes, edgepaths = data else: edges, nodes, edgepaths = data, None, None - if nodes is not None and not isinstance(nodes, Nodes): - nodes = Nodes(nodes) + if nodes is not None: + node_info = None + if isinstance(nodes, Nodes): + pass + elif not isinstance(nodes, Dataset) or nodes.ndims == 3: + nodes = Nodes(nodes) + else: + node_info = nodes + nodes = None + else: + node_info = None if edgepaths is not None and not isinstance(edgepaths, EdgePaths): edgepaths = EdgePaths(edgepaths) self.nodes = nodes self._edgepaths = edgepaths super(Graph, self).__init__(edges, **params) if self.nodes is None: - self.nodes = layout_nodes(self) + nodes = layout_nodes(self) + if node_info: + nodes = nodes.clone(datatype=['pandas', 'dictionary']) + for d in node_info.dimensions(): + nodes = nodes.add_dimension(d, len(nodes.vdims), + node_info.dimension_values(d), + vdim=True) + self.nodes = nodes self.redim = graph_redim(self, mode='dataset') def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): From aeae574efb64c3a1bd69cdf5bb494b58c5f0145e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 00:13:00 +0100 Subject: [PATCH 44/60] Various additions to graph user guide --- examples/user_guide/Network_Graphs.ipynb | 62 ++++++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb index b2e71e5a21..dcfb6821ca 100644 --- a/examples/user_guide/Network_Graphs.ipynb +++ b/examples/user_guide/Network_Graphs.ipynb @@ -11,7 +11,7 @@ "import holoviews as hv\n", "import networkx as nx\n", "\n", - "hv.notebook_extension('bokeh')\n", + "hv.extension('bokeh')\n", "\n", "%opts Graph [width=400 height=400]" ] @@ -113,7 +113,7 @@ "source": [ "#### Hover and selection policies\n", "\n", - "Thanks to Bokeh we can reveal more about the graph by hovering over the nodes and edges. The ``Graph`` element provides an ``inspection_policy`` and a ``selection_policy``, which define whether hovering and selection highlight edges associated with the selected node or nodes associated with the selected edge, these policies can be toggled by setting the policy to ``'nodes'`` and ``'edges'``." + "Thanks to Bokeh we can reveal more about the graph by hovering over the nodes and edges. The ``Graph`` element provides an ``inspection_policy`` and a ``selection_policy``, which define whether hovering and selection highlight edges associated with the selected node or nodes associated with the selected edge, these policies can be toggled by setting the policy to ``'nodes'`` (the default) and ``'edges'``." ] }, { @@ -122,7 +122,6 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Graph [tools=['hover']]\n", "bezier_graph.opts(plot=dict(inspection_policy='edges'))" ] }, @@ -149,7 +148,7 @@ "source": [ "#### Additional information\n", "\n", - "We can also associate additional information with the nodes of a graph. By constructing the ``Nodes`` explicitly we can declare an additional value dimension, which we can reveal when hovering and color the nodes by:" + "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." ] }, { @@ -158,9 +157,30 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Graph [tools=['hover'] color_index='Type'] (cmap='Set1')\n", - "nodes = hv.Nodes((x, y, node_indices, ['Output']+['Input']*(N-1)), vdims=['Type'])\n", - "hv.Graph(((source, target), nodes, paths)).redim.range(**padding)" + "%%opts Graph [color_index='Type'] (cmap='Set1')\n", + "node_labels = ['Output']+['Input']*(N-1)\n", + "edge_labels = list('ABCDEFGH')\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 + graph.opts(plot=dict(inspection_policy='edges'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you want to supply additional node information without speciying a explicit node positions you may pass in a ``Dataset`` object consisting of various value dimensions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "node_info = hv.Dataset(edge_labels, vdims=['Label'])\n", + "hv.Graph(((source, target), node_info)).redim.range(**padding)" ] }, { @@ -196,19 +216,28 @@ "#### Animating graphs" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Like all other elements ``Graph`` can be updated in a ``HoloMap`` or ``DynamicMap``. Here we animate how the Fruchterman-Reingold force-directed algorithm lays out the nodes in real time." + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "%%opts Graph {+framewise}\n", - "layouts = [nx.layout.circular_layout, nx.layout.fruchterman_reingold_layout, nx.layout.spectral_layout,\n", - " nx.layout.spring_layout, nx.layout.random_layout]\n", - "\n", + "%%opts Graph\n", "G = nx.karate_club_graph()\n", - "hv.HoloMap({l.__name__: hv.Graph.from_networkx(G, l) for l in layouts},\n", - " kdims=['Layout'])" + "\n", + "def get_graph(iteration):\n", + " np.random.seed(10)\n", + " return hv.Graph.from_networkx(G, nx.spring_layout, iterations=iteration)\n", + "\n", + "hv.HoloMap({i: get_graph(i) for i in range(10, 70, 5)},\n", + " kdims=['Iterations']).redim.range(x=(-.2, 1.2), y=(-.2, 1.2))" ] }, { @@ -231,7 +260,7 @@ "metadata": {}, "outputs": [], "source": [ - "%opts Nodes Graph [width=800 height=800 xaxis=None yaxis=None tools=['hover']]" + "%opts Nodes Graph [width=800 height=800 xaxis=None yaxis=None]" ] }, { @@ -296,7 +325,7 @@ "outputs": [], "source": [ "%%opts Nodes [color_index='circle'] (size=10 cmap=colors) Overlay [show_legend=False]\n", - "datashade(bundled, width=800, height=800) * bundled.nodes" + "datashade(bundled, normalization='linear', width=800, height=800) * bundled.nodes" ] }, { @@ -320,7 +349,8 @@ "outputs": [], "source": [ "%%opts Graph (node_fill_color='white')\n", - "datashade(bundle_graph(fb_graph, split=False), width=800, height=800) * bundled.select(circle='circle15')" + "datashade(bundle_graph(fb_graph, split=False), normalization='linear', width=800, height=800) *\\\n", + "bundled.select(circle='circle15')" ] } ], From 85d4ea312683f55fd1728f91250f4ad0b99724f3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 01:42:46 +0100 Subject: [PATCH 45/60] Added dimensions ranges option --- holoviews/core/data/array.py | 2 +- holoviews/core/data/dask.py | 2 +- holoviews/core/data/dictionary.py | 6 +++--- holoviews/core/data/iris.py | 2 +- holoviews/core/data/pandas.py | 4 ++-- holoviews/core/dimension.py | 2 +- holoviews/core/element.py | 2 +- holoviews/element/graphs.py | 2 +- holoviews/plotting/plot.py | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/holoviews/core/data/array.py b/holoviews/core/data/array.py index 24d45f2206..7864195bbd 100644 --- a/holoviews/core/data/array.py +++ b/holoviews/core/data/array.py @@ -71,7 +71,7 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def validate(cls, dataset): - ndims = len(dataset.kdims+dataset.vdims) + ndims = len(dataset.dimensions()) ncols = dataset.data.shape[1] if dataset.data.ndim > 1 else 1 if ncols < ndims: raise ValueError("Supplied data does not match specified " diff --git a/holoviews/core/data/dask.py b/holoviews/core/data/dask.py index 3e8b0012a2..15247dd490 100644 --- a/holoviews/core/data/dask.py +++ b/holoviews/core/data/dask.py @@ -250,7 +250,7 @@ def iloc(cls, dataset, index): rows, cols = index scalar = False if isinstance(cols, slice): - cols = [d.name for d in dataset.kdims+dataset.vdims][cols] + cols = [d.name for d in dataset.dimensions()][cols] elif np.isscalar(cols): scalar = np.isscalar(rows) cols = [dataset.get_dimension(cols).name] diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index 686e72e15a..d2528161c8 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -87,11 +87,11 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def validate(cls, dataset): - dimensions = dataset.kdims+dataset.vdims - not_found = [d for d in dimensions if d.name not in dataset.data] + dimensions = dataset.dimensions(label='name') + not_found = [d for d in dimensions if d not in dataset.data] if not_found: raise ValueError('Following dimensions not found in data: %s' % not_found) - lengths = [len(dataset.data[dim.name]) for dim in dimensions] + lengths = [len(dataset.data[dim]) for dim in dimensions] if len({l for l in lengths if l > 1}) > 1: raise ValueError('Length of columns do not match') diff --git a/holoviews/core/data/iris.py b/holoviews/core/data/iris.py index 0edffaa135..d66af4e14b 100644 --- a/holoviews/core/data/iris.py +++ b/holoviews/core/data/iris.py @@ -137,7 +137,7 @@ def shape(cls, dataset, gridded=False): if gridded: return dataset.data.shape else: - return (cls.length(dataset), len(dataset.kdims+dataset.vdims)) + return (cls.length(dataset), len(dataset.dimensions())) @classmethod diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 3f635bed47..29f594a5e6 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -82,8 +82,8 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def validate(cls, dataset): - not_found = [d for d in dataset.kdims+dataset.vdims - if d.name not in dataset.data.columns] + not_found = [d for d in dataset.dimensions(label='name') + if d not in dataset.data.columns] if not_found: raise ValueError("Supplied data does not contain specified " "dimensions, the following dimensions were " diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index cb555d6b5a..43223f6841 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -865,7 +865,7 @@ def dimensions(self, selection='all', label=False): 'v': (lambda x: x.vdims, {}), 'c': (lambda x: x.cdims, {})} aliases = {'key': 'k', 'value': 'v', 'constant': 'c'} - if selection == 'all': + if selection in ['all', 'ranges']: groups = [d for d in self._dim_groups if d != 'cdims'] dims = [dim for group in groups for dim in getattr(self, group)] diff --git a/holoviews/core/element.py b/holoviews/core/element.py index 659d689ec7..211027e25c 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -207,7 +207,7 @@ def rows(self): @property def cols(self): - return len(self.kdims+self.vdims) + return len(self.dimensions()) def pprint_cell(self, row, col): diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 850b97094e..7c6ae4efe8 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -168,7 +168,7 @@ def range(self, dimension, data_range=True): def dimensions(self, selection='all', label=False): dimensions = super(Graph, self).dimensions(selection, label) - if self.nodes and selection == 'all': + if self.nodes and selection == 'ranges': return dimensions+self.nodes.dimensions(selection, label) return dimensions diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index ac67302956..ad20f6c646 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -408,7 +408,7 @@ def _compute_group_range(group, elements, ranges): group_ranges = OrderedDict() for el in elements: if isinstance(el, (Empty, Table)): continue - for dim in el.dimensions(label=True): + for dim in el.dimensions('ranges', label=True): dim_range = el.range(dim) if dim not in group_ranges: group_ranges[dim] = [] From 2ef887a42404eb6365209021d5973e7c994cadcb Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 03:20:25 +0100 Subject: [PATCH 46/60] Ensure Nodes and EdgePaths dimensions match --- holoviews/element/graphs.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 7c6ae4efe8..6a4fe32c91 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -105,6 +105,15 @@ def __init__(self, data, **params): node_info.dimension_values(d), vdim=True) self.nodes = nodes + if self._edgepaths: + mismatch = [] + for kd1, kd2 in zip(self.nodes.kdims, self.edgepaths.kdims): + if kd1 != kd2: + print(kd1, kd2) + mismatch.append('%s != %s' % (kd1, kd2)) + if mismatch: + raise ValueError('Ensure that the first two key dimensions on ' + 'Nodes and EdgePaths match: %s' % ', '.join(mismatch)) self.redim = graph_redim(self, mode='dataset') def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): @@ -188,7 +197,8 @@ def edgepaths(self): 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])]) - return EdgePaths(paths) + return EdgePaths(paths, kdims=self.nodes.kdims[:2]) + @classmethod def from_networkx(cls, G, layout_function, nodes=None, **kwargs): From 477c0b23f07d44154d133e8ab492ea62eb033195 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 03:21:03 +0100 Subject: [PATCH 47/60] Handle node filtering when selecting source/target --- holoviews/element/graphs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 6a4fe32c91..558e7a85df 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -139,13 +139,16 @@ def select(self, selection_specs=None, **selection): specs match the selected object. """ selection = {dim: sel for dim, sel in selection.items() - if dim in self.dimensions()+['selection_mask']} + if dim in self.dimensions('ranges')+['selection_mask']} if (selection_specs and not any(self.matches(sp) for sp in selection_specs) or not selection): return self - nodes = self.nodes.select(**selection) + index_dim = self.nodes.kdims[2].name dimensions = self.kdims+self.vdims + node_selection = {index_dim: v for k, v in selection.items() + if k in self.kdims} + nodes = self.nodes.select(**dict(selection, **node_selection)) selection = {k: v for k, v in selection.items() if k in dimensions} if len(nodes) != len(self): xdim, ydim = dimensions[:2] From 0c95477de549eedbe2d8fd222e013f6154e26a7e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 03:25:28 +0100 Subject: [PATCH 48/60] Added basic Graph element unit tests --- holoviews/element/comparison.py | 26 +++++++++++++++ tests/testgraphelement.py | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/testgraphelement.py diff --git a/holoviews/element/comparison.py b/holoviews/element/comparison.py index 35598f179c..443061d8c6 100644 --- a/holoviews/element/comparison.py +++ b/holoviews/element/comparison.py @@ -166,6 +166,11 @@ def register(cls): cls.equality_type_funcs[BoxWhisker] = cls.compare_boxwhisker cls.equality_type_funcs[VectorField] = cls.compare_vectorfield + # Graphs + cls.equality_type_funcs[Graph] = cls.compare_graph + cls.equality_type_funcs[Nodes] = cls.compare_nodes + cls.equality_type_funcs[EdgePaths] = cls.compare_edgepaths + # Tables cls.equality_type_funcs[ItemTable] = cls.compare_itemtables cls.equality_type_funcs[Table] = cls.compare_tables @@ -561,6 +566,27 @@ def compare_spikes(cls, el1, el2, msg='Spikes'): def compare_boxwhisker(cls, el1, el2, msg='BoxWhisker'): cls.compare_dataset(el1, el2, msg) + + #=========# + # Graphs # + #=========# + + @classmethod + def compare_graph(cls, el1, el2, msg='Graph'): + cls.compare_dataset(el1, el2, msg) + cls.compare_nodes(el1.nodes, el2.nodes, msg) + if el1._edgepaths or el2._edgepaths: + cls.compare_edgepaths(el1.nodes, el2.nodes, msg) + + @classmethod + def compare_nodes(cls, el1, el2, msg='Nodes'): + cls.compare_dataset(el1, el2, msg) + + @classmethod + def compare_edgepaths(cls, el1, el2, msg='Nodes'): + cls.compare_paths(el1, el2, msg) + + #=========# # Rasters # #=========# diff --git a/tests/testgraphelement.py b/tests/testgraphelement.py new file mode 100644 index 0000000000..6c7ad5dd0c --- /dev/null +++ b/tests/testgraphelement.py @@ -0,0 +1,57 @@ +""" +Unit tests of Graph Element. +""" +import numpy as np +from holoviews.element.graphs import Graph, Nodes, circular_layout +from holoviews.element.comparison import ComparisonTestCase + + +class GraphTests(ComparisonTestCase): + + def setUp(self): + N = 8 + self.nodes = circular_layout(np.arange(N)) + self.source = np.arange(N) + self.target = np.zeros(N) + + def test_basic_constructor(self): + graph = Graph(((self.source, self.target),)) + nodes = Nodes(self.nodes) + self.assertEqual(graph.nodes, nodes) + + def test_constructor_with_nodes(self): + graph = Graph(((self.source, self.target), self.nodes)) + nodes = Nodes(self.nodes) + self.assertEqual(graph.nodes, nodes) + + def test_constructor_with_nodes_and_paths(self): + paths = Graph(((self.source, self.target), self.nodes)).edgepaths + graph = Graph(((self.source, self.target), self.nodes, paths.data)) + nodes = Nodes(self.nodes) + self.assertEqual(graph._edgepaths, paths) + + def test_constructor_with_nodes_and_paths_dimension_mismatch(self): + paths = Graph(((self.source, self.target), self.nodes)).edgepaths + exception = 'Ensure that the first two key dimensions on Nodes and EdgePaths match: x != x2' + with self.assertRaisesRegexp(ValueError, exception): + graph = Graph(((self.source, self.target), self.nodes, paths.redim(x='x2'))) + + def test_select_by_node(self): + graph = Graph(((self.source, self.target),)) + selection = Graph(([(0,0), (1, 0)], list(zip(*self.nodes))[:2])) + self.assertEqual(graph.select(index=(0, 2)), selection) + + def test_select_by_source(self): + graph = Graph(((self.source, self.target),)) + selection = Graph(([(0,0), (1, 0)], list(zip(*self.nodes))[:2])) + self.assertEqual(graph.select(start=(0, 2)), selection) + + def test_select_by_target(self): + graph = Graph(((self.target, self.source),)) + selection = Graph(([(0,0), (0, 1)], list(zip(*self.nodes))[:2])) + self.assertEqual(graph.select(end=(0, 2)), selection) + + def test_graph_node_range(self): + graph = Graph(((self.target, self.source),)) + self.assertEqual(graph.range('x'), (-1, 1)) + self.assertEqual(graph.range('y'), (-1, 1)) From cc6260970f067d0f4a74da7d6765ceb5098b1912 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 13:05:21 +0100 Subject: [PATCH 49/60] Added bokeh graph tests and upgraded to bokeh 0.12.9 --- .travis.yml | 4 +- tests/testbokehgraphs.py | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 tests/testbokehgraphs.py diff --git a/.travis.yml b/.travis.yml index 2715277bf0..deb5fd1954 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,10 +27,10 @@ install: - conda update -q conda # Useful for debugging any issues with conda - conda info -a - - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION scipy=0.18.1 numpy freetype nose bokeh=0.12.5 pandas=0.19.2 jupyter ipython=4.2.0 param pyqt=4 matplotlib=1.5.1 xarray + - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION scipy=0.18.1 numpy freetype nose pandas=0.19.2 jupyter ipython=4.2.0 param pyqt=4 matplotlib=1.5.1 xarray - source activate test-environment - conda install -c conda-forge iris sip=4.18 plotly flexx - - conda install -c bokeh datashader dask=0.13 + - conda install -c bokeh datashader dask=0.13 bokeh=0.12.9 - if [[ "$TRAVIS_PYTHON_VERSION" == "3.4" ]]; then conda install python=3.4.3; fi diff --git a/tests/testbokehgraphs.py b/tests/testbokehgraphs.py new file mode 100644 index 0000000000..ccfe59665a --- /dev/null +++ b/tests/testbokehgraphs.py @@ -0,0 +1,95 @@ +from __future__ import absolute_import + +from unittest import SkipTest + +import numpy as np +from holoviews.core.options import Store +from holoviews.element import Graph, circular_layout +from holoviews.element.comparison import ComparisonTestCase + +try: + from holoviews.plotting.bokeh.util import bokeh_version + bokeh_renderer = Store.renderers['bokeh'] + from bokeh.models import (NodesAndLinkedEdges, EdgesAndLinkedNodes) +except : + bokeh_renderer = None + + +class BokehGraphPlotTests(ComparisonTestCase): + + def setUp(self): + if bokeh_version < str('0.12.9'): + raise SkipTest("Bokeh >= 0.12.9 required to test graphs") + N = 8 + self.nodes = circular_layout(np.arange(N)) + self.source = np.arange(N) + self.target = np.zeros(N) + self.graph = Graph(((self.source, self.target),)) + + def test_plot_simple_graph(self): + plot = bokeh_renderer.get_plot(self.graph) + node_source = plot.handles['scatter_1_source'] + edge_source = plot.handles['multi_line_1_source'] + layout_source = plot.handles['layout_source'] + self.assertEqual(node_source.data['index'], self.source) + self.assertEqual(edge_source.data['start'], self.source) + self.assertEqual(edge_source.data['end'], self.target) + layout = {z: (x, y) for x, y, z in self.graph.nodes.array()} + self.assertEqual(layout_source.graph_layout, layout) + + def test_plot_graph_with_paths(self): + graph = self.graph.clone((self.graph.data, self.graph.nodes, self.graph.edgepaths)) + plot = bokeh_renderer.get_plot(graph) + node_source = plot.handles['scatter_1_source'] + edge_source = plot.handles['multi_line_1_source'] + layout_source = plot.handles['layout_source'] + self.assertEqual(node_source.data['index'], self.source) + self.assertEqual(edge_source.data['start'], self.source) + self.assertEqual(edge_source.data['end'], self.target) + edges = graph.edgepaths.split() + self.assertEqual(edge_source.data['xs'], [path.dimension_values(0) for path in edges]) + self.assertEqual(edge_source.data['ys'], [path.dimension_values(1) for path in edges]) + layout = {z: (x, y) for x, y, z in self.graph.nodes.array()} + self.assertEqual(layout_source.graph_layout, layout) + + def test_graph_inspection_policy_nodes(self): + plot = bokeh_renderer.get_plot(self.graph) + renderer = plot.handles['glyph_renderer'] + hover = plot.handles['hover'] + self.assertIsInstance(renderer.inspection_policy, NodesAndLinkedEdges) + self.assertEqual(hover.tooltips, [('index', '@{index}')]) + self.assertIn(renderer, hover.renderers) + + def test_graph_inspection_policy_edges(self): + plot = bokeh_renderer.get_plot(self.graph.opts(plot=dict(inspection_policy='edges'))) + renderer = plot.handles['glyph_renderer'] + hover = plot.handles['hover'] + self.assertIsInstance(renderer.inspection_policy, EdgesAndLinkedNodes) + self.assertEqual(hover.tooltips, [('start', '@{start}'), ('end', '@{end}')]) + self.assertIn(renderer, hover.renderers) + + def test_graph_inspection_policy_none(self): + plot = bokeh_renderer.get_plot(self.graph.opts(plot=dict(inspection_policy=None))) + renderer = plot.handles['glyph_renderer'] + hover = plot.handles['hover'] + self.assertIs(renderer.inspection_policy, None) + + def test_graph_selection_policy_nodes(self): + plot = bokeh_renderer.get_plot(self.graph) + renderer = plot.handles['glyph_renderer'] + hover = plot.handles['hover'] + self.assertIsInstance(renderer.selection_policy, NodesAndLinkedEdges) + self.assertIn(renderer, hover.renderers) + + def test_graph_selection_policy_edges(self): + plot = bokeh_renderer.get_plot(self.graph.opts(plot=dict(selection_policy='edges'))) + renderer = plot.handles['glyph_renderer'] + hover = plot.handles['hover'] + self.assertIsInstance(renderer.selection_policy, EdgesAndLinkedNodes) + self.assertIn(renderer, hover.renderers) + + def test_graph_selection_policy_none(self): + plot = bokeh_renderer.get_plot(self.graph.opts(plot=dict(selection_policy=None))) + renderer = plot.handles['glyph_renderer'] + hover = plot.handles['hover'] + self.assertIs(renderer.selection_policy, None) From aa9ddbf615fc9970177a40b0466f2a13c00feb1a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:07:42 +0100 Subject: [PATCH 50/60] Fixed updating mpl Graph node colors --- holoviews/plotting/mpl/graphs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index 76fae51fa7..8732f557ee 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -84,6 +84,12 @@ def update_handles(self, key, axis, element, ranges, style): data, style, axis_kwargs = self.get_data(element, ranges, style) xs, ys = data['nodes'] nodes.set_offsets(np.column_stack([xs, ys])) + cdim = element.nodes.get_dimension(self.color_index) + if cdim: + nodes.set_clim((style['vmin'], style['vmax'])) + nodes.set_array(style['c']) + if 'norm' in style: + nodes.norm = style['norm'] edges = self.handles['edges'] paths = data['edges'] From 92266ef0f621e20fe286837aa7af6bd941a51da6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:08:16 +0100 Subject: [PATCH 51/60] Fixed Graph without hover/select policy --- holoviews/plotting/bokeh/graphs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index f51961404d..f1fc62d6d7 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -60,6 +60,8 @@ def _hover_opts(self, element): dims = element.nodes.dimensions()[2:] elif self.inspection_policy == 'edges': dims = element.kdims+element.vdims + else: + dims = [] return dims, {} def get_extents(self, element, ranges): From b91f80595b3c2d8a67d02b0cd6d0a9a1f568a9da Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:08:48 +0100 Subject: [PATCH 52/60] Added Graph plot tests --- tests/testbokehgraphs.py | 23 +++++++++++++-- tests/testmplgraphs.py | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 tests/testmplgraphs.py diff --git a/tests/testbokehgraphs.py b/tests/testbokehgraphs.py index ccfe59665a..1b522c023a 100644 --- a/tests/testbokehgraphs.py +++ b/tests/testbokehgraphs.py @@ -6,6 +6,7 @@ from holoviews.core.options import Store from holoviews.element import Graph, circular_layout from holoviews.element.comparison import ComparisonTestCase +from holoviews.plotting import comms try: from holoviews.plotting.bokeh.util import bokeh_version @@ -17,14 +18,32 @@ class BokehGraphPlotTests(ComparisonTestCase): + def setUp(self): - if bokeh_version < str('0.12.9'): + if not bokeh_renderer: + raise SkipTest("Bokeh required to test plot instantiation") + elif bokeh_version < str('0.12.9'): raise SkipTest("Bokeh >= 0.12.9 required to test graphs") + self.previous_backend = Store.current_backend + Store.current_backend = 'bokeh' + Callback._comm_type = comms.Comm + self.default_comm = bokeh_renderer.comms['default'] + bokeh_renderer.comms['default'] = (comms.Comm, '') + N = 8 self.nodes = circular_layout(np.arange(N)) self.source = np.arange(N) self.target = np.zeros(N) self.graph = Graph(((self.source, self.target),)) + self.node_info = Dataset(['Output']+['Input']*(N-1), vdims=['Label']) + self.graph2 = Graph(((self.source, self.target), self.node_info)) + + + def tearDown(self): + Store.current_backend = self.previous_backend + Callback._comm_type = comms.JupyterCommJS + mpl_renderer.comms['default'] = self.default_comm + Callback._callbacks = {} def test_plot_simple_graph(self): plot = bokeh_renderer.get_plot(self.graph) @@ -36,7 +55,7 @@ def test_plot_simple_graph(self): self.assertEqual(edge_source.data['end'], self.target) layout = {z: (x, y) for x, y, z in self.graph.nodes.array()} self.assertEqual(layout_source.graph_layout, layout) - + def test_plot_graph_with_paths(self): graph = self.graph.clone((self.graph.data, self.graph.nodes, self.graph.edgepaths)) plot = bokeh_renderer.get_plot(graph) diff --git a/tests/testmplgraphs.py b/tests/testmplgraphs.py new file mode 100644 index 0000000000..4a846f0258 --- /dev/null +++ b/tests/testmplgraphs.py @@ -0,0 +1,60 @@ +from __future__ import absolute_import + +from unittest import SkipTest + +import numpy as np +from holoviews.core.data import Dataset +from holoviews.core.options import Store +from holoviews.element import Graph, circular_layout +from holoviews.element.comparison import ComparisonTestCase +from holoviews.plotting import comms + +# Standardize backend due to random inconsistencies +try: + from matplotlib import pyplot + pyplot.switch_backend('agg') + from holoviews.plotting.mpl import OverlayPlot + mpl_renderer = Store.renderers['matplotlib'] +except: + mpl_renderer = None + + +class MplGraphPlotTests(ComparisonTestCase): + + def setUp(self): + if not mpl_renderer: + raise SkipTest('Matplotlib tests require matplotlib to be available') + self.previous_backend = Store.current_backend + Store.current_backend = 'matplotlib' + self.default_comm = mpl_renderer.comms['default'] + mpl_renderer.comms['default'] = (comms.Comm, '') + + N = 8 + self.nodes = circular_layout(np.arange(N)) + self.source = np.arange(N) + self.target = np.zeros(N) + self.graph = Graph(((self.source, self.target),)) + self.node_info = Dataset(['Output']+['Input']*(N-1), vdims=['Label']) + self.graph2 = Graph(((self.source, self.target), self.node_info)) + + def tearDown(self): + mpl_renderer.comms['default'] = self.default_comm + Store.current_backend = self.previous_backend + + def test_plot_simple_graph(self): + plot = mpl_renderer.get_plot(self.graph) + nodes = plot.handles['nodes'] + edges = plot.handles['edges'] + self.assertEqual(nodes.get_offsets(), self.graph.nodes.array([0, 1])) + self.assertEqual([p.vertices for p in edges.get_paths()], + [p.array() for p in self.graph.edgepaths.split()]) + + def test_plot_graph_colored_nodes(self): + g = self.graph2.opts(plot=dict(color_index='Label'), style=dict(cmap='Set1')) + plot = mpl_renderer.get_plot(g) + nodes = plot.handles['nodes'] + edges = plot.handles['edges'] + self.assertEqual(nodes.get_offsets(), self.graph.nodes.array([0, 1])) + self.assertEqual([p.vertices for p in edges.get_paths()], + [p.array() for p in self.graph.edgepaths.split()]) + self.assertEqual(nodes.get_array(), np.array([1, 0, 0, 0, 0, 0, 0, 0])) From 1174c8c4089880aa73675506b6bfdd05ba42cc9c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:30:11 +0100 Subject: [PATCH 53/60] Add selenium to travis requirements --- .travis.yml | 2 +- tests/testbokehgraphs.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index deb5fd1954..c72b3916a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ install: - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION scipy=0.18.1 numpy freetype nose pandas=0.19.2 jupyter ipython=4.2.0 param pyqt=4 matplotlib=1.5.1 xarray - source activate test-environment - conda install -c conda-forge iris sip=4.18 plotly flexx - - conda install -c bokeh datashader dask=0.13 bokeh=0.12.9 + - conda install -c bokeh datashader dask=0.13 bokeh=0.12.9 selenium - if [[ "$TRAVIS_PYTHON_VERSION" == "3.4" ]]; then conda install python=3.4.3; fi diff --git a/tests/testbokehgraphs.py b/tests/testbokehgraphs.py index 1b522c023a..18700fe1f3 100644 --- a/tests/testbokehgraphs.py +++ b/tests/testbokehgraphs.py @@ -26,9 +26,7 @@ def setUp(self): raise SkipTest("Bokeh >= 0.12.9 required to test graphs") self.previous_backend = Store.current_backend Store.current_backend = 'bokeh' - Callback._comm_type = comms.Comm self.default_comm = bokeh_renderer.comms['default'] - bokeh_renderer.comms['default'] = (comms.Comm, '') N = 8 self.nodes = circular_layout(np.arange(N)) @@ -41,10 +39,8 @@ def setUp(self): def tearDown(self): Store.current_backend = self.previous_backend - Callback._comm_type = comms.JupyterCommJS - mpl_renderer.comms['default'] = self.default_comm - Callback._callbacks = {} - + bokeh_renderer.comms['default'] = self.default_comm + def test_plot_simple_graph(self): plot = bokeh_renderer.get_plot(self.graph) node_source = plot.handles['scatter_1_source'] From 19e7584b279328e793f2825ffa6f522a2af6f910 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:30:28 +0100 Subject: [PATCH 54/60] Added Graph redim test --- tests/testgraphelement.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/testgraphelement.py b/tests/testgraphelement.py index 6c7ad5dd0c..bb4b21f645 100644 --- a/tests/testgraphelement.py +++ b/tests/testgraphelement.py @@ -13,6 +13,7 @@ def setUp(self): self.nodes = circular_layout(np.arange(N)) self.source = np.arange(N) self.target = np.zeros(N) + self.graph = Graph(((self.source, self.target),)) def test_basic_constructor(self): graph = Graph(((self.source, self.target),)) @@ -55,3 +56,9 @@ def test_graph_node_range(self): graph = Graph(((self.target, self.source),)) self.assertEqual(graph.range('x'), (-1, 1)) self.assertEqual(graph.range('y'), (-1, 1)) + + def test_graph_redim_nodes(self): + graph = Graph(((self.target, self.source),)) + redimmed = graph.redim(x='x2', y='y2') + self.assertEqual(redimmed.nodes, graph.nodes.redim(x='x2', y='y2')) + self.assertEqual(redimmed.edgepaths, graph.edgepaths.redim(x='x2', y='y2')) From 8ae63db65aa83f98b3a014fc7563ef9630e08737 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:31:44 +0100 Subject: [PATCH 55/60] Added bokeh Graph colormapping test --- tests/testbokehgraphs.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/testbokehgraphs.py b/tests/testbokehgraphs.py index 18700fe1f3..569058ca45 100644 --- a/tests/testbokehgraphs.py +++ b/tests/testbokehgraphs.py @@ -3,6 +3,7 @@ from unittest import SkipTest import numpy as np +from holoviews.core.data import Dataset from holoviews.core.options import Store from holoviews.element import Graph, circular_layout from holoviews.element.comparison import ComparisonTestCase @@ -12,6 +13,7 @@ from holoviews.plotting.bokeh.util import bokeh_version bokeh_renderer = Store.renderers['bokeh'] from bokeh.models import (NodesAndLinkedEdges, EdgesAndLinkedNodes) + from bokeh.models.mappers import CategoricalColorMapper except : bokeh_renderer = None @@ -35,7 +37,6 @@ def setUp(self): self.graph = Graph(((self.source, self.target),)) self.node_info = Dataset(['Output']+['Input']*(N-1), vdims=['Label']) self.graph2 = Graph(((self.source, self.target), self.node_info)) - def tearDown(self): Store.current_backend = self.previous_backend @@ -108,3 +109,14 @@ def test_graph_selection_policy_none(self): renderer = plot.handles['glyph_renderer'] hover = plot.handles['hover'] self.assertIs(renderer.selection_policy, None) + + def test_graph_nodes_colormapped(self): + g = self.graph2.opts(plot=dict(color_index='Label'), style=dict(cmap='Set1')) + plot = bokeh_renderer.get_plot(g) + cmapper = plot.handles['color_mapper'] + node_source = plot.handles['scatter_1_source'] + glyph = plot.handles['scatter_1_glyph'] + self.assertIsInstance(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, ['Input', 'Output']) + self.assertEqual(node_source.data['Label'], self.node_info['Label']) + self.assertEqual(glyph.fill_color, {'field': 'Label', 'transform': cmapper}) From fcd8ff13938d0671eb81a0f094a1d1cd3a43d8ce Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:32:55 +0100 Subject: [PATCH 56/60] Small fix for network graph user guide --- examples/user_guide/Network_Graphs.ipynb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb index dcfb6821ca..87903f31c2 100644 --- a/examples/user_guide/Network_Graphs.ipynb +++ b/examples/user_guide/Network_Graphs.ipynb @@ -179,7 +179,8 @@ "metadata": {}, "outputs": [], "source": [ - "node_info = hv.Dataset(edge_labels, vdims=['Label'])\n", + "%%opts Graph [color_index='Label'] (cmap='Set1')\n", + "node_info = hv.Dataset(node_labels, vdims=['Label'])\n", "hv.Graph(((source, target), node_info)).redim.range(**padding)" ] }, From e7155d96f5bd957a0c6c23b74841c48cd9f71f80 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Sep 2017 14:48:59 +0100 Subject: [PATCH 57/60] Updated bokeh test for v0.12.9 --- tests/testplotinstantiation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py index b06d427ab1..0256ae7400 100644 --- a/tests/testplotinstantiation.py +++ b/tests/testplotinstantiation.py @@ -1191,7 +1191,9 @@ def test_box_whisker_datetime(self): dt.timedelta(days=1)) box = BoxWhisker((times, np.random.rand(len(times))), kdims=['Date']) plot = bokeh_renderer.get_plot(box) - formatted = [box.kdims[0].pprint_value(t).replace(':', ';') for t in times] + formatted = [box.kdims[0].pprint_value(t) for t in times] + if bokeh_version < str('0.12.7'): + formatted = [f.replace(':', ';') for f in formatted] self.assertTrue(all(cds.data['index'][0] in formatted for cds in plot.state.select(ColumnDataSource) if len(cds.data.get('index', [])))) From 8fd156664221eb06874c9dc6272267ac7663db24 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 20 Sep 2017 12:32:17 +0100 Subject: [PATCH 58/60] Added Graph element and demo notebooks --- .../gallery/demos/bokeh/network_graph.ipynb | 74 ++++++++++ .../demos/matplotlib/network_graph.ipynb | 75 ++++++++++ examples/reference/elements/bokeh/Graph.ipynb | 132 +++++++++++++++++ .../reference/elements/matplotlib/Graph.ipynb | 134 ++++++++++++++++++ 4 files changed, 415 insertions(+) create mode 100644 examples/gallery/demos/bokeh/network_graph.ipynb create mode 100644 examples/gallery/demos/matplotlib/network_graph.ipynb create mode 100644 examples/reference/elements/bokeh/Graph.ipynb create mode 100644 examples/reference/elements/matplotlib/Graph.ipynb diff --git a/examples/gallery/demos/bokeh/network_graph.ipynb b/examples/gallery/demos/bokeh/network_graph.ipynb new file mode 100644 index 0000000000..02b01e33ce --- /dev/null +++ b/examples/gallery/demos/bokeh/network_graph.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The data in this example represents Facebook social circle obtained from [SNAP](http://snap.stanford.edu/data/egonets-Facebook.html).\n", + "\n", + "Most examples work across multiple plotting backends, this example is also available for:\n", + "\n", + "* [Matplotlib network_graph](../matplotlib/network_graph.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import holoviews as hv\n", + "hv.extension('bokeh')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Declaring data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "edges_df = pd.read_csv('../../../assets/fb_edges.csv')\n", + "nodes_df = pd.read_csv('../../../assets/fb_nodes.csv')\n", + "\n", + "fb_nodes = hv.Nodes(nodes_df).sort()\n", + "fb_graph = hv.Graph((edges_df, fb_nodes), label='Facebook Circles')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "colors = ['#000000']+hv.Cycle('Category20').values\n", + "plot_opts = dict(color_index='circle', width=800, height=800, xaxis=None, yaxis=None, show_frame=False)\n", + "style_opts = dict(node_size=10, edge_line_width=1, cmap=colors)\n", + "fb_graph = fb_graph.redim.range(x=(-0.05, 1.05), y=(-0.05, 1.05)).opts(style=style_opts, plot=plot_opts)\n", + "fb_graph" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/gallery/demos/matplotlib/network_graph.ipynb b/examples/gallery/demos/matplotlib/network_graph.ipynb new file mode 100644 index 0000000000..bcc1d4bbfc --- /dev/null +++ b/examples/gallery/demos/matplotlib/network_graph.ipynb @@ -0,0 +1,75 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The data in this example represents Facebook social circle obtained from [SNAP](http://snap.stanford.edu/data/egonets-Facebook.html).\n", + "\n", + "Most examples work across multiple plotting backends, this example is also available for:\n", + "\n", + "* [Bokeh network_graph](../bokeh/network_graph.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import holoviews as hv\n", + "hv.extension('matplotlib')\n", + "\n", + "%output fig='svg'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Declaring data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "edges_df = pd.read_csv('../../../assets/fb_edges.csv')\n", + "nodes_df = pd.read_csv('../../../assets/fb_nodes.csv')\n", + "\n", + "fb_nodes = hv.Nodes(nodes_df).sort()\n", + "fb_graph = hv.Graph((edges_df, fb_nodes), label='Facebook Circles')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_opts = dict(color_index='circle', fig_size=350, xaxis=None, yaxis=None, show_frame=False)\n", + "style_opts = dict(node_size=10, cmap='tab20')\n", + "fb_graph = fb_graph.redim.range(x=(-0.05, 1.05), y=(-0.05, 1.05)).opts(style=style_opts, plot=plot_opts)\n", + "fb_graph" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/reference/elements/bokeh/Graph.ipynb b/examples/reference/elements/bokeh/Graph.ipynb new file mode 100644 index 0000000000..d919747ea8 --- /dev/null +++ b/examples/reference/elements/bokeh/Graph.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "
\n", + "
Title
Graph Element
\n", + "
Dependencies
Bokeh
\n", + "
Backends
Bokeh
Matplotlib
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import holoviews as hv\n", + "import networkx as nx\n", + "\n", + "hv.extension('bokeh')\n", + "\n", + "%opts Graph [width=400 height=400]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``Graph`` element provides an easy way to represent and visualize network graphs. It differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes. By default the element will automatically compute concrete ``x`` and ``y`` positions for the nodes and represent them using a ``Nodes`` element, which is stored on the Graph. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``EdgePaths``, providing explicit coordinates for each edge to follow.\n", + "\n", + "To summarize a ``Graph`` consists of three different components:\n", + "\n", + "* The ``Graph`` itself holds the abstract edges stored as a table of node indices.\n", + "* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``. The ``Nodes`` may also define any number of value dimensions, which can be revealed when hovering over the nodes or to color the nodes by.\n", + "* The ``EdgePaths`` can optionally be supplied to declare explicit node paths.\n", + "\n", + "This reference document describes only basic functionality, for a more detailed summary on how to work with network graphs in HoloViews see the [User Guide](../../../user_guide/Network_Graphs.ipynb).\n", + "\n", + "#### A simple Graph\n", + "\n", + "Let's start by declaring a very simple graph connecting one node to all others. If we simply supply the abstract connectivity of the ``Graph``, it will automatically compute a layout for the nodes using the ``layout_nodes`` operation, which defaults to a circular layout:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Declare abstract edges\n", + "N = 8\n", + "node_indices = np.arange(N)\n", + "source = np.zeros(N)\n", + "target = node_indices\n", + "\n", + "padding = dict(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", + "\n", + "simple_graph = hv.Graph(((source, target),)).redim.range(**padding)\n", + "simple_graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Accessing the nodes and edges\n", + "\n", + "We can easily access the ``Nodes`` and ``EdgePaths`` on the ``Graph`` element using the corresponding properties:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "simple_graph.nodes + simple_graph.edgepaths" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Additional features\n", + "\n", + "Next we will extend this example by supplying explicit edges:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Node info\n", + "x, y = simple_graph.nodes.array([0, 1]).T\n", + "node_labels = ['Output']+['Input']*(N-1)\n", + "\n", + "# Compute edge paths\n", + "def bezier(start, end, control, steps=np.linspace(0, 1, 100)):\n", + " return (1-steps)**2*start + 2*(1-steps)*steps*control+steps**2*end\n", + "\n", + "paths = []\n", + "for node_index in node_indices:\n", + " ex, ey = x[node_index], y[node_index]\n", + " paths.append(np.column_stack([bezier(x[0], ex, 0), bezier(y[0], ey, 0)]))\n", + "\n", + "# Declare Graph\n", + "nodes = hv.Nodes((x, y, node_indices, node_labels), vdims=['Type'])\n", + "graph = hv.Graph(((source, target), nodes, paths))\n", + "\n", + "graph.redim.range(**padding).opts(plot=dict(color_index='Type'),\n", + " style=dict(cmap=['blue', 'yellow']))" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/reference/elements/matplotlib/Graph.ipynb b/examples/reference/elements/matplotlib/Graph.ipynb new file mode 100644 index 0000000000..82fe16540d --- /dev/null +++ b/examples/reference/elements/matplotlib/Graph.ipynb @@ -0,0 +1,134 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "
\n", + "
Title
Graph Element
\n", + "
Dependencies
Matplotlib
\n", + "
Backends
\n", + "
Matplotlib
\n", + "
Bokeh
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import holoviews as hv\n", + "import networkx as nx\n", + "\n", + "hv.extension('matplotlib')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``Graph`` element provides an easy way to represent and visualize network graphs. It differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes. By default the element will automatically compute concrete ``x`` and ``y`` positions for the nodes and represent them using a ``Nodes`` element, which is stored on the Graph. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``EdgePaths``, providing explicit coordinates for each edge to follow.\n", + "\n", + "To summarize a ``Graph`` consists of three different components:\n", + "\n", + "* The ``Graph`` itself holds the abstract edges stored as a table of node indices.\n", + "* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``. The ``Nodes`` may also define any number of value dimensions, which can be revealed when hovering over the nodes or to color the nodes by.\n", + "* The ``EdgePaths`` can optionally be supplied to declare explicit node paths.\n", + "\n", + "This reference document describes only basic functionality, for a more detailed summary on how to work with network graphs in HoloViews see the [User Guide](../../../user_guide/Network_Graphs.ipynb).\n", + "\n", + "#### A simple Graph\n", + "\n", + "Let's start by declaring a very simple graph connecting one node to all others. If we simply supply the abstract connectivity of the ``Graph``, it will automatically compute a layout for the nodes using the ``layout_nodes`` operation, which defaults to a circular layout:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Declare abstract edges\n", + "N = 8\n", + "node_indices = np.arange(N)\n", + "source = np.zeros(N)\n", + "target = node_indices\n", + "\n", + "padding = dict(x=(-1.2, 1.2), y=(-1.2, 1.2))\n", + "\n", + "simple_graph = hv.Graph(((source, target),)).redim.range(**padding)\n", + "simple_graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Accessing the nodes and edges\n", + "\n", + "We can easily access the ``Nodes`` and ``EdgePaths`` on the ``Graph`` element using the corresponding properties:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "simple_graph.nodes + simple_graph.edgepaths" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Additional features\n", + "\n", + "Next we will extend this example by supplying explicit edges:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib.colors import ListedColormap\n", + "\n", + "# Node info\n", + "x, y = simple_graph.nodes.array([0, 1]).T\n", + "node_labels = ['Output']+['Input']*(N-1)\n", + "\n", + "# Compute edge paths\n", + "def bezier(start, end, control, steps=np.linspace(0, 1, 100)):\n", + " return (1-steps)**2*start + 2*(1-steps)*steps*control+steps**2*end\n", + "\n", + "paths = []\n", + "for node_index in node_indices:\n", + " ex, ey = x[node_index], y[node_index]\n", + " paths.append(np.column_stack([bezier(x[0], ex, 0), bezier(y[0], ey, 0)]))\n", + "\n", + "# Declare Graph\n", + "nodes = hv.Nodes((x, y, node_indices, node_labels), vdims=['Type'])\n", + "graph = hv.Graph(((source, target), nodes, paths))\n", + "\n", + "graph.redim.range(**padding).opts(plot=dict(color_index='Type'),\n", + " style=dict(cmap=ListedColormap(['blue', 'yellow'])))" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 5e646d67d7997edd1e3a272b91f97e98825b018a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 20 Sep 2017 12:32:45 +0100 Subject: [PATCH 59/60] Small update for Network Graphs user guide --- examples/user_guide/Network_Graphs.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user_guide/Network_Graphs.ipynb b/examples/user_guide/Network_Graphs.ipynb index 87903f31c2..a9a992f2ac 100644 --- a/examples/user_guide/Network_Graphs.ipynb +++ b/examples/user_guide/Network_Graphs.ipynb @@ -22,7 +22,7 @@ "source": [ "Visualizing and working with network graphs is a common problem in many different disciplines. HoloViews provides the ability to represent and visualize graphs very simply and easily with facilities for interactively exploring the nodes and edges of the graph, especially using the bokeh plotting interface.\n", "\n", - "The ``Graph`` ``Element`` differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes, on its own this abstract graph cannot be visualized. In order to visualize it we need to give each node in the ``Graph`` a concrete ``x`` and ``y`` position in form of the ``Nodes``. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``EdgePaths``, providing explicit coordinates for each edge to follow.\n", + "The ``Graph`` ``Element`` differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes. By default the element will automatically compute concrete ``x`` and ``y`` positions for the nodes and represent them using a ``Nodes`` element, which is stored on the Graph. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``EdgePaths``, providing explicit coordinates for each edge to follow.\n", "\n", "To summarize a ``Graph`` consists of three different components:\n", "\n", From 0ec07e012fe39d737c667a4e2982b7896dc931f8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 20 Sep 2017 18:50:12 +0100 Subject: [PATCH 60/60] Compute Graph Nodes lazily if none supplied --- holoviews/element/graphs.py | 43 ++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index 558e7a85df..2325063335 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -93,29 +93,27 @@ def __init__(self, data, **params): node_info = None if edgepaths is not None and not isinstance(edgepaths, EdgePaths): edgepaths = EdgePaths(edgepaths) - self.nodes = nodes + self._nodes = nodes self._edgepaths = edgepaths super(Graph, self).__init__(edges, **params) - if self.nodes is None: - nodes = layout_nodes(self) - if node_info: - nodes = nodes.clone(datatype=['pandas', 'dictionary']) - for d in node_info.dimensions(): - nodes = nodes.add_dimension(d, len(nodes.vdims), - node_info.dimension_values(d), - vdim=True) - self.nodes = nodes + if self._nodes is None and node_info: + nodes = self.nodes.clone(datatype=['pandas', 'dictionary']) + for d in node_info.dimensions(): + nodes = nodes.add_dimension(d, len(nodes.vdims), + node_info.dimension_values(d), + vdim=True) + self._nodes = nodes if self._edgepaths: mismatch = [] for kd1, kd2 in zip(self.nodes.kdims, self.edgepaths.kdims): if kd1 != kd2: - print(kd1, kd2) mismatch.append('%s != %s' % (kd1, kd2)) if mismatch: raise ValueError('Ensure that the first two key dimensions on ' 'Nodes and EdgePaths match: %s' % ', '.join(mismatch)) self.redim = graph_redim(self, mode='dataset') + def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): if data is None: data = (self.data, self.nodes) @@ -180,11 +178,30 @@ def range(self, dimension, data_range=True): def dimensions(self, selection='all', label=False): dimensions = super(Graph, self).dimensions(selection, label) - if self.nodes and selection == 'ranges': - return dimensions+self.nodes.dimensions(selection, label) + if selection == 'ranges': + if self._nodes: + node_dims = self.nodes.dimensions(selection, label) + else: + node_dims = Nodes.kdims+Nodes.vdims + if label in ['name', True, 'short']: + node_dims = [d.name for d in node_dims] + elif label in ['long', 'label']: + node_dims = [d.label for d in node_dims] + return dimensions+node_dims return dimensions + @property + def nodes(self): + """ + Computes the node positions the first time they are requested + if no explicit node information was supplied. + """ + if self._nodes is None: + self._nodes = layout_nodes(self) + return self._nodes + + @property def edgepaths(self): """