Skip to content

Commit

Permalink
Added bokeh VectorField plot
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Mar 12, 2017
1 parent 07a6752 commit f750aee
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 49 deletions.
7 changes: 4 additions & 3 deletions doc/Tutorials/Bokeh_Elements.ipynb
Expand Up @@ -38,7 +38,7 @@
" <dt><a href=\"#BoxWhisker\"><code>BoxWhisker</code></a></dt><dd>Distributions of data varying by 0-N key dimensions.<font color='green'>&#x2713;</font></dd>\n",
" <dt><a href=\"#Scatter\"><code>Scatter</code></a></dt><dd>Discontinuous collection of points indexed over a single dimension. <font color='green'>&#x2713;</font></dd>\n",
" <dt><a href=\"#Points\"><code>Points</code></a></dt><dd>Discontinuous collection of points indexed over two dimensions. <font color='green'>&#x2713;</font></dd>\n",
" <dt><a href=\"#VectorField\"><code>VectorField</code></a></dt><dd>Cyclic variable (and optional auxiliary data) distributed over two-dimensional space. <font color='red'>&#x2717;</font></dd>\n",
" <dt><a href=\"#VectorField\"><code>VectorField</code></a></dt><dd>Cyclic variable (and optional auxiliary data) distributed over two-dimensional space. <font color='green'>&#x2713;</font></dd>\n",
" <dt><a href=\"#Spikes\"><code>Spikes</code></a></dt><dd>A collection of horizontal or vertical lines at various locations with fixed height (1D) or variable height (2D). <font color='green'>&#x2713;</font></dd>\n",
" <dt><a href=\"#SideHistogram\"><code>SideHistogram</code></a></dt><dd>Histogram binning data contained by some other <code>Element</code>. <font color='green'>&#x2713;</font></dd>\n",
" </dl>\n",
Expand Down Expand Up @@ -674,6 +674,7 @@
},
"outputs": [],
"source": [
"%%opts VectorField [size_index=3]\n",
"x,y = np.mgrid[-10:10,-10:10] * 0.25\n",
"sine_rings = np.sin(x**2+y**2)*np.pi+np.pi\n",
"exp_falloff = 1/np.exp((x**2+y**2)/8)\n",
Expand All @@ -699,8 +700,8 @@
},
"outputs": [],
"source": [
"%%opts VectorField.A [color_dim='angle'] VectorField.M [color_dim='magnitude']\n",
"hv.VectorField(vector_data, group='A')"
"%%opts VectorField [size_index=3] VectorField.A [color_index=2] VectorField.M [color_index=3]\n",
"hv.VectorField(vector_data, group='A') + hv.VectorField(vector_data, group='M')"
]
},
{
Expand Down
7 changes: 5 additions & 2 deletions holoviews/plotting/bokeh/__init__.py
Expand Up @@ -6,7 +6,8 @@
RGB, Histogram, Spread, HeatMap, Contours, Bars,
Box, Bounds, Ellipse, Polygons, BoxWhisker,
ErrorBars, Text, HLine, VLine, Spline, Spikes,
Table, ItemTable, Area, HSV, QuadMesh, GridImage)
Table, ItemTable, Area, HSV, QuadMesh, GridImage,
VectorField)
from ...core.options import Options, Cycle

