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

Allow defining color intervals #2797

Merged
merged 5 commits into from
Jun 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions examples/user_guide/Styling_Plots.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,38 @@
"img.options(cmap='PiYG', color_levels=5) + img.options(cmap='PiYG', color_levels=11) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Custom color intervals\n",
"\n",
"In addition to a simple integer defining the number of discrete levels, the ``color_levels`` option also allows defining a set of custom intervals. This can be useful for defining a fixed scale, such as the Saffir-Simpson hurricane wind scale. Below we declare the color levels along with a list of colors, declaring the scale. Note that the levels define the intervals to map each color to, so if there are N colors we have to define N+1 levels.\n",
"\n",
"Having defined the scale we can generate a theoretical hurricane path with wind speed values and use the ``color_levels`` and ``cmap`` to supply the custom color scale:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"levels = [0, 38, 73, 95, 110, 130, 156, 999] \n",
"colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']\n",
"\n",
"path = [\n",
" (-75.1, 23.1, 0), (-76.2, 23.8, 0), (-76.9, 25.4, 0), (-78.4, 26.1, 39), (-79.6, 26.2, 39),\n",
" (-80.3, 25.9, 39), (-82.0, 25.1, 74), (-83.3, 24.6, 74), (-84.7, 24.4, 96), (-85.9, 24.8, 111),\n",
" (-87.7, 25.7, 111), (-89.2, 27.2, 131), (-89.6, 29.3, 156), (-89.6, 30.2, 156), (-89.1, 32.6, 131),\n",
" (-88.0, 35.6, 111), (-85.3, 38.6, 96)\n",
"]\n",
"\n",
"hv.Path([path], vdims='Wind Speed').options(\n",
" color_index='Wind Speed', color_levels=levels, cmap=colors, line_width=8, colorbar=True, width=450\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
20 changes: 16 additions & 4 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ...core import util
from ...streams import Buffer
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import dynamic_update, process_cmap
from ..util import dynamic_update, process_cmap, color_intervals
from .plot import BokehPlot, TOOLS
from .util import (mpl_to_bokeh, get_tab_title, py2js_tickformatter,
rgba_tuple, recursive_model_update, glyph_order,
Expand Down Expand Up @@ -986,8 +986,9 @@ class ColorbarPlot(ElementPlot):
'opts': {'location': 'bottom_right',
'orientation': 'horizontal'}}}

color_levels = param.Integer(default=None, doc="""
Number of discrete colors to use when colormapping.""")
color_levels = param.ClassSelector(default=None, class_=(int, list), doc="""
Number of discrete colors to use when colormapping or a set of color
intervals defining the range of values to map each color to.""")

colorbar = param.Boolean(default=False, doc="""
Whether to display a colorbar.""")
Expand Down Expand Up @@ -1079,7 +1080,18 @@ def _get_colormapper(self, dim, element, ranges, style, factors=None, colors=Non
if isinstance(cmap, dict) and factors:
palette = [cmap.get(f, nan_colors.get('NaN', self._default_nan)) for f in factors]
else:
palette = process_cmap(cmap, self.color_levels or ncolors, categorical=ncolors is not None)
if isinstance(self.color_levels, int):
ncolors = self.color_levels
elif isinstance(self.color_levels, list):
ncolors = len(self.color_levels) - 1
if isinstance(cmap, list) and len(cmap) != ncolors:
raise ValueError('The number of colors in the colormap '
'must match the intervals defined in the '
'color_levels, expected %d colors found %d.'
% (ncolors, len(cmap)))
palette = process_cmap(cmap, ncolors, categorical=ncolors is not None)
if isinstance(self.color_levels, list):
palette = color_intervals(palette, self.color_levels, clip=(low, high))
colormapper, opts = self._get_cmapper_opts(low, high, factors, nan_colors)

cmapper = self.handles.get(name)
Expand Down
23 changes: 17 additions & 6 deletions holoviews/plotting/mpl/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
CompositeOverlay, Element3D, Element)
from ...core.options import abbreviated_exception
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import dynamic_update, process_cmap
from ..util import dynamic_update, process_cmap, color_intervals
from .plot import MPLPlot, mpl_rc_context
from .util import wrap_formatter
from distutils.version import LooseVersion
Expand Down Expand Up @@ -479,8 +479,9 @@ class ColorbarPlot(ElementPlot):
colorbar = param.Boolean(default=False, doc="""
Whether to draw a colorbar.""")

color_levels = param.Integer(default=None, doc="""
Number of discrete colors to use when colormapping.""")
color_levels = param.ClassSelector(default=None, class_=(int, list), doc="""
Number of discrete colors to use when colormapping or a set of color
intervals defining the range of values to map each color to.""")

