Skip to content

Commit

Permalink
Added support for categorical colormapping in bokeh backend
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Feb 14, 2017
1 parent 511a550 commit f233af1
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 25 deletions.
16 changes: 10 additions & 6 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,18 @@ def get_data(self, element, ranges=None, empty=False):
mapping = dict(x=dims[xidx], y=dims[yidx])
data = {}

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())

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

Expand All @@ -85,10 +93,6 @@ def get_data(self, element, ranges=None, empty=False):
data[map_key] = np.sqrt(sizes)
mapping['size'] = map_key

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
43 changes: 25 additions & 18 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from bokeh.models.mappers import LinearColorMapper
try:
from bokeh.models import ColorBar
from bokeh.models.mappers import LogColorMapper
from bokeh.models.mappers import LogColorMapper, CategoricalColorMapper
except ImportError:
LogColorMapper, ColorBar = None, None
from bokeh.models import LogTicker, BasicTicker
Expand Down Expand Up @@ -870,13 +870,11 @@ def _draw_colorbar(self, plot, color_mapper):
self.handles['colorbar'] = color_bar


def _get_colormapper(self, dim, element, ranges, style):
def _get_colormapper(self, dim, element, ranges, style, factors=None):
# The initial colormapper instance is cached the first time
# and then only updated
if dim is None:
return None
low, high = ranges.get(dim.name, element.range(dim.name))
palette = mplcmap_to_palette(style.pop('cmap', 'viridis'))
if self.adjoined:
cmappers = self.adjoined.traverse(lambda x: (x.handles.get('color_dim'),
x.handles.get('color_mapper')))
Expand All @@ -887,25 +885,34 @@ def _get_colormapper(self, dim, element, ranges, style):
return cmapper
else:
return None
colors = self.clipping_colors
if isinstance(low, (bool, np.bool_)): low = int(low)
if isinstance(high, (bool, np.bool_)): high = int(high)
opts = {'low': low, 'high': high}
color_opts = [('NaN', 'nan_color'), ('max', 'high_color'), ('min', 'low_color')]
for name, opt in color_opts:
color = colors.get(name)
if not color:
continue
elif isinstance(color, tuple):
color = [int(c*255) if i<3 else c for i, c in enumerate(color)]
opts[opt] = color

ncolors = None if factors is None else len(factors)
low, high = ranges.get(dim.name, element.range(dim.name))
palette = mplcmap_to_palette(style.pop('cmap', 'viridis'), ncolors)

color_rgb = lambda color: [int(c*255) if i<3 else c for i, c in enumerate(color)]
colors = {k: color_rgb(v) if isinstance(v, tuple) else v
for k, v in self.clipping_colors}

if factors is None:
colormapper = LogColorMapper if self.logz else LinearColorMapper
if isinstance(low, (bool, np.bool_)): low = int(low)
if isinstance(high, (bool, np.bool_)): high = int(high)
opts = {'low': low, 'high': high}
color_opts = [('NaN', 'nan_color'), ('max', 'high_color'), ('min', 'low_color')]
opts.update({opt: colors[name] for name, opt in color_opts if name in colors})
else:
colormapper = CategoricalColorMapper
opts = dict(factors=factors)
if 'NaN' in colors:
opts['nan_color'] = colors['NaN']

if 'color_mapper' in self.handles:
cmapper = self.handles['color_mapper']
cmapper.palette = palette
cmapper.update(**opts)
else:
colormapper = LogColorMapper if self.logz else LinearColorMapper
cmapper = colormapper(palette, **opts)
cmapper = colormapper(palette=palette, **opts)
self.handles['color_mapper'] = cmapper
self.handles['color_dim'] = dim
return cmapper
Expand Down
4 changes: 3 additions & 1 deletion holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@ def rgb2hex(rgb):
return "#{0:02x}{1:02x}{2:02x}".format(*(int(v*255) for v in rgb))


def mplcmap_to_palette(cmap):
def mplcmap_to_palette(cmap, ncolors=None):
"""
Converts a matplotlib colormap to palette of RGB hex strings."
"""
if colors is None:
raise ValueError("Using cmaps on objects requires matplotlib.")
with abbreviated_exception():
colormap = cm.get_cmap(cmap) #choose any matplotlib colormap here
if ncolors:
return [rgb2hex(colormap(i)) for i in np.linspace(0, 1, ncolors)]
return [rgb2hex(m) for m in colormap(np.arange(colormap.N))]


Expand Down

0 comments on commit f233af1

Please sign in to comment.