From 719c9816d7dc70acf680376a7653d9f82892b302 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Tue, 12 Jun 2018 14:23:51 +0100
Subject: [PATCH 1/5] Added support for defining color intervals
---
holoviews/plotting/bokeh/element.py | 20 ++++++++++++++++----
holoviews/plotting/mpl/element.py | 23 +++++++++++++++++------
holoviews/plotting/util.py | 23 +++++++++++++++++++++++
3 files changed, 56 insertions(+), 10 deletions(-)
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index 1336c20574..87bd3cd429 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -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,
@@ -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.""")
@@ -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)
diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py
index 1986f62d2e..92618055f8 100644
--- a/holoviews/plotting/mpl/element.py
+++ b/holoviews/plotting/mpl/element.py
@@ -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
@@ -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
@@ -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:
@@ -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':
@@ -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'])
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index f6fdea2f25..5cf6fe4656 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -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.'
+ % (ncolors, len(cmap)))
+ intervals = np.diff(levels)
+ cmin, cmax = min(levels), max(levels)
+ interval = cmax-cmin
+ cmap = []
+ for intv, c in zip(intervals, colors):
+ cmap += [c]*int(N*(intv/interval))
+ if clip is not None:
+ clmin, clmax = clip
+ lidx = int(N*((clmin-cmin)/interval))
+ uidx = int(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.
From 150b8f31601c6e59249648d4b2090d17fcf63c6f Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Tue, 12 Jun 2018 14:24:28 +0100
Subject: [PATCH 2/5] Added docs section on defining custom color intervals
---
examples/user_guide/Styling_Plots.ipynb | 32 +++++++++++++++++++++++++
holoviews/plotting/util.py | 2 +-
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/examples/user_guide/Styling_Plots.ipynb b/examples/user_guide/Styling_Plots.ipynb
index cd70884314..b1a9d5cc61 100644
--- a/examples/user_guide/Styling_Plots.ipynb
+++ b/examples/user_guide/Styling_Plots.ipynb
@@ -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": {},
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 5cf6fe4656..82ba7eeed8 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -844,7 +844,7 @@ def color_intervals(colors, levels, clip=None, N=255):
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)))
+ % (N, len(colors)))
intervals = np.diff(levels)
cmin, cmax = min(levels), max(levels)
interval = cmax-cmin
From d0225ec7a5d41f97f4f95bdbd5c4efc35c44547e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 13 Jun 2018 13:50:13 +0100
Subject: [PATCH 3/5] Add tests for color intervals
---
tests/plotting/testplotutils.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/tests/plotting/testplotutils.py b/tests/plotting/testplotutils.py
index 1824a503a4..608e8ae371 100644
--- a/tests/plotting/testplotutils.py
+++ b/tests/plotting/testplotutils.py
@@ -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:
@@ -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):
From 63490dd1a8c1c4f7706fb6464f8c536bb59f24c3 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 13 Jun 2018 13:50:44 +0100
Subject: [PATCH 4/5] Minor fix for color intervals
---
holoviews/plotting/util.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 82ba7eeed8..6ba753606b 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -850,11 +850,11 @@ def color_intervals(colors, levels, clip=None, N=255):
interval = cmax-cmin
cmap = []
for intv, c in zip(intervals, colors):
- cmap += [c]*int(N*(intv/interval))
+ cmap += [c]*int(round(N*(intv/interval)))
if clip is not None:
clmin, clmax = clip
- lidx = int(N*((clmin-cmin)/interval))
- uidx = int(N*((cmax-clmax)/interval))
+ lidx = int(round(N*((clmin-cmin)/interval)))
+ uidx = int(round(N*((cmax-clmax)/interval)))
cmap = cmap[lidx:N-uidx]
return cmap
From f4b1c34ffd301d6dc55874f5b7857ec44010e74d Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 13 Jun 2018 14:23:06 +0100
Subject: [PATCH 5/5] Fixed division issues in plot utils
---
holoviews/plotting/util.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 6ba753606b..7dfee18e58 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals, absolute_import
+from __future__ import unicode_literals, absolute_import, division
from collections import defaultdict, namedtuple