From 432ab07e91987d03d58a0fc93b3434861d5b865a Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 22 Nov 2017 16:46:25 +0000
Subject: [PATCH 01/28] Added TriMesh element
---
holoviews/element/graphs.py | 67 ++++++++++++++++++++++++++++
holoviews/plotting/bokeh/__init__.py | 6 ++-
holoviews/plotting/bokeh/graphs.py | 8 ++++
holoviews/plotting/mpl/__init__.py | 1 +
4 files changed, 80 insertions(+), 2 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index 514a3c308d..6c4a11cd54 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -392,3 +392,70 @@ class EdgePaths(Path):
"""
group = param.String(default='EdgePaths', constant=True)
+
+
+class TriMesh(Graph):
+ """
+ A TriMesh represents a mesh of triangles represented as the
+ simplices and nodes. The simplices represent a indices into the
+ nodes array. The mesh therefore follows a datastructure very
+ similar to a graph, with the abstract connectivity between nodes
+ stored on the TriMesh element itself, the node positions stored on
+ a Nodes element and the concrete paths making up each triangle
+ generated when required by accessing the edgepaths.
+ """
+
+ kdims = param.List(default=['node1', 'node2', 'node3'])
+
+ group = param.String(default='TriMesh')
+
+ def __init__(self, data, kdims=None, vdims=None, **params):
+ if isinstance(data, tuple):
+ data = data + (None,)* (3-len(data))
+ edges, nodes, edgepaths = data
+ else:
+ edges, nodes, edgepaths = data, None, None
+ if nodes is None:
+ raise ValueError("TriMesh expects both simplices and nodes "
+ "to be supplied.")
+
+ if isinstance(nodes, Nodes):
+ pass
+ elif isinstance(nodes, Points):
+ nodes = Nodes(Dataset(nodes).add_dimension('index', 2, np.arange(len(nodes))))
+ elif not isinstance(nodes, Dataset) or nodes.ndims in [2, 3]:
+ try:
+ nodes = Nodes(nodes)
+ except:
+ points = Dataset(Points(nodes)).add_dimension('index', 2, np.arange(len(nodes)))
+ nodes = Nodes(points)
+ if not isinstance(nodes, Nodes):
+ raise ValueError("Nodes argument could not be interpreted, expected "
+ "data with two or three columns representing the "
+ "x/y positions and optionally the node indices.")
+ if edgepaths is not None and not isinstance(edgepaths, EdgePaths):
+ edgepaths = EdgePaths(edgepaths)
+ super(TriMesh, self).__init__(edges, kdims=kdims, vdims=vdims, **params)
+ self._nodes = nodes
+ self._edgepaths = edgepaths
+
+
+ @property
+ def edgepaths(self):
+ """
+ Returns the EdgePaths by generating a triangle for each simplex.
+ """
+ if self._edgepaths:
+ return self._edgepaths
+
+ paths = []
+ simplices = self.array([0, 1, 2])
+ pts = self.nodes.array([0, 1])
+ empty = np.array([np.NaN, np.NaN])
+ for tri in pts[simplices]:
+ paths.append(np.vstack([tri[0, :], tri[1, :], empty,
+ tri[1, :], tri[2, :], empty,
+ tri[2, :], tri[0, :]]))
+ edgepaths = EdgePaths(paths, kdims=self.nodes.kdims[:2])
+ self._edgepaths = edgepaths
+ return edgepaths
diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py
index 6cf7aabb64..e860455194 100644
--- a/holoviews/plotting/bokeh/__init__.py
+++ b/holoviews/plotting/bokeh/__init__.py
@@ -13,7 +13,8 @@
Box, Bounds, Ellipse, Polygons, BoxWhisker, Arrow,
ErrorBars, Text, HLine, VLine, Spline, Spikes,
Table, ItemTable, Area, HSV, QuadMesh, VectorField,
- Graph, Nodes, EdgePaths, Distribution, Bivariate)
+ Graph, Nodes, EdgePaths, Distribution, Bivariate,
+ TriMesh)
from ...core.options import Options, Cycle, Palette
from ...core.util import VersionError
@@ -33,7 +34,7 @@
from .chart import (PointPlot, CurvePlot, SpreadPlot, ErrorPlot, HistogramPlot,
SideHistogramPlot, BarPlot, SpikesPlot, SideSpikesPlot,
AreaPlot, VectorFieldPlot, BoxWhiskerPlot)
-from .graphs import GraphPlot, NodePlot
+from .graphs import GraphPlot, NodePlot, TriMeshPlot
from .path import PathPlot, PolygonPlot, ContourPlot
from .plot import GridPlot, LayoutPlot, AdjointLayoutPlot
from .raster import RasterPlot, RGBPlot, HeatMapPlot, HSVPlot, QuadMeshPlot
@@ -97,6 +98,7 @@
Graph: GraphPlot,
Nodes: NodePlot,
EdgePaths: PathPlot,
+ TriMesh: TriMeshPlot,
# Tabular
Table: TablePlot,
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index 6ed47ec595..531ec7e9c4 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -249,3 +249,11 @@ class NodePlot(PointPlot):
def _hover_opts(self, element):
return element.dimensions()[2:], {}
+
+
+class TriMeshPlot(GraphPlot):
+
+ def get_data(self, element, ranges, style):
+ # Ensure the edgepaths for the triangles are generated
+ element.edgepaths
+ return super(TriMeshPlot, self).get_data(element, ranges, style)
diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py
index 06608eb228..2abcf966d5 100644
--- a/holoviews/plotting/mpl/__init__.py
+++ b/holoviews/plotting/mpl/__init__.py
@@ -148,6 +148,7 @@ def grid_selector(grid):
# Graph Elements
Graph: GraphPlot,
+ TriMesh: GraphPlot,
Nodes: PointPlot,
EdgePaths: PathPlot,
From c1e60ad7b5772b738a761b8ed26a3a2336a1e3fd Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 22 Nov 2017 17:13:55 +0000
Subject: [PATCH 02/28] Simplified TriMesh path generation
---
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 6c4a11cd54..1f02fea061 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -453,9 +453,9 @@ def edgepaths(self):
pts = self.nodes.array([0, 1])
empty = np.array([np.NaN, np.NaN])
for tri in pts[simplices]:
- paths.append(np.vstack([tri[0, :], tri[1, :], empty,
- tri[1, :], tri[2, :], empty,
- tri[2, :], tri[0, :]]))
+ paths.append(np.vstack([tri[[0, 1], :], empty,
+ tri[[1, 2], :], empty,
+ tri[[2, 1], :]]))
edgepaths = EdgePaths(paths, kdims=self.nodes.kdims[:2])
self._edgepaths = edgepaths
return edgepaths
From c095010ed990fd93292eb76dc68440cb3e3f9d72 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 22 Nov 2017 17:50:55 +0000
Subject: [PATCH 03/28] Declared TriMesh.group constant
---
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 1f02fea061..c8a9f12d1e 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -407,7 +407,7 @@ class TriMesh(Graph):
kdims = param.List(default=['node1', 'node2', 'node3'])
- group = param.String(default='TriMesh')
+ group = param.String(default='TriMesh', constant=True)
def __init__(self, data, kdims=None, vdims=None, **params):
if isinstance(data, tuple):
From 75b36763ceddc27eb3dd5dd3e664bf7f59297982 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 23 Nov 2017 03:03:11 +0000
Subject: [PATCH 04/28] Simplified TriMesh edgepaths code
---
holoviews/element/graphs.py | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index c8a9f12d1e..2cbb7b1d57 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -448,14 +448,9 @@ def edgepaths(self):
if self._edgepaths:
return self._edgepaths
- paths = []
simplices = self.array([0, 1, 2])
pts = self.nodes.array([0, 1])
- empty = np.array([np.NaN, np.NaN])
- for tri in pts[simplices]:
- paths.append(np.vstack([tri[[0, 1], :], empty,
- tri[[1, 2], :], empty,
- tri[[2, 1], :]]))
+ paths = [tri[[0, 1, 2, 0], :] for tri in pts[simplices]]
edgepaths = EdgePaths(paths, kdims=self.nodes.kdims[:2])
self._edgepaths = edgepaths
return edgepaths
From 21451e6fade0acf65c2774e84c883dcf2d636b8e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 24 Nov 2017 13:27:43 +0000
Subject: [PATCH 05/28] Improvements for TriMesh
---
holoviews/element/graphs.py | 44 ++++++++++++++++++++++++++++++-------
1 file changed, 36 insertions(+), 8 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index 2cbb7b1d57..7bfa126c12 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -403,15 +403,20 @@ class TriMesh(Graph):
stored on the TriMesh element itself, the node positions stored on
a Nodes element and the concrete paths making up each triangle
generated when required by accessing the edgepaths.
+
+ Unlike a Graph each simplex is represented as the node indices of
+ the three corners of each triangle.
"""
- kdims = param.List(default=['node1', 'node2', 'node3'])
+ kdims = param.List(default=['node1', 'node2', 'node3'],
+ bounds=(3, 3), doc="""
+ Dimensions declaring the node indices of each triangle.""")
group = param.String(default='TriMesh', constant=True)
def __init__(self, data, kdims=None, vdims=None, **params):
if isinstance(data, tuple):
- data = data + (None,)* (3-len(data))
+ data = data + (None,)*(3-len(data))
edges, nodes, edgepaths = data
else:
edges, nodes, edgepaths = data, None, None
@@ -422,17 +427,21 @@ def __init__(self, data, kdims=None, vdims=None, **params):
if isinstance(nodes, Nodes):
pass
elif isinstance(nodes, Points):
+ # Add index to make it a valid Nodes object
nodes = Nodes(Dataset(nodes).add_dimension('index', 2, np.arange(len(nodes))))
elif not isinstance(nodes, Dataset) or nodes.ndims in [2, 3]:
try:
+ # Try assuming data contains indices (3 columns)
nodes = Nodes(nodes)
except:
- points = Dataset(Points(nodes)).add_dimension('index', 2, np.arange(len(nodes)))
- nodes = Nodes(points)
- if not isinstance(nodes, Nodes):
- raise ValueError("Nodes argument could not be interpreted, expected "
- "data with two or three columns representing the "
- "x/y positions and optionally the node indices.")
+ # Try assuming data contains just coordinates (2 columns)
+ try:
+ points = Points(nodes)
+ nodes = Nodes(Dataset(points).add_dimension('index', 2, np.arange(len(nodes))))
+ except:
+ raise ValueError("Nodes argument could not be interpreted, expected "
+ "data with two or three columns representing the "
+ "x/y positions and optionally the node indices.")
if edgepaths is not None and not isinstance(edgepaths, EdgePaths):
edgepaths = EdgePaths(edgepaths)
super(TriMesh, self).__init__(edges, kdims=kdims, vdims=vdims, **params)
@@ -454,3 +463,22 @@ def edgepaths(self):
edgepaths = EdgePaths(paths, kdims=self.nodes.kdims[:2])
self._edgepaths = edgepaths
return edgepaths
+
+ def select(self, selection_specs=None, selection_mode='edges', **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.
+
+ Selecting by a node dimensions selects all edges and nodes that are
+ connected to the selected nodes. To select only edges between the
+ selected nodes set the selection_mode to 'nodes'.
+ """
+ self.edgepaths
+ return super(TriMesh, self).select(selection_specs=None,
+ selection_mode='edges',
+ **selection)
From 63067ce9e1137d934a3f43392dc35637a788b68b Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 24 Nov 2017 14:00:04 +0000
Subject: [PATCH 06/28] Added support for filled TriMesh simplices
---
holoviews/plotting/bokeh/graphs.py | 50 ++++++++++++++++++++++--------
holoviews/plotting/mpl/graphs.py | 29 +++++++++++++++--
2 files changed, 63 insertions(+), 16 deletions(-)
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index 531ec7e9c4..f63b76597c 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -2,7 +2,7 @@
import numpy as np
from bokeh.models import HoverTool, ColumnDataSource
from bokeh.models import (StaticLayoutProvider, NodesAndLinkedEdges,
- EdgesAndLinkedNodes)
+ EdgesAndLinkedNodes, Patches)
from ...core.util import basestring, dimension_sanitizer, unique_array
from ...core.options import Cycle
@@ -33,10 +33,14 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot):
A list of plugin tools to use on the plot.""")
# Map each glyph to a style group
- _style_groups = {'scatter': 'node', 'multi_line': 'edge'}
+ _style_groups = {'scatter': 'node', 'multi_line': 'edge', 'patches': 'edge'}
- style_opts = (['edge_'+p for p in line_properties] +\
- ['node_'+p for p in fill_properties+line_properties]+['node_size', 'cmap', 'edge_cmap'])
+ style_opts = (['edge_'+p for p in line_properties] +
+ ['node_'+p for p in fill_properties+line_properties] +
+ ['node_size', 'cmap', 'edge_cmap'])
+
+ # Filled is only supported for subclasses
+ filled = False
def _hover_opts(self, element):
if self.inspection_policy == 'nodes':
@@ -101,9 +105,10 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
mapper = self._get_colormapper(cdim, element, ranges, edge_style,
factors, colors, 'edge_colormapper')
transform = {'field': field, 'transform': mapper}
- edge_mapping['edge_line_color'] = transform
- edge_mapping['edge_nonselection_line_color'] = transform
- edge_mapping['edge_selection_line_color'] = transform
+ color_type = 'fill_color' if self.filled else 'line_color'
+ edge_mapping['edge_'+color_type] = transform
+ edge_mapping['edge_nonselection_'+color_type] = transform
+ edge_mapping['edge_selection_'+color_type] = transform
def get_data(self, element, ranges, style):
@@ -170,8 +175,9 @@ def get_data(self, element, ranges, style):
elif self.inspection_policy == 'edges':
for d in element.dimensions():
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': edge_mapping}
+ edge_glyph = 'patches_1' if self.filled else 'multi_line_1'
+ data = {'scatter_1': point_data, edge_glyph: path_data, 'layout': layout}
+ mapping = {'scatter_1': point_mapping, edge_glyph: edge_mapping}
return data, mapping, style
@@ -190,11 +196,12 @@ def _init_glyphs(self, plot, element, ranges, source):
style = self.style[self.cyclic_index]
data, mapping, style = self.get_data(element, ranges, style)
self.handles['previous_id'] = element._plot_id
+ edge_glyph = 'patches_1' if self.filled else 'multi_line_1'
properties = {}
mappings = {}
for key in list(mapping):
- if not any(glyph in key for glyph in ('scatter_1', 'multi_line_1')):
+ if not any(glyph in key for glyph in ('scatter_1', edge_glyph)):
continue
source = self._init_datasource(data.pop(key, {}))
self.handles[key+'_source'] = source
@@ -212,7 +219,7 @@ def _init_glyphs(self, plot, element, ranges, source):
# Define static layout
layout = StaticLayoutProvider(graph_layout=layout)
node_source = self.handles['scatter_1_source']
- edge_source = self.handles['multi_line_1_source']
+ edge_source = self.handles[edge_glyph+'_source']
renderer = plot.graph(node_source, edge_source, layout, **properties)
# Initialize GraphRenderer
@@ -233,9 +240,19 @@ def _init_glyphs(self, plot, element, ranges, source):
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[edge_glyph+'_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 self.filled:
+ allowed_properties = Patches.properties()
+ for glyph_type in ('', 'selection_', 'nonselection_', 'hover_', 'muted_'):
+ glyph = getattr(renderer.edge_renderer, glyph_type+'glyph', None)
+ if glyph is None:
+ continue
+ props = self._process_properties(edge_glyph, properties, mappings)
+ filtered = self._filter_properties(props, glyph_type, allowed_properties)
+ patches = Patches(**dict(filtered, xs='xs', ys='ys'))
+ glyph = setattr(renderer.edge_renderer, glyph_type+'glyph', patches)
+ self.handles[edge_glyph+'_glyph'] = renderer.edge_renderer.glyph
if 'hover' in self.handles:
self.handles['hover'].renderers.append(renderer)
@@ -253,6 +270,13 @@ def _hover_opts(self, element):
class TriMeshPlot(GraphPlot):
+ filled = param.Boolean(default=False, doc="""
+ Whether the triangles should be drawn as filled.""")
+
+ style_opts = (['edge_'+p for p in line_properties+fill_properties] +
+ ['node_'+p for p in fill_properties+line_properties] +
+ ['node_size', 'cmap', 'edge_cmap'])
+
def get_data(self, element, ranges, style):
# Ensure the edgepaths for the triangles are generated
element.edgepaths
diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py
index dedbc8f3a3..183521d034 100644
--- a/holoviews/plotting/mpl/graphs.py
+++ b/holoviews/plotting/mpl/graphs.py
@@ -1,7 +1,7 @@
import param
import numpy as np
-from matplotlib.collections import LineCollection
+from matplotlib.collections import LineCollection, PolyCollection
from ...core.options import Cycle
from ...core.util import basestring, unique_array, search_indices
@@ -26,6 +26,8 @@ class GraphPlot(ColorbarPlot):
_style_groups = ['node', 'edge']
+ filled = False
+
def _compute_styles(self, element, ranges, style):
elstyle = self.lookup_options(element, 'style')
color = elstyle.kwargs.get('node_color')
@@ -81,6 +83,7 @@ def _compute_styles(self, element, ranges, style):
style['edge_clim'] = (style.pop('edge_vmin'), style.pop('edge_vmax'))
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))
@@ -92,6 +95,7 @@ def get_data(self, element, ranges, style):
paths = [p[:, ::-1] for p in paths]
return {'nodes': (pxs, pys), 'edges': paths}, style, {'dimensions': dims}
+
def get_extents(self, element, ranges):
"""
Extents are set to '' and None because x-axis is categorical and
@@ -101,6 +105,7 @@ def get_extents(self, element, ranges):
y0, y1 = element.nodes.range(1)
return (x0, y0, x1, y1)
+
def init_artists(self, ax, plot_args, plot_kwargs):
# Draw edges
color_opts = ['c', 'cmap', 'vmin', 'vmax', 'norm']
@@ -110,7 +115,13 @@ def init_artists(self, ax, plot_args, plot_kwargs):
if not any(k.startswith(p) for p in groups)
and k not in color_opts}
paths = plot_args['edges']
- edges = LineCollection(paths, **edge_opts)
+ if self.filled:
+ coll = PolyCollection
+ if 'colors' in edge_opts:
+ edge_opts['facecolors'] = edge_opts.pop('colors')
+ else:
+ coll = LineCollection
+ edges = coll(paths, **edge_opts)
ax.add_collection(edges)
# Draw nodes
@@ -150,7 +161,10 @@ def _update_edges(self, element, data, style):
if 'norm' in style:
edges.norm = style['edge_norm']
elif 'edge_colors' in style:
- edges.set_edgecolors(style['edge_colors'])
+ if self.filled:
+ edges.set_facecolors(style['edge_colors'])
+ else:
+ edges.set_edgecolors(style['edge_colors'])
def update_handles(self, key, axis, element, ranges, style):
@@ -158,3 +172,12 @@ def update_handles(self, key, axis, element, ranges, style):
self._update_nodes(element, data, style)
self._update_edges(element, data, style)
return axis_kwargs
+
+
+
+class TriMeshPlot(GraphPlot):
+
+ filled = param.Boolean(default=False, doc="""
+ Whether the triangles should be drawn as filled.""")
+
+ style_opts = GraphPlot.style_opts + ['edge_facecolors']
From c6e9146dfffd128d206dd2cfc7106630d0953ec2 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 24 Nov 2017 15:22:46 +0000
Subject: [PATCH 07/28] Defined default TriMesh styling
---
holoviews/plotting/bokeh/__init__.py | 9 +++++++++
holoviews/plotting/mpl/__init__.py | 6 ++++--
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py
index e860455194..b5a8dfbd80 100644
--- a/holoviews/plotting/bokeh/__init__.py
+++ b/holoviews/plotting/bokeh/__init__.py
@@ -199,6 +199,15 @@ def colormap_generator(palette):
edge_line_color='black', edge_line_width=2,
edge_nonselection_line_color='black',
edge_hover_line_color='limegreen')
+options.TriMesh = Options('style', node_size=5, node_line_color='black',
+ node_fill_color='white', edge_line_color='black',
+ node_hover_fill_color='limegreen',
+ edge_hover_line_color='limegreen',
+ edge_nonselection_alpha=0.2,
+ edge_nonselection_line_color='black',
+ node_nonselection_alpha=0.2,
+ edge_line_width=1)
+options.TriMesh = Options('plot', tools=[])
options.Nodes = Options('style', line_color='black', color=Cycle(),
size=20, nonselection_fill_color=Cycle(),
selection_fill_color='limegreen',
diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py
index 2abcf966d5..d202c2f3da 100644
--- a/holoviews/plotting/mpl/__init__.py
+++ b/holoviews/plotting/mpl/__init__.py
@@ -148,7 +148,7 @@ def grid_selector(grid):
# Graph Elements
Graph: GraphPlot,
- TriMesh: GraphPlot,
+ TriMesh: TriMeshPlot,
Nodes: PointPlot,
EdgePaths: PathPlot,
@@ -263,7 +263,9 @@ def grid_selector(grid):
# Graphs
options.Graph = Options('style', node_edgecolors='black', node_facecolors=Cycle(),
- edge_color='black', node_size=20)
+ edge_color='black', node_size=15)
+options.TriMesh = Options('style', node_edgecolors='black', node_facecolors='white',
+ edge_color='black', node_size=5, edge_linewidth=1)
options.Nodes = Options('style', edgecolors='black', facecolors=Cycle(),
marker='o', s=20**2)
options.EdgePaths = Options('style', color='black')
From 3f61ccac3d2aec20f556db7fc4dd6cf966cdd4b4 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 24 Nov 2017 15:23:20 +0000
Subject: [PATCH 08/28] Small fix for TriMesh path computation
---
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 7bfa126c12..2e3a80ed4a 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -457,7 +457,7 @@ def edgepaths(self):
if self._edgepaths:
return self._edgepaths
- simplices = self.array([0, 1, 2])
+ simplices = self.array([0, 1, 2]).astype(np.int32)
pts = self.nodes.array([0, 1])
paths = [tri[[0, 1, 2, 0], :] for tri in pts[simplices]]
edgepaths = EdgePaths(paths, kdims=self.nodes.kdims[:2])
From bcbb7bf79e7a8941c07179b6450be5748d8c373e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 24 Nov 2017 15:23:48 +0000
Subject: [PATCH 09/28] Added TriMesh reference entries
---
.../reference/elements/bokeh/TriMesh.ipynb | 165 ++++++++++++++++++
.../elements/matplotlib/TriMesh.ipynb | 165 ++++++++++++++++++
2 files changed, 330 insertions(+)
create mode 100644 examples/reference/elements/bokeh/TriMesh.ipynb
create mode 100644 examples/reference/elements/matplotlib/TriMesh.ipynb
diff --git a/examples/reference/elements/bokeh/TriMesh.ipynb b/examples/reference/elements/bokeh/TriMesh.ipynb
new file mode 100644
index 0000000000..387638fd16
--- /dev/null
+++ b/examples/reference/elements/bokeh/TriMesh.ipynb
@@ -0,0 +1,165 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "
\n",
+ " - Title
- TriMesh 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",
+ "from scipy.spatial import Delaunay\n",
+ "\n",
+ "hv.extension('bokeh')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A ``TriMesh`` represents a mesh of triangles represented as the simplices and nodes. The simplices represent the indices into the node data, made up of three indices per triangle. The mesh therefore follows a datastructure very similar to a graph, with the abstract connectivity between nodes stored on the ``TriMesh`` element itself, the node positions stored on a ``Nodes`` element and the concrete ``EdgePaths`` making up each triangle generated when required by accessing the edgepaths.\n",
+ "\n",
+ "Unlike a Graph each simplex is represented as the node indices of the three corners of each triangle rather than the usual source and target node.\n",
+ "\n",
+ "We will begin with a simple random mesh, generated by sampling some random integers and then applying Delaunay triangulation, which is available in SciPy. We can then construct the ``TriMesh`` by passing it the **simplices** and the points (or **nodes**)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "n_verts = 100\n",
+ "pts = np.random.randint(1, n_verts, (n_verts, 2))\n",
+ "tris = Delaunay(pts)\n",
+ "\n",
+ "trimesh = hv.TriMesh((tris.simplices, pts))\n",
+ "trimesh"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Just like the ``Graph`` element we can access the ``Nodes`` and ``EdgePaths`` via the ``.nodes`` and ``.edgepaths`` attributes respectively."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "trimesh.nodes + trimesh.edgepaths"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now let's make a slightly more interesting example by generating a more complex geometry. Here we will compute a geometry, then apply Delaunay triangulation again and finally apply a mask to drop nodes in the center."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# First create the x and y coordinates of the points.\n",
+ "n_angles = 36\n",
+ "n_radii = 8\n",
+ "min_radius = 0.25\n",
+ "radii = np.linspace(min_radius, 0.95, n_radii)\n",
+ "\n",
+ "angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)\n",
+ "angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)\n",
+ "angles[:, 1::2] += np.pi/n_angles\n",
+ "\n",
+ "x = (radii*np.cos(angles)).flatten()\n",
+ "y = (radii*np.sin(angles)).flatten()\n",
+ "nodes = np.column_stack([x, y])\n",
+ "z = (np.cos(radii)*np.cos(angles*3.0)).flatten()\n",
+ "\n",
+ "# Apply Delaunay triangulation\n",
+ "delauney = Delaunay(np.column_stack([x, y]))\n",
+ "\n",
+ "# Mask off unwanted triangles.\n",
+ "xmid = x[delauney.simplices].mean(axis=1)\n",
+ "ymid = y[delauney.simplices].mean(axis=1)\n",
+ "mask = np.where(xmid*xmid + ymid*ymid < min_radius*min_radius, 1, 0)\n",
+ "simplices = delauney.simplices[np.logical_not(mask)]\n",
+ "z = z[simplices].mean(axis=1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Once again we can simply supply the simplices and nodes to the ``TriMesh``."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hv.TriMesh((simplices, nodes))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also do something more interesting, e.g. adding a value dimension to the simplices, which we can color each triangle by."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%opts TriMesh [filled=True edge_color_index='z' width=400 height=400 tools=['hover'] inspection_policy='edges'] (cmap='viridis')\n",
+ "hv.TriMesh((np.column_stack([simplices, z]), nodes), vdims='z')"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/examples/reference/elements/matplotlib/TriMesh.ipynb b/examples/reference/elements/matplotlib/TriMesh.ipynb
new file mode 100644
index 0000000000..189a0aa8d5
--- /dev/null
+++ b/examples/reference/elements/matplotlib/TriMesh.ipynb
@@ -0,0 +1,165 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "
\n",
+ " - Title
- TriMesh Element
\n",
+ " - Dependencies
- Bokeh
\n",
+ " - Backends
- Matplotlib
- 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",
+ "from scipy.spatial import Delaunay\n",
+ "\n",
+ "hv.extension('matplotlib')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A ``TriMesh`` represents a mesh of triangles represented as the simplices and nodes. The simplices represent the indices into the node data, made up of three indices per triangle. The mesh therefore follows a datastructure very similar to a graph, with the abstract connectivity between nodes stored on the ``TriMesh`` element itself, the node positions stored on a ``Nodes`` element and the concrete ``EdgePaths`` making up each triangle generated when required by accessing the edgepaths.\n",
+ "\n",
+ "Unlike a Graph each simplex is represented as the node indices of the three corners of each triangle rather than the usual source and target node.\n",
+ "\n",
+ "We will begin with a simple random mesh, generated by sampling some random integers and then applying Delaunay triangulation, which is available in SciPy. We can then construct the ``TriMesh`` by passing it the **simplices** and the points (or **nodes**)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "n_verts = 100\n",
+ "pts = np.random.randint(1, n_verts, (n_verts, 2))\n",
+ "tris = Delaunay(pts)\n",
+ "\n",
+ "trimesh = hv.TriMesh((tris.simplices, pts))\n",
+ "trimesh"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Just like the ``Graph`` element we can access the ``Nodes`` and ``EdgePaths`` via the ``.nodes`` and ``.edgepaths`` attributes respectively."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "trimesh.nodes + trimesh.edgepaths"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now let's make a slightly more interesting example by generating a more complex geometry. Here we will compute a geometry, then apply Delaunay triangulation again and finally apply a mask to drop nodes in the center."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# First create the x and y coordinates of the points.\n",
+ "n_angles = 36\n",
+ "n_radii = 8\n",
+ "min_radius = 0.25\n",
+ "radii = np.linspace(min_radius, 0.95, n_radii)\n",
+ "\n",
+ "angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)\n",
+ "angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)\n",
+ "angles[:, 1::2] += np.pi/n_angles\n",
+ "\n",
+ "x = (radii*np.cos(angles)).flatten()\n",
+ "y = (radii*np.sin(angles)).flatten()\n",
+ "nodes = np.column_stack([x, y])\n",
+ "z = (np.cos(radii)*np.cos(angles*3.0)).flatten()\n",
+ "\n",
+ "# Apply Delaunay triangulation\n",
+ "delauney = Delaunay(np.column_stack([x, y]))\n",
+ "\n",
+ "# Mask off unwanted triangles.\n",
+ "xmid = x[delauney.simplices].mean(axis=1)\n",
+ "ymid = y[delauney.simplices].mean(axis=1)\n",
+ "mask = np.where(xmid*xmid + ymid*ymid < min_radius*min_radius, 1, 0)\n",
+ "simplices = delauney.simplices[np.logical_not(mask)]\n",
+ "z = z[simplices].mean(axis=1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Once again we can simply supply the simplices and nodes to the ``TriMesh``."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hv.TriMesh((simplices, nodes))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also do something more interesting, e.g. adding a value dimension to the simplices, which we can color each triangle by."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%opts TriMesh [filled=True edge_color_index='z' fig_size=200] (cmap='viridis')\n",
+ "hv.TriMesh((np.column_stack([simplices, z]), nodes), vdims='z')"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
From bdbb3cca3bdb83c099cd8b20169d7c2206797a4e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 24 Nov 2017 16:42:44 +0000
Subject: [PATCH 10/28] Added tests and small fixes for TriMesh element
---
holoviews/element/graphs.py | 10 ++--
holoviews/plotting/bokeh/graphs.py | 10 +++-
tests/testbokehgraphs.py | 75 ++++++++++++++++++++++++++++--
tests/testgraphelement.py | 38 ++++++++++++++-
tests/testmplgraphs.py | 71 +++++++++++++++++++++++++++-
5 files changed, 188 insertions(+), 16 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index 2e3a80ed4a..93bf84e183 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -437,7 +437,7 @@ def __init__(self, data, kdims=None, vdims=None, **params):
# Try assuming data contains just coordinates (2 columns)
try:
points = Points(nodes)
- nodes = Nodes(Dataset(points).add_dimension('index', 2, np.arange(len(nodes))))
+ nodes = Nodes(Dataset(points).add_dimension('index', 2, np.arange(len(points))))
except:
raise ValueError("Nodes argument could not be interpreted, expected "
"data with two or three columns representing the "
@@ -464,7 +464,7 @@ def edgepaths(self):
self._edgepaths = edgepaths
return edgepaths
- def select(self, selection_specs=None, selection_mode='edges', **selection):
+ 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
@@ -473,12 +473,8 @@ def select(self, selection_specs=None, selection_mode='edges', **selection):
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.
-
- Selecting by a node dimensions selects all edges and nodes that are
- connected to the selected nodes. To select only edges between the
- selected nodes set the selection_mode to 'nodes'.
"""
self.edgepaths
return super(TriMesh, self).select(selection_specs=None,
- selection_mode='edges',
+ selection_mode='nodes',
**selection)
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index f63b76597c..40c166df22 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -42,6 +42,9 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot):
# Filled is only supported for subclasses
filled = False
+ # Declares which columns in the data refer to node indices
+ _node_indices = [0, 1]
+
def _hover_opts(self, element):
if self.inspection_policy == 'nodes':
dims = element.nodes.dimensions()
@@ -77,7 +80,7 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
idx = element.get_dimension_index(cdim)
field = dimension_sanitizer(cdim.name)
cvals = element.dimension_values(cdim)
- if idx in [0, 1]:
+ if idx in self._node_indices:
factors = element.nodes.dimension_values(2, expanded=False)
elif idx == 2 and cvals.dtype.kind in 'if':
factors = None
@@ -86,7 +89,7 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
default_cmap = 'viridis' if factors is None else 'tab20'
cmap = style.get('edge_cmap', style.get('cmap', default_cmap))
- if factors is None or (factors.dtype.kind == 'f' and idx not in [0, 1]):
+ if factors is None or (factors.dtype.kind in 'if' and idx not in self._node_indices):
colors, factors = None, None
else:
if factors.dtype.kind == 'f':
@@ -277,6 +280,9 @@ class TriMeshPlot(GraphPlot):
['node_'+p for p in fill_properties+line_properties] +
['node_size', 'cmap', 'edge_cmap'])
+ # Declares that three columns in TriMesh refer to edges
+ _node_indices = [0, 1, 2]
+
def get_data(self, element, ranges, style):
# Ensure the edgepaths for the triangles are generated
element.edgepaths
diff --git a/tests/testbokehgraphs.py b/tests/testbokehgraphs.py
index b596e82e22..03a2718882 100644
--- a/tests/testbokehgraphs.py
+++ b/tests/testbokehgraphs.py
@@ -5,21 +5,20 @@
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 import Graph, TriMesh, circular_layout
from holoviews.element.comparison import ComparisonTestCase
from holoviews.plotting import comms
try:
from holoviews.plotting.bokeh.util import bokeh_version
bokeh_renderer = Store.renderers['bokeh']
- from bokeh.models import (NodesAndLinkedEdges, EdgesAndLinkedNodes)
+ from bokeh.models import (NodesAndLinkedEdges, EdgesAndLinkedNodes, Patches)
from bokeh.models.mappers import CategoricalColorMapper, LinearColorMapper
except :
bokeh_renderer = None
class BokehGraphPlotTests(ComparisonTestCase):
-
def setUp(self):
if not bokeh_renderer:
@@ -156,7 +155,7 @@ def test_graph_edges_categorical_colormapped(self):
self.assertEqual(cmapper.factors, factors)
self.assertEqual(edge_source.data['start_str'], factors)
self.assertEqual(glyph.line_color, {'field': 'start_str', 'transform': cmapper})
-
+
def test_graph_nodes_numerically_colormapped(self):
g = self.graph4.opts(plot=dict(edge_color_index='Weight'),
style=dict(edge_cmap=['#FFFFFF', '#000000']))
@@ -169,3 +168,71 @@ def test_graph_nodes_numerically_colormapped(self):
self.assertEqual(cmapper.high, self.weights.max())
self.assertEqual(edge_source.data['Weight'], self.node_info2['Weight'])
self.assertEqual(glyph.line_color, {'field': 'Weight', 'transform': cmapper})
+
+
+class TestBokehTriMeshPlots(ComparisonTestCase):
+
+ def setUp(self):
+ if not bokeh_renderer:
+ raise SkipTest("Bokeh required to test plot instantiation")
+ self.previous_backend = Store.current_backend
+ Store.current_backend = 'bokeh'
+ self.default_comm = bokeh_renderer.comms['default']
+
+ self.nodes = [(0, 0, 0), (0.5, 1, 1), (1., 0, 2), (1.5, 1, 3)]
+ self.simplices = [(0, 1, 2, 0), (1, 2, 3, 1)]
+ self.trimesh = TriMesh((self.simplices, self.nodes))
+ self.trimesh_weighted = TriMesh((self.simplices, self.nodes), vdims='weight')
+
+ def tearDown(self):
+ Store.current_backend = self.previous_backend
+ bokeh_renderer.comms['default'] = self.default_comm
+
+ def test_plot_simple_trimesh(self):
+ plot = bokeh_renderer.get_plot(self.trimesh)
+ 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'], np.arange(4))
+ self.assertEqual(edge_source.data['start'], np.arange(2))
+ self.assertEqual(edge_source.data['end'], np.arange(1, 3))
+ layout = {str(int(z)): (x, y) for x, y, z in self.trimesh.nodes.array()}
+ self.assertEqual(layout_source.graph_layout, layout)
+
+ def test_plot_simple_trimesh_filled(self):
+ plot = bokeh_renderer.get_plot(self.trimesh.opts(plot=dict(filled=True)))
+ node_source = plot.handles['scatter_1_source']
+ edge_source = plot.handles['patches_1_source']
+ layout_source = plot.handles['layout_source']
+ self.assertIsInstance(plot.handles['patches_1_glyph'], Patches)
+ self.assertEqual(node_source.data['index'], np.arange(4))
+ self.assertEqual(edge_source.data['start'], np.arange(2))
+ self.assertEqual(edge_source.data['end'], np.arange(1, 3))
+ layout = {str(int(z)): (x, y) for x, y, z in self.trimesh.nodes.array()}
+ self.assertEqual(layout_source.graph_layout, layout)
+
+ def test_graph_edges_categorical_colormapped(self):
+ g = self.trimesh.opts(plot=dict(edge_color_index='node1'),
+ style=dict(edge_cmap=['#FFFFFF', '#000000']))
+ plot = bokeh_renderer.get_plot(g)
+ cmapper = plot.handles['edge_colormapper']
+ edge_source = plot.handles['multi_line_1_source']
+ glyph = plot.handles['multi_line_1_glyph']
+ self.assertIsInstance(cmapper, CategoricalColorMapper)
+ factors = ['0', '1', '2', '3']
+ self.assertEqual(cmapper.factors, factors)
+ self.assertEqual(edge_source.data['node1_str'], ['0', '1'])
+ self.assertEqual(glyph.line_color, {'field': 'node1_str', 'transform': cmapper})
+
+ def test_graph_nodes_numerically_colormapped(self):
+ g = self.trimesh_weighted.opts(plot=dict(edge_color_index='weight'),
+ style=dict(edge_cmap=['#FFFFFF', '#000000']))
+ plot = bokeh_renderer.get_plot(g)
+ cmapper = plot.handles['edge_colormapper']
+ edge_source = plot.handles['multi_line_1_source']
+ glyph = plot.handles['multi_line_1_glyph']
+ self.assertIsInstance(cmapper, LinearColorMapper)
+ self.assertEqual(cmapper.low, 0)
+ self.assertEqual(cmapper.high, 1)
+ self.assertEqual(edge_source.data['weight'], np.array([0, 1]))
+ self.assertEqual(glyph.line_color, {'field': 'weight', 'transform': cmapper})
diff --git a/tests/testgraphelement.py b/tests/testgraphelement.py
index 7547b9f123..da4b48f74b 100644
--- a/tests/testgraphelement.py
+++ b/tests/testgraphelement.py
@@ -3,8 +3,10 @@
"""
import numpy as np
from holoviews.core.data import Dataset
+from holoviews.element.chart import Points
from holoviews.element.graphs import (
- Graph, Nodes, circular_layout, connect_edges, connect_edges_pd)
+ Graph, Nodes, EdgePaths, TriMesh, circular_layout, connect_edges,
+ connect_edges_pd)
from holoviews.element.comparison import ComparisonTestCase
@@ -123,3 +125,37 @@ def test_graph_redim_nodes(self):
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'))
+
+
+class TriMeshTests(ComparisonTestCase):
+
+ def setUp(self):
+ self.nodes = [(0, 0, 0), (0.5, 1, 1), (1., 0, 2), (1.5, 1, 3)]
+ self.simplices = [(0, 1, 2), (1, 2, 3)]
+
+ def test_trimesh_constructor(self):
+ trimesh = TriMesh((self.simplices, self.nodes))
+ self.assertEqual(trimesh.array(), np.array(self.simplices))
+ self.assertEqual(trimesh.nodes.array(), np.array(self.nodes))
+
+ def test_trimesh_constructor_tuple_nodes(self):
+ nodes = tuple(zip(*self.nodes))[:2]
+ trimesh = TriMesh((self.simplices, nodes))
+ self.assertEqual(trimesh.array(), np.array(self.simplices))
+ self.assertEqual(trimesh.nodes.array(), np.array(self.nodes))
+
+ def test_trimesh_constructor_point_nodes(self):
+ trimesh = TriMesh((self.simplices, Points([n[:2] for n in self.nodes])))
+ self.assertEqual(trimesh.array(), np.array(self.simplices))
+ self.assertEqual(trimesh.nodes.array(), np.array(self.nodes))
+
+ def test_trimesh_edgepaths(self):
+ trimesh = TriMesh((self.simplices, self.nodes))
+ paths = [np.array([(0, 0), (0.5, 1), (1, 0), (0, 0)]),
+ np.array([(0.5, 1), (1, 0), (1.5, 1), (0.5, 1)])]
+ for p1, p2 in zip(trimesh.edgepaths.split(datatype='array'), paths):
+ self.assertEqual(p1, p2)
+
+ def test_trimesh_select(self):
+ trimesh = TriMesh((self.simplices, self.nodes)).select(x=(0.1, None))
+ self.assertEqual(trimesh.array(), np.array(self.simplices[1:]))
diff --git a/tests/testmplgraphs.py b/tests/testmplgraphs.py
index 8a27b67b4a..4276294f32 100644
--- a/tests/testmplgraphs.py
+++ b/tests/testmplgraphs.py
@@ -4,8 +4,8 @@
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.core.options import Store, Cycle
+from holoviews.element import Graph, TriMesh, circular_layout
from holoviews.element.comparison import ComparisonTestCase
from holoviews.plotting import comms
@@ -14,6 +14,7 @@
from matplotlib import pyplot
pyplot.switch_backend('agg')
from holoviews.plotting.mpl import OverlayPlot
+ from matplotlib.collections import LineCollection, PolyCollection
mpl_renderer = Store.renderers['matplotlib']
except:
mpl_renderer = None
@@ -97,3 +98,69 @@ def test_plot_graph_numerically_colored_edges(self):
edges = plot.handles['edges']
self.assertEqual(edges.get_array(), self.weights)
self.assertEqual(edges.get_clim(), (self.weights.min(), self.weights.max()))
+
+
+
+class TestMplTriMeshPlots(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, '')
+
+ self.nodes = [(0, 0, 0), (0.5, 1, 1), (1., 0, 2), (1.5, 1, 3)]
+ self.simplices = [(0, 1, 2, 0), (1, 2, 3, 1)]
+ self.trimesh = TriMesh((self.simplices, self.nodes))
+ self.trimesh_weighted = TriMesh((self.simplices, self.nodes), vdims='weight')
+
+ def tearDown(self):
+ mpl_renderer.comms['default'] = self.default_comm
+ Store.current_backend = self.previous_backend
+
+ def test_plot_simple_trimesh(self):
+ plot = mpl_renderer.get_plot(self.trimesh)
+ nodes = plot.handles['nodes']
+ edges = plot.handles['edges']
+ self.assertIsInstance(edges, LineCollection)
+ self.assertEqual(nodes.get_offsets(), self.trimesh.nodes.array([0, 1]))
+ self.assertEqual([p.vertices for p in edges.get_paths()],
+ [p.array() for p in self.trimesh.edgepaths.split()])
+
+ def test_plot_simple_trimesh_filled(self):
+ plot = mpl_renderer.get_plot(self.trimesh.opts(plot=dict(filled=True)))
+ nodes = plot.handles['nodes']
+ edges = plot.handles['edges']
+ self.assertIsInstance(edges, PolyCollection)
+ self.assertEqual(nodes.get_offsets(), self.trimesh.nodes.array([0, 1]))
+ self.assertEqual([p.vertices for p in edges.get_paths()],
+ [p.array()[[0, 1, 2, 3, 3]]
+ for p in self.trimesh.edgepaths.split()])
+
+ def test_plot_trimesh_colored_edges(self):
+ opts = dict(plot=dict(edge_color_index='weight'), style=dict(edge_cmap='Greys'))
+ plot = mpl_renderer.get_plot(self.trimesh_weighted.opts(**opts))
+ edges = plot.handles['edges']
+ colors = np.array([[ 1., 1., 1., 1.],
+ [ 0., 0., 0., 1.]])
+ self.assertEqual(edges.get_edgecolors(), colors)
+
+ def test_plot_trimesh_categorically_colored_edges(self):
+ opts = dict(plot=dict(edge_color_index='node1'), style=dict(edge_color=Cycle('Set1')))
+ plot = mpl_renderer.get_plot(self.trimesh_weighted.opts(**opts))
+ edges = plot.handles['edges']
+ colors = np.array([[0.894118, 0.101961, 0.109804, 1.],
+ [0.215686, 0.494118, 0.721569, 1.]])
+ self.assertEqual(edges.get_edgecolors(), colors)
+
+ def test_plot_trimesh_categorically_colored_edges_filled(self):
+ opts = dict(plot=dict(edge_color_index='node1', filled=True),
+ style=dict(edge_color=Cycle('Set1')))
+ plot = mpl_renderer.get_plot(self.trimesh_weighted.opts(**opts))
+ edges = plot.handles['edges']
+ colors = np.array([[0.894118, 0.101961, 0.109804, 1.],
+ [0.215686, 0.494118, 0.721569, 1.]])
+ self.assertEqual(edges.get_facecolors(), colors)
+
From 58b98b044a5a5ea5b8bcaf878a233f52ebdc3b18 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Tue, 28 Nov 2017 21:43:38 +0000
Subject: [PATCH 11/28] Various TriMesh improvements
---
holoviews/element/graphs.py | 37 +++++++++++++++++++++++++++++--------
1 file changed, 29 insertions(+), 8 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index 93bf84e183..b5552fbb9e 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -414,6 +414,10 @@ class TriMesh(Graph):
group = param.String(default='TriMesh', constant=True)
+ _node_type = Nodes
+
+ _edge_type = EdgePaths
+
def __init__(self, data, kdims=None, vdims=None, **params):
if isinstance(data, tuple):
data = data + (None,)*(3-len(data))
@@ -424,30 +428,46 @@ def __init__(self, data, kdims=None, vdims=None, **params):
raise ValueError("TriMesh expects both simplices and nodes "
"to be supplied.")
- if isinstance(nodes, Nodes):
+ if isinstance(nodes, self._node_type):
pass
elif isinstance(nodes, Points):
# Add index to make it a valid Nodes object
- nodes = Nodes(Dataset(nodes).add_dimension('index', 2, np.arange(len(nodes))))
+ nodes = self._node_type(Dataset(nodes).add_dimension('index', 2, np.arange(len(nodes))))
elif not isinstance(nodes, Dataset) or nodes.ndims in [2, 3]:
try:
# Try assuming data contains indices (3 columns)
- nodes = Nodes(nodes)
+ nodes = self._node_type(nodes)
except:
# Try assuming data contains just coordinates (2 columns)
try:
points = Points(nodes)
- nodes = Nodes(Dataset(points).add_dimension('index', 2, np.arange(len(points))))
+ ds = Dataset(points).add_dimension('index', 2, np.arange(len(points)))
+ nodes = self._node_type(ds)
except:
raise ValueError("Nodes argument could not be interpreted, expected "
"data with two or three columns representing the "
"x/y positions and optionally the node indices.")
- if edgepaths is not None and not isinstance(edgepaths, EdgePaths):
- edgepaths = EdgePaths(edgepaths)
+ if edgepaths is not None and not isinstance(edgepaths, self._edge_type):
+ edgepaths = self._edge_type(edgepaths)
super(TriMesh, self).__init__(edges, kdims=kdims, vdims=vdims, **params)
self._nodes = nodes
self._edgepaths = edgepaths
+ @classmethod
+ def from_points(cls, points):
+ """
+ Uses Delauney triangulation to compute triangle simplices for
+ each point.
+ """
+ try:
+ from scipy.spatial import Delaunay
+ except:
+ raise ImportError("Generating triangles from points requires, "
+ "SciPy to be installed.")
+ if not isinstance(points, Points):
+ points = Points(points)
+ tris = Delaunay(points.array([0, 1]))
+ return cls((tris.simplices, points))
@property
def edgepaths(self):
@@ -459,8 +479,9 @@ def edgepaths(self):
simplices = self.array([0, 1, 2]).astype(np.int32)
pts = self.nodes.array([0, 1])
- paths = [tri[[0, 1, 2, 0], :] for tri in pts[simplices]]
- edgepaths = EdgePaths(paths, kdims=self.nodes.kdims[:2])
+ empty = np.array([[np.NaN, np.NaN]])
+ paths = [arr for tri in pts[simplices] for arr in (tri[[0, 1, 2, 0], :], empty)][:-1]
+ edgepaths = self._edge_type([np.concatenate(paths)], kdims=self.nodes.kdims[:2])
self._edgepaths = edgepaths
return edgepaths
From 2dcbc26e77e92bab147f9c1b756fc0e0dee0797e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Tue, 28 Nov 2017 21:44:36 +0000
Subject: [PATCH 12/28] Added trimesh rasterization and generalized API
---
holoviews/operation/datashader.py | 81 ++++++++++++++++++++++++++++---
1 file changed, 74 insertions(+), 7 deletions(-)
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index 104ca30a10..244198da96 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -1,6 +1,7 @@
from __future__ import absolute_import, division
from collections import Callable, Iterable
+from functools import partial
from distutils.version import LooseVersion
import warnings
@@ -25,7 +26,7 @@
from ..core.data import PandasInterface, XArrayInterface
from ..core.sheetcoords import BoundingBox
from ..core.util import get_param_values, basestring, datetime_types, dt_to_int
-from ..element import Image, Path, Curve, RGB, Graph
+from ..element import Image, Path, Curve, RGB, Graph, TriMesh, Points, Scatter, Dataset
from ..streams import RangeXY, PlotSize
@@ -184,7 +185,7 @@ class aggregate(ResamplingOperation):
"""
aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
- default=ds.count())
+ default=None)
@classmethod
def get_agg_data(cls, obj, category=None):
@@ -264,7 +265,8 @@ def _aggregate_ndoverlay(self, element, agg_fn):
"""
# Compute overall bounds
x, y = element.last.dimensions()[0:2]
- (x_range, y_range), (xs, ys), (width, height), (xtype, ytype) = self._get_sampling(element, x, y)
+ info = self._get_sampling(element, x, y)
+ (x_range, y_range), (xs, ys), (width, height), (xtype, ytype) = info
agg_params = dict({k: v for k, v in self.p.items() if k in aggregate.params()},
x_range=x_range, y_range=y_range)
@@ -343,7 +345,7 @@ def _process(self, element, key=None):
cvs = ds.Canvas(plot_width=width, plot_height=height,
x_range=x_range, y_range=y_range)
- column = agg_fn.column
+ column = agg_fn.column if agg_fn else None
if column and isinstance(agg_fn, ds.count_cat):
name = '%s Count' % agg_fn.column
else:
@@ -420,7 +422,8 @@ def _process(self, element, key=None):
raise RuntimeError('regrid operation requires datashader>=0.6.0')
x, y = element.kdims
- (x_range, y_range), _, (width, height), (xtype, ytype) = self._get_sampling(element, x, y)
+ info = self._get_sampling(element, x, y)
+ (x_range, y_range), _, (width, height), (xtype, ytype) = info
coords = tuple(element.dimension_values(d, expanded=False)
for d in [x, y])
@@ -478,6 +481,70 @@ def _process(self, element, key=None):
return element.clone(regridded, bounds=bbox, datatype=['xarray'])
+class trimesh_rasterize(aggregate):
+ """
+ Rasterize the TriMesh element using the supplied aggregator. If
+ the TriMesh nodes or edges define a value dimension will plot
+ filled and shaded polygons otherwise returns a wiremesh of the
+ data.
+ """
+
+ aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
+ default=None)
+
+ def _process(self, element, key=None):
+ x, y = element.nodes.kdims[:2]
+ info = self._get_sampling(element, x, y)
+ (x_range, y_range), _, (width, height), (xtype, ytype) = info
+ cvs = ds.Canvas(plot_width=width, plot_height=height,
+ x_range=x_range, y_range=y_range)
+
+ if element.vdims:
+ simplices = element.dframe([0, 1, 2, 3])
+ pts = element.nodes.dframe([0, 1])
+ vdim = element.vdims[0]
+ elif element.nodes.vdims:
+ simplices = element.dframe([0, 1, 2])
+ pts = element.nodes.dframe([0, 1, 3])
+ vdim = element.nodes.vdims[0]
+ else:
+ return aggregate._process(self, element, key)
+
+ agg = cvs.trimesh(pts, simplices, agg=self.p.aggregator)
+ params = dict(get_param_values(element), kdims=[x, y],
+ datatype=['xarray'], vdims=[vdim])
+ return Image(agg, **params)
+
+
+class rasterize(trimesh_rasterize):
+ """
+ Rasterize is a high-level operation which will rasterize any
+ Element or combination of Elements supplied as an (Nd)Overlay
+ by aggregating with the supplied aggregation function.
+ """
+
+ aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
+ default=ds.count())
+
+ def _process(self, element, key=None):
+ # Get input Images to avoid multiple rasterization
+ imgs = element.traverse(lambda x: x, [Image])
+
+ # Rasterize TriMeshes
+ trirasterize = partial(trimesh_rasterize._process, self)
+ element = element.map(trirasterize, TriMesh)
+
+ # Rasterize NdOverlay of objects
+ dsrasterize = partial(aggregate._process, self)
+ predicate = lambda x: (isinstance(x, NdOverlay) and issubclass(x.type, Dataset)
+ and not issubclass(x.type, Image))
+ element = element.map(dsrasterize, predicate)
+
+ # Rasterize other Dataset types
+ predicate = lambda x: isinstance(x, Dataset) and (not isinstance(x, Image) or x in imgs)
+ element = element.map(dsrasterize, predicate)
+ return element
+
class shade(Operation):
"""
@@ -623,7 +690,7 @@ def _process(self, element, key=None):
-class datashade(aggregate, shade):
+class datashade(rasterize, shade):
"""
Applies the aggregate and shade operations, aggregating all
elements in the supplied object and then applying normalization
@@ -633,7 +700,7 @@ class datashade(aggregate, shade):
"""
def _process(self, element, key=None):
- agg = aggregate._process(self, element, key)
+ agg = rasterize._process(self, element, key)
shaded = shade._process(self, agg, key)
return shaded
From d16a637c3f0588e903c703f74201d954360701fa Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sun, 3 Dec 2017 02:31:09 +0000
Subject: [PATCH 13/28] Split matplotlib paths before displaying them
---
holoviews/element/graphs.py | 2 +-
holoviews/plotting/mpl/graphs.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index b5552fbb9e..fa0056c8bb 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -293,7 +293,7 @@ def select(self, selection_specs=None, selection_mode='edges', **selection):
@property
def _split_edgepaths(self):
- if len(self) == len(self._edgepaths.data):
+ if len(self) == len(self.edgepaths.data):
return self._edgepaths
else:
return self._edgepaths.clone(split_path(self._edgepaths))
diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py
index 183521d034..2e69ba5411 100644
--- a/holoviews/plotting/mpl/graphs.py
+++ b/holoviews/plotting/mpl/graphs.py
@@ -90,7 +90,7 @@ def get_data(self, element, ranges, style):
dims = element.nodes.dimensions()
self._compute_styles(element, ranges, style)
- paths = element.edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims)
+ paths = element._split_edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims)
if self.invert_axes:
paths = [p[:, ::-1] for p in paths]
return {'nodes': (pxs, pys), 'edges': paths}, style, {'dimensions': dims}
From d5a6ea6e383b17eb2afec326ed7cd98abeb724f9 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sun, 3 Dec 2017 13:40:19 +0000
Subject: [PATCH 14/28] Optimized static bokeh Graph plots
---
holoviews/plotting/bokeh/graphs.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index 40c166df22..3c25b3c772 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -115,6 +115,10 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
def get_data(self, element, ranges, style):
+ # Force static source to False
+ static = self.static_source
+ self.handles['static_source'] = static
+ self.static_source = False
xidx, yidx = (1, 0) if self.invert_axes else (0, 1)
# Get node data
@@ -159,7 +163,7 @@ def get_data(self, element, ranges, style):
end = np.array([node_indices.get(y, nan_node) for y in end], dtype=np.int32)
path_data = dict(start=start, end=end)
self._get_edge_colors(element, ranges, path_data, edge_mapping, style)
- if element._edgepaths and not self.static_source:
+ if element._edgepaths and not static:
edges = element._split_edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims)
if len(edges) == len(start):
path_data['xs'] = [path[:, 0] for path in edges]
@@ -189,7 +193,10 @@ def _update_datasource(self, source, data):
Update datasource with data for a new frame.
"""
if isinstance(source, ColumnDataSource):
- source.data.update(data)
+ if self.handles['static_source']:
+ source.trigger('data')
+ else:
+ source.data.update(data)
else:
source.graph_layout = data
From 3f2a28d50b5641a85855079972067f0379bd1056 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sun, 3 Dec 2017 15:24:37 +0000
Subject: [PATCH 15/28] Fixed various Graph edgepaths bugs
---
holoviews/element/graphs.py | 12 +++++++-----
holoviews/element/util.py | 7 ++++---
tests/testmplgraphs.py | 8 ++++----
3 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index fa0056c8bb..73fd3de16f 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -294,9 +294,9 @@ def select(self, selection_specs=None, selection_mode='edges', **selection):
@property
def _split_edgepaths(self):
if len(self) == len(self.edgepaths.data):
- return self._edgepaths
+ return self.edgepaths
else:
- return self._edgepaths.clone(split_path(self._edgepaths))
+ return self.edgepaths.clone(split_path(self.edgepaths))
def range(self, dimension, data_range=True):
@@ -478,10 +478,12 @@ def edgepaths(self):
return self._edgepaths
simplices = self.array([0, 1, 2]).astype(np.int32)
- pts = self.nodes.array([0, 1])
+ pts = self.nodes.array([0, 1]).astype(float)
empty = np.array([[np.NaN, np.NaN]])
- paths = [arr for tri in pts[simplices] for arr in (tri[[0, 1, 2, 0], :], empty)][:-1]
- edgepaths = self._edge_type([np.concatenate(paths)], kdims=self.nodes.kdims[:2])
+ paths = [arr for tri in pts[simplices] for arr in
+ (tri[[0, 1, 2, 0], :], empty)][:-1]
+ edgepaths = self._edge_type([np.concatenate(paths)],
+ kdims=self.nodes.kdims[:2])
self._edgepaths = edgepaths
return edgepaths
diff --git a/holoviews/element/util.py b/holoviews/element/util.py
index f9084be52b..94bab31bb7 100644
--- a/holoviews/element/util.py
+++ b/holoviews/element/util.py
@@ -46,13 +46,14 @@ def split_path(path):
Split a Path type containing a single NaN separated path into
multiple subpaths.
"""
- path = path.split()[0]
+ path = path.split(0, 1)[0]
values = path.dimension_values(0)
- splits = np.concatenate([[0], np.where(np.isnan(values))[0]+1, [0]])
+ splits = np.concatenate([[0], np.where(np.isnan(values))[0]+1, [None]])
subpaths = []
data = PandasInterface.as_dframe(path) if pd else path.array()
for i in range(len(splits)-1):
- slc = slice(splits[i], splits[i+1]-1)
+ end = splits[i+1]
+ slc = slice(splits[i], None if end is None else end-1)
subpath = data.iloc[slc] if pd else data[slc]
if len(subpath):
subpaths.append(subpath)
diff --git a/tests/testmplgraphs.py b/tests/testmplgraphs.py
index 4276294f32..6476eadf9f 100644
--- a/tests/testmplgraphs.py
+++ b/tests/testmplgraphs.py
@@ -127,7 +127,7 @@ def test_plot_simple_trimesh(self):
self.assertIsInstance(edges, LineCollection)
self.assertEqual(nodes.get_offsets(), self.trimesh.nodes.array([0, 1]))
self.assertEqual([p.vertices for p in edges.get_paths()],
- [p.array() for p in self.trimesh.edgepaths.split()])
+ [p.array() for p in self.trimesh._split_edgepaths.split()])
def test_plot_simple_trimesh_filled(self):
plot = mpl_renderer.get_plot(self.trimesh.opts(plot=dict(filled=True)))
@@ -135,9 +135,9 @@ def test_plot_simple_trimesh_filled(self):
edges = plot.handles['edges']
self.assertIsInstance(edges, PolyCollection)
self.assertEqual(nodes.get_offsets(), self.trimesh.nodes.array([0, 1]))
- self.assertEqual([p.vertices for p in edges.get_paths()],
- [p.array()[[0, 1, 2, 3, 3]]
- for p in self.trimesh.edgepaths.split()])
+ paths = self.trimesh._split_edgepaths.split(datatype='array')
+ self.assertEqual([p.vertices[:4] for p in edges.get_paths()],
+ paths)
def test_plot_trimesh_colored_edges(self):
opts = dict(plot=dict(edge_color_index='weight'), style=dict(edge_cmap='Greys'))
From d8cfd66fa0504d7bf1de1313bfaca8ca1d178b5b Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sun, 3 Dec 2017 15:52:48 +0000
Subject: [PATCH 16/28] Restored default datashading aggregator
---
holoviews/operation/datashader.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index 244198da96..d44187cdbd 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -185,7 +185,7 @@ class aggregate(ResamplingOperation):
"""
aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
- default=None)
+ default=ds.count())
@classmethod
def get_agg_data(cls, obj, category=None):
From 1c9585df8b579c98cdd6eb405a7e5d1c38b1dd1e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sun, 3 Dec 2017 15:57:52 +0000
Subject: [PATCH 17/28] Updated unit test
---
tests/testgraphelement.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/testgraphelement.py b/tests/testgraphelement.py
index da4b48f74b..06a9dd52b9 100644
--- a/tests/testgraphelement.py
+++ b/tests/testgraphelement.py
@@ -151,8 +151,8 @@ def test_trimesh_constructor_point_nodes(self):
def test_trimesh_edgepaths(self):
trimesh = TriMesh((self.simplices, self.nodes))
- paths = [np.array([(0, 0), (0.5, 1), (1, 0), (0, 0)]),
- np.array([(0.5, 1), (1, 0), (1.5, 1), (0.5, 1)])]
+ paths = [np.array([(0, 0), (0.5, 1), (1, 0), (0, 0), (np.NaN, np.NaN),
+ (0.5, 1), (1, 0), (1.5, 1), (0.5, 1)])]
for p1, p2 in zip(trimesh.edgepaths.split(datatype='array'), paths):
self.assertEqual(p1, p2)
From 9f9b569ca1d4107f47d2467fb7a2267088c837b8 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 7 Dec 2017 13:02:10 +0000
Subject: [PATCH 18/28] Improved datashader dimension lookup
---
holoviews/operation/datashader.py | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index d44187cdbd..8a5385ce11 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -346,12 +346,17 @@ def _process(self, element, key=None):
x_range=x_range, y_range=y_range)
column = agg_fn.column if agg_fn else None
- if column and isinstance(agg_fn, ds.count_cat):
- name = '%s Count' % agg_fn.column
+ if column:
+ dims = [d for d in element.dimensions('ranges') if d == column]
+ if not dims:
+ raise ValueError("Aggregation column %s not found on %s element. "
+ "Ensure the aggregator references an existing "
+ "dimension.")
+ if isinstance(agg_fn, ds.count_cat):
+ name = '%s Count' % agg_fn.column
+ vdims = [dims[0](column)]
else:
- name = column
- vdims = [element.get_dimension(column)(name) if column
- else Dimension('Count')]
+ vdims = Dimension('Count')
params = dict(get_param_values(element), kdims=[x, y],
datatype=['xarray'], vdims=vdims)
From b7f04aecea3d4304e7690321af463830e5c1cf94 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 7 Dec 2017 13:02:31 +0000
Subject: [PATCH 19/28] Added interpolation support for trimesh rasterization
---
holoviews/operation/datashader.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index 8a5385ce11..ea576159b6 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -497,12 +497,18 @@ class trimesh_rasterize(aggregate):
aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
default=None)
+ interpolation = param.ObjectSelector(default='bilinear',
+ objects=['bilinear'], doc="""
+ The interpolation method to apply during rasterization.""")
+
def _process(self, element, key=None):
x, y = element.nodes.kdims[:2]
info = self._get_sampling(element, x, y)
(x_range, y_range), _, (width, height), (xtype, ytype) = info
+ interpolate = bool(self.p.interpolation)
cvs = ds.Canvas(plot_width=width, plot_height=height,
- x_range=x_range, y_range=y_range)
+ x_range=x_range, y_range=y_range,
+ interp=interpolate)
if element.vdims:
simplices = element.dframe([0, 1, 2, 3])
From 635a8ceadc9e3f89a7523f378d5ebb98dd118409 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 8 Dec 2017 16:47:30 +0000
Subject: [PATCH 20/28] Improved datashader operations
---
holoviews/operation/datashader.py | 37 ++++++++++++++++++++++---------
1 file changed, 27 insertions(+), 10 deletions(-)
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index ea576159b6..51c167e1d6 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -164,8 +164,8 @@ class aggregate(ResamplingOperation):
"""
aggregate implements 2D binning for any valid HoloViews Element
type using datashader. I.e., this operation turns a HoloViews
- Element or overlay of Elements into an hv.Image or an overlay of
- hv.Images by rasterizing it, which provides a fixed-sized
+ Element or overlay of Elements into an hImage or an overlay of
+ .Images by rasterizing it, which provides a fixed-sized
representation independent of the original dataset size.
By default it will simply count the number of values in each bin
@@ -498,17 +498,15 @@ class trimesh_rasterize(aggregate):
default=None)
interpolation = param.ObjectSelector(default='bilinear',
- objects=['bilinear'], doc="""
+ objects=['bilinear', None], doc="""
The interpolation method to apply during rasterization.""")
def _process(self, element, key=None):
x, y = element.nodes.kdims[:2]
info = self._get_sampling(element, x, y)
(x_range, y_range), _, (width, height), (xtype, ytype) = info
- interpolate = bool(self.p.interpolation)
cvs = ds.Canvas(plot_width=width, plot_height=height,
- x_range=x_range, y_range=y_range,
- interp=interpolate)
+ x_range=x_range, y_range=y_range)
if element.vdims:
simplices = element.dframe([0, 1, 2, 3])
@@ -521,7 +519,9 @@ def _process(self, element, key=None):
else:
return aggregate._process(self, element, key)
- agg = cvs.trimesh(pts, simplices, agg=self.p.aggregator)
+ interpolate = bool(self.p.interpolation)
+ agg = cvs.trimesh(pts, simplices, agg=self.p.aggregator,
+ interp=interpolate)
params = dict(get_param_values(element), kdims=[x, y],
datatype=['xarray'], vdims=[vdim])
return Image(agg, **params)
@@ -532,10 +532,25 @@ class rasterize(trimesh_rasterize):
Rasterize is a high-level operation which will rasterize any
Element or combination of Elements supplied as an (Nd)Overlay
by aggregating with the supplied aggregation function.
+
+ By default it will simply count the number of values in each bin
+ but other aggregators can be supplied implementing mean, max, min
+ and other reduction operations.
+
+ The bins of the aggregate are defined by the width and height and
+ the x_range and y_range. If x_sampling or y_sampling are supplied
+ the operation will ensure that a bin is no smaller than the minimum
+ sampling distance by reducing the width and height when zoomed in
+ beyond the minimum sampling distance.
+
+ By default, the PlotSize stream is applied when this operation
+ is used dynamically, which means that the height and width
+ will automatically be set to match the inner dimensions of
+ the linked plot.
"""
aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
- default=ds.count())
+ default=None)
def _process(self, element, key=None):
# Get input Images to avoid multiple rasterization
@@ -547,12 +562,14 @@ def _process(self, element, key=None):
# Rasterize NdOverlay of objects
dsrasterize = partial(aggregate._process, self)
- predicate = lambda x: (isinstance(x, NdOverlay) and issubclass(x.type, Dataset)
+ predicate = lambda x: (isinstance(x, NdOverlay) and
+ issubclass(x.type, Dataset)
and not issubclass(x.type, Image))
element = element.map(dsrasterize, predicate)
# Rasterize other Dataset types
- predicate = lambda x: isinstance(x, Dataset) and (not isinstance(x, Image) or x in imgs)
+ predicate = lambda x: (isinstance(x, Dataset) and
+ (not isinstance(x, Image) or x in imgs))
element = element.map(dsrasterize, predicate)
return element
From cf0d3c429ae19aa6dd39e57d44272343352f0473 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 8 Dec 2017 16:48:28 +0000
Subject: [PATCH 21/28] Improvements for bokeh GraphPlot
---
holoviews/plotting/bokeh/graphs.py | 72 +++++++++++++++++++-----------
1 file changed, 47 insertions(+), 25 deletions(-)
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index 3c25b3c772..77fbae3673 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -2,7 +2,7 @@
import numpy as np
from bokeh.models import HoverTool, ColumnDataSource
from bokeh.models import (StaticLayoutProvider, NodesAndLinkedEdges,
- EdgesAndLinkedNodes, Patches)
+ EdgesAndLinkedNodes, Patches, Bezier)
from ...core.util import basestring, dimension_sanitizer, unique_array
from ...core.options import Cycle
@@ -33,7 +33,7 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot):
A list of plugin tools to use on the plot.""")
# Map each glyph to a style group
- _style_groups = {'scatter': 'node', 'multi_line': 'edge', 'patches': 'edge'}
+ _style_groups = {'scatter': 'node', 'multi_line': 'edge', 'patches': 'edge', 'bezier': 'edge'}
style_opts = (['edge_'+p for p in line_properties] +
['node_'+p for p in fill_properties+line_properties] +
@@ -42,6 +42,9 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot):
# Filled is only supported for subclasses
filled = False
+ # Bezier paths
+ bezier = False
+
# Declares which columns in the data refer to node indices
_node_indices = [0, 1]
@@ -114,12 +117,25 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
edge_mapping['edge_selection_'+color_type] = transform
+ def _get_edge_paths(self, element):
+ path_data = {}
+ xidx, yidx = (1, 0) if self.invert_axes else (0, 1)
+ if element._edgepaths:
+ edges = element._split_edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims)
+ if len(edges) == len(element):
+ path_data['xs'] = [path[:, xidx] for path in edges]
+ path_data['ys'] = [path[:, yidx] for path in edges]
+ else:
+ self.warning('Graph edge paths do not match the number of abstract edges '
+ 'and will be skipped')
+ return path_data, {'xs': 'xs', 'ys': 'ys'}
+
+
def get_data(self, element, ranges, style):
# Force static source to False
static = self.static_source
self.handles['static_source'] = static
self.static_source = False
- xidx, yidx = (1, 0) if self.invert_axes else (0, 1)
# Get node data
nodes = element.nodes.dimension_values(2)
@@ -152,7 +168,6 @@ def get_data(self, element, ranges, style):
['node_fill_color', 'node_nonselection_fill_color']}
point_mapping['node_nonselection_fill_color'] = point_mapping['node_fill_color']
- # Get edge data
edge_mapping = {}
nan_node = index.max()+1
start, end = (element.dimension_values(i) for i in range(2))
@@ -163,14 +178,10 @@ def get_data(self, element, ranges, style):
end = np.array([node_indices.get(y, nan_node) for y in end], dtype=np.int32)
path_data = dict(start=start, end=end)
self._get_edge_colors(element, ranges, path_data, edge_mapping, style)
- if element._edgepaths and not static:
- edges = element._split_edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims)
- if len(edges) == len(start):
- path_data['xs'] = [path[:, 0] for path in edges]
- path_data['ys'] = [path[:, 1] for path in edges]
- else:
- self.warning('Graph edge paths do not match the number of abstract edges '
- 'and will be skipped')
+ if not static:
+ pdata, pmapping = self._get_edge_paths(element)
+ path_data.update(pdata)
+ edge_mapping.update(pmapping)
# Get hover data
if any(isinstance(t, HoverTool) for t in self.state.tools):
@@ -182,11 +193,19 @@ def get_data(self, element, ranges, style):
elif self.inspection_policy == 'edges':
for d in element.dimensions():
path_data[dimension_sanitizer(d.name)] = element.dimension_values(d)
- edge_glyph = 'patches_1' if self.filled else 'multi_line_1'
- data = {'scatter_1': point_data, edge_glyph: path_data, 'layout': layout}
- mapping = {'scatter_1': point_mapping, edge_glyph: edge_mapping}
+ data = {'scatter_1': point_data, self.edge_glyph: path_data, 'layout': layout}
+ mapping = {'scatter_1': point_mapping, self.edge_glyph: edge_mapping}
return data, mapping, style
+ @property
+ def edge_glyph(self):
+ if self.filled:
+ edge_glyph = 'patches_1'
+ elif self.bezier:
+ edge_glyph = 'bezier_1'
+ else:
+ edge_glyph = 'multi_line_1'
+ return edge_glyph
def _update_datasource(self, source, data):
"""
@@ -205,13 +224,14 @@ def _init_glyphs(self, plot, element, ranges, source):
# Get data and initialize data source
style = self.style[self.cyclic_index]
data, mapping, style = self.get_data(element, ranges, style)
+ edge_mapping = {k: v for k, v in mapping[self.edge_glyph].items()
+ if 'color' not in k}
self.handles['previous_id'] = element._plot_id
- edge_glyph = 'patches_1' if self.filled else 'multi_line_1'
properties = {}
mappings = {}
for key in list(mapping):
- if not any(glyph in key for glyph in ('scatter_1', edge_glyph)):
+ if not any(glyph in key for glyph in ('scatter_1', self.edge_glyph)):
continue
source = self._init_datasource(data.pop(key, {}))
self.handles[key+'_source'] = source
@@ -229,7 +249,7 @@ def _init_glyphs(self, plot, element, ranges, source):
# Define static layout
layout = StaticLayoutProvider(graph_layout=layout)
node_source = self.handles['scatter_1_source']
- edge_source = self.handles[edge_glyph+'_source']
+ edge_source = self.handles[self.edge_glyph+'_source']
renderer = plot.graph(node_source, edge_source, layout, **properties)
# Initialize GraphRenderer
@@ -250,19 +270,20 @@ def _init_glyphs(self, plot, element, ranges, source):
self.handles['layout_source'] = layout
self.handles['glyph_renderer'] = renderer
self.handles['scatter_1_glyph_renderer'] = renderer.node_renderer
- self.handles[edge_glyph+'_glyph_renderer'] = renderer.edge_renderer
+ self.handles[self.edge_glyph+'_glyph_renderer'] = renderer.edge_renderer
self.handles['scatter_1_glyph'] = renderer.node_renderer.glyph
- if self.filled:
- allowed_properties = Patches.properties()
+ if self.filled or self.bezier:
+ glyph_model = Patches if self.filled else Bezier
+ allowed_properties = glyph_model.properties()
for glyph_type in ('', 'selection_', 'nonselection_', 'hover_', 'muted_'):
glyph = getattr(renderer.edge_renderer, glyph_type+'glyph', None)
if glyph is None:
continue
- props = self._process_properties(edge_glyph, properties, mappings)
+ props = self._process_properties(self.edge_glyph, properties, mappings)
filtered = self._filter_properties(props, glyph_type, allowed_properties)
- patches = Patches(**dict(filtered, xs='xs', ys='ys'))
- glyph = setattr(renderer.edge_renderer, glyph_type+'glyph', patches)
- self.handles[edge_glyph+'_glyph'] = renderer.edge_renderer.glyph
+ new_glyph = glyph_model(**dict(filtered, **edge_mapping))
+ setattr(renderer.edge_renderer, glyph_type+'glyph', new_glyph)
+ self.handles[self.edge_glyph+'_glyph'] = renderer.edge_renderer.glyph
if 'hover' in self.handles:
self.handles['hover'].renderers.append(renderer)
@@ -294,3 +315,4 @@ def get_data(self, element, ranges, style):
# Ensure the edgepaths for the triangles are generated
element.edgepaths
return super(TriMeshPlot, self).get_data(element, ranges, style)
+
From 2243bf6968b5841ed08df851c6ce387762e70f70 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 8 Dec 2017 22:09:27 +0000
Subject: [PATCH 22/28] Cleaned up datashader class hierarchy
---
holoviews/operation/datashader.py | 27 +++++++++++++++++++--------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index 51c167e1d6..3e36bee465 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -164,9 +164,10 @@ class aggregate(ResamplingOperation):
"""
aggregate implements 2D binning for any valid HoloViews Element
type using datashader. I.e., this operation turns a HoloViews
- Element or overlay of Elements into an hImage or an overlay of
- .Images by rasterizing it, which provides a fixed-sized
- representation independent of the original dataset size.
+ Element or overlay of Elements into an Image or an overlay of
+ Images by rasterizing it. This allows quickly aggregating large
+ datasets computing a fixed-sized representation independent
+ of the original dataset size.
By default it will simply count the number of values in each bin
but other aggregators can be supplied implementing mean, max, min
@@ -527,11 +528,13 @@ def _process(self, element, key=None):
return Image(agg, **params)
-class rasterize(trimesh_rasterize):
+
+class rasterize(ResamplingOperation):
"""
Rasterize is a high-level operation which will rasterize any
- Element or combination of Elements supplied as an (Nd)Overlay
- by aggregating with the supplied aggregation function.
+ Element or combination of Elements supplied as an (Nd)Overlay by
+ aggregating with the supplied aggregation it with the declared
+ aggregator and interpolation methods.
By default it will simply count the number of values in each bin
but other aggregators can be supplied implementing mean, max, min
@@ -552,16 +555,24 @@ class rasterize(trimesh_rasterize):
aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
default=None)
+ interpolation = param.ObjectSelector(default='bilinear',
+ objects=['bilinear', None], doc="""
+ The interpolation method to apply during rasterization.""")
+
def _process(self, element, key=None):
# Get input Images to avoid multiple rasterization
imgs = element.traverse(lambda x: x, [Image])
# Rasterize TriMeshes
- trirasterize = partial(trimesh_rasterize._process, self)
+ tri_params = dict({k: v for k, v in self.p.items()
+ if k in aggregate.params()}, dynamic=False)
+ trirasterize = trimesh_rasterize.instance(**tri_params)
element = element.map(trirasterize, TriMesh)
# Rasterize NdOverlay of objects
- dsrasterize = partial(aggregate._process, self)
+ agg_params = dict({k: v for k, v in self.p.items()
+ if k in aggregate.params()}, dynamic=False)
+ dsrasterize = aggregate.instance(**agg_params)
predicate = lambda x: (isinstance(x, NdOverlay) and
issubclass(x.type, Dataset)
and not issubclass(x.type, Image))
From 586a506dd993c13f73ef88ac4f7170b5e962867d Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sat, 9 Dec 2017 00:26:15 +0000
Subject: [PATCH 23/28] Handled empty TriMesh
---
holoviews/element/graphs.py | 11 +++++++++--
holoviews/plotting/bokeh/graphs.py | 4 ++--
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index 73fd3de16f..8e1d195c50 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -425,8 +425,11 @@ def __init__(self, data, kdims=None, vdims=None, **params):
else:
edges, nodes, edgepaths = data, None, None
if nodes is None:
- raise ValueError("TriMesh expects both simplices and nodes "
- "to be supplied.")
+ if isinstance(edges, list) and len(edges) == 0:
+ nodes = []
+ else:
+ raise ValueError("TriMesh expects both simplices and nodes "
+ "to be supplied.")
if isinstance(nodes, self._node_type):
pass
@@ -476,6 +479,10 @@ def edgepaths(self):
"""
if self._edgepaths:
return self._edgepaths
+ elif not len(self):
+ edgepaths = self._edge_type([], kdims=self.nodes.kdims[:2])
+ self._edgepaths = edgepaths
+ return edgepaths
simplices = self.array([0, 1, 2]).astype(np.int32)
pts = self.nodes.array([0, 1]).astype(float)
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index 77fbae3673..f7eb08e44e 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -120,7 +120,7 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
def _get_edge_paths(self, element):
path_data = {}
xidx, yidx = (1, 0) if self.invert_axes else (0, 1)
- if element._edgepaths:
+ if element._edgepaths is not None:
edges = element._split_edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims)
if len(edges) == len(element):
path_data['xs'] = [path[:, xidx] for path in edges]
@@ -169,7 +169,7 @@ def get_data(self, element, ranges, style):
point_mapping['node_nonselection_fill_color'] = point_mapping['node_fill_color']
edge_mapping = {}
- nan_node = index.max()+1
+ nan_node = index.max()+1 if len(index) else 0
start, end = (element.dimension_values(i) for i in range(2))
if nodes.dtype.kind == 'f':
start, end = start.astype(np.int32), end.astype(np.int32)
From 3480316a1064091ff42ec56a7ef031ef5f56aff9 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 14 Dec 2017 14:32:42 +0000
Subject: [PATCH 24/28] Addressed various TriMesh review comments
---
holoviews/element/graphs.py | 3 ++-
holoviews/plotting/bokeh/graphs.py | 32 +++++++++++++++---------------
2 files changed, 18 insertions(+), 17 deletions(-)
diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py
index 8e1d195c50..bdd84f7f57 100644
--- a/holoviews/element/graphs.py
+++ b/holoviews/element/graphs.py
@@ -457,7 +457,7 @@ def __init__(self, data, kdims=None, vdims=None, **params):
self._edgepaths = edgepaths
@classmethod
- def from_points(cls, points):
+ def from_vertices(cls, points):
"""
Uses Delauney triangulation to compute triangle simplices for
each point.
@@ -504,6 +504,7 @@ def select(self, selection_specs=None, **selection):
supplied, which will ensure the selection is only applied if the
specs match the selected object.
"""
+ # Ensure that edgepaths are initialized so they can be selected on
self.edgepaths
return super(TriMesh, self).select(selection_specs=None,
selection_mode='nodes',
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index f7eb08e44e..900c874ad2 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -46,7 +46,16 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot):
bezier = False
# Declares which columns in the data refer to node indices
- _node_indices = [0, 1]
+ _node_columns = [0, 1]
+
+ @property
+ def edge_glyph(self):
+ if self.filled:
+ return 'patches_1'
+ elif self.bezier:
+ return 'bezier_1'
+ else:
+ return 'multi_line_1'
def _hover_opts(self, element):
if self.inspection_policy == 'nodes':
@@ -83,7 +92,7 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
idx = element.get_dimension_index(cdim)
field = dimension_sanitizer(cdim.name)
cvals = element.dimension_values(cdim)
- if idx in self._node_indices:
+ if idx in self._node_columns:
factors = element.nodes.dimension_values(2, expanded=False)
elif idx == 2 and cvals.dtype.kind in 'if':
factors = None
@@ -92,7 +101,7 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style):
default_cmap = 'viridis' if factors is None else 'tab20'
cmap = style.get('edge_cmap', style.get('cmap', default_cmap))
- if factors is None or (factors.dtype.kind in 'if' and idx not in self._node_indices):
+ if factors is None or (factors.dtype.kind in 'if' and idx not in self._node_columns):
colors, factors = None, None
else:
if factors.dtype.kind == 'f':
@@ -126,8 +135,8 @@ def _get_edge_paths(self, element):
path_data['xs'] = [path[:, xidx] for path in edges]
path_data['ys'] = [path[:, yidx] for path in edges]
else:
- self.warning('Graph edge paths do not match the number of abstract edges '
- 'and will be skipped')
+ raise ValueError("Edge paths do not match the number of supplied edges."
+ "Expected %d, found %d paths." % (len(element), len(edges)))
return path_data, {'xs': 'xs', 'ys': 'ys'}
@@ -197,15 +206,6 @@ def get_data(self, element, ranges, style):
mapping = {'scatter_1': point_mapping, self.edge_glyph: edge_mapping}
return data, mapping, style
- @property
- def edge_glyph(self):
- if self.filled:
- edge_glyph = 'patches_1'
- elif self.bezier:
- edge_glyph = 'bezier_1'
- else:
- edge_glyph = 'multi_line_1'
- return edge_glyph
def _update_datasource(self, source, data):
"""
@@ -309,10 +309,10 @@ class TriMeshPlot(GraphPlot):
['node_size', 'cmap', 'edge_cmap'])
# Declares that three columns in TriMesh refer to edges
- _node_indices = [0, 1, 2]
+ _node_columns = [0, 1, 2]
def get_data(self, element, ranges, style):
- # Ensure the edgepaths for the triangles are generated
+ # Ensure the edgepaths for the triangles are generated before plotting
element.edgepaths
return super(TriMeshPlot, self).get_data(element, ranges, style)
From ad2fcca9e2da2273eeef842d87db76d2f9088d69 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 14 Dec 2017 14:33:17 +0000
Subject: [PATCH 25/28] Fixed Dataset.add_dimension dtype bug
---
holoviews/core/data/__init__.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py
index c3fea76be3..9f23a5db13 100644
--- a/holoviews/core/data/__init__.py
+++ b/holoviews/core/data/__init__.py
@@ -275,7 +275,11 @@ def add_dimension(self, dimension, dim_pos, dim_val, vdim=False, **kwargs):
dims.insert(dim_pos, dimension)
dimensions = dict(kdims=dims)
- data = self.interface.add_dimension(self, dimension, dim_pos, dim_val, vdim)
+ if issubclass(self.interface, ArrayInterface) and np.asarray(dim_val).dtype != self.data.dtype:
+ element = self.clone(datatype=['pandas', 'dictionary'])
+ data = element.interface.add_dimension(element, dimension, dim_pos, dim_val, vdim)
+ else:
+ data = self.interface.add_dimension(self, dimension, dim_pos, dim_val, vdim)
return self.clone(data, **dimensions)
From 74f961aabbfa7cb39fddf7f2a6c5e44736322c3d Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 14 Dec 2017 14:33:35 +0000
Subject: [PATCH 26/28] Fixed datashade operation docstrings
---
holoviews/operation/datashader.py | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index 3e36bee465..adf7d6d7f1 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -532,13 +532,13 @@ def _process(self, element, key=None):
class rasterize(ResamplingOperation):
"""
Rasterize is a high-level operation which will rasterize any
- Element or combination of Elements supplied as an (Nd)Overlay by
- aggregating with the supplied aggregation it with the declared
- aggregator and interpolation methods.
+ Element or combination of Elements aggregating it with the supplied
+ aggregator and interpolation method.
- By default it will simply count the number of values in each bin
- but other aggregators can be supplied implementing mean, max, min
- and other reduction operations.
+ The default aggregation method depends on the type of Element but
+ usually defaults to the count of samples in each bin, other
+ aggregators can be supplied implementing mean, max, min and other
+ reduction operations.
The bins of the aggregate are defined by the width and height and
the x_range and y_range. If x_sampling or y_sampling are supplied
@@ -546,10 +546,10 @@ class rasterize(ResamplingOperation):
sampling distance by reducing the width and height when zoomed in
beyond the minimum sampling distance.
- By default, the PlotSize stream is applied when this operation
- is used dynamically, which means that the height and width
- will automatically be set to match the inner dimensions of
- the linked plot.
+ By default, the PlotSize and RangeXY streams are applied when this
+ operation is used dynamically, which means that the width, height,
+ x_range and y_range will automatically be set to match the inner
+ dimensions of the linked plot and the ranges of the axes.
"""
aggregator = param.ClassSelector(class_=ds.reductions.Reduction,
@@ -585,6 +585,7 @@ def _process(self, element, key=None):
return element
+
class shade(Operation):
"""
shade applies a normalization function followed by colormapping to
From 65a44bd89ea84a7a9f1775b98500cc70d7d6a152 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 14 Dec 2017 14:34:43 +0000
Subject: [PATCH 27/28] Added vertex averaging to TriMesh plots
---
holoviews/plotting/bokeh/graphs.py | 7 +++++++
holoviews/plotting/mpl/graphs.py | 13 +++++++++++++
2 files changed, 20 insertions(+)
diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py
index 900c874ad2..7fbb18432c 100644
--- a/holoviews/plotting/bokeh/graphs.py
+++ b/holoviews/plotting/bokeh/graphs.py
@@ -313,6 +313,13 @@ class TriMeshPlot(GraphPlot):
def get_data(self, element, ranges, style):
# Ensure the edgepaths for the triangles are generated before plotting
+ simplex_dim = element.get_dimension(self.edge_color_index)
+ vertex_dim = element.nodes.get_dimension(self.edge_color_index)
+ if not isinstance(self.edge_color_index, int) and vertex_dim and not simplex_dim:
+ simplices = element.array([0, 1, 2])
+ z = element.nodes.dimension_values(vertex_dim)
+ z = z[simplices].mean(axis=1)
+ element = element.add_dimension(vertex_dim, len(element.vdims), z, vdim=True)
element.edgepaths
return super(TriMeshPlot, self).get_data(element, ranges, style)
diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py
index 2e69ba5411..cdc5996b50 100644
--- a/holoviews/plotting/mpl/graphs.py
+++ b/holoviews/plotting/mpl/graphs.py
@@ -181,3 +181,16 @@ class TriMeshPlot(GraphPlot):
Whether the triangles should be drawn as filled.""")
style_opts = GraphPlot.style_opts + ['edge_facecolors']
+
+ def get_data(self, element, ranges, style):
+ simplex_dim = element.get_dimension(self.edge_color_index)
+ vertex_dim = element.nodes.get_dimension(self.edge_color_index)
+ if not isinstance(self.edge_color_index, int) and vertex_dim and not simplex_dim:
+ simplices = element.array([0, 1, 2])
+ z = element.nodes.dimension_values(vertex_dim)
+ z = z[simplices].mean(axis=1)
+ element = element.add_dimension(vertex_dim, len(element.vdims), z, vdim=True)
+ # Ensure the edgepaths for the triangles are generated before plotting
+ element.edgepaths
+ return super(TriMeshPlot, self).get_data(element, ranges, style)
+
From ddf38cd8880e3a9657b231dc955726631ffc06b2 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 14 Dec 2017 14:38:03 +0000
Subject: [PATCH 28/28] Revised TriMesh reference notebooks
---
.../reference/elements/bokeh/TriMesh.ipynb | 30 ++++++++++++++-----
.../elements/matplotlib/TriMesh.ipynb | 29 +++++++++++++-----
2 files changed, 45 insertions(+), 14 deletions(-)
diff --git a/examples/reference/elements/bokeh/TriMesh.ipynb b/examples/reference/elements/bokeh/TriMesh.ipynb
index 387638fd16..e2778a81a9 100644
--- a/examples/reference/elements/bokeh/TriMesh.ipynb
+++ b/examples/reference/elements/bokeh/TriMesh.ipynb
@@ -31,11 +31,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "A ``TriMesh`` represents a mesh of triangles represented as the simplices and nodes. The simplices represent the indices into the node data, made up of three indices per triangle. The mesh therefore follows a datastructure very similar to a graph, with the abstract connectivity between nodes stored on the ``TriMesh`` element itself, the node positions stored on a ``Nodes`` element and the concrete ``EdgePaths`` making up each triangle generated when required by accessing the edgepaths.\n",
+ "A ``TriMesh`` represents a mesh of triangles represented as the simplexes and vertexes. The simplexes represent the indices into the vertex data, made up of three indices per triangle. The mesh therefore follows a datastructure very similar to a graph, with the abstract connectivity between nodes stored on the ``TriMesh`` element itself, the node or vertex positions stored on a ``Nodes`` element and the concrete ``EdgePaths`` making up each triangle generated when required by accessing the edgepaths attribute.\n",
"\n",
"Unlike a Graph each simplex is represented as the node indices of the three corners of each triangle rather than the usual source and target node.\n",
"\n",
- "We will begin with a simple random mesh, generated by sampling some random integers and then applying Delaunay triangulation, which is available in SciPy. We can then construct the ``TriMesh`` by passing it the **simplices** and the points (or **nodes**)."
+ "We will begin with a simple random mesh, generated by sampling some random integers and then applying Delaunay triangulation, which is available in SciPy. We can then construct the ``TriMesh`` by passing it the **simplexes** and the **vertices** (or **nodes**)."
]
},
{
@@ -52,6 +52,22 @@
"trimesh"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To make this easier TriMesh also provides a convenient ``from_vertices`` method, which will apply the Delaunay triangulation and construct the ``TriMesh`` for us:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hv.TriMesh.from_vertices(np.random.randn(100, 2))"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -93,8 +109,8 @@
"\n",
"x = (radii*np.cos(angles)).flatten()\n",
"y = (radii*np.sin(angles)).flatten()\n",
- "nodes = np.column_stack([x, y])\n",
"z = (np.cos(radii)*np.cos(angles*3.0)).flatten()\n",
+ "nodes = np.column_stack([x, y, z])\n",
"\n",
"# Apply Delaunay triangulation\n",
"delauney = Delaunay(np.column_stack([x, y]))\n",
@@ -103,8 +119,7 @@
"xmid = x[delauney.simplices].mean(axis=1)\n",
"ymid = y[delauney.simplices].mean(axis=1)\n",
"mask = np.where(xmid*xmid + ymid*ymid < min_radius*min_radius, 1, 0)\n",
- "simplices = delauney.simplices[np.logical_not(mask)]\n",
- "z = z[simplices].mean(axis=1)"
+ "simplices = delauney.simplices[np.logical_not(mask)]"
]
},
{
@@ -120,6 +135,7 @@
"metadata": {},
"outputs": [],
"source": [
+ "nodes = hv.Points(nodes, vdims='z')\n",
"hv.TriMesh((simplices, nodes))"
]
},
@@ -127,7 +143,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "We can also do something more interesting, e.g. adding a value dimension to the simplices, which we can color each triangle by."
+ "We can also do something more interesting, e.g. by adding a value dimension to the vertices and coloring the edges by the vertex averaged value using the ``edge_color_index`` plot option:"
]
},
{
@@ -137,7 +153,7 @@
"outputs": [],
"source": [
"%%opts TriMesh [filled=True edge_color_index='z' width=400 height=400 tools=['hover'] inspection_policy='edges'] (cmap='viridis')\n",
- "hv.TriMesh((np.column_stack([simplices, z]), nodes), vdims='z')"
+ "hv.TriMesh((simplices, nodes))"
]
}
],
diff --git a/examples/reference/elements/matplotlib/TriMesh.ipynb b/examples/reference/elements/matplotlib/TriMesh.ipynb
index 189a0aa8d5..312d4d1a30 100644
--- a/examples/reference/elements/matplotlib/TriMesh.ipynb
+++ b/examples/reference/elements/matplotlib/TriMesh.ipynb
@@ -31,11 +31,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "A ``TriMesh`` represents a mesh of triangles represented as the simplices and nodes. The simplices represent the indices into the node data, made up of three indices per triangle. The mesh therefore follows a datastructure very similar to a graph, with the abstract connectivity between nodes stored on the ``TriMesh`` element itself, the node positions stored on a ``Nodes`` element and the concrete ``EdgePaths`` making up each triangle generated when required by accessing the edgepaths.\n",
+ "A ``TriMesh`` represents a mesh of triangles represented as the simplexes and vertexes. The simplexes represent the indices into the vertex data, made up of three indices per triangle. The mesh therefore follows a datastructure very similar to a graph, with the abstract connectivity between nodes stored on the ``TriMesh`` element itself, the node or vertex positions stored on a ``Nodes`` element and the concrete ``EdgePaths`` making up each triangle generated when required by accessing the edgepaths attribute.\n",
"\n",
"Unlike a Graph each simplex is represented as the node indices of the three corners of each triangle rather than the usual source and target node.\n",
"\n",
- "We will begin with a simple random mesh, generated by sampling some random integers and then applying Delaunay triangulation, which is available in SciPy. We can then construct the ``TriMesh`` by passing it the **simplices** and the points (or **nodes**)."
+ "We will begin with a simple random mesh, generated by sampling some random integers and then applying Delaunay triangulation, which is available in SciPy. We can then construct the ``TriMesh`` by passing it the **simplexes** and the **vertices** (or **nodes**)."
]
},
{
@@ -52,6 +52,22 @@
"trimesh"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To make this easier TriMesh also provides a convenient ``from_vertices`` method, which will apply the Delaunay triangulation and construct the ``TriMesh`` for us:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hv.TriMesh.from_vertices(np.random.randn(100, 2))"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -93,8 +109,8 @@
"\n",
"x = (radii*np.cos(angles)).flatten()\n",
"y = (radii*np.sin(angles)).flatten()\n",
- "nodes = np.column_stack([x, y])\n",
"z = (np.cos(radii)*np.cos(angles*3.0)).flatten()\n",
+ "nodes = np.column_stack([x, y, z])\n",
"\n",
"# Apply Delaunay triangulation\n",
"delauney = Delaunay(np.column_stack([x, y]))\n",
@@ -103,8 +119,7 @@
"xmid = x[delauney.simplices].mean(axis=1)\n",
"ymid = y[delauney.simplices].mean(axis=1)\n",
"mask = np.where(xmid*xmid + ymid*ymid < min_radius*min_radius, 1, 0)\n",
- "simplices = delauney.simplices[np.logical_not(mask)]\n",
- "z = z[simplices].mean(axis=1)"
+ "simplices = delauney.simplices[np.logical_not(mask)]"
]
},
{
@@ -127,7 +142,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "We can also do something more interesting, e.g. adding a value dimension to the simplices, which we can color each triangle by."
+ "We can also do something more interesting, e.g. by adding a value dimension to the vertices and coloring the edges by the vertex averaged value using the ``edge_color_index`` plot option:"
]
},
{
@@ -137,7 +152,7 @@
"outputs": [],
"source": [
"%%opts TriMesh [filled=True edge_color_index='z' fig_size=200] (cmap='viridis')\n",
- "hv.TriMesh((np.column_stack([simplices, z]), nodes), vdims='z')"
+ "hv.TriMesh((simplices, hv.Points(nodes, vdims='z')))"
]
}
],