Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add consistent support for categorical axes in bokeh #1089

Merged
merged 24 commits into from
Jan 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2ea7468
Fixed HeatMap sorting resolution
philippjfr Jan 28, 2017
87b0f51
Added support for updating factor ranges on HeatMaps
philippjfr Jan 28, 2017
ef7db9e
Support string types in ranges
philippjfr Jan 29, 2017
e37f592
Add support for categoricals in bokeh axis ranges
philippjfr Jan 29, 2017
f74bb5e
Add support for categoricals on Points and Curve
philippjfr Jan 29, 2017
7ff36ff
Small categorical fixes
philippjfr Jan 29, 2017
884ecc0
Handle updating categorical ranges
philippjfr Jan 29, 2017
6c47c49
Made Annotation.dimension_values consistent with other Elements
philippjfr Jan 29, 2017
9f8659c
Support for non-numeric Text coordinates
philippjfr Jan 29, 2017
b44308f
Support categorical coordinates on ErroBars and Text
philippjfr Jan 29, 2017
34cefeb
Simplified HeatmapPlot._get_factors
philippjfr Jan 29, 2017
9a36cc4
Fixed unicode handling of categoricals
philippjfr Jan 29, 2017
1ea688d
Added tests for categorical axes in bokeh
philippjfr Jan 29, 2017
e08199d
Correctly implemented Arrow.dimension_values
philippjfr Jan 29, 2017
2535898
Avoid unneccessary lookups on Dimension.range
philippjfr Jan 29, 2017
5b28dc8
Fixed Bokeh invert_x/yaxis
philippjfr Jan 29, 2017
d3f4050
Fixed and added tests for axis inversions
philippjfr Jan 29, 2017
1aa18fe
Ensured extents do not override shared_axes ranges
philippjfr Jan 29, 2017
9dfae50
Avoid concatenating categorical axis values if empty
philippjfr Jan 29, 2017
7ed6183
Disable webgl by default
philippjfr Jan 29, 2017
bdf152d
Add categorical axes examples to bokeh backend tutorial
philippjfr Jan 29, 2017
8b05b57
Cleaned up and documented categorize_data
philippjfr Jan 30, 2017
5593ab0
Format categorical axis integer factors to string
philippjfr Jan 30, 2017
3a2f5ea
Merge branch 'master' into factor_range_update
philippjfr Jan 30, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions doc/Tutorials/Bokeh_Backend.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,61 @@
"hv.Curve((aapl_dates, AAPL['adj_close']), kdims=['Date'], vdims=['Stock Index'], label='Apple')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Categorical axes\n",
"\n",
"A number of Elements will also support categorical (i.e. string types) as dimension values, these include ``HeatMap``, ``Points``, ``Scatter``, ``Curve``, ``ErrorBar`` and ``Text`` types.\n",
"\n",
"Here we create a set of points indexed by ascending alphabetical x- and y-coordinates and values multiplying the integer index of each coordinate. We then overlay a ``HeatMap`` of the points with the points themselves enabling the hover tool for both and scaling the point size by the 'z' coordines."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"%%opts Points [size_index='z' tools=['hover']] HeatMap [toolbar='above' tools=['hover']]\n",
"points = hv.Points([(chr(i+65), chr(j+65), i*j) for i in range(10) for j in range(10)], vdims=['z'])\n",
"hv.HeatMap(points) * points"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In the example above both axes are categorical because a HeatMap by definition represents 2D categorical coordinates (unlike Image and Raster types). Other Element types will automatically infer a categorical dimension if the coordinates along that dimension include string types.\n",
"\n",
"Here we will generate random samples indexed by categories from 'A' to 'E' using the Scatter Element and overlay them.\n",
"Secondly we compute the mean and standard deviation for each category and finally we overlay these two elements with a curve representing the mean value and a text element specifying the global mean. All these Elements respect the categorical index, providing us a view of the distribution of values in each category:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"%%opts Overlay [show_legend=False height=400 width=600] ErrorBars (line_width=5) Scatter(alpha=0.2 size=6)\n",
"\n",
"overlay = hv.NdOverlay({group: hv.Scatter(([group]*100, np.random.randn(100)*(i+1)+i))\n",
" for i, group in enumerate(['A', 'B', 'C', 'D', 'E'])})\n",
"\n",
"errorbars = hv.ErrorBars([(k, el.reduce(function=np.mean), el.reduce(function=np.std))\n",
" for k, el in overlay.items()])\n",
"\n",
"global_mean = hv.Text('A', 12, 'Global mean: %.3f' % overlay.dimension_values('y').mean())\n",
"\n",
"errorbars * overlay * hv.Curve(errorbars) * global_mean"
]
},
{
"cell_type": "markdown",
"metadata": {
Expand Down
4 changes: 2 additions & 2 deletions holoviews/core/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,12 +872,12 @@ def range(self, dimension, data_range=True):
if None not in dimension.range:
return dimension.range
elif data_range:
if dimension in self.kdims or dimension in self.vdims:
if dimension in self.kdims+self.vdims:
dim_vals = self.dimension_values(dimension.name)
drange = find_range(dim_vals)
else:
dname = dimension.name
match_fn = lambda x: dname in x.dimensions(['key', 'value'], True)
match_fn = lambda x: dname in x.kdims + x.vdims
range_fn = lambda x: x.range(dname)
ranges = self.traverse(range_fn, [match_fn])
drange = max_range(ranges)
Expand Down
7 changes: 7 additions & 0 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,9 @@ def max_range(ranges):
if pd and all(isinstance(v, pd.tslib.Timestamp) for r in values for v in r):
values = [(v1.to_datetime64(), v2.to_datetime64()) for v1, v2 in values]
arr = np.array(values)
if arr.dtype.kind in 'OSU':
arr = np.sort([v for v in arr.flat if not is_nan(v)])
return arr[0], arr[-1]
if arr.dtype.kind in 'M':
return arr[:, 0].min(), arr[:, 1].max()
return (np.nanmin(arr[:, 0]), np.nanmax(arr[:, 1]))
Expand Down Expand Up @@ -492,10 +495,14 @@ def max_extents(extents, zrange=False):
upper = [v for v in arr[uidx] if v is not None]
if lower and isinstance(lower[0], np.datetime64):
extents[lidx] = np.min(lower)
elif any(isinstance(l, basestring) for l in lower):
extents[lidx] = np.sort(lower)[0]
elif lower:
extents[lidx] = np.nanmin(lower)
if upper and isinstance(upper[0], np.datetime64):
extents[uidx] = np.max(upper)
elif any(isinstance(u, basestring) for u in upper):
extents[uidx] = np.sort(upper)[-1]
elif upper:
extents[uidx] = np.nanmax(upper)
return tuple(extents)
Expand Down
24 changes: 17 additions & 7 deletions holoviews/element/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ def __getitem__(self, key):
return self.clone(self.data, extents=(xstart, ystart, xstop, ystop))


def dimension_values(self, dimension):
def dimension_values(self, dimension, expanded=True, flat=True):
index = self.get_dimension_index(dimension)
if index == 0:
return [self.data if np.isscalar(self.data) else self.data[index]]
return np.array([self.data if np.isscalar(self.data) else self.data[index]])
elif index == 1:
return [] if np.isscalar(self.data) else [self.data[1]]
return [] if np.isscalar(self.data) else np.array([self.data[1]])
else:
return super(Annotation, self).dimension_values(dimension)

Expand Down Expand Up @@ -106,10 +106,10 @@ def __init__(self, spline_points, **params):
super(Spline, self).__init__(spline_points, **params)


def dimension_values(self, dimension):
def dimension_values(self, dimension, expanded=True, flat=True):
index = self.get_dimension_index(dimension)
if index in [0, 1]:
return [point[index] for point in self.data[0]]
return np.array([point[index] for point in self.data[0]])
else:
return super(Spline, self).dimension_values(dimension)

Expand Down Expand Up @@ -158,16 +158,26 @@ def clone(self, *args, **overrides):
settings = dict(self.get_param_values(), **overrides)
return self.__class__(*args, **settings)

def dimension_values(self, dimension, expanded=True, flat=True):
index = self.get_dimension_index(dimension)
if index == 0:
return np.array([self.x])
elif index == 1:
return np.array([self.y])
else:
return super(Text, self).dimension_values(dimension)



class Text(Annotation):
"""
Draw a text annotation at the specified position with custom
fontsize, alignment and rotation.
"""

x = param.Number(default=0, doc="The x-position of the text.")
x = param.Parameter(default=0, doc="The x-position of the text.")

y = param.Number(default=0, doc="The y-position of text.")
y = param.Parameter(default=0, doc="The y-position of text.")

text = param.String(default='', doc="The text to be displayed.")

Expand Down
10 changes: 8 additions & 2 deletions holoviews/element/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,21 @@ def _get_coords(self, obj):
grouped = obj.groupby(xdim, container_type=OrderedDict,
group_type=Dataset).values()
orderings = OrderedDict()
sort = True
for group in grouped:
vals = group.dimension_values(ydim)
vals = group.dimension_values(ydim, False)
if len(vals) == 1:
orderings[vals[0]] = [vals[0]]
else:
for i in range(len(vals)-1):
p1, p2 = vals[i:i+2]
orderings[p1] = [p2]
if one_to_one(orderings, ycoords):
if sort:
if vals.dtype.kind in ('i', 'f'):
sort = (np.diff(vals)>=0).all()
else:
sort = np.array_equal(np.sort(vals), vals)
if sort or one_to_one(orderings, ycoords):
ycoords = np.sort(ycoords)
elif not is_cyclic(orderings):
ycoords = list(itertools.chain(*sort_topologically(orderings)))
Expand Down
1 change: 1 addition & 0 deletions holoviews/plotting/bokeh/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def get_data(self, element, ranges=None, empty=False):
data = dict(x=[element.y], y=[element.x])
else:
data = dict(x=[element.x], y=[element.y])
self._categorize_data(data, ('x', 'y'), element.dimensions())
data['text'] = [element.text]
return (data, mapping)

Expand Down
16 changes: 10 additions & 6 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
except:
Bar, BokehBoxPlot = None, None
from bokeh.models import (Circle, GlyphRenderer, ColumnDataSource,
Range1d, CustomJS)
Range1d, CustomJS, FactorRange)
from bokeh.models.tools import BoxSelectTool

from ...element import Raster, Points, Polygons, Spikes
Expand Down Expand Up @@ -84,8 +84,10 @@ def get_data(self, element, ranges=None, empty=False):
data[map_key] = np.sqrt(sizes)
mapping['size'] = map_key

data[dims[xidx]] = [] if empty else element.dimension_values(xidx)
data[dims[yidx]] = [] if empty else element.dimension_values(yidx)
xdim, ydim = dims[xidx], dims[yidx]
data[xdim] = [] if empty else element.dimension_values(xidx)
data[ydim] = [] if empty else element.dimension_values(yidx)
self._categorize_data(data, (xdim, ydim), element.dimensions())
self._get_hover_data(data, element, empty)
return data, mapping

Expand Down Expand Up @@ -144,9 +146,10 @@ def get_data(self, element, ranges=None, empty=False):
xidx, yidx = (1, 0) if self.invert_axes else (0, 1)
x = element.get_dimension(xidx).name
y = element.get_dimension(yidx).name
return ({x: [] if empty else element.dimension_values(xidx),
y: [] if empty else element.dimension_values(yidx)},
dict(x=x, y=y))
data = {x: [] if empty else element.dimension_values(xidx),
y: [] if empty else element.dimension_values(yidx)}
self._categorize_data(data, (x, y), element.dimensions())
return (data, dict(x=x, y=y))

def _hover_tooltips(self, element):
if self.batched:
Expand Down Expand Up @@ -369,6 +372,7 @@ def get_data(self, element, ranges=None, empty=False):
data = dict(xs=err_ys, ys=err_xs)
else:
data = dict(xs=err_xs, ys=err_ys)
self._categorize_data(data, ('xs', 'ys'), element.dimensions())
return (data, dict(self._mapping))


Expand Down
Loading