try:
Expand All @@ -21,7 +22,7 @@
from .element import OverlayPlot, BokehMPLWrapper
from .chart import (PointPlot, CurvePlot, SpreadPlot, ErrorPlot, HistogramPlot,
SideHistogramPlot, BoxPlot, BarPlot, SpikesPlot,
SideSpikesPlot, AreaPlot)
SideSpikesPlot, AreaPlot, VectorFieldPlot)
from .path import PathPlot, PolygonPlot
from .plot import GridPlot, LayoutPlot, AdjointLayoutPlot
from .raster import (RasterPlot, ImagePlot, RGBPlot, HeatmapPlot,
Expand All @@ -47,6 +48,7 @@
Spread: SpreadPlot,
Spikes: SpikesPlot,
Area: AreaPlot,
VectorField: VectorFieldPlot,

# Rasters
Image: RasterPlot,
Expand Down Expand Up @@ -130,6 +132,7 @@

options.Spikes = Options('style', color='black')
options.Area = Options('style', color=Cycle(), line_color='black')
options.VectorField = Options('style', line_color='black')

# Paths
options.Contours = Options('style', color=Cycle())
Expand Down
117 changes: 84 additions & 33 deletions holoviews/plotting/bokeh/chart.py
Expand Up @@ -14,7 +14,8 @@
from ...core.util import max_range, basestring, dimension_sanitizer
from ...core.options import abbreviated_exception
from ...operation import interpolate_curve
from ..util import compute_sizes, get_sideplot_ranges, match_spec, map_colors
from ..util import (compute_sizes, get_sideplot_ranges, match_spec,
map_colors, get_min_distance)
from .element import ElementPlot, ColorbarPlot, LegendPlot, line_properties, fill_properties
from .path import PathPlot, PolygonPlot
from .util import get_cmap, mpl_to_bokeh, update_plot, rgb2hex, bokeh_version
Expand Down Expand Up @@ -50,6 +51,26 @@ class PointPlot(LegendPlot, ColorbarPlot):

_plot_methods = dict(single='scatter', batched='scatter')

def _get_size_data(self, element, ranges, style):
data, mapping = {}, {}
sdim = element.get_dimension(self.size_index)
if sdim:
map_key = 'size_' + sdim.name
ms = style.get('size', np.sqrt(6))**2
sizes = element.dimension_values(self.size_index)
sizes = compute_sizes(sizes, self.size_fn,
self.scaling_factor,
self.scaling_method, ms)
if sizes is None:
eltype = type(element).__name__
self.warning('%s dimension is not numeric, cannot '
'use to scale %s size.' % (sdim, eltype))
else:
data[map_key] = np.sqrt(sizes)
mapping['size'] = map_key
return data, mapping


def get_data(self, element, ranges=None, empty=False):
style = self.style[self.cyclic_index]
dims = element.dimensions(label=True)
Expand All @@ -63,39 +84,13 @@ def get_data(self, element, ranges=None, empty=False):
data[ydim] = [] if empty else element.dimension_values(yidx)
self._categorize_data(data, (xdim, ydim), element.dimensions())

cdim = element.get_dimension(self.color_index)
if cdim:
cdata = data[cdim.name] if cdim.name in data else element.dimension_values(cdim)
factors = None
if isinstance(cdata, list) or cdata.dtype.kind in 'OSU':
factors = list(np.unique(cdata))
mapper = self._get_colormapper(cdim, element, ranges, style,
factors)
data[cdim.name] = cdata
if factors is not None:
mapping['legend'] = {'field': cdim.name}
mapping['color'] = {'field': cdim.name,
'transform': mapper}
cdata, cmapping = self._get_color_data(element, ranges, style)
data.update(cdata)
mapping.update(cmapping)

sdim = element.get_dimension(self.size_index)
if sdim:
map_key = 'size_' + sdim.name
if empty:
data[map_key] = []
mapping['size'] = map_key
else:
ms = style.get('size', np.sqrt(6))**2
sizes = element.dimension_values(self.size_index)
sizes = compute_sizes(sizes, self.size_fn,
self.scaling_factor,
self.scaling_method, ms)
if sizes is None:
eltype = type(element).__name__
self.warning('%s dimension is not numeric, cannot '
'use to scale %s size.' % (sdim, eltype))
else:
data[map_key] = np.sqrt(sizes)
mapping['size'] = map_key
sdata, smapping = self._get_size_data(element, ranges, style)
data.update(sdata)
mapping.update(smapping)

self._get_hover_data(data, element, empty)
return data, mapping
Expand Down Expand Up @@ -152,6 +147,62 @@ def _init_glyph(self, plot, mapping, properties):
return renderer, renderer.glyph


class VectorFieldPlot(ColorbarPlot):

color_index = param.ClassSelector(default=None, class_=(basestring, int),
allow_None=True, doc="""
Index of the dimension from which the color will the drawn""")

size_index = param.ClassSelector(default=None, class_=(basestring, int),
allow_None=True, doc="""
Index of the dimension from which the sizes will the drawn.""")

normalize_lengths = param.Boolean(default=True, doc="""
Whether to normalize vector magnitudes automatically. If False,
it will be assumed that the lengths have already been correctly
normalized.""")

style_opts = ['color'] + line_properties
_plot_methods = dict(single='ray')

def _get_lengths(self, element, ranges):
mag_dim = element.get_dimension(self.size_index)
(x0, x1), (y0, y1) = (element.range(i) for i in range(2))
distance = get_min_distance(element)
base_dist = np.sqrt(distance/np.min([x1-x0, y1-y0]))
if mag_dim:
magnitudes = element.dimension_values(mag_dim)
_, max_magnitude = ranges[mag_dim.name]
if self.normalize_lengths and max_magnitude != 0:
magnitudes = magnitudes / max_magnitude
magnitudes *= base_dist
else:
magnitudes = np.ones(len(element))*base_dist
return magnitudes


def get_data(self, element, ranges=None, empty=False):
style = self.style[self.cyclic_index]

xidx, yidx = (1, 0) if self.invert_axes else (0, 1)
x = element.get_dimension(xidx).name
y = element.get_dimension(yidx).name
angle_dim = element.get_dimension(2).name
angles = element.dimension_values(2)

data = {x: element.dimension_values(xidx),
y: element.dimension_values(yidx), angle_dim: angles}
mapping = dict(x=x, y=y, angle=angle_dim, length='length')

cdata, cmapping = self._get_color_data(element, ranges, style,
name='line_color')
data.update(cdata)
mapping.update(cmapping)

data['length'] = self._get_lengths(element, ranges)
return (data, mapping)


class CurvePlot(ElementPlot):

interpolation = param.ObjectSelector(objects=['linear', 'steps-mid',
Expand Down
18 changes: 18 additions & 0 deletions holoviews/plotting/bokeh/element.py
Expand Up @@ -940,6 +940,24 @@ def _get_colormapper(self, dim, element, ranges, style, factors=None):
return cmapper


def _get_color_data(self, element, ranges, style, name='color'):
data, mapping = {}, {}
cdim = element.get_dimension(self.color_index)
if cdim:
cdata = element.dimension_values(cdim)
factors = None
if isinstance(cdata, list) or cdata.dtype.kind in 'OSU':
factors = list(np.unique(cdata))
mapper = self._get_colormapper(cdim, element, ranges, style,
factors)
data[cdim.name] = cdata
if factors is not None:
mapping['legend'] = {'field': cdim.name}
mapping[name] = {'field': cdim.name,
'transform': mapper}
return data, mapping


def _get_cmapper_opts(self, low, high, factors, colors):
if factors is None:
colormapper = LogColorMapper if self.logz else LinearColorMapper
Expand Down
14 changes: 3 additions & 11 deletions holoviews/plotting/mpl/chart.py
Expand Up @@ -20,7 +20,8 @@
basestring, max_range, unicode)
from ...element import Points, Raster, Polygons, HeatMap
from ...operation import interpolate_curve
from ..util import compute_sizes, get_sideplot_ranges, map_colors
from ..util import (compute_sizes, get_sideplot_ranges, map_colors,
get_min_distance)
from .element import ElementPlot, ColorbarPlot, LegendPlot
from .path import PathPlot
from .plot import AdjoinedPlot
Expand Down Expand Up @@ -633,16 +634,7 @@ def _get_map_info(self, vmap):
"""
Get the minimum sample distance and maximum magnitude
"""
return np.min([self._get_min_dist(vfield) for vfield in vmap])


def _get_min_dist(self, vfield):
"Get the minimum sampling distance."
xys = vfield.array([0, 1]).view(dtype=np.complex128)
m, n = np.meshgrid(xys, xys)
distances = np.abs(m-n)
np.fill_diagonal(distances, np.inf)
return distances[distances>0].min()
return np.min([get_min_distance(vfield) for vfield in vmap])


def get_data(self, element, ranges, style):
Expand Down
11 changes: 11 additions & 0 deletions holoviews/plotting/util.py
Expand Up @@ -336,6 +336,17 @@ def traverse_setter(obj, attribute, value):
obj.traverse(lambda x: setattr(x, attribute, value))


def get_min_distance(element):
"""
Gets the minimum sampling distance of the x- and y-coordinates
in a grid.
"""
xys = element.array([0, 1]).view(dtype=np.complex128)
m, n = np.meshgrid(xys, xys)
distances = np.abs(m-n)
np.fill_diagonal(distances, np.inf)
return distances[distances>0].min()


# linear_kryw_0_100_c71 (aka "fire"):
# A perceptually uniform equivalent of matplotlib's "hot" colormap, from
Expand Down

0 comments on commit f750aee

Please sign in to comment.