clipping_colors = param.Dict(default={}, doc="""
Dictionary to specify colors for clipped values, allows
Expand Down Expand Up @@ -629,9 +630,18 @@ def _norm_kwargs(self, element, ranges, opts, vdim, prefix=''):
opts[prefix+'vmin'] = clim[0]
opts[prefix+'vmax'] = clim[1]

# Check whether the colorbar should indicate clipping
cmap = opts.get(prefix+'cmap', 'viridis')
if values.dtype.kind not in 'OSUM':
ncolors = self.color_levels
ncolors = None
if isinstance(self.color_levels, int):
ncolors = self.color_levels
elif isinstance(self.color_levels, list):
ncolors = len(self.color_levels) - 1
if isinstance(cmap, list) and len(cmap) != ncolors:
raise ValueError('The number of colors in the colormap '
'must match the intervals defined in the '
'color_levels, expected %d colors found %d.'
% (ncolors, len(cmap)))
try:
el_min, el_max = np.nanmin(values), np.nanmax(values)
except ValueError:
Expand All @@ -649,7 +659,6 @@ def _norm_kwargs(self, element, ranges, opts, vdim, prefix=''):
self._cbar_extend = 'max'

# Define special out-of-range colors on colormap
cmap = opts.get(prefix+'cmap', 'viridis')
colors = {}
for k, val in self.clipping_colors.items():
if val == 'transparent':
Expand All @@ -672,6 +681,8 @@ def _norm_kwargs(self, element, ranges, opts, vdim, prefix=''):
for f in factors]
else:
palette = process_cmap(cmap, ncolors, categorical=categorical)
if isinstance(self.color_levels, list):
palette = color_intervals(palette, self.color_levels, clip=(vmin, vmax))
cmap = mpl_colors.ListedColormap(palette)
if 'max' in colors: cmap.set_over(**colors['max'])
if 'min' in colors: cmap.set_under(**colors['min'])
Expand Down
25 changes: 24 additions & 1 deletion holoviews/plotting/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import unicode_literals, absolute_import
from __future__ import unicode_literals, absolute_import, division

from collections import defaultdict, namedtuple

Expand Down Expand Up @@ -836,6 +836,29 @@ def process_cmap(cmap, ncolors=None, provider=None, categorical=False):
return palette


def color_intervals(colors, levels, clip=None, N=255):
"""
Maps a set of intervals to colors given a fixed color range.
"""
if len(colors) != len(levels)-1:
raise ValueError('The number of colors in the colormap '
'must match the intervals defined in the '
'color_levels, expected %d colors found %d.'
% (N, len(colors)))
intervals = np.diff(levels)
cmin, cmax = min(levels), max(levels)
interval = cmax-cmin
cmap = []
for intv, c in zip(intervals, colors):
cmap += [c]*int(round(N*(intv/interval)))
if clip is not None:
clmin, clmax = clip
lidx = int(round(N*((clmin-cmin)/interval)))
uidx = int(round(N*((cmax-clmax)/interval)))
cmap = cmap[lidx:N-uidx]
return cmap


def dim_axis_label(dimensions, separator=', '):
"""
Returns an axis label for one or more dimensions.
Expand Down
16 changes: 15 additions & 1 deletion tests/plotting/testplotutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from holoviews.plotting.util import (
compute_overlayable_zorders, get_min_distance, process_cmap,
initialize_dynamic, split_dmap_overlay, _get_min_distance_numpy,
bokeh_palette_to_palette, mplcmap_to_palette)
bokeh_palette_to_palette, mplcmap_to_palette, color_intervals)
from holoviews.streams import PointerX

try:
Expand Down Expand Up @@ -565,6 +565,20 @@ def test_bokeh_palette_perceptually_uniform_reverse(self):
colors = bokeh_palette_to_palette('viridis_r', 4)
self.assertEqual(colors, ['#440154', '#30678D', '#35B778', '#FDE724'][::-1])

def test_color_intervals(self):
levels = [0, 38, 73, 95, 110, 130, 156]
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20']
cmap = color_intervals(colors, levels, N=10)
self.assertEqual(cmap, ['#5ebaff', '#5ebaff', '#00faf4',
'#00faf4', '#ffffcc', '#ffe775',
'#ffc140', '#ff8f20', '#ff8f20'])

def test_color_intervals_clipped(self):
levels = [0, 38, 73, 95, 110, 130, 156, 999]
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']
cmap = color_intervals(colors, levels, clip=(10, 90), N=100)
self.assertEqual(cmap, ['#5ebaff', '#5ebaff', '#5ebaff', '#00faf4', '#00faf4',
'#00faf4', '#00faf4', '#ffffcc'])


class TestPlotUtils(ComparisonTestCase):
Expand